Merge branch 'css' of github.com:Textualize/textual into easing-examples

This commit is contained in:
Darren Burns
2022-09-02 10:55:46 +01:00
32 changed files with 410 additions and 181 deletions

View File

@@ -4,18 +4,18 @@
* {
transition: color 300ms linear, background 300ms linear;
}
}
*:hover {
/* tint: 30% red;
/* tint: 30% red;
/* outline: heavy red; */
}
App > Screen {
background: $surface;
color: $text-surface;
color: $text-surface;
layers: base sidebar;
color: $text-background;
@@ -23,12 +23,12 @@ App > Screen {
layout: vertical;
overflow: hidden;
}
#tree-container {
overflow-y: auto;
height: 20;
height: 20;
margin: 1 3;
background: $panel;
padding: 1 2;
@@ -37,7 +37,7 @@ App > Screen {
DirectoryTree {
padding: 0 1;
height: auto;
}
@@ -46,10 +46,10 @@ DirectoryTree {
DataTable {
/*border:heavy red;*/
/* tint: 10% green; */
/* opacity: 50%; */
/* text-opacity: 50%; */
padding: 1;
margin: 1 2;
height: 24;
margin: 1 2;
height: 24;
}
#sidebar {
@@ -59,7 +59,7 @@ DataTable {
width: 30;
margin-bottom: 1;
offset-x: -100%;
transition: offset 500ms in_out_cubic;
layer: sidebar;
}
@@ -97,8 +97,8 @@ DataTable {
Tweet {
height:12;
width: 100%;
margin: 0 2;
margin: 0 2;
background: $panel;
color: $text-panel;
layout: vertical;
@@ -121,9 +121,9 @@ Tweet {
layout: vertical;
}
.code {
.code {
height: auto;
}
@@ -133,12 +133,12 @@ TweetHeader {
color: $text-accent
}
TweetBody {
TweetBody {
width: 100%;
background: $panel;
color: $text-panel;
height: auto;
padding: 0 1 0 0;
height: auto;
padding: 0 1 0 0;
}
Tweet.scroll-horizontal TweetBody {
@@ -158,7 +158,7 @@ Tweet.scroll-horizontal TweetBody {
/* padding: 1 0 0 0 ; */
transition: background 400ms in_out_cubic, color 400ms in_out_cubic;
}
.button:hover {
@@ -178,7 +178,7 @@ Tweet.scroll-horizontal TweetBody {
color: $text-accent;
background: $accent;
height: 1;
content-align: center middle;
dock:bottom;
}
@@ -213,7 +213,7 @@ Error {
color: $text-error;
border-top: tall $error-darken-2;
border-bottom: tall $error-darken-2;
padding: 0;
text-style: bold;
align-horizontal: center;
@@ -226,21 +226,21 @@ Warning {
color: $text-warning-fade-1;
border-top: tall $warning-darken-2;
border-bottom: tall $warning-darken-2;
text-style: bold;
align-horizontal: center;
}
Success {
width: 100%;
height:auto;
height:auto;
box-sizing: border-box;
background: $success;
color: $text-success-fade-1;
color: $text-success-fade-1;
border-top: hkey $success-darken-2;
border-bottom: hkey $success-darken-2;
border-bottom: hkey $success-darken-2;
text-style: bold ;

View File

@@ -9,12 +9,12 @@ Stopwatch {
TimeDisplay {
content-align: center middle;
opacity: 60%;
text-opacity: 60%;
height: 3;
}
Button {
width: 16;
width: 16;
}
#start {
@@ -30,14 +30,14 @@ Button {
dock: right;
}
.started {
.started {
text-style: bold;
background: $success;
color: $text-success;
}
.started TimeDisplay {
opacity: 100%;
text-opacity: 100%;
}
.started #start {

View File

@@ -8,12 +8,12 @@ Stopwatch {
TimeDisplay {
content-align: center middle;
opacity: 60%;
text-opacity: 60%;
height: 3;
}
Button {
width: 16;
width: 16;
}
#start {

View File

@@ -9,12 +9,12 @@ Stopwatch {
TimeDisplay {
content-align: center middle;
opacity: 60%;
text-opacity: 60%;
height: 3;
}
Button {
width: 16;
width: 16;
}
#start {
@@ -30,14 +30,14 @@ Button {
dock: right;
}
.started {
.started {
text-style: bold;
background: $success;
color: $text-success;
}
.started TimeDisplay {
opacity: 100%;
text-opacity: 100%;
}
.started #start {

View File

@@ -0,0 +1,31 @@
#zero-opacity {
opacity: 0%;
}
#quarter-opacity {
opacity: 25%;
}
#half-opacity {
opacity: 50%;
}
#three-quarter-opacity {
opacity: 75%;
}
#full-opacity {
opacity: 100%;
}
Screen {
background: antiquewhite;
}
Static {
height: 1fr;
border: outer dodgerblue;
background: lightseagreen;
content-align: center middle;
text-style: bold;
}

View File

@@ -0,0 +1,14 @@
from textual.app import App
from textual.widgets import Static
class OpacityApp(App):
def compose(self):
yield Static("opacity: 0%", id="zero-opacity")
yield Static("opacity: 25%", id="quarter-opacity")
yield Static("opacity: 50%", id="half-opacity")
yield Static("opacity: 75%", id="three-quarter-opacity")
yield Static("opacity: 100%", id="full-opacity")
app = OpacityApp(css_path="opacity.css")

View File

@@ -0,0 +1,25 @@
#zero-opacity {
text-opacity: 0%;
}
#quarter-opacity {
text-opacity: 25%;
}
#half-opacity {
text-opacity: 50%;
}
#three-quarter-opacity {
text-opacity: 75%;
}
#full-opacity {
text-opacity: 100%;
}
Static {
height: 1fr;
text-align: center;
text-style: bold;
}

View File

@@ -0,0 +1,14 @@
from textual.app import App
from textual.widgets import Static
class TextOpacityApp(App):
def compose(self):
yield Static("text-opacity: 0%", id="zero-opacity")
yield Static("text-opacity: 25%", id="quarter-opacity")
yield Static("text-opacity: 50%", id="half-opacity")
yield Static("text-opacity: 75%", id="three-quarter-opacity")
yield Static("text-opacity: 100%", id="full-opacity")
app = TextOpacityApp(css_path="text_opacity.css")

54
docs/styles/opacity.md Normal file
View File

@@ -0,0 +1,54 @@
# Opacity
The `opacity` property can be used to make a widget partially or fully transparent.
## Syntax
```
opacity: <FRACTIONAL>;
```
### Values
As a fractional property, `opacity` can be set to either a float (between 0 and 1),
or a percentage, e.g. `45%`.
Float values will be clamped between 0 and 1.
Percentage values will be clamped between 0% and 100%.
## Example
This example shows, from top to bottom, increasing opacity values.
=== "opacity.py"
```python
--8<-- "docs/examples/styles/opacity.py"
```
=== "opacity.css"
```scss
--8<-- "docs/examples/styles/opacity.css"
```
=== "Output"
```{.textual path="docs/examples/styles/opacity.py"}
```
## CSS
```sass
/* Fade the widget to 50% against its parent's background */
Widget {
opacity: 50%;
}
```
## Python
```python
# Fade the widget to 50% against its parent's background
widget.styles.opacity = "50%"
```

View File

@@ -0,0 +1,53 @@
# Text-opacity
The `text-opacity` blends the color of the content of a widget with the color of the background.
## Syntax
```
text-opacity: <FRACTIONAL>;
```
### Values
As a fractional property, `text-opacity` can be set to either a float (between 0 and 1),
or a percentage, e.g. `45%`.
Float values will be clamped between 0 and 1.
Percentage values will be clamped between 0% and 100%.
## Example
This example shows, from top to bottom, increasing text-opacity values.
=== "text_opacity.py"
```python
--8<-- "docs/examples/styles/text_opacity.py"
```
=== "text_opacity.css"
```css
--8<-- "docs/examples/styles/text_opacity.css"
```
=== "Output"
```{.textual path="docs/examples/styles/text_opacity.py"}
```
## CSS
```sass
/* Set the text to be "half-faded" against the background of the widget */
Widget {
text-opacity: 50%;
}
```
## Python
```python
# Set the text to be "half-faded" against the background of the widget
widget.styles.text_opacity = "50%"
```

View File

@@ -4,33 +4,33 @@
* {
transition: color 300ms linear, background 300ms linear;
}
}
*:hover {
/* tint: 30% red;
/* tint: 30% red;
/* outline: heavy red; */
}
App > Screen {
background: $surface;
color: $text-surface;
color: $text-surface;
layers: sidebar;
color: $text-background;
background: $background;
layout: vertical;
}
DataTable {
/*border:heavy red;*/
/* tint: 10% green; */
/* opacity: 50%; */
/* text-opacity: 50%; */
padding: 1;
margin: 1 2;
height: 12;
margin: 1 2;
height: 12;
}
#sidebar {
@@ -39,7 +39,7 @@ DataTable {
dock: left;
width: 30;
offset-x: -100%;
transition: offset 500ms in_out_cubic;
layer: sidebar;
}
@@ -76,7 +76,7 @@ DataTable {
background: $secondary-background;
height: 1;
content-align: center middle;
dock: top;
}
@@ -84,8 +84,8 @@ DataTable {
Tweet {
height:12;
width: 100%;
background: $panel;
color: $text-panel;
layout: vertical;
@@ -100,7 +100,7 @@ Tweet {
.scrollable {
overflow-y: scroll;
margin: 1 2;
height: 20;
@@ -108,9 +108,9 @@ Tweet {
layout: vertical;
}
.code {
.code {
height: auto;
}
@@ -120,12 +120,12 @@ TweetHeader {
color: $text-accent
}
TweetBody {
TweetBody {
width: 100%;
background: $panel;
color: $text-panel;
height: auto;
padding: 0 1 0 0;
height: auto;
padding: 0 1 0 0;
}
Tweet.scroll-horizontal TweetBody {
@@ -145,7 +145,7 @@ Tweet.scroll-horizontal TweetBody {
/* padding: 1 0 0 0 ; */
transition: background 400ms in_out_cubic, color 400ms in_out_cubic;
}
.button:hover {
@@ -165,7 +165,7 @@ Tweet.scroll-horizontal TweetBody {
color: $text-accent;
background: $accent;
height: 1;
content-align: center middle;
dock:bottom;
}
@@ -200,7 +200,7 @@ Error {
color: $text-error;
border-top: tall $error-darken-2;
border-bottom: tall $error-darken-2;
padding: 0;
text-style: bold;
align-horizontal: center;
@@ -213,21 +213,21 @@ Warning {
color: $text-warning-fade-1;
border-top: tall $warning-darken-2;
border-bottom: tall $warning-darken-2;
text-style: bold;
align-horizontal: center;
}
Success {
width: 100%;
height:auto;
height:auto;
box-sizing: border-box;
background: $success;
color: $text-success-fade-1;
color: $text-success-fade-1;
border-top: hkey $success-darken-2;
border-bottom: hkey $success-darken-2;
border-bottom: hkey $success-darken-2;
text-style: bold ;

View File

@@ -51,6 +51,7 @@ nav:
- "styles/min_height.md"
- "styles/min_width.md"
- "styles/offset.md"
- "styles/opacity.md"
- "styles/outline.md"
- "styles/overflow.md"
- "styles/padding.md"
@@ -59,6 +60,7 @@ nav:
- "styles/scrollbar_size.md"
- "styles/text_align.md"
- "styles/text_style.md"
- "styles/text_opacity.md"
- "styles/tint.md"
- "styles/visibility.md"
- "styles/width.md"

View File

@@ -1,4 +1,6 @@
Button {
padding-left: 1;
padding-right: 1;
margin: 3;
text-opacity: 30%;
}

View File

@@ -1,29 +1,8 @@
Screen {
background: lightcoral;
}
#sidebar {
color: $text-panel;
background: $panel;
dock: left;
width: 30;
offset-x: -100%;
transition: offset 500ms in_out_cubic 2s;
layer: sidebar;
}
#sidebar.-active {
offset-x: 0;
background: darkslategrey;
}
.box1 {
background: orangered;
height: 12;
width: 30;
}
.box2 {
background: blueviolet;
height: 6;
width: 12;
background: darkmagenta;
width: auto;
}

View File

@@ -1,48 +1,14 @@
from __future__ import annotations
from rich.console import RenderableType
from textual import events
from textual.app import App, ComposeResult
from textual.widget import Widget
class Box(Widget, can_focus=True):
CSS = "#box {background: blue;}"
def __init__(
self, id: str | None = None, classes: str | None = None, *children: Widget
):
super().__init__(*children, id=id, classes=classes)
def render(self) -> RenderableType:
return "Box"
from textual.widgets import Static
class JustABox(App):
def on_load(self):
self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar")
def compose(self) -> ComposeResult:
self.box = Box(classes="box1")
yield self.box
yield Box(classes="box2")
yield Widget(id="sidebar")
def key_a(self):
self.animator.animate(
self.box.styles,
"opacity",
value=0.0,
duration=2.0,
delay=2.0,
on_complete=self.box.remove,
)
async def on_key(self, event: events.Key) -> None:
await self.dispatch_key(event)
yield Static("Hello, world!", classes="box1")
if __name__ == "__main__":
app = JustABox(css_path="just_a_box.css", watch_css=True)
app = JustABox(css_path="../darren/just_a_box.css", watch_css=True)
app.run()

View File

@@ -54,7 +54,7 @@ Widget:hover {
}
#footer {
opacity: 1;
text-opacity: 1;
color: $text;
background: $background;
height: 3;
@@ -62,5 +62,5 @@ Widget:hover {
}
#footer.dim {
opacity: 0.5;
text-opacity: 0.5;
}

View File

@@ -46,7 +46,7 @@ DirectoryTree {
DataTable {
/*border:heavy red;*/
/* tint: 10% green; */
/* opacity: 50%; */
/* text-opacity: 50%; */
padding: 1;
margin: 1 2;
height: 24;

View File

@@ -130,7 +130,7 @@ class BasicApp(App, css_path="basic.css"):
classes="scrollable",
),
table,
Widget(DirectoryTree("~/projects/textual"), id="tree-container"),
Widget(DirectoryTree("~/"), id="tree-container"),
Error(),
Tweet(TweetBody(), classes="scrollbar-size-custom"),
Warning(),

45
src/textual/_opacity.py Normal file
View File

@@ -0,0 +1,45 @@
from typing import Iterable
from rich.segment import Segment
from rich.style import Style
from textual.color import Color
def _apply_opacity(
segments: Iterable[Segment],
base_background: Color,
opacity: float,
) -> Iterable[Segment]:
"""Takes an iterable of foreground Segments and blends them into the supplied
background color, yielding copies of the Segments with blended foreground and
background colors applied.
Args:
segments (Iterable[Segment]): The segments in the foreground.
base_background (Color): The background color to blend foreground into.
opacity (float): The blending factor. A value of 1.0 means output segments will
have identical foreground and background colors to input segments.
"""
_Segment = Segment
from_rich_color = Color.from_rich_color
from_color = Style.from_color
blend = base_background.blend
for segment in segments:
text, style, _ = segment
if not style:
yield segment
continue
blended_style = style
if style.color:
color = from_rich_color(style.color)
blended_foreground = blend(color, factor=opacity)
blended_style += from_color(color=blended_foreground.rich_color)
if style.bgcolor:
bgcolor = from_rich_color(style.bgcolor)
blended_background = blend(bgcolor, factor=opacity)
blended_style += from_color(bgcolor=blended_background.rich_color)
yield _Segment(text, blended_style)

30
src/textual/_path.py Normal file
View File

@@ -0,0 +1,30 @@
from __future__ import annotations
import inspect
import sys
from pathlib import Path, PurePath
def _make_path_object_relative(path: str | PurePath, obj: object) -> Path:
"""Convert the supplied path to a Path object that is relative to a given Python object.
If the supplied path is absolute, it will simply be converted to a Path object.
Used, for example, to return the path of a CSS file relative to a Textual App instance.
Args:
path (str | Path): A path.
obj (object): A Python object to resolve the path relative to.
Returns:
Path: A resolved Path object, relative to obj
"""
path = Path(path)
# If the path supplied by the user is absolute, we can use it directly
if path.is_absolute():
return path
# Otherwise (relative path), resolve it relative to obj...
subclass_module = sys.modules[obj.__module__]
subclass_path = Path(inspect.getfile(subclass_module))
resolved_path = (subclass_path.parent / path).resolve()
return resolved_path

View File

@@ -7,11 +7,12 @@ from rich.segment import Segment
from rich.style import Style
from ._border import get_box, render_row
from ._opacity import _apply_opacity
from ._segment_tools import line_crop, line_pad, line_trim
from ._types import Lines
from .color import Color
from .geometry import Region, Size, Spacing
from .renderables.opacity import Opacity
from .renderables.text_opacity import TextOpacity
from .renderables.tint import Tint
if sys.version_info >= (3, 10):
@@ -237,10 +238,13 @@ class StylesCache:
Returns:
list[Segment]: New list of segments
"""
if styles.opacity != 1.0:
segments = Opacity.process_segments(segments, styles.opacity)
if styles.text_opacity != 1.0:
segments = TextOpacity.process_segments(segments, styles.text_opacity)
if styles.tint.a:
segments = Tint.process_segments(segments, styles.tint)
if styles.opacity != 1.0:
segments = _apply_opacity(segments, base_background, styles.opacity)
segments = list(segments)
return segments if isinstance(segments, list) else list(segments)
line: Iterable[Segment]

View File

@@ -9,10 +9,9 @@ import sys
import warnings
from contextlib import redirect_stdout, redirect_stderr
from datetime import datetime
from pathlib import PurePath
from pathlib import PurePath, Path
from time import perf_counter
from typing import (
TYPE_CHECKING,
Any,
Generic,
Iterable,
@@ -25,6 +24,7 @@ from typing import (
from weakref import WeakSet, WeakValueDictionary
from ._ansi_sequences import SYNC_END, SYNC_START
from ._path import _make_path_object_relative
if sys.version_info >= (3, 8):
from typing import Literal
@@ -63,8 +63,6 @@ from .renderables.blank import Blank
from .screen import Screen
from .widget import Widget
if TYPE_CHECKING:
from .css.query import DOMQuery
PLATFORM = platform.system()
WINDOWS = PLATFORM == "Windows"
@@ -147,7 +145,7 @@ class App(Generic[ReturnType], DOMNode):
CSS = """
App {
background: $background;
color: $text-background;
color: $text-background;
}
"""
@@ -225,7 +223,15 @@ class App(Generic[ReturnType], DOMNode):
self.stylesheet = Stylesheet(variables=self.get_css_variables())
self._require_stylesheet_update: set[DOMNode] = set()
self.css_path = css_path or self.CSS_PATH
# We want the CSS path to be resolved from the location of the App subclass
css_path = css_path or self.CSS_PATH
if css_path is not None:
if isinstance(css_path, str):
css_path = Path(css_path)
css_path = _make_path_object_relative(css_path, self) if css_path else None
self.css_path = css_path
self._registry: WeakSet[DOMNode] = WeakSet()

View File

@@ -1,5 +1,7 @@
from __future__ import annotations
import os
import runpy
from typing import cast, TYPE_CHECKING
from importlib_metadata import version
@@ -57,16 +59,12 @@ def import_app(import_name: str) -> App:
lib, _colon, name = import_name.partition(":")
if lib.endswith(".py"):
# We're assuming the user wants to load a .py file
path = os.path.abspath(lib)
try:
with open(lib) as python_file:
py_code = python_file.read()
global_vars = runpy.run_path(path)
except Exception as error:
raise AppFail(str(error))
global_vars: dict[str, object] = {}
exec(py_code, global_vars)
if name:
# User has given a name, use that
try:

View File

@@ -332,7 +332,7 @@ class StylesBuilder:
"visibility", valid_values=list(VALID_VISIBILITY), context="css"
)
def process_opacity(self, name: str, tokens: list[Token]) -> None:
def _process_fractional(self, name: str, tokens: list[Token]) -> None:
if not tokens:
return
token = tokens[0]
@@ -342,16 +342,17 @@ class StylesBuilder:
else:
token_name = token.name
value = token.value
rule_name = name.replace("-", "_")
if token_name == "scalar" and value.endswith("%"):
try:
opacity = percentage_string_to_float(value)
self.styles.set_rule(name, opacity)
text_opacity = percentage_string_to_float(value)
self.styles.set_rule(rule_name, text_opacity)
except ValueError:
error = True
elif token_name == "number":
try:
opacity = clamp(float(value), 0, 1)
self.styles.set_rule(name, opacity)
text_opacity = clamp(float(value), 0, 1)
self.styles.set_rule(rule_name, text_opacity)
except ValueError:
error = True
else:
@@ -360,6 +361,9 @@ class StylesBuilder:
if error:
self.error(name, token, fractional_property_help_text(name, context="css"))
process_opacity = _process_fractional
process_text_opacity = _process_fractional
def _process_space(self, name: str, tokens: list[Token]) -> None:
space: list[int] = []
append = space.append

View File

@@ -88,6 +88,7 @@ class RulesMap(TypedDict, total=False):
text_style: Style
opacity: float
text_opacity: float
padding: Spacing
margin: Spacing
@@ -185,6 +186,7 @@ class StylesBase(ABC):
"color",
"background",
"opacity",
"text_opacity",
"tint",
"scrollbar_color",
"scrollbar_color_hover",
@@ -205,6 +207,7 @@ class StylesBase(ABC):
text_style = StyleFlagsProperty()
opacity = FractionalProperty()
text_opacity = FractionalProperty()
padding = SpacingProperty()
margin = SpacingProperty()

View File

@@ -1,4 +1,3 @@
from __future__ import annotations
import os
from pathlib import PurePath
from typing import Callable
@@ -12,7 +11,7 @@ from ._callback import invoke
class FileMonitor:
"""Monitors a file for changes and invokes a callback when it does."""
def __init__(self, path: str | PurePath, callback: Callable) -> None:
def __init__(self, path: PurePath, callback: Callable) -> None:
self.path = path
self.callback = callback
self._modified = self._get_modified()

View File

@@ -31,7 +31,7 @@ def _get_blended_style_cached(
)
class Opacity:
class TextOpacity:
"""Blend foreground in to background."""
def __init__(self, renderable: RenderableType, opacity: float = 1.0) -> None:
@@ -96,7 +96,7 @@ if __name__ == "__main__":
)
console.print(panel)
opacity_panel = Opacity(panel, opacity=0.5)
opacity_panel = TextOpacity(panel, opacity=0.5)
console.print(opacity_panel)
def frange(start, end, step):

View File

@@ -29,14 +29,14 @@ class HeaderClock(Widget):
"""Display a clock on the right of the header."""
CSS = """
HeaderClock {
HeaderClock {
dock: right;
width: auto;
padding: 0 1;
background: $secondary-background-lighten-1;
color: $text-secondary-background;
opacity: 85%;
content-align: center middle;
text-opacity: 85%;
content-align: center middle;
}
"""
@@ -51,9 +51,9 @@ class HeaderTitle(Widget):
"""Display the title / subtitle in the header."""
CSS = """
HeaderTitle {
HeaderTitle {
content-align: center middle;
width: 100%;
width: 100%;
}
"""
@@ -79,7 +79,7 @@ class Header(Widget):
height: 1;
}
Header.tall {
height: 3;
height: 3;
}
"""

View File

@@ -14,7 +14,7 @@ from textual import events
from textual._layout_resolve import layout_resolve, Edge
from textual.keys import Keys
from textual.reactive import Reactive
from textual.renderables.opacity import Opacity
from textual.renderables.text_opacity import TextOpacity
from textual.renderables.underline_bar import UnderlineBar
from textual.widget import Widget
@@ -125,7 +125,7 @@ class TabsRenderable:
style=inactive_tab_style
+ Style.from_meta({"@click": f"range_clicked('{tab.name}')"}),
)
dimmed_tab_content = Opacity(
dimmed_tab_content = TextOpacity(
tab_content, opacity=self.inactive_text_opacity
)
segments = console.render(dimmed_tab_content)

View File

@@ -1099,15 +1099,15 @@ class TestParseOpacity:
],
)
def test_opacity_to_styles(self, css_value, styles_value):
css = f"#some-widget {{ opacity: {css_value} }}"
css = f"#some-widget {{ text-opacity: {css_value} }}"
stylesheet = Stylesheet()
stylesheet.add_source(css)
assert stylesheet.rules[0].styles.opacity == styles_value
assert stylesheet.rules[0].styles.text_opacity == styles_value
assert not stylesheet.rules[0].errors
def test_opacity_invalid_value(self):
css = "#some-widget { opacity: 123x }"
css = "#some-widget { text-opacity: 123x }"
stylesheet = Stylesheet()
with pytest.raises(StylesheetParseError):

View File

@@ -120,7 +120,7 @@ def test_render_styles_border():
def test_get_opacity_default():
styles = RenderStyles(DOMNode(), Styles(), Styles())
assert styles.opacity == 1.0
assert styles.text_opacity == 1.0
@pytest.mark.parametrize(
@@ -136,14 +136,14 @@ def test_get_opacity_default():
)
def test_opacity_set_then_get(set_value, expected):
styles = RenderStyles(DOMNode(), Styles(), Styles())
styles.opacity = set_value
assert styles.opacity == expected
styles.text_opacity = set_value
assert styles.text_opacity == expected
def test_opacity_set_invalid_type_error():
styles = RenderStyles(DOMNode(), Styles(), Styles())
with pytest.raises(StyleValueError):
styles.opacity = "invalid value"
styles.text_opacity = "invalid value"
@pytest.mark.parametrize(

View File

@@ -2,7 +2,7 @@ import pytest
from rich.text import Text
from tests.utilities.render import render
from textual.renderables.opacity import Opacity
from textual.renderables.text_opacity import TextOpacity
STOP = "\x1b[0m"
@@ -12,39 +12,39 @@ def text():
return Text("Hello, world!", style="#ff0000 on #00ff00", end="")
def test_simple_opacity(text):
def test_simple_text_opacity(text):
blended_red_on_green = "\x1b[38;2;127;127;0;48;2;0;255;0m"
assert render(Opacity(text, opacity=.5)) == (
assert render(TextOpacity(text, opacity=.5)) == (
f"{blended_red_on_green}Hello, world!{STOP}"
)
def test_value_zero_sets_foreground_color_to_background_color(text):
foreground = background = "0;255;0"
assert render(Opacity(text, opacity=0)) == (
assert render(TextOpacity(text, opacity=0)) == (
f"\x1b[38;2;{foreground};48;2;{background}mHello, world!{STOP}"
)
def test_opacity_value_of_one_noop(text):
assert render(Opacity(text, opacity=1)) == render(text)
def test_text_opacity_value_of_one_noop(text):
assert render(TextOpacity(text, opacity=1)) == render(text)
def test_ansi_colors_noop():
ansi_colored_text = Text("Hello, world!", style="red on green", end="")
assert render(Opacity(ansi_colored_text, opacity=.5)) == render(ansi_colored_text)
assert render(TextOpacity(ansi_colored_text, opacity=.5)) == render(ansi_colored_text)
def test_opacity_no_style_noop():
def test_text_opacity_no_style_noop():
text_no_style = Text("Hello, world!", end="")
assert render(Opacity(text_no_style, opacity=.2)) == render(text_no_style)
assert render(TextOpacity(text_no_style, opacity=.2)) == render(text_no_style)
def test_opacity_only_fg_noop():
def test_text_opacity_only_fg_noop():
text_only_fg = Text("Hello, world!", style="#ff0000", end="")
assert render(Opacity(text_only_fg, opacity=.5)) == render(text_only_fg)
assert render(TextOpacity(text_only_fg, opacity=.5)) == render(text_only_fg)
def test_opacity_only_bg_noop():
def test_text_opacity_only_bg_noop():
text_only_bg = Text("Hello, world!", style="on #ff0000", end="")
assert render(Opacity(text_only_bg, opacity=.5)) == render(text_only_bg)
assert render(TextOpacity(text_only_bg, opacity=.5)) == render(text_only_bg)