Repository: zauberzeug/nicegui
Pull request #3482 (2024-08-12 11:46:37)

Allow filtering nodes in `ui.tree`
Inspired by https://github.com/zauberzeug/nicegui/discussions/3436#discussioncomment-10297039, this PR adds the `FilterElement` mixin to `ui.tree`. This allows to filter nodes by name.


Patch:
diff --git a/nicegui/elements/scene.js b/nicegui/elements/scene.js
index e5e7edaa..38de4898 100644
--- a/nicegui/elements/scene.js
+++ b/nicegui/elements/scene.js
@@ -388,6 +388,11 @@ export default {
       if (!this.objects.has(object_id)) return;
       this.objects.get(object_id).geometry = texture_geometry(coords);
     },
+    set_points(object_id, position, color) {
+      const geometry = this.objects.get(object_id).geometry;
+      geometry.setAttribute("position", new THREE.Float32BufferAttribute(position.flat(), 3));
+      geometry.setAttribute("color", new THREE.Float32BufferAttribute(color.flat(), 3));
+    },
     move_camera(x, y, z, look_at_x, look_at_y, look_at_z, up_x, up_y, up_z, duration) {
       if (this.camera_tween) this.camera_tween.stop();
       this.camera_tween = new TWEEN.Tween([
diff --git a/nicegui/elements/scene_objects.py b/nicegui/elements/scene_objects.py
index 8b7822e7..01d393dd 100644
--- a/nicegui/elements/scene_objects.py
+++ b/nicegui/elements/scene_objects.py
@@ -329,3 +329,9 @@ class PointCloud(Object3D):
         :param point_size: size of the points (default: 1.0)
         """
         super().__init__('point_cloud', points, colors, point_size)
+
+    def set_points(self, points: List[List[float]], colors: List[List[float]]) -> None:
+        """Change the points and colors of the point cloud."""
+        self.args[0] = points
+        self.args[1] = colors
+        self.scene.run_method('set_points', self.id, points, colors)
diff --git a/nicegui/elements/tree.py b/nicegui/elements/tree.py
index 01010bc3..e814b7bc 100644
--- a/nicegui/elements/tree.py
+++ b/nicegui/elements/tree.py
@@ -3,11 +3,11 @@ from typing import Any, Callable, Dict, Iterator, List, Literal, Optional, Set
 from typing_extensions import Self
 
 from .. import helpers
-from ..element import Element
 from ..events import GenericEventArguments, ValueChangeEventArguments, handle_event
+from .mixins.filter_element import FilterElement
 
 
-class Tree(Element):
+class Tree(FilterElement):
 
     def __init__(self,
                  nodes: List[Dict], *,
@@ -36,7 +36,7 @@ class Tree(Element):
         :param on_tick: callback which is invoked when a node is ticked or unticked
         :param tick_strategy: whether and how to use checkboxes ("leaf", "leaf-filtered" or "strict"; default: ``None``)
         """
-        super().__init__('q-tree')
+        super().__init__(tag='q-tree', filter=None)
         self._props['nodes'] = nodes
         self._props['node-key'] = node_key
         self._props['label-key'] = label_key
diff --git a/website/documentation/content/scene_documentation.py b/website/documentation/content/scene_documentation.py
index 39277063..78b7289b 100644
--- a/website/documentation/content/scene_documentation.py
+++ b/website/documentation/content/scene_documentation.py
@@ -142,15 +142,24 @@ def immediate_updates() -> None:
 @doc.demo('Rendering point clouds', '''
     You can render point clouds using the `point_cloud` method.
     The `points` argument is a list of point coordinates, and the `colors` argument is a list of RGB colors (0..1).
+    You can update the cloud using its `set_points()` method.
 ''')
 def point_clouds() -> None:
     import numpy as np
 
-    with ui.scene().classes('w-full h-64') as scene:
+    def generate_data(frequency: float = 1.0):
         x, y = np.meshgrid(np.linspace(-3, 3), np.linspace(-3, 3))
-        z = np.sin(x) * np.cos(y) + 1
+        z = np.sin(x * frequency) * np.cos(y * frequency) + 1
         points = np.dstack([x, y, z]).reshape(-1, 3)
-        scene.point_cloud(points=points, colors=points, point_size=0.1)
+        colors = points / [6, 6, 2] + [0.5, 0.5, 0]
+        return points, colors
+
+    with ui.scene().classes('w-full h-64') as scene:
+        points, colors = generate_data()
+        point_cloud = scene.point_cloud(points, colors, point_size=0.1)
+
+    ui.slider(min=0.1, max=3, step=0.1, value=1) \
+        .on_value_change(lambda e: point_cloud.set_points(*generate_data(e.value)))
 
 
 @doc.demo('Wait for Initialization', '''
diff --git a/website/documentation/content/tree_documentation.py b/website/documentation/content/tree_documentation.py
index c0ae0e4f..893b4c48 100644
--- a/website/documentation/content/tree_documentation.py
+++ b/website/documentation/content/tree_documentation.py
@@ -94,4 +94,16 @@ def tick_programmatically():
         ui.button('Untick all', on_click=t.untick)
 
 
+@doc.demo('Filter nodes', '''
+    You can filter nodes by setting the `filter` property.
+    The tree will only show nodes that match the filter.
+''')
+def filter_nodes():
+    t = ui.tree([
+        {'id': 'fruits', 'children': [{'id': 'Apple'}, {'id': 'Banana'}]},
+        {'id': 'vegetables', 'children': [{'id': 'Potato'}, {'id': 'Tomato'}]},
+    ], label_key='id').expand()
+    ui.input('filter').bind_value_to(t, 'filter')
+
+
 doc.reference(ui.tree)


Test patch:
diff --git a/tests/test_tree.py b/tests/test_tree.py
index bebb56e2..b59c44c4 100644
--- a/tests/test_tree.py
+++ b/tests/test_tree.py
@@ -108,3 +108,20 @@ def test_tick_untick_node_or_nodes(screen: Screen):
 
     screen.click('Untick all')
     screen.should_contain('Ticked: []')
+
+
+def test_filter(screen: Screen):
+    t = ui.tree([
+        {'id': 'fruits', 'children': [{'id': 'Apple'}, {'id': 'Banana'}, {'id': 'Cherry'}]},
+    ], label_key='id', tick_strategy='leaf-filtered').expand()
+    ui.button('Filter', on_click=lambda: t.set_filter('a'))
+
+    screen.open('/')
+    screen.should_contain('Apple')
+    screen.should_contain('Banana')
+    screen.should_contain('Cherry')
+
+    screen.click('Filter')
+    screen.should_contain('Apple')
+    screen.should_contain('Banana')
+    screen.should_not_contain('Cherry')