mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
color animation
This commit is contained in:
@@ -5,15 +5,12 @@ $primary: #20639b;
|
|||||||
App > Screen {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: side=left/1;
|
docks: side=left/1;
|
||||||
text: on $primary;
|
background: $primary;
|
||||||
}
|
|
||||||
|
|
||||||
Widget:hover {
|
|
||||||
outline: solid green;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
text: #09312e on #3caea3;
|
color: #09312e;
|
||||||
|
background: #3caea3;
|
||||||
dock: side;
|
dock: side;
|
||||||
width: 30;
|
width: 30;
|
||||||
offset-x: -100%;
|
offset-x: -100%;
|
||||||
@@ -27,17 +24,21 @@ Widget:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#header {
|
#header {
|
||||||
text: white on #173f5f;
|
color: white;
|
||||||
|
background: #173f5f;
|
||||||
height: 3;
|
height: 3;
|
||||||
border: hkey;
|
border: hkey white;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
text: white on $primary;
|
color: white;
|
||||||
|
background: $primary;
|
||||||
border-bottom: hkey #0f2b41;
|
border-bottom: hkey #0f2b41;
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
text: #3a3009 on #f6d55c;
|
color: #3a3009;
|
||||||
|
background: #f6d55c;
|
||||||
|
|
||||||
height: 3;
|
height: 3;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#uber1 {
|
#uber1 {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
|
|
||||||
text-background: dark_green;
|
background: dark_green;
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
border: heavy white;
|
border: heavy white;
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,5 @@
|
|||||||
.list-item {
|
.list-item {
|
||||||
height: 8;
|
height: 8;
|
||||||
min-width: 80;
|
min-width: 80;
|
||||||
text-background: dark_blue;
|
background: dark_blue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ class Animator:
|
|||||||
|
|
||||||
easing_function = EASING[easing] if isinstance(easing, str) else easing
|
easing_function = EASING[easing] if isinstance(easing, str) else easing
|
||||||
|
|
||||||
animation: Animation
|
animation: Animation | None = None
|
||||||
if hasattr(obj, "__textual_animation__"):
|
if hasattr(obj, "__textual_animation__"):
|
||||||
animation = getattr(obj, "__textual_animation__")(
|
animation = getattr(obj, "__textual_animation__")(
|
||||||
attribute,
|
attribute,
|
||||||
@@ -196,7 +196,7 @@ class Animator:
|
|||||||
speed=speed,
|
speed=speed,
|
||||||
easing=easing_function,
|
easing=easing_function,
|
||||||
)
|
)
|
||||||
else:
|
if animation is None:
|
||||||
start_value = getattr(obj, attribute)
|
start_value = getattr(obj, attribute)
|
||||||
|
|
||||||
if start_value == value:
|
if start_value == value:
|
||||||
|
|||||||
@@ -720,4 +720,4 @@ class App(DOMNode):
|
|||||||
self.screen.query(selector).toggle_class(class_name)
|
self.screen.query(selector).toggle_class(class_name)
|
||||||
|
|
||||||
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
||||||
self.stylesheet.update(self)
|
self.stylesheet.update(self, animate=True)
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ from functools import lru_cache
|
|||||||
import re
|
import re
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
from .geometry import clamp
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.color import Color as RichColor
|
from rich.color import Color as RichColor
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from . import log
|
||||||
|
from .geometry import clamp
|
||||||
|
|
||||||
|
|
||||||
ANSI_COLOR_NAMES = {
|
ANSI_COLOR_NAMES = {
|
||||||
"black": 0,
|
"black": 0,
|
||||||
"red": 1,
|
"red": 1,
|
||||||
@@ -534,16 +537,29 @@ class Color(NamedTuple):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_rich_color(cls, rich_color: RichColor) -> Color:
|
def from_rich_color(cls, rich_color: RichColor) -> Color:
|
||||||
|
"""Create color from Rich's color class."""
|
||||||
r, g, b = rich_color.get_truecolor()
|
r, g, b = rich_color.get_truecolor()
|
||||||
return cls(r, g, b)
|
return cls(r, g, b)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rich_color(self) -> RichColor:
|
def rich_color(self) -> RichColor:
|
||||||
|
"""This color encoded in Rich's Color class."""
|
||||||
r, g, b, _a = self
|
r, g, b, _a = self
|
||||||
return RichColor.from_rgb(r, g, b)
|
return RichColor.from_rgb(r, g, b)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_transparent(self) -> bool:
|
||||||
|
"""Check if the color is transparent."""
|
||||||
|
return self.a == 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hex(self) -> str:
|
def hex(self) -> str:
|
||||||
|
"""The color in CSS hex form, with 6 digits for RGB, and 8 digits for RGBA.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A CSS hex-style color, e.g. "#46b3de" or "#3342457f"
|
||||||
|
|
||||||
|
"""
|
||||||
r, g, b, a = self
|
r, g, b, a = self
|
||||||
return (
|
return (
|
||||||
f"#{r:02X}{g:02X}{b:02X}"
|
f"#{r:02X}{g:02X}{b:02X}"
|
||||||
@@ -553,6 +569,12 @@ class Color(NamedTuple):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def css(self) -> str:
|
def css(self) -> str:
|
||||||
|
"""The color in CSS rgb or rgba form.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: A CSS color, e.g. "rgb(10,20,30)" or "(rgb(50,70,80,0.5)"
|
||||||
|
|
||||||
|
"""
|
||||||
r, g, b, a = self
|
r, g, b, a = self
|
||||||
return f"rgb({r},{g},{b})" if a == 1 else f"rgba({r},{g},{b},{a})"
|
return f"rgb({r},{g},{b})" if a == 1 else f"rgba({r},{g},{b},{a})"
|
||||||
|
|
||||||
@@ -563,6 +585,25 @@ class Color(NamedTuple):
|
|||||||
yield b
|
yield b
|
||||||
yield "a", a
|
yield "a", a
|
||||||
|
|
||||||
|
def blend(self, destination: Color, factor: float) -> Color:
|
||||||
|
"""Generate a new color between two colors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
destination (Color): Another color.
|
||||||
|
factor (float): A blend factor, 0 -> 1
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Color: A new color.
|
||||||
|
"""
|
||||||
|
r1, g1, b1, a1 = self
|
||||||
|
r2, g2, b2, a2 = destination
|
||||||
|
return Color(
|
||||||
|
int(r1 + (r2 - r1) * factor),
|
||||||
|
int(g1 + (g2 - g1) * factor),
|
||||||
|
int(b1 + (b2 - b1) * factor),
|
||||||
|
a1 + (a2 - a1) * factor,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@lru_cache(maxsize=1024 * 4)
|
@lru_cache(maxsize=1024 * 4)
|
||||||
def parse(cls, color_text: str) -> Color:
|
def parse(cls, color_text: str) -> Color:
|
||||||
@@ -587,7 +628,10 @@ class Color(NamedTuple):
|
|||||||
|
|
||||||
if rgb_hex is not None:
|
if rgb_hex is not None:
|
||||||
color = cls(
|
color = cls(
|
||||||
int(rgb_hex[0:2], 16), int(rgb_hex[2:4], 16), int(rgb_hex[4:6], 16), 1
|
int(rgb_hex[0:2], 16),
|
||||||
|
int(rgb_hex[2:4], 16),
|
||||||
|
int(rgb_hex[4:6], 16),
|
||||||
|
1.0,
|
||||||
)
|
)
|
||||||
elif rgba_hex is not None:
|
elif rgba_hex is not None:
|
||||||
color = cls(
|
color = cls(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import rich.repr
|
|||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from .. import log
|
from .. import log
|
||||||
from ..color import Color
|
from ..color import Color, ColorPair
|
||||||
from ._error_tools import friendly_list
|
from ._error_tools import friendly_list
|
||||||
from .constants import NULL_SPACING
|
from .constants import NULL_SPACING
|
||||||
from .errors import StyleTypeError, StyleValueError
|
from .errors import StyleTypeError, StyleValueError
|
||||||
@@ -316,46 +316,9 @@ class StyleProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
A ``Style`` object.
|
A ``Style`` object.
|
||||||
"""
|
"""
|
||||||
has_rule = obj.has_rule
|
style = ColorPair(obj.color, obj.background).style
|
||||||
|
|
||||||
style = Style.from_color(
|
|
||||||
obj.text_color.rich_color if has_rule("text_color") else None,
|
|
||||||
obj.text_background.rich_color if has_rule("text_background") else None,
|
|
||||||
)
|
|
||||||
if has_rule("text_style"):
|
|
||||||
style += obj.text_style
|
|
||||||
|
|
||||||
return style
|
return style
|
||||||
|
|
||||||
def __set__(self, obj: StylesBase, style: Style | str | None):
|
|
||||||
"""Set the Style
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj (Styles): The ``Styles`` object.
|
|
||||||
style (Style | str, optional): You can supply the ``Style`` directly, or a
|
|
||||||
string (e.g. ``"blue on #f0f0f0"``).
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
StyleSyntaxError: When the supplied style string has invalid syntax.
|
|
||||||
"""
|
|
||||||
obj.refresh()
|
|
||||||
|
|
||||||
if style is None:
|
|
||||||
clear_rule = obj.clear_rule
|
|
||||||
clear_rule("text_color")
|
|
||||||
clear_rule("text_background")
|
|
||||||
clear_rule("text_style")
|
|
||||||
else:
|
|
||||||
if isinstance(style, str):
|
|
||||||
style = Style.parse(style)
|
|
||||||
|
|
||||||
if style.color is not None:
|
|
||||||
obj.text_color = style.color
|
|
||||||
if style.bgcolor is not None:
|
|
||||||
obj.text_background = style.bgcolor
|
|
||||||
if style.without_color:
|
|
||||||
obj.text_style = str(style.without_color)
|
|
||||||
|
|
||||||
|
|
||||||
class SpacingProperty:
|
class SpacingProperty:
|
||||||
"""Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
|
"""Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
|
||||||
@@ -660,9 +623,7 @@ class NameListProperty:
|
|||||||
) -> tuple[str, ...]:
|
) -> tuple[str, ...]:
|
||||||
return obj.get_rule(self.name, ())
|
return obj.get_rule(self.name, ())
|
||||||
|
|
||||||
def __set__(
|
def __set__(self, obj: StylesBase, names: str | tuple[str] | None = None):
|
||||||
self, obj: StylesBase, names: str | tuple[str] | None = None
|
|
||||||
) -> str | tuple[str] | None:
|
|
||||||
|
|
||||||
if names is None:
|
if names is None:
|
||||||
if obj.clear_rule(self.name):
|
if obj.clear_rule(self.name):
|
||||||
@@ -687,7 +648,7 @@ class ColorProperty:
|
|||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def __get__(self, obj: StylesBase, objtype: type[Styles] | None = None) -> Color:
|
def __get__(self, obj: StylesBase, objtype: type[Styles] | None = None) -> Color:
|
||||||
"""Get the ``Color``, or ``Color.default()`` if no color is set.
|
"""Get a ``Color``.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (Styles): The ``Styles`` object.
|
obj (Styles): The ``Styles`` object.
|
||||||
@@ -696,7 +657,7 @@ class ColorProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
Color: The Color
|
Color: The Color
|
||||||
"""
|
"""
|
||||||
return obj.get_rule(self.name) or self._default_color
|
return obj.get_rule(self.name, self._default_color)
|
||||||
|
|
||||||
def __set__(self, obj: StylesBase, color: Color | str | None):
|
def __set__(self, obj: StylesBase, color: Color | str | None):
|
||||||
"""Set the Color
|
"""Set the Color
|
||||||
|
|||||||
@@ -459,36 +459,34 @@ class StylesBuilder:
|
|||||||
f"invalid value for layout (received {value!r}, expected {friendly_list(LAYOUT_MAP.keys())})",
|
f"invalid value for layout (received {value!r}, expected {friendly_list(LAYOUT_MAP.keys())})",
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
|
# def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||||
style_definition = _join_tokens(tokens, joiner=" ")
|
# style_definition = _join_tokens(tokens, joiner=" ")
|
||||||
|
|
||||||
# If every token in the value is a referenced by the same variable,
|
# # If every token in the value is a referenced by the same variable,
|
||||||
# we can display the variable name before the style definition.
|
# # we can display the variable name before the style definition.
|
||||||
# TODO: Factor this out to apply it to other properties too.
|
# # TODO: Factor this out to apply it to other properties too.
|
||||||
unique_references = {t.referenced_by for t in tokens if t.referenced_by}
|
# unique_references = {t.referenced_by for t in tokens if t.referenced_by}
|
||||||
if tokens and tokens[0].referenced_by and len(unique_references) == 1:
|
# if tokens and tokens[0].referenced_by and len(unique_references) == 1:
|
||||||
variable_prefix = f"${tokens[0].referenced_by.name}="
|
# variable_prefix = f"${tokens[0].referenced_by.name}="
|
||||||
else:
|
# else:
|
||||||
variable_prefix = ""
|
# variable_prefix = ""
|
||||||
|
|
||||||
try:
|
# try:
|
||||||
style = Style.parse(style_definition)
|
# style = Style.parse(style_definition)
|
||||||
self.styles.text = style
|
# self.styles.text = style
|
||||||
except Exception as error:
|
# except Exception as error:
|
||||||
message = f"property 'text' has invalid value {variable_prefix}{style_definition!r}; {error}"
|
# message = f"property 'text' has invalid value {variable_prefix}{style_definition!r}; {error}"
|
||||||
self.error(name, tokens[0], message)
|
# self.error(name, tokens[0], message)
|
||||||
if important:
|
# if important:
|
||||||
self.styles.important.update(
|
# self.styles.important.update(
|
||||||
{"text_style", "text_background", "text_color"}
|
# {"text_style", "text_background", "text_color"}
|
||||||
)
|
# )
|
||||||
|
|
||||||
def process_text_color(
|
def process_color(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||||
self, name: str, tokens: list[Token], important: bool
|
|
||||||
) -> None:
|
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token.name in ("color", "token"):
|
if token.name in ("color", "token"):
|
||||||
try:
|
try:
|
||||||
self.styles._rules["text_color"] = Color.parse(token.value)
|
self.styles._rules["color"] = Color.parse(token.value)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.error(
|
self.error(
|
||||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||||
@@ -498,13 +496,13 @@ class StylesBuilder:
|
|||||||
name, token, f"unexpected token {token.value!r} in declaration"
|
name, token, f"unexpected token {token.value!r} in declaration"
|
||||||
)
|
)
|
||||||
|
|
||||||
def process_text_background(
|
def process_background(
|
||||||
self, name: str, tokens: list[Token], important: bool
|
self, name: str, tokens: list[Token], important: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
for token in tokens:
|
for token in tokens:
|
||||||
if token.name in ("color", "token"):
|
if token.name in ("color", "token"):
|
||||||
try:
|
try:
|
||||||
self.styles._rules["text_background"] = Color.parse(token.value)
|
self.styles._rules["background"] = Color.parse(token.value)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.error(
|
self.error(
|
||||||
name, token, f"failed to parse color {token.value!r}; {error}"
|
name, token, f"failed to parse color {token.value!r}; {error}"
|
||||||
|
|||||||
@@ -61,16 +61,16 @@ class RulesMap(TypedDict, total=False):
|
|||||||
|
|
||||||
Any key may be absent, indiciating that rule has not been set.
|
Any key may be absent, indiciating that rule has not been set.
|
||||||
|
|
||||||
Does not define composite rules, that is a rule that is made of a combination of other rules. For instance,
|
Does not define composite rules, that is a rule that is made of a combination of other rules.
|
||||||
the text style is made up of text_color, text_background, and text_style.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
display: Display
|
display: Display
|
||||||
visibility: Visibility
|
visibility: Visibility
|
||||||
layout: "Layout"
|
layout: "Layout"
|
||||||
|
|
||||||
text_color: Color
|
color: Color
|
||||||
text_background: Color
|
background: Color
|
||||||
text_style: Style
|
text_style: Style
|
||||||
|
|
||||||
opacity: float
|
opacity: float
|
||||||
@@ -132,6 +132,8 @@ class StylesBase(ABC):
|
|||||||
"min_height",
|
"min_height",
|
||||||
"max_width",
|
"max_width",
|
||||||
"max_height",
|
"max_height",
|
||||||
|
"color",
|
||||||
|
"background",
|
||||||
}
|
}
|
||||||
|
|
||||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||||
@@ -139,8 +141,8 @@ class StylesBase(ABC):
|
|||||||
layout = LayoutProperty()
|
layout = LayoutProperty()
|
||||||
|
|
||||||
text = StyleProperty()
|
text = StyleProperty()
|
||||||
text_color = ColorProperty(Color(255, 255, 255))
|
color = ColorProperty(Color(255, 255, 255))
|
||||||
text_background = ColorProperty(Color(255, 255, 255))
|
background = ColorProperty(Color(255, 255, 255))
|
||||||
text_style = StyleFlagsProperty()
|
text_style = StyleFlagsProperty()
|
||||||
|
|
||||||
opacity = FractionalProperty()
|
opacity = FractionalProperty()
|
||||||
@@ -257,14 +259,6 @@ class StylesBase(ABC):
|
|||||||
layout (bool, optional): Also require a layout. Defaults to False.
|
layout (bool, optional): Also require a layout. Defaults to False.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def check_refresh(self) -> tuple[bool, bool]:
|
|
||||||
"""Check if the Styles must be refreshed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[bool, bool]: (repaint required, layout_required)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the rules to initial state."""
|
"""Reset the rules to initial state."""
|
||||||
@@ -402,9 +396,6 @@ class Styles(StylesBase):
|
|||||||
|
|
||||||
_rules: RulesMap = field(default_factory=dict)
|
_rules: RulesMap = field(default_factory=dict)
|
||||||
|
|
||||||
_layout_required: bool = False
|
|
||||||
_repaint_required: bool = False
|
|
||||||
|
|
||||||
important: set[str] = field(default_factory=set)
|
important: set[str] = field(default_factory=set)
|
||||||
|
|
||||||
def copy(self) -> Styles:
|
def copy(self) -> Styles:
|
||||||
@@ -449,18 +440,8 @@ class Styles(StylesBase):
|
|||||||
return self._rules.get(rule, default)
|
return self._rules.get(rule, default)
|
||||||
|
|
||||||
def refresh(self, *, layout: bool = False) -> None:
|
def refresh(self, *, layout: bool = False) -> None:
|
||||||
self._repaint_required = True
|
if self.node is not None:
|
||||||
self._layout_required = self._layout_required or layout
|
self.node.refresh(layout=layout)
|
||||||
|
|
||||||
def check_refresh(self) -> tuple[bool, bool]:
|
|
||||||
"""Check if the Styles must be refreshed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[bool, bool]: (repaint required, layout_required)
|
|
||||||
"""
|
|
||||||
result = (self._repaint_required, self._layout_required)
|
|
||||||
self._repaint_required = self._layout_required = False
|
|
||||||
return result
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the rules to initial state."""
|
"""Reset the rules to initial state."""
|
||||||
@@ -637,19 +618,12 @@ class Styles(StylesBase):
|
|||||||
assert self.layout is not None
|
assert self.layout is not None
|
||||||
append_declaration("layout", self.layout.name)
|
append_declaration("layout", self.layout.name)
|
||||||
|
|
||||||
if (
|
if has_rule("color"):
|
||||||
has_rule("text_color")
|
append_declaration("color", self.color.hex)
|
||||||
and has_rule("text_background")
|
if has_rule("background"):
|
||||||
and has_rule("text_style")
|
append_declaration("background", self.background.hex)
|
||||||
):
|
if has_rule("text_style"):
|
||||||
append_declaration("text", str(self.text))
|
append_declaration("text-style", str(get_rule("text_style")))
|
||||||
else:
|
|
||||||
if has_rule("text_color"):
|
|
||||||
append_declaration("text-color", self.text_color.hex)
|
|
||||||
if has_rule("text_background"):
|
|
||||||
append_declaration("text-background", self.text_background.hex)
|
|
||||||
if has_rule("text_style"):
|
|
||||||
append_declaration("text-style", str(get_rule("text_style")))
|
|
||||||
|
|
||||||
if has_rule("overflow-x"):
|
if has_rule("overflow-x"):
|
||||||
append_declaration("overflow-x", self.overflow_x)
|
append_declaration("overflow-x", self.overflow_x)
|
||||||
@@ -725,17 +699,6 @@ class RenderStyles(StylesBase):
|
|||||||
def merge_rules(self, rules: RulesMap) -> None:
|
def merge_rules(self, rules: RulesMap) -> None:
|
||||||
self._inline_styles.merge_rules(rules)
|
self._inline_styles.merge_rules(rules)
|
||||||
|
|
||||||
def check_refresh(self) -> tuple[bool, bool]:
|
|
||||||
"""Check if the Styles must be refreshed.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[bool, bool]: (repaint required, layout_required)
|
|
||||||
"""
|
|
||||||
base_repaint, base_layout = self._base_styles.check_refresh()
|
|
||||||
inline_repaint, inline_layout = self._inline_styles.check_refresh()
|
|
||||||
result = (base_repaint or inline_repaint, base_layout or inline_layout)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Reset the rules to initial state."""
|
"""Reset the rules to initial state."""
|
||||||
self._inline_styles.reset()
|
self._inline_styles.reset()
|
||||||
|
|||||||
@@ -248,12 +248,6 @@ class Stylesheet:
|
|||||||
for key in modified_rule_keys:
|
for key in modified_rule_keys:
|
||||||
setattr(base_styles, key, rules.get(key))
|
setattr(base_styles, key, rules.get(key))
|
||||||
|
|
||||||
# The styles object may have requested a refresh / layout
|
|
||||||
# It's the style properties that set these flags
|
|
||||||
repaint, layout = styles.check_refresh()
|
|
||||||
if repaint:
|
|
||||||
node.refresh(layout=layout)
|
|
||||||
|
|
||||||
def update(self, root: DOMNode, animate: bool = False) -> None:
|
def update(self, root: DOMNode, animate: bool = False) -> None:
|
||||||
"""Update a node and its children."""
|
"""Update a node and its children."""
|
||||||
apply = self.apply
|
apply = self.apply
|
||||||
|
|||||||
@@ -314,8 +314,7 @@ class DOMNode(MessagePump):
|
|||||||
for node in self.walk_children():
|
for node in self.walk_children():
|
||||||
node._css_styles.reset()
|
node._css_styles.reset()
|
||||||
if isinstance(node, Widget):
|
if isinstance(node, Widget):
|
||||||
# node.clear_render_cache()
|
node.set_dirty()
|
||||||
node._repaint_required = True
|
|
||||||
node._layout_required = True
|
node._layout_required = True
|
||||||
|
|
||||||
def on_style_change(self) -> None:
|
def on_style_change(self) -> None:
|
||||||
|
|||||||
@@ -495,11 +495,11 @@ class Region(NamedTuple):
|
|||||||
cut_x ↓
|
cut_x ↓
|
||||||
┌────────┐┌───┐
|
┌────────┐┌───┐
|
||||||
│ ││ │
|
│ ││ │
|
||||||
│ ││ │
|
│ 0 ││ 1 │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
cut_y → └────────┘└───┘
|
cut_y → └────────┘└───┘
|
||||||
┌────────┐┌───┐
|
┌────────┐┌───┐
|
||||||
│ ││ │
|
│ 2 ││ 3 │
|
||||||
└────────┘└───┘
|
└────────┘└───┘
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -531,7 +531,7 @@ class Region(NamedTuple):
|
|||||||
|
|
||||||
cut ↓
|
cut ↓
|
||||||
┌────────┐┌───┐
|
┌────────┐┌───┐
|
||||||
│ ││ │
|
│ 0 ││ 1 │
|
||||||
│ ││ │
|
│ ││ │
|
||||||
└────────┘└───┘
|
└────────┘└───┘
|
||||||
|
|
||||||
@@ -556,10 +556,11 @@ class Region(NamedTuple):
|
|||||||
"""Split a region in to two, from a given x offset.
|
"""Split a region in to two, from a given x offset.
|
||||||
|
|
||||||
┌─────────┐
|
┌─────────┐
|
||||||
│ │
|
│ 0 │
|
||||||
│ │
|
│ │
|
||||||
cut → └─────────┘
|
cut → └─────────┘
|
||||||
┌─────────┐
|
┌─────────┐
|
||||||
|
│ 1 │
|
||||||
└─────────┘
|
└─────────┘
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -134,6 +134,7 @@ class Screen(Widget):
|
|||||||
widget = message.widget
|
widget = message.widget
|
||||||
assert isinstance(widget, Widget)
|
assert isinstance(widget, Widget)
|
||||||
self._dirty_widgets.append(widget)
|
self._dirty_widgets.append(widget)
|
||||||
|
self.check_idle()
|
||||||
|
|
||||||
async def handle_layout(self, message: messages.Layout) -> None:
|
async def handle_layout(self, message: messages.Layout) -> None:
|
||||||
message.stop()
|
message.stop()
|
||||||
@@ -204,10 +205,8 @@ class Screen(Widget):
|
|||||||
widget, _region = self.get_widget_at(event.x, event.y)
|
widget, _region = self.get_widget_at(event.x, event.y)
|
||||||
except errors.NoWidget:
|
except errors.NoWidget:
|
||||||
return
|
return
|
||||||
self.log("forward", widget, event)
|
|
||||||
scroll_widget = widget
|
scroll_widget = widget
|
||||||
if scroll_widget is not None:
|
if scroll_widget is not None:
|
||||||
await scroll_widget.forward_event(event)
|
await scroll_widget.forward_event(event)
|
||||||
else:
|
else:
|
||||||
self.log("view.forwarded", event)
|
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ class Widget(DOMNode):
|
|||||||
self._layout_required = False
|
self._layout_required = False
|
||||||
self._animate: BoundAnimator | None = None
|
self._animate: BoundAnimator | None = None
|
||||||
self._reactive_watches: dict[str, Callable] = {}
|
self._reactive_watches: dict[str, Callable] = {}
|
||||||
self._mouse_over: bool = False
|
|
||||||
self.highlight_style: Style | None = None
|
self.highlight_style: Style | None = None
|
||||||
|
|
||||||
self._vertical_scrollbar: ScrollBar | None = None
|
self._vertical_scrollbar: ScrollBar | None = None
|
||||||
@@ -356,7 +355,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def get_pseudo_classes(self) -> Iterable[str]:
|
def get_pseudo_classes(self) -> Iterable[str]:
|
||||||
"""Pseudo classes for a widget"""
|
"""Pseudo classes for a widget"""
|
||||||
if self._mouse_over:
|
if self.mouse_over:
|
||||||
yield "hover"
|
yield "hover"
|
||||||
if self.has_focus:
|
if self.has_focus:
|
||||||
yield "focus"
|
yield "focus"
|
||||||
@@ -472,6 +471,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def on_style_change(self) -> None:
|
def on_style_change(self) -> None:
|
||||||
self.set_dirty()
|
self.set_dirty()
|
||||||
|
self.check_idle()
|
||||||
|
|
||||||
def size_updated(
|
def size_updated(
|
||||||
self, size: Size, virtual_size: Size, container_size: Size
|
self, size: Size, virtual_size: Size, container_size: Size
|
||||||
@@ -575,15 +575,11 @@ class Widget(DOMNode):
|
|||||||
Args:
|
Args:
|
||||||
event (events.Idle): Idle event.
|
event (events.Idle): Idle event.
|
||||||
"""
|
"""
|
||||||
# Check if the styles have changed
|
|
||||||
repaint, layout = self.styles.check_refresh()
|
|
||||||
if self._dirty_regions:
|
|
||||||
repaint = True
|
|
||||||
|
|
||||||
if layout or self.check_layout():
|
if self.check_layout():
|
||||||
self._reset_check_layout()
|
self._reset_check_layout()
|
||||||
self.screen.post_message_no_wait(messages.Layout(self))
|
self.screen.post_message_no_wait(messages.Layout(self))
|
||||||
elif repaint:
|
elif self._dirty_regions:
|
||||||
self.emit_no_wait(messages.Update(self, self))
|
self.emit_no_wait(messages.Update(self, self))
|
||||||
|
|
||||||
async def focus(self) -> None:
|
async def focus(self) -> None:
|
||||||
@@ -622,6 +618,12 @@ class Widget(DOMNode):
|
|||||||
async def on_key(self, event: events.Key) -> None:
|
async def on_key(self, event: events.Key) -> None:
|
||||||
await self.dispatch_key(event)
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
|
def on_leave(self) -> None:
|
||||||
|
self.mouse_over = False
|
||||||
|
|
||||||
|
def on_enter(self) -> None:
|
||||||
|
self.mouse_over = True
|
||||||
|
|
||||||
def on_mouse_scroll_down(self) -> None:
|
def on_mouse_scroll_down(self) -> None:
|
||||||
self.scroll_down(animate=True)
|
self.scroll_down(animate=True)
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,8 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from rich.color import Color
|
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from textual.color import Color
|
||||||
from textual.css.errors import StyleTypeError
|
from textual.css.errors import StyleTypeError
|
||||||
from textual.css.styles import Styles, RenderStyles
|
from textual.css.styles import Styles, RenderStyles
|
||||||
from textual.dom import DOMNode
|
from textual.dom import DOMNode
|
||||||
@@ -93,20 +94,20 @@ def test_render_styles_text():
|
|||||||
assert styles_view.text == Style()
|
assert styles_view.text == Style()
|
||||||
|
|
||||||
# Base is bold blue
|
# Base is bold blue
|
||||||
base.text_color = "blue"
|
base.color = "blue"
|
||||||
base.text_style = "bold"
|
base.text_style = "bold"
|
||||||
assert styles_view.text == Style.parse("bold blue")
|
assert styles_view.text == Style.parse("bold blue")
|
||||||
|
|
||||||
# Base is bold blue, inline is red
|
# Base is bold blue, inline is red
|
||||||
inline.text_color = "red"
|
inline.color = "red"
|
||||||
assert styles_view.text == Style.parse("bold red")
|
assert styles_view.text == Style.parse("bold red")
|
||||||
|
|
||||||
# Base is bold yellow, inline is red
|
# Base is bold yellow, inline is red
|
||||||
base.text_color = "yellow"
|
base.color = "yellow"
|
||||||
assert styles_view.text == Style.parse("bold red")
|
assert styles_view.text == Style.parse("bold red")
|
||||||
|
|
||||||
# Base is bold blue
|
# Base is bold blue
|
||||||
inline.text_color = None
|
inline.color = None
|
||||||
assert styles_view.text == Style.parse("bold yellow")
|
assert styles_view.text == Style.parse("bold yellow")
|
||||||
|
|
||||||
|
|
||||||
@@ -125,25 +126,28 @@ def test_render_styles_border():
|
|||||||
assert styles_view.border_left == ("rounded", Color.parse("green"))
|
assert styles_view.border_left == ("rounded", Color.parse("green"))
|
||||||
assert styles_view.border == (
|
assert styles_view.border == (
|
||||||
("heavy", Color.parse("red")),
|
("heavy", Color.parse("red")),
|
||||||
("", Color.default()),
|
("", Color(0, 255, 0)),
|
||||||
("", Color.default()),
|
("", Color(0, 255, 0)),
|
||||||
("rounded", Color.parse("green")),
|
("rounded", Color.parse("green")),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_get_opacity_default():
|
def test_get_opacity_default():
|
||||||
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
||||||
assert styles.opacity == 1.
|
assert styles.opacity == 1.0
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("set_value, expected", [
|
@pytest.mark.parametrize(
|
||||||
[0.2, 0.2],
|
"set_value, expected",
|
||||||
[-0.4, 0.0],
|
[
|
||||||
[5.8, 1.0],
|
[0.2, 0.2],
|
||||||
["25%", 0.25],
|
[-0.4, 0.0],
|
||||||
["-10%", 0.0],
|
[5.8, 1.0],
|
||||||
["120%", 1.0],
|
["25%", 0.25],
|
||||||
])
|
["-10%", 0.0],
|
||||||
|
["120%", 1.0],
|
||||||
|
],
|
||||||
|
)
|
||||||
def test_opacity_set_then_get(set_value, expected):
|
def test_opacity_set_then_get(set_value, expected):
|
||||||
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
||||||
styles.opacity = set_value
|
styles.opacity = set_value
|
||||||
|
|||||||
Reference in New Issue
Block a user