mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #294 from Textualize/reset-css
Implement CSS defaults
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
# Dev Sandbox
|
||||
|
||||
This directory contains test code. None of the .py files here are guaranteed to run or do anything useful, but you are welcome to look around.
|
||||
This directory contains test code for Textual devs to experiment with new features. None of the .py files here are guaranteed to run or do anything useful, but you are welcome to look around.
|
||||
|
||||
33
sandbox/dev_sandbox.py
Normal file
33
sandbox/dev_sandbox.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from rich.console import RenderableType
|
||||
from rich.panel import Panel
|
||||
|
||||
from textual.app import App
|
||||
from textual.widget import Widget
|
||||
|
||||
|
||||
class PanelWidget(Widget):
|
||||
def render(self) -> RenderableType:
|
||||
return Panel("hello world!", title="Title")
|
||||
|
||||
|
||||
class BasicApp(App):
|
||||
"""Sandbox application used for testing/development by Textual developers"""
|
||||
|
||||
def on_load(self):
|
||||
"""Bind keys here."""
|
||||
self.bind("tab", "toggle_class('#sidebar', '-active')")
|
||||
self.bind("a", "toggle_class('#header', '-visible')")
|
||||
self.bind("c", "toggle_class('#content', '-content-visible')")
|
||||
self.bind("d", "toggle_class('#footer', 'dim')")
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
self.mount(
|
||||
header=Widget(),
|
||||
content=PanelWidget(),
|
||||
footer=Widget(),
|
||||
sidebar=Widget(),
|
||||
)
|
||||
|
||||
|
||||
BasicApp.run(css_file="dev_sandbox.scss", watch_css=True, log="textual.log")
|
||||
63
sandbox/dev_sandbox.scss
Normal file
63
sandbox/dev_sandbox.scss
Normal file
@@ -0,0 +1,63 @@
|
||||
/* CSS file for dev_sandbox.py */
|
||||
|
||||
$text: #f0f0f0;
|
||||
$primary: #021720;
|
||||
$secondary:#95d52a;
|
||||
$background: #262626;
|
||||
|
||||
$primary-style: $text on $background;
|
||||
$animation-speed: 500ms;
|
||||
$animation: offset $animation-speed in_out_cubic;
|
||||
|
||||
App > View {
|
||||
docks: side=left/1;
|
||||
text: on $background;
|
||||
}
|
||||
|
||||
Widget:hover {
|
||||
outline: heavy;
|
||||
text: bold !important;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
text: $primary-style;
|
||||
dock: side;
|
||||
width: 30;
|
||||
offset-x: -100%;
|
||||
transition: $animation;
|
||||
border-right: outer $secondary;
|
||||
}
|
||||
|
||||
#sidebar.-active {
|
||||
offset-x: 0;
|
||||
}
|
||||
|
||||
#header {
|
||||
text: $text on $primary;
|
||||
height: 3;
|
||||
border-bottom: hkey $secondary;
|
||||
}
|
||||
|
||||
#header.-visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#content {
|
||||
text: $text on $background;
|
||||
offset-y: -3;
|
||||
}
|
||||
|
||||
#content.-content-visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#footer {
|
||||
opacity: 1;
|
||||
text: $text on $primary;
|
||||
height: 3;
|
||||
border-top: hkey $secondary;
|
||||
}
|
||||
|
||||
#footer.dim {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -5,3 +5,7 @@ App > View {
|
||||
Widget {
|
||||
text: on blue;
|
||||
}
|
||||
|
||||
Widget.-highlight {
|
||||
outline: heavy red;
|
||||
}
|
||||
|
||||
@@ -25,5 +25,9 @@ class BasicApp(App):
|
||||
def key_b(self) -> None:
|
||||
self["#footer"].set_styles("text: on green")
|
||||
|
||||
def key_c(self) -> None:
|
||||
self["#header"].toggle_class("-highlight")
|
||||
self.log(self["#header"].styles)
|
||||
|
||||
|
||||
BasicApp.run(css_file="local_styles.css", log="textual.log")
|
||||
|
||||
@@ -45,6 +45,7 @@ class SimpleAnimation(Animation):
|
||||
duration: float
|
||||
start_value: float | Animatable
|
||||
end_value: float | Animatable
|
||||
final_value: float | Animatable
|
||||
easing: EasingFunction
|
||||
|
||||
def __call__(self, time: float) -> bool:
|
||||
@@ -62,7 +63,9 @@ class SimpleAnimation(Animation):
|
||||
factor = min(1.0, (time - self.start_time) / self.duration)
|
||||
eased_factor = self.easing(factor)
|
||||
|
||||
if isinstance(self.start_value, Animatable):
|
||||
if factor == 1.0:
|
||||
value = self.end_value
|
||||
elif isinstance(self.start_value, Animatable):
|
||||
assert isinstance(
|
||||
self.end_value, Animatable
|
||||
), "end_value must be animatable"
|
||||
@@ -98,6 +101,7 @@ class BoundAnimator:
|
||||
attribute: str,
|
||||
value: float,
|
||||
*,
|
||||
final_value: Any = ...,
|
||||
duration: float | None = None,
|
||||
speed: float | None = None,
|
||||
easing: EasingFunction | str = DEFAULT_EASING,
|
||||
@@ -107,6 +111,7 @@ class BoundAnimator:
|
||||
self._obj,
|
||||
attribute=attribute,
|
||||
value=value,
|
||||
final_value=final_value,
|
||||
duration=duration,
|
||||
speed=speed,
|
||||
easing=easing_function,
|
||||
@@ -148,11 +153,25 @@ class Animator:
|
||||
attribute: str,
|
||||
value: Any,
|
||||
*,
|
||||
final_value: Any = ...,
|
||||
duration: float | None = None,
|
||||
speed: float | None = None,
|
||||
easing: EasingFunction | str = DEFAULT_EASING,
|
||||
) -> None:
|
||||
"""Animate an attribute to a new value.
|
||||
|
||||
Args:
|
||||
obj (object): The object containing the attribute.
|
||||
attribute (str): The name of the attribute.
|
||||
value (Any): The destination value of the attribute.
|
||||
final_value (Any, optional): The final value, or ellipsis if it is the same as ``value``. Defaults to ....
|
||||
duration (float | None, optional): The duration of the animation, or ``None`` to use speed. Defaults to ``None``.
|
||||
speed (float | None, optional): The speed of the animation. Defaults to None.
|
||||
easing (EasingFunction | str, optional): An easing function. Defaults to DEFAULT_EASING.
|
||||
"""
|
||||
|
||||
if final_value is ...:
|
||||
final_value = value
|
||||
start_time = time()
|
||||
|
||||
animation_key = (id(obj), attribute)
|
||||
@@ -190,6 +209,7 @@ class Animator:
|
||||
duration=animation_duration,
|
||||
start_value=start_value,
|
||||
end_value=value,
|
||||
final_value=final_value,
|
||||
easing=easing_function,
|
||||
)
|
||||
assert animation is not None, "animation expected to be non-None"
|
||||
@@ -206,4 +226,5 @@ class Animator:
|
||||
animation = self._animations[animation_key]
|
||||
if animation(animation_time):
|
||||
del self._animations[animation_key]
|
||||
# TODO: We should be able to do animation without refreshing everything
|
||||
self.target.view.refresh(True, True)
|
||||
|
||||
@@ -682,17 +682,12 @@ class App(DOMNode):
|
||||
|
||||
async def action_add_class_(self, selector: str, class_name: str) -> None:
|
||||
self.view.query(selector).add_class(class_name)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
async def action_remove_class_(self, selector: str, class_name: str) -> None:
|
||||
self.view.query(selector).remove_class(class_name)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
async def action_toggle_class(self, selector: str, class_name: str) -> None:
|
||||
self.view.query(selector).toggle_class(class_name)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
async def handle_styles_updated(self, message: messages.StylesUpdated) -> None:
|
||||
self.reset_styles()
|
||||
self.stylesheet.update(self)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
@@ -105,8 +105,8 @@ class ScalarProperty:
|
||||
)
|
||||
if new_value is not None and new_value.is_percent:
|
||||
new_value = Scalar(float(new_value.value), self.percent_unit, Unit.WIDTH)
|
||||
obj.set_rule(self.name, new_value)
|
||||
obj.refresh()
|
||||
if obj.set_rule(self.name, new_value):
|
||||
obj.refresh()
|
||||
|
||||
|
||||
class BoxProperty:
|
||||
@@ -151,7 +151,8 @@ class BoxProperty:
|
||||
StyleSyntaxError: If the string supplied for the color has invalid syntax.
|
||||
"""
|
||||
if border is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh()
|
||||
else:
|
||||
_type, color = border
|
||||
new_value = border
|
||||
@@ -159,8 +160,8 @@ class BoxProperty:
|
||||
new_value = (_type, Color.parse(color))
|
||||
elif isinstance(color, Color):
|
||||
new_value = (_type, color)
|
||||
obj.set_rule(self.name, new_value)
|
||||
obj.refresh()
|
||||
if obj.set_rule(self.name, new_value):
|
||||
obj.refresh()
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
@@ -378,11 +379,13 @@ class SpacingProperty:
|
||||
ValueError: When the value is malformed, e.g. a ``tuple`` with a length that is
|
||||
not 1, 2, or 4.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
|
||||
if spacing is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh(layout=True)
|
||||
else:
|
||||
obj.set_rule(self.name, Spacing.unpack(spacing))
|
||||
if obj.set_rule(self.name, Spacing.unpack(spacing)):
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class DocksProperty:
|
||||
@@ -411,12 +414,12 @@ class DocksProperty:
|
||||
obj (Styles): The ``Styles`` object.
|
||||
docks (Iterable[DockGroup]): Iterable of DockGroups
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
if docks is None:
|
||||
obj.clear_rule("docks")
|
||||
|
||||
if obj.clear_rule("docks"):
|
||||
obj.refresh(layout=True)
|
||||
else:
|
||||
obj.set_rule("docks", tuple(docks))
|
||||
if obj.set_rule("docks", tuple(docks)):
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class DockProperty:
|
||||
@@ -445,8 +448,8 @@ class DockProperty:
|
||||
obj (Styles): The ``Styles`` object
|
||||
spacing (str | None): The spacing to use.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
obj.set_rule("dock", spacing)
|
||||
if obj.set_rule("dock", spacing):
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class LayoutProperty:
|
||||
@@ -477,14 +480,15 @@ class LayoutProperty:
|
||||
|
||||
from ..layouts.factory import get_layout, Layout # Prevents circular import
|
||||
|
||||
obj.refresh(layout=True)
|
||||
|
||||
if layout is None:
|
||||
obj.clear_rule("layout")
|
||||
if obj.clear_rule("layout"):
|
||||
obj.refresh(layout=True)
|
||||
elif isinstance(layout, Layout):
|
||||
obj.set_rule("layout", layout)
|
||||
if obj.set_rule("layout", layout):
|
||||
obj.refresh(layout=True)
|
||||
else:
|
||||
obj.set_rule("layout", get_layout(layout))
|
||||
if obj.set_rule("layout", get_layout(layout)):
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class OffsetProperty:
|
||||
@@ -525,11 +529,13 @@ class OffsetProperty:
|
||||
ScalarParseError: If any of the string values supplied in the 2-tuple cannot
|
||||
be parsed into a Scalar. For example, if you specify an non-existent unit.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
|
||||
if offset is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh(layout=True)
|
||||
elif isinstance(offset, ScalarOffset):
|
||||
obj.set_rule(self.name, offset)
|
||||
if obj.set_rule(self.name, offset):
|
||||
obj.refresh(layout=True)
|
||||
else:
|
||||
x, y = offset
|
||||
scalar_x = (
|
||||
@@ -543,7 +549,8 @@ class OffsetProperty:
|
||||
else Scalar(float(y), Unit.CELLS, Unit.HEIGHT)
|
||||
)
|
||||
_offset = ScalarOffset(scalar_x, scalar_y)
|
||||
obj.set_rule(self.name, _offset)
|
||||
if obj.set_rule(self.name, _offset):
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class StringEnumProperty:
|
||||
@@ -580,15 +587,17 @@ class StringEnumProperty:
|
||||
Raises:
|
||||
StyleValueError: If the value is not in the set of valid values.
|
||||
"""
|
||||
obj.refresh()
|
||||
|
||||
if value is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh()
|
||||
else:
|
||||
if value not in self._valid_values:
|
||||
raise StyleValueError(
|
||||
f"{self.name} must be one of {friendly_list(self._valid_values)}"
|
||||
)
|
||||
obj.set_rule(self.name, value)
|
||||
if obj.set_rule(self.name, value):
|
||||
obj.refresh()
|
||||
|
||||
|
||||
class NameProperty:
|
||||
@@ -619,13 +628,15 @@ class NameProperty:
|
||||
Raises:
|
||||
StyleTypeError: If the value is not a ``str``.
|
||||
"""
|
||||
obj.refresh(layout=True)
|
||||
|
||||
if name is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh(layout=True)
|
||||
else:
|
||||
if not isinstance(name, str):
|
||||
raise StyleTypeError(f"{self.name} must be a str")
|
||||
obj.set_rule(self.name, name)
|
||||
if obj.set_rule(self.name, name):
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class NameListProperty:
|
||||
@@ -640,15 +651,18 @@ class NameListProperty:
|
||||
def __set__(
|
||||
self, obj: Styles, names: str | tuple[str] | None = None
|
||||
) -> str | tuple[str] | None:
|
||||
obj.refresh(layout=True)
|
||||
|
||||
if names is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh(layout=True)
|
||||
elif isinstance(names, str):
|
||||
obj.set_rule(
|
||||
if obj.set_rule(
|
||||
self.name, tuple(name.strip().lower() for name in names.split(" "))
|
||||
)
|
||||
):
|
||||
obj.refresh(layout=True)
|
||||
elif isinstance(names, tuple):
|
||||
obj.set_rule(self.name, names)
|
||||
if obj.set_rule(self.name, names):
|
||||
obj.refresh(layout=True)
|
||||
|
||||
|
||||
class ColorProperty:
|
||||
@@ -681,13 +695,16 @@ class ColorProperty:
|
||||
Raises:
|
||||
ColorParseError: When the color string is invalid.
|
||||
"""
|
||||
obj.refresh()
|
||||
|
||||
if color is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh()
|
||||
elif isinstance(color, Color):
|
||||
obj.set_rule(self.name, color)
|
||||
if obj.set_rule(self.name, color):
|
||||
obj.refresh()
|
||||
elif isinstance(color, str):
|
||||
obj.set_rule(self.name, Color.parse(color))
|
||||
if obj.set_rule(self.name, Color.parse(color)):
|
||||
obj.refresh()
|
||||
|
||||
|
||||
class StyleFlagsProperty:
|
||||
@@ -722,7 +739,7 @@ class StyleFlagsProperty:
|
||||
"""
|
||||
return obj.get_rule(self.name, Style.null())
|
||||
|
||||
def __set__(self, obj: Styles, style_flags: str | None):
|
||||
def __set__(self, obj: Styles, style_flags: Style | str | None):
|
||||
"""Set the style using a style flag string
|
||||
|
||||
Args:
|
||||
@@ -733,9 +750,12 @@ class StyleFlagsProperty:
|
||||
Raises:
|
||||
StyleValueError: If the value is an invalid style flag
|
||||
"""
|
||||
obj.refresh()
|
||||
if style_flags is None:
|
||||
obj.clear_rule(self.name)
|
||||
if obj.clear_rule(self.name):
|
||||
obj.refresh()
|
||||
elif isinstance(style_flags, Style):
|
||||
if obj.set_rule(self.name, style_flags):
|
||||
obj.refresh()
|
||||
else:
|
||||
words = [word.strip() for word in style_flags.split(" ")]
|
||||
valid_word = self._VALID_PROPERTIES.__contains__
|
||||
@@ -746,7 +766,8 @@ class StyleFlagsProperty:
|
||||
f"valid values are {friendly_list(self._VALID_PROPERTIES)}"
|
||||
)
|
||||
style = Style.parse(style_flags)
|
||||
obj.set_rule(self.name, style)
|
||||
if obj.set_rule(self.name, style):
|
||||
obj.refresh()
|
||||
|
||||
|
||||
class TransitionsProperty:
|
||||
@@ -806,10 +827,10 @@ class FractionalProperty:
|
||||
value (float|str|None): The value to set as a float between 0 and 1, or
|
||||
as a percentage string such as '10%'.
|
||||
"""
|
||||
obj.refresh()
|
||||
name = self.name
|
||||
if value is None:
|
||||
obj.clear_rule(name)
|
||||
if obj.clear_rule(name):
|
||||
obj.refresh()
|
||||
return
|
||||
|
||||
if isinstance(value, float):
|
||||
@@ -820,4 +841,5 @@ class FractionalProperty:
|
||||
raise StyleTypeError(
|
||||
f"{self.name} must be a str (e.g. '10%') or a float (e.g. 0.1)"
|
||||
)
|
||||
obj.set_rule(name, clamp(float_value, 0, 1))
|
||||
if obj.set_rule(name, clamp(float_value, 0, 1)):
|
||||
obj.refresh()
|
||||
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from operator import attrgetter
|
||||
from typing import TYPE_CHECKING, Any, Iterable, NamedTuple, cast
|
||||
|
||||
import rich.repr
|
||||
@@ -94,6 +95,7 @@ class RulesMap(TypedDict, total=False):
|
||||
|
||||
|
||||
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
||||
_rule_getter = attrgetter(*RULE_NAMES)
|
||||
|
||||
|
||||
class DockGroup(NamedTuple):
|
||||
@@ -154,6 +156,12 @@ class StylesBase(ABC):
|
||||
layers = NameListProperty()
|
||||
transitions = TransitionsProperty()
|
||||
|
||||
def __eq__(self, styles: object) -> bool:
|
||||
"""Check that Styles containts the same rules."""
|
||||
if not isinstance(styles, StylesBase):
|
||||
return NotImplemented
|
||||
return self.get_rules() == styles.get_rules()
|
||||
|
||||
@abstractmethod
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
"""Check if a rule is set on this Styles object.
|
||||
@@ -166,11 +174,14 @@ class StylesBase(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def clear_rule(self, rule: str) -> None:
|
||||
def clear_rule(self, rule: str) -> bool:
|
||||
"""Removes the rule from the Styles object, as if it had never been set.
|
||||
|
||||
Args:
|
||||
rule (str): Rule name.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if a rule was cleared, or ``False`` if the rule is already not set.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@@ -182,12 +193,15 @@ class StylesBase(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def set_rule(self, rule: str, value: object | None) -> None:
|
||||
"""Set an individual rule.
|
||||
def set_rule(self, rule: str, value: object | None) -> bool:
|
||||
"""Set a rule.
|
||||
|
||||
Args:
|
||||
rule (str): Name of rule.
|
||||
value (object): Value of rule.
|
||||
rule (str): Rule name.
|
||||
value (object | None): New rule value.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the rule changed, otherwise ``False``.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@@ -203,8 +217,8 @@ class StylesBase(ABC):
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def refresh(self, layout: bool = False) -> None:
|
||||
"""Mark the styles are requiring a refresh.
|
||||
def refresh(self, *, layout: bool = False) -> None:
|
||||
"""Mark the styles as requiring a refresh.
|
||||
|
||||
Args:
|
||||
layout (bool, optional): Also require a layout. Defaults to False.
|
||||
@@ -238,6 +252,12 @@ class StylesBase(ABC):
|
||||
rules (RulesMap): A mapping of rules.
|
||||
"""
|
||||
|
||||
def get_render_rules(self) -> RulesMap:
|
||||
"""Get rules map with defaults."""
|
||||
# Get a dictionary of rules, going through the properties
|
||||
rules = dict(zip(RULE_NAMES, _rule_getter(self)))
|
||||
return cast(RulesMap, rules)
|
||||
|
||||
@classmethod
|
||||
def is_animatable(cls, rule: str) -> bool:
|
||||
"""Check if a given rule may be animated.
|
||||
@@ -289,27 +309,50 @@ class Styles(StylesBase):
|
||||
|
||||
important: set[str] = field(default_factory=set)
|
||||
|
||||
def copy(self) -> Styles:
|
||||
"""Get a copy of this Styles object."""
|
||||
return Styles(node=self.node, _rules=self.get_rules(), important=self.important)
|
||||
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
return rule in self._rules
|
||||
|
||||
def clear_rule(self, rule: str) -> None:
|
||||
self._rules.pop(rule, None)
|
||||
def clear_rule(self, rule: str) -> bool:
|
||||
"""Removes the rule from the Styles object, as if it had never been set.
|
||||
|
||||
Args:
|
||||
rule (str): Rule name.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if a rule was cleared, or ``False`` if it was already not set.
|
||||
"""
|
||||
return self._rules.pop(rule, None) is not None
|
||||
|
||||
def get_rules(self) -> RulesMap:
|
||||
return self._rules.copy()
|
||||
|
||||
def set_rule(self, rule: str, value: object | None) -> None:
|
||||
def set_rule(self, rule: str, value: object | None) -> bool:
|
||||
"""Set a rule.
|
||||
|
||||
Args:
|
||||
rule (str): Rule name.
|
||||
value (object | None): New rule value.
|
||||
|
||||
Returns:
|
||||
bool: ``True`` if the rule changed, otherwise ``False``.
|
||||
"""
|
||||
if value is None:
|
||||
self._rules.pop(rule, None)
|
||||
return self._rules.pop(rule, None) is not None
|
||||
else:
|
||||
current = self._rules.get(rule)
|
||||
self._rules[rule] = value
|
||||
return current != value
|
||||
|
||||
def get_rule(self, rule: str, default: object = None) -> object:
|
||||
return self._rules.get(rule, default)
|
||||
|
||||
def refresh(self, layout: bool = False) -> None:
|
||||
def refresh(self, *, layout: bool = False) -> None:
|
||||
self._repaint_required = True
|
||||
self._layout_required = layout
|
||||
self._layout_required = self._layout_required or layout
|
||||
|
||||
def check_refresh(self) -> tuple[bool, bool]:
|
||||
"""Check if the Styles must be refreshed.
|
||||
@@ -322,9 +365,7 @@ class Styles(StylesBase):
|
||||
return result
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset internal style rules to ``None``, reverting to default styles.
|
||||
"""
|
||||
"""Reset the rules to initial state."""
|
||||
self._rules.clear()
|
||||
|
||||
def merge(self, other: Styles) -> None:
|
||||
@@ -561,11 +602,7 @@ class RenderStyles(StylesBase):
|
||||
if self.has_rule(rule_name):
|
||||
yield rule_name, getattr(self, rule_name)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the inline styles."""
|
||||
self._inline_styles.reset()
|
||||
|
||||
def refresh(self, layout: bool = False) -> None:
|
||||
def refresh(self, *, layout: bool = False) -> None:
|
||||
self._inline_styles.refresh(layout=layout)
|
||||
|
||||
def merge(self, other: Styles) -> None:
|
||||
@@ -590,21 +627,25 @@ class RenderStyles(StylesBase):
|
||||
result = (base_repaint or inline_repaint, base_layout or inline_layout)
|
||||
return result
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the rules to initial state."""
|
||||
self._inline_styles.reset()
|
||||
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
"""Check if a rule has been set."""
|
||||
return self._inline_styles.has_rule(rule) or self._base_styles.has_rule(rule)
|
||||
|
||||
def set_rule(self, rule: str, value: object | None) -> None:
|
||||
self._inline_styles.set_rule(rule, value)
|
||||
def set_rule(self, rule: str, value: object | None) -> bool:
|
||||
return self._inline_styles.set_rule(rule, value)
|
||||
|
||||
def get_rule(self, rule: str, default: object = None) -> object:
|
||||
if self._inline_styles.has_rule(rule):
|
||||
return self._inline_styles.get_rule(rule, default)
|
||||
return self._base_styles.get_rule(rule, default)
|
||||
|
||||
def clear_rule(self, rule_name: str) -> None:
|
||||
def clear_rule(self, rule_name: str) -> bool:
|
||||
"""Clear a rule (from inline)."""
|
||||
self._inline_styles.clear_rule(rule_name)
|
||||
return self._inline_styles.clear_rule(rule_name)
|
||||
|
||||
def get_rules(self) -> RulesMap:
|
||||
"""Get rules as a dictionary"""
|
||||
|
||||
@@ -16,11 +16,12 @@ from rich.syntax import Syntax
|
||||
from rich.text import Text
|
||||
|
||||
from textual._loop import loop_last
|
||||
from .._context import active_app
|
||||
from .errors import StylesheetError
|
||||
from .match import _check_selectors
|
||||
from .model import RuleSet
|
||||
from .parse import parse
|
||||
from .styles import RulesMap
|
||||
from .styles import RULE_NAMES, Styles, RulesMap
|
||||
from .types import Specificity3, Specificity4
|
||||
from ..dom import DOMNode
|
||||
from .. import log
|
||||
@@ -179,47 +180,79 @@ class Stylesheet:
|
||||
},
|
||||
)
|
||||
|
||||
self.apply_rules(node, node_rules, animate=animate)
|
||||
self.replace_rules(node, node_rules, animate=animate)
|
||||
|
||||
@classmethod
|
||||
def apply_rules(cls, node: DOMNode, rules: RulesMap, animate: bool = False) -> None:
|
||||
"""Apply style rules to a node, animating as required.
|
||||
def replace_rules(
|
||||
cls, node: DOMNode, rules: RulesMap, animate: bool = False
|
||||
) -> None:
|
||||
"""Replace style rules on a node, animating as required.
|
||||
|
||||
Args:
|
||||
node (DOMNode): A DOM node.
|
||||
rules (RulesMap): Mapping of rules.
|
||||
animate (bool, optional): Enable animation. Defaults to False.
|
||||
"""
|
||||
|
||||
# Alias styles and base styles
|
||||
styles = node.styles
|
||||
base_styles = styles.base
|
||||
|
||||
# Styles currently used an new rules
|
||||
modified_rule_keys = {*base_styles.get_rules().keys(), *rules.keys()}
|
||||
# Current render rules (missing rules are filled with default)
|
||||
current_render_rules = styles.get_render_rules()
|
||||
|
||||
# Calculate replacement rules (defaults + new rules)
|
||||
new_styles = node._default_styles.copy()
|
||||
new_styles.merge_rules(rules)
|
||||
|
||||
if new_styles == base_styles:
|
||||
# Nothing to change, return early
|
||||
return
|
||||
|
||||
# New render rules
|
||||
new_render_rules = new_styles.get_render_rules()
|
||||
|
||||
# Some aliases
|
||||
is_animatable = styles.is_animatable
|
||||
get_current_render_rule = current_render_rules.get
|
||||
get_new_render_rule = new_render_rules.get
|
||||
|
||||
if animate:
|
||||
for key in modified_rule_keys:
|
||||
# Get old and new render rules
|
||||
old_render_value = get_current_render_rule(key)
|
||||
new_render_value = get_new_render_rule(key)
|
||||
# Get new rule value (may be None)
|
||||
new_value = rules.get(key)
|
||||
|
||||
is_animatable = styles.is_animatable
|
||||
current_rules = styles.get_rules()
|
||||
set_rule = styles.base.set_rule
|
||||
|
||||
for key, value in rules.items():
|
||||
current = current_rules.get(key)
|
||||
if current == value:
|
||||
continue
|
||||
if is_animatable(key):
|
||||
transition = styles.get_transition(key)
|
||||
if transition is None:
|
||||
styles.base.set_rule(key, value)
|
||||
else:
|
||||
# Check if this can / should be animated
|
||||
if is_animatable(key) and new_render_value != old_render_value:
|
||||
transition = new_styles.get_transition(key)
|
||||
if transition is not None:
|
||||
duration, easing, delay = transition
|
||||
node.app.animator.animate(
|
||||
node.styles.base,
|
||||
key,
|
||||
value,
|
||||
new_render_value,
|
||||
final_value=new_value,
|
||||
duration=duration,
|
||||
easing=easing,
|
||||
)
|
||||
else:
|
||||
set_rule(key, value)
|
||||
continue
|
||||
# Default is to set value (if new_value is None, rule will be removed)
|
||||
setattr(base_styles, key, new_value)
|
||||
else:
|
||||
styles.base.merge_rules(rules)
|
||||
# Not animated, so we apply the rules directly
|
||||
for key in modified_rule_keys:
|
||||
setattr(base_styles, key, rules.get(key))
|
||||
|
||||
node.on_style_change()
|
||||
# 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:
|
||||
"""Update a node and its children."""
|
||||
|
||||
@@ -45,8 +45,8 @@ class DOMNode(MessagePump):
|
||||
self.INLINE_STYLES, repr(self), node=self
|
||||
)
|
||||
self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
|
||||
default_styles = Styles.parse(self.DEFAULT_STYLES, repr(self))
|
||||
self._default_rules = default_styles.extract_rules((0, 0, 0))
|
||||
self._default_styles = Styles.parse(self.DEFAULT_STYLES, repr(self))
|
||||
self._default_rules = self._default_styles.extract_rules((0, 0, 0))
|
||||
super().__init__()
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
|
||||
@@ -12,6 +12,7 @@ from rich.control import Control
|
||||
from rich.segment import Segment, SegmentLines
|
||||
from rich.style import Style
|
||||
|
||||
from . import log
|
||||
from ._loop import loop_last
|
||||
from ._types import Lines
|
||||
from .geometry import Region, Offset, Size
|
||||
@@ -425,4 +426,5 @@ class Layout(ABC):
|
||||
update_region = region.intersection(clip)
|
||||
update_lines = self.render(console, crop=update_region).lines
|
||||
update = LayoutUpdate(update_lines, update_region)
|
||||
log(update)
|
||||
return update
|
||||
|
||||
Reference in New Issue
Block a user