Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Opacity/transparency issues with overlapped widgets #3676

Open
rodrigogiraoserrao opened this issue Nov 14, 2023 · 4 comments
Open

Opacity/transparency issues with overlapped widgets #3676

rodrigogiraoserrao opened this issue Nov 14, 2023 · 4 comments
Labels
bug Something isn't working

Comments

@rodrigogiraoserrao
Copy link
Contributor

rodrigogiraoserrao commented Nov 14, 2023

This change [to how opacity works] might have inadvertently introduced a bug for layers with opacity. Here's a simple example adapted from the docs for Layers, with a binding that changes the opacity of #box1.

Before v0.29.0

before-screenshot

After v0.29.0

after-screenshot

Excerpt of a comment posted by @TomJGooding in #3652 (comment)

@rodrigogiraoserrao
Copy link
Contributor Author

The examples below show situations in which widget overlaps don't take transparencies into account correctly.

Base app:

from textual.app import App
from textual.widgets import Label


class BGApp(App):
    CSS_PATH = "bg.tcss"
    def compose(self):
        yield Label("below", id="below")
        yield Label("above", id="above")


if __name__ == "__main__":
    BGApp().run()
  1. Overlapping widgets because of layers and the top one has a transparent background:
Screen {
    layers: below above;
}

#below {
    width: 30;
    height: 5;
    layer: below;
    background: blue;
}

#above {
    width: 20;
    height: 3;
    layer: above;
    background: red 0%;
}
Screenshot 2023-11-14 at 14 29 26
  1. Overlapping widgets because of layers and opacity: 0% set on the top one:
Screen {
    layers: below above;
}

#below {
    layer: below;
    width: 30;
    height: 5;
    background: blue;
}

#above {
    layer: above;
    width: 20;
    height: 3;
    background: red;
    opacity: 0%;
    # background: red 0%;
}
Screenshot 2023-11-14 at 14 46 53
  1. Overlapping widgets because of offsets and the top one has opacity: 0%:
#below {
    width: 30;
    height: 5;
    background: blue;
}

#above {
    offset: 0 -3;
    width: 20;
    height: 3;
    background: red;
    opacity: 0%;
}
Screenshot 2023-11-14 at 14 49 22
  1. Overlapping widgets because of offsets and top one has transparent background:
#below {
    width: 30;
    height: 5;
    background: blue;
}

#above {
    offset: 0 -3;
    width: 20;
    height: 3;
    background: red 0%;
}
Screenshot 2023-11-14 at 14 50 05

@rodrigogiraoserrao rodrigogiraoserrao removed their assignment Nov 15, 2023
@TomJGooding
Copy link
Contributor

TomJGooding commented Mar 14, 2024

Thanks @rodrigogiraoserrao for checking all these scenarios.

After looking at this again , it seems only the opacity style worked before v0.29, whereas the background percentage has possibly never been taken into account.

This probably isn't helpful given the significant changes in #2814, but making a note just in case.

EDIT: Digging deeper, actually only opacity: 0% worked before v0.29. For example background: white; opacity: 50%; would show as grey, rather than a light blue as you might expect.

@darrenburns
Copy link
Member

Confirmed this is still broken on main as of 17th July 2024 (commit hash: 0d25607).

MRE without external CSS file:

from textual.app import App
from textual.widgets import Label


class BGApp(App):
    CSS = """\
Screen {
    layers: below above;
}

#below {
    width: 30;
    height: 5;
    layer: below;
    background: blue;
}

#above {
    width: 20;
    height: 3;
    layer: above;
    background: red 0%;
}
"""

    def compose(self):
        yield Label("below", id="below")
        yield Label("above", id="above")


if __name__ == "__main__":
    BGApp().run()

@dfrtz
Copy link

dfrtz commented Oct 13, 2024

I believe this is impacting dialogs being able to have transparency in portions of their bodies as well.

I attempted to dig into this a bit, in order to try to narrow it down and help prevent similar issues with transparent portions of dialogs only showing black. Having 0 experience with the backend rendering system in Textual, it led to a lot of questions as I poked around, so I couldn't quite pinpoint it. What I did find however is that is seems to be related to either:

  • textual._styles_cache.StylesCache.render_widget
  • Or the other render* methods in there.
  • But it might be "chops" related.

I attempted a lot of fairly hacking poking trying to figure it out, such as creating a custom Compositor for Screens. The modification to _arrange_root may not be needed, but I think a long term solution might, since without it the visible widgets in the screen do not contain the items in the background. Maybe BackgroundScreen is suppose to prevent the need for this, but I am not sure that widgets in a screen/layer have visibility to the styles of widgets behind them without it. If I skipped the "render_lines" call for base widgets in screens, then I did see the expected widgets behind rendering as expected (so I at least can "confirm" they are not cropping out portions).

class Compositor2(Compositor):
    def _arrange_root(self, root, size, visible_only=True):
        map = {}
        widgets = set()
        # This does prevent the background screen from working, as it appears to render on top of it, but I think it might be necessary.
        # for screen in root.app._background_screens:
        #     new_map, new_widgets = super()._arrange_root(screen._compositor.root, size, visible_only)
        #     map |= new_map
        #     widgets.update(new_widgets)
        new_map, new_widgets = super()._arrange_root(root, size, visible_only)
        map |= new_map
        widgets.update(new_widgets)
        return map, widgets

    def _get_renders(self, crop=None):
        _rich_traceback_guard = True
        _Region = Region
        visible_widgets = self.visible_widgets

        if crop:
            crop_overlaps = crop.overlaps
            widget_regions = [
                (widget, region, clip)
                for widget, (region, clip) in visible_widgets.items()
                if crop_overlaps(clip)
            ]
        else:
            widget_regions = [
                (widget, region, clip)
                for widget, (region, clip) in visible_widgets.items()
            ]

        for widget, region, clip in widget_regions:
            if 'dialog-base' in widget.classes or 'dialog-body' in widget.classes:
                # This was the trick to at least see that widgets in the back are at least rendering full bodies, before being overwritten.
                print(widget, region, widget.styles.background)
                continue
            yield (
                region,
                clip,
                widget.render_lines(
                    _Region(
                        0,
                        0,
                        region.width,
                        region.height,
                    )
                ),
            )

I also checked the styles to ensure they show alpha values correctly, but it appears they just can't seem to pull what the values/lines are from behind them when transparent. Maybe this has more to do with how it is creating chops though in Compositors, but I can't be certain since this was my first deep dive into the rendering logic.

Hoping this might be able to at least help speed up future investigations. Not sure when/if I will have time to dig into it again to try to provide a more concrete cause.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

4 participants