default styles

This commit is contained in:
Will McGugan
2022-02-03 11:56:12 +00:00
parent c90cdd4ec8
commit b2974aad6e
13 changed files with 82 additions and 48 deletions

View File

@@ -1,15 +1,11 @@
/* CSS file for basic.py */ /* CSS file for basic.py */
App > View { App > View {
layout: dock;
docks: side=left/1; docks: side=left/1;
text: on #20639b; text: on #20639b;
} }
Widget:hover {
outline: heavy;
text: bold !important;
}
#sidebar { #sidebar {
text: #09312e on #3caea3; text: #09312e on #3caea3;
dock: side; dock: side;
@@ -29,10 +25,6 @@ Widget:hover {
border: hkey; border: hkey;
} }
#header.-visible {
visibility: hidden;
}
#content { #content {
text: white on #20639b; text: white on #20639b;
border-bottom: hkey #0f2b41; border-bottom: hkey #0f2b41;

View File

@@ -8,7 +8,6 @@ class BasicApp(App):
def on_load(self): def on_load(self):
"""Bind keys here.""" """Bind keys here."""
self.bind("tab", "toggle_class('#sidebar', '-active')") self.bind("tab", "toggle_class('#sidebar', '-active')")
self.bind("a", "toggle_class('#header', '-visible')")
def on_mount(self): def on_mount(self):
"""Build layout here.""" """Build layout here."""

View File

@@ -16,18 +16,11 @@ class BasicApp(App):
sidebar=Widget(), sidebar=Widget(),
) )
async def on_key(self, event: events.Key) -> None: def key_a(self) -> None:
await self.dispatch_key(event) self.query("#footer").set_styles(text="on magenta")
def key_a(self) -> bool | None: def key_b(self) -> None:
self.query("#footer").set_styles(text="on magenta").refresh() self["#footer"].set_styles("text: on green")
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") BasicApp.run(css_file="local_styles.css", log="textual.log")

View File

@@ -130,7 +130,10 @@ class Animator:
self._timer.start() self._timer.start()
async def stop(self) -> None: async def stop(self) -> None:
await self._timer.stop() try:
await self._timer.stop()
except asyncio.CancelledError:
pass
def bind(self, obj: object) -> BoundAnimator: def bind(self, obj: object) -> BoundAnimator:
return BoundAnimator(self, obj) return BoundAnimator(self, obj)

View File

@@ -5,6 +5,7 @@ from asyncio import (
get_event_loop, get_event_loop,
CancelledError, CancelledError,
Event, Event,
shield,
sleep, sleep,
Task, Task,
) )

View File

@@ -135,7 +135,6 @@ class DOMQuery:
return self return self
def refresh(self, repaint: bool = True, layout: bool = False) -> DOMQuery: def refresh(self, repaint: bool = True, layout: bool = False) -> DOMQuery:
"""Refresh matched nodes. """Refresh matched nodes.
Args: Args:

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import lru_cache from functools import lru_cache
from operator import attrgetter
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
import rich.repr import rich.repr
@@ -49,6 +50,22 @@ if TYPE_CHECKING:
from ..dom import DOMNode from ..dom import DOMNode
_text_getter = attrgetter(
"_rule_text_color", "_rule_text_background", "_rule_text_style"
)
_border_getter = attrgetter(
"_rule_border_top", "_rule_border_right", "_rule_border_bottom", "_rule_border_left"
)
_outline_getter = attrgetter(
"_rule_outline_top",
"_rule_outline_right",
"_rule_outline_bottom",
"_rule_outline_left",
)
class DockGroup(NamedTuple): class DockGroup(NamedTuple):
name: str name: str
edge: Edge edge: Edge
@@ -103,7 +120,15 @@ class Styles:
def has_rule(self, rule: str) -> bool: def has_rule(self, rule: str) -> bool:
"""Check if a rule has been set.""" """Check if a rule has been set."""
return getattr(self, f"_rule_{rule}") != None if rule in RULE_NAMES and getattr(self, f"_rule_{rule}") is not None:
return True
if rule == "text":
return not all(rule is None for rule in _text_getter(self))
if rule == "border" and any(self.border):
return not all(rule is None for rule in _border_getter(self))
if rule == "outline" and any(self.outline):
return not all(rule is None for rule in _outline_getter(self))
return False
display = StringEnumProperty(VALID_DISPLAY, "block") display = StringEnumProperty(VALID_DISPLAY, "block")
visibility = StringEnumProperty(VALID_VISIBILITY, "visible") visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
@@ -164,10 +189,11 @@ class Styles:
@classmethod @classmethod
@lru_cache(maxsize=1024) @lru_cache(maxsize=1024)
def parse(cls, css: str, path: str) -> Styles: def parse(cls, css: str, path: str, *, node: DOMNode = None) -> Styles:
from .parse import parse_declarations from .parse import parse_declarations
styles = parse_declarations(css, path) styles = parse_declarations(css, path)
styles.node = node
return styles return styles
def __textual_animation__( def __textual_animation__(
@@ -345,11 +371,11 @@ class Styles:
_type, style = self._rule_outline_left _type, style = self._rule_outline_left
append_declaration("outline-left", f"{_type} {style}") append_declaration("outline-left", f"{_type} {style}")
if self.offset: if self._rule_offset is not None:
x, y = self.offset x, y = self.offset
append_declaration("offset", f"{x} {y}") append_declaration("offset", f"{x} {y}")
if self._rule_dock: if self._rule_dock:
append_declaration("dock-group", self._rule_dock) append_declaration("dock", self._rule_dock)
if self._rule_docks: if self._rule_docks:
append_declaration( append_declaration(
"docks", "docks",
@@ -363,6 +389,7 @@ class Styles:
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: if self._rule_layout is not None:
assert self.layout is not None
append_declaration("layout", self.layout.name) 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))
@@ -415,10 +442,9 @@ class StyleViewProperty(Generic[GetType, SetType]):
def __get__( def __get__(
self, obj: StylesView, objtype: type[StylesView] | None = None self, obj: StylesView, objtype: type[StylesView] | None = None
) -> GetType: ) -> GetType:
styles_value = getattr(obj._inline_styles, self._internal_name, None) if obj._inline_styles.has_rule(self._name):
if styles_value is None: return getattr(obj._inline_styles, self._name)
return getattr(obj._base_styles, self._name) return getattr(obj._base_styles, self._name)
return styles_value
@rich.repr.auto @rich.repr.auto
@@ -448,6 +474,17 @@ class StylesView:
"""Reset the inline styles.""" """Reset the inline styles."""
self._inline_styles.reset() self._inline_styles.reset()
def refresh(self, layout: bool = False) -> None:
self._inline_styles.refresh(layout=layout)
def merge(self, other: Styles) -> None:
"""Merge values from another Styles.
Args:
other (Styles): A Styles object.
"""
self._inline_styles.merge(other)
def check_refresh(self) -> tuple[bool, bool]: def check_refresh(self) -> tuple[bool, bool]:
"""Check if the Styles must be refreshed. """Check if the Styles must be refreshed.
@@ -475,10 +512,11 @@ class StylesView:
display: StyleViewProperty[str, str | None] = StyleViewProperty() display: StyleViewProperty[str, str | None] = StyleViewProperty()
visibility: StyleViewProperty[str, str | None] = StyleViewProperty() visibility: StyleViewProperty[str, str | None] = StyleViewProperty()
layout: StyleViewProperty[Layout | None, str | Layout] = StyleViewProperty() layout: StyleViewProperty[Layout | None, str | Layout] = StyleViewProperty()
text: StyleViewProperty[Style, Style | str | None] = StyleViewProperty() text: StyleViewProperty[Style, Style | str | None] = StyleViewProperty()
color: StyleViewProperty[Color, Color | str | None] = StyleViewProperty() text_color: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()
background: StyleViewProperty[Color, Color | str | None] = StyleViewProperty() text_background: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()
style: StyleViewProperty[Style, str | None] = StyleViewProperty() text_style: StyleViewProperty[Style, str | None] = StyleViewProperty()
padding: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty() padding: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty()
margin: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty() margin: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty()
@@ -537,6 +575,8 @@ class StylesView:
tuple[str, ...], str | tuple[str] | None tuple[str, ...], str | tuple[str] | None
] = StyleViewProperty() ] = StyleViewProperty()
transitions: StyleViewProperty[dict[str, Transition], None] = StyleViewProperty()
if __name__ == "__main__": if __name__ == "__main__":
styles = Styles() styles = Styles()

View File

@@ -135,8 +135,8 @@ class Stylesheet:
node._css_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:

View File

@@ -32,6 +32,7 @@ class DOMNode(MessagePump):
""" """
DEFAULT_STYLES = ""
STYLES = "" STYLES = ""
def __init__(self, name: str | None = None, id: str | None = None) -> None: def __init__(self, name: str | None = None, id: str | None = None) -> None:
@@ -40,9 +41,11 @@ class DOMNode(MessagePump):
self._classes: set[str] = set() self._classes: set[str] = set()
self.children = NodeList() self.children = NodeList()
self._css_styles: Styles = Styles(self) self._css_styles: Styles = Styles(self)
self._inline_styles: Styles = Styles.parse(self.STYLES, repr(self)) self._inline_styles: Styles = Styles.parse(self.STYLES, repr(self), node=self)
self.styles = StylesView(self._css_styles, self._inline_styles) self.styles = StylesView(self._css_styles, self._inline_styles)
super().__init__() super().__init__()
self.default_styles = Styles.parse(self.DEFAULT_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
@@ -301,7 +304,9 @@ class DOMNode(MessagePump):
) )
apply_css = f"{css or ''}\n{kwarg_css}\n" apply_css = f"{css or ''}\n{kwarg_css}\n"
new_styles = parse_declarations(apply_css, f"<custom styles for ${self!r}>") new_styles = parse_declarations(apply_css, f"<custom styles for ${self!r}>")
self._inline_styles.merge(new_styles) self.styles.merge(new_styles)
self.log(repr(self.styles))
self.log(self._inline_styles, self._css_styles, self.styles.text)
self.refresh() self.refresh()
def has_class(self, *class_names: str) -> bool: def has_class(self, *class_names: str) -> bool:

View File

@@ -36,6 +36,7 @@ class Dock(NamedTuple):
class DockLayout(Layout): class DockLayout(Layout):
"""Dock Widgets to edge of screen."""
name = "dock" name = "dock"

View File

@@ -311,7 +311,7 @@ class MessagePump:
else: else:
return False return False
async def dispatch_key(self, event: events.Key) -> None: async def dispatch_key(self, event: events.Key) -> bool:
"""Dispatch a key event to method. """Dispatch a key event to method.
This method will call the method named 'key_<event.key>' if it exists. This key method This method will call the method named 'key_<event.key>' if it exists. This key method
@@ -323,9 +323,9 @@ class MessagePump:
key_method = getattr(self, f"key_{event.key}", None) key_method = getattr(self, f"key_{event.key}", None)
if key_method is not None: if key_method is not None:
key_result = await invoke(key_method, event) await invoke(key_method, event)
if key_result is None or key_result: return True
event.prevent_default() return False
async def on_timer(self, event: events.Timer) -> None: async def on_timer(self, event: events.Timer) -> None:
event.prevent_default() event.prevent_default()

View File

@@ -15,9 +15,8 @@ from .widget import Widget
@rich.repr.auto @rich.repr.auto
class View(Widget): class View(Widget):
STYLES = """ DEFAULT_STYLES = """
layout: dock; layout: dock;
docks: main=top;
""" """
def __init__(self, name: str | None = None, id: str | None = None) -> None: def __init__(self, name: str | None = None, id: str | None = None) -> None:
@@ -55,9 +54,7 @@ class View(Widget):
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 assert self.styles.layout
return self.styles.layout return self.styles.layout

View File

@@ -58,7 +58,7 @@ class Widget(DOMNode):
_counts: ClassVar[dict[str, int]] = {} _counts: ClassVar[dict[str, int]] = {}
can_focus: bool = False can_focus: bool = False
STYLES = """ DEFAULT_STYLES = """
dock: _default; dock: _default;
""" """
@@ -360,3 +360,7 @@ class Widget(DOMNode):
async def on_leave(self, event: events.Leave) -> None: async def on_leave(self, event: events.Leave) -> None:
self._mouse_over = False self._mouse_over = False
self.app.update_styles() self.app.update_styles()
async def on_key(self, event: events.Key) -> None:
if await self.dispatch_key(event):
event.prevent_default()