mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
fix for footer
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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):
|
||||
|
||||
13
sandbox/will/footer.py
Normal file
13
sandbox/will/footer.py
Normal 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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user