From 0332f637ab1bb86050f4ad1c8358217c47cd2865 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 14 Sep 2022 17:40:26 +0100 Subject: [PATCH 1/6] text-opacity, dont render text when 0 --- sandbox/darren/just_a_box.css | 2 ++ sandbox/darren/just_a_box.py | 12 +++++++++++- src/textual/renderables/text_opacity.py | 14 ++++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/sandbox/darren/just_a_box.css b/sandbox/darren/just_a_box.css index c5317c9ef..57c79e95f 100644 --- a/sandbox/darren/just_a_box.css +++ b/sandbox/darren/just_a_box.css @@ -1,8 +1,10 @@ Screen { + layout: center; background: darkslategrey; } .box1 { background: darkmagenta; width: auto; + padding: 4 8; } diff --git a/sandbox/darren/just_a_box.py b/sandbox/darren/just_a_box.py index 52f87ffc4..c3287c89d 100644 --- a/sandbox/darren/just_a_box.py +++ b/sandbox/darren/just_a_box.py @@ -1,12 +1,22 @@ from __future__ import annotations from textual.app import App, ComposeResult -from textual.widgets import Static +from textual.binding import Binding +from textual.widgets import Static, Footer class JustABox(App): + BINDINGS = [ + Binding(key="t", action="text_fade_out", description="text-opacity fade out") + ] + def compose(self) -> ComposeResult: yield Static("Hello, world!", classes="box1") + yield Footer() + + def action_text_fade_out(self) -> None: + box = self.query_one(".box1") + self.animator.animate(box.styles, "text_opacity", value=0.0, duration=1) app = JustABox(watch_css=True, css_path="../darren/just_a_box.css") diff --git a/src/textual/renderables/text_opacity.py b/src/textual/renderables/text_opacity.py index d05c944b4..cc6845d9e 100644 --- a/src/textual/renderables/text_opacity.py +++ b/src/textual/renderables/text_opacity.py @@ -1,6 +1,7 @@ import functools from typing import Iterable +from rich.cells import cell_len from rich.color import Color from rich.console import ConsoleOptions, Console, RenderResult, RenderableType from rich.segment import Segment @@ -67,11 +68,16 @@ class TextOpacity: color = style.color bgcolor = style.bgcolor - if color and color.triplet and bgcolor and bgcolor.triplet: - color_style = _get_blended_style_cached(bgcolor, color, opacity) - yield _Segment(text, style + color_style) + + if opacity > 0: + if color and color.triplet and bgcolor and bgcolor.triplet: + color_style = _get_blended_style_cached(bgcolor, color, opacity) + yield _Segment(text, style + color_style) + else: + yield segment else: - yield segment + empty_text = cell_len(text) * " " + yield _Segment(empty_text, Style.from_color(bgcolor=bgcolor)) def __rich_console__( self, console: Console, options: ConsoleOptions From 4668b4a0d8c7b8e0c955e181078982ad8e2e0891 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 14 Sep 2022 18:00:32 +0100 Subject: [PATCH 2/6] Dont render widgets with zero opacity --- sandbox/darren/just_a_box.css | 2 +- sandbox/darren/just_a_box.py | 11 ++++++++--- src/textual/_compositor.py | 13 +++++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/sandbox/darren/just_a_box.css b/sandbox/darren/just_a_box.css index 57c79e95f..9f9d7da7e 100644 --- a/sandbox/darren/just_a_box.css +++ b/sandbox/darren/just_a_box.css @@ -3,7 +3,7 @@ Screen { background: darkslategrey; } -.box1 { +#box1 { background: darkmagenta; width: auto; padding: 4 8; diff --git a/sandbox/darren/just_a_box.py b/sandbox/darren/just_a_box.py index c3287c89d..080157a52 100644 --- a/sandbox/darren/just_a_box.py +++ b/sandbox/darren/just_a_box.py @@ -7,17 +7,22 @@ from textual.widgets import Static, Footer class JustABox(App): BINDINGS = [ - Binding(key="t", action="text_fade_out", description="text-opacity fade out") + Binding(key="t", action="text_fade_out", description="text-opacity fade out"), + Binding(key="o", action="widget_fade_out", description="opacity fade out"), ] def compose(self) -> ComposeResult: - yield Static("Hello, world!", classes="box1") + yield Static("Hello, world!", id="box1") yield Footer() def action_text_fade_out(self) -> None: - box = self.query_one(".box1") + box = self.query_one("#box1") self.animator.animate(box.styles, "text_opacity", value=0.0, duration=1) + def action_widget_fade_out(self) -> None: + box = self.query_one("#box1") + self.animator.animate(box.styles, "opacity", value=0.0, duration=1) + app = JustABox(watch_css=True, css_path="../darren/just_a_box.css") diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 5632f50b5..074154056 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -604,19 +604,28 @@ class Compositor: # up to this point. _rich_traceback_guard = True + def is_visible(widget: Widget) -> bool: + """Return True if the widget is (literally) visible by examining various + properties which affect whether it can be seen or not.""" + return ( + widget.visible + and not widget.is_transparent + and widget.styles.opacity > 0 + ) + if self.map: if crop: overlaps = crop.overlaps mapped_regions = [ (widget, region, order, clip) for widget, (region, order, clip, *_) in self.map.items() - if widget.visible and not widget.is_transparent and overlaps(crop) + if is_visible(widget) and overlaps(crop) ] else: mapped_regions = [ (widget, region, order, clip) for widget, (region, order, clip, *_) in self.map.items() - if widget.visible and not widget.is_transparent + if is_visible(widget) ] widget_regions = sorted(mapped_regions, key=itemgetter(2), reverse=True) From d632091d93c6f3b942f0b70799f474c91061ab58 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Wed, 14 Sep 2022 18:02:48 +0100 Subject: [PATCH 3/6] Test ensuring text isnt rendered when text-opacity 0 --- tests/renderables/test_text_opacity.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/renderables/test_text_opacity.py b/tests/renderables/test_text_opacity.py index 04f8aaca6..b543667ba 100644 --- a/tests/renderables/test_text_opacity.py +++ b/tests/renderables/test_text_opacity.py @@ -19,10 +19,9 @@ def test_simple_text_opacity(text): ) -def test_value_zero_sets_foreground_color_to_background_color(text): - foreground = background = "0;255;0" +def test_value_zero_doesnt_render_the_text(text): assert render(TextOpacity(text, opacity=0)) == ( - f"\x1b[38;2;{foreground};48;2;{background}mHello, world!{STOP}" + f"\x1b[48;2;0;255;0m {STOP}" ) From 0762fff6bfb633d7b7268514114a39a3a33cd7cd Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 15 Sep 2022 12:19:05 +0100 Subject: [PATCH 4/6] Inline some properties, pull condition outside loop --- docs/guide/app.md | 10 +++++----- src/textual/_compositor.py | 5 +++-- src/textual/renderables/text_opacity.py | 25 +++++++++++++------------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/docs/guide/app.md b/docs/guide/app.md index 8e924dc2b..9a807bea7 100644 --- a/docs/guide/app.md +++ b/docs/guide/app.md @@ -41,7 +41,7 @@ One such event is the *mount* event which is sent to an application after it ent !!! info - You may have noticed we use the term "send" and "sent" in relation to event handler methods in preference to "calling". This is because Textual uses a message passing system where events are passed (or *sent*) between components. We will cover the details in [events][./events.md]. + You may have noticed we use the term "send" and "sent" in relation to event handler methods in preference to "calling". This is because Textual uses a message passing system where events are passed (or *sent*) between components. We will cover the details in [events](./events.md). Another such event is the *key* event which is sent when the user presses a key. The following example contains handlers for both those events: @@ -80,7 +80,7 @@ Widgets are self-contained components responsible for generating the output for Widgets can be as simple as a piece of text, a button, or a fully-fledge component like a text editor or file browser (which may contain widgets of their own). -### Composing +### Composing To add widgets to your app implement a [`compose()`][textual.app.App.compose] method which should return a iterable of Widget instances. A list would work, but it is convenient to yield widgets, making the method a *generator*. @@ -103,7 +103,7 @@ While composing is the preferred way of adding widgets when your app starts it i Here's an app which adds the welcome widget in response to any key press: -```python title="widgets02.py" +```python title="widgets02.py" --8<-- "docs/examples/app/widgets02.py" ``` @@ -118,7 +118,7 @@ An app will run until you call [App.exit()][textual.app.App.exit] which will exi The exit method will also accept an optional positional value to be returned by `run()`. The following example uses this to return the `id` (identifier) of a clicked button. -```python title="question01.py" +```python title="question01.py" --8<-- "docs/examples/app/question01.py" ``` @@ -159,7 +159,7 @@ The following example sets the `css_path` attribute on the app: If the path is relative (as it is above) then it is taken as relative to where the app is defined. Hence this example references `"question01.css"` in the same directory as the Python code. Here is that CSS file: -```sass title="question02.css" +```sass title="question02.css" --8<-- "docs/examples/app/question02.css" ``` diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 074154056..fc294f490 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -608,8 +608,9 @@ class Compositor: """Return True if the widget is (literally) visible by examining various properties which affect whether it can be seen or not.""" return ( - widget.visible - and not widget.is_transparent + widget.styles.visibility != "hidden" + and not widget.is_scrollable + and widget.styles.background.is_transparent and widget.styles.opacity > 0 ) diff --git a/src/textual/renderables/text_opacity.py b/src/textual/renderables/text_opacity.py index cc6845d9e..dffb5667b 100644 --- a/src/textual/renderables/text_opacity.py +++ b/src/textual/renderables/text_opacity.py @@ -60,24 +60,25 @@ class TextOpacity: """ _Segment = Segment - for segment in segments: - text, style, control = segment - if not style: - yield segment - continue + _from_color = Style.from_color + if opacity == 0: + for text, style, control in segments: + invisible_style = _from_color(bgcolor=style.bgcolor) + yield _Segment(cell_len(text) * " ", invisible_style) + else: + for segment in segments: + text, style, control = segment + if not style: + yield segment + continue - color = style.color - bgcolor = style.bgcolor - - if opacity > 0: + color = style.color + bgcolor = style.bgcolor if color and color.triplet and bgcolor and bgcolor.triplet: color_style = _get_blended_style_cached(bgcolor, color, opacity) yield _Segment(text, style + color_style) else: yield segment - else: - empty_text = cell_len(text) * " " - yield _Segment(empty_text, Style.from_color(bgcolor=bgcolor)) def __rich_console__( self, console: Console, options: ConsoleOptions From 3dd83d3df21fe48b7ce1d6ec21b143378032d07e Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 15 Sep 2022 13:31:20 +0100 Subject: [PATCH 5/6] Small changes to _get_renders --- src/textual/_compositor.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index fc294f490..67a9d7576 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -607,12 +607,7 @@ class Compositor: def is_visible(widget: Widget) -> bool: """Return True if the widget is (literally) visible by examining various properties which affect whether it can be seen or not.""" - return ( - widget.styles.visibility != "hidden" - and not widget.is_scrollable - and widget.styles.background.is_transparent - and widget.styles.opacity > 0 - ) + return widget.styles.visibility != "hidden" and widget.styles.opacity > 0 if self.map: if crop: From c3eb54319be2928a38ade9ff3551ae5bf3ab3816 Mon Sep 17 00:00:00 2001 From: Darren Burns Date: Thu, 15 Sep 2022 14:12:42 +0100 Subject: [PATCH 6/6] Undo inlining --- src/textual/_compositor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/textual/_compositor.py b/src/textual/_compositor.py index 67a9d7576..074154056 100644 --- a/src/textual/_compositor.py +++ b/src/textual/_compositor.py @@ -607,7 +607,11 @@ class Compositor: def is_visible(widget: Widget) -> bool: """Return True if the widget is (literally) visible by examining various properties which affect whether it can be seen or not.""" - return widget.styles.visibility != "hidden" and widget.styles.opacity > 0 + return ( + widget.visible + and not widget.is_transparent + and widget.styles.opacity > 0 + ) if self.map: if crop: