diff --git a/sandbox/will/basic.css b/sandbox/will/basic.css index fef9e4248..829bc3db1 100644 --- a/sandbox/will/basic.css +++ b/sandbox/will/basic.css @@ -24,6 +24,23 @@ App > Screen { } +#tree-container { + overflow-y: auto; + height: 20; + margin: 1 2; + background: $panel; + padding: 1 2; +} + +DirectoryTree { + padding: 0 1; + height: auto; + +} + + + + DataTable { /*border:heavy red;*/ /* tint: 10% green; */ diff --git a/sandbox/will/basic.py b/sandbox/will/basic.py index bda44f314..db0d41b54 100644 --- a/sandbox/will/basic.py +++ b/sandbox/will/basic.py @@ -6,7 +6,8 @@ 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 +from textual.widgets import Static, DataTable, DirectoryTree +from textual.layout import Vertical CODE = ''' from __future__ import annotations @@ -130,6 +131,7 @@ class BasicApp(App, css_path="basic.css"): classes="scrollable", ), table, + Widget(DirectoryTree("~/projects/textual"), id="tree-container"), Error(), Tweet(TweetBody(), classes="scrollbar-size-custom"), Warning(), diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index 0cb60243a..9c596c034 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -13,7 +13,7 @@ from .. import events from ..message import Message from ..reactive import Reactive from .._types import MessageTarget -from . import TreeControl, TreeClick, TreeNode, NodeID +from ._tree_control import TreeControl, TreeClick, TreeNode, NodeID @dataclass @@ -30,11 +30,18 @@ class FileClick(Message, bubble=True): class DirectoryTree(TreeControl[DirEntry]): - def __init__(self, path: str, name: str = None) -> None: - self.path = path.rstrip("/") + def __init__( + self, + path: str, + *, + name: str | None = None, + id: str | None = None, + classes: str | None = None, + ) -> None: + self.path = os.path.expanduser(path.rstrip("/")) label = os.path.basename(self.path) - data = DirEntry(path, True) - super().__init__(label, name=name, data=data) + data = DirEntry(self.path, True) + super().__init__(label, data, name=name, id=id, classes=classes) self.root.tree.guide_style = "black" has_focus: Reactive[bool] = Reactive(False) @@ -50,7 +57,7 @@ class DirectoryTree(TreeControl[DirEntry]): node.tree.guide_style = ( "bold not dim red" if node.id == hover_node else "black" ) - self.refresh(layout=True) + self.refresh() def render_node(self, node: TreeNode[DirEntry]) -> RenderableType: return self.render_tree_label( @@ -81,12 +88,12 @@ class DirectoryTree(TreeControl[DirEntry]): if is_hover: label.stylize("underline") if is_dir: - label.stylize("bold magenta") + label.stylize("bold") icon = "📂" if expanded else "📁" else: - label.stylize("bright_green") + icon = "📄" - label.highlight_regex(r"\..*$", "green") + label.highlight_regex(r"\..*$", "italic") if label.plain.startswith("."): label.stylize("dim") @@ -112,7 +119,7 @@ class DirectoryTree(TreeControl[DirEntry]): await node.expand() self.refresh(layout=True) - async def handle_tree_click(self, message: TreeClick[DirEntry]) -> None: + async def on_tree_click(self, message: TreeClick[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 ca21022c2..4f22ae3ce 100644 --- a/src/textual/widgets/_tree_control.py +++ b/src/textual/widgets/_tree_control.py @@ -1,15 +1,14 @@ from __future__ import annotations -from typing import Generic, Iterator, NewType, TypeVar +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 -from rich.padding import PaddingDimensions +from .. import events from ..reactive import Reactive from .._types import MessageTarget from ..widget import Widget @@ -169,33 +168,52 @@ class TreeClick(Generic[NodeDataType], Message, bubble=True): yield "node", self.node -class TreeControl(Generic[NodeDataType], Widget): +class TreeControl(Generic[NodeDataType], Widget, can_focus=True): + CSS = """ + TreeControl { + background: $panel; + color: $text-panel; + height: auto; + width: 100%; + } + + TreeControl > .tree--guides { + color: $secondary; + } + + """ + + COMPONENT_CLASSES: ClassVar[set[str]] = { + "tree--guides", + "tree--labels", + } + def __init__( self, label: TextType, data: NodeDataType, *, name: str | None = None, - padding: PaddingDimensions = (1, 1), + id: str | None = None, + classes: str | None = None, ) -> None: self.data = data - self.id = NodeID(0) + self.node_id = NodeID(0) self.nodes: dict[NodeID, TreeNode[NodeDataType]] = {} self._tree = Tree(label) self.root: TreeNode[NodeDataType] = TreeNode( - None, self.id, self, self._tree, label, data + None, self.node_id, self, self._tree, label, data ) self._tree.label = self.root - self.nodes[NodeID(self.id)] = self.root - super().__init__(name=name) - self.padding = padding + self.nodes[NodeID(self.node_id)] = self.root + super().__init__(name=name, id=id, classes=classes) hover_node: Reactive[NodeID | None] = Reactive(None) - cursor: Reactive[NodeID] = Reactive(NodeID(0), layout=True) - cursor_line: Reactive[int] = Reactive(0, repaint=False) - show_cursor: Reactive[bool] = Reactive(False, layout=True) + cursor: Reactive[NodeID] = Reactive(NodeID(0)) + cursor_line: Reactive[int] = Reactive(0) + show_cursor: Reactive[bool] = Reactive(False) def watch_show_cursor(self, value: bool) -> None: self.emit_no_wait(CursorMove(self, self.cursor_line)) @@ -211,14 +229,14 @@ class TreeControl(Generic[NodeDataType], Widget): data: NodeDataType, ) -> None: parent = self.nodes[node_id] - self.id = NodeID(self.id + 1) + self.node_id = NodeID(self.node_id + 1) child_tree = parent._tree.add(label) child_node: TreeNode[NodeDataType] = TreeNode( - parent, self.id, self, child_tree, label, data + parent, self.node_id, self, child_tree, label, data ) parent.children.append(child_node) child_tree.label = child_node - self.nodes[self.id] = child_node + self.nodes[self.node_id] = child_node self.refresh(layout=True) @@ -249,6 +267,7 @@ class TreeControl(Generic[NodeDataType], Widget): return None def render(self) -> RenderableType: + self._tree.guide_style = self.component_styles["tree--guides"].node.rich_style return self._tree def render_node(self, node: TreeNode[NodeDataType]) -> RenderableType: @@ -307,24 +326,3 @@ class TreeControl(Generic[NodeDataType], Widget): if previous_node is not None: self.cursor_line -= 1 self.cursor = previous_node.id - - -if __name__ == "__main__": - - from textual import events - from textual.app import App - - class TreeApp(App): - async def on_mount(self, event: events.Mount) -> None: - await self.screen.dock(TreeControl("Tree Root", data="foo")) - - async def handle_tree_click(self, message: TreeClick) -> None: - if message.node.empty: - await message.node.add("foo") - await message.node.add("bar") - await message.node.add("baz") - await message.node.expand() - else: - await message.node.toggle() - - TreeApp(log_path="textual.log").run()