mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
css updates
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
38
examples/example.css
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import (
|
||||
TypeVar,
|
||||
Generic,
|
||||
Union,
|
||||
Iterator,
|
||||
Iterable,
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
67
src/textual/css/scalar_animation.py
Normal file
67
src/textual/css/scalar_animation.py
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user