mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
new tree control
This commit is contained in:
1
docs/api/directory_tree.md
Normal file
1
docs/api/directory_tree.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.DirectoryTree
|
||||
1
docs/api/tree.md
Normal file
1
docs/api/tree.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.Tree
|
||||
1
docs/api/tree_node.md
Normal file
1
docs/api/tree_node.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.TreeNode
|
||||
12
docs/examples/widgets/directory_tree.py
Normal file
12
docs/examples/widgets/directory_tree.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import DirectoryTree
|
||||
|
||||
|
||||
class DirectoryTreeApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
yield DirectoryTree("./")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = DirectoryTreeApp()
|
||||
app.run()
|
||||
18
docs/examples/widgets/tree.py
Normal file
18
docs/examples/widgets/tree.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Tree
|
||||
|
||||
|
||||
class TreeApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
tree: Tree = Tree("Dune")
|
||||
tree.root.expand()
|
||||
characters = tree.root.add("Characters", expand=True)
|
||||
characters.add_leaf("Paul")
|
||||
characters.add_leaf("Jessica")
|
||||
characters.add_leaf("Channi")
|
||||
yield tree
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TreeApp()
|
||||
app.run()
|
||||
36
docs/widgets/directory_tree.md
Normal file
36
docs/widgets/directory_tree.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# DirectoryTree
|
||||
|
||||
A tree control to navigate the contents of your filesystem.
|
||||
|
||||
- [x] Focusable
|
||||
- [ ] Container
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
The example below creates a simple tree to navigate the current working directory.
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/widgets/directory_tree.py"
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Default handler | Description |
|
||||
| ------------------- | --------------------------------- | --------------------------------------- |
|
||||
| `Tree.FileSelected` | `on_directory_tree_file_selected` | Sent when the user selects a file node. |
|
||||
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------- | ------ | ------- | ----------------------------------------------- |
|
||||
| `show_root` | `bool` | `True` | Show the root node. |
|
||||
| `show_guides` | `bool` | `True` | Show guide lines between levels. |
|
||||
| `guide_depth` | `int` | `4` | Amount of indentation between parent and child. |
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
* [Tree][textual.widgets.DirectoryTree] code reference
|
||||
* [Tree][textual.widgets.Tree] code reference
|
||||
46
docs/widgets/tree.md
Normal file
46
docs/widgets/tree.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Tree
|
||||
|
||||
A tree control widget.
|
||||
|
||||
- [x] Focusable
|
||||
- [ ] Container
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
The example below creates a simple tree.
|
||||
|
||||
=== "Output"
|
||||
|
||||
```{.textual path="docs/examples/widgets/tree.py"}
|
||||
```
|
||||
|
||||
=== "tree.py"
|
||||
|
||||
```python
|
||||
--8<-- "docs/examples/widgets/tree.py"
|
||||
```
|
||||
|
||||
A each tree widget has a "root" attribute which is an instance of a [TreeNode][textual.widgets.TreeNode]. Call [add()][textual.widgets.TreeNode.add] or [add_leaf()][textual.widgets.TreeNode.add_leaf] to add new nodes underneath the root. Both these methods return a TreeNode for the child, so you can add more levels.
|
||||
|
||||
## Events
|
||||
|
||||
| Event | Default handler | Description |
|
||||
| -------------------- | ------------------------ | ------------------------------------------------ |
|
||||
| `Tree.NodeSelected` | `on_tree_node_selected` | Sent when the user selects a tree node. |
|
||||
| `Tree.NodeExpanded` | `on_tree_node_expanded` | Sent when the user expands a node in the tree. |
|
||||
| `Tree.NodeCollapsed` | `on_tree_node_collapsed` | Sent when the user collapsed a node in the tree. |
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ------------- | ------ | ------- | ----------------------------------------------- |
|
||||
| `show_root` | `bool` | `True` | Show the root node. |
|
||||
| `show_guides` | `bool` | `True` | Show guide lines between levels. |
|
||||
| `guide_depth` | `int` | `4` | Amount of indentation between parent and child. |
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
* [Tree][textual.widgets.Tree] code reference
|
||||
* [TreeNode][textual.widgets.TreeNode] code reference
|
||||
@@ -1 +0,0 @@
|
||||
# TreeControl
|
||||
79
examples/json_tree.py
Normal file
79
examples/json_tree.py
Normal file
@@ -0,0 +1,79 @@
|
||||
import json
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Header, Footer, Tree, TreeNode
|
||||
|
||||
|
||||
class TreeApp(App):
|
||||
|
||||
BINDINGS = [
|
||||
("a", "add", "Add node"),
|
||||
("c", "clear", "Clear"),
|
||||
("t", "toggle_root", "Toggle root"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Footer()
|
||||
yield Tree("Root")
|
||||
|
||||
@classmethod
|
||||
def add_json(cls, node: TreeNode, json_data: object) -> None:
|
||||
"""Adds JSON data to a node.
|
||||
|
||||
Args:
|
||||
node (TreeNode): A Tree node.
|
||||
json_data (object): An object decoded from JSON.
|
||||
"""
|
||||
|
||||
from rich.highlighter import ReprHighlighter
|
||||
|
||||
highlighter = ReprHighlighter()
|
||||
|
||||
def add_node(name: str, node: TreeNode, data: object) -> None:
|
||||
if isinstance(data, dict):
|
||||
node._label = Text(f"{{}} {name}")
|
||||
for key, value in data.items():
|
||||
new_node = node.add("")
|
||||
add_node(key, new_node, value)
|
||||
elif isinstance(data, list):
|
||||
node._label = Text(f"[] {name}")
|
||||
for index, value in enumerate(data):
|
||||
new_node = node.add("")
|
||||
add_node(str(index), new_node, value)
|
||||
else:
|
||||
node._allow_expand = False
|
||||
if name:
|
||||
label = Text.assemble(
|
||||
Text.from_markup(f"[b]{name}[/b]="), highlighter(repr(data))
|
||||
)
|
||||
else:
|
||||
label = Text(repr(data))
|
||||
node._label = label
|
||||
|
||||
add_node("JSON", node, json_data)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
with open("food.json") as data_file:
|
||||
self.json_data = json.load(data_file)
|
||||
|
||||
def action_add(self) -> None:
|
||||
tree = self.query_one(Tree)
|
||||
json_node = tree.root.add("JSON")
|
||||
self.add_json(json_node, self.json_data)
|
||||
tree.root.expand()
|
||||
|
||||
def action_clear(self) -> None:
|
||||
tree = self.query_one(Tree)
|
||||
tree.clear()
|
||||
|
||||
def action_toggle_root(self) -> None:
|
||||
tree = self.query_one(Tree)
|
||||
tree.show_root = not tree.show_root
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TreeApp()
|
||||
app.run()
|
||||
@@ -1,46 +0,0 @@
|
||||
import json
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import Header, Footer, Tree, DirectoryTree
|
||||
|
||||
|
||||
with open("food.json") as data_file:
|
||||
data = json.load(data_file)
|
||||
|
||||
from rich import print
|
||||
|
||||
print(data)
|
||||
|
||||
|
||||
class TreeApp(App):
|
||||
|
||||
BINDINGS = [
|
||||
("a", "add", "Add node"),
|
||||
("c", "clear", "Clear"),
|
||||
("t", "toggle_root", "Toggle root"),
|
||||
]
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Header()
|
||||
yield Footer()
|
||||
yield DirectoryTree("../")
|
||||
|
||||
def action_add(self) -> None:
|
||||
tree = self.query_one(Tree)
|
||||
|
||||
json_node = tree.root.add("JSON")
|
||||
tree.root.expand()
|
||||
tree.add_json(json_node, data)
|
||||
|
||||
def action_clear(self) -> None:
|
||||
tree = self.query_one(Tree)
|
||||
tree.clear()
|
||||
|
||||
def action_toggle_root(self) -> None:
|
||||
tree = self.query_one(Tree)
|
||||
tree.show_root = not tree.show_root
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TreeApp()
|
||||
app.run()
|
||||
@@ -89,16 +89,17 @@ nav:
|
||||
- "styles/visibility.md"
|
||||
- "styles/width.md"
|
||||
- Widgets:
|
||||
- "widgets/index.md"
|
||||
- "widgets/button.md"
|
||||
- "widgets/checkbox.md"
|
||||
- "widgets/data_table.md"
|
||||
- "widgets/directory_tree.md"
|
||||
- "widgets/footer.md"
|
||||
- "widgets/header.md"
|
||||
- "widgets/index.md"
|
||||
- "widgets/input.md"
|
||||
- "widgets/label.md"
|
||||
- "widgets/static.md"
|
||||
- "widgets/tree_control.md"
|
||||
- "widgets/tree.md"
|
||||
- API:
|
||||
- "api/index.md"
|
||||
- "api/app.md"
|
||||
|
||||
@@ -8,22 +8,23 @@ from ..case import camel_to_snake
|
||||
# but also to the `__init__.pyi` file in this same folder - otherwise text editors and type checkers won't
|
||||
# be able to "see" them.
|
||||
if typing.TYPE_CHECKING:
|
||||
from ..widget import Widget
|
||||
|
||||
from ._button import Button
|
||||
from ._checkbox import Checkbox
|
||||
from ._data_table import DataTable
|
||||
from ._directory_tree import DirectoryTree
|
||||
from ._footer import Footer
|
||||
from ._header import Header
|
||||
from ._input import Input
|
||||
from ._label import Label
|
||||
from ._placeholder import Placeholder
|
||||
from ._pretty import Pretty
|
||||
from ._static import Static
|
||||
from ._input import Input
|
||||
from ._text_log import TextLog
|
||||
from ._tree import Tree
|
||||
from ._tree_control import TreeControl
|
||||
from ._tree_node import TreeNode
|
||||
from ._welcome import Welcome
|
||||
from ..widget import Widget
|
||||
|
||||
__all__ = [
|
||||
"Button",
|
||||
@@ -39,7 +40,7 @@ __all__ = [
|
||||
"Static",
|
||||
"TextLog",
|
||||
"Tree",
|
||||
"TreeControl",
|
||||
"TreeNode",
|
||||
"Welcome",
|
||||
]
|
||||
|
||||
|
||||
@@ -12,5 +12,5 @@ from ._static import Static as Static
|
||||
from ._input import Input as Input
|
||||
from ._text_log import TextLog as TextLog
|
||||
from ._tree import Tree as Tree
|
||||
from ._tree_control import TreeControl as TreeControl
|
||||
from ._tree_node import TreeNode as TreeNode
|
||||
from ._welcome import Welcome as Welcome
|
||||
|
||||
@@ -56,6 +56,8 @@ class _TreeLine:
|
||||
|
||||
@rich.repr.auto
|
||||
class TreeNode(Generic[TreeDataType]):
|
||||
"""An object that represents a "node" in a tree control."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tree: Tree[TreeDataType],
|
||||
@@ -90,8 +92,9 @@ class TreeNode(Generic[TreeDataType]):
|
||||
self._selected_ = False
|
||||
self._updates += 1
|
||||
|
||||
@property
|
||||
def line(self) -> int:
|
||||
"""Get the line number for this node, or -1 if it is not displayed."""
|
||||
"""int: Get the line number for this node, or -1 if it is not displayed."""
|
||||
return self._line
|
||||
|
||||
@property
|
||||
@@ -116,9 +119,33 @@ class TreeNode(Generic[TreeDataType]):
|
||||
|
||||
@property
|
||||
def id(self) -> NodeID:
|
||||
"""Get the node ID."""
|
||||
"""NodeID: Get the node ID."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def is_expanded(self) -> bool:
|
||||
"""bool: Check if the node is expanded."""
|
||||
return self._expanded
|
||||
|
||||
@property
|
||||
def is_last(self) -> bool:
|
||||
"""bool: Check if this is the last child."""
|
||||
if self._parent is None:
|
||||
return True
|
||||
return bool(
|
||||
self._parent._children and self._parent._children[-1] == self,
|
||||
)
|
||||
|
||||
@property
|
||||
def allow_expand(self) -> bool:
|
||||
"""bool: Check if the node is allowed to expand."""
|
||||
return self._allow_expand
|
||||
|
||||
@allow_expand.setter
|
||||
def allow_expand(self, allow_expand: bool) -> None:
|
||||
self._allow_expand = allow_expand
|
||||
self._updates += 1
|
||||
|
||||
def expand(self) -> None:
|
||||
"""Expand a node (show its children)."""
|
||||
self._expanded = True
|
||||
@@ -147,30 +174,6 @@ class TreeNode(Generic[TreeDataType]):
|
||||
text_label = self._tree.process_label(label)
|
||||
self._label = text_label
|
||||
|
||||
@property
|
||||
def is_expanded(self) -> bool:
|
||||
"""bool: Check if the node is expanded."""
|
||||
return self._expanded
|
||||
|
||||
@property
|
||||
def is_last(self) -> bool:
|
||||
"""bool: Check if this is the last child."""
|
||||
if self._parent is None:
|
||||
return True
|
||||
return bool(
|
||||
self._parent._children and self._parent._children[-1] == self,
|
||||
)
|
||||
|
||||
@property
|
||||
def allow_expand(self) -> bool:
|
||||
"""bool: Check if the node is allowed to expand."""
|
||||
return self._allow_expand
|
||||
|
||||
@allow_expand.setter
|
||||
def allow_expand(self, allow_expand: bool) -> bool:
|
||||
self._allow_expand = allow_expand
|
||||
self._updates += 1
|
||||
|
||||
def add(
|
||||
self,
|
||||
label: TextType,
|
||||
@@ -199,6 +202,21 @@ class TreeNode(Generic[TreeDataType]):
|
||||
self._tree._invalidate()
|
||||
return node
|
||||
|
||||
def add_leaf(
|
||||
self, label: TextType, data: TreeDataType | None = None
|
||||
) -> TreeNode[TreeDataType]:
|
||||
"""Add a 'leaf' node (a node that can not expand).
|
||||
|
||||
Args:
|
||||
label (TextType): Label for the node.
|
||||
data (TreeDataType | None, optional): Optional data. Defaults to None.
|
||||
|
||||
Returns:
|
||||
TreeNode[TreeDataType]: New node.
|
||||
"""
|
||||
node = self.add(label, data, expand=False, allow_expand=False)
|
||||
return node
|
||||
|
||||
|
||||
class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
|
||||
@@ -451,36 +469,6 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
else:
|
||||
return line.node
|
||||
|
||||
def add_json(self, node: TreeNode, json_data: object) -> None:
|
||||
|
||||
from rich.highlighter import ReprHighlighter
|
||||
|
||||
highlighter = ReprHighlighter()
|
||||
|
||||
def add_node(name: str, node: TreeNode, data: object) -> None:
|
||||
if isinstance(data, dict):
|
||||
node._label = Text(f"{{}} {name}")
|
||||
for key, value in data.items():
|
||||
new_node = node.add("")
|
||||
add_node(key, new_node, value)
|
||||
elif isinstance(data, list):
|
||||
node._label = Text(f"[] {name}")
|
||||
for index, value in enumerate(data):
|
||||
new_node = node.add("")
|
||||
add_node(str(index), new_node, value)
|
||||
else:
|
||||
node._allow_expand = False
|
||||
if name:
|
||||
label = Text.assemble(
|
||||
Text.from_markup(f"[b]{name}[/b]="), highlighter(repr(data))
|
||||
)
|
||||
else:
|
||||
label = Text(repr(data))
|
||||
node._label = label
|
||||
|
||||
add_node("JSON", node, json_data)
|
||||
self._invalidate()
|
||||
|
||||
def validate_cursor_line(self, value: int) -> int:
|
||||
return clamp(value, 0, len(self._tree_lines) - 1)
|
||||
|
||||
@@ -642,10 +630,11 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
width = self.size.width
|
||||
|
||||
self.virtual_size = Size(width, len(lines))
|
||||
if self.cursor_node is not None:
|
||||
self.cursor_line = self.cursor_node._line
|
||||
if self.cursor_line >= len(lines):
|
||||
self.cursor_line = -1
|
||||
if self.cursor_line != -1:
|
||||
if self.cursor_node is not None:
|
||||
self.cursor_line = self.cursor_node._line
|
||||
if self.cursor_line >= len(lines):
|
||||
self.cursor_line = -1
|
||||
self.refresh()
|
||||
|
||||
def render_line(self, y: int) -> list[Segment]:
|
||||
@@ -813,7 +802,8 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True):
|
||||
def action_cursor_down(self) -> None:
|
||||
if self.cursor_line == -1:
|
||||
self.cursor_line = 0
|
||||
self.cursor_line += 1
|
||||
else:
|
||||
self.cursor_line += 1
|
||||
self.scroll_to_line(self.cursor_line)
|
||||
|
||||
def action_page_down(self) -> None:
|
||||
|
||||
@@ -1,427 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
from typing import ClassVar, Generic, Iterator, NewType, TypeVar
|
||||
|
||||
import rich.repr
|
||||
from rich.console import RenderableType
|
||||
from rich.style import Style, NULL_STYLE
|
||||
from rich.text import Text, TextType
|
||||
from rich.tree import Tree
|
||||
|
||||
from ..geometry import Region, Size
|
||||
from .. import events
|
||||
from ..reactive import Reactive
|
||||
from .._types import MessageTarget
|
||||
from ..widgets import Static
|
||||
from ..message import Message
|
||||
from .. import messages
|
||||
|
||||
|
||||
NodeID = NewType("NodeID", int)
|
||||
|
||||
|
||||
NodeDataType = TypeVar("NodeDataType")
|
||||
EventNodeDataType = TypeVar("EventNodeDataType")
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class TreeNode(Generic[NodeDataType]):
|
||||
def __init__(
|
||||
self,
|
||||
parent: TreeNode[NodeDataType] | None,
|
||||
node_id: NodeID,
|
||||
control: TreeControl,
|
||||
tree: Tree,
|
||||
label: TextType,
|
||||
data: NodeDataType,
|
||||
) -> None:
|
||||
self.parent = parent
|
||||
self.id = node_id
|
||||
self._control = control
|
||||
self._tree = tree
|
||||
self.label = label
|
||||
self.data = data
|
||||
self.loaded = False
|
||||
self._expanded = False
|
||||
self._empty = False
|
||||
self._tree.expanded = False
|
||||
self.children: list[TreeNode] = []
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "id", self.id
|
||||
yield "label", self.label
|
||||
yield "data", self.data
|
||||
|
||||
@property
|
||||
def control(self) -> TreeControl:
|
||||
return self._control
|
||||
|
||||
@property
|
||||
def empty(self) -> bool:
|
||||
return self._empty
|
||||
|
||||
@property
|
||||
def expanded(self) -> bool:
|
||||
return self._expanded
|
||||
|
||||
@property
|
||||
def is_cursor(self) -> bool:
|
||||
return self.control.cursor == self.id and self.control.show_cursor
|
||||
|
||||
@property
|
||||
def tree(self) -> Tree:
|
||||
return self._tree
|
||||
|
||||
@property
|
||||
def next_node(self) -> TreeNode[NodeDataType] | None:
|
||||
"""The next node in the tree, or None if at the end."""
|
||||
|
||||
if self.expanded and self.children:
|
||||
return self.children[0]
|
||||
else:
|
||||
|
||||
sibling = self.next_sibling
|
||||
if sibling is not None:
|
||||
return sibling
|
||||
|
||||
node = self
|
||||
while True:
|
||||
if node.parent is None:
|
||||
return None
|
||||
sibling = node.parent.next_sibling
|
||||
if sibling is not None:
|
||||
return sibling
|
||||
else:
|
||||
node = node.parent
|
||||
|
||||
@property
|
||||
def previous_node(self) -> TreeNode[NodeDataType] | None:
|
||||
"""The previous node in the tree, or None if at the end."""
|
||||
|
||||
sibling = self.previous_sibling
|
||||
if sibling is not None:
|
||||
|
||||
def last_sibling(node) -> TreeNode[NodeDataType]:
|
||||
if node.expanded and node.children:
|
||||
return last_sibling(node.children[-1])
|
||||
else:
|
||||
return (
|
||||
node.children[-1] if (node.children and node.expanded) else node
|
||||
)
|
||||
|
||||
return last_sibling(sibling)
|
||||
|
||||
if self.parent is None:
|
||||
return None
|
||||
return self.parent
|
||||
|
||||
@property
|
||||
def next_sibling(self) -> TreeNode[NodeDataType] | None:
|
||||
"""The next sibling, or None if last sibling."""
|
||||
if self.parent is None:
|
||||
return None
|
||||
iter_siblings = iter(self.parent.children)
|
||||
try:
|
||||
for node in iter_siblings:
|
||||
if node is self:
|
||||
return next(iter_siblings)
|
||||
except StopIteration:
|
||||
pass
|
||||
return None
|
||||
|
||||
@property
|
||||
def previous_sibling(self) -> TreeNode[NodeDataType] | None:
|
||||
"""Previous sibling or None if first sibling."""
|
||||
if self.parent is None:
|
||||
return None
|
||||
iter_siblings = iter(self.parent.children)
|
||||
sibling: TreeNode[NodeDataType] | None = None
|
||||
|
||||
for node in iter_siblings:
|
||||
if node is self:
|
||||
return sibling
|
||||
sibling = node
|
||||
return None
|
||||
|
||||
def expand(self, expanded: bool = True) -> None:
|
||||
self._expanded = expanded
|
||||
self._tree.expanded = expanded
|
||||
self._control.refresh(layout=True)
|
||||
|
||||
def toggle(self) -> None:
|
||||
self.expand(not self._expanded)
|
||||
|
||||
def add(self, label: TextType, data: NodeDataType) -> None:
|
||||
self._control.add(self.id, label, data=data)
|
||||
self._control.refresh(layout=True)
|
||||
self._empty = False
|
||||
|
||||
def __rich__(self) -> RenderableType:
|
||||
return self._control.render_node(self)
|
||||
|
||||
|
||||
class TreeControl(Generic[NodeDataType], Static, can_focus=True):
|
||||
DEFAULT_CSS = """
|
||||
TreeControl {
|
||||
color: $text;
|
||||
height: auto;
|
||||
width: 100%;
|
||||
link-style: not underline;
|
||||
}
|
||||
|
||||
TreeControl > .tree--guides {
|
||||
color: $success;
|
||||
}
|
||||
|
||||
TreeControl > .tree--guides-highlight {
|
||||
color: $success;
|
||||
text-style: uu;
|
||||
}
|
||||
|
||||
TreeControl > .tree--guides-cursor {
|
||||
color: $secondary;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
TreeControl > .tree--labels {
|
||||
color: $text;
|
||||
}
|
||||
|
||||
TreeControl > .tree--cursor {
|
||||
background: $secondary;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
||||
"tree--guides",
|
||||
"tree--guides-highlight",
|
||||
"tree--guides-cursor",
|
||||
"tree--labels",
|
||||
"tree--cursor",
|
||||
}
|
||||
|
||||
class NodeSelected(Generic[EventNodeDataType], Message, bubble=False):
|
||||
def __init__(
|
||||
self, sender: MessageTarget, node: TreeNode[EventNodeDataType]
|
||||
) -> None:
|
||||
self.node = node
|
||||
super().__init__(sender)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
label: TextType,
|
||||
data: NodeDataType,
|
||||
*,
|
||||
name: str | None = None,
|
||||
id: str | None = None,
|
||||
classes: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(name=name, id=id, classes=classes)
|
||||
self.data = data
|
||||
|
||||
self.node_id = NodeID(0)
|
||||
self.nodes: dict[NodeID, TreeNode[NodeDataType]] = {}
|
||||
self._tree = Tree(label)
|
||||
|
||||
self.root: TreeNode[NodeDataType] = TreeNode(
|
||||
None, self.node_id, self, self._tree, label, data
|
||||
)
|
||||
|
||||
self._tree.label = self.root
|
||||
self.nodes[NodeID(self.node_id)] = self.root
|
||||
|
||||
self.auto_links = False
|
||||
|
||||
hover_node: Reactive[NodeID | None] = Reactive(None)
|
||||
cursor: Reactive[NodeID] = Reactive(NodeID(0))
|
||||
cursor_line: Reactive[int] = Reactive(0)
|
||||
show_cursor: Reactive[bool] = Reactive(False)
|
||||
|
||||
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 get_content_height(self, container: Size, viewport: Size, width: int) -> int:
|
||||
def get_size(tree: Tree) -> int:
|
||||
return 1 + sum(
|
||||
get_size(child) if child.expanded else 1 for child in tree.children
|
||||
)
|
||||
|
||||
size = get_size(self._tree)
|
||||
return size
|
||||
|
||||
def add(
|
||||
self,
|
||||
node_id: NodeID,
|
||||
label: TextType,
|
||||
data: NodeDataType,
|
||||
) -> None:
|
||||
|
||||
parent = self.nodes[node_id]
|
||||
self.node_id = NodeID(self.node_id + 1)
|
||||
child_tree = parent._tree.add(label)
|
||||
child_tree.guide_style = self._guide_style
|
||||
child_node: TreeNode[NodeDataType] = TreeNode(
|
||||
parent, self.node_id, self, child_tree, label, data
|
||||
)
|
||||
parent.children.append(child_node)
|
||||
child_tree.label = child_node
|
||||
self.nodes[self.node_id] = child_node
|
||||
|
||||
self.refresh(layout=True)
|
||||
|
||||
def find_cursor(self) -> int | None:
|
||||
"""Find the line location for the cursor node."""
|
||||
|
||||
node_id = self.cursor
|
||||
line = 0
|
||||
|
||||
stack: list[Iterator[TreeNode[NodeDataType]]]
|
||||
stack = [iter([self.root])]
|
||||
|
||||
pop = stack.pop
|
||||
push = stack.append
|
||||
while stack:
|
||||
iter_children = pop()
|
||||
try:
|
||||
node = next(iter_children)
|
||||
except StopIteration:
|
||||
continue
|
||||
else:
|
||||
if node.id == node_id:
|
||||
return line
|
||||
line += 1
|
||||
push(iter_children)
|
||||
if node.children and node.expanded:
|
||||
push(iter(node.children))
|
||||
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:
|
||||
label_style = self.get_component_styles("tree--labels").rich_style
|
||||
label = (
|
||||
Text(node.label, no_wrap=True, style=label_style, overflow="ellipsis")
|
||||
if isinstance(node.label, str)
|
||||
else node.label
|
||||
)
|
||||
if node.id == self.hover_node:
|
||||
label.stylize("underline")
|
||||
label.apply_meta({"@click": f"click_label({node.id})", "tree_node": node.id})
|
||||
return label
|
||||
|
||||
def action_click_label(self, node_id: NodeID) -> None:
|
||||
node = self.nodes[node_id]
|
||||
self.cursor = node.id
|
||||
self.cursor_line = self.find_cursor() or 0
|
||||
self.show_cursor = True
|
||||
self.post_message_no_wait(self.NodeSelected(self, node))
|
||||
|
||||
def on_mount(self) -> None:
|
||||
self._tree.guide_style = self._guide_style
|
||||
|
||||
@property
|
||||
def _guide_style(self) -> Style:
|
||||
return self.get_component_rich_style("tree--guides")
|
||||
|
||||
@property
|
||||
def _highlight_guide_style(self) -> Style:
|
||||
return self.get_component_rich_style("tree--guides-highlight")
|
||||
|
||||
@property
|
||||
def _cursor_guide_style(self) -> Style:
|
||||
return self.get_component_rich_style("tree--guides-cursor")
|
||||
|
||||
def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
self.hover_node = event.style.meta.get("tree_node")
|
||||
|
||||
def key_down(self, event: events.Key) -> None:
|
||||
event.stop()
|
||||
self.cursor_down()
|
||||
|
||||
def key_up(self, event: events.Key) -> None:
|
||||
event.stop()
|
||||
self.cursor_up()
|
||||
|
||||
def key_pagedown(self) -> None:
|
||||
assert self.parent is not None
|
||||
height = self.container_viewport.height
|
||||
|
||||
cursor = self.cursor
|
||||
cursor_line = self.cursor_line
|
||||
for _ in range(height):
|
||||
cursor_node = self.nodes[cursor]
|
||||
next_node = cursor_node.next_node
|
||||
if next_node is not None:
|
||||
cursor_line += 1
|
||||
cursor = next_node.id
|
||||
self.cursor = cursor
|
||||
self.cursor_line = cursor_line
|
||||
|
||||
def key_pageup(self) -> None:
|
||||
assert self.parent is not None
|
||||
height = self.container_viewport.height
|
||||
cursor = self.cursor
|
||||
cursor_line = self.cursor_line
|
||||
for _ in range(height):
|
||||
cursor_node = self.nodes[cursor]
|
||||
previous_node = cursor_node.previous_node
|
||||
if previous_node is not None:
|
||||
cursor_line -= 1
|
||||
cursor = previous_node.id
|
||||
self.cursor = cursor
|
||||
self.cursor_line = cursor_line
|
||||
|
||||
def key_home(self) -> None:
|
||||
self.cursor_line = 0
|
||||
self.cursor = NodeID(0)
|
||||
|
||||
def key_end(self) -> None:
|
||||
self.cursor = self.nodes[NodeID(0)].children[-1].id
|
||||
self.cursor_line = self.find_cursor() or 0
|
||||
|
||||
def key_enter(self, event: events.Key) -> None:
|
||||
cursor_node = self.nodes[self.cursor]
|
||||
event.stop()
|
||||
self.post_message_no_wait(self.NodeSelected(self, cursor_node))
|
||||
|
||||
def cursor_down(self) -> None:
|
||||
if not self.show_cursor:
|
||||
self.show_cursor = True
|
||||
return
|
||||
cursor_node = self.nodes[self.cursor]
|
||||
next_node = cursor_node.next_node
|
||||
if next_node is not None:
|
||||
self.cursor_line += 1
|
||||
self.cursor = next_node.id
|
||||
|
||||
def cursor_up(self) -> None:
|
||||
if not self.show_cursor:
|
||||
self.show_cursor = True
|
||||
return
|
||||
cursor_node = self.nodes[self.cursor]
|
||||
previous_node = cursor_node.previous_node
|
||||
if previous_node is not None:
|
||||
self.cursor_line -= 1
|
||||
self.cursor = previous_node.id
|
||||
1
src/textual/widgets/_tree_node.py
Normal file
1
src/textual/widgets/_tree_node.py
Normal file
@@ -0,0 +1 @@
|
||||
from ._tree import TreeNode as TreeNode
|
||||
@@ -6792,6 +6792,162 @@
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_tree_example
|
||||
'''
|
||||
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Generated with Rich https://www.textualize.io -->
|
||||
<style>
|
||||
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Regular"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Regular.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Regular.woff") format("woff");
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Fira Code";
|
||||
src: local("FiraCode-Bold"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff2/FiraCode-Bold.woff2") format("woff2"),
|
||||
url("https://cdnjs.cloudflare.com/ajax/libs/firacode/6.2.0/woff/FiraCode-Bold.woff") format("woff");
|
||||
font-style: bold;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.terminal-1345646321-matrix {
|
||||
font-family: Fira Code, monospace;
|
||||
font-size: 20px;
|
||||
line-height: 24.4px;
|
||||
font-variant-east-asian: full-width;
|
||||
}
|
||||
|
||||
.terminal-1345646321-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
font-family: arial;
|
||||
}
|
||||
|
||||
.terminal-1345646321-r1 { fill: #e2e3e3 }
|
||||
.terminal-1345646321-r2 { fill: #c5c8c6 }
|
||||
.terminal-1345646321-r3 { fill: #008139 }
|
||||
</style>
|
||||
|
||||
<defs>
|
||||
<clipPath id="terminal-1345646321-clip-terminal">
|
||||
<rect x="0" y="0" width="975.0" height="584.5999999999999" />
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-0">
|
||||
<rect x="0" y="1.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-1">
|
||||
<rect x="0" y="25.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-2">
|
||||
<rect x="0" y="50.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-3">
|
||||
<rect x="0" y="74.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-4">
|
||||
<rect x="0" y="99.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-5">
|
||||
<rect x="0" y="123.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-6">
|
||||
<rect x="0" y="147.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-7">
|
||||
<rect x="0" y="172.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-8">
|
||||
<rect x="0" y="196.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-9">
|
||||
<rect x="0" y="221.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-10">
|
||||
<rect x="0" y="245.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-11">
|
||||
<rect x="0" y="269.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-12">
|
||||
<rect x="0" y="294.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-13">
|
||||
<rect x="0" y="318.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-14">
|
||||
<rect x="0" y="343.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-15">
|
||||
<rect x="0" y="367.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-16">
|
||||
<rect x="0" y="391.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-17">
|
||||
<rect x="0" y="416.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-18">
|
||||
<rect x="0" y="440.7" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-19">
|
||||
<rect x="0" y="465.1" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-20">
|
||||
<rect x="0" y="489.5" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-21">
|
||||
<rect x="0" y="513.9" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
<clipPath id="terminal-1345646321-line-22">
|
||||
<rect x="0" y="538.3" width="976" height="24.65"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" x="1" y="1" width="992" height="633.6" rx="8"/><text class="terminal-1345646321-title" fill="#c5c8c6" text-anchor="middle" x="496" y="27">TreeApp</text>
|
||||
<g transform="translate(26,22)">
|
||||
<circle cx="0" cy="0" r="7" fill="#ff5f57"/>
|
||||
<circle cx="22" cy="0" r="7" fill="#febc2e"/>
|
||||
<circle cx="44" cy="0" r="7" fill="#28c840"/>
|
||||
</g>
|
||||
|
||||
<g transform="translate(9, 41)" clip-path="url(#terminal-1345646321-clip-terminal)">
|
||||
<rect fill="#24292f" x="0" y="1.5" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="24.4" y="1.5" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="73.2" y="1.5" width="902.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="25.9" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="48.8" y="25.9" width="24.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="73.2" y="25.9" width="122" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="195.2" y="25.9" width="780.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="50.3" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="97.6" y="50.3" width="48.8" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="146.4" y="50.3" width="829.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="74.7" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="97.6" y="74.7" width="85.4" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="183" y="74.7" width="793" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="99.1" width="97.6" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="97.6" y="99.1" width="73.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="170.8" y="99.1" width="805.2" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="123.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="147.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="172.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="196.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="221.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="245.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="269.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="294.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="318.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="343.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="367.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="391.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="416.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="440.7" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="465.1" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="489.5" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="513.9" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="538.3" width="976" height="24.65" shape-rendering="crispEdges"/><rect fill="#24292f" x="0" y="562.7" width="976" height="24.65" shape-rendering="crispEdges"/>
|
||||
<g class="terminal-1345646321-matrix">
|
||||
<text class="terminal-1345646321-r1" x="0" y="20" textLength="24.4" clip-path="url(#terminal-1345646321-line-0)">▼ </text><text class="terminal-1345646321-r1" x="24.4" y="20" textLength="48.8" clip-path="url(#terminal-1345646321-line-0)">Dune</text><text class="terminal-1345646321-r2" x="976" y="20" textLength="12.2" clip-path="url(#terminal-1345646321-line-0)">
|
||||
</text><text class="terminal-1345646321-r3" x="0" y="44.4" textLength="48.8" clip-path="url(#terminal-1345646321-line-1)">└── </text><text class="terminal-1345646321-r1" x="48.8" y="44.4" textLength="24.4" clip-path="url(#terminal-1345646321-line-1)">▼ </text><text class="terminal-1345646321-r1" x="73.2" y="44.4" textLength="122" clip-path="url(#terminal-1345646321-line-1)">Characters</text><text class="terminal-1345646321-r2" x="976" y="44.4" textLength="12.2" clip-path="url(#terminal-1345646321-line-1)">
|
||||
</text><text class="terminal-1345646321-r3" x="0" y="68.8" textLength="97.6" clip-path="url(#terminal-1345646321-line-2)">    ├── </text><text class="terminal-1345646321-r1" x="97.6" y="68.8" textLength="48.8" clip-path="url(#terminal-1345646321-line-2)">Paul</text><text class="terminal-1345646321-r2" x="976" y="68.8" textLength="12.2" clip-path="url(#terminal-1345646321-line-2)">
|
||||
</text><text class="terminal-1345646321-r3" x="0" y="93.2" textLength="97.6" clip-path="url(#terminal-1345646321-line-3)">    ├── </text><text class="terminal-1345646321-r1" x="97.6" y="93.2" textLength="85.4" clip-path="url(#terminal-1345646321-line-3)">Jessica</text><text class="terminal-1345646321-r2" x="976" y="93.2" textLength="12.2" clip-path="url(#terminal-1345646321-line-3)">
|
||||
</text><text class="terminal-1345646321-r3" x="0" y="117.6" textLength="97.6" clip-path="url(#terminal-1345646321-line-4)">    └── </text><text class="terminal-1345646321-r1" x="97.6" y="117.6" textLength="73.2" clip-path="url(#terminal-1345646321-line-4)">Channi</text><text class="terminal-1345646321-r2" x="976" y="117.6" textLength="12.2" clip-path="url(#terminal-1345646321-line-4)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="142" textLength="12.2" clip-path="url(#terminal-1345646321-line-5)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="166.4" textLength="12.2" clip-path="url(#terminal-1345646321-line-6)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="190.8" textLength="12.2" clip-path="url(#terminal-1345646321-line-7)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="215.2" textLength="12.2" clip-path="url(#terminal-1345646321-line-8)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="239.6" textLength="12.2" clip-path="url(#terminal-1345646321-line-9)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="264" textLength="12.2" clip-path="url(#terminal-1345646321-line-10)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="288.4" textLength="12.2" clip-path="url(#terminal-1345646321-line-11)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="312.8" textLength="12.2" clip-path="url(#terminal-1345646321-line-12)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="337.2" textLength="12.2" clip-path="url(#terminal-1345646321-line-13)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="361.6" textLength="12.2" clip-path="url(#terminal-1345646321-line-14)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="386" textLength="12.2" clip-path="url(#terminal-1345646321-line-15)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="410.4" textLength="12.2" clip-path="url(#terminal-1345646321-line-16)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="434.8" textLength="12.2" clip-path="url(#terminal-1345646321-line-17)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="459.2" textLength="12.2" clip-path="url(#terminal-1345646321-line-18)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="483.6" textLength="12.2" clip-path="url(#terminal-1345646321-line-19)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="508" textLength="12.2" clip-path="url(#terminal-1345646321-line-20)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="532.4" textLength="12.2" clip-path="url(#terminal-1345646321-line-21)">
|
||||
</text><text class="terminal-1345646321-r2" x="976" y="556.8" textLength="12.2" clip-path="url(#terminal-1345646321-line-22)">
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
'''
|
||||
# ---
|
||||
# name: test_vertical_layout
|
||||
'''
|
||||
<svg class="rich-terminal" viewBox="0 0 994 635.5999999999999" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -11,6 +11,7 @@ SNAPSHOT_APPS_DIR = Path("./snapshot_apps")
|
||||
|
||||
# --- Layout related stuff ---
|
||||
|
||||
|
||||
def test_grid_layout_basic(snap_compare):
|
||||
assert snap_compare(LAYOUT_EXAMPLES_DIR / "grid_layout1.py")
|
||||
|
||||
@@ -48,6 +49,7 @@ def test_dock_layout_sidebar(snap_compare):
|
||||
# When adding a new widget, ideally we should also create a snapshot test
|
||||
# from these examples which test rendering and simple interactions with it.
|
||||
|
||||
|
||||
def test_checkboxes(snap_compare):
|
||||
"""Tests checkboxes but also acts a regression test for using
|
||||
width: auto in a Horizontal layout context."""
|
||||
@@ -98,6 +100,10 @@ def test_fr_units(snap_compare):
|
||||
assert snap_compare("snapshot_apps/fr_units.py")
|
||||
|
||||
|
||||
def test_tree_example(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "tree.py")
|
||||
|
||||
|
||||
# --- CSS properties ---
|
||||
# We have a canonical example for each CSS property that is shown in their docs.
|
||||
# If any of these change, something has likely broken, so snapshot each of them.
|
||||
@@ -122,5 +128,6 @@ def test_multiple_css(snap_compare):
|
||||
|
||||
# --- Other ---
|
||||
|
||||
|
||||
def test_key_display(snap_compare):
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "key_display.py")
|
||||
|
||||
Reference in New Issue
Block a user