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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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