css updates

This commit is contained in:
Will McGugan
2021-12-26 17:58:33 +00:00
parent a034c76405
commit c5e1d76cfd
17 changed files with 161 additions and 56 deletions

View File

@@ -1,39 +1,39 @@
App > View { App > View {
layout: dock; layout: dock;
docks: side=left/1 header=top footer=bottom; docks: side=left/1 header=top footer=bottom;
layers: base panels;
} }
#sidebar { #sidebar {
text: bold #09312e on #3caea3; text: bold #09312e on #3caea3;
dock-group: side; dock: side;
width: 30; width: 30;
height: 1fr; height: 1fr;
layer: panels;
border-right: outer #09312e; border-right: outer #09312e;
offset-x: -100%; offset-x: -100%;
transition: offset 1.2s in_cubic 200ms; transition: offset 400ms in_out_cubic;
} }
#sidebar.-active { #sidebar.-active {
offset-x: 0; offset-x: 0;
transition: offset 400ms in_out_cubic;
text: on red;
} }
#header { #header {
text: on #173f5f; text: on #173f5f;
dock-group: header; dock: header;
height: 3; height: 3;
border: hkey white; border: hkey white;
} }
#footer { #footer {
dock-group: header; dock: header;
height: 3; height: 3;
border-top: hkey #0f2b41; border-top: hkey #0f2b41;
text: #3a3009 on #f6d55c; text: #3a3009 on #f6d55c;
} }
#content { #content {
dock-group: header; dock: header;
text: on #20639b; text: bold on #20639b;
} }

View File

@@ -6,6 +6,7 @@ class BasicApp(App):
"""A basic app demonstrating CSS""" """A basic app demonstrating CSS"""
def on_load(self): def on_load(self):
"""Bind keys here."""
self.bind("tab", "toggle_class('#sidebar', '-active')") self.bind("tab", "toggle_class('#sidebar', '-active')")
def on_mount(self): def on_mount(self):
@@ -18,4 +19,4 @@ class BasicApp(App):
) )
BasicApp.run(log="textual.log", css_file="basic.css", watch_css=True) BasicApp.run(css_file="basic.css", watch_css=True)

38
examples/example.css Normal file
View File

@@ -0,0 +1,38 @@
App > View {
layout: dock;
docks: side=left/1 header=top footer=bottom;
}
#sidebar {
text: bold #09312e on #3caea3;
dock: side;
width: 30;
height: 1fr;
border-right: outer #09312e;
offset-x: -100%;
transition: offset 400ms in_out_cubic;
}
#sidebar.-active {
offset-x: 0;
transition: offset 400ms in_out_cubic;
}
#header {
text: on #173f5f;
dock: header;
height: 3;
border: hkey white;
}
#footer {
dock: header;
height: 3;
border-top: hkey #0f2b41;
text: #3a3009 on #f6d55c;
}
#content {
dock: header;
text: bold on #20639b;
}

View File

@@ -4,8 +4,7 @@ from abc import ABC, abstractmethod
import asyncio import asyncio
import sys import sys
from time import time from time import time
from tracemalloc import start from typing import Any, Callable, TypeVar
from typing import Callable, TypeVar
from dataclasses import dataclass from dataclasses import dataclass
@@ -62,7 +61,6 @@ class SimpleAnimation(Animation):
factor = min(1.0, (time - self.start_time) / self.duration) factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing(factor) eased_factor = self.easing(factor)
log("ANIMATE", self.start_value, self.end_value)
if isinstance(self.start_value, Animatable): if isinstance(self.start_value, Animatable):
assert isinstance( assert isinstance(
self.end_value, Animatable, "end_value must be animatable" self.end_value, Animatable, "end_value must be animatable"
@@ -117,6 +115,7 @@ class BoundAnimator:
class Animator: class Animator:
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None: def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
self._animations: dict[tuple[object, str], SimpleAnimation] = {} self._animations: dict[tuple[object, str], SimpleAnimation] = {}
self.target = target
self._timer = Timer( self._timer = Timer(
target, target,
1 / frames_per_second, 1 / frames_per_second,
@@ -144,13 +143,13 @@ class Animator:
self, self,
obj: object, obj: object,
attribute: str, attribute: str,
value: float, value: Any,
*, *,
duration: float | None = None, duration: float | None = None,
speed: float | None = None, speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING, easing: EasingFunction | str = DEFAULT_EASING,
) -> None: ) -> None:
log("animate", obj, attribute, value)
start_time = time() start_time = time()
animation_key = (id(obj), attribute) animation_key = (id(obj), attribute)
@@ -167,7 +166,7 @@ class Animator:
start_time, start_time,
duration=duration, duration=duration,
speed=speed, speed=speed,
easing=easing, easing=easing_function,
) )
if animation is None: if animation is None:
@@ -206,3 +205,4 @@ class Animator:
animation = self._animations[animation_key] animation = self._animations[animation_key]
if animation(animation_time): if animation(animation_time):
del self._animations[animation_key] del self._animations[animation_key]
self.target.view.refresh(True, True)

View File

@@ -145,6 +145,7 @@ class LinuxDriver(Driver):
if not self.exit_event.is_set(): if not self.exit_event.is_set():
signal.signal(signal.SIGWINCH, signal.SIG_DFL) signal.signal(signal.SIGWINCH, signal.SIG_DFL)
self._disable_mouse_support() self._disable_mouse_support()
termios.tcflush(self.fileno, termios.TCIFLUSH)
self.exit_event.set() self.exit_event.set()
if self._key_thread is not None: if self._key_thread is not None:
self._key_thread.join() self._key_thread.join()

View File

@@ -9,7 +9,6 @@ from typing import (
TypeVar, TypeVar,
Generic, Generic,
Union, Union,
Iterator,
Iterable, Iterable,
) )

View File

@@ -158,7 +158,6 @@ class App(DOMNode):
@property @property
def size(self) -> Size: def size(self) -> Size:
width, height = self.console.size
return Size(*self.console.size) return Size(*self.console.size)
def log(self, *args: Any, verbosity: int = 1, **kwargs) -> None: def log(self, *args: Any, verbosity: int = 1, **kwargs) -> None:
@@ -240,7 +239,7 @@ class App(DOMNode):
self.reset_styles() self.reset_styles()
self.stylesheet = stylesheet self.stylesheet = stylesheet
self.stylesheet.update(self) self.stylesheet.update(self)
self.view.refresh(layout=True) self.refresh(layout=True)
def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None: def mount(self, *anon_widgets: Widget, **widgets: Widget) -> None:
self.register(self.view, *anon_widgets, **widgets) self.register(self.view, *anon_widgets, **widgets)

View File

@@ -259,13 +259,13 @@ class DocksProperty:
return docks return docks
class DockGroupProperty: class DockProperty:
def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str: def __get__(self, obj: Styles, objtype: type[Styles] | None = None) -> str:
return obj._rule_dock_group or "" return obj._rule_dock or ""
def __set__(self, obj: Styles, spacing: str | None) -> str | None: def __set__(self, obj: Styles, spacing: str | None) -> str | None:
obj.refresh(True) obj.refresh(True)
obj._rule_dock_group = spacing obj._rule_dock = spacing
return spacing return spacing
@@ -279,9 +279,12 @@ class OffsetProperty:
) )
def __set__( def __set__(
self, obj: Styles, offset: tuple[int | str, int | str] self, obj: Styles, offset: tuple[int | str, int | str] | ScalarOffset
) -> tuple[int | str, int | str]: ) -> tuple[int | str, int | str] | ScalarOffset:
obj.refresh(True) obj.refresh(True)
if isinstance(offset, ScalarOffset):
setattr(obj, self._internal_name, offset)
return offset
x, y = offset x, y = offset
scalar_x = ( scalar_x = (
Scalar.parse(x, Unit.WIDTH) Scalar.parse(x, Unit.WIDTH)

View File

@@ -294,15 +294,15 @@ class StylesBuilder:
style_definition = " ".join(token.value for token in tokens) style_definition = " ".join(token.value for token in tokens)
self.styles.text_style = style_definition self.styles.text_style = style_definition
def process_dock_group(self, name: str, tokens: list[Token]) -> None: def process_dock(self, name: str, tokens: list[Token]) -> None:
if len(tokens) > 1: if len(tokens) > 1:
self.error( self.error(
name, name,
tokens[1], tokens[1],
f"unexpected tokens in dock-group declaration", f"unexpected tokens in dock declaration",
) )
self.styles._rule_dock_group = tokens[0].value if tokens else "" self.styles._rule_dock = tokens[0].value if tokens else ""
def process_docks(self, name: str, tokens: list[Token]) -> None: def process_docks(self, name: str, tokens: list[Token]) -> None:
docks: list[DockGroup] = [] docks: list[DockGroup] = []
@@ -333,7 +333,7 @@ class StylesBuilder:
token, token,
f"unexpected token {token.value!r} in docks declaration", f"unexpected token {token.value!r} in docks declaration",
) )
self.styles._rule_docks = tuple(docks) self.styles._rule_docks = tuple(docks + [DockGroup("_default", "top", 0)])
def process_layer(self, name: str, tokens: list[Token]) -> None: def process_layer(self, name: str, tokens: list[Token]) -> None:
if len(tokens) > 1: if len(tokens) > 1:

View File

@@ -0,0 +1,67 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .. import events
from ..geometry import Offset
from .._animator import Animation
from .scalar import ScalarOffset
from .._animator import EasingFunction
if TYPE_CHECKING:
from ..widget import Widget
from .styles import Styles
class ScalarAnimation(Animation):
def __init__(
self,
widget: Widget,
styles: Styles,
start_time: float,
attribute: str,
value: ScalarOffset,
duration: float | None,
speed: float | None,
easing: EasingFunction,
):
assert (
speed is not None or duration is not None
), "One of speed or duration required"
self.widget = widget
self.styles = styles
self.start_time = start_time
self.attribute = attribute
self.final_value = value
self.easing = easing
size = widget.size
viewport = widget.app.size
self.start: Offset = getattr(styles, attribute).resolve(size, viewport)
self.destination: Offset = value.resolve(size, viewport)
if speed is not None:
distance = self.start.get_distance_to(self.destination)
self.duration = distance / speed
else:
assert duration is not None, "Duration expected to be non-None"
self.duration = duration
def __call__(self, time: float) -> bool:
factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing(factor)
if eased_factor >= 1:
offset = self.final_value
setattr(self.styles, self.attribute, self.final_value)
return True
offset = self.start + (self.destination - self.start) * eased_factor
current = getattr(self.styles, f"_rule_{self.attribute}")
if current != offset:
setattr(self.styles, f"{self.attribute}", offset)
return False

View File

@@ -31,7 +31,7 @@ from ._style_properties import (
BoxProperty, BoxProperty,
ColorProperty, ColorProperty,
DocksProperty, DocksProperty,
DockGroupProperty, DockProperty,
OffsetProperty, OffsetProperty,
NameProperty, NameProperty,
NameListProperty, NameListProperty,
@@ -94,7 +94,7 @@ class Styles:
_rule_min_width: Scalar | None = None _rule_min_width: Scalar | None = None
_rule_min_height: Scalar | None = None _rule_min_height: Scalar | None = None
_rule_dock_group: str | None = None _rule_dock: str | None = None
_rule_docks: tuple[DockGroup, ...] | None = None _rule_docks: tuple[DockGroup, ...] | None = None
_rule_layers: tuple[str, ...] | None = None _rule_layers: tuple[str, ...] | None = None
@@ -137,7 +137,7 @@ class Styles:
min_width = ScalarProperty(percent_unit=Unit.WIDTH) min_width = ScalarProperty(percent_unit=Unit.WIDTH)
min_height = ScalarProperty(percent_unit=Unit.HEIGHT) min_height = ScalarProperty(percent_unit=Unit.HEIGHT)
dock_group = DockGroupProperty() dock = DockProperty()
docks = DocksProperty() docks = DocksProperty()
layer = NameProperty() layer = NameProperty()
@@ -145,8 +145,6 @@ class Styles:
transitions = TransitionsProperty() transitions = TransitionsProperty()
ANIMATABLE = { ANIMATABLE = {
"offset-x",
"offset-y",
"offset", "offset",
"padding", "padding",
"margin", "margin",
@@ -192,6 +190,8 @@ class Styles:
def refresh(self, layout: bool = False) -> None: def refresh(self, layout: bool = False) -> None:
self._repaint_required = True self._repaint_required = True
self._layout_required = layout self._layout_required = layout
# if self.node is not None:
# self.node.post_message_no_wait(events.Null(self.node))
def check_refresh(self) -> tuple[bool, bool]: def check_refresh(self) -> tuple[bool, bool]:
result = (self._repaint_required, self._layout_required) result = (self._repaint_required, self._layout_required)
@@ -252,25 +252,19 @@ class Styles:
current = getattr(styles, f"_rule_{key}") current = getattr(styles, f"_rule_{key}")
if current == value: if current == value:
continue continue
log(key, "=", value)
if is_animatable(key): if is_animatable(key):
log("animatable", key)
transition = styles.get_transition(key) transition = styles.get_transition(key)
log("transition", transition)
if transition is None: if transition is None:
setattr(styles, f"_rule_{key}", value) setattr(styles, f"_rule_{key}", value)
else: else:
duration, easing, delay = transition duration, easing, delay = transition
log("ANIMATING")
self.node.app.animator.animate( self.node.app.animator.animate(
styles, key, value, duration=duration, easing=easing styles, key, value, duration=duration, easing=easing
) )
else: else:
log("not animatable")
setattr(styles, f"_rule_{key}", value) setattr(styles, f"_rule_{key}", value)
if self.node is not None: if self.node is not None:
self.node.post_message_no_wait(events.Null(self.node)) self.node.on_style_change()
def __rich_repr__(self) -> rich.repr.Result: def __rich_repr__(self) -> rich.repr.Result:
for rule_name, internal_rule_name in zip(RULE_NAMES, INTERNAL_RULE_NAMES): for rule_name, internal_rule_name in zip(RULE_NAMES, INTERNAL_RULE_NAMES):
@@ -362,8 +356,8 @@ class Styles:
if self.offset: if self.offset:
x, y = self.offset x, y = self.offset
append_declaration("offset", f"{x} {y}") append_declaration("offset", f"{x} {y}")
if self._rule_dock_group: if self._rule_dock:
append_declaration("dock-group", self._rule_dock_group) append_declaration("dock-group", self._rule_dock)
if self._rule_docks: if self._rule_docks:
append_declaration( append_declaration(
"docks", "docks",
@@ -416,7 +410,7 @@ if __name__ == "__main__":
styles.outline_right = ("solid", "red") styles.outline_right = ("solid", "red")
styles.docks = "foo bar" styles.docks = "foo bar"
styles.text_style = "italic" styles.text_style = "italic"
styles.dock_group = "bar" styles.dock = "bar"
styles.layers = "foo bar" styles.layers = "foo bar"
from rich import inspect, print from rich import inspect, print

View File

@@ -128,7 +128,6 @@ class Stylesheet:
(name, max(specificity_rules, key=get_first_item)[1]) (name, max(specificity_rules, key=get_first_item)[1])
for name, specificity_rules in rule_attributes.items() for name, specificity_rules in rule_attributes.items()
] ]
node.styles.apply_rules(node_rules) node.styles.apply_rules(node_rules)
def update(self, root: DOMNode) -> None: def update(self, root: DOMNode) -> None:

View File

@@ -139,9 +139,14 @@ 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.styles = Styles(node=node)
if isinstance(node, Widget): if isinstance(node, Widget):
node.clear_render_cache() node.clear_render_cache()
node._repaint_required = True
node._layout_required = True
def on_style_change(self) -> None:
pass
def add_child(self, node: DOMNode) -> None: def add_child(self, node: DOMNode) -> None:
self.children._append(node) self.children._append(node)

View File

@@ -49,9 +49,7 @@ class LayoutMap:
layout_offset = Offset(0, 0) layout_offset = Offset(0, 0)
if widget.styles.has_offset: if widget.styles.has_offset:
log("r", region, "c", clip.size)
layout_offset = widget.styles.offset.resolve(region.size, clip.size) layout_offset = widget.styles.offset.resolve(region.size, clip.size)
log("layout_offset", layout_offset)
self.widgets[widget] = RenderRegion(region + layout_offset, order, clip) self.widgets[widget] = RenderRegion(region + layout_offset, order, clip)
@@ -70,7 +68,7 @@ class LayoutMap:
self.add_widget( self.add_widget(
sub_widget, sub_widget,
sub_region + region.origin - scroll, sub_region + region.origin - scroll,
sub_widget.z, sub_widget.z + (z,),
sub_clip, sub_clip,
) )
view.virtual_size = total_region.size view.virtual_size = total_region.size

View File

@@ -52,14 +52,14 @@ class DockLayout(Layout):
for child in view.children: for child in view.children:
assert isinstance(child, Widget) assert isinstance(child, Widget)
if child.visible: if child.visible:
groups[child.styles.dock_group].append(child) groups[child.styles.dock].append(child)
docks: list[Dock] = [] docks: list[Dock] = []
append_dock = docks.append append_dock = docks.append
for name, edge, z in view.styles.docks: for name, edge, z in view.styles.docks:
append_dock(Dock(edge, groups[name], z)) append_dock(Dock(edge, groups[name], z))
return docks return docks
def get_widgets(self, view: View) -> Iterable[DOMNode]: def get_widgets(self, view: View) -> Iterable[Widget]:
for dock in self.get_docks(view): for dock in self.get_docks(view):
yield from dock.widgets yield from dock.widgets

View File

@@ -40,7 +40,8 @@ class LayoutProperty:
class View(Widget): class View(Widget):
STYLES = """ STYLES = """
docks: main=top 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:
@@ -120,7 +121,6 @@ class View(Widget):
if cached_size == size and cached_scroll == scroll: if cached_size == size and cached_scroll == scroll:
return arrangement return arrangement
arrangement = list(self._layout.arrange(self, size, scroll)) arrangement = list(self._layout.arrange(self, size, scroll))
self.log(arrangement)
self._cached_arrangement = (size, scroll, arrangement) self._cached_arrangement = (size, scroll, arrangement)
return arrangement return arrangement

View File

@@ -61,7 +61,7 @@ class Widget(DOMNode):
can_focus: bool = False can_focus: bool = False
STYLES = """ STYLES = """
dock-group: main; dock: _default;
""" """
def __init__(self, name: str | None = None, id: str | None = None) -> None: def __init__(self, name: str | None = None, id: str | None = None) -> None:
@@ -194,6 +194,9 @@ class Widget(DOMNode):
) )
return gutter return gutter
def on_style_change(self) -> None:
self.clear_render_cache()
def _update_size(self, size: Size) -> None: def _update_size(self, size: Size) -> None:
self._size = size self._size = size
@@ -286,14 +289,12 @@ class Widget(DOMNode):
async def on_idle(self, event: events.Idle) -> None: async def on_idle(self, event: events.Idle) -> None:
repaint, layout = self.styles.check_refresh() repaint, layout = self.styles.check_refresh()
if layout or self.check_layout(): if layout or self.check_layout():
self.log("layout required") # self.render_cache = None
self.render_cache = None
self.reset_check_repaint() self.reset_check_repaint()
self.reset_check_layout() self.reset_check_layout()
await self.emit(Layout(self)) await self.emit(Layout(self))
elif repaint or self.check_repaint(): elif repaint or self.check_repaint():
self.log("repaint required") # self.render_cache = None
self.render_cache = None
self.reset_check_repaint() self.reset_check_repaint()
await self.emit(Update(self, self)) await self.emit(Update(self, self))