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

View File

@@ -6,6 +6,7 @@ class BasicApp(App):
"""A basic app demonstrating CSS"""
def on_load(self):
"""Bind keys here."""
self.bind("tab", "toggle_class('#sidebar', '-active')")
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 sys
from time import time
from tracemalloc import start
from typing import Callable, TypeVar
from typing import Any, Callable, TypeVar
from dataclasses import dataclass
@@ -62,7 +61,6 @@ class SimpleAnimation(Animation):
factor = min(1.0, (time - self.start_time) / self.duration)
eased_factor = self.easing(factor)
log("ANIMATE", self.start_value, self.end_value)
if isinstance(self.start_value, Animatable):
assert isinstance(
self.end_value, Animatable, "end_value must be animatable"
@@ -117,6 +115,7 @@ class BoundAnimator:
class Animator:
def __init__(self, target: MessageTarget, frames_per_second: int = 60) -> None:
self._animations: dict[tuple[object, str], SimpleAnimation] = {}
self.target = target
self._timer = Timer(
target,
1 / frames_per_second,
@@ -144,13 +143,13 @@ class Animator:
self,
obj: object,
attribute: str,
value: float,
value: Any,
*,
duration: float | None = None,
speed: float | None = None,
easing: EasingFunction | str = DEFAULT_EASING,
) -> None:
log("animate", obj, attribute, value)
start_time = time()
animation_key = (id(obj), attribute)
@@ -167,7 +166,7 @@ class Animator:
start_time,
duration=duration,
speed=speed,
easing=easing,
easing=easing_function,
)
if animation is None:
@@ -206,3 +205,4 @@ class Animator:
animation = self._animations[animation_key]
if animation(animation_time):
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():
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
self._disable_mouse_support()
termios.tcflush(self.fileno, termios.TCIFLUSH)
self.exit_event.set()
if self._key_thread is not None:
self._key_thread.join()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -139,9 +139,14 @@ class DOMNode(MessagePump):
from .widget import Widget
for node in self.walk_children():
# node.styles = Styles()
node.styles = Styles(node=node)
if isinstance(node, Widget):
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:
self.children._append(node)

View File

@@ -49,9 +49,7 @@ class LayoutMap:
layout_offset = Offset(0, 0)
if widget.styles.has_offset:
log("r", region, "c", 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)
@@ -70,7 +68,7 @@ class LayoutMap:
self.add_widget(
sub_widget,
sub_region + region.origin - scroll,
sub_widget.z,
sub_widget.z + (z,),
sub_clip,
)
view.virtual_size = total_region.size

View File

@@ -52,14 +52,14 @@ class DockLayout(Layout):
for child in view.children:
assert isinstance(child, Widget)
if child.visible:
groups[child.styles.dock_group].append(child)
groups[child.styles.dock].append(child)
docks: list[Dock] = []
append_dock = docks.append
for name, edge, z in view.styles.docks:
append_dock(Dock(edge, groups[name], z))
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):
yield from dock.widgets

View File

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

View File

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