1497 lines
50 KiB
Python
1497 lines
50 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from functools import partial
|
|
from operator import attrgetter
|
|
from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, Literal, cast
|
|
|
|
import rich.repr
|
|
from rich.style import Style
|
|
from typing_extensions import TypedDict
|
|
|
|
from textual._animator import DEFAULT_EASING, Animatable, BoundAnimator, EasingFunction
|
|
from textual._types import AnimationLevel, CallbackType
|
|
from textual.color import Color
|
|
from textual.css._style_properties import (
|
|
AlignProperty,
|
|
BooleanProperty,
|
|
BorderProperty,
|
|
BoxProperty,
|
|
ColorProperty,
|
|
DockProperty,
|
|
FractionalProperty,
|
|
HatchProperty,
|
|
IntegerProperty,
|
|
KeylineProperty,
|
|
LayoutProperty,
|
|
NameListProperty,
|
|
NameProperty,
|
|
OffsetProperty,
|
|
OverflowProperty,
|
|
ScalarListProperty,
|
|
ScalarProperty,
|
|
ScrollbarColorProperty,
|
|
SpacingProperty,
|
|
SplitProperty,
|
|
StringEnumProperty,
|
|
StyleFlagsProperty,
|
|
TransitionsProperty,
|
|
)
|
|
from textual.css.constants import (
|
|
VALID_ALIGN_HORIZONTAL,
|
|
VALID_ALIGN_VERTICAL,
|
|
VALID_BOX_SIZING,
|
|
VALID_CONSTRAIN,
|
|
VALID_DISPLAY,
|
|
VALID_EXPAND,
|
|
VALID_OVERFLOW,
|
|
VALID_OVERLAY,
|
|
VALID_POSITION,
|
|
VALID_SCROLLBAR_GUTTER,
|
|
VALID_SCROLLBAR_VISIBILITY,
|
|
VALID_TEXT_ALIGN,
|
|
VALID_TEXT_OVERFLOW,
|
|
VALID_TEXT_WRAP,
|
|
VALID_VISIBILITY,
|
|
)
|
|
from textual.css.scalar import Scalar, ScalarOffset, Unit
|
|
from textual.css.scalar_animation import ScalarAnimation
|
|
from textual.css.transition import Transition
|
|
from textual.css.types import (
|
|
AlignHorizontal,
|
|
AlignVertical,
|
|
BoxSizing,
|
|
Constrain,
|
|
Display,
|
|
Expand,
|
|
Overflow,
|
|
Overlay,
|
|
ScrollbarGutter,
|
|
Specificity3,
|
|
Specificity6,
|
|
TextAlign,
|
|
TextOverflow,
|
|
TextWrap,
|
|
Visibility,
|
|
)
|
|
from textual.geometry import Offset, Spacing
|
|
|
|
if TYPE_CHECKING:
|
|
from textual.css.types import CSSLocation
|
|
from textual.dom import DOMNode
|
|
from textual.layout import Layout
|
|
|
|
|
|
class RulesMap(TypedDict, total=False):
|
|
"""A typed dict for CSS rules.
|
|
|
|
Any key may be absent, indicating that rule has not been set.
|
|
|
|
Does not define composite rules, that is a rule that is made of a combination of other rules.
|
|
"""
|
|
|
|
display: Display
|
|
visibility: Visibility
|
|
layout: "Layout"
|
|
|
|
auto_color: bool
|
|
color: Color
|
|
background: Color
|
|
text_style: Style
|
|
|
|
background_tint: Color
|
|
|
|
opacity: float
|
|
text_opacity: float
|
|
|
|
padding: Spacing
|
|
margin: Spacing
|
|
offset: ScalarOffset
|
|
position: str
|
|
|
|
border_top: tuple[str, Color]
|
|
border_right: tuple[str, Color]
|
|
border_bottom: tuple[str, Color]
|
|
border_left: tuple[str, Color]
|
|
|
|
border_title_align: AlignHorizontal
|
|
border_subtitle_align: AlignHorizontal
|
|
|
|
outline_top: tuple[str, Color]
|
|
outline_right: tuple[str, Color]
|
|
outline_bottom: tuple[str, Color]
|
|
outline_left: tuple[str, Color]
|
|
|
|
keyline: tuple[str, Color]
|
|
|
|
box_sizing: BoxSizing
|
|
width: Scalar
|
|
height: Scalar
|
|
min_width: Scalar
|
|
min_height: Scalar
|
|
max_width: Scalar
|
|
max_height: Scalar
|
|
|
|
dock: str
|
|
split: str
|
|
|
|
overflow_x: Overflow
|
|
overflow_y: Overflow
|
|
|
|
layers: tuple[str, ...]
|
|
layer: str
|
|
|
|
transitions: dict[str, Transition]
|
|
|
|
tint: Color
|
|
|
|
scrollbar_color: Color
|
|
scrollbar_color_hover: Color
|
|
scrollbar_color_active: Color
|
|
|
|
scrollbar_corner_color: Color
|
|
|
|
scrollbar_background: Color
|
|
scrollbar_background_hover: Color
|
|
scrollbar_background_active: Color
|
|
scrollbar_gutter: ScrollbarGutter
|
|
scrollbar_size_vertical: int
|
|
scrollbar_size_horizontal: int
|
|
scrollbar_visibility: ScrollbarVisibility
|
|
|
|
align_horizontal: AlignHorizontal
|
|
align_vertical: AlignVertical
|
|
|
|
content_align_horizontal: AlignHorizontal
|
|
content_align_vertical: AlignVertical
|
|
|
|
grid_size_rows: int
|
|
grid_size_columns: int
|
|
grid_gutter_horizontal: int
|
|
grid_gutter_vertical: int
|
|
grid_rows: tuple[Scalar, ...]
|
|
grid_columns: tuple[Scalar, ...]
|
|
|
|
row_span: int
|
|
column_span: int
|
|
|
|
text_align: TextAlign
|
|
|
|
link_color: Color
|
|
auto_link_color: bool
|
|
link_background: Color
|
|
link_style: Style
|
|
|
|
link_color_hover: Color
|
|
auto_link_color_hover: bool
|
|
link_background_hover: Color
|
|
link_style_hover: Style
|
|
|
|
auto_border_title_color: bool
|
|
border_title_color: Color
|
|
border_title_background: Color
|
|
border_title_style: Style
|
|
|
|
auto_border_subtitle_color: bool
|
|
border_subtitle_color: Color
|
|
border_subtitle_background: Color
|
|
border_subtitle_style: Style
|
|
|
|
hatch: tuple[str, Color] | Literal["none"]
|
|
|
|
overlay: Overlay
|
|
constrain_x: Constrain
|
|
constrain_y: Constrain
|
|
|
|
text_wrap: TextWrap
|
|
text_overflow: TextOverflow
|
|
expand: Expand
|
|
|
|
line_pad: int
|
|
|
|
|
|
RULE_NAMES = list(RulesMap.__annotations__.keys())
|
|
RULE_NAMES_SET = frozenset(RULE_NAMES)
|
|
_rule_getter = attrgetter(*RULE_NAMES)
|
|
|
|
|
|
class StylesBase:
|
|
"""A common base class for Styles and RenderStyles"""
|
|
|
|
ANIMATABLE = {
|
|
"offset",
|
|
"padding",
|
|
"margin",
|
|
"width",
|
|
"height",
|
|
"min_width",
|
|
"min_height",
|
|
"max_width",
|
|
"max_height",
|
|
"auto_color",
|
|
"color",
|
|
"background",
|
|
"background_tint",
|
|
"opacity",
|
|
"position",
|
|
"text_opacity",
|
|
"tint",
|
|
"scrollbar_color",
|
|
"scrollbar_color_hover",
|
|
"scrollbar_color_active",
|
|
"scrollbar_background",
|
|
"scrollbar_background_hover",
|
|
"scrollbar_background_active",
|
|
"scrollbar_visibility",
|
|
"link_color",
|
|
"link_background",
|
|
"link_color_hover",
|
|
"link_background_hover",
|
|
"text_wrap",
|
|
"text_overflow",
|
|
"line_pad",
|
|
}
|
|
|
|
node: DOMNode | None = None
|
|
|
|
display = StringEnumProperty(VALID_DISPLAY, "block", layout=True, display=True)
|
|
"""Set the display of the widget, defining how it's rendered.
|
|
|
|
Valid values are "block" or "none".
|
|
|
|
"none" will hide and allow other widgets to fill the space that this widget would occupy.
|
|
|
|
Set to None to clear any value that was set at runtime.
|
|
|
|
Raises:
|
|
StyleValueError: If an invalid display is specified.
|
|
"""
|
|
|
|
visibility = StringEnumProperty(VALID_VISIBILITY, "visible", layout=True)
|
|
"""Set the visibility of the widget.
|
|
|
|
Valid values are "visible" or "hidden".
|
|
|
|
"hidden" will hide the widget, but reserve the space for this widget.
|
|
If you want to hide the widget and allow another widget to fill the space,
|
|
set the display attribute to "none" instead.
|
|
|
|
Set to None to clear any value that was set at runtime.
|
|
|
|
Raises:
|
|
StyleValueError: If an invalid visibility is specified.
|
|
"""
|
|
|
|
layout = LayoutProperty()
|
|
"""Set the layout of the widget, defining how its children are laid out.
|
|
|
|
Valid values are "grid", "stream", "horizontal", or "vertical" or None to clear any layout
|
|
that was set at runtime.
|
|
|
|
Raises:
|
|
MissingLayout: If an invalid layout is specified.
|
|
"""
|
|
|
|
auto_color = BooleanProperty(default=False)
|
|
"""Enable automatic picking of best contrasting color."""
|
|
color = ColorProperty(Color(255, 255, 255))
|
|
"""Set the foreground (text) color of the widget.
|
|
Supports `Color` objects but also strings e.g. "red" or "#ff0000".
|
|
You can also specify an opacity after a color e.g. "blue 10%"
|
|
"""
|
|
background = ColorProperty(Color(0, 0, 0, 0))
|
|
"""Set the background color of the widget.
|
|
Supports `Color` objects but also strings e.g. "red" or "#ff0000"
|
|
You can also specify an opacity after a color e.g. "blue 10%"
|
|
"""
|
|
background_tint = ColorProperty(Color(0, 0, 0, 0))
|
|
"""Set a color to tint (blend) with the background.
|
|
Supports `Color` objects but also strings e.g. "red" or "#ff0000"
|
|
You can also specify an opacity after a color e.g. "blue 10%"
|
|
"""
|
|
text_style = StyleFlagsProperty()
|
|
"""Set the text style of the widget using Rich StyleFlags.
|
|
e.g. `"bold underline"` or `"b u strikethrough"`.
|
|
"""
|
|
opacity = FractionalProperty(children=True)
|
|
"""Set the opacity of the widget, defining how it blends with the parent."""
|
|
text_opacity = FractionalProperty()
|
|
"""Set the opacity of the content within the widget against the widget's background."""
|
|
padding = SpacingProperty()
|
|
"""Set the padding (spacing between border and content) of the widget."""
|
|
margin = SpacingProperty()
|
|
"""Set the margin (spacing outside the border) of the widget."""
|
|
offset = OffsetProperty()
|
|
"""Set the offset of the widget relative to where it would have been otherwise."""
|
|
position = StringEnumProperty(VALID_POSITION, "relative")
|
|
"""If `relative` offset is applied to widgets current position, if `absolute` it is applied to (0, 0)."""
|
|
|
|
border = BorderProperty(layout=True)
|
|
"""Set the border of the widget e.g. ("round", "green") or "none"."""
|
|
|
|
border_top = BoxProperty(Color(0, 255, 0))
|
|
"""Set the top border of the widget e.g. ("round", "green") or "none"."""
|
|
border_right = BoxProperty(Color(0, 255, 0))
|
|
"""Set the right border of the widget e.g. ("round", "green") or "none"."""
|
|
border_bottom = BoxProperty(Color(0, 255, 0))
|
|
"""Set the bottom border of the widget e.g. ("round", "green") or "none"."""
|
|
border_left = BoxProperty(Color(0, 255, 0))
|
|
"""Set the left border of the widget e.g. ("round", "green") or "none"."""
|
|
|
|
border_title_align = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
|
|
"""The alignment of the border title text."""
|
|
border_subtitle_align = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "right")
|
|
"""The alignment of the border subtitle text."""
|
|
|
|
outline = BorderProperty(layout=False)
|
|
"""Set the outline of the widget e.g. ("round", "green") or "none".
|
|
The outline is drawn *on top* of the widget, rather than around it like border.
|
|
"""
|
|
outline_top = BoxProperty(Color(0, 255, 0))
|
|
"""Set the top outline of the widget e.g. ("round", "green") or "none"."""
|
|
outline_right = BoxProperty(Color(0, 255, 0))
|
|
"""Set the right outline of the widget e.g. ("round", "green") or "none"."""
|
|
outline_bottom = BoxProperty(Color(0, 255, 0))
|
|
"""Set the bottom outline of the widget e.g. ("round", "green") or "none"."""
|
|
outline_left = BoxProperty(Color(0, 255, 0))
|
|
"""Set the left outline of the widget e.g. ("round", "green") or "none"."""
|
|
|
|
keyline = KeylineProperty()
|
|
"""Keyline parameters."""
|
|
|
|
box_sizing = StringEnumProperty(VALID_BOX_SIZING, "border-box", layout=True)
|
|
"""Box sizing method ("border-box" or "conetnt-box")"""
|
|
width = ScalarProperty(percent_unit=Unit.WIDTH)
|
|
"""Set the width of the widget."""
|
|
height = ScalarProperty(percent_unit=Unit.HEIGHT)
|
|
"""Set the height of the widget."""
|
|
min_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False)
|
|
"""Set the minimum width of the widget."""
|
|
min_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False)
|
|
"""Set the minimum height of the widget."""
|
|
max_width = ScalarProperty(percent_unit=Unit.WIDTH, allow_auto=False)
|
|
"""Set the maximum width of the widget."""
|
|
max_height = ScalarProperty(percent_unit=Unit.HEIGHT, allow_auto=False)
|
|
"""Set the maximum height of the widget."""
|
|
dock = DockProperty()
|
|
"""Set which edge of the parent to dock this widget to e.g. "top", "left", "right", "bottom", "none".
|
|
"""
|
|
split = SplitProperty()
|
|
|
|
overflow_x = OverflowProperty(VALID_OVERFLOW, "hidden")
|
|
"""Control what happens when the content extends horizontally beyond the widget's width.
|
|
|
|
Valid values are "scroll", "hidden", or "auto".
|
|
"""
|
|
|
|
overflow_y = OverflowProperty(VALID_OVERFLOW, "hidden")
|
|
"""Control what happens when the content extends vertically beyond the widget's height.
|
|
|
|
Valid values are "scroll", "hidden", or "auto".
|
|
"""
|
|
|
|
layer = NameProperty()
|
|
layers = NameListProperty()
|
|
transitions = TransitionsProperty()
|
|
|
|
tint = ColorProperty("transparent")
|
|
"""Set the tint of the widget. This allows you apply an opaque color above the widget.
|
|
|
|
You can specify an opacity after a color e.g. "blue 10%"
|
|
"""
|
|
scrollbar_color = ScrollbarColorProperty("ansi_bright_magenta")
|
|
"""Set the color of the handle of the scrollbar."""
|
|
scrollbar_color_hover = ScrollbarColorProperty("ansi_yellow")
|
|
"""Set the color of the handle of the scrollbar when hovered."""
|
|
scrollbar_color_active = ScrollbarColorProperty("ansi_bright_yellow")
|
|
"""Set the color of the handle of the scrollbar when active (being dragged)."""
|
|
scrollbar_corner_color = ScrollbarColorProperty("#666666")
|
|
"""Set the color of the space between the horizontal and vertical scrollbars."""
|
|
scrollbar_background = ScrollbarColorProperty("#555555")
|
|
"""Set the background color of the scrollbar (the track that the handle sits on)."""
|
|
scrollbar_background_hover = ScrollbarColorProperty("#444444")
|
|
"""Set the background color of the scrollbar when hovered."""
|
|
scrollbar_background_active = ScrollbarColorProperty("black")
|
|
"""Set the background color of the scrollbar when active (being dragged)."""
|
|
|
|
scrollbar_gutter = StringEnumProperty(
|
|
VALID_SCROLLBAR_GUTTER, "auto", layout=True, refresh_children=True
|
|
)
|
|
"""Set to "stable" to reserve space for the scrollbar even when it's not visible.
|
|
This can prevent content from shifting when a scrollbar appears.
|
|
"""
|
|
|
|
scrollbar_size_vertical = IntegerProperty(default=2, layout=True)
|
|
"""Set the width of the vertical scrollbar (measured in cells)."""
|
|
scrollbar_size_horizontal = IntegerProperty(default=1, layout=True)
|
|
"""Set the height of the horizontal scrollbar (measured in cells)."""
|
|
scrollbar_visibility = StringEnumProperty(
|
|
VALID_SCROLLBAR_VISIBILITY, "visible", layout=True
|
|
)
|
|
"""Sets the visibility of the scrollbar."""
|
|
|
|
align_horizontal = StringEnumProperty(
|
|
VALID_ALIGN_HORIZONTAL, "left", layout=True, refresh_children=True
|
|
)
|
|
align_vertical = StringEnumProperty(
|
|
VALID_ALIGN_VERTICAL, "top", layout=True, refresh_children=True
|
|
)
|
|
align = AlignProperty()
|
|
|
|
content_align_horizontal = StringEnumProperty(VALID_ALIGN_HORIZONTAL, "left")
|
|
content_align_vertical = StringEnumProperty(VALID_ALIGN_VERTICAL, "top")
|
|
content_align = AlignProperty()
|
|
|
|
grid_rows = ScalarListProperty(percent_unit=Unit.HEIGHT, refresh_children=True)
|
|
grid_columns = ScalarListProperty(percent_unit=Unit.WIDTH, refresh_children=True)
|
|
|
|
grid_size_columns = IntegerProperty(default=1, layout=True, refresh_children=True)
|
|
grid_size_rows = IntegerProperty(default=0, layout=True, refresh_children=True)
|
|
grid_gutter_horizontal = IntegerProperty(
|
|
default=0, layout=True, refresh_children=True
|
|
)
|
|
grid_gutter_vertical = IntegerProperty(
|
|
default=0, layout=True, refresh_children=True
|
|
)
|
|
|
|
row_span = IntegerProperty(default=1, layout=True)
|
|
column_span = IntegerProperty(default=1, layout=True)
|
|
|
|
text_align: StringEnumProperty[TextAlign] = StringEnumProperty(
|
|
VALID_TEXT_ALIGN, "start"
|
|
)
|
|
|
|
link_color = ColorProperty("transparent")
|
|
auto_link_color = BooleanProperty(False)
|
|
link_background = ColorProperty("transparent")
|
|
link_style = StyleFlagsProperty()
|
|
|
|
link_color_hover = ColorProperty("transparent")
|
|
auto_link_color_hover = BooleanProperty(False)
|
|
link_background_hover = ColorProperty("transparent")
|
|
link_style_hover = StyleFlagsProperty()
|
|
|
|
auto_border_title_color = BooleanProperty(default=False)
|
|
border_title_color = ColorProperty(Color(255, 255, 255, 0))
|
|
border_title_background = ColorProperty(Color(0, 0, 0, 0))
|
|
border_title_style = StyleFlagsProperty()
|
|
|
|
auto_border_subtitle_color = BooleanProperty(default=False)
|
|
border_subtitle_color = ColorProperty(Color(255, 255, 255, 0))
|
|
border_subtitle_background = ColorProperty(Color(0, 0, 0, 0))
|
|
border_subtitle_style = StyleFlagsProperty()
|
|
|
|
hatch = HatchProperty()
|
|
"""Add a hatched background effect e.g. ("right", "yellow") or "none" to use no hatch.
|
|
"""
|
|
|
|
overlay = StringEnumProperty(
|
|
VALID_OVERLAY, "none", layout=True, refresh_parent=True
|
|
)
|
|
constrain_x: StringEnumProperty[Constrain] = StringEnumProperty(
|
|
VALID_CONSTRAIN, "none"
|
|
)
|
|
constrain_y: StringEnumProperty[Constrain] = StringEnumProperty(
|
|
VALID_CONSTRAIN, "none"
|
|
)
|
|
text_wrap: StringEnumProperty[TextWrap] = StringEnumProperty(
|
|
VALID_TEXT_WRAP, "wrap"
|
|
)
|
|
text_overflow: StringEnumProperty[TextOverflow] = StringEnumProperty(
|
|
VALID_TEXT_OVERFLOW, "fold"
|
|
)
|
|
expand: StringEnumProperty[Expand] = StringEnumProperty(VALID_EXPAND, "greedy")
|
|
line_pad = IntegerProperty(default=0, layout=True)
|
|
"""Padding added to left and right of lines."""
|
|
|
|
def __textual_animation__(
|
|
self,
|
|
attribute: str,
|
|
start_value: object,
|
|
value: object,
|
|
start_time: float,
|
|
duration: float | None,
|
|
speed: float | None,
|
|
easing: EasingFunction,
|
|
on_complete: CallbackType | None = None,
|
|
level: AnimationLevel = "full",
|
|
) -> ScalarAnimation | None:
|
|
if self.node is None:
|
|
return None
|
|
|
|
# Check we are animating a Scalar or Scalar offset
|
|
if isinstance(start_value, (Scalar, ScalarOffset)):
|
|
# If destination is a number, we can convert that to a scalar
|
|
if isinstance(value, (int, float)):
|
|
value = Scalar(value, Unit.CELLS, Unit.CELLS)
|
|
|
|
# We can only animate to Scalar
|
|
if not isinstance(value, (Scalar, ScalarOffset)):
|
|
return None
|
|
|
|
from textual.widget import Widget
|
|
|
|
assert isinstance(self.node, Widget)
|
|
return ScalarAnimation(
|
|
self.node,
|
|
self,
|
|
start_time,
|
|
attribute,
|
|
value,
|
|
duration=duration,
|
|
speed=speed,
|
|
easing=easing,
|
|
on_complete=(
|
|
partial(self.node.app.call_later, on_complete)
|
|
if on_complete is not None
|
|
else None
|
|
),
|
|
level=level,
|
|
)
|
|
return None
|
|
|
|
def __eq__(self, styles: object) -> bool:
|
|
"""Check that Styles contains the same rules."""
|
|
if not isinstance(styles, StylesBase):
|
|
return NotImplemented
|
|
return self.get_rules() == styles.get_rules()
|
|
|
|
def __getitem__(self, key: str) -> object:
|
|
if key not in RULE_NAMES_SET:
|
|
raise KeyError(key)
|
|
return getattr(self, key)
|
|
|
|
def get(self, key: str, default: object | None = None) -> object:
|
|
return getattr(self, key) if key in RULE_NAMES_SET else default
|
|
|
|
def __len__(self) -> int:
|
|
return len(RULE_NAMES)
|
|
|
|
def __iter__(self) -> Iterator[str]:
|
|
return iter(RULE_NAMES)
|
|
|
|
def __contains__(self, key: object) -> bool:
|
|
return key in RULE_NAMES_SET
|
|
|
|
def keys(self) -> Iterable[str]:
|
|
return RULE_NAMES
|
|
|
|
def values(self) -> Iterable[object]:
|
|
for key in RULE_NAMES:
|
|
yield getattr(self, key)
|
|
|
|
def items(self) -> Iterable[tuple[str, object]]:
|
|
for key in RULE_NAMES:
|
|
yield (key, getattr(self, key))
|
|
|
|
@property
|
|
def gutter(self) -> Spacing:
|
|
"""Get space around widget.
|
|
|
|
Returns:
|
|
Space around widget content.
|
|
"""
|
|
return self.padding + self.border.spacing
|
|
|
|
@property
|
|
def auto_dimensions(self) -> bool:
|
|
"""Check if width or height are set to 'auto'."""
|
|
has_rule = self.has_rule
|
|
return (has_rule("width") and self.width.is_auto) or ( # type: ignore
|
|
has_rule("height") and self.height.is_auto # type: ignore
|
|
)
|
|
|
|
@property
|
|
def is_relative_width(self, _relative_units={Unit.FRACTION, Unit.PERCENT}) -> bool:
|
|
"""Does the node have a relative width?"""
|
|
width = self.width
|
|
return width is not None and width.unit in _relative_units
|
|
|
|
@property
|
|
def is_relative_height(self, _relative_units={Unit.FRACTION, Unit.PERCENT}) -> bool:
|
|
"""Does the node have a relative width?"""
|
|
height = self.height
|
|
return height is not None and height.unit in _relative_units
|
|
|
|
@property
|
|
def is_auto_width(self, _auto=Unit.AUTO) -> bool:
|
|
"""Does the node have automatic width?"""
|
|
width = self.width
|
|
return width is not None and width.unit == _auto
|
|
|
|
@property
|
|
def is_auto_height(self, _auto=Unit.AUTO) -> bool:
|
|
"""Does the node have automatic height?"""
|
|
height = self.height
|
|
return height is not None and height.unit == _auto
|
|
|
|
@property
|
|
def is_dynamic_height(
|
|
self, _dynamic_units={Unit.AUTO, Unit.FRACTION, Unit.PERCENT}
|
|
) -> bool:
|
|
"""Does the node have a dynamic (not fixed) height?"""
|
|
height = self.height
|
|
return height is not None and height.unit in _dynamic_units
|
|
|
|
@property
|
|
def is_docked(self) -> bool:
|
|
"""Is the node docked?"""
|
|
return self.dock != "none"
|
|
|
|
@property
|
|
def is_split(self) -> bool:
|
|
"""Is the node split?"""
|
|
return self.split != "none"
|
|
|
|
def has_rule(self, rule_name: str) -> bool:
|
|
"""Check if a rule is set on this Styles object.
|
|
|
|
Args:
|
|
rule_name: Rule name.
|
|
|
|
Returns:
|
|
``True`` if the rules is present, otherwise ``False``.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def clear_rule(self, rule_name: str) -> bool:
|
|
"""Removes the rule from the Styles object, as if it had never been set.
|
|
|
|
Args:
|
|
rule_name: Rule name.
|
|
|
|
Returns:
|
|
``True`` if a rule was cleared, or ``False`` if the rule is already not set.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_rules(self) -> RulesMap:
|
|
"""Get the rules in a mapping.
|
|
|
|
Returns:
|
|
A TypedDict of the rules.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def set_rule(self, rule_name: str, value: object | None) -> bool:
|
|
"""Set a rule.
|
|
|
|
Args:
|
|
rule_name: Rule name.
|
|
value: New rule value.
|
|
|
|
Returns:
|
|
``True`` if the rule changed, otherwise ``False``.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def get_rule(self, rule_name: str, default: object = None) -> object:
|
|
"""Get an individual rule.
|
|
|
|
Args:
|
|
rule_name: Name of rule.
|
|
default: Default if rule does not exists.
|
|
|
|
Returns:
|
|
Rule value or default.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def refresh(
|
|
self,
|
|
*,
|
|
layout: bool = False,
|
|
children: bool = False,
|
|
parent: bool = False,
|
|
repaint: bool = True,
|
|
) -> None:
|
|
"""Mark the styles as requiring a refresh.
|
|
|
|
Args:
|
|
layout: Also require a layout.
|
|
children: Also refresh children.
|
|
parent: Also refresh the parent.
|
|
repaint: Repaint the widgets.
|
|
"""
|
|
|
|
def reset(self) -> None:
|
|
"""Reset the rules to initial state."""
|
|
|
|
def merge(self, other: StylesBase) -> None:
|
|
"""Merge values from another Styles.
|
|
|
|
Args:
|
|
other: A Styles object.
|
|
"""
|
|
|
|
def merge_rules(self, rules: RulesMap) -> None:
|
|
"""Merge rules into Styles.
|
|
|
|
Args:
|
|
rules: 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.
|
|
|
|
Args:
|
|
rule: Name of the rule.
|
|
|
|
Returns:
|
|
``True`` if the rule may be animated, otherwise ``False``.
|
|
"""
|
|
return rule in cls.ANIMATABLE
|
|
|
|
@classmethod
|
|
def parse(
|
|
cls, css: str, read_from: CSSLocation, *, node: DOMNode | None = None
|
|
) -> Styles:
|
|
"""Parse CSS and return a Styles object.
|
|
|
|
Args:
|
|
css: Textual CSS.
|
|
read_from: Location where the CSS was read from.
|
|
node: Node to associate with the Styles.
|
|
|
|
Returns:
|
|
A Styles instance containing result of parsing CSS.
|
|
"""
|
|
from textual.css.parse import parse_declarations
|
|
|
|
styles = parse_declarations(css, read_from)
|
|
styles.node = node
|
|
return styles
|
|
|
|
def _get_transition(self, key: str) -> Transition | None:
|
|
"""Get a transition.
|
|
|
|
Args:
|
|
key: Transition key.
|
|
|
|
Returns:
|
|
Transition object or None it no transition exists.
|
|
"""
|
|
if key in self.ANIMATABLE:
|
|
return self.transitions.get(key, None)
|
|
else:
|
|
return None
|
|
|
|
def _align_width(self, width: int, parent_width: int) -> int:
|
|
"""Align the width dimension.
|
|
|
|
Args:
|
|
width: Width of the content.
|
|
parent_width: Width of the parent container.
|
|
|
|
Returns:
|
|
An offset to add to the X coordinate.
|
|
"""
|
|
offset_x = 0
|
|
align_horizontal = self.align_horizontal
|
|
if align_horizontal != "left":
|
|
if align_horizontal == "center":
|
|
offset_x = (parent_width - width) // 2
|
|
else:
|
|
offset_x = parent_width - width
|
|
|
|
return offset_x
|
|
|
|
def _align_height(self, height: int, parent_height: int) -> int:
|
|
"""Align the height dimensions
|
|
|
|
Args:
|
|
height: Height of the content.
|
|
parent_height: Height of the parent container.
|
|
|
|
Returns:
|
|
An offset to add to the Y coordinate.
|
|
"""
|
|
offset_y = 0
|
|
align_vertical = self.align_vertical
|
|
if align_vertical != "top":
|
|
if align_vertical == "middle":
|
|
offset_y = (parent_height - height) // 2
|
|
else:
|
|
offset_y = parent_height - height
|
|
return offset_y
|
|
|
|
def _align_size(self, child: tuple[int, int], parent: tuple[int, int]) -> Offset:
|
|
"""Align a size according to alignment rules.
|
|
|
|
Args:
|
|
child: The size of the child (width, height)
|
|
parent: The size of the parent (width, height)
|
|
|
|
Returns:
|
|
Offset required to align the child.
|
|
"""
|
|
width, height = child
|
|
parent_width, parent_height = parent
|
|
return Offset(
|
|
self._align_width(width, parent_width),
|
|
self._align_height(height, parent_height),
|
|
)
|
|
|
|
@property
|
|
def partial_rich_style(self) -> Style:
|
|
"""Get the style properties associated with this node only (not including parents in the DOM).
|
|
|
|
Returns:
|
|
Rich Style object.
|
|
"""
|
|
style = Style(
|
|
color=(
|
|
self.color.rich_color
|
|
if self.has_rule("color") and self.color.a > 0
|
|
else None
|
|
),
|
|
bgcolor=(
|
|
self.background.rich_color
|
|
if self.has_rule("background") and self.background.a > 0
|
|
else None
|
|
),
|
|
)
|
|
style += self.text_style
|
|
return style
|
|
|
|
|
|
@rich.repr.auto
|
|
@dataclass
|
|
class Styles(StylesBase):
|
|
node: DOMNode | None = None
|
|
_rules: RulesMap = field(default_factory=RulesMap)
|
|
_updates: int = 0
|
|
|
|
important: set[str] = field(default_factory=set)
|
|
|
|
def __post_init__(self) -> None:
|
|
self.get_rule: Callable[[str, object], object] = self._rules.get # type: ignore[assignment]
|
|
self.has_rule: Callable[[str], bool] = self._rules.__contains__ # type: ignore[assignment]
|
|
|
|
def copy(self) -> Styles:
|
|
"""Get a copy of this Styles object."""
|
|
return Styles(
|
|
node=self.node,
|
|
_rules=self.get_rules(),
|
|
important=self.important,
|
|
)
|
|
|
|
def clear_rule(self, rule_name: str) -> bool:
|
|
"""Removes the rule from the Styles object, as if it had never been set.
|
|
|
|
Args:
|
|
rule_name: Rule name.
|
|
|
|
Returns:
|
|
``True`` if a rule was cleared, or ``False`` if it was already not set.
|
|
"""
|
|
changed = self._rules.pop(rule_name, None) is not None # type: ignore
|
|
if changed:
|
|
self._updates += 1
|
|
return changed
|
|
|
|
def get_rules(self) -> RulesMap:
|
|
return self._rules.copy()
|
|
|
|
def set_rule(self, rule: str, value: object | None) -> bool:
|
|
"""Set a rule.
|
|
|
|
Args:
|
|
rule: Rule name.
|
|
value: New rule value.
|
|
|
|
Returns:
|
|
``True`` if the rule changed, otherwise ``False``.
|
|
"""
|
|
if value is None:
|
|
changed = self._rules.pop(rule, None) is not None # type: ignore
|
|
if changed:
|
|
self._updates += 1
|
|
return changed
|
|
current = self._rules.get(rule)
|
|
self._rules[rule] = value # type: ignore
|
|
changed = current != value
|
|
if changed:
|
|
self._updates += 1
|
|
return changed
|
|
|
|
def refresh(
|
|
self,
|
|
*,
|
|
layout: bool = False,
|
|
children: bool = False,
|
|
parent: bool = False,
|
|
repaint=True,
|
|
) -> None:
|
|
node = self.node
|
|
if node is None or not node._is_mounted:
|
|
return
|
|
if parent and node._parent is not None:
|
|
node._parent.refresh(repaint=repaint)
|
|
node.refresh(layout=layout)
|
|
if children:
|
|
for child in node.walk_children(with_self=False, reverse=True):
|
|
child.refresh(layout=layout, repaint=repaint)
|
|
|
|
def reset(self) -> None:
|
|
"""Reset the rules to initial state."""
|
|
self._updates += 1
|
|
self._rules.clear() # type: ignore
|
|
|
|
def merge(self, other: StylesBase) -> None:
|
|
"""Merge values from another Styles.
|
|
|
|
Args:
|
|
other: A Styles object.
|
|
"""
|
|
self._updates += 1
|
|
self._rules.update(other.get_rules())
|
|
|
|
def merge_rules(self, rules: RulesMap) -> None:
|
|
self._updates += 1
|
|
self._rules.update(rules)
|
|
|
|
def extract_rules(
|
|
self,
|
|
specificity: Specificity3,
|
|
is_default_rules: bool = False,
|
|
tie_breaker: int = 0,
|
|
) -> list[tuple[str, Specificity6, Any]]:
|
|
"""Extract rules from Styles object, and apply !important css specificity as
|
|
well as higher specificity of user CSS vs widget CSS.
|
|
|
|
Args:
|
|
specificity: A node specificity.
|
|
is_default_rules: True if the rules we're extracting are
|
|
default (i.e. in Widget.DEFAULT_CSS) rules. False if they're from user defined CSS.
|
|
|
|
Returns:
|
|
A list containing a tuple of <RULE NAME>, <SPECIFICITY> <RULE VALUE>.
|
|
"""
|
|
is_important = self.important.__contains__
|
|
default_rules = 0 if is_default_rules else 1
|
|
rules: list[tuple[str, Specificity6, Any]] = [
|
|
(
|
|
rule_name,
|
|
(
|
|
default_rules,
|
|
1 if is_important(rule_name) else 0,
|
|
*specificity,
|
|
tie_breaker,
|
|
),
|
|
rule_value,
|
|
)
|
|
for rule_name, rule_value in self._rules.items()
|
|
]
|
|
|
|
return rules
|
|
|
|
def __rich_repr__(self) -> rich.repr.Result:
|
|
has_rule = self.has_rule
|
|
for name in RULE_NAMES:
|
|
if has_rule(name):
|
|
yield name, getattr(self, name)
|
|
if self.important:
|
|
yield "important", self.important
|
|
|
|
def _get_border_css_lines(
|
|
self, rules: RulesMap, name: str
|
|
) -> Iterable[tuple[str, str]]:
|
|
"""Get pairs of strings containing <RULE NAME>, <RULE VALUE> for border css declarations.
|
|
|
|
Args:
|
|
rules: A rules map.
|
|
name: Name of rules (border or outline)
|
|
|
|
Returns:
|
|
An iterable of CSS declarations.
|
|
"""
|
|
|
|
has_rule = rules.__contains__
|
|
get_rule = rules.__getitem__
|
|
|
|
has_top = has_rule(f"{name}_top")
|
|
has_right = has_rule(f"{name}_right")
|
|
has_bottom = has_rule(f"{name}_bottom")
|
|
has_left = has_rule(f"{name}_left")
|
|
if not any((has_top, has_right, has_bottom, has_left)):
|
|
# No border related rules
|
|
return
|
|
|
|
if all((has_top, has_right, has_bottom, has_left)):
|
|
# All rules are set
|
|
# See if we can set them with a single border: declaration
|
|
top = get_rule(f"{name}_top")
|
|
right = get_rule(f"{name}_right")
|
|
bottom = get_rule(f"{name}_bottom")
|
|
left = get_rule(f"{name}_left")
|
|
|
|
if top == right and right == bottom and bottom == left:
|
|
border_type, border_color = rules[f"{name}_top"] # type: ignore
|
|
yield name, f"{border_type} {border_color.hex}"
|
|
return
|
|
|
|
# Check for edges
|
|
if has_top:
|
|
border_type, border_color = rules[f"{name}_top"] # type: ignore
|
|
yield f"{name}-top", f"{border_type} {border_color.hex}"
|
|
|
|
if has_right:
|
|
border_type, border_color = rules[f"{name}_right"] # type: ignore
|
|
yield f"{name}-right", f"{border_type} {border_color.hex}"
|
|
|
|
if has_bottom:
|
|
border_type, border_color = rules[f"{name}_bottom"] # type: ignore
|
|
yield f"{name}-bottom", f"{border_type} {border_color.hex}"
|
|
|
|
if has_left:
|
|
border_type, border_color = rules[f"{name}_left"] # type: ignore
|
|
yield f"{name}-left", f"{border_type} {border_color.hex}"
|
|
|
|
@property
|
|
def css_lines(self) -> list[str]:
|
|
lines: list[str] = []
|
|
append = lines.append
|
|
|
|
def append_declaration(name: str, value: str) -> None:
|
|
if name in self.important:
|
|
append(f"{name}: {value} !important;")
|
|
else:
|
|
append(f"{name}: {value};")
|
|
|
|
rules = self.get_rules()
|
|
get_rule = rules.get
|
|
|
|
if "display" in rules:
|
|
append_declaration("display", rules["display"])
|
|
if "visibility" in rules:
|
|
append_declaration("visibility", rules["visibility"])
|
|
if "padding" in rules:
|
|
append_declaration("padding", rules["padding"].css)
|
|
if "margin" in rules:
|
|
append_declaration("margin", rules["margin"].css)
|
|
|
|
for name, rule in self._get_border_css_lines(rules, "border"):
|
|
append_declaration(name, rule)
|
|
|
|
for name, rule in self._get_border_css_lines(rules, "outline"):
|
|
append_declaration(name, rule)
|
|
|
|
if "offset" in rules:
|
|
x, y = self.offset
|
|
append_declaration("offset", f"{x} {y}")
|
|
if "position" in rules:
|
|
append_declaration("position", self.position)
|
|
if "dock" in rules:
|
|
append_declaration("dock", rules["dock"])
|
|
if "split" in rules:
|
|
append_declaration("split", rules["split"])
|
|
if "layers" in rules:
|
|
append_declaration("layers", " ".join(self.layers))
|
|
if "layer" in rules:
|
|
append_declaration("layer", self.layer)
|
|
if "layout" in rules:
|
|
assert self.layout is not None
|
|
append_declaration("layout", self.layout.name)
|
|
|
|
if "color" in rules:
|
|
append_declaration("color", self.color.hex)
|
|
if "background" in rules:
|
|
append_declaration("background", self.background.hex)
|
|
if "background_tint" in rules:
|
|
append_declaration("background-tint", self.background_tint.hex)
|
|
if "text_style" in rules:
|
|
append_declaration("text-style", str(get_rule("text_style")))
|
|
if "tint" in rules:
|
|
append_declaration("tint", self.tint.css)
|
|
|
|
if "overflow_x" in rules:
|
|
append_declaration("overflow-x", self.overflow_x)
|
|
if "overflow_y" in rules:
|
|
append_declaration("overflow-y", self.overflow_y)
|
|
|
|
if "scrollbar_color" in rules:
|
|
append_declaration("scrollbar-color", self.scrollbar_color.css)
|
|
if "scrollbar_color_hover" in rules:
|
|
append_declaration("scrollbar-color-hover", self.scrollbar_color_hover.css)
|
|
if "scrollbar_color_active" in rules:
|
|
append_declaration(
|
|
"scrollbar-color-active", self.scrollbar_color_active.css
|
|
)
|
|
|
|
if "scrollbar_corner_color" in rules:
|
|
append_declaration(
|
|
"scrollbar-corner-color", self.scrollbar_corner_color.css
|
|
)
|
|
|
|
if "scrollbar_background" in rules:
|
|
append_declaration("scrollbar-background", self.scrollbar_background.css)
|
|
if "scrollbar_background_hover" in rules:
|
|
append_declaration(
|
|
"scrollbar-background-hover", self.scrollbar_background_hover.css
|
|
)
|
|
if "scrollbar_background_active" in rules:
|
|
append_declaration(
|
|
"scrollbar-background-active", self.scrollbar_background_active.css
|
|
)
|
|
|
|
if "scrollbar_gutter" in rules:
|
|
append_declaration("scrollbar-gutter", self.scrollbar_gutter)
|
|
if "scrollbar_size" in rules:
|
|
append_declaration(
|
|
"scrollbar-size",
|
|
f"{self.scrollbar_size_horizontal} {self.scrollbar_size_vertical}",
|
|
)
|
|
else:
|
|
if "scrollbar_size_horizontal" in rules:
|
|
append_declaration(
|
|
"scrollbar-size-horizontal", str(self.scrollbar_size_horizontal)
|
|
)
|
|
if "scrollbar_size_vertical" in rules:
|
|
append_declaration(
|
|
"scrollbar-size-vertical", str(self.scrollbar_size_vertical)
|
|
)
|
|
if "scrollbar_visibility" in rules:
|
|
append_declaration("scrollbar-visibility", self.scrollbar_visibility)
|
|
|
|
if "box_sizing" in rules:
|
|
append_declaration("box-sizing", self.box_sizing)
|
|
if "width" in rules:
|
|
append_declaration("width", str(self.width))
|
|
if "height" in rules:
|
|
append_declaration("height", str(self.height))
|
|
if "min_width" in rules:
|
|
append_declaration("min-width", str(self.min_width))
|
|
if "min_height" in rules:
|
|
append_declaration("min-height", str(self.min_height))
|
|
if "max_width" in rules:
|
|
append_declaration("max-width", str(self.max_width))
|
|
if "max_height" in rules:
|
|
append_declaration("max-height", str(self.max_height))
|
|
if "transitions" in rules:
|
|
append_declaration(
|
|
"transition",
|
|
", ".join(
|
|
f"{name} {transition}"
|
|
for name, transition in self.transitions.items()
|
|
),
|
|
)
|
|
|
|
if "align_horizontal" in rules and "align_vertical" in rules:
|
|
append_declaration(
|
|
"align", f"{self.align_horizontal} {self.align_vertical}"
|
|
)
|
|
elif "align_horizontal" in rules:
|
|
append_declaration("align-horizontal", self.align_horizontal)
|
|
elif "align_vertical" in rules:
|
|
append_declaration("align-vertical", self.align_vertical)
|
|
|
|
if "content_align_horizontal" in rules and "content_align_vertical" in rules:
|
|
append_declaration(
|
|
"content-align",
|
|
f"{self.content_align_horizontal} {self.content_align_vertical}",
|
|
)
|
|
elif "content_align_horizontal" in rules:
|
|
append_declaration(
|
|
"content-align-horizontal", self.content_align_horizontal
|
|
)
|
|
elif "content_align_vertical" in rules:
|
|
append_declaration("content-align-vertical", self.content_align_vertical)
|
|
|
|
if "text_align" in rules:
|
|
append_declaration("text-align", self.text_align)
|
|
|
|
if "border_title_align" in rules:
|
|
append_declaration("border-title-align", self.border_title_align)
|
|
if "border_subtitle_align" in rules:
|
|
append_declaration("border-subtitle-align", self.border_subtitle_align)
|
|
|
|
if "opacity" in rules:
|
|
append_declaration("opacity", str(self.opacity))
|
|
if "text_opacity" in rules:
|
|
append_declaration("text-opacity", str(self.text_opacity))
|
|
|
|
if "grid_columns" in rules:
|
|
append_declaration(
|
|
"grid-columns",
|
|
" ".join(str(scalar) for scalar in self.grid_columns or ()),
|
|
)
|
|
if "grid_rows" in rules:
|
|
append_declaration(
|
|
"grid-rows",
|
|
" ".join(str(scalar) for scalar in self.grid_rows or ()),
|
|
)
|
|
if "grid_size_columns" in rules:
|
|
append_declaration("grid-size-columns", str(self.grid_size_columns))
|
|
if "grid_size_rows" in rules:
|
|
append_declaration("grid-size-rows", str(self.grid_size_rows))
|
|
|
|
if "grid_gutter_horizontal" in rules:
|
|
append_declaration(
|
|
"grid-gutter-horizontal", str(self.grid_gutter_horizontal)
|
|
)
|
|
if "grid_gutter_vertical" in rules:
|
|
append_declaration("grid-gutter-vertical", str(self.grid_gutter_vertical))
|
|
|
|
if "row_span" in rules:
|
|
append_declaration("row-span", str(self.row_span))
|
|
if "column_span" in rules:
|
|
append_declaration("column-span", str(self.column_span))
|
|
|
|
if "link_color" in rules:
|
|
append_declaration("link-color", self.link_color.css)
|
|
if "link_background" in rules:
|
|
append_declaration("link-background", self.link_background.css)
|
|
if "link_style" in rules:
|
|
append_declaration("link-style", str(self.link_style))
|
|
|
|
if "link_color_hover" in rules:
|
|
append_declaration("link-color-hover", self.link_color_hover.css)
|
|
if "link_background_hover" in rules:
|
|
append_declaration("link-background-hover", self.link_background_hover.css)
|
|
if "link_style_hover" in rules:
|
|
append_declaration("link-style-hover", str(self.link_style_hover))
|
|
|
|
if "border_title_color" in rules:
|
|
append_declaration("title-color", self.border_title_color.css)
|
|
if "border_title_background" in rules:
|
|
append_declaration("title-background", self.border_title_background.css)
|
|
if "border_title_style" in rules:
|
|
append_declaration("title-text-style", str(self.border_title_style))
|
|
|
|
if "border_subtitle_color" in rules:
|
|
append_declaration("subtitle-color", self.border_subtitle_color.css)
|
|
if "border_subtitle_background" in rules:
|
|
append_declaration(
|
|
"subtitle-background", self.border_subtitle_background.css
|
|
)
|
|
if "border_subtitle_text_style" in rules:
|
|
append_declaration("subtitle-text-style", str(self.border_subtitle_style))
|
|
if "overlay" in rules:
|
|
append_declaration("overlay", str(self.overlay))
|
|
if "constrain_x" in rules and "constrain_y" in rules:
|
|
if self.constrain_x == self.constrain_y:
|
|
append_declaration("constrain", self.constrain_x)
|
|
else:
|
|
append_declaration(
|
|
"constrain", f"{self.constrain_x} {self.constrain_y}"
|
|
)
|
|
elif "constrain_x" in rules:
|
|
append_declaration("constrain-x", self.constrain_x)
|
|
elif "constrain_y" in rules:
|
|
append_declaration("constrain-y", self.constrain_y)
|
|
|
|
if "keyline" in rules:
|
|
keyline_type, keyline_color = self.keyline
|
|
if keyline_type != "none":
|
|
append_declaration("keyline", f"{keyline_type}, {keyline_color.css}")
|
|
if "hatch" in rules:
|
|
hatch_character, hatch_color = self.hatch
|
|
append_declaration("hatch", f'"{hatch_character}" {hatch_color.css}')
|
|
if "text_wrap" in rules:
|
|
append_declaration("text-wrap", self.text_wrap)
|
|
if "text_overflow" in rules:
|
|
append_declaration("text-overflow", self.text_overflow)
|
|
if "expand" in rules:
|
|
append_declaration("expand", self.expand)
|
|
if "line_pad" in rules:
|
|
append_declaration("line-pad", str(self.line_pad))
|
|
lines.sort()
|
|
return lines
|
|
|
|
@property
|
|
def css(self) -> str:
|
|
return "\n".join(self.css_lines)
|
|
|
|
|
|
@rich.repr.auto
|
|
class RenderStyles(StylesBase):
|
|
"""Presents a combined view of two Styles object: a base Styles and inline Styles."""
|
|
|
|
def __init__(self, node: DOMNode, base: Styles, inline_styles: Styles) -> None:
|
|
self.node = node
|
|
self._base_styles = base
|
|
self._inline_styles = inline_styles
|
|
self._animate: BoundAnimator | None = None
|
|
self._updates: int = 0
|
|
self._rich_style: tuple[int, Style] | None = None
|
|
self._gutter: tuple[int, Spacing] | None = None
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, RenderStyles):
|
|
return (
|
|
self._base_styles._rules == other._base_styles._rules
|
|
and self._inline_styles._rules == other._inline_styles._rules
|
|
)
|
|
return NotImplemented
|
|
|
|
@property
|
|
def _cache_key(self) -> int:
|
|
"""A cache key, that changes when any style is changed.
|
|
|
|
Returns:
|
|
An opaque integer.
|
|
"""
|
|
return self._updates + self._base_styles._updates + self._inline_styles._updates
|
|
|
|
@property
|
|
def base(self) -> Styles:
|
|
"""Quick access to base (css) style."""
|
|
return self._base_styles
|
|
|
|
@property
|
|
def inline(self) -> Styles:
|
|
"""Quick access to the inline styles."""
|
|
return self._inline_styles
|
|
|
|
@property
|
|
def rich_style(self) -> Style:
|
|
"""Get a Rich style for this Styles object."""
|
|
assert self.node is not None
|
|
return self.node.rich_style
|
|
|
|
@property
|
|
def gutter(self) -> Spacing:
|
|
"""Get space around widget (padding + border)
|
|
|
|
Returns:
|
|
Space around widget content.
|
|
"""
|
|
# This is (surprisingly) a bit of a bottleneck
|
|
if self._gutter is not None:
|
|
cache_key, gutter = self._gutter
|
|
if cache_key == self._cache_key:
|
|
return gutter
|
|
gutter = self.padding + self.border.spacing
|
|
self._gutter = (self._cache_key, gutter)
|
|
return gutter
|
|
|
|
def animate(
|
|
self,
|
|
attribute: str,
|
|
value: str | float | Animatable,
|
|
*,
|
|
final_value: object = ...,
|
|
duration: float | None = None,
|
|
speed: float | None = None,
|
|
delay: float = 0.0,
|
|
easing: EasingFunction | str = DEFAULT_EASING,
|
|
on_complete: CallbackType | None = None,
|
|
level: AnimationLevel = "full",
|
|
) -> None:
|
|
"""Animate an attribute.
|
|
|
|
Args:
|
|
attribute: Name of the attribute to animate.
|
|
value: The value to animate to.
|
|
final_value: The final value of the animation. Defaults to `value` if not set.
|
|
duration: The duration (in seconds) of the animation.
|
|
speed: The speed of the animation.
|
|
delay: A delay (in seconds) before the animation starts.
|
|
easing: An easing method.
|
|
on_complete: A callable to invoke when the animation is finished.
|
|
level: Minimum level required for the animation to take place (inclusive).
|
|
"""
|
|
if self._animate is None:
|
|
assert self.node is not None
|
|
self._animate = self.node.app.animator.bind(self)
|
|
assert self._animate is not None
|
|
self._animate(
|
|
attribute,
|
|
value,
|
|
final_value=final_value,
|
|
duration=duration,
|
|
speed=speed,
|
|
delay=delay,
|
|
easing=easing,
|
|
on_complete=on_complete,
|
|
level=level,
|
|
)
|
|
|
|
def __rich_repr__(self) -> rich.repr.Result:
|
|
yield self.node
|
|
for rule_name in RULE_NAMES:
|
|
if self.has_rule(rule_name):
|
|
yield rule_name, getattr(self, rule_name)
|
|
|
|
def refresh(
|
|
self,
|
|
*,
|
|
layout: bool = False,
|
|
children: bool = False,
|
|
parent: bool = False,
|
|
repaint: bool = True,
|
|
) -> None:
|
|
self._inline_styles.refresh(
|
|
layout=layout, children=children, parent=parent, repaint=repaint
|
|
)
|
|
|
|
def merge(self, other: StylesBase) -> None:
|
|
"""Merge values from another Styles.
|
|
|
|
Args:
|
|
other: A Styles object.
|
|
"""
|
|
self._inline_styles.merge(other)
|
|
|
|
def merge_rules(self, rules: RulesMap) -> None:
|
|
self._inline_styles.merge_rules(rules)
|
|
self._updates += 1
|
|
|
|
def reset(self) -> None:
|
|
"""Reset the rules to initial state."""
|
|
self._inline_styles.reset()
|
|
self._updates += 1
|
|
|
|
def has_rule(self, rule_name: str) -> bool:
|
|
"""Check if a rule has been set."""
|
|
return self._inline_styles.has_rule(rule_name) or self._base_styles.has_rule(
|
|
rule_name
|
|
)
|
|
|
|
def has_any_rules(self, *rule_names: str) -> bool:
|
|
"""Check if any of the supplied rules have been set.
|
|
|
|
Args:
|
|
rule_names: Number of rules.
|
|
|
|
Returns:
|
|
`True` if any of the supplied rules have been set, `False` if none have.
|
|
"""
|
|
inline_has_rule = self._inline_styles.has_rule
|
|
base_has_rule = self._base_styles.has_rule
|
|
return any(inline_has_rule(name) or base_has_rule(name) for name in rule_names)
|
|
|
|
def set_rule(self, rule_name: str, value: object | None) -> bool:
|
|
return self._inline_styles.set_rule(rule_name, value)
|
|
|
|
def get_rule(self, rule_name: str, default: object = None) -> object:
|
|
if self._inline_styles.has_rule(rule_name):
|
|
return self._inline_styles.get_rule(rule_name, default)
|
|
return self._base_styles.get_rule(rule_name, default)
|
|
|
|
def clear_rule(self, rule_name: str) -> bool:
|
|
"""Clear a rule (from inline)."""
|
|
return self._inline_styles.clear_rule(rule_name)
|
|
|
|
def get_rules(self) -> RulesMap:
|
|
"""Get rules as a dictionary"""
|
|
rules = {**self._base_styles._rules, **self._inline_styles._rules}
|
|
return cast(RulesMap, rules)
|
|
|
|
@property
|
|
def css(self) -> str:
|
|
"""Get the CSS for the combined styles."""
|
|
styles = Styles()
|
|
styles.merge(self._base_styles)
|
|
styles.merge(self._inline_styles)
|
|
combined_css = styles.css
|
|
return combined_css
|