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]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^6.2.3"
|
pytest = "^6.2.3"
|
||||||
black = "^21.11b1"
|
black = "^22.1.0"
|
||||||
mypy = "^0.910"
|
mypy = "^0.910"
|
||||||
pytest-cov = "^2.12.1"
|
pytest-cov = "^2.12.1"
|
||||||
mkdocs = "^1.2.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 platform
|
||||||
import warnings
|
import warnings
|
||||||
from asyncio import AbstractEventLoop
|
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
|
import rich.repr
|
||||||
from rich.console import Console, RenderableType
|
from rich.console import Console, RenderableType
|
||||||
@@ -35,6 +35,11 @@ from .reactive import Reactive
|
|||||||
from .view import View
|
from .view import View
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
|
from .css.query import EmptyQueryError
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .css.query import DOMQuery
|
||||||
|
|
||||||
PLATFORM = platform.system()
|
PLATFORM = platform.system()
|
||||||
WINDOWS = PLATFORM == "Windows"
|
WINDOWS = PLATFORM == "Windows"
|
||||||
|
|
||||||
@@ -260,6 +265,27 @@ class App(DOMNode):
|
|||||||
self.stylesheet.update(self)
|
self.stylesheet.update(self)
|
||||||
self.view.refresh(layout=True)
|
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:
|
def update_styles(self) -> None:
|
||||||
"""Request update of styles.
|
"""Request update of styles.
|
||||||
|
|
||||||
@@ -513,6 +539,10 @@ class App(DOMNode):
|
|||||||
"""
|
"""
|
||||||
return self.view.get_widget_at(x, y)
|
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:
|
async def press(self, key: str) -> bool:
|
||||||
"""Handle a key press.
|
"""Handle a key press.
|
||||||
|
|
||||||
@@ -640,7 +670,7 @@ class App(DOMNode):
|
|||||||
1 / 0
|
1 / 0
|
||||||
|
|
||||||
async def action_bell(self) -> None:
|
async def action_bell(self) -> None:
|
||||||
self.console.bell()
|
self.bell()
|
||||||
|
|
||||||
async def action_add_class_(self, selector: str, class_name: str) -> None:
|
async def action_add_class_(self, selector: str, class_name: str) -> None:
|
||||||
self.view.query(selector).add_class(class_name)
|
self.view.query(selector).add_class(class_name)
|
||||||
|
|||||||
@@ -33,8 +33,13 @@ if TYPE_CHECKING:
|
|||||||
from ..layout import Layout
|
from ..layout import Layout
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
from .styles import DockGroup
|
from .styles import DockGroup
|
||||||
|
|
||||||
from .._box import BoxType
|
from .._box import BoxType
|
||||||
from ..layouts.factory import LayoutName
|
|
||||||
|
BorderDefinition = (
|
||||||
|
Sequence[tuple[BoxType, str | Color | Style] | None]
|
||||||
|
| tuple[BoxType, str | Color | Style]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class ScalarProperty:
|
class ScalarProperty:
|
||||||
@@ -228,9 +233,7 @@ class BorderProperty:
|
|||||||
def __set__(
|
def __set__(
|
||||||
self,
|
self,
|
||||||
obj: Styles,
|
obj: Styles,
|
||||||
border: Sequence[tuple[BoxType, str | Color | Style] | None]
|
border: BorderDefinition | None,
|
||||||
| tuple[BoxType, str | Color | Style]
|
|
||||||
| None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the border
|
"""Set the border
|
||||||
|
|
||||||
@@ -443,7 +446,9 @@ class LayoutProperty:
|
|||||||
def __set_name__(self, owner: Styles, name: str) -> None:
|
def __set_name__(self, owner: Styles, name: str) -> None:
|
||||||
self._internal_name = f"_rule_{name}"
|
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:
|
Args:
|
||||||
obj (Styles): The Styles object
|
obj (Styles): The Styles object
|
||||||
@@ -453,13 +458,14 @@ class LayoutProperty:
|
|||||||
"""
|
"""
|
||||||
return getattr(obj, self._internal_name)
|
return getattr(obj, self._internal_name)
|
||||||
|
|
||||||
def __set__(self, obj: Styles, layout: LayoutName | Layout):
|
def __set__(self, obj: Styles, layout: str | Layout):
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
obj (Styles): The Styles object.
|
obj (Styles): The Styles object.
|
||||||
layout (LayoutName | Layout): The layout to use. You can supply a ``LayoutName``
|
layout (str | Layout): The layout to use. You can supply a the name of the layout
|
||||||
(a string literal such as ``"dock"``) or a ``Layout`` object.
|
or a ``Layout`` object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ..layouts.factory import get_layout, Layout # Prevents circular import
|
from ..layouts.factory import get_layout, Layout # Prevents circular import
|
||||||
|
|
||||||
obj.refresh(layout=True)
|
obj.refresh(layout=True)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ from .types import Edge, Display, Visibility
|
|||||||
from .._duration import _duration_as_seconds
|
from .._duration import _duration_as_seconds
|
||||||
from .._easing import EASING
|
from .._easing import EASING
|
||||||
from ..geometry import Spacing, SpacingDimensions
|
from ..geometry import Spacing, SpacingDimensions
|
||||||
from ..layouts.factory import get_layout, LayoutName, MissingLayout, LAYOUT_MAP
|
|
||||||
|
|
||||||
|
|
||||||
class StylesBuilder:
|
class StylesBuilder:
|
||||||
@@ -285,12 +284,14 @@ class StylesBuilder:
|
|||||||
self.styles._rule_offset = ScalarOffset(x, y)
|
self.styles._rule_offset = ScalarOffset(x, y)
|
||||||
|
|
||||||
def process_layout(self, name: str, tokens: list[Token], important: bool) -> None:
|
def process_layout(self, name: str, tokens: list[Token], important: bool) -> None:
|
||||||
|
from ..layouts.factory import get_layout, MissingLayout, LAYOUT_MAP
|
||||||
|
|
||||||
if tokens:
|
if tokens:
|
||||||
if len(tokens) != 1:
|
if len(tokens) != 1:
|
||||||
self.error(name, tokens[0], "unexpected tokens in declaration")
|
self.error(name, tokens[0], "unexpected tokens in declaration")
|
||||||
else:
|
else:
|
||||||
value = tokens[0].value
|
value = tokens[0].value
|
||||||
layout_name = cast(LayoutName, value)
|
layout_name = value
|
||||||
try:
|
try:
|
||||||
self.styles._rule_layout = get_layout(layout_name)
|
self.styles._rule_layout = get_layout(layout_name)
|
||||||
except MissingLayout:
|
except MissingLayout:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
|
|
||||||
from typing import Iterable, Iterator, TYPE_CHECKING
|
from typing import Iterator, TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
from .match import match
|
from .match import match
|
||||||
@@ -26,6 +26,10 @@ if TYPE_CHECKING:
|
|||||||
from ..dom import DOMNode
|
from ..dom import DOMNode
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyQueryError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto(angular=True)
|
@rich.repr.auto(angular=True)
|
||||||
class DOMQuery:
|
class DOMQuery:
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -97,7 +101,10 @@ class DOMQuery:
|
|||||||
DOMNode: A DOM Node.
|
DOMNode: A DOM Node.
|
||||||
"""
|
"""
|
||||||
# TODO: Better response to empty query than an IndexError
|
# TODO: Better response to empty query than an IndexError
|
||||||
|
if self._nodes:
|
||||||
return self._nodes[0]
|
return self._nodes[0]
|
||||||
|
else:
|
||||||
|
raise EmptyQueryError("Query is empty")
|
||||||
|
|
||||||
def add_class(self, *class_names: str) -> DOMQuery:
|
def add_class(self, *class_names: str) -> DOMQuery:
|
||||||
"""Add the given class name(s) to nodes."""
|
"""Add the given class name(s) to nodes."""
|
||||||
@@ -116,3 +123,28 @@ class DOMQuery:
|
|||||||
for node in self._nodes:
|
for node in self._nodes:
|
||||||
node.toggle_class(*class_names)
|
node.toggle_class(*class_names)
|
||||||
return self
|
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
|
x: Scalar
|
||||||
y: Scalar
|
y: Scalar
|
||||||
|
|
||||||
|
def __bool__(self) -> bool:
|
||||||
|
x, y = self
|
||||||
|
return bool(x.value or y.value)
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield None, str(self.x)
|
yield None, str(self.x)
|
||||||
yield None, str(self.y)
|
yield None, str(self.y)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
|
||||||
@@ -10,6 +9,8 @@ from rich.color import Color
|
|||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
from ._style_properties import (
|
from ._style_properties import (
|
||||||
|
Edges,
|
||||||
|
BorderDefinition,
|
||||||
BorderProperty,
|
BorderProperty,
|
||||||
BoxProperty,
|
BoxProperty,
|
||||||
ColorProperty,
|
ColorProperty,
|
||||||
@@ -39,7 +40,8 @@ from .types import Display, Edge, Visibility
|
|||||||
from .types import Specificity3, Specificity4
|
from .types import Specificity3, Specificity4
|
||||||
from .. import log
|
from .. import log
|
||||||
from .._animator import Animation, EasingFunction
|
from .._animator import Animation, EasingFunction
|
||||||
from ..geometry import Spacing
|
from ..geometry import Spacing, SpacingDimensions
|
||||||
|
from .._box import BoxType
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -99,6 +101,10 @@ class Styles:
|
|||||||
|
|
||||||
important: set[str] = field(default_factory=set)
|
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")
|
display = StringEnumProperty(VALID_DISPLAY, "block")
|
||||||
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
|
||||||
layout = LayoutProperty()
|
layout = LayoutProperty()
|
||||||
@@ -194,32 +200,15 @@ class Styles:
|
|||||||
self._layout_required = layout
|
self._layout_required = layout
|
||||||
|
|
||||||
def check_refresh(self) -> tuple[bool, bool]:
|
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)
|
result = (self._repaint_required, self._layout_required)
|
||||||
self._repaint_required = self._layout_required = False
|
self._repaint_required = self._layout_required = False
|
||||||
return result
|
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:
|
def get_transition(self, key: str) -> Transition | None:
|
||||||
if key in self.ANIMATABLE:
|
if key in self.ANIMATABLE:
|
||||||
return self.transitions.get(key, None)
|
return self.transitions.get(key, None)
|
||||||
@@ -281,21 +270,16 @@ class Styles:
|
|||||||
if self.important:
|
if self.important:
|
||||||
yield "important", self.important
|
yield "important", self.important
|
||||||
|
|
||||||
@classmethod
|
def merge(self, other: Styles) -> None:
|
||||||
def combine(cls, style1: Styles, style2: Styles) -> Styles:
|
"""Merge values from another Styles.
|
||||||
"""Combine rule with another to produce a new rule.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
style1 (Style): A style.
|
other (Styles): A Styles object.
|
||||||
style2 (Style): Second style.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Style: New rule with attributes of style2 overriding style1
|
|
||||||
"""
|
"""
|
||||||
result = cls()
|
|
||||||
for name in INTERNAL_RULE_NAMES:
|
for name in INTERNAL_RULE_NAMES:
|
||||||
setattr(result, name, getattr(style1, name) or getattr(style2, name))
|
value = getattr(other, name)
|
||||||
return result
|
if value is not None:
|
||||||
|
setattr(self, name, value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def css_lines(self) -> list[str]:
|
def css_lines(self) -> list[str]:
|
||||||
@@ -378,6 +362,8 @@ class Styles:
|
|||||||
append_declaration("layers", " ".join(self.layers))
|
append_declaration("layers", " ".join(self.layers))
|
||||||
if self._rule_layer is not None:
|
if self._rule_layer is not None:
|
||||||
append_declaration("layer", self.layer)
|
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:
|
if self._rule_text_color or self._rule_text_background or self._rule_text_style:
|
||||||
append_declaration("text", str(self.text))
|
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_")]
|
RULE_NAMES = [name[6:] for name in dir(Styles) if name.startswith("_rule_")]
|
||||||
INTERNAL_RULE_NAMES = [f"_rule_{name}" for name in RULE_NAMES]
|
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__":
|
if __name__ == "__main__":
|
||||||
styles = Styles()
|
styles = Styles()
|
||||||
|
|
||||||
|
|||||||
@@ -132,11 +132,11 @@ class Stylesheet:
|
|||||||
_check_rule = self._check_rule
|
_check_rule = self._check_rule
|
||||||
|
|
||||||
# TODO: The line below breaks inline styles and animations
|
# TODO: The line below breaks inline styles and animations
|
||||||
node.styles.reset()
|
node._css_styles.reset()
|
||||||
|
|
||||||
# Collect default node CSS rules
|
# Collect default node CSS rules
|
||||||
for key, default_specificity, value in node._default_rules:
|
# for key, default_specificity, value in node._default_rules:
|
||||||
rule_attributes[key].append((default_specificity, value))
|
# rule_attributes[key].append((default_specificity, value))
|
||||||
|
|
||||||
# Collect the rules defined in the stylesheet
|
# Collect the rules defined in the stylesheet
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
@@ -153,7 +153,7 @@ class Stylesheet:
|
|||||||
for name, specificity_rules in rule_attributes.items()
|
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:
|
def update(self, root: DOMNode) -> None:
|
||||||
"""Update a node and its children."""
|
"""Update a node and its children."""
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ from ._node_list import NodeList
|
|||||||
from .css._error_tools import friendly_list
|
from .css._error_tools import friendly_list
|
||||||
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
from .css.constants import VALID_DISPLAY, VALID_VISIBILITY
|
||||||
from .css.errors import StyleValueError
|
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
|
from .message_pump import MessagePump
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -38,10 +39,10 @@ class DOMNode(MessagePump):
|
|||||||
self._id = id
|
self._id = id
|
||||||
self._classes: set[str] = set()
|
self._classes: set[str] = set()
|
||||||
self.children = NodeList()
|
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__()
|
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:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield "name", self._name, None
|
yield "name", self._name, None
|
||||||
@@ -49,6 +50,10 @@ class DOMNode(MessagePump):
|
|||||||
if self._classes:
|
if self._classes:
|
||||||
yield "classes", self._classes
|
yield "classes", self._classes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inline_styles(self) -> Styles:
|
||||||
|
return self._inline_styles
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self) -> DOMNode:
|
def parent(self) -> DOMNode:
|
||||||
"""Get the parent node.
|
"""Get the parent node.
|
||||||
@@ -240,7 +245,7 @@ class DOMNode(MessagePump):
|
|||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
for node in self.walk_children():
|
for node in self.walk_children():
|
||||||
node.styles = Styles(node=node)
|
node._css_styles.reset()
|
||||||
if isinstance(node, Widget):
|
if isinstance(node, Widget):
|
||||||
# node.clear_render_cache()
|
# node.clear_render_cache()
|
||||||
node._repaint_required = True
|
node._repaint_required = True
|
||||||
@@ -289,6 +294,16 @@ class DOMNode(MessagePump):
|
|||||||
|
|
||||||
return DOMQuery(self, selector)
|
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:
|
def has_class(self, *class_names: str) -> bool:
|
||||||
return self._classes.issuperset(class_names)
|
return self._classes.issuperset(class_names)
|
||||||
|
|
||||||
@@ -309,3 +324,6 @@ class DOMNode(MessagePump):
|
|||||||
"""Check for pseudo class (such as hover, focus etc)"""
|
"""Check for pseudo class (such as hover, focus etc)"""
|
||||||
has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
|
has_pseudo_classes = self.pseudo_classes.issuperset(class_names)
|
||||||
return has_pseudo_classes
|
return has_pseudo_classes
|
||||||
|
|
||||||
|
def refresh(self, repaint: bool = True, layout: bool = False) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|||||||
@@ -57,8 +57,9 @@ class WidgetPlacement(NamedTuple):
|
|||||||
|
|
||||||
def apply_margin(self) -> "WidgetPlacement":
|
def apply_margin(self) -> "WidgetPlacement":
|
||||||
region, widget, order = self
|
region, widget, order = self
|
||||||
|
if widget is not None:
|
||||||
styles = widget.styles
|
styles = widget.styles
|
||||||
if styles.has_margin:
|
if any(styles.margin):
|
||||||
return WidgetPlacement(
|
return WidgetPlacement(
|
||||||
region=region.shrink(styles.margin),
|
region=region.shrink(styles.margin),
|
||||||
widget=widget,
|
widget=widget,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class LayoutMap:
|
|||||||
return
|
return
|
||||||
|
|
||||||
layout_offset = Offset(0, 0)
|
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)
|
layout_offset = widget.styles.offset.resolve(region.size, clip.size)
|
||||||
|
|
||||||
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
|
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
|
||||||
|
|||||||
@@ -36,10 +36,16 @@ class Dock(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class DockLayout(Layout):
|
class DockLayout(Layout):
|
||||||
|
|
||||||
|
name = "dock"
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._docks: list[Dock] | None = None
|
self._docks: list[Dock] | None = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<DockLayout>"
|
||||||
|
|
||||||
def get_docks(self, view: View) -> list[Dock]:
|
def get_docks(self, view: View) -> list[Dock]:
|
||||||
groups: dict[str, list[Widget]] = defaultdict(list)
|
groups: dict[str, list[Widget]] = defaultdict(list)
|
||||||
for child in view.children:
|
for child in view.children:
|
||||||
@@ -71,17 +77,15 @@ class DockLayout(Layout):
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
DockOptions(
|
DockOptions(
|
||||||
styles.width.cells if styles._rule_width is not None else None,
|
styles.width.cells if styles.has_rule("width") else None,
|
||||||
styles.width.fraction if styles._rule_width is not None else 1,
|
styles.width.fraction if styles.has_rule("width") else 1,
|
||||||
styles.min_width.cells if styles._rule_min_width is not None else 1,
|
styles.min_width.cells if styles.has_rule("min_width") else 1,
|
||||||
)
|
)
|
||||||
if edge in ("left", "right")
|
if edge in ("left", "right")
|
||||||
else DockOptions(
|
else DockOptions(
|
||||||
styles.height.cells if styles._rule_height is not None else None,
|
styles.height.cells if styles.has_rule("height") else None,
|
||||||
styles.height.fraction if styles._rule_height is not None else 1,
|
styles.height.fraction if styles.has_rule("height") else 1,
|
||||||
styles.min_height.cells
|
styles.min_height.cells if styles.has_rule("min_height") else 1,
|
||||||
if styles._rule_min_height is not None
|
|
||||||
else 1,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ if sys.version_info >= (3, 8):
|
|||||||
else:
|
else:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
LayoutName = Literal["dock", "grid", "vertical"]
|
|
||||||
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
LAYOUT_MAP = {"dock": DockLayout, "grid": GridLayout, "vertical": VerticalLayout}
|
||||||
|
|
||||||
|
|
||||||
@@ -18,7 +17,7 @@ class MissingLayout(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_layout(name: LayoutName) -> Layout:
|
def get_layout(name: str) -> Layout:
|
||||||
"""Get a named layout object.
|
"""Get a named layout object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class VerticalLayout(Layout):
|
class VerticalLayout(Layout):
|
||||||
|
name = "vertical"
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
auto_width: bool = False,
|
auto_width: bool = False,
|
||||||
z: int = 0,
|
z: int = 0,
|
||||||
gutter: SpacingDimensions = (0, 0, 0, 0)
|
gutter: SpacingDimensions = (0, 0, 0, 0),
|
||||||
):
|
):
|
||||||
self.auto_width = auto_width
|
self.auto_width = auto_width
|
||||||
self.z = z
|
self.z = z
|
||||||
|
|||||||
@@ -311,6 +311,22 @@ class MessagePump:
|
|||||||
else:
|
else:
|
||||||
return False
|
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:
|
async def on_timer(self, event: events.Timer) -> None:
|
||||||
event.prevent_default()
|
event.prevent_default()
|
||||||
event.stop()
|
event.stop()
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ class View(Widget):
|
|||||||
)
|
)
|
||||||
super().__init__(name=name, id=id)
|
super().__init__(name=name, id=id)
|
||||||
|
|
||||||
def __init_subclass__(
|
# def __init_subclass__(
|
||||||
cls, layout: Callable[[], Layout] | None = None, **kwargs
|
# cls, layout: Callable[[], Layout] | None = None, **kwargs
|
||||||
) -> None:
|
# ) -> None:
|
||||||
if layout is not None:
|
# if layout is not None:
|
||||||
cls.layout_factory = layout
|
# cls.layout_factory = layout
|
||||||
super().__init_subclass__(**kwargs)
|
# super().__init_subclass__(**kwargs)
|
||||||
|
|
||||||
background: Reactive[str] = Reactive("")
|
background: Reactive[str] = Reactive("")
|
||||||
scroll_x: Reactive[int] = Reactive(0)
|
scroll_x: Reactive[int] = Reactive(0)
|
||||||
@@ -51,22 +51,26 @@ class View(Widget):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def layout(self) -> Layout:
|
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
|
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
|
return self.styles.layout
|
||||||
|
|
||||||
@layout.setter
|
# @layout.setter
|
||||||
def layout(self, new_value: Layout) -> None:
|
# def layout(self, new_value: Layout) -> None:
|
||||||
"""Convenience property setter for setting ``view.styles.layout``.
|
# """Convenience property setter for setting ``view.styles.layout``.
|
||||||
Args:
|
# Args:
|
||||||
new_value:
|
# new_value:
|
||||||
|
|
||||||
Returns:
|
# Returns:
|
||||||
None
|
# None
|
||||||
"""
|
# """
|
||||||
self.styles.layout = new_value
|
# self.styles.layout = new_value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scroll(self) -> Offset:
|
def scroll(self) -> Offset:
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class WindowView(View, layout=VerticalLayout):
|
|||||||
*,
|
*,
|
||||||
auto_width: bool = False,
|
auto_width: bool = False,
|
||||||
gutter: SpacingDimensions = (0, 0),
|
gutter: SpacingDimensions = (0, 0),
|
||||||
name: str | None = None
|
name: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
layout = VerticalLayout(gutter=gutter, auto_width=auto_width)
|
layout = VerticalLayout(gutter=gutter, auto_width=auto_width)
|
||||||
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from .message import Message
|
|||||||
from .messages import Layout, Update
|
from .messages import Layout, Update
|
||||||
from .reactive import watch
|
from .reactive import watch
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .view import View
|
from .view import View
|
||||||
|
|
||||||
@@ -93,7 +94,6 @@ class Widget(DOMNode):
|
|||||||
pseudo_classes = self.pseudo_classes
|
pseudo_classes = self.pseudo_classes
|
||||||
if pseudo_classes:
|
if pseudo_classes:
|
||||||
yield "pseudo_classes", pseudo_classes
|
yield "pseudo_classes", pseudo_classes
|
||||||
yield "outline", self.styles.outline
|
|
||||||
|
|
||||||
def __rich__(self) -> RenderableType:
|
def __rich__(self) -> RenderableType:
|
||||||
renderable = self.render_styled()
|
renderable = self.render_styled()
|
||||||
@@ -155,6 +155,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
renderable = self.render()
|
renderable = self.render()
|
||||||
styles = self.styles
|
styles = self.styles
|
||||||
|
|
||||||
parent_text_style = self.parent.text_style
|
parent_text_style = self.parent.text_style
|
||||||
|
|
||||||
text_style = styles.text
|
text_style = styles.text
|
||||||
@@ -162,15 +163,15 @@ class Widget(DOMNode):
|
|||||||
if renderable_text_style:
|
if renderable_text_style:
|
||||||
renderable = Styled(renderable, renderable_text_style)
|
renderable = Styled(renderable, renderable_text_style)
|
||||||
|
|
||||||
if styles.has_padding:
|
if any(styles.padding):
|
||||||
renderable = Padding(
|
renderable = Padding(
|
||||||
renderable, styles.padding, style=renderable_text_style
|
renderable, styles.padding, style=renderable_text_style
|
||||||
)
|
)
|
||||||
|
|
||||||
if styles.has_border:
|
if any(styles.border):
|
||||||
renderable = Border(renderable, styles.border, style=renderable_text_style)
|
renderable = Border(renderable, styles.border, style=renderable_text_style)
|
||||||
|
|
||||||
if styles.has_outline:
|
if any(styles.outline):
|
||||||
renderable = Border(
|
renderable = Border(
|
||||||
renderable,
|
renderable,
|
||||||
styles.outline,
|
styles.outline,
|
||||||
@@ -302,7 +303,7 @@ class Widget(DOMNode):
|
|||||||
if not self.check_message_enabled(message):
|
if not self.check_message_enabled(message):
|
||||||
return True
|
return True
|
||||||
if not self.is_running:
|
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)
|
return await super().post_message(message)
|
||||||
|
|
||||||
async def on_resize(self, event: events.Resize) -> None:
|
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:
|
async def broker_event(self, event_name: str, event: events.Event) -> bool:
|
||||||
return await self.app.broker_event(event_name, event, default_namespace=self)
|
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:
|
async def on_mouse_down(self, event: events.MouseUp) -> None:
|
||||||
await self.broker_event("mouse.down", event)
|
await self.broker_event("mouse.down", event)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class ScrollView(View):
|
|||||||
name: str | None = None,
|
name: str | None = None,
|
||||||
style: StyleType = "",
|
style: StyleType = "",
|
||||||
fluid: bool = True,
|
fluid: bool = True,
|
||||||
gutter: SpacingDimensions = (0, 0)
|
gutter: SpacingDimensions = (0, 0),
|
||||||
) -> None:
|
) -> None:
|
||||||
from ..views import WindowView
|
from ..views import WindowView
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user