mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
CSS inheritance
This commit is contained in:
@@ -3,16 +3,18 @@ from textual.widgets import Button
|
|||||||
|
|
||||||
|
|
||||||
class ButtonApp(App):
|
class ButtonApp(App):
|
||||||
|
|
||||||
CSS = """
|
CSS = """
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def compose(self):
|
def compose(self):
|
||||||
yield Button("Light", id="light")
|
yield Button("Lights off")
|
||||||
yield Button("Dark", id="dark")
|
|
||||||
|
|
||||||
def handle_pressed(self, event):
|
def handle_pressed(self, event):
|
||||||
self.dark = event.button.id == "dark"
|
self.dark = not self.dark
|
||||||
|
event.button.label = "Lights ON" if self.dark else "Lights OFF"
|
||||||
|
|||||||
@@ -11,19 +11,21 @@
|
|||||||
scrollbar-background-hover: $panel-darken-3;
|
scrollbar-background-hover: $panel-darken-3;
|
||||||
scrollbar-color: $system;
|
scrollbar-color: $system;
|
||||||
scrollbar-color-active: $accent-darken-1;
|
scrollbar-color-active: $accent-darken-1;
|
||||||
|
scrollbar-size-horizontal: 1;
|
||||||
|
scrollbar-size-vertical: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
App > Screen {
|
App > Screen {
|
||||||
layout: dock;
|
layout: dock;
|
||||||
docks: side=left/1;
|
docks: side=left/1;
|
||||||
background: $surface;
|
background: $surface;
|
||||||
color: $text-surface;
|
color: $text-surface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#sidebar {
|
#sidebar {
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
background: $primary;
|
background: $primary-background;
|
||||||
dock: side;
|
dock: side;
|
||||||
width: 30;
|
width: 30;
|
||||||
offset-x: -100%;
|
offset-x: -100%;
|
||||||
@@ -37,7 +39,7 @@ App > Screen {
|
|||||||
|
|
||||||
#sidebar .title {
|
#sidebar .title {
|
||||||
height: 3;
|
height: 3;
|
||||||
background: $primary-darken-2;
|
background: $primary-background-darken-2;
|
||||||
color: $text-primary-darken-2 ;
|
color: $text-primary-darken-2 ;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: outer $primary-darken-3;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
@@ -45,16 +47,16 @@ App > Screen {
|
|||||||
|
|
||||||
#sidebar .user {
|
#sidebar .user {
|
||||||
height: 8;
|
height: 8;
|
||||||
background: $primary-darken-1;
|
background: $primary-background-darken-1;
|
||||||
color: $text-primary-darken-1;
|
color: $text-primary-darken-1;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: outer $primary-background-darken-3;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar .content {
|
#sidebar .content {
|
||||||
background: $primary;
|
background: $primary-background;
|
||||||
color: $text-primary;
|
color: $text-primary-background;
|
||||||
border-right: outer $primary-darken-3;
|
border-right: outer $primary-background-darken-3;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,14 +92,6 @@ Tweet {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
Tweet.scrollbar-size-custom {
|
|
||||||
scrollbar-size-vertical: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Tweet.scroll-horizontal {
|
|
||||||
scrollbar-size-horizontal: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scrollable {
|
.scrollable {
|
||||||
width: 80;
|
width: 80;
|
||||||
@@ -178,8 +172,8 @@ Tweet.scroll-horizontal TweetBody {
|
|||||||
|
|
||||||
OptionItem {
|
OptionItem {
|
||||||
height: 3;
|
height: 3;
|
||||||
background: $primary;
|
background: $primary-background;
|
||||||
border-right: outer $primary-darken-2;
|
border-right: outer $primary-background-darken-2;
|
||||||
border-left: blank;
|
border-left: blank;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
}
|
}
|
||||||
@@ -187,7 +181,7 @@ OptionItem {
|
|||||||
OptionItem:hover {
|
OptionItem:hover {
|
||||||
height: 3;
|
height: 3;
|
||||||
color: $accent;
|
color: $accent;
|
||||||
background: $primary-darken-1;
|
background: $primary-background-darken-1;
|
||||||
/* border-top: hkey $accent2-darken-3;
|
/* border-top: hkey $accent2-darken-3;
|
||||||
border-bottom: hkey $accent2-darken-3; */
|
border-bottom: hkey $accent2-darken-3; */
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ class Hr(Widget):
|
|||||||
|
|
||||||
|
|
||||||
class Info(Widget):
|
class Info(Widget):
|
||||||
DEFAULT_STYLES = "height: 2;"
|
|
||||||
|
|
||||||
def __init__(self, text: str) -> None:
|
def __init__(self, text: str) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.text = text
|
self.text = text
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ from ._context import active_app
|
|||||||
from ._event_broker import extract_handler_actions, NoHandler
|
from ._event_broker import extract_handler_actions, NoHandler
|
||||||
from .binding import Bindings, NoBinding
|
from .binding import Bindings, NoBinding
|
||||||
from .css.stylesheet import Stylesheet
|
from .css.stylesheet import Stylesheet
|
||||||
|
from .css.styles import RenderStyles
|
||||||
from .css.query import NoMatchingNodesError
|
from .css.query import NoMatchingNodesError
|
||||||
from .design import ColorSystem
|
from .design import ColorSystem
|
||||||
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
|
from .devtools.client import DevtoolsClient, DevtoolsConnectionError, DevtoolsLog
|
||||||
@@ -61,6 +62,7 @@ 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.blank import Blank
|
from .renderables.blank import Blank
|
||||||
|
|
||||||
from .screen import Screen
|
from .screen import Screen
|
||||||
from .widget import Widget
|
from .widget import Widget
|
||||||
|
|
||||||
@@ -109,9 +111,9 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
"""The base class for Textual Applications"""
|
"""The base class for Textual Applications"""
|
||||||
|
|
||||||
CSS = """
|
CSS = """
|
||||||
$WIDGET {
|
App {
|
||||||
background: $surface;
|
background: $surface;
|
||||||
color: $text-surface;
|
color: $text-surface;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -144,6 +146,7 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
# this will create some first references to an asyncio loop.
|
# this will create some first references to an asyncio loop.
|
||||||
_init_uvloop()
|
_init_uvloop()
|
||||||
|
|
||||||
|
super().__init__()
|
||||||
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
|
self.features: frozenset[FeatureFlag] = parse_features(os.getenv("TEXTUAL", ""))
|
||||||
|
|
||||||
self.console = Console(
|
self.console = Console(
|
||||||
@@ -206,10 +209,10 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
super().__init__()
|
def __init_subclass__(
|
||||||
|
cls, css_path: str | None = None, inherit_css: bool = True
|
||||||
def __init_subclass__(cls, css_path: str | None = None) -> None:
|
) -> None:
|
||||||
super().__init_subclass__()
|
super().__init_subclass__(inherit_css=inherit_css)
|
||||||
cls.CSS_PATH = css_path
|
cls.CSS_PATH = css_path
|
||||||
|
|
||||||
title: Reactive[str] = Reactive("Textual")
|
title: Reactive[str] = Reactive("Textual")
|
||||||
@@ -340,7 +343,15 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
def watch_dark(self, dark: bool) -> None:
|
def watch_dark(self, dark: bool) -> None:
|
||||||
"""Watches the dark bool."""
|
"""Watches the dark bool."""
|
||||||
|
|
||||||
self.screen.dark = dark
|
self.screen.dark = dark
|
||||||
|
if dark:
|
||||||
|
self.add_class("-dark-mode")
|
||||||
|
self.remove_class("-light-mode")
|
||||||
|
else:
|
||||||
|
self.remove_class("-dark-mode")
|
||||||
|
self.add_class("-light-mode")
|
||||||
|
|
||||||
self.refresh_css()
|
self.refresh_css()
|
||||||
|
|
||||||
def get_driver_class(self) -> Type[Driver]:
|
def get_driver_class(self) -> Type[Driver]:
|
||||||
@@ -364,6 +375,18 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield "title", self.title
|
yield "title", self.title
|
||||||
|
yield "id", self.id, None
|
||||||
|
if self.name:
|
||||||
|
yield "name", self.name
|
||||||
|
if self.classes:
|
||||||
|
yield "classes", set(self.classes)
|
||||||
|
pseudo_classes = self.pseudo_classes
|
||||||
|
if pseudo_classes:
|
||||||
|
yield "pseudo_classes", set(pseudo_classes)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_transparent(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def animator(self) -> Animator:
|
def animator(self) -> Animator:
|
||||||
@@ -373,10 +396,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
def screen(self) -> Screen:
|
def screen(self) -> Screen:
|
||||||
return self._screen_stack[-1]
|
return self._screen_stack[-1]
|
||||||
|
|
||||||
@property
|
|
||||||
def css_type(self) -> str:
|
|
||||||
return "app"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> Size:
|
def size(self) -> Size:
|
||||||
return Size(*self.console.size)
|
return Size(*self.console.size)
|
||||||
@@ -680,10 +699,6 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
error (Exception): An exception instance.
|
error (Exception): An exception instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if "tb" in self.features:
|
|
||||||
self.fatal_error()
|
|
||||||
return
|
|
||||||
|
|
||||||
if hasattr(error, "__rich__"):
|
if hasattr(error, "__rich__"):
|
||||||
# Exception has a rich method, so we can defer to that for the rendering
|
# Exception has a rich method, so we can defer to that for the rendering
|
||||||
self.panic(error)
|
self.panic(error)
|
||||||
@@ -725,13 +740,8 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
try:
|
try:
|
||||||
if self.css_path is not None:
|
if self.css_path is not None:
|
||||||
self.stylesheet.read(self.css_path)
|
self.stylesheet.read(self.css_path)
|
||||||
if self.CSS is not None:
|
for path, css in self.css:
|
||||||
css_code = string.Template(self.CSS).safe_substitute(
|
self.stylesheet.add_source(css, path=path)
|
||||||
{"WIDGET": self.css_type}
|
|
||||||
)
|
|
||||||
self.stylesheet.add_source(
|
|
||||||
css_code, path=f"<{self.__class__.__name__}>"
|
|
||||||
)
|
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
self.on_exception(error)
|
self.on_exception(error)
|
||||||
self._print_error_renderables()
|
self._print_error_renderables()
|
||||||
|
|||||||
@@ -86,8 +86,10 @@ class Selector:
|
|||||||
return node.has_pseudo_class(*self.pseudo_classes)
|
return node.has_pseudo_class(*self.pseudo_classes)
|
||||||
|
|
||||||
def _check_type(self, node: DOMNode) -> bool:
|
def _check_type(self, node: DOMNode) -> bool:
|
||||||
if node.css_type != self._name_lower:
|
if self._name_lower not in node.css_type_names:
|
||||||
return False
|
return False
|
||||||
|
# if node.css_type != self._name_lower:
|
||||||
|
# return False
|
||||||
if self.pseudo_classes and not node.has_pseudo_class(*self.pseudo_classes):
|
if self.pseudo_classes and not node.has_pseudo_class(*self.pseudo_classes):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ class DOMQuery:
|
|||||||
|
|
||||||
if selector is not None:
|
if selector is not None:
|
||||||
selector_set = parse_selectors(selector)
|
selector_set = parse_selectors(selector)
|
||||||
|
print(selector_set)
|
||||||
self._nodes = [_node for _node in self._nodes if match(selector_set, _node)]
|
self._nodes = [_node for _node in self._nodes if match(selector_set, _node)]
|
||||||
|
print(self._nodes)
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return len(self._nodes)
|
return len(self._nodes)
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class StylesheetErrors:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto(angular=True)
|
||||||
class Stylesheet:
|
class Stylesheet:
|
||||||
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
|
def __init__(self, *, variables: dict[str, str] | None = None) -> None:
|
||||||
self._rules: list[RuleSet] = []
|
self._rules: list[RuleSet] = []
|
||||||
@@ -124,7 +124,7 @@ class Stylesheet:
|
|||||||
self._require_parse = False
|
self._require_parse = False
|
||||||
|
|
||||||
def __rich_repr__(self) -> rich.repr.Result:
|
def __rich_repr__(self) -> rich.repr.Result:
|
||||||
yield self.rules
|
yield list(self.source.keys())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def rules(self) -> list[RuleSet]:
|
def rules(self) -> list[RuleSet]:
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ class ColorSystem:
|
|||||||
COLORS = [
|
COLORS = [
|
||||||
("primary", primary),
|
("primary", primary),
|
||||||
("secondary", secondary),
|
("secondary", secondary),
|
||||||
|
("primary-background", primary),
|
||||||
|
("secondary-background", secondary),
|
||||||
("background", background),
|
("background", background),
|
||||||
("panel", panel),
|
("panel", panel),
|
||||||
("surface", surface),
|
("surface", surface),
|
||||||
@@ -199,7 +201,7 @@ class ColorSystem:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Colors names that have a dark variant
|
# Colors names that have a dark variant
|
||||||
DARK_SHADES = {"primary", "secondary"}
|
DARK_SHADES = {"primary-background", "secondary-background"}
|
||||||
|
|
||||||
for name, color in COLORS:
|
for name, color in COLORS:
|
||||||
is_dark_shade = dark and name in DARK_SHADES
|
is_dark_shade = dark and name in DARK_SHADES
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Iterable, Iterator, TYPE_CHECKING
|
from inspect import getfile
|
||||||
|
from typing import ClassVar, Iterable, Iterator, Type, TYPE_CHECKING
|
||||||
|
|
||||||
import rich.repr
|
import rich.repr
|
||||||
from rich.highlighter import ReprHighlighter
|
from rich.highlighter import ReprHighlighter
|
||||||
@@ -38,8 +39,10 @@ class DOMNode(MessagePump):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_STYLES = ""
|
CSS: ClassVar[str] = ""
|
||||||
INLINE_STYLES = ""
|
|
||||||
|
inherit_css: ClassVar[bool] = True
|
||||||
|
css_type_names: ClassVar[frozenset[str]] = frozenset()
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -53,14 +56,38 @@ class DOMNode(MessagePump):
|
|||||||
self._classes: set[str] = set() if classes is None else set(classes.split())
|
self._classes: set[str] = set() if classes is None else set(classes.split())
|
||||||
self.children = NodeList()
|
self.children = NodeList()
|
||||||
self._css_styles: Styles = Styles(self)
|
self._css_styles: Styles = Styles(self)
|
||||||
self._inline_styles: Styles = Styles.parse(
|
self._inline_styles: Styles = Styles(self)
|
||||||
self.INLINE_STYLES, repr(self), node=self
|
|
||||||
)
|
|
||||||
self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
|
self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
|
||||||
self._default_styles = Styles()
|
self._default_styles = Styles()
|
||||||
self._default_rules = self._default_styles.extract_rules((0, 0, 0))
|
self._default_rules = self._default_styles.extract_rules((0, 0, 0))
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
def __init_subclass__(cls, inherit_css: bool = True) -> None:
|
||||||
|
super().__init_subclass__()
|
||||||
|
cls.inherit_css = inherit_css
|
||||||
|
css_type_names: set[str] = set()
|
||||||
|
for base in cls._css_bases(cls):
|
||||||
|
css_type_names.add(base.__name__.lower())
|
||||||
|
cls.css_type_names = frozenset(css_type_names)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _node_bases(self) -> Iterator[Type[DOMNode]]:
|
||||||
|
return self._css_bases(self.__class__)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _css_bases(cls, base: Type[DOMNode]) -> Iterator[Type[DOMNode]]:
|
||||||
|
_class = base
|
||||||
|
while True:
|
||||||
|
yield _class
|
||||||
|
if not _class.inherit_css:
|
||||||
|
break
|
||||||
|
for _base in _class.__bases__:
|
||||||
|
if issubclass(_base, DOMNode):
|
||||||
|
_class = _base
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
def on_register(self, app: App) -> None:
|
def on_register(self, app: App) -> None:
|
||||||
"""Called when the widget is registered
|
"""Called when the widget is registered
|
||||||
|
|
||||||
@@ -74,6 +101,25 @@ class DOMNode(MessagePump):
|
|||||||
if self._classes:
|
if self._classes:
|
||||||
yield "classes", " ".join(self._classes)
|
yield "classes", " ".join(self._classes)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def css(self) -> list[tuple[str, str]]:
|
||||||
|
"""Combined CSS from base classes"""
|
||||||
|
|
||||||
|
css_stack: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
def get_path(base: Type[DOMNode]) -> str:
|
||||||
|
try:
|
||||||
|
return f"{getfile(base)}:{base.__name__}"
|
||||||
|
except TypeError:
|
||||||
|
return f"{base.__name__}"
|
||||||
|
|
||||||
|
for base in self._node_bases:
|
||||||
|
css = base.CSS.strip()
|
||||||
|
if css:
|
||||||
|
css_stack.append((get_path(base), css))
|
||||||
|
|
||||||
|
return css_stack
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def parent(self) -> DOMNode | None:
|
def parent(self) -> DOMNode | None:
|
||||||
"""Get the parent node.
|
"""Get the parent node.
|
||||||
@@ -158,15 +204,6 @@ class DOMNode(MessagePump):
|
|||||||
pseudo_classes = frozenset({*self.get_pseudo_classes()})
|
pseudo_classes = frozenset({*self.get_pseudo_classes()})
|
||||||
return pseudo_classes
|
return pseudo_classes
|
||||||
|
|
||||||
@property
|
|
||||||
def css_type(self) -> str:
|
|
||||||
"""Gets the CSS type, used by the CSS.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A type used in CSS (lower cased class name).
|
|
||||||
"""
|
|
||||||
return self.__class__.__name__.lower()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def css_path_nodes(self) -> list[DOMNode]:
|
def css_path_nodes(self) -> list[DOMNode]:
|
||||||
"""A list of nodes from the root to this node, forming a "path".
|
"""A list of nodes from the root to this node, forming a "path".
|
||||||
@@ -238,20 +275,29 @@ class DOMNode(MessagePump):
|
|||||||
from rich.console import Group
|
from rich.console import Group
|
||||||
from rich.panel import Panel
|
from rich.panel import Panel
|
||||||
|
|
||||||
highlighter = ReprHighlighter()
|
from .widget import Widget
|
||||||
tree = Tree(highlighter(repr(self)))
|
|
||||||
|
|
||||||
def add_children(tree, node):
|
def render_info(node: DOMNode) -> Columns:
|
||||||
for child in node.children:
|
if isinstance(node, Widget):
|
||||||
info = Columns(
|
info = Columns(
|
||||||
[
|
[
|
||||||
Pretty(child),
|
Pretty(node),
|
||||||
highlighter(f"region={child.region!r}"),
|
highlighter(f"region={node.region!r}"),
|
||||||
highlighter(
|
highlighter(
|
||||||
f"virtual_size={child.virtual_size!r}",
|
f"virtual_size={node.virtual_size!r}",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
info = Columns([Pretty(node)])
|
||||||
|
return info
|
||||||
|
|
||||||
|
highlighter = ReprHighlighter()
|
||||||
|
tree = Tree(render_info(self))
|
||||||
|
|
||||||
|
def add_children(tree, node):
|
||||||
|
for child in node.children:
|
||||||
|
info = render_info(child)
|
||||||
css = child.styles.css
|
css = child.styles.css
|
||||||
if css:
|
if css:
|
||||||
info = Group(
|
info = Group(
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class Screen(Widget):
|
|||||||
"""A widget for the root of the app."""
|
"""A widget for the root of the app."""
|
||||||
|
|
||||||
CSS = """
|
CSS = """
|
||||||
$WIDGET {
|
Screen {
|
||||||
layout: vertical;
|
layout: vertical;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,9 +67,6 @@ class RenderCache(NamedTuple):
|
|||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class Widget(DOMNode):
|
class Widget(DOMNode):
|
||||||
|
|
||||||
CSS = """
|
|
||||||
"""
|
|
||||||
|
|
||||||
can_focus: bool = False
|
can_focus: bool = False
|
||||||
can_focus_children: bool = True
|
can_focus_children: bool = True
|
||||||
|
|
||||||
@@ -177,11 +174,9 @@ class Widget(DOMNode):
|
|||||||
Args:
|
Args:
|
||||||
app (App): App instance.
|
app (App): App instance.
|
||||||
"""
|
"""
|
||||||
css_code = string.Template(self.CSS).safe_substitute({"WIDGET": self.css_type})
|
# Parse the Widget's CSS
|
||||||
# Parser the Widget's CSS
|
for path, css in self.css:
|
||||||
self.app.stylesheet.add_source(
|
self.app.stylesheet.add_source(css, path=path)
|
||||||
css_code, f"{__file__}:<{self.__class__.__name__}>"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_box_model(
|
def get_box_model(
|
||||||
self, container: Size, viewport: Size, fraction_unit: Fraction
|
self, container: Size, viewport: Size, fraction_unit: Fraction
|
||||||
@@ -536,12 +531,16 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def scroll_page_left(self, *, animate: bool = True) -> bool:
|
def scroll_page_left(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
x=self.scroll_target_x - self.container_size.width, animate=animate
|
x=self.scroll_target_x - self.container_size.width,
|
||||||
|
animate=animate,
|
||||||
|
duration=0.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_page_right(self, *, animate: bool = True) -> bool:
|
def scroll_page_right(self, *, animate: bool = True) -> bool:
|
||||||
return self.scroll_to(
|
return self.scroll_to(
|
||||||
x=self.scroll_target_x + self.container_size.width, animate=animate
|
x=self.scroll_target_x + self.container_size.width,
|
||||||
|
animate=animate,
|
||||||
|
duration=0.3,
|
||||||
)
|
)
|
||||||
|
|
||||||
def scroll_to_widget(self, widget: Widget, *, animate: bool = True) -> bool:
|
def scroll_to_widget(self, widget: Widget, *, animate: bool = True) -> bool:
|
||||||
@@ -589,9 +588,12 @@ class Widget(DOMNode):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def __init_subclass__(
|
def __init_subclass__(
|
||||||
cls, can_focus: bool = True, can_focus_children: bool = True
|
cls,
|
||||||
|
can_focus: bool = True,
|
||||||
|
can_focus_children: bool = True,
|
||||||
|
inherit_css: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init_subclass__()
|
super().__init_subclass__(inherit_css=inherit_css)
|
||||||
cls.can_focus = can_focus
|
cls.can_focus = can_focus
|
||||||
cls.can_focus_children = can_focus_children
|
cls.can_focus_children = can_focus_children
|
||||||
|
|
||||||
@@ -1019,7 +1021,7 @@ class Widget(DOMNode):
|
|||||||
|
|
||||||
def handle_scroll_to(self, message: ScrollTo) -> None:
|
def handle_scroll_to(self, message: ScrollTo) -> None:
|
||||||
if self.is_container:
|
if self.is_container:
|
||||||
self.scroll_to(message.x, message.y, animate=message.animate)
|
self.scroll_to(message.x, message.y, animate=message.animate, duration=0.1)
|
||||||
message.stop()
|
message.stop()
|
||||||
|
|
||||||
def handle_scroll_up(self, event: ScrollUp) -> None:
|
def handle_scroll_up(self, event: ScrollUp) -> None:
|
||||||
|
|||||||
@@ -17,25 +17,38 @@ class Button(Widget, can_focus=True):
|
|||||||
|
|
||||||
CSS = """
|
CSS = """
|
||||||
|
|
||||||
$WIDGET {
|
|
||||||
|
Button {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 3;
|
height: 3;
|
||||||
background: $primary;
|
background: $primary;
|
||||||
color: $text-primary;
|
color: $text-primary;
|
||||||
content-align: center middle;
|
content-align: center middle;
|
||||||
border: tall $primary-lighten-3;
|
border: tall $primary-lighten-3;
|
||||||
|
|
||||||
margin: 1 0;
|
margin: 1 0;
|
||||||
align: center middle;
|
align: center middle;
|
||||||
text-style: bold;
|
text-style: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
$WIDGET:hover {
|
.-dark-mode Button {
|
||||||
|
border: tall white $primary-lighten-2;
|
||||||
|
color: $primary-lighten-2;
|
||||||
|
background: $background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.-dark-mode Button:hover {
|
||||||
|
background: $surface;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Button:hover {
|
||||||
background:$primary-darken-2;
|
background:$primary-darken-2;
|
||||||
color: $text-primary-darken-2;
|
color: $text-primary-darken-2;
|
||||||
border: tall $primary-lighten-1;
|
border: tall $primary-lighten-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
App.-show-focus $WIDGET:focus {
|
App.-show-focus Button:focus {
|
||||||
tint: $accent 20%;
|
tint: $accent 20%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,8 +191,6 @@ class Tabs(Widget):
|
|||||||
that character.
|
that character.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_STYLES = "height: 2;"
|
|
||||||
|
|
||||||
_active_tab_name: Reactive[str | None] = Reactive("")
|
_active_tab_name: Reactive[str | None] = Reactive("")
|
||||||
_bar_offset: Reactive[float] = Reactive(0.0)
|
_bar_offset: Reactive[float] = Reactive(0.0)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user