diff --git a/CHANGELOG.md b/CHANGELOG.md index 2471d5ed43..4d941360c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## Unreleased + +### Added + +- Added support for a detail column to the Tree widget https://github.com/Textualize/textual/pull/5096 + ## [0.83.0] - 2024-10-10 ### Added diff --git a/docs/examples/widgets/detail_tree.py b/docs/examples/widgets/detail_tree.py new file mode 100644 index 0000000000..ac94e525fb --- /dev/null +++ b/docs/examples/widgets/detail_tree.py @@ -0,0 +1,37 @@ +from rich.text import Text + +from textual.app import App, ComposeResult +from textual.widgets import Tree + + +class TreeApp(App): + def compose(self) -> ComposeResult: + tree: Tree[dict] = Tree("A bit of everything") + tree.root.expand() + fruit = tree.root.add("Fruit", expand=True) + fruit.add_leaf("Orange", detail="🍊") + fruit.add_leaf("Apple", detail="🍎") + fruit.add_leaf("Banana", detail=":banana:") + fruit.add_leaf("Pear", detail="🍐") + + # https://en.wikipedia.org/wiki/Demographics_of_the_United_Kingdom + pop = tree.root.add("Population", expand=True) + uk = pop.add("United Kingdom", expand=True, detail="67,081,234") + uk.add_leaf("England", detail="56,550,138") + uk.add_leaf("Scotland", detail="5,466,000") + uk.add_leaf("Wales", detail="3,169,586") + uk.add_leaf("Northern Ireland", detail="1,895,510") + + # https://en.wikipedia.org/wiki/List_of_countries_by_average_yearly_temperature + temps = tree.root.add("Average Temperatures", expand=True) + temps.add_leaf("Burkina Faso", detail=Text("30.40 °C", style="red")) + temps.add_leaf("New Zealand", detail="[red]10.46 °C[/red]") + temps.add_leaf("Canada", detail="[blue]-4.03 °C[/blue]") + temps.add_leaf("Greenland", detail=Text("-18.68 °C", style="blue")) + + yield tree + + +if __name__ == "__main__": + app = TreeApp() + app.run() diff --git a/docs/widgets/tree.md b/docs/widgets/tree.md index e1c4f33d1e..84ae827666 100644 --- a/docs/widgets/tree.md +++ b/docs/widgets/tree.md @@ -26,6 +26,26 @@ The example below creates a simple tree. Tree widgets have a "root" attribute which is an instance of a [TreeNode][textual.widgets.tree.TreeNode]. Call [add()][textual.widgets.tree.TreeNode.add] or [add_leaf()][textual.widgets.tree.TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child which you can use to add additional levels. +All nodes have a `detail` attribute that can be used to provide additional information about the node. This information will be shown right justified in the tree node. The following example demonstrates how to use the `detail` attribute. + +=== "Output" + + + ```{.textual path="docs/examples/widgets/detail_tree.py"} + + ``` + + +=== "detail_tree.py" + + + ```python + + --8<-- "docs/examples/widgets/detail_tree.py" + + ``` + + ## Reactive Attributes | Name | Type | Default | Description | diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index f73dd8515c..f86371c8bf 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -101,6 +101,7 @@ def __init__( *, expanded: bool = True, allow_expand: bool = True, + detail: str | Text | None = None, ) -> None: """Initialise the node. @@ -112,6 +113,7 @@ def __init__( data: Optional data to associate with the node. expanded: Should the node be attached in an expanded state? allow_expand: Should the node allow being expanded by the user? + detail: Optional detail text to display. """ self._tree = tree self._parent = parent @@ -128,6 +130,11 @@ def __init__( self._updates: int = 0 self._line: int = -1 + if detail is None: + self._detail = Text("") + else: + self._detail = tree.process_label(detail) + def __rich_repr__(self) -> rich.repr.Result: yield self._label.plain yield self.data @@ -356,6 +363,29 @@ def set_label(self, label: TextType) -> None: self._label = text_label self._tree.call_later(self._tree._refresh_node, self) + @property + def detail(self) -> Text: + """Detail text for the node.""" + return self._detail + + @detail.setter + def detail(self, detail: str | Text | None) -> None: + self.set_detail(detail) + + def set_detail(self, detail: str | Text | None) -> None: + """Set the detail text for the node. + + Args: + detail: A string or Text object with the detail text. + """ + if detail is None: + detail = Text("") + + self._updates += 1 + text_detail = self._tree.process_label(detail) + self._detail = text_detail + self._tree.call_later(self._tree._refresh_node, self) + def add( self, label: TextType, @@ -365,6 +395,7 @@ def add( after: int | TreeNode[TreeDataType] | None = None, expand: bool = False, allow_expand: bool = True, + detail: str | Text | None = None, ) -> TreeNode[TreeDataType]: """Add a node to the sub-tree. @@ -424,7 +455,7 @@ def add( ) text_label = self._tree.process_label(label) - node = self._tree._add_node(self, text_label, data) + node = self._tree._add_node(self, text_label, data, detail=detail) node._expanded = expand node._allow_expand = allow_expand self._updates += 1 @@ -440,6 +471,7 @@ def add_leaf( *, before: int | TreeNode[TreeDataType] | None = None, after: int | TreeNode[TreeDataType] | None = None, + detail: str | Text | None = None, ) -> TreeNode[TreeDataType]: """Add a 'leaf' node (a node that can not expand). @@ -448,6 +480,7 @@ def add_leaf( data: Optional data. before: Optional index or `TreeNode` to add the node before. after: Optional index or `TreeNode` to add the node after. + detail: Optional detail text. Returns: New node. @@ -466,6 +499,7 @@ def add_leaf( after=after, expand=False, allow_expand=False, + detail=detail, ) return node @@ -763,6 +797,7 @@ def __init__( id: str | None = None, classes: str | None = None, disabled: bool = False, + detail: str | Text | None = None, ) -> None: """Initialise a Tree. @@ -777,10 +812,14 @@ def __init__( text_label = self.process_label(label) + text_detail = Text("") + if detail is not None: + text_detail = self.process_label(detail) + self._updates = 0 self._tree_nodes: dict[NodeID, TreeNode[TreeDataType]] = {} self._current_id = 0 - self.root = self._add_node(None, text_label, data) + self.root = self._add_node(None, text_label, data, detail=text_detail) """The root node of the tree.""" self._line_cache: LRUCache[LineCacheKey, Strip] = LRUCache(1024) self._tree_lines_cached: list[_TreeLine[TreeDataType]] | None = None @@ -822,8 +861,11 @@ def _add_node( label: Text, data: TreeDataType | None, expand: bool = False, + detail: str | Text | None = None, ) -> TreeNode[TreeDataType]: - node = TreeNode(self, parent, self._new_id(), label, data, expanded=expand) + node = TreeNode( + self, parent, self._new_id(), label, data, expanded=expand, detail=detail + ) self._tree_nodes[node._id] = node self._updates += 1 return node @@ -853,6 +895,31 @@ def render_label( prefix = ("", base_style) text = Text.assemble(prefix, node_label) + + if node._detail.cell_len > 0: + node_detail = node._detail.copy() + node_detail.stylize(style) + + total_width = self.size.width + line = self._tree_lines[node.line] + + guide_width = line._get_guide_width(self.guide_depth, self.show_root) + + right_margin = 1 + if self.show_vertical_scrollbar: + right_margin += 2 + + space_width = ( + total_width + - text.cell_len + - node_detail.cell_len + - guide_width + - right_margin + ) + space_width = max(1, space_width) + space_text = Text(" " * space_width) + text = Text.assemble(text, space_text, node_detail) + return text def get_label_width(self, node: TreeNode[TreeDataType]) -> int: diff --git a/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_with_detail_example.svg b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_with_detail_example.svg new file mode 100644 index 0000000000..71df1822b4 --- /dev/null +++ b/tests/snapshot_tests/__snapshots__/test_snapshots/test_tree_with_detail_example.svg @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TreeApp + + + + + + + + + + ▼ A bit of everything +┣━━ ▼ Fruit +┃   ┣━━ Orange                                                             🍊 +┃   ┣━━ Apple                                                              🍎 +┃   ┣━━ Banana                                                             🍌 +┃   ┗━━ Pear                                                               🍐 +┣━━ ▼ Population +┃   ┗━━ ▼ United Kingdom                                           67,081,234 +┃       ┣━━ England                                                56,550,138 +┃       ┣━━ Scotland                                                5,466,000 +┃       ┣━━ Wales                                                   3,169,586 +┃       ┗━━ Northern Ireland                                        1,895,510 +┗━━ ▼ Average Temperatures +    ┣━━ Burkina Faso                                                 30.40 °C +    ┣━━ New Zealand                                                  10.46 °C +    ┣━━ Canada                                                       -4.03 °C +    ┗━━ Greenland                                                   -18.68 °C + + + + + + + + + + diff --git a/tests/snapshot_tests/test_snapshots.py b/tests/snapshot_tests/test_snapshots.py index e35b3e9393..7b26f11453 100644 --- a/tests/snapshot_tests/test_snapshots.py +++ b/tests/snapshot_tests/test_snapshots.py @@ -295,6 +295,10 @@ def test_tree_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "tree.py") +def test_tree_with_detail_example(snap_compare): + assert snap_compare(WIDGET_EXAMPLES_DIR / "detail_tree.py") + + def test_markdown_example(snap_compare): assert snap_compare(WIDGET_EXAMPLES_DIR / "markdown.py")