fix for footer

This commit is contained in:
Will McGugan
2022-08-11 16:45:31 +01:00
parent e8d0d7f3ef
commit bd82ece9cf
10 changed files with 117 additions and 36 deletions

View File

@@ -47,7 +47,7 @@ DataTable {
/* opacity: 50%; */ /* opacity: 50%; */
padding: 1; padding: 1;
margin: 1 2; margin: 1 2;
height: 12; height: 24;
} }
#sidebar { #sidebar {
@@ -55,6 +55,7 @@ DataTable {
background: $panel; background: $panel;
dock: left; dock: left;
width: 30; width: 30;
margin-bottom: 1;
offset-x: -100%; offset-x: -100%;
transition: offset 500ms in_out_cubic; transition: offset 500ms in_out_cubic;

View File

@@ -6,7 +6,7 @@ from rich.text import Text
from textual.app import App, ComposeResult from textual.app import App, ComposeResult
from textual.reactive import Reactive from textual.reactive import Reactive
from textual.widget import Widget 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 from textual.layout import Vertical
CODE = ''' CODE = '''
@@ -109,7 +109,8 @@ class BasicApp(App, css_path="basic.css"):
def on_load(self): def on_load(self):
"""Bind keys here.""" """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: def compose(self) -> ComposeResult:
table = DataTable() 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"), 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 Footer()
yield Widget(
Widget(classes="title"),
Widget(classes="user"),
OptionItem(),
OptionItem(),
OptionItem(),
Widget(classes="content"),
id="sidebar",
)
table.add_column("Foo", width=20) table.add_column("Foo", width=20)
table.add_column("Bar", 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: async def on_key(self, event) -> None:
await self.dispatch_key(event) await self.dispatch_key(event)
def key_d(self): def action_toggle_dark(self):
self.dark = not self.dark self.dark = not self.dark
async def key_q(self): async def key_q(self):

13
sandbox/will/footer.py Normal file
View File

@@ -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()

View File

@@ -12,7 +12,7 @@ from rich.style import Style
from .._animator import Animation, EasingFunction from .._animator import Animation, EasingFunction
from ..color import Color from ..color import Color
from ..geometry import Offset, Size, Spacing from ..geometry import Offset, Spacing
from ._style_properties import ( from ._style_properties import (
AlignProperty, AlignProperty,
BorderProperty, BorderProperty,
@@ -223,7 +223,7 @@ class StylesBase(ABC):
layers = NameListProperty() layers = NameListProperty()
transitions = TransitionsProperty() transitions = TransitionsProperty()
rich_style = StyleProperty() # rich_style = StyleProperty()
tint = ColorProperty("transparent") tint = ColorProperty("transparent")
scrollbar_color = ColorProperty("ansi_bright_magenta") scrollbar_color = ColorProperty("ansi_bright_magenta")
@@ -800,6 +800,12 @@ class RenderStyles(StylesBase):
"""Quick access to the inline styles.""" """Quick access to the inline styles."""
return self._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: def __rich_repr__(self) -> rich.repr.Result:
for rule_name in RULE_NAMES: for rule_name in RULE_NAMES:
if self.has_rule(rule_name): if self.has_rule(rule_name):

View File

@@ -23,7 +23,7 @@ from .parse import parse
from .styles import RulesMap, Styles from .styles import RulesMap, Styles
from .tokenize import tokenize_values, Token from .tokenize import tokenize_values, Token
from .tokenizer import TokenError from .tokenizer import TokenError
from .types import Specificity3, Specificity5 from .types import Specificity3, Specificity6
from ..dom import DOMNode from ..dom import DOMNode
from .. import messages from .. import messages
@@ -325,7 +325,7 @@ class Stylesheet:
# We can use this to determine, for a given rule, whether we should apply it # 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 # 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. # 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) rule_attributes = defaultdict(list)
_check_rule = self._check_rule _check_rule = self._check_rule
@@ -352,12 +352,12 @@ class Stylesheet:
self.replace_rules(node, node_rules, animate=animate) self.replace_rules(node, node_rules, animate=animate)
node.component_styles.clear() node._component_styles.clear()
for component in node.COMPONENT_CLASSES: for component in node.COMPONENT_CLASSES:
virtual_node = DOMNode(classes=component) virtual_node = DOMNode(classes=component)
virtual_node.set_parent(node) virtual_node.set_parent(node)
self.apply(virtual_node, animate=False) self.apply(virtual_node, animate=False)
node.component_styles[component] = virtual_node.styles node._component_styles[component] = virtual_node.styles
@classmethod @classmethod
def replace_rules( def replace_rules(

View File

@@ -68,7 +68,7 @@ class DOMNode(MessagePump):
self._inline_styles: Styles = Styles(self) self._inline_styles: Styles = Styles(self)
self.styles = RenderStyles(self, self._css_styles, self._inline_styles) self.styles = RenderStyles(self, self._css_styles, self._inline_styles)
# A mapping of class names to Styles set in COMPONENT_CLASSES # 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__() super().__init__()
@@ -80,6 +80,23 @@ class DOMNode(MessagePump):
css_type_names.add(base.__name__.lower()) css_type_names.add(base.__name__.lower())
cls._css_type_names = frozenset(css_type_names) 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 @property
def _node_bases(self) -> Iterator[Type[DOMNode]]: def _node_bases(self) -> Iterator[Type[DOMNode]]:
"""Get the DOMNode bases classes (including self.__class__) """Get the DOMNode bases classes (including self.__class__)

View File

@@ -938,15 +938,13 @@ class Widget(DOMNode):
def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None: def watch(self, attribute_name, callback: Callable[[Any], Awaitable[None]]) -> None:
watch(self, attribute_name, callback) watch(self, attribute_name, callback)
def _render_styled(self) -> RenderableType: def post_render(self, renderable: RenderableType) -> RenderableType:
"""Applies style attributes to the default renderable. """Applies style attributes to the default renderable.
Returns: Returns:
RenderableType: A new renderable. RenderableType: A new renderable.
""" """
renderable = self.render()
if isinstance(renderable, str): if isinstance(renderable, str):
renderable = Text.from_markup(renderable) renderable = Text.from_markup(renderable)
@@ -1002,7 +1000,8 @@ class Widget(DOMNode):
def _render_content(self) -> None: def _render_content(self) -> None:
"""Render all lines.""" """Render all lines."""
width, height = self.size 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( options = self.console.options.update_dimensions(width, height).update(
highlight=False highlight=False
) )

View File

@@ -349,9 +349,9 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
Lines: A list of segments per line. Lines: A list of segments per line.
""" """
if hover: if hover:
style += self.component_styles["datatable--highlight"].node.rich_style style += self.get_component_styles("datatable--highlight").rich_style
if cursor: 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) cell_key = (row_index, column_index, style, cursor, hover)
if cell_key not in self._cell_render_cache: if cell_key not in self._cell_render_cache:
style += Style.from_meta({"row": row_index, "column": column_index}) 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 render_cell = self._render_cell
if self.fixed_columns: 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_style += Style.from_meta({"fixed": True})
fixed_row = [ fixed_row = [
render_cell(row_index, column.index, fixed_style, column.width)[line_no] 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 = [] fixed_row = []
if row_index == -1: 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: else:
if self.zebra_stripes: if self.zebra_stripes:
component_row_style = ( component_row_style = (
"datatable--odd-row" if row_index % 2 else "datatable--even-row" "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: else:
row_style = base_style row_style = base_style

View File

@@ -12,10 +12,41 @@ from ..widget import Widget
@rich.repr.auto @rich.repr.auto
class Footer(Widget): 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: def __init__(self) -> None:
self.keys: list[tuple[str, str]] = []
super().__init__() super().__init__()
self.layout_size = 1
self._key_text: Text | None = None self._key_text: Text | None = None
highlight_key: Reactive[str | None] = Reactive(None) highlight_key: Reactive[str | None] = Reactive(None)
@@ -37,13 +68,19 @@ class Footer(Widget):
def make_key_text(self) -> Text: def make_key_text(self) -> Text:
"""Create text containing all the keys.""" """Create text containing all the keys."""
base_style = self.rich_style
text = Text( text = Text(
style="white on dark_green", style=self.rich_style,
no_wrap=True, no_wrap=True,
overflow="ellipsis", overflow="ellipsis",
justify="left", justify="left",
end="", 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: for binding in self.app.bindings.shown_keys:
key_display = ( key_display = (
binding.key.upper() binding.key.upper()
@@ -52,13 +89,20 @@ class Footer(Widget):
) )
hovered = self.highlight_key == binding.key hovered = self.highlight_key == binding.key
key_text = Text.assemble( key_text = Text.assemble(
(f" {key_display} ", "reverse" if hovered else "default on default"), (f" {key_display} ", highlight_key_style if hovered else key_style),
f" {binding.description} ", (
f" {binding.description} ",
highlight_style if hovered else base_style,
),
meta={"@click": f"app.press('{binding.key}')", "key": binding.key}, meta={"@click": f"app.press('{binding.key}')", "key": binding.key},
) )
text.append_text(key_text) text.append_text(key_text)
self.log(text)
return text return text
def post_render(self, renderable):
return renderable
def render(self) -> RenderableType: def render(self) -> RenderableType:
if self._key_text is None: if self._key_text is None:
self._key_text = self.make_key_text() self._key_text = self.make_key_text()

View File

@@ -267,7 +267,7 @@ class TreeControl(Generic[NodeDataType], Widget, can_focus=True):
return None return None
def render(self) -> RenderableType: 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 return self._tree
def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType: def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType: