mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
color scheme, compositor fix
This commit is contained in:
@@ -1,44 +1,62 @@
|
|||||||
/* CSS file for basic.py */
|
/* CSS file for basic.py */
|
||||||
|
|
||||||
$primary: #20639b;
|
|
||||||
|
|
||||||
App > Screen {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: side=left/1;
|
docks: side=left/1;
|
||||||
background: $primary;
|
background: $background;
|
||||||
|
color: $on-background;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
color: #09312e;
|
color: $on-primary;
|
||||||
background: #3caea3;
|
background: $primary;
|
||||||
dock: side;
|
dock: side;
|
||||||
width: 30;
|
width: 30;
|
||||||
offset-x: -100%;
|
offset-x: -100%;
|
||||||
|
layout: dock;
|
||||||
transition: offset 500ms in_out_cubic;
|
transition: offset 500ms in_out_cubic;
|
||||||
border-right: outer #09312e;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar.-active {
|
#sidebar.-active {
|
||||||
offset-x: 0;
|
offset-x: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#sidebar .title {
|
||||||
|
height: 1;
|
||||||
|
background: $secondary-darken1;
|
||||||
|
color: $on-secondary-darken1;
|
||||||
|
border-right: vkey $secondary-darken2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .user {
|
||||||
|
height: 8;
|
||||||
|
background: $secondary;
|
||||||
|
color: $on-secondary;
|
||||||
|
border-right: vkey $secondary-darken2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sidebar .content {
|
||||||
|
background: $secondary-lighten1;
|
||||||
|
color: $on-secondary-lighten1;
|
||||||
|
border-right: vkey $secondary-darken2;
|
||||||
|
}
|
||||||
|
|
||||||
#header {
|
#header {
|
||||||
color: white;
|
color: $on-primary;
|
||||||
background: #173f5f;
|
background: $primary;
|
||||||
height: 3;
|
height: 3;
|
||||||
border: hkey white;
|
border: hkey $primary-darken2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
color: white;
|
color: $on-background;
|
||||||
background: $primary;
|
background: $background;
|
||||||
border-bottom: hkey #0f2b41;
|
border-bottom: hkey #0f2b41;
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
color: #3a3009;
|
color: $on-accent1;
|
||||||
background: #f6d55c;
|
background: $accent1;
|
||||||
|
|
||||||
height: 3;
|
height: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,21 @@ class BasicApp(App):
|
|||||||
header=Widget(),
|
header=Widget(),
|
||||||
content=Widget(),
|
content=Widget(),
|
||||||
footer=Widget(),
|
footer=Widget(),
|
||||||
sidebar=Widget(),
|
sidebar=Widget(
|
||||||
|
Widget(classes={"title"}),
|
||||||
|
Widget(classes={"user"}),
|
||||||
|
Widget(classes={"content"}),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def on_key(self, event) -> None:
|
||||||
|
await self.dispatch_key(event)
|
||||||
|
|
||||||
|
def key_d(self):
|
||||||
|
self.dark = not self.dark
|
||||||
|
|
||||||
|
def key_x(self):
|
||||||
|
self.panic(self.tree)
|
||||||
|
|
||||||
|
|
||||||
BasicApp.run(css_file="basic.css", watch_css=True, log="textual.log")
|
BasicApp.run(css_file="basic.css", watch_css=True, log="textual.log")
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ Screen {
|
|||||||
|
|
||||||
#borders {
|
#borders {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
text-background: #212121;
|
background: #212121;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
Lorem.border {
|
Lorem.border {
|
||||||
height: 12;
|
height: 12;
|
||||||
margin: 2 4;
|
margin: 2 4;
|
||||||
text-background: #303f9f;
|
background: #303f9f;
|
||||||
}
|
}
|
||||||
|
|
||||||
Lorem.round {
|
Lorem.round {
|
||||||
|
|||||||
@@ -205,6 +205,8 @@ class Compositor:
|
|||||||
else ORIGIN
|
else ORIGIN
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# region += layout_offset
|
||||||
|
|
||||||
# Container region is minus border
|
# Container region is minus border
|
||||||
container_region = region.shrink(widget.styles.gutter)
|
container_region = region.shrink(widget.styles.gutter)
|
||||||
|
|
||||||
@@ -233,8 +235,13 @@ class Compositor:
|
|||||||
if sub_widget is not None:
|
if sub_widget is not None:
|
||||||
add_widget(
|
add_widget(
|
||||||
sub_widget,
|
sub_widget,
|
||||||
sub_region + child_region.origin - scroll_offset,
|
(
|
||||||
sub_widget.z + (z,),
|
sub_region
|
||||||
|
+ child_region.origin
|
||||||
|
- scroll_offset
|
||||||
|
+ layout_offset
|
||||||
|
),
|
||||||
|
(z,) + sub_widget.z,
|
||||||
sub_clip,
|
sub_clip,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -19,15 +19,13 @@ class NodeList:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._node_refs: list[ref[DOMNode]] = []
|
self._nodes: list[DOMNode] = []
|
||||||
self.__nodes: list[DOMNode] | None = []
|
|
||||||
|
|
||||||
def __bool__(self) -> bool:
|
def __bool__(self) -> bool:
|
||||||
self._prune()
|
return bool(self._nodes)
|
||||||
return bool(self._node_refs)
|
|
||||||
|
|
||||||
def __length_hint__(self) -> int:
|
def __length_hint__(self) -> int:
|
||||||
return len(self._node_refs)
|
return len(self._nodes)
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield self._nodes
|
yield self._nodes
|
||||||
@@ -38,32 +36,12 @@ class NodeList:
|
|||||||
def __contains__(self, widget: DOMNode) -> bool:
|
def __contains__(self, widget: DOMNode) -> bool:
|
||||||
return widget in self._nodes
|
return widget in self._nodes
|
||||||
|
|
||||||
@property
|
|
||||||
def _nodes(self) -> list[DOMNode]:
|
|
||||||
if self.__nodes is None:
|
|
||||||
self.__nodes = list(
|
|
||||||
filter(None, [widget_ref() for widget_ref in self._node_refs])
|
|
||||||
)
|
|
||||||
return self.__nodes
|
|
||||||
|
|
||||||
def _prune(self) -> None:
|
|
||||||
"""Remove expired references."""
|
|
||||||
self._node_refs[:] = filter(
|
|
||||||
None,
|
|
||||||
[
|
|
||||||
None if widget_ref() is None else widget_ref
|
|
||||||
for widget_ref in self._node_refs
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
def _append(self, widget: DOMNode) -> None:
|
def _append(self, widget: DOMNode) -> None:
|
||||||
if widget not in self._nodes:
|
if widget not in self._nodes:
|
||||||
self._node_refs.append(ref(widget))
|
self._nodes.append(widget)
|
||||||
self.__nodes = None
|
|
||||||
|
|
||||||
def _clear(self) -> None:
|
def _clear(self) -> None:
|
||||||
del self._node_refs[:]
|
del self._nodes[:]
|
||||||
self.__nodes = None
|
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[DOMNode]:
|
def __iter__(self) -> Iterator[DOMNode]:
|
||||||
return iter(self._nodes)
|
return iter(self._nodes)
|
||||||
@@ -77,6 +55,5 @@ class NodeList:
|
|||||||
...
|
...
|
||||||
|
|
||||||
def __getitem__(self, index: int | slice) -> DOMNode | list[DOMNode]:
|
def __getitem__(self, index: int | slice) -> DOMNode | list[DOMNode]:
|
||||||
self._prune()
|
|
||||||
assert self._nodes is not None
|
assert self._nodes is not None
|
||||||
return self._nodes[index]
|
return self._nodes[index]
|
||||||
|
|||||||
@@ -12,10 +12,12 @@ import rich.repr
|
|||||||
from rich.console import Console, RenderableType
|
from rich.console import Console, RenderableType
|
||||||
from rich.control import Control
|
from rich.control import Control
|
||||||
from rich.measure import Measurement
|
from rich.measure import Measurement
|
||||||
|
from rich.segment import Segments
|
||||||
from rich.screen import Screen as ScreenRenderable
|
from rich.screen import Screen as ScreenRenderable
|
||||||
from rich.traceback import Traceback
|
from rich.traceback import Traceback
|
||||||
|
|
||||||
from . import actions
|
from . import actions
|
||||||
|
|
||||||
from . import events
|
from . import events
|
||||||
from . import log
|
from . import log
|
||||||
from . import messages
|
from . import messages
|
||||||
@@ -26,6 +28,7 @@ from ._event_broker import extract_handler_actions, NoHandler
|
|||||||
from ._profile import timer
|
from ._profile import timer
|
||||||
from .binding import Bindings, NoBinding
|
from .binding import Bindings, NoBinding
|
||||||
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
|
from .css.stylesheet import Stylesheet, StylesheetParseError, StylesheetError
|
||||||
|
from .design import ColorSystem
|
||||||
from .dom import DOMNode
|
from .dom import DOMNode
|
||||||
from .driver import Driver
|
from .driver import Driver
|
||||||
from .file_monitor import FileMonitor
|
from .file_monitor import FileMonitor
|
||||||
@@ -33,7 +36,6 @@ from .geometry import Offset, Region, Size
|
|||||||
from .layouts.dock import Dock
|
from .layouts.dock import Dock
|
||||||
from .message_pump import MessagePump
|
from .message_pump import MessagePump
|
||||||
from .reactive import Reactive
|
from .reactive import Reactive
|
||||||
from .renderables.gradient import VerticalGradient
|
|
||||||
from .screen import Screen
|
from .screen import Screen
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
@@ -110,7 +112,17 @@ class App(DOMNode):
|
|||||||
self.bindings.bind("ctrl+c", "quit", show=False, allow_forward=False)
|
self.bindings.bind("ctrl+c", "quit", show=False, allow_forward=False)
|
||||||
self._refresh_required = False
|
self._refresh_required = False
|
||||||
|
|
||||||
self.stylesheet = Stylesheet()
|
self.design = ColorSystem(
|
||||||
|
accent1="#ffa726",
|
||||||
|
secondary="#00695c",
|
||||||
|
warning="#ffa000",
|
||||||
|
error="#C62828",
|
||||||
|
success="#558B2F",
|
||||||
|
primary="#1976D2",
|
||||||
|
accent3="#512DA8",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.stylesheet = Stylesheet(variables=self.get_css_variables())
|
||||||
|
|
||||||
self.css_file = css_file
|
self.css_file = css_file
|
||||||
self.css_monitor = (
|
self.css_monitor = (
|
||||||
@@ -128,6 +140,16 @@ class App(DOMNode):
|
|||||||
title: Reactive[str] = Reactive("Textual")
|
title: Reactive[str] = Reactive("Textual")
|
||||||
sub_title: Reactive[str] = Reactive("")
|
sub_title: Reactive[str] = Reactive("")
|
||||||
background: Reactive[str] = Reactive("black")
|
background: Reactive[str] = Reactive("black")
|
||||||
|
dark = Reactive(True)
|
||||||
|
|
||||||
|
def get_css_variables(self) -> dict[str, str]:
|
||||||
|
variables = self.design.generate(self.dark)
|
||||||
|
return variables
|
||||||
|
|
||||||
|
def watch_dark(self, dark: bool) -> None:
|
||||||
|
self.log(dark=dark)
|
||||||
|
self.screen.dark = dark
|
||||||
|
self.refresh_css()
|
||||||
|
|
||||||
def get_driver_class(self) -> Type[Driver]:
|
def get_driver_class(self) -> Type[Driver]:
|
||||||
"""Get a driver class for this platform.
|
"""Get a driver class for this platform.
|
||||||
@@ -137,6 +159,7 @@ class App(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
Driver: A Driver class which manages input and display.
|
Driver: A Driver class which manages input and display.
|
||||||
"""
|
"""
|
||||||
|
driver_class: Type[Driver]
|
||||||
if WINDOWS:
|
if WINDOWS:
|
||||||
from .drivers.windows_driver import WindowsDriver
|
from .drivers.windows_driver import WindowsDriver
|
||||||
|
|
||||||
@@ -248,7 +271,7 @@ class App(DOMNode):
|
|||||||
async def _on_css_change(self) -> None:
|
async def _on_css_change(self) -> None:
|
||||||
|
|
||||||
if self.css_file is not None:
|
if self.css_file is not None:
|
||||||
stylesheet = Stylesheet()
|
stylesheet = Stylesheet(variables=self.get_css_variables())
|
||||||
try:
|
try:
|
||||||
self.log("loading", self.css_file)
|
self.log("loading", self.css_file)
|
||||||
stylesheet.read(self.css_file)
|
stylesheet.read(self.css_file)
|
||||||
@@ -367,16 +390,18 @@ class App(DOMNode):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
if not renderables:
|
if not renderables:
|
||||||
|
|
||||||
renderables = (
|
renderables = (
|
||||||
Traceback(
|
Traceback(
|
||||||
show_locals=True,
|
show_locals=True, width=None, locals_max_length=5, suppress=[rich]
|
||||||
width=None,
|
|
||||||
locals_max_length=5,
|
|
||||||
suppress=[rich],
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._exit_renderables.extend(renderables)
|
|
||||||
|
prerendered = [
|
||||||
|
Segments(self.console.render(renderable, self.console.options))
|
||||||
|
for renderable in renderables
|
||||||
|
]
|
||||||
|
|
||||||
|
self._exit_renderables.extend(prerendered)
|
||||||
self.close_messages_no_wait()
|
self.close_messages_no_wait()
|
||||||
|
|
||||||
def _print_error_renderables(self) -> None:
|
def _print_error_renderables(self) -> None:
|
||||||
@@ -458,24 +483,30 @@ class App(DOMNode):
|
|||||||
Args:
|
Args:
|
||||||
parent (Widget): Parent Widget
|
parent (Widget): Parent Widget
|
||||||
"""
|
"""
|
||||||
|
self.log("app.register", parent, anon_widgets)
|
||||||
if not anon_widgets and not widgets:
|
if not anon_widgets and not widgets:
|
||||||
raise AppError(
|
raise AppError(
|
||||||
"Nothing to mount, did you forget parent as first positional arg?"
|
"Nothing to mount, did you forget parent as first positional arg?"
|
||||||
)
|
)
|
||||||
name_widgets: Iterable[tuple[str | None, Widget]]
|
name_widgets: Iterable[tuple[str | None, Widget]]
|
||||||
name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()]
|
name_widgets = [*((None, widget) for widget in anon_widgets), *widgets.items()]
|
||||||
|
self.log("name_widgets", name_widgets, bool(name_widgets))
|
||||||
apply_stylesheet = self.stylesheet.apply
|
apply_stylesheet = self.stylesheet.apply
|
||||||
|
|
||||||
# Register children
|
# Register children
|
||||||
for widget_id, widget in name_widgets:
|
# for widget_id, widget in name_widgets:
|
||||||
if widget.children:
|
# if widget.children:
|
||||||
self.register(widget, *widget.children)
|
# for child in widget.children:
|
||||||
|
# self.register(child, *child.children)
|
||||||
|
|
||||||
for widget_id, widget in name_widgets:
|
for widget_id, widget in name_widgets:
|
||||||
|
self.log(widget_id=widget_id, widget=widget, _in=widget in self.registry)
|
||||||
if widget not in self.registry:
|
if widget not in self.registry:
|
||||||
if widget_id is not None:
|
if widget_id is not None:
|
||||||
widget.id = widget_id
|
widget.id = widget_id
|
||||||
self._register_child(parent, child=widget)
|
self._register_child(parent, widget)
|
||||||
|
if widget.children:
|
||||||
|
self.register(widget, *widget.children)
|
||||||
apply_stylesheet(widget)
|
apply_stylesheet(widget)
|
||||||
|
|
||||||
for _widget_id, widget in name_widgets:
|
for _widget_id, widget in name_widgets:
|
||||||
@@ -531,6 +562,17 @@ class App(DOMNode):
|
|||||||
except Exception:
|
except Exception:
|
||||||
self.panic()
|
self.panic()
|
||||||
|
|
||||||
|
def refresh_css(self, animate: bool = True) -> None:
|
||||||
|
"""Refresh CSS.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
animate (bool, optional): Also execute CSS animations. Defaults to True.
|
||||||
|
"""
|
||||||
|
# TODO: This doesn't update variables
|
||||||
|
self.app.stylesheet.set_variables(self.get_css_variables())
|
||||||
|
self.app.stylesheet.update(self.app, animate=animate)
|
||||||
|
self.refresh(layout=True)
|
||||||
|
|
||||||
def display(self, renderable: RenderableType) -> None:
|
def display(self, renderable: RenderableType) -> None:
|
||||||
if not self._running:
|
if not self._running:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ from ..geometry import Spacing, SpacingDimensions, clamp
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..layout import Layout
|
from ..layout import Layout
|
||||||
from .styles import Styles, StylesBase
|
from .styles import DockGroup, Styles, StylesBase
|
||||||
from .styles import DockGroup
|
|
||||||
|
|
||||||
from .types import EdgeType
|
from .types import EdgeType
|
||||||
|
|
||||||
@@ -378,7 +378,11 @@ class DocksProperty:
|
|||||||
Returns:
|
Returns:
|
||||||
tuple[DockGroup, ...]: A ``tuple`` containing the defined docks.
|
tuple[DockGroup, ...]: A ``tuple`` containing the defined docks.
|
||||||
"""
|
"""
|
||||||
return obj.get_rule("docks", ())
|
if obj.has_rule("docks"):
|
||||||
|
return obj.get_rule("docks")
|
||||||
|
from .styles import DockGroup
|
||||||
|
|
||||||
|
return (DockGroup("_default", "top", 1),)
|
||||||
|
|
||||||
def __set__(self, obj: StylesBase, docks: Iterable[DockGroup] | None):
|
def __set__(self, obj: StylesBase, docks: Iterable[DockGroup] | None):
|
||||||
"""Set the Docks property
|
"""Set the Docks property
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from .model import (
|
|||||||
SelectorType,
|
SelectorType,
|
||||||
)
|
)
|
||||||
from .styles import Styles
|
from .styles import Styles
|
||||||
from .tokenize import tokenize, tokenize_declarations, Token
|
from .tokenize import tokenize, tokenize_declarations, Token, tokenize_values
|
||||||
from .tokenizer import EOFError, ReferencedBy
|
from .tokenizer import EOFError, ReferencedBy
|
||||||
|
|
||||||
SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
|
SELECTOR_MAP: dict[str, tuple[SelectorType, tuple[int, int, int]]] = {
|
||||||
@@ -212,13 +212,13 @@ def _unresolved(
|
|||||||
|
|
||||||
|
|
||||||
def substitute_references(
|
def substitute_references(
|
||||||
tokens: Iterator[Token], css_variables: dict[str, list[Token]] | None = None
|
tokens: Iterable[Token], css_variables: dict[str, list[Token]] | None = None
|
||||||
) -> Iterable[Token]:
|
) -> Iterable[Token]:
|
||||||
"""Replace variable references with values by substituting variable reference
|
"""Replace variable references with values by substituting variable reference
|
||||||
tokens with the tokens representing their values.
|
tokens with the tokens representing their values.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
tokens (Iterator[Token]): Iterator of Tokens which may contain tokens
|
tokens (Iterable[Token]): Iterator of Tokens which may contain tokens
|
||||||
with the name "variable_ref".
|
with the name "variable_ref".
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -230,8 +230,10 @@ def substitute_references(
|
|||||||
"""
|
"""
|
||||||
variables: dict[str, list[Token]] = css_variables.copy() if css_variables else {}
|
variables: dict[str, list[Token]] = css_variables.copy() if css_variables else {}
|
||||||
|
|
||||||
|
iter_tokens = iter(tokens)
|
||||||
|
|
||||||
while tokens:
|
while tokens:
|
||||||
token = next(tokens, None)
|
token = next(iter_tokens, None)
|
||||||
if token is None:
|
if token is None:
|
||||||
break
|
break
|
||||||
if token.name == "variable_name":
|
if token.name == "variable_name":
|
||||||
@@ -239,7 +241,7 @@ def substitute_references(
|
|||||||
yield token
|
yield token
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
token = next(tokens, None)
|
token = next(iter_tokens, None)
|
||||||
# TODO: Mypy error looks legit
|
# TODO: Mypy error looks legit
|
||||||
if token.name == "whitespace":
|
if token.name == "whitespace":
|
||||||
yield token
|
yield token
|
||||||
@@ -281,7 +283,7 @@ def substitute_references(
|
|||||||
else:
|
else:
|
||||||
variables.setdefault(variable_name, []).append(token)
|
variables.setdefault(variable_name, []).append(token)
|
||||||
yield token
|
yield token
|
||||||
token = next(tokens, None)
|
token = next(iter_tokens, None)
|
||||||
elif token.name == "variable_ref":
|
elif token.name == "variable_ref":
|
||||||
variable_name = token.value[1:] # Trim the $, so $x -> x
|
variable_name = token.value[1:] # Trim the $, so $x -> x
|
||||||
if variable_name in variables:
|
if variable_name in variables:
|
||||||
@@ -302,7 +304,9 @@ def substitute_references(
|
|||||||
yield token
|
yield token
|
||||||
|
|
||||||
|
|
||||||
def parse(css: str, path: str) -> Iterable[RuleSet]:
|
def parse(
|
||||||
|
css: str, path: str, variables: dict[str, str] | None = None
|
||||||
|
) -> Iterable[RuleSet]:
|
||||||
"""Parse CSS by tokenizing it, performing variable substitution,
|
"""Parse CSS by tokenizing it, performing variable substitution,
|
||||||
and generating rule sets from it.
|
and generating rule sets from it.
|
||||||
|
|
||||||
@@ -310,7 +314,8 @@ def parse(css: str, path: str) -> Iterable[RuleSet]:
|
|||||||
css (str): The input CSS
|
css (str): The input CSS
|
||||||
path (str): Path to the CSS
|
path (str): Path to the CSS
|
||||||
"""
|
"""
|
||||||
tokens = iter(substitute_references(tokenize(css, path)))
|
variable_tokens = tokenize_values(variables or {})
|
||||||
|
tokens = iter(substitute_references(tokenize(css, path), variable_tokens))
|
||||||
while True:
|
while True:
|
||||||
token = next(tokens, None)
|
token = next(tokens, None)
|
||||||
if token is None:
|
if token is None:
|
||||||
|
|||||||
@@ -36,10 +36,14 @@ class StylesheetParseError(Exception):
|
|||||||
|
|
||||||
|
|
||||||
class StylesheetErrors:
|
class StylesheetErrors:
|
||||||
def __init__(self, stylesheet: "Stylesheet") -> None:
|
def __init__(
|
||||||
|
self, stylesheet: "Stylesheet", variables: dict[str, str] | None = None
|
||||||
|
) -> None:
|
||||||
self.stylesheet = stylesheet
|
self.stylesheet = stylesheet
|
||||||
self.variables: dict[str, object] = {}
|
self.variables: dict[str, str] = {}
|
||||||
self._css_variables: dict[str, list[Token]] = {}
|
self._css_variables: dict[str, list[Token]] = {}
|
||||||
|
if variables:
|
||||||
|
self.set_variables(variables)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _get_snippet(cls, code: str, line_no: int) -> Panel:
|
def _get_snippet(cls, code: str, line_no: int) -> Panel:
|
||||||
@@ -54,7 +58,7 @@ class StylesheetErrors:
|
|||||||
)
|
)
|
||||||
return Panel(syntax, border_style="red")
|
return Panel(syntax, border_style="red")
|
||||||
|
|
||||||
def add_variables(self, **variable_map: object) -> None:
|
def set_variables(self, variable_map: dict[str, str]) -> None:
|
||||||
"""Pre-populate CSS variables."""
|
"""Pre-populate CSS variables."""
|
||||||
self.variables.update(variable_map)
|
self.variables.update(variable_map)
|
||||||
self._css_variables = tokenize_values(self.variables)
|
self._css_variables = tokenize_values(self.variables)
|
||||||
@@ -95,8 +99,9 @@ class StylesheetErrors:
|
|||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Stylesheet:
|
class Stylesheet:
|
||||||
def __init__(self) -> None:
|
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
|
||||||
self.rules: list[RuleSet] = []
|
self.rules: list[RuleSet] = []
|
||||||
|
self.variables = variables or {}
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield self.rules
|
yield self.rules
|
||||||
@@ -114,6 +119,9 @@ class Stylesheet:
|
|||||||
def error_renderable(self) -> StylesheetErrors:
|
def error_renderable(self) -> StylesheetErrors:
|
||||||
return StylesheetErrors(self)
|
return StylesheetErrors(self)
|
||||||
|
|
||||||
|
def set_variables(self, variables: dict[str, str]) -> None:
|
||||||
|
self.variables = variables
|
||||||
|
|
||||||
def read(self, filename: str) -> None:
|
def read(self, filename: str) -> None:
|
||||||
filename = os.path.expanduser(filename)
|
filename = os.path.expanduser(filename)
|
||||||
try:
|
try:
|
||||||
@@ -123,14 +131,14 @@ class Stylesheet:
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise StylesheetError(f"unable to read {filename!r}; {error}")
|
raise StylesheetError(f"unable to read {filename!r}; {error}")
|
||||||
try:
|
try:
|
||||||
rules = list(parse(css, path))
|
rules = list(parse(css, path, variables=self.variables))
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise StylesheetError(f"failed to parse {filename!r}; {error}")
|
raise StylesheetError(f"failed to parse {filename!r}; {error}")
|
||||||
self.rules.extend(rules)
|
self.rules.extend(rules)
|
||||||
|
|
||||||
def parse(self, css: str, *, path: str = "") -> None:
|
def parse(self, css: str, *, path: str = "") -> None:
|
||||||
try:
|
try:
|
||||||
rules = list(parse(css, path))
|
rules = list(parse(css, path, variables=self.variables))
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
raise StylesheetError(f"failed to parse css; {error}")
|
raise StylesheetError(f"failed to parse css; {error}")
|
||||||
self.rules.extend(rules)
|
self.rules.extend(rules)
|
||||||
|
|||||||
@@ -31,7 +31,13 @@ class ColorProperty:
|
|||||||
|
|
||||||
|
|
||||||
class ColorSystem:
|
class ColorSystem:
|
||||||
"""Defines a standard set of colors and variations for building a UI."""
|
"""Defines a standard set of colors and variations for building a UI.
|
||||||
|
|
||||||
|
Primary is the main theme color
|
||||||
|
Secondary is a second theme color
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
COLOR_NAMES = [
|
COLOR_NAMES = [
|
||||||
"primary",
|
"primary",
|
||||||
@@ -97,7 +103,7 @@ class ColorSystem:
|
|||||||
def generate(
|
def generate(
|
||||||
self,
|
self,
|
||||||
dark: bool = False,
|
dark: bool = False,
|
||||||
luminosity_spread: float = 0.2,
|
luminosity_spread: float = 0.15,
|
||||||
text_alpha: float = 0.9,
|
text_alpha: float = 0.9,
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Generate a mapping of color name on to a CSS color.
|
"""Generate a mapping of color name on to a CSS color.
|
||||||
@@ -129,6 +135,12 @@ class ColorSystem:
|
|||||||
colors: dict[str, str] = {}
|
colors: dict[str, str] = {}
|
||||||
|
|
||||||
def luminosity_range(spread) -> Iterable[tuple[str, float]]:
|
def luminosity_range(spread) -> Iterable[tuple[str, float]]:
|
||||||
|
"""Get the range of shades from darken2 to lighten2.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Iterable of tuples (<SHADE SUFFIX, LUMINOSITY DELTA>)
|
||||||
|
|
||||||
|
"""
|
||||||
luminosity_step = spread / 2
|
luminosity_step = spread / 2
|
||||||
for n in range(-2, +3):
|
for n in range(-2, +3):
|
||||||
if n < 0:
|
if n < 0:
|
||||||
@@ -139,6 +151,7 @@ class ColorSystem:
|
|||||||
label = ""
|
label = ""
|
||||||
yield (f"{label}{abs(n) if n else ''}"), n * luminosity_step
|
yield (f"{label}{abs(n) if n else ''}"), n * luminosity_step
|
||||||
|
|
||||||
|
# Color names and color
|
||||||
COLORS = [
|
COLORS = [
|
||||||
("primary", primary),
|
("primary", primary),
|
||||||
("secondary", secondary),
|
("secondary", secondary),
|
||||||
@@ -152,6 +165,7 @@ class ColorSystem:
|
|||||||
("accent3", accent3),
|
("accent3", accent3),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Colors names that have a dark varient
|
||||||
DARK_SHADES = {"primary", "secondary"}
|
DARK_SHADES = {"primary", "secondary"}
|
||||||
|
|
||||||
for name, color in COLORS:
|
for name, color in COLORS:
|
||||||
@@ -203,8 +217,8 @@ class ColorSystem:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
color_system = ColorSystem(
|
color_system = ColorSystem(
|
||||||
primary="#4caf50",
|
primary="#1b5e20",
|
||||||
secondary="#ffa000",
|
secondary="#263238",
|
||||||
warning="#ffa000",
|
warning="#ffa000",
|
||||||
error="#C62828",
|
error="#C62828",
|
||||||
success="#558B2F",
|
success="#558B2F",
|
||||||
|
|||||||
@@ -19,26 +19,6 @@ class WidgetPlacement(NamedTuple):
|
|||||||
widget: Widget | None = None # A widget of None means empty space
|
widget: Widget | None = None # A widget of None means empty space
|
||||||
order: int = 0
|
order: int = 0
|
||||||
|
|
||||||
def apply_margin(self) -> "WidgetPlacement":
|
|
||||||
"""Apply any margin present in the styles of the widget by shrinking the
|
|
||||||
region appropriately.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
WidgetPlacement: Returns ``self`` if no ``margin`` styles are present in
|
|
||||||
the widget. Otherwise, returns a copy of self with a region shrunk to
|
|
||||||
account for margin.
|
|
||||||
"""
|
|
||||||
region, widget, order = self
|
|
||||||
if widget is not None:
|
|
||||||
styles = widget.styles
|
|
||||||
if styles.margin:
|
|
||||||
return WidgetPlacement(
|
|
||||||
region=region.shrink(styles.margin),
|
|
||||||
widget=widget,
|
|
||||||
order=order,
|
|
||||||
)
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class Layout(ABC):
|
class Layout(ABC):
|
||||||
"""Responsible for arranging Widgets in a view and rendering them."""
|
"""Responsible for arranging Widgets in a view and rendering them."""
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from . import events, messages, errors
|
|||||||
|
|
||||||
from .geometry import Offset, Region
|
from .geometry import Offset, Region
|
||||||
from ._compositor import Compositor
|
from ._compositor import Compositor
|
||||||
|
from .reactive import Reactive
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
from .renderables.gradient import VerticalGradient
|
from .renderables.gradient import VerticalGradient
|
||||||
|
|
||||||
@@ -24,11 +25,16 @@ class Screen(Widget):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
dark = Reactive(False)
|
||||||
|
|
||||||
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
def __init__(self, name: str | None = None, id: str | None = None) -> None:
|
||||||
super().__init__(name=name, id=id)
|
super().__init__(name=name, id=id)
|
||||||
self._compositor = Compositor()
|
self._compositor = Compositor()
|
||||||
self._dirty_widgets: list[Widget] = []
|
self._dirty_widgets: list[Widget] = []
|
||||||
|
|
||||||
|
def watch_dark(self, dark: bool) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_transparent(self) -> bool:
|
def is_transparent(self) -> bool:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class Widget(DOMNode):
|
|||||||
can_focus: bool = False
|
can_focus: bool = False
|
||||||
|
|
||||||
DEFAULT_STYLES = """
|
DEFAULT_STYLES = """
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -421,7 +421,10 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def region(self) -> Region:
|
def region(self) -> Region:
|
||||||
return self.screen._compositor.get_widget_region(self)
|
try:
|
||||||
|
return self.screen._compositor.get_widget_region(self)
|
||||||
|
except errors.NoWidget:
|
||||||
|
return Region()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def scroll_offset(self) -> Offset:
|
def scroll_offset(self) -> Offset:
|
||||||
|
|||||||
Reference in New Issue
Block a user