color animation
This commit is contained in:
parent
736a56182c
commit
5508ece2e3
15 changed files with 1015 additions and 324 deletions
|
|
@ -5,15 +5,12 @@ $primary: #20639b;
|
|||
App > Screen {
|
||||
layout: dock;
|
||||
docks: side=left/1;
|
||||
text: on $primary;
|
||||
}
|
||||
|
||||
Widget:hover {
|
||||
outline: solid green;
|
||||
background: $primary;
|
||||
}
|
||||
|
||||
#sidebar {
|
||||
text: #09312e on #3caea3;
|
||||
color: #09312e;
|
||||
background: #3caea3;
|
||||
dock: side;
|
||||
width: 30;
|
||||
offset-x: -100%;
|
||||
|
|
@ -27,17 +24,21 @@ Widget:hover {
|
|||
}
|
||||
|
||||
#header {
|
||||
text: white on #173f5f;
|
||||
color: white;
|
||||
background: #173f5f;
|
||||
height: 3;
|
||||
border: hkey;
|
||||
border: hkey white;
|
||||
}
|
||||
|
||||
#content {
|
||||
text: white on $primary;
|
||||
color: white;
|
||||
background: $primary;
|
||||
border-bottom: hkey #0f2b41;
|
||||
}
|
||||
|
||||
#footer {
|
||||
text: #3a3009 on #f6d55c;
|
||||
color: #3a3009;
|
||||
background: #f6d55c;
|
||||
|
||||
height: 3;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#uber1 {
|
||||
layout: vertical;
|
||||
|
||||
text-background: dark_green;
|
||||
|
||||
background: dark_green;
|
||||
overflow: hidden auto;
|
||||
border: heavy white;
|
||||
}
|
||||
|
|
@ -9,5 +9,5 @@
|
|||
.list-item {
|
||||
height: 8;
|
||||
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
|
||||
|
||||
animation: Animation
|
||||
animation: Animation | None = None
|
||||
if hasattr(obj, "__textual_animation__"):
|
||||
animation = getattr(obj, "__textual_animation__")(
|
||||
attribute,
|
||||
|
|
@ -196,7 +196,7 @@ class Animator:
|
|||
speed=speed,
|
||||
easing=easing_function,
|
||||
)
|
||||
else:
|
||||
if animation is None:
|
||||
start_value = getattr(obj, attribute)
|
||||
|
||||
if start_value == value:
|
||||
|
|
|
|||
|
|
@ -720,4 +720,4 @@ class App(DOMNode):
|
|||
self.screen.query(selector).toggle_class(class_name)
|
||||
|
||||
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
|
||||
from typing import NamedTuple
|
||||
|
||||
from .geometry import clamp
|
||||
import rich.repr
|
||||
from rich.color import Color as RichColor
|
||||
from rich.style import Style
|
||||
|
||||
from . import log
|
||||
from .geometry import clamp
|
||||
|
||||
|
||||
ANSI_COLOR_NAMES = {
|
||||
"black": 0,
|
||||
"red": 1,
|
||||
|
|
@ -534,16 +537,29 @@ class Color(NamedTuple):
|
|||
|
||||
@classmethod
|
||||
def from_rich_color(cls, rich_color: RichColor) -> Color:
|
||||
"""Create color from Rich's color class."""
|
||||
r, g, b = rich_color.get_truecolor()
|
||||
return cls(r, g, b)
|
||||
|
||||
@property
|
||||
def rich_color(self) -> RichColor:
|
||||
"""This color encoded in Rich's Color class."""
|
||||
r, g, b, _a = self
|
||||
return RichColor.from_rgb(r, g, b)
|
||||
|
||||
@property
|
||||
def is_transparent(self) -> bool:
|
||||
"""Check if the color is transparent."""
|
||||
return self.a == 0
|
||||
|
||||
@property
|
||||
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
|
||||
return (
|
||||
f"#{r:02X}{g:02X}{b:02X}"
|
||||
|
|
@ -553,6 +569,12 @@ class Color(NamedTuple):
|
|||
|
||||
@property
|
||||
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
|
||||
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 "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
|
||||
@lru_cache(maxsize=1024 * 4)
|
||||
def parse(cls, color_text: str) -> Color:
|
||||
|
|
@ -587,7 +628,10 @@ class Color(NamedTuple):
|
|||
|
||||
if rgb_hex is not None:
|
||||
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:
|
||||
color = cls(
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import rich.repr
|
|||
from rich.style import Style
|
||||
|
||||
from .. import log
|
||||
from ..color import Color
|
||||
from ..color import Color, ColorPair
|
||||
from ._error_tools import friendly_list
|
||||
from .constants import NULL_SPACING
|
||||
from .errors import StyleTypeError, StyleValueError
|
||||
|
|
@ -316,46 +316,9 @@ class StyleProperty:
|
|||
Returns:
|
||||
A ``Style`` object.
|
||||
"""
|
||||
has_rule = obj.has_rule
|
||||
|
||||
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
|
||||
|
||||
style = ColorPair(obj.color, obj.background).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:
|
||||
"""Descriptor for getting and setting spacing properties (e.g. padding and margin)."""
|
||||
|
|
@ -660,9 +623,7 @@ class NameListProperty:
|
|||
) -> tuple[str, ...]:
|
||||
return obj.get_rule(self.name, ())
|
||||
|
||||
def __set__(
|
||||
self, obj: StylesBase, names: str | tuple[str] | None = None
|
||||
) -> str | tuple[str] | None:
|
||||
def __set__(self, obj: StylesBase, names: str | tuple[str] | None = None):
|
||||
|
||||
if names is None:
|
||||
if obj.clear_rule(self.name):
|
||||
|
|
@ -687,7 +648,7 @@ class ColorProperty:
|
|||
self.name = name
|
||||
|
||||
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:
|
||||
obj (Styles): The ``Styles`` object.
|
||||
|
|
@ -696,7 +657,7 @@ class ColorProperty:
|
|||
Returns:
|
||||
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):
|
||||
"""Set the Color
|
||||
|
|
|
|||
|
|
@ -459,36 +459,34 @@ class StylesBuilder:
|
|||
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:
|
||||
style_definition = _join_tokens(tokens, joiner=" ")
|
||||
# def process_text(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
# style_definition = _join_tokens(tokens, joiner=" ")
|
||||
|
||||
# If every token in the value is a referenced by the same variable,
|
||||
# we can display the variable name before the style definition.
|
||||
# TODO: Factor this out to apply it to other properties too.
|
||||
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:
|
||||
variable_prefix = f"${tokens[0].referenced_by.name}="
|
||||
else:
|
||||
variable_prefix = ""
|
||||
# # If every token in the value is a referenced by the same variable,
|
||||
# # we can display the variable name before the style definition.
|
||||
# # TODO: Factor this out to apply it to other properties too.
|
||||
# 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:
|
||||
# variable_prefix = f"${tokens[0].referenced_by.name}="
|
||||
# else:
|
||||
# variable_prefix = ""
|
||||
|
||||
try:
|
||||
style = Style.parse(style_definition)
|
||||
self.styles.text = style
|
||||
except Exception as error:
|
||||
message = f"property 'text' has invalid value {variable_prefix}{style_definition!r}; {error}"
|
||||
self.error(name, tokens[0], message)
|
||||
if important:
|
||||
self.styles.important.update(
|
||||
{"text_style", "text_background", "text_color"}
|
||||
)
|
||||
# try:
|
||||
# style = Style.parse(style_definition)
|
||||
# self.styles.text = style
|
||||
# except Exception as error:
|
||||
# message = f"property 'text' has invalid value {variable_prefix}{style_definition!r}; {error}"
|
||||
# self.error(name, tokens[0], message)
|
||||
# if important:
|
||||
# self.styles.important.update(
|
||||
# {"text_style", "text_background", "text_color"}
|
||||
# )
|
||||
|
||||
def process_text_color(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
def process_color(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rules["text_color"] = Color.parse(token.value)
|
||||
self.styles._rules["color"] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.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"
|
||||
)
|
||||
|
||||
def process_text_background(
|
||||
def process_background(
|
||||
self, name: str, tokens: list[Token], important: bool
|
||||
) -> None:
|
||||
for token in tokens:
|
||||
if token.name in ("color", "token"):
|
||||
try:
|
||||
self.styles._rules["text_background"] = Color.parse(token.value)
|
||||
self.styles._rules["background"] = Color.parse(token.value)
|
||||
except Exception as error:
|
||||
self.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.
|
||||
|
||||
Does not define composite rules, that is a rule that is made of a combination of other rules. For instance,
|
||||
the text style is made up of text_color, text_background, and text_style.
|
||||
Does not define composite rules, that is a rule that is made of a combination of other rules.
|
||||
|
||||
"""
|
||||
|
||||
display: Display
|
||||
visibility: Visibility
|
||||
layout: "Layout"
|
||||
|
||||
text_color: Color
|
||||
text_background: Color
|
||||
color: Color
|
||||
background: Color
|
||||
text_style: Style
|
||||
|
||||
opacity: float
|
||||
|
|
@ -132,6 +132,8 @@ class StylesBase(ABC):
|
|||
"min_height",
|
||||
"max_width",
|
||||
"max_height",
|
||||
"color",
|
||||
"background",
|
||||
}
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
|
|
@ -139,8 +141,8 @@ class StylesBase(ABC):
|
|||
layout = LayoutProperty()
|
||||
|
||||
text = StyleProperty()
|
||||
text_color = ColorProperty(Color(255, 255, 255))
|
||||
text_background = ColorProperty(Color(255, 255, 255))
|
||||
color = ColorProperty(Color(255, 255, 255))
|
||||
background = ColorProperty(Color(255, 255, 255))
|
||||
text_style = StyleFlagsProperty()
|
||||
|
||||
opacity = FractionalProperty()
|
||||
|
|
@ -257,14 +259,6 @@ class StylesBase(ABC):
|
|||
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
|
||||
def reset(self) -> None:
|
||||
"""Reset the rules to initial state."""
|
||||
|
|
@ -402,9 +396,6 @@ class Styles(StylesBase):
|
|||
|
||||
_rules: RulesMap = field(default_factory=dict)
|
||||
|
||||
_layout_required: bool = False
|
||||
_repaint_required: bool = False
|
||||
|
||||
important: set[str] = field(default_factory=set)
|
||||
|
||||
def copy(self) -> Styles:
|
||||
|
|
@ -449,18 +440,8 @@ class Styles(StylesBase):
|
|||
return self._rules.get(rule, default)
|
||||
|
||||
def refresh(self, *, layout: bool = False) -> None:
|
||||
self._repaint_required = True
|
||||
self._layout_required = self._layout_required or 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
|
||||
if self.node is not None:
|
||||
self.node.refresh(layout=layout)
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the rules to initial state."""
|
||||
|
|
@ -637,19 +618,12 @@ class Styles(StylesBase):
|
|||
assert self.layout is not None
|
||||
append_declaration("layout", self.layout.name)
|
||||
|
||||
if (
|
||||
has_rule("text_color")
|
||||
and has_rule("text_background")
|
||||
and has_rule("text_style")
|
||||
):
|
||||
append_declaration("text", str(self.text))
|
||||
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("color"):
|
||||
append_declaration("color", self.color.hex)
|
||||
if has_rule("background"):
|
||||
append_declaration("background", self.background.hex)
|
||||
if has_rule("text_style"):
|
||||
append_declaration("text-style", str(get_rule("text_style")))
|
||||
|
||||
if has_rule("overflow-x"):
|
||||
append_declaration("overflow-x", self.overflow_x)
|
||||
|
|
@ -725,17 +699,6 @@ class RenderStyles(StylesBase):
|
|||
def merge_rules(self, rules: RulesMap) -> None:
|
||||
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:
|
||||
"""Reset the rules to initial state."""
|
||||
self._inline_styles.reset()
|
||||
|
|
|
|||
|
|
@ -248,12 +248,6 @@ class Stylesheet:
|
|||
for key in modified_rule_keys:
|
||||
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:
|
||||
"""Update a node and its children."""
|
||||
apply = self.apply
|
||||
|
|
|
|||
|
|
@ -314,8 +314,7 @@ class DOMNode(MessagePump):
|
|||
for node in self.walk_children():
|
||||
node._css_styles.reset()
|
||||
if isinstance(node, Widget):
|
||||
# node.clear_render_cache()
|
||||
node._repaint_required = True
|
||||
node.set_dirty()
|
||||
node._layout_required = True
|
||||
|
||||
def on_style_change(self) -> None:
|
||||
|
|
|
|||
|
|
@ -495,11 +495,11 @@ class Region(NamedTuple):
|
|||
cut_x ↓
|
||||
┌────────┐┌───┐
|
||||
│ ││ │
|
||||
│ ││ │
|
||||
│ 0 ││ 1 │
|
||||
│ ││ │
|
||||
cut_y → └────────┘└───┘
|
||||
┌────────┐┌───┐
|
||||
│ ││ │
|
||||
│ 2 ││ 3 │
|
||||
└────────┘└───┘
|
||||
|
||||
Args:
|
||||
|
|
@ -531,7 +531,7 @@ class Region(NamedTuple):
|
|||
|
||||
cut ↓
|
||||
┌────────┐┌───┐
|
||||
│ ││ │
|
||||
│ 0 ││ 1 │
|
||||
│ ││ │
|
||||
└────────┘└───┘
|
||||
|
||||
|
|
@ -556,10 +556,11 @@ class Region(NamedTuple):
|
|||
"""Split a region in to two, from a given x offset.
|
||||
|
||||
┌─────────┐
|
||||
│ │
|
||||
│ 0 │
|
||||
│ │
|
||||
cut → └─────────┘
|
||||
┌─────────┐
|
||||
│ 1 │
|
||||
└─────────┘
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -134,6 +134,7 @@ class Screen(Widget):
|
|||
widget = message.widget
|
||||
assert isinstance(widget, Widget)
|
||||
self._dirty_widgets.append(widget)
|
||||
self.check_idle()
|
||||
|
||||
async def handle_layout(self, message: messages.Layout) -> None:
|
||||
message.stop()
|
||||
|
|
@ -204,10 +205,8 @@ class Screen(Widget):
|
|||
widget, _region = self.get_widget_at(event.x, event.y)
|
||||
except errors.NoWidget:
|
||||
return
|
||||
self.log("forward", widget, event)
|
||||
scroll_widget = widget
|
||||
if scroll_widget is not None:
|
||||
await scroll_widget.forward_event(event)
|
||||
else:
|
||||
self.log("view.forwarded", event)
|
||||
await self.post_message(event)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ class Widget(DOMNode):
|
|||
self._layout_required = False
|
||||
self._animate: BoundAnimator | None = None
|
||||
self._reactive_watches: dict[str, Callable] = {}
|
||||
self._mouse_over: bool = False
|
||||
self.highlight_style: Style | None = None
|
||||
|
||||
self._vertical_scrollbar: ScrollBar | None = None
|
||||
|
|
@ -356,7 +355,7 @@ class Widget(DOMNode):
|
|||
|
||||
def get_pseudo_classes(self) -> Iterable[str]:
|
||||
"""Pseudo classes for a widget"""
|
||||
if self._mouse_over:
|
||||
if self.mouse_over:
|
||||
yield "hover"
|
||||
if self.has_focus:
|
||||
yield "focus"
|
||||
|
|
@ -472,6 +471,7 @@ class Widget(DOMNode):
|
|||
|
||||
def on_style_change(self) -> None:
|
||||
self.set_dirty()
|
||||
self.check_idle()
|
||||
|
||||
def size_updated(
|
||||
self, size: Size, virtual_size: Size, container_size: Size
|
||||
|
|
@ -575,15 +575,11 @@ class Widget(DOMNode):
|
|||
Args:
|
||||
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.screen.post_message_no_wait(messages.Layout(self))
|
||||
elif repaint:
|
||||
elif self._dirty_regions:
|
||||
self.emit_no_wait(messages.Update(self, self))
|
||||
|
||||
async def focus(self) -> None:
|
||||
|
|
@ -622,6 +618,12 @@ class Widget(DOMNode):
|
|||
async def on_key(self, event: events.Key) -> None:
|
||||
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:
|
||||
self.scroll_down(animate=True)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,8 @@
|
|||
import pytest
|
||||
from rich.color import Color
|
||||
|
||||
from rich.style import Style
|
||||
|
||||
from textual.color import Color
|
||||
from textual.css.errors import StyleTypeError
|
||||
from textual.css.styles import Styles, RenderStyles
|
||||
from textual.dom import DOMNode
|
||||
|
|
@ -93,20 +94,20 @@ def test_render_styles_text():
|
|||
assert styles_view.text == Style()
|
||||
|
||||
# Base is bold blue
|
||||
base.text_color = "blue"
|
||||
base.color = "blue"
|
||||
base.text_style = "bold"
|
||||
assert styles_view.text == Style.parse("bold blue")
|
||||
|
||||
# Base is bold blue, inline is red
|
||||
inline.text_color = "red"
|
||||
inline.color = "red"
|
||||
assert styles_view.text == Style.parse("bold red")
|
||||
|
||||
# Base is bold yellow, inline is red
|
||||
base.text_color = "yellow"
|
||||
base.color = "yellow"
|
||||
assert styles_view.text == Style.parse("bold red")
|
||||
|
||||
# Base is bold blue
|
||||
inline.text_color = None
|
||||
inline.color = None
|
||||
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 == (
|
||||
("heavy", Color.parse("red")),
|
||||
("", Color.default()),
|
||||
("", Color.default()),
|
||||
("", Color(0, 255, 0)),
|
||||
("", Color(0, 255, 0)),
|
||||
("rounded", Color.parse("green")),
|
||||
)
|
||||
|
||||
|
||||
def test_get_opacity_default():
|
||||
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
||||
assert styles.opacity == 1.
|
||||
assert styles.opacity == 1.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("set_value, expected", [
|
||||
[0.2, 0.2],
|
||||
[-0.4, 0.0],
|
||||
[5.8, 1.0],
|
||||
["25%", 0.25],
|
||||
["-10%", 0.0],
|
||||
["120%", 1.0],
|
||||
])
|
||||
@pytest.mark.parametrize(
|
||||
"set_value, expected",
|
||||
[
|
||||
[0.2, 0.2],
|
||||
[-0.4, 0.0],
|
||||
[5.8, 1.0],
|
||||
["25%", 0.25],
|
||||
["-10%", 0.0],
|
||||
["120%", 1.0],
|
||||
],
|
||||
)
|
||||
def test_opacity_set_then_get(set_value, expected):
|
||||
styles = RenderStyles(DOMNode(), Styles(), Styles())
|
||||
styles.opacity = set_value
|
||||
|
|
|
|||
Loading…
Reference in a new issue