mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
implement inline styles
This commit is contained in:
@@ -26,7 +26,7 @@ typing-extensions = { version = "^3.10.0", python = "<3.8" }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.2.3"
|
||||
black = "^21.11b1"
|
||||
black = "^22.1.0"
|
||||
mypy = "^0.910"
|
||||
pytest-cov = "^2.12.1"
|
||||
mkdocs = "^1.2.1"
|
||||
|
||||
3
sandbox/README.md
Normal file
3
sandbox/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Dev Sandbox
|
||||
|
||||
This directory contains test code. None of the .py files here are garanteed to run or do anything useful, but you are welcome to look around.
|
||||
7
sandbox/local_styles.css
Normal file
7
sandbox/local_styles.css
Normal file
@@ -0,0 +1,7 @@
|
||||
App > View {
|
||||
layout: dock;
|
||||
}
|
||||
|
||||
Widget {
|
||||
text: on blue;
|
||||
}
|
||||
33
sandbox/local_styles.py
Normal file
33
sandbox/local_styles.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from textual.app import App
|
||||
from textual.widgets import Placeholder
|
||||
from textual.widget import Widget
|
||||
from textual import events
|
||||
|
||||
|
||||
class BasicApp(App):
|
||||
"""Sandbox application used for testing/development by Textual developers"""
|
||||
|
||||
def on_mount(self):
|
||||
"""Build layout here."""
|
||||
self.mount(
|
||||
header=Widget(),
|
||||
content=Placeholder(),
|
||||
footer=Widget(),
|
||||
sidebar=Widget(),
|
||||
)
|
||||
|
||||
async def on_key(self, event: events.Key) -> None:
|
||||
await self.dispatch_key(event)
|
||||
|
||||
def key_a(self) -> bool | None:
|
||||
self.query("#footer").set_styles(text="on magenta").refresh()
|
||||
|
||||
self.log(self["#footer"].styles.css)
|
||||
self.bell()
|
||||
self.refresh()
|
||||
|
||||
def key_b(self) -> bool | None:
|
||||
self["#content"].set_styles("text: on magenta")
|
||||
|
||||
|
||||
BasicApp.run(css_file="local_styles.css", log="textual.log")
|
||||
@@ -5,7 +5,7 @@ import os
|
||||
import platform
|
||||
import warnings
|
||||
from asyncio import AbstractEventLoop
|
||||
from typing import Any, Callable, Iterable, Type, TypeVar
|
||||
from typing import Any, Callable, Iterable, Type, TypeVar, TYPE_CHECKING
|
||||
|
||||
import rich.repr
|
||||
from rich.console import Console, RenderableType
|
||||
@@ -35,6 +35,11 @@ from .reactive import Reactive
|
||||
from .view import View
|
||||
from .widget import Widget
|
||||
|
||||
from .css.query import EmptyQueryError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .css.query import DOMQuery
|
||||
|
||||
PLATFORM = platform.system()
|
||||
WINDOWS = PLATFORM == "Windows"
|
||||
|
||||
@@ -260,6 +265,27 @@ class App(DOMNode):
|
||||
self.stylesheet.update(self)
|
||||
self.view.refresh(layout=True)
|
||||
|
||||
def query(self, selector: str | None = None) -> DOMQuery:
|
||||
"""Get a DOM query in the current view.
|
||||
|
||||
Args:
|
||||
selector (str, optional): A CSS selector or `None` for all nodes. Defaults to None.
|
||||
|
||||
Returns:
|
||||
DOMQuery: A query object.
|
||||
"""
|
||||
from .css.query import DOMQuery
|
||||
|
||||
return DOMQuery(self.view, selector)
|
||||
|
||||
def __getitem__(self, selector: str) -> DOMNode:
|
||||
from .css.query import DOMQuery
|
||||
|
||||
try:
|
||||
return DOMQuery(self.view, selector).first()
|
||||
except EmptyQueryError:
|
||||
raise KeyError(selector)
|
||||
|
||||
def update_styles(self) -> None:
|
||||
"""Request update of styles.
|
||||
|
||||
@@ -513,6 +539,10 @@ class App(DOMNode):
|
||||
"""
|
||||
return self.view.get_widget_at(x, y)
|
||||
|
||||
def bell(self) -> None:
|
||||
"""Play the console 'bell'."""
|
||||
self.console.bell()
|
||||
|
||||
async def press(self, key: str) -> bool:
|
||||
"""Handle a key press.
|
||||
|
||||
@@ -640,7 +670,7 @@ class App(DOMNode):
|
||||
1 / 0
|
||||
|
||||
async def action_bell(self) -> None:
|
||||
self.console.bell()
|
||||
self.bell()
|
||||
|
||||
async def action_add_class_(self, selector: str, class_name: str) -> None:
|
||||
self.view.query(selector).add_class(class_name)
|
||||
|
||||
@@ -33,8 +33,13 @@ if TYPE_CHECKING:
|
||||
from ..layout import Layout
|
||||
from .styles import Styles
|
||||
from .styles import DockGroup
|
||||
from .._box import BoxType
|
||||
from ..layouts.factory import LayoutName
|
||||
|
||||
from .._box import BoxType
|
||||
|
||||
BorderDefinition = (
|
||||
Sequence[tuple[BoxType, str | Color | Style] | None]
|
||||
| tuple[BoxType, str | Color | Style]
|
||||
)
|
||||
|
||||
|
||||
class ScalarProperty:
|
||||
@@ -228,9 +233,7 @@ class BorderProperty:
|
||||
def __set__(
|
||||
self,
|
||||
obj: Styles,
|
||||
border: Sequence[tuple[BoxType, str | Color | Style] | None]
|
||||
| tuple[BoxType, str | Color | Style]
|
||||
| None,
|
||||
border: BorderDefinition | None,
|
||||
) -> None:
|
||||
"""Set the border
|
||||
|
||||
@@ -443,7 +446,9 @@ class LayoutProperty:
|
||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||
self._internal_name = f"_rule_{name}"
|
||||
|
||||
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> Layout:
|
||||
def __get__(
|
||||
self, obj: Styles, objtype: type[Styles] | None = None
|
||||
) -> Layout | None:
|
||||
"""
|
||||
Args:
|
||||
obj (Styles): The Styles object
|
||||
@@ -453,13 +458,14 @@ class LayoutProperty:
|
||||
"""
|
||||
return getattr(obj, self._internal_name)
|
||||
|
||||
def __set__(self, obj: Styles, layout: LayoutName | Layout):
|
||||
def __set__(self, obj: Styles, layout: str | Layout):
|
||||
"""
|
||||
Args:
|
||||
obj (Styles): The Styles object.
|
||||
layout (LayoutName | Layout): The layout to use. You can supply a ``LayoutName``
|
||||
(a string literal such as ``"dock"``) or a ``Layout`` object.
|
||||
layout (str | Layout): The layout to use. You can supply a the name of the layout
|
||||
or a ``Layout`` object.
|
||||
"""
|
||||
|
||||
from ..layouts.factory import get_layout, Layout # Prevents circular import
|
||||
|
||||
obj.refresh(layout=True)
|
||||
|
||||
@@ -18,7 +18,6 @@ from .types import Edge, Display, Visibility
|
||||
from .._duration import _duration_as_seconds
|
||||
from .._easing import EASING
|
||||
from ..geometry import Spacing, SpacingDimensions
|
||||
from ..layouts.factory import get_layout, LayoutName, MissingLayout, LAYOUT_MAP
|
||||
|
||||
|
||||
class StylesBuilder:
|
||||
@@ -285,12 +284,14 @@ class StylesBuilder:
|
||||
self.styles._rule_offset = ScalarOffset(x, y)
|
||||
|
||||
def process_layout(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||
from ..layouts.factory import get_layout, MissingLayout, LAYOUT_MAP
|
||||
|
||||
if tokens:
|
||||
if len(tokens) != 1:
|
||||
self.error(name, tokens[0], "unexpected tokens in declaration")
|
||||
else:
|
||||
value = tokens[0].value
|
||||
layout_name = cast(LayoutName, value)
|
||||
layout_name = value
|
||||
try:
|
||||
self.styles._rule_layout = get_layout(layout_name)
|
||||
except MissingLayout:
|
||||
|
||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
||||
|
||||
import rich.repr
|
||||
|
||||
from typing import Iterable, Iterator, TYPE_CHECKING
|
||||
from typing import Iterator, TYPE_CHECKING
|
||||
|
||||
|
||||
from .match import match
|
||||
@@ -26,6 +26,10 @@ if TYPE_CHECKING:
|
||||
from ..dom import DOMNode
|
||||
|
||||
|
||||
class EmptyQueryError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
@rich.repr.auto(angular=True)
|
||||
class DOMQuery:
|
||||
def __init__(
|
||||
@@ -97,7 +101,10 @@ class DOMQuery:
|
||||
DOMNode: A DOM Node.
|
||||
"""
|
||||
# TODO: Better response to empty query than an IndexError
|
||||
return self._nodes[0]
|
||||
if self._nodes:
|
||||
return self._nodes[0]
|
||||
else:
|
||||
raise EmptyQueryError("Query is empty")
|
||||
|
||||
def add_class(self, *class_names: str) -> DOMQuery:
|
||||
"""Add the given class name(s) to nodes."""
|
||||
@@ -116,3 +123,28 @@ class DOMQuery:
|
||||
for node in self._nodes:
|
||||
node.toggle_class(*class_names)
|
||||
return self
|
||||
|
||||
def set_styles(self, css: str | None = None, **styles: str) -> DOMQuery:
|
||||
"""Set styles on matched nodes.
|
||||
|
||||
Args:
|
||||
css (str, optional): CSS declarations to parser, or None. Defaults to None.
|
||||
"""
|
||||
for node in self._nodes:
|
||||
node.set_styles(css, **styles)
|
||||
return self
|
||||
|
||||
def refresh(self, repaint: bool = True, layout: bool = False) -> DOMQuery:
|
||||
|
||||
"""Refresh matched nodes.
|
||||
|
||||
Args:
|
||||
repaint (bool): Repaint node(s). defaults to True.
|
||||
layout (bool): Layout node(s). Defaults to False.
|
||||
|
||||
Returns:
|
||||
[type]: [description]
|
||||
"""
|
||||
for node in self._nodes:
|
||||
node.refresh(repaint=repaint, layout=layout)
|
||||
return self
|
||||
|
||||
@@ -145,6 +145,10 @@ class ScalarOffset(NamedTuple):
|
||||
x: Scalar
|
||||
y: Scalar
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
x, y = self
|
||||
return bool(x.value or y.value)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield None, str(self.x)
|
||||
yield None, str(self.y)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from functools import lru_cache
|
||||
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
||||
@@ -10,6 +9,8 @@ from rich.color import Color
|
||||
from rich.style import Style
|
||||
|
||||
from ._style_properties import (
|
||||
Edges,
|
||||
BorderDefinition,
|
||||
BorderProperty,
|
||||
BoxProperty,
|
||||
ColorProperty,
|
||||
@@ -39,7 +40,8 @@ from .types import Display, Edge, Visibility
|
||||
from .types import Specificity3, Specificity4
|
||||
from .. import log
|
||||
from .._animator import Animation, EasingFunction
|
||||
from ..geometry import Spacing
|
||||
from ..geometry import Spacing, SpacingDimensions
|
||||
from .._box import BoxType
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -99,6 +101,10 @@ class Styles:
|
||||
|
||||
important: set[str] = field(default_factory=set)
|
||||
|
||||
def has_rule(self, rule: str) -> bool:
|
||||
"""Check if a rule has been set."""
|
||||
return getattr(self, f"_rule_{rule}") != None
|
||||
|
||||
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||
layout = LayoutProperty()
|
||||
@@ -194,32 +200,15 @@ class Styles:
|
||||
self._layout_required = 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
|
||||
|
||||
@property
|
||||
def has_border(self) -> bool:
|
||||
"""Check in a border is present."""
|
||||
return any(edge for edge, _style in self.border)
|
||||
|
||||
@property
|
||||
def has_padding(self) -> bool:
|
||||
return self._rule_padding is not None
|
||||
|
||||
@property
|
||||
def has_margin(self) -> bool:
|
||||
return self._rule_margin is not None
|
||||
|
||||
@property
|
||||
def has_outline(self) -> bool:
|
||||
"""Check if an outline is present."""
|
||||
return any(edge for edge, _style in self.outline)
|
||||
|
||||
@property
|
||||
def has_offset(self) -> bool:
|
||||
return self._rule_offset is not None
|
||||
|
||||
def get_transition(self, key: str) -> Transition | None:
|
||||
if key in self.ANIMATABLE:
|
||||
return self.transitions.get(key, None)
|
||||
@@ -281,21 +270,16 @@ class Styles:
|
||||
if self.important:
|
||||
yield "important", self.important
|
||||
|
||||
@classmethod
|
||||
def combine(cls, style1: Styles, style2: Styles) -> Styles:
|
||||
"""Combine rule with another to produce a new rule.
|
||||
def merge(self, other: Styles) -> None:
|
||||
"""Merge values from another Styles.
|
||||
|
||||
Args:
|
||||
style1 (Style): A style.
|
||||
style2 (Style): Second style.
|
||||
|
||||
Returns:
|
||||
Style: New rule with attributes of style2 overriding style1
|
||||
other (Styles): A Styles object.
|
||||
"""
|
||||
result = cls()
|
||||
for name in INTERNAL_RULE_NAMES:
|
||||
setattr(result, name, getattr(style1, name) or getattr(style2, name))
|
||||
return result
|
||||
value = getattr(other, name)
|
||||
if value is not None:
|
||||
setattr(self, name, value)
|
||||
|
||||
@property
|
||||
def css_lines(self) -> list[str]:
|
||||
@@ -378,6 +362,8 @@ class Styles:
|
||||
append_declaration("layers", " ".join(self.layers))
|
||||
if self._rule_layer is not None:
|
||||
append_declaration("layer", self.layer)
|
||||
if self._rule_layout is not None:
|
||||
append_declaration("layout", self.layout.name)
|
||||
if self._rule_text_color or self._rule_text_background or self._rule_text_style:
|
||||
append_declaration("text", str(self.text))
|
||||
|
||||
@@ -409,6 +395,149 @@ class Styles:
|
||||
RULE_NAMES = [name[6:] for name in dir(Styles) if name.startswith("_rule_")]
|
||||
INTERNAL_RULE_NAMES = [f"_rule_{name}" for name in RULE_NAMES]
|
||||
|
||||
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
GetType = TypeVar("GetType")
|
||||
SetType = TypeVar("SetType")
|
||||
|
||||
|
||||
class StyleViewProperty(Generic[GetType, SetType]):
|
||||
"""Presents a view of a base Styles object, plus inline styles."""
|
||||
|
||||
def __set_name__(self, owner: StylesView, name: str) -> None:
|
||||
self._name = name
|
||||
self._internal_name = f"_rule_{name}"
|
||||
|
||||
def __set__(self, obj: StylesView, value: SetType) -> None:
|
||||
setattr(obj._inline_styles, self._name, value)
|
||||
|
||||
def __get__(
|
||||
self, obj: StylesView, objtype: type[StylesView] | None = None
|
||||
) -> GetType:
|
||||
styles_value = getattr(obj._inline_styles, self._internal_name, None)
|
||||
if styles_value is None:
|
||||
return getattr(obj._base_styles, self._name)
|
||||
return styles_value
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class StylesView:
|
||||
"""Presents a combined view of two Styles object: a base Styles and inline Styles."""
|
||||
|
||||
def __init__(self, base: Styles, inline_styles: Styles) -> None:
|
||||
self._base_styles = base
|
||||
self._inline_styles = inline_styles
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
for rule_name in RULE_NAMES:
|
||||
if self.has_rule(rule_name):
|
||||
yield rule_name, getattr(self, rule_name)
|
||||
|
||||
@property
|
||||
def gutter(self) -> Spacing:
|
||||
"""Get the gutter (additional space reserved for margin / padding / border).
|
||||
|
||||
Returns:
|
||||
Spacing: Space around edges.
|
||||
"""
|
||||
gutter = self.margin + self.padding + self.border.spacing
|
||||
return gutter
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset the inline styles."""
|
||||
self._inline_styles.reset()
|
||||
|
||||
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 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)
|
||||
|
||||
@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
|
||||
|
||||
display: StyleViewProperty[str, str | None] = StyleViewProperty()
|
||||
visibility: StyleViewProperty[str, str | None] = StyleViewProperty()
|
||||
layout: StyleViewProperty[Layout | None, str | Layout] = StyleViewProperty()
|
||||
text: StyleViewProperty[Style, Style | str | None] = StyleViewProperty()
|
||||
color: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()
|
||||
background: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()
|
||||
style: StyleViewProperty[Style, str | None] = StyleViewProperty()
|
||||
|
||||
padding: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty()
|
||||
margin: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty()
|
||||
offset: StyleViewProperty[
|
||||
ScalarOffset, tuple[int | str, int | str] | ScalarOffset
|
||||
] = StyleViewProperty()
|
||||
|
||||
border: StyleViewProperty[Edges, BorderDefinition | None] = StyleViewProperty()
|
||||
border_top: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
border_right: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
border_bottom: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
border_left: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
|
||||
outline: StyleViewProperty[Edges, BorderDefinition | None] = StyleViewProperty()
|
||||
outline_top: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
outline_right: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
outline_bottom: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
outline_left: StyleViewProperty[
|
||||
tuple[BoxType, Style], tuple[BoxType, str | Color | Style] | None
|
||||
] = StyleViewProperty()
|
||||
|
||||
width: StyleViewProperty[
|
||||
Scalar | None, float | Scalar | str | None
|
||||
] = StyleViewProperty()
|
||||
height: StyleViewProperty[
|
||||
Scalar | None, float | Scalar | str | None
|
||||
] = StyleViewProperty()
|
||||
min_width: StyleViewProperty[
|
||||
Scalar | None, float | Scalar | str | None
|
||||
] = StyleViewProperty()
|
||||
min_height: StyleViewProperty[
|
||||
Scalar | None, float | Scalar | str | None
|
||||
] = StyleViewProperty()
|
||||
|
||||
dock: StyleViewProperty[str, str | None] = StyleViewProperty()
|
||||
docks: StyleViewProperty[
|
||||
tuple[DockGroup, ...], Iterable[DockGroup] | None
|
||||
] = StyleViewProperty()
|
||||
|
||||
layer: StyleViewProperty[str, str | None] = StyleViewProperty()
|
||||
layers: StyleViewProperty[
|
||||
tuple[str, ...], str | tuple[str] | None
|
||||
] = StyleViewProperty()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
styles = Styles()
|
||||
|
||||
|
||||
@@ -132,11 +132,11 @@ class Stylesheet:
|
||||
_check_rule = self._check_rule
|
||||
|
||||
# TODO: The line below breaks inline styles and animations
|
||||
node.styles.reset()
|
||||
node._css_styles.reset()
|
||||
|
||||
# Collect default node CSS rules
|
||||
for key, default_specificity, value in node._default_rules:
|
||||
rule_attributes[key].append((default_specificity, value))
|
||||
# for key, default_specificity, value in node._default_rules:
|
||||
# rule_attributes[key].append((default_specificity, value))
|
||||
|
||||
# Collect the rules defined in the stylesheet
|
||||
for rule in self.rules:
|
||||
@@ -153,7 +153,7 @@ class Stylesheet:
|
||||
for name, specificity_rules in rule_attributes.items()
|
||||
]
|
||||
|
||||
node.styles.apply_rules(node_rules)
|
||||
node._css_styles.apply_rules(node_rules)
|
||||
|
||||
def update(self, root: DOMNode) -> None:
|
||||
"""Update a node and its children."""
|
||||
|
||||
@@ -12,7 +12,8 @@ from ._node_list import NodeList
|
||||
from .css._error_tools import friendly_list
|
||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||
from .css.errors import StyleValueError
|
||||
from .css.styles import Styles
|
||||
from .css.styles import Styles, StylesView
|
||||
from .css.parse import parse_declarations
|
||||
from .message_pump import MessagePump
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -38,10 +39,10 @@ class DOMNode(MessagePump):
|
||||
self._id = id
|
||||
self._classes: set[str] = set()
|
||||
self.children = NodeList()
|
||||
self.styles: Styles = Styles(self)
|
||||
self._css_styles: Styles = Styles(self)
|
||||
self._inline_styles: Styles = Styles.parse(self.STYLES, repr(self))
|
||||
self.styles = StylesView(self._css_styles, self._inline_styles)
|
||||
super().__init__()
|
||||
self.default_styles = Styles.parse(self.STYLES, repr(self))
|
||||
self._default_rules = self.default_styles.extract_rules((0, 0, 0))
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "name", self._name, None
|
||||
@@ -49,6 +50,10 @@ class DOMNode(MessagePump):
|
||||
if self._classes:
|
||||
yield "classes", self._classes
|
||||
|
||||
@property
|
||||
def inline_styles(self) -> Styles:
|
||||
return self._inline_styles
|
||||
|
||||
@property
|
||||
def parent(self) -> DOMNode:
|
||||
"""Get the parent node.
|
||||
@@ -240,7 +245,7 @@ class DOMNode(MessagePump):
|
||||
from .widget import Widget
|
||||
|
||||
for node in self.walk_children():
|
||||
node.styles = Styles(node=node)
|
||||
node._css_styles.reset()
|
||||
if isinstance(node, Widget):
|
||||
# node.clear_render_cache()
|
||||
node._repaint_required = True
|
||||
@@ -289,6 +294,16 @@ class DOMNode(MessagePump):
|
||||
|
||||
return DOMQuery(self, selector)
|
||||
|
||||
def set_styles(self, css: str | None = None, **styles) -> None:
|
||||
"""Set custom styles on this object."""
|
||||
kwarg_css = "\n".join(
|
||||
f"{key.replace('_', '-')}: {value}" for key, value in styles.items()
|
||||
)
|
||||
apply_css = f"{css or ''}\n{kwarg_css}\n"
|
||||
new_styles = parse_declarations(apply_css, f"<custom styles for ${self!r}>")
|
||||
self._inline_styles.merge(new_styles)
|
||||
self.refresh()
|
||||
|
||||
def has_class(self, *class_names: str) -> bool:
|
||||
return self._classes.issuperset(class_names)
|
||||
|
||||
@@ -309,3 +324,6 @@ class DOMNode(MessagePump):
|
||||
"""Check for pseudo class (such as hover, focus etc)"""
|
||||
has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
|
||||
return has_pseudo_classes
|
||||
|
||||
def refresh(self, repaint: bool = True, layout: bool = False) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -57,13 +57,14 @@ class WidgetPlacement(NamedTuple):
|
||||
|
||||
def apply_margin(self) -> "WidgetPlacement":
|
||||
region, widget, order = self
|
||||
styles = widget.styles
|
||||
if styles.has_margin:
|
||||
return WidgetPlacement(
|
||||
region=region.shrink(styles.margin),
|
||||
widget=widget,
|
||||
order=order,
|
||||
)
|
||||
if widget is not None:
|
||||
styles = widget.styles
|
||||
if any(styles.margin):
|
||||
return WidgetPlacement(
|
||||
region=region.shrink(styles.margin),
|
||||
widget=widget,
|
||||
order=order,
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class LayoutMap:
|
||||
return
|
||||
|
||||
layout_offset = Offset(0, 0)
|
||||
if widget.styles.has_offset:
|
||||
if any(widget.styles.offset):
|
||||
layout_offset = widget.styles.offset.resolve(region.size, clip.size)
|
||||
|
||||
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
|
||||
|
||||
@@ -36,10 +36,16 @@ class Dock(NamedTuple):
|
||||
|
||||
|
||||
class DockLayout(Layout):
|
||||
|
||||
name = "dock"
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._docks: list[Dock] | None = None
|
||||
|
||||
def __repr__(self):
|
||||
return "<DockLayout>"
|
||||
|
||||
def get_docks(self, view: View) -> list[Dock]:
|
||||
groups: dict[str, list[Widget]] = defaultdict(list)
|
||||
for child in view.children:
|
||||
@@ -71,17 +77,15 @@ class DockLayout(Layout):
|
||||
|
||||
return (
|
||||
DockOptions(
|
||||
styles.width.cells if styles._rule_width is not None else None,
|
||||
styles.width.fraction if styles._rule_width is not None else 1,
|
||||
styles.min_width.cells if styles._rule_min_width is not None else 1,
|
||||
styles.width.cells if styles.has_rule("width") else None,
|
||||
styles.width.fraction if styles.has_rule("width") else 1,
|
||||
styles.min_width.cells if styles.has_rule("min_width") else 1,
|
||||
)
|
||||
if edge in ("left", "right")
|
||||
else DockOptions(
|
||||
styles.height.cells if styles._rule_height is not None else None,
|
||||
styles.height.fraction if styles._rule_height is not None else 1,
|
||||
styles.min_height.cells
|
||||
if styles._rule_min_height is not None
|
||||
else 1,
|
||||
styles.height.cells if styles.has_rule("height") else None,
|
||||
styles.height.fraction if styles.has_rule("height") else 1,
|
||||
styles.min_height.cells if styles.has_rule("min_height") else 1,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ if sys.version_info >= (3, 8):
|
||||
else:
|
||||
from typing_extensions import Literal
|
||||
|
||||
LayoutName = Literal["dock", "grid", "vertical"]
|
||||
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
||||
|
||||
|
||||
@@ -18,7 +17,7 @@ class MissingLayout(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_layout(name: LayoutName) -> Layout:
|
||||
def get_layout(name: str) -> Layout:
|
||||
"""Get a named layout object.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -12,12 +12,14 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class VerticalLayout(Layout):
|
||||
name = "vertical"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
auto_width: bool = False,
|
||||
z: int = 0,
|
||||
gutter: SpacingDimensions = (0, 0, 0, 0)
|
||||
gutter: SpacingDimensions = (0, 0, 0, 0),
|
||||
):
|
||||
self.auto_width = auto_width
|
||||
self.z = z
|
||||
|
||||
@@ -311,6 +311,22 @@ class MessagePump:
|
||||
else:
|
||||
return False
|
||||
|
||||
async def dispatch_key(self, event: events.Key) -> None:
|
||||
"""Dispatch a key event to method.
|
||||
|
||||
This method will call the method named 'key_<event.key>' if it exists. This key method
|
||||
should return True if the key event was handled. Otherwise it should return False or None.
|
||||
|
||||
Args:
|
||||
event (events.Key): A key event.
|
||||
"""
|
||||
|
||||
key_method = getattr(self, f"key_{event.key}", None)
|
||||
if key_method is not None:
|
||||
key_result = await invoke(key_method, event)
|
||||
if key_result is None or key_result:
|
||||
event.prevent_default()
|
||||
|
||||
async def on_timer(self, event: events.Timer) -> None:
|
||||
event.prevent_default()
|
||||
event.stop()
|
||||
|
||||
@@ -33,12 +33,12 @@ class View(Widget):
|
||||
)
|
||||
super().__init__(name=name, id=id)
|
||||
|
||||
def __init_subclass__(
|
||||
cls, layout: Callable[[], Layout] | None = None, **kwargs
|
||||
) -> None:
|
||||
if layout is not None:
|
||||
cls.layout_factory = layout
|
||||
super().__init_subclass__(**kwargs)
|
||||
# def __init_subclass__(
|
||||
# cls, layout: Callable[[], Layout] | None = None, **kwargs
|
||||
# ) -> None:
|
||||
# if layout is not None:
|
||||
# cls.layout_factory = layout
|
||||
# super().__init_subclass__(**kwargs)
|
||||
|
||||
background: Reactive[str] = Reactive("")
|
||||
scroll_x: Reactive[int] = Reactive(0)
|
||||
@@ -51,22 +51,26 @@ class View(Widget):
|
||||
|
||||
@property
|
||||
def layout(self) -> Layout:
|
||||
"""Convenience property for accessing ``view.styles.layout``.
|
||||
"""Convenience property for accessing ``self.styles.layout``.
|
||||
|
||||
Returns: The Layout associated with this view
|
||||
"""
|
||||
# self.log("I", self._inline_styles)
|
||||
# self.log("C", self._css_styles)
|
||||
# self.log("S", self.styles)
|
||||
assert self.styles.layout
|
||||
return self.styles.layout
|
||||
|
||||
@layout.setter
|
||||
def layout(self, new_value: Layout) -> None:
|
||||
"""Convenience property setter for setting ``view.styles.layout``.
|
||||
Args:
|
||||
new_value:
|
||||
# @layout.setter
|
||||
# def layout(self, new_value: Layout) -> None:
|
||||
# """Convenience property setter for setting ``view.styles.layout``.
|
||||
# Args:
|
||||
# new_value:
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.styles.layout = new_value
|
||||
# Returns:
|
||||
# None
|
||||
# """
|
||||
# self.styles.layout = new_value
|
||||
|
||||
@property
|
||||
def scroll(self) -> Offset:
|
||||
|
||||
@@ -24,7 +24,7 @@ class WindowView(View, layout=VerticalLayout):
|
||||
*,
|
||||
auto_width: bool = False,
|
||||
gutter: SpacingDimensions = (0, 0),
|
||||
name: str | None = None
|
||||
name: str | None = None,
|
||||
) -> None:
|
||||
layout = VerticalLayout(gutter=gutter, auto_width=auto_width)
|
||||
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
||||
|
||||
@@ -33,6 +33,7 @@ from .message import Message
|
||||
from .messages import Layout, Update
|
||||
from .reactive import watch
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .view import View
|
||||
|
||||
@@ -93,7 +94,6 @@ class Widget(DOMNode):
|
||||
pseudo_classes = self.pseudo_classes
|
||||
if pseudo_classes:
|
||||
yield "pseudo_classes", pseudo_classes
|
||||
yield "outline", self.styles.outline
|
||||
|
||||
def __rich__(self) -> RenderableType:
|
||||
renderable = self.render_styled()
|
||||
@@ -155,6 +155,7 @@ class Widget(DOMNode):
|
||||
|
||||
renderable = self.render()
|
||||
styles = self.styles
|
||||
|
||||
parent_text_style = self.parent.text_style
|
||||
|
||||
text_style = styles.text
|
||||
@@ -162,15 +163,15 @@ class Widget(DOMNode):
|
||||
if renderable_text_style:
|
||||
renderable = Styled(renderable, renderable_text_style)
|
||||
|
||||
if styles.has_padding:
|
||||
if any(styles.padding):
|
||||
renderable = Padding(
|
||||
renderable, styles.padding, style=renderable_text_style
|
||||
)
|
||||
|
||||
if styles.has_border:
|
||||
if any(styles.border):
|
||||
renderable = Border(renderable, styles.border, style=renderable_text_style)
|
||||
|
||||
if styles.has_outline:
|
||||
if any(styles.outline):
|
||||
renderable = Border(
|
||||
renderable,
|
||||
styles.outline,
|
||||
@@ -302,7 +303,7 @@ class Widget(DOMNode):
|
||||
if not self.check_message_enabled(message):
|
||||
return True
|
||||
if not self.is_running:
|
||||
self.log(self, "IS NOT RUNNING")
|
||||
self.log(self, f"IS NOT RUNNING, {message!r} not sent")
|
||||
return await super().post_message(message)
|
||||
|
||||
async def on_resize(self, event: events.Resize) -> None:
|
||||
@@ -343,19 +344,6 @@ class Widget(DOMNode):
|
||||
async def broker_event(self, event_name: str, event: events.Event) -> bool:
|
||||
return await self.app.broker_event(event_name, event, default_namespace=self)
|
||||
|
||||
async def dispatch_key(self, event: events.Key) -> None:
|
||||
"""Dispatch a key event to method.
|
||||
|
||||
This method will call the method named 'key_<event.key>' if it exists.
|
||||
|
||||
Args:
|
||||
event (events.Key): A key event.
|
||||
"""
|
||||
|
||||
key_method = getattr(self, f"key_{event.key}", None)
|
||||
if key_method is not None:
|
||||
await invoke(key_method, event)
|
||||
|
||||
async def on_mouse_down(self, event: events.MouseUp) -> None:
|
||||
await self.broker_event("mouse.down", event)
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ class ScrollView(View):
|
||||
name: str | None = None,
|
||||
style: StyleType = "",
|
||||
fluid: bool = True,
|
||||
gutter: SpacingDimensions = (0, 0)
|
||||
gutter: SpacingDimensions = (0, 0),
|
||||
) -> None:
|
||||
from ..views import WindowView
|
||||
|
||||
|
||||
Reference in New Issue
Block a user