diff --git a/sandbox/will/basic.css b/sandbox/will/basic.css index c57b43f9c..7c2ad4c39 100644 --- a/sandbox/will/basic.css +++ b/sandbox/will/basic.css @@ -30,7 +30,7 @@ App > Screen { overflow-y: auto; height: 20; margin: 1 2; - background: $panel; + background: $surface; padding: 1 2; } diff --git a/src/textual/app.py b/src/textual/app.py index 51ba50633..3b376fb9e 100644 --- a/src/textual/app.py +++ b/src/textual/app.py @@ -899,11 +899,12 @@ class App(Generic[ReturnType], DOMNode): self.log.system(f"{self.screen} is active") return previous_screen - def set_focus(self, widget: Widget | None) -> None: + def set_focus(self, widget: Widget | None, scroll_visible: bool = False) -> None: """Focus (or unfocus) a widget. A focused widget will receive key events first. Args: - widget (Widget): [description] + widget (Widget): Widget to focus. + scroll_visible (bool, optional): Scroll widget in to view. """ if widget == self.focused: # Widget is already focused @@ -924,7 +925,8 @@ class App(Generic[ReturnType], DOMNode): # Change focus self.focused = widget # Send focus event - self.screen.scroll_to_widget(widget) + if scroll_visible: + self.screen.scroll_to_widget(widget) widget.post_message_no_wait(events.Focus(self)) widget.emit_no_wait(events.DescendantFocus(self)) diff --git a/src/textual/widget.py b/src/textual/widget.py index 693bdc6a3..65b847d23 100644 --- a/src/textual/widget.py +++ b/src/textual/widget.py @@ -4,7 +4,6 @@ from asyncio import Lock from fractions import Fraction from itertools import islice from operator import attrgetter -from types import GeneratorType from typing import TYPE_CHECKING, ClassVar, Collection, Iterable, NamedTuple import rich.repr diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 71d235dfb..d718888de 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -11,9 +11,8 @@ from rich.text import Text from .. import events from ..message import Message -from ..reactive import Reactive from .._types import MessageTarget -from ._tree_control import TreeControl, TreeClick, TreeNode, NodeID +from ._tree_control import TreeControl, TreeNode @dataclass @@ -91,6 +90,9 @@ class DirectoryTree(TreeControl[DirEntry]): icon_label.apply_meta(meta) return icon_label + def on_styles_updated(self) -> None: + self.render_tree_label.cache_clear() + def on_mount(self) -> None: self.call_later(self.load_directory, self.root) @@ -105,7 +107,9 @@ class DirectoryTree(TreeControl[DirEntry]): node.expand() self.refresh(layout=True) - async def on_tree_control_node_selected(self, message: TreeClick[DirEntry]) -> None: + async def on_tree_control_node_selected( + self, message: TreeControl.NodeSelected[DirEntry] + ) -> None: dir_entry = message.node.data if not dir_entry.is_dir: await self.emit(FileClick(self, dir_entry.path)) diff --git a/src/textual/widgets/_tree_control.py b/src/textual/widgets/_tree_control.py index dcc0363b9..53624991e 100644 --- a/src/textual/widgets/_tree_control.py +++ b/src/textual/widgets/_tree_control.py @@ -5,6 +5,7 @@ from typing import ClassVar, Generic, Iterator, NewType, TypeVar import rich.repr from rich.console import RenderableType +from rich.style import Style from rich.text import Text, TextType from rich.tree import Tree @@ -21,6 +22,7 @@ NodeID = NewType("NodeID", int) NodeDataType = TypeVar("NodeDataType") +EventNodeDataType = TypeVar("EventNodeDataType") @rich.repr.auto @@ -159,21 +161,11 @@ class TreeNode(Generic[NodeDataType]): return self._control.render_node(self) -@rich.repr.auto -class TreeClick(Generic[NodeDataType], Message, bubble=True): - def __init__(self, sender: MessageTarget, node: TreeNode[NodeDataType]) -> None: - self.node = node - super().__init__(sender) - - def __rich_repr__(self) -> rich.repr.Result: - yield "node", self.node - - class TreeControl(Generic[NodeDataType], Widget, can_focus=True): DEFAULT_CSS = """ TreeControl { - background: $panel; - color: $text-panel; + background: $surface; + color: $text-surface; height: auto; width: 100%; } @@ -183,7 +175,13 @@ class TreeControl(Generic[NodeDataType], Widget, can_focus=True): } TreeControl > .tree--guides-highlight { - color: $secondary; + color: $success; + text-style: uu; + + } + + TreeControl > .tree--guides-cursor { + color: $secondary; text-style: bold; } @@ -201,12 +199,15 @@ class TreeControl(Generic[NodeDataType], Widget, can_focus=True): COMPONENT_CLASSES: ClassVar[set[str]] = { "tree--guides", "tree--guides-highlight", + "tree--guides-cursor", "tree--labels", "tree--cursor", } - class NodeSelected(Message, bubble=False): - def __init__(self, sender: MessageTarget, node: TreeNode[NodeDataType]) -> None: + class NodeSelected(Generic[EventNodeDataType], Message, bubble=False): + def __init__( + self, sender: MessageTarget, node: TreeNode[EventNodeDataType] + ) -> None: self.node = node super().__init__(sender) @@ -238,33 +239,10 @@ class TreeControl(Generic[NodeDataType], Widget, can_focus=True): cursor_line: Reactive[int] = Reactive(0) show_cursor: Reactive[bool] = Reactive(False) - def watch_show_cursor(self, value: bool) -> None: - line_region = Region(0, self.cursor_line, self.size.width, 1) - self.emit_no_wait(messages.ScrollToRegion(self, line_region)) - def watch_cursor_line(self, value: int) -> None: line_region = Region(0, value, self.size.width, 1) self.emit_no_wait(messages.ScrollToRegion(self, line_region)) - def watch_hover_node(self, previous_hover_node: NodeID, hover_node: NodeID) -> None: - previous_hover = self.nodes.get(previous_hover_node) - if previous_hover is not None: - previous_hover._tree.guide_style = self._guide_style - hover = self.nodes.get(hover_node) - if hover is not None: - hover._tree.guide_style = self._highlight_guide_style - self.refresh() - - def watch_cursor(self, previous_cursor_node: NodeID, cursor_node: NodeID) -> None: - - previous_cursor = self.nodes.get(previous_cursor_node) - if previous_cursor is not None: - previous_cursor._tree.guide_style = self._guide_style - cursor = self.nodes.get(cursor_node) - if cursor is not None: - cursor._tree.guide_style = self._highlight_guide_style - self.refresh() - def get_content_height(self, container: Size, viewport: Size, width: int) -> int: def get_size(tree: Tree) -> int: return 1 + sum( @@ -321,6 +299,23 @@ class TreeControl(Generic[NodeDataType], Widget, can_focus=True): return None def render(self) -> RenderableType: + guide_style = self._guide_style + + def update_guide_style(tree: Tree) -> None: + tree.guide_style = guide_style + for child in tree.children: + if child.expanded: + update_guide_style(child) + + update_guide_style(self._tree) + if self.hover_node is not None: + hover = self.nodes.get(self.hover_node) + if hover is not None: + hover._tree.guide_style = self._highlight_guide_style + if self.cursor is not None and self.show_cursor: + cursor = self.nodes.get(self.cursor) + if cursor is not None: + cursor._tree.guide_style = self._cursor_guide_style return self._tree def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType: @@ -343,12 +338,33 @@ class TreeControl(Generic[NodeDataType], Widget, can_focus=True): self.post_message_no_wait(self.NodeSelected(self, node)) def on_mount(self) -> None: - self._guide_style = self.get_component_styles("tree--guides").rich_style - self._highlight_guide_style = self.get_component_styles( - "tree--guides-highlight" - ).rich_style self._tree.guide_style = self._guide_style + @property + def _guide_style(self) -> Style: + return self.get_component_styles("tree--guides").rich_style + + @property + def _highlight_guide_style(self) -> Style: + return self.get_component_styles("tree--guides-highlight").rich_style + + @property + def _cursor_guide_style(self) -> Style: + return self.get_component_styles("tree--guides-cursor").rich_style + + def on_styles_updated(self) -> None: + guide_style = self._guide_style + + def update_guide_style(tree: Tree) -> None: + tree.guide_style = guide_style + for child in tree.children: + if child.expanded: + update_guide_style(child) + + update_guide_style(self._tree) + self._render_cache = None + self.refresh() + def on_mouse_move(self, event: events.MouseMove) -> None: self.hover_node = event.style.meta.get("tree_node")