Merge pull request #774 from Textualize/dont-render-zero-opacity-text

Minimise rendering when text-opacity/opacity is zero
This commit is contained in:
Will McGugan
2022-09-20 10:50:53 +01:00
committed by GitHub
6 changed files with 56 additions and 24 deletions

View File

@@ -84,7 +84,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). 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*. 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*.
@@ -107,7 +107,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: 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" --8<-- "docs/examples/app/widgets02.py"
``` ```
@@ -122,7 +122,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. 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" --8<-- "docs/examples/app/question01.py"
``` ```
@@ -161,7 +161,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: 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" --8<-- "docs/examples/app/question02.css"
``` ```

View File

@@ -1,8 +1,10 @@
Screen { Screen {
layout: center;
background: darkslategrey; background: darkslategrey;
} }
.box1 { #box1 {
background: darkmagenta; background: darkmagenta;
width: auto; width: auto;
padding: 4 8;
} }

View File

@@ -1,12 +1,27 @@
from __future__ import annotations from __future__ import annotations
from textual.app import App, ComposeResult 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): class JustABox(App):
BINDINGS = [
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: 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")
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") app = JustABox(watch_css=True, css_path="../darren/just_a_box.css")

View File

@@ -604,19 +604,28 @@ class Compositor:
# up to this point. # up to this point.
_rich_traceback_guard = True _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 self.map:
if crop: if crop:
overlaps = crop.overlaps overlaps = crop.overlaps
mapped_regions = [ mapped_regions = [
(widget, region, order, clip) (widget, region, order, clip)
for widget, (region, order, clip, *_) in self.map.items() 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: else:
mapped_regions = [ mapped_regions = [
(widget, region, order, clip) (widget, region, order, clip)
for widget, (region, order, clip, *_) in self.map.items() 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) widget_regions = sorted(mapped_regions, key=itemgetter(2), reverse=True)

View File

@@ -1,6 +1,7 @@
import functools import functools
from typing import Iterable from typing import Iterable
from rich.cells import cell_len
from rich.color import Color from rich.color import Color
from rich.console import ConsoleOptions, Console, RenderResult, RenderableType from rich.console import ConsoleOptions, Console, RenderResult, RenderableType
from rich.segment import Segment from rich.segment import Segment
@@ -59,19 +60,25 @@ class TextOpacity:
""" """
_Segment = Segment _Segment = Segment
for segment in segments: _from_color = Style.from_color
text, style, control = segment if opacity == 0:
if not style: for text, style, control in segments:
yield segment invisible_style = _from_color(bgcolor=style.bgcolor)
continue 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 color = style.color
bgcolor = style.bgcolor bgcolor = style.bgcolor
if color and color.triplet and bgcolor and bgcolor.triplet: if color and color.triplet and bgcolor and bgcolor.triplet:
color_style = _get_blended_style_cached(bgcolor, color, opacity) color_style = _get_blended_style_cached(bgcolor, color, opacity)
yield _Segment(text, style + color_style) yield _Segment(text, style + color_style)
else: else:
yield segment yield segment
def __rich_console__( def __rich_console__(
self, console: Console, options: ConsoleOptions self, console: Console, options: ConsoleOptions

View File

@@ -19,10 +19,9 @@ def test_simple_text_opacity(text):
) )
def test_value_zero_sets_foreground_color_to_background_color(text): def test_value_zero_doesnt_render_the_text(text):
foreground = background = "0;255;0"
assert render(TextOpacity(text, opacity=0)) == ( 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}"
) )