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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
from dataclasses import dataclass, field
from functools import lru_cache
from operator import attrgetter
from typing import Any, Iterable, NamedTuple, TYPE_CHECKING
import rich.repr
@@ -49,6 +50,22 @@ if TYPE_CHECKING:
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):
name: str
edge: Edge
@@ -103,7 +120,15 @@ class Styles:
def has_rule(self, rule: str) -> bool:
"""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")
visibility = StringEnumProperty(VALID_VISIBILITY, "visible")
@@ -164,10 +189,11 @@ class Styles:
@classmethod
@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
styles = parse_declarations(css, path)
styles.node = node
return styles
def __textual_animation__(
@@ -345,11 +371,11 @@ class Styles:
_type, style = self._rule_outline_left
append_declaration("outline-left", f"{_type} {style}")
if self.offset:
if self._rule_offset is not None:
x, y = self.offset
append_declaration("offset", f"{x} {y}")
if self._rule_dock:
append_declaration("dock-group", self._rule_dock)
append_declaration("dock", self._rule_dock)
if self._rule_docks:
append_declaration(
"docks",
@@ -363,6 +389,7 @@ class Styles:
if self._rule_layer is not None:
append_declaration("layer", self.layer)
if self._rule_layout is not None:
assert self.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))
@@ -415,10 +442,9 @@ class StyleViewProperty(Generic[GetType, SetType]):
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
if obj._inline_styles.has_rule(self._name):
return getattr(obj._inline_styles, self._name)
return getattr(obj._base_styles, self._name)
@rich.repr.auto
@@ -448,6 +474,17 @@ class StylesView:
"""Reset the inline styles."""
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]:
"""Check if the Styles must be refreshed.
@@ -475,10 +512,11 @@ class StylesView:
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()
text_color: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()
text_background: StyleViewProperty[Color, Color | str | None] = StyleViewProperty()
text_style: StyleViewProperty[Style, str | None] = StyleViewProperty()
padding: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty()
margin: StyleViewProperty[Spacing, SpacingDimensions] = StyleViewProperty()
@@ -537,6 +575,8 @@ class StylesView:
tuple[str, ...], str | tuple[str] | None
] = StyleViewProperty()
transitions: StyleViewProperty[dict[str, Transition], None] = StyleViewProperty()
if __name__ == "__main__":
styles = Styles()

View File

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

View File

@@ -32,6 +32,7 @@ class DOMNode(MessagePump):
"""
DEFAULT_STYLES = ""
STYLES = ""
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.children = NodeList()
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)
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:
yield "name", self._name, None
@@ -301,7 +304,9 @@ class DOMNode(MessagePump):
)
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.styles.merge(new_styles)
self.log(repr(self.styles))
self.log(self._inline_styles, self._css_styles, self.styles.text)
self.refresh()
def has_class(self, *class_names: str) -> bool:

View File

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

View File

@@ -311,7 +311,7 @@ class MessagePump:
else:
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.
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)
if key_method is not None:
key_result = await invoke(key_method, event)
if key_result is None or key_result:
event.prevent_default()
await invoke(key_method, event)
return True
return False
async def on_timer(self, event: events.Timer) -> None:
event.prevent_default()

View File

@@ -15,9 +15,8 @@ from .widget import Widget
@rich.repr.auto
class View(Widget):
STYLES = """
DEFAULT_STYLES = """
layout: dock;
docks: main=top;
"""
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
"""
# 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

View File

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