From bd82ece9cf5dffb8ef7691153aaa64db7614e813 Mon Sep 17 00:00:00 2001 From: Will McGugan Date: Thu, 11 Aug 2022 16:45:31 +0100 Subject: [PATCH] fix for footer --- sandbox/will/basic.css | 3 +- sandbox/will/basic.py | 27 +++++++------- sandbox/will/footer.py | 13 +++++++ src/textual/css/styles.py | 10 ++++-- src/textual/css/stylesheet.py | 8 ++--- src/textual/dom.py | 19 +++++++++- src/textual/widget.py | 7 ++-- src/textual/widgets/_data_table.py | 10 +++--- src/textual/widgets/_footer.py | 54 +++++++++++++++++++++++++--- src/textual/widgets/_tree_control.py | 2 +- 10 files changed, 117 insertions(+), 36 deletions(-) create mode 100644 sandbox/will/footer.py diff --git a/sandbox/will/basic.css b/sandbox/will/basic.css index 829bc3db1..f315a990a 100644 --- a/sandbox/will/basic.css +++ b/sandbox/will/basic.css @@ -47,7 +47,7 @@ DataTable { /* opacity: 50%; */ padding: 1; margin: 1 2; - height: 12; + height: 24; } #sidebar { @@ -55,6 +55,7 @@ DataTable { background: $panel; dock: left; width: 30; + margin-bottom: 1; offset-x: -100%; transition: offset 500ms in_out_cubic; diff --git a/sandbox/will/basic.py b/sandbox/will/basic.py index db0d41b54..ce18199f9 100644 --- a/sandbox/will/basic.py +++ b/sandbox/will/basic.py @@ -6,7 +6,7 @@ from rich.text import Text from textual.app import App, ComposeResult from textual.reactive import Reactive from textual.widget import Widget -from textual.widgets import Static, DataTable, DirectoryTree +from textual.widgets import Static, DataTable, DirectoryTree, Footer from textual.layout import Vertical CODE = ''' @@ -109,7 +109,8 @@ class BasicApp(App, css_path="basic.css"): def on_load(self): """Bind keys here.""" - self.bind("s", "toggle_class('#sidebar', '-active')") + self.bind("s", "toggle_class('#sidebar', '-active')", description="Sidebar") + self.bind("d", "toggle_dark()", description="Dark mode") def compose(self) -> ComposeResult: table = DataTable() @@ -142,17 +143,17 @@ class BasicApp(App, css_path="basic.css"): Tweet(TweetBody(), classes="scroll-horizontal"), Tweet(TweetBody(), classes="scroll-horizontal"), Tweet(TweetBody(), classes="scroll-horizontal"), + Widget( + Widget(classes="title"), + Widget(classes="user"), + OptionItem(), + OptionItem(), + OptionItem(), + Widget(classes="content"), + id="sidebar", + ), ) - yield Widget(id="footer") - yield Widget( - Widget(classes="title"), - Widget(classes="user"), - OptionItem(), - OptionItem(), - OptionItem(), - Widget(classes="content"), - id="sidebar", - ) + yield Footer() table.add_column("Foo", width=20) table.add_column("Bar", width=20) @@ -167,7 +168,7 @@ class BasicApp(App, css_path="basic.css"): async def on_key(self, event) -> None: await self.dispatch_key(event) - def key_d(self): + def action_toggle_dark(self): self.dark = not self.dark async def key_q(self): diff --git a/sandbox/will/footer.py b/sandbox/will/footer.py new file mode 100644 index 000000000..b09db10ea --- /dev/null +++ b/sandbox/will/footer.py @@ -0,0 +1,13 @@ +from textual.app import App +from textual.widgets import Footer + + +class FooterApp(App): + def on_mount(self): + self.dark = True + self.bind("b", "app.bell", description="Play the Bell") + self.bind("f1", "app.bell", description="Hello World") + self.bind("f2", "app.bell", description="Do something") + + def compose(self): + yield Footer() diff --git a/src/textual/css/styles.py b/src/textual/css/styles.py index 8485e650b..2dd11713a 100644 --- a/src/textual/css/styles.py +++ b/src/textual/css/styles.py @@ -12,7 +12,7 @@ from rich.style import Style from .._animator import Animation, EasingFunction from ..color import Color -from ..geometry import Offset, Size, Spacing +from ..geometry import Offset, Spacing from ._style_properties import ( AlignProperty, BorderProperty, @@ -223,7 +223,7 @@ class StylesBase(ABC): layers = NameListProperty() transitions = TransitionsProperty() - rich_style = StyleProperty() + # rich_style = StyleProperty() tint = ColorProperty("transparent") scrollbar_color = ColorProperty("ansi_bright_magenta") @@ -800,6 +800,12 @@ class RenderStyles(StylesBase): """Quick access to the inline styles.""" return self._inline_styles + @property + def rich_style(self) -> Style: + """Get a Rich style for this Styles object.""" + assert self.node is not None + return self.node.rich_style + def __rich_repr__(self) -> rich.repr.Result: for rule_name in RULE_NAMES: if self.has_rule(rule_name): diff --git a/src/textual/css/stylesheet.py b/src/textual/css/stylesheet.py index ff6a32948..06957892e 100644 --- a/src/textual/css/stylesheet.py +++ b/src/textual/css/stylesheet.py @@ -23,7 +23,7 @@ from .parse import parse from .styles import RulesMap, Styles from .tokenize import tokenize_values, Token from .tokenizer import TokenError -from .types import Specificity3, Specificity5 +from .types import Specificity3, Specificity6 from ..dom import DOMNode from .. import messages @@ -325,7 +325,7 @@ class Stylesheet: # We can use this to determine, for a given rule, whether we should apply it # or not by examining the specificity. If we have two rules for the # same attribute, then we can choose the most specific rule and use that. - rule_attributes: dict[str, list[tuple[Specificity5, object]]] + rule_attributes: dict[str, list[tuple[Specificity6, object]]] rule_attributes = defaultdict(list) _check_rule = self._check_rule @@ -352,12 +352,12 @@ class Stylesheet: self.replace_rules(node, node_rules, animate=animate) - node.component_styles.clear() + node._component_styles.clear() for component in node.COMPONENT_CLASSES: virtual_node = DOMNode(classes=component) virtual_node.set_parent(node) self.apply(virtual_node, animate=False) - node.component_styles[component] = virtual_node.styles + node._component_styles[component] = virtual_node.styles @classmethod def replace_rules( diff --git a/src/textual/dom.py b/src/textual/dom.py index d67ae9db7..1fbf3c03d 100644 --- a/src/textual/dom.py +++ b/src/textual/dom.py @@ -68,7 +68,7 @@ class DOMNode(MessagePump): self._inline_styles: Styles = Styles(self) self.styles = RenderStyles(self, self._css_styles, self._inline_styles) # A mapping of class names to Styles set in COMPONENT_CLASSES - self.component_styles: dict[str, StylesBase] = {} + self._component_styles: dict[str, RenderStyles] = {} super().__init__() @@ -80,6 +80,23 @@ class DOMNode(MessagePump): css_type_names.add(base.__name__.lower()) cls._css_type_names = frozenset(css_type_names) + def get_component_styles(self, name: str) -> RenderStyles: + """Get a "component" styles object (must be defined in COMPONENT_CLASSES classvar). + + Args: + name (str): Name of the component. + + Raises: + KeyError: If the component class doesn't exist. + + Returns: + RenderStyles: A Styles object. + """ + if name not in self._component_styles: + raise KeyError(f"No {name!r} key in COMPONENT_CLASSES") + styles = self._component_styles[name] + return styles + @property def _node_bases(self) -> Iterator[Type[DOMNode]]: """Get the DOMNode bases classes (including self.__class__) diff --git a/src/textual/widget.py b/src/textual/widget.py index e8d4fc825..a5159845f 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -938,15 +938,13 @@ class Widget(DOMNode): def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None: watch(self, attribute_name, callback) - def _render_styled(self) -> RenderableType: + def post_render(self, renderable: RenderableType) -> RenderableType: """Applies style attributes to the default renderable. Returns: RenderableType: A new renderable. """ - renderable = self.render() - if isinstance(renderable, str): renderable = Text.from_markup(renderable) @@ -1002,7 +1000,8 @@ class Widget(DOMNode): def _render_content(self) -> None: """Render all lines.""" width, height = self.size - renderable = self._render_styled() + renderable = self.render() + renderable = self.post_render(renderable) options = self.console.options.update_dimensions(width, height).update( highlight=False ) diff --git a/src/textual/widgets/_data_table.py b/src/textual/widgets/_data_table.py index ed5dde3f8..201a4228d 100644 --- a/src/textual/widgets/_data_table.py +++ b/src/textual/widgets/_data_table.py @@ -349,9 +349,9 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): Lines: A list of segments per line. """ if hover: - style += self.component_styles["datatable--highlight"].node.rich_style + style += self.get_component_styles("datatable--highlight").rich_style if cursor: - style += self.component_styles["datatable--cursor"].node.rich_style + style += self.get_component_styles("datatable--cursor").rich_style cell_key = (row_index, column_index, style, cursor, hover) if cell_key not in self._cell_render_cache: style += Style.from_meta({"row": row_index, "column": column_index}) @@ -394,7 +394,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): render_cell = self._render_cell if self.fixed_columns: - fixed_style = self.component_styles["datatable--fixed"].node.rich_style + fixed_style = self.get_component_styles("datatable--fixed").rich_style fixed_style += Style.from_meta({"fixed": True}) fixed_row = [ render_cell(row_index, column.index, fixed_style, column.width)[line_no] @@ -404,13 +404,13 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True): fixed_row = [] if row_index == -1: - row_style = self.component_styles["datatable--header"].node.rich_style + row_style = self.get_component_styles("datatable--header").rich_style else: if self.zebra_stripes: component_row_style = ( "datatable--odd-row" if row_index % 2 else "datatable--even-row" ) - row_style = self.component_styles[component_row_style].node.rich_style + row_style = self.get_component_styles(component_row_style).rich_style else: row_style = base_style diff --git a/src/textual/widgets/_footer.py b/src/textual/widgets/_footer.py index fba2efe1f..fe678a160 100644 --- a/src/textual/widgets/_footer.py +++ b/src/textual/widgets/_footer.py @@ -12,10 +12,41 @@ from ..widget import Widget @rich.repr.auto class Footer(Widget): + + CSS = """ + Footer { + background: $accent; + color: $text-accent; + dock: bottom; + height: 1; + } + Footer > .footer--highlight { + background: $accent-darken-1; + color: $text-accent-darken-1; + } + + Footer > .footer--highlight-key { + background: $secondary; + color: $text-secondary; + text-style: bold; + } + + Footer > .footer--key { + text-style: bold; + background: $accent-darken-2; + color: $text-accent-darken-2; + } + """ + + COMPONENT_CLASSES = { + "footer--description", + "footer--key", + "footer--highlight", + "footer--highlight-key", + } + def __init__(self) -> None: - self.keys: list[tuple[str, str]] = [] super().__init__() - self.layout_size = 1 self._key_text: Text | None = None highlight_key: Reactive[str | None] = Reactive(None) @@ -37,13 +68,19 @@ class Footer(Widget): def make_key_text(self) -> Text: """Create text containing all the keys.""" + base_style = self.rich_style text = Text( - style="white on dark_green", + style=self.rich_style, no_wrap=True, overflow="ellipsis", justify="left", end="", ) + highlight_style = self.get_component_styles("footer--highlight").rich_style + highlight_key_style = self.get_component_styles( + "footer--highlight-key" + ).rich_style + key_style = self.get_component_styles("footer--key").rich_style for binding in self.app.bindings.shown_keys: key_display = ( binding.key.upper() @@ -52,13 +89,20 @@ class Footer(Widget): ) hovered = self.highlight_key == binding.key key_text = Text.assemble( - (f" {key_display} ", "reverse" if hovered else "default on default"), - f" {binding.description} ", + (f" {key_display} ", highlight_key_style if hovered else key_style), + ( + f" {binding.description} ", + highlight_style if hovered else base_style, + ), meta={"@click": f"app.press('{binding.key}')", "key": binding.key}, ) text.append_text(key_text) + self.log(text) return text + def post_render(self, renderable): + return renderable + def render(self) -> RenderableType: if self._key_text is None: self._key_text = self.make_key_text() diff --git a/src/textual/widgets/_tree_control.py b/src/textual/widgets/_tree_control.py index 4f22ae3ce..86d2b0e9e 100644 --- a/src/textual/widgets/_tree_control.py +++ b/src/textual/widgets/_tree_control.py @@ -267,7 +267,7 @@ class TreeControl(Generic[NodeDataType], Widget, can_focus=True): return None def render(self) -> RenderableType: - self._tree.guide_style = self.component_styles["tree--guides"].node.rich_style + self._tree.guide_style = self._component_styles["tree--guides"].node.rich_style return self._tree def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType: