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%; */
|
/* 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;
|
||||||
|
|||||||
@@ -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
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 .._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):
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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__)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user