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).
### 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*.
@@ -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:
```python title="widgets02.py"
```python title="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.
```python title="question01.py"
```python title="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:
```sass title="question02.css"
```sass title="question02.css"
--8<-- "docs/examples/app/question02.css"
```

View File

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

View File

@@ -1,12 +1,27 @@
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"),
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")
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")

View File

@@ -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)

View File

@@ -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
@@ -59,19 +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 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
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
def __rich_console__(
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):
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}"
)