implement inline styles

This commit is contained in:
Will McGugan
2022-02-02 15:44:43 +00:00
parent 19b835b8a1
commit c90cdd4ec8
22 changed files with 392 additions and 115 deletions

View File

@@ -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
View 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
View File

@@ -0,0 +1,7 @@
App > View {
layout: dock;
}
Widget {
text: on blue;
}

33
sandbox/local_styles.py Normal file
View 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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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()

View File

@@ -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,

View File

@@ -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)

View File

@@ -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,
) )
) )

View File

@@ -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:

View File

@@ -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

View File

@@ -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()

View File

@@ -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:

View File

@@ -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)

View File

@@ -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)

View File

@@ -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