Skip to main content

ferroui_controls/
tree_view_item.rs

1use crate::generators::RecycleKey;
2use crate::i_selectable::{register_selectable, ISelectable};
3use crate::items_source::ItemsChangedEventArgs;
4use crate::metadata::{PseudoClassesAttribute, TemplatePartAttribute};
5use crate::mixins::{PressedMixin, SelectableMixin};
6use crate::primitives::{
7    HeaderedItemsControl, HeaderedItemsControlImpl, SelectingItemsControl, TemplateAppliedEventArgs,
8    TemplatedControlImpl,
9};
10use crate::templates::{FuncTemplate, ITemplateOf};
11use crate::{
12    Control, ControlImpl, ItemsControl, ItemsControlImpl, ItemsControlImplExt, Panel, RequestBringIntoViewEventArgs,
13    StackPanel, TreeView,
14};
15use ferroui_base::collections::NotifyCollectionChangedAction;
16use ferroui_base::data::BindingMode;
17use ferroui_base::input::{
18    InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyModifiers, NavigationMethod,
19    PointerEventArgs, PointerPressedEventArgs, PointerReleasedEventArgs, TappedEventArgs,
20};
21use ferroui_base::interactivity::{
22    InteractiveImpl, RoutedEvent, RoutedEventArgs, RoutedEventHandlerToken, RoutingStrategies,
23};
24use ferroui_base::layout::LayoutableImpl;
25use ferroui_base::logical_tree::LogicalTreeAttachmentEventArgs;
26use ferroui_base::{
27    ferro_class, ferro_impl_classes, ferro_property, ferro_routed_event, instantiate, BoxedValue, DirectProperty,
28    FerroObjectImpl, FerroProperty, FerroPropertyChangedEventArgs, Rect, Ref, Size, StaticType,
29    StyledElement, StyledElementImpl, StyledElementImplExt, StyledProperty, StyledPropertyOptions, VisualImpl,
30    WeakRef,
31};
32use std::cell::{Cell, RefCell};
33use std::rc::Rc;
34
35/// An item in a [`TreeView`].
36#[repr(C)]
37pub struct TreeViewItem {
38    base: HeaderedItemsControl,
39    tree_view: RefCell<Option<WeakRef<TreeView>>>,
40    header: RefCell<Option<Ref<Control>>>,
41    header_presenter: RefCell<Option<Ref<Control>>>,
42    header_double_tapped_token: Cell<Option<RoutedEventHandlerToken>>,
43    level: Cell<i32>,
44    template_applied: Cell<bool>,
45    deferred_bring_into_view_flag: Cell<bool>,
46    items_panel_pointer_pressed: Cell<bool>,
47}
48
49ferro_class! {
50    TreeViewItem: HeaderedItemsControl, virtuals TreeViewItemImpl: HeaderedItemsControlImpl {
51        /// Called when a request to bring a control into view reaches the
52        /// item.
53        fn on_request_bring_into_view(this, e: &RequestBringIntoViewEventArgs);
54        /// Invoked when the double tapped event occurs in the header.
55        fn on_header_double_tapped(this, e: &TappedEventArgs);
56    }
57}
58ferroui_base::ferro_class_info!(TreeViewItem { new: TreeViewItem::new });
59
60ferro_impl_classes!(
61    TreeViewItem: VisualImpl,
62    LayoutableImpl,
63    InteractiveImpl,
64    HeaderedItemsControlImpl
65);
66
67impl ControlImpl for TreeViewItem {
68    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
69        crate::automation::peers::TreeViewItemAutomationPeer::new(this).upcast()
70    }
71}
72
73ferroui_base::ferro_impl_classes!(TreeViewItem: FerroObjectImpl);
74
75impl StyledElementImpl for TreeViewItem {
76    fn on_attached_to_logical_tree(this: &Self, e: &LogicalTreeAttachmentEventArgs) {
77        Self::parent_on_attached_to_logical_tree(this, e);
78
79        let mut tree_view = None;
80        let mut ancestor = this.parent();
81        while let Some(current) = ancestor {
82            if let Some(found) = current.cast::<TreeView>() {
83                tree_view = Some(found);
84                break;
85            }
86            ancestor = current.parent();
87        }
88
89        *this.tree_view.borrow_mut() = tree_view.as_ref().map(Ref::downgrade);
90
91        this.set_level(Self::calculate_distance_from_logical_parent::<TreeView>(Some(this.to_ref().upcast()), -1) - 1);
92
93        let Some(tree_view) = tree_view else { return };
94
95        if this.item_template().is_none() {
96            if let Some(item_template) = tree_view.item_template() {
97                this.set_current_value(ItemsControl::item_template_property(), Some(item_template));
98            }
99        }
100
101        if this.item_container_theme().is_none() {
102            if let Some(item_container_theme) = tree_view.item_container_theme() {
103                this.set_current_value(ItemsControl::item_container_theme_property(), Some(item_container_theme));
104            }
105        }
106    }
107}
108
109impl InputElementImpl for TreeViewItem {
110    fn on_key_down(this: &Self, e: &KeyEventArgs) {
111        if !e.handled() {
112            let handler = match e.key {
113                Key::Left => {
114                    Some(apply_to_item_or_recursively_if_ctrl(focus_aware_collapse_item, e.key_modifiers))
115                }
116                Key::Right => Some(apply_to_item_or_recursively_if_ctrl(expand_item, e.key_modifiers)),
117                Key::Enter => Some(apply_to_item_or_recursively_if_ctrl(
118                    if this.is_expanded() { collapse_item } else { expand_item },
119                    e.key_modifiers,
120                )),
121
122                // Do not handle the control modifier with the numpad keys.
123                Key::Subtract => Some(KeyHandler::Item(focus_aware_collapse_item)),
124                Key::Add => Some(KeyHandler::Item(expand_item)),
125                Key::Divide => Some(KeyHandler::Subtree(collapse_item)),
126                Key::Multiply => Some(KeyHandler::Subtree(expand_item)),
127                _ => None,
128            };
129
130            match handler {
131                Some(handler) => e.set_handled(handler.invoke(this)),
132                None => {
133                    if let Some(owner) = this.tree_view_owner() {
134                        owner.update_selection_from_event(&this.to_ref().upcast(), e);
135                    }
136                }
137            }
138        }
139
140        // Don't call the base implementation: let events bubble up to the
141        // containing tree view.
142    }
143
144    fn on_pointer_pressed(this: &Self, e: &PointerPressedEventArgs) {
145        Self::parent_on_pointer_pressed(this, e);
146
147        this.items_panel_pointer_pressed.set(this.is_items_panel_event(e));
148
149        // Don't select if the pointer was pressed over the items panel.
150        if !this.items_panel_pointer_pressed.get() {
151            if let Some(owner) = this.tree_view_owner() {
152                owner.update_selection_from_event(&this.to_ref().upcast(), e);
153            }
154        }
155    }
156
157    fn on_pointer_released(this: &Self, e: &PointerReleasedEventArgs) {
158        Self::parent_on_pointer_released(this, e);
159
160        // Don't select if the gesture either started on the items panel, or
161        // ended on it.
162        if !this.items_panel_pointer_pressed.get() && !this.is_items_panel_event(e) {
163            if let Some(owner) = this.tree_view_owner() {
164                owner.update_selection_from_event(&this.to_ref().upcast(), e);
165            }
166        }
167
168        this.items_panel_pointer_pressed.set(false);
169    }
170}
171
172impl TemplatedControlImpl for TreeViewItem {
173    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
174        let previous = this.header_presenter.take();
175        if let (Some(previous), Some(token)) = (previous, this.header_double_tapped_token.take()) {
176            previous.remove_handler(InputElement::double_tapped_event(), token);
177        }
178
179        *this.header.borrow_mut() = e.name_scope().find_as::<Control>("PART_Header");
180        let header_presenter = e.name_scope().find_as::<Control>("PART_HeaderPresenter");
181        *this.header_presenter.borrow_mut() = header_presenter.clone();
182        this.template_applied.set(true);
183
184        if let Some(header_presenter) = header_presenter {
185            let weak = this.to_ref().downgrade();
186            let token = header_presenter.add_handler(InputElement::double_tapped_event(), move |_, e| {
187                if let Some(this) = weak.upgrade() {
188                    this.header_double_tapped(e);
189                }
190            });
191            this.header_double_tapped_token.set(Some(token));
192        }
193
194        if this.deferred_bring_into_view_flag.get() {
195            this.deferred_bring_into_view_flag.set(false);
196            this.bring_into_view();
197        }
198    }
199}
200
201impl ItemsControlImpl for TreeViewItem {
202    fn create_container_for_item_override(
203        this: &Self,
204        item: &Option<BoxedValue>,
205        index: i32,
206        recycle_key: Option<RecycleKey>,
207    ) -> Ref<Control> {
208        this.ensure_tree_view().create_container_for_item_override(item, index, recycle_key)
209    }
210
211    fn needs_container_override(this: &Self, item: &Option<BoxedValue>, index: i32) -> (bool, Option<RecycleKey>) {
212        this.ensure_tree_view().needs_container_override(item, index)
213    }
214
215    fn prepare_container_for_item_override(
216        this: &Self,
217        container: &Ref<Control>,
218        item: &Option<BoxedValue>,
219        index: i32,
220    ) {
221        this.ensure_tree_view().prepare_container_for_item_override(container, item, index)
222    }
223
224    fn container_for_item_prepared_override(
225        this: &Self,
226        container: &Ref<Control>,
227        item: &Option<BoxedValue>,
228        index: i32,
229    ) {
230        this.ensure_tree_view().container_for_item_prepared_override(container, item, index)
231    }
232
233    fn on_items_view_collection_changed(this: &Self, e: &ItemsChangedEventArgs<'_>) {
234        Self::parent_on_items_view_collection_changed(this, e);
235
236        let Some(tree_view) = this.tree_view_owner() else { return };
237
238        match e.action {
239            NotifyCollectionChangedAction::Remove | NotifyCollectionChangedAction::Replace => {
240                for i in e.old_items {
241                    tree_view.selected_items().remove(&i);
242                }
243            }
244            NotifyCollectionChangedAction::Reset => {
245                for i in this.get_realized_containers() {
246                    if i.downcast_ref::<TreeViewItem>().is_some_and(|tvi| tvi.is_selected()) {
247                        tree_view.selected_items().remove(&i.data_context());
248                    }
249                }
250            }
251            _ => {}
252        }
253    }
254}
255
256impl TreeViewItemImpl for TreeViewItem {
257    fn on_request_bring_into_view(this: &Self, e: &RequestBringIntoViewEventArgs) {
258        let this_visual: Ref<ferroui_base::Visual> = this.to_ref().upcast();
259        let is_target = e.target_object.as_ref() == Some(&this_visual);
260        if !is_target {
261            return;
262        }
263
264        if !this.template_applied.get() {
265            this.deferred_bring_into_view_flag.set(true);
266            return;
267        }
268
269        let header = this.header.borrow().clone();
270        if let Some(header) = header {
271            if let Some(m) = header.transform_to_visual(this) {
272                let bounds = Rect::from_size(Self::get_header_target_size(&header));
273                let rect = bounds.transform_to_aabb(m);
274                e.set_target_rect(rect);
275            }
276        }
277    }
278
279    fn on_header_double_tapped(this: &Self, e: &TappedEventArgs) {
280        if this.item_count() > 0 {
281            this.set_current_value(Self::is_expanded_property(), !this.is_expanded());
282            e.set_handled(true);
283        }
284    }
285}
286
287impl ISelectable for TreeViewItem {
288    fn is_selected(&self) -> bool {
289        TreeViewItem::is_selected(self)
290    }
291
292    fn set_is_selected(&self, value: bool) {
293        TreeViewItem::set_is_selected(self, value)
294    }
295}
296
297/// A key handler of an item: applied to the item alone or to every item of
298/// its subtree.
299enum KeyHandler {
300    Item(fn(&TreeViewItem) -> bool),
301    Subtree(fn(&TreeViewItem) -> bool),
302}
303
304impl KeyHandler {
305    // NOTE: the subtree handlers do not use the expand/collapse subtree
306    // functions of the tree view because we want to know if any items were
307    // in fact expanded to set the event handled status. Also the handling
308    // here avoids a potential infinite recursion/stack overflow.
309    fn invoke(&self, tree_view_item: &TreeViewItem) -> bool {
310        match self {
311            KeyHandler::Item(f) => f(tree_view_item),
312            KeyHandler::Subtree(f) => {
313                // All the items are enumerated before applying the function.
314                // This avoids a potential infinite loop if there is an
315                // infinite tree (a lazily infinite tree). But it also means
316                // a lazily loaded tree will not be expanded completely.
317                let mut items = Vec::new();
318                sub_tree(&tree_view_item.to_ref(), &mut items);
319
320                let mut result = false;
321                for item in &items {
322                    let applied = f(item);
323                    result = result || applied;
324                }
325                result
326            }
327        }
328    }
329}
330
331fn apply_to_item_or_recursively_if_ctrl(f: fn(&TreeViewItem) -> bool, key_modifiers: KeyModifiers) -> KeyHandler {
332    if key_modifiers.contains(KeyModifiers::CONTROL) {
333        return KeyHandler::Subtree(f);
334    }
335
336    KeyHandler::Item(f)
337}
338
339fn expand_item(tree_view_item: &TreeViewItem) -> bool {
340    if tree_view_item.item_count() > 0 && !tree_view_item.is_expanded() {
341        tree_view_item.set_current_value(TreeViewItem::is_expanded_property(), true);
342        return true;
343    }
344
345    false
346}
347
348fn collapse_item(tree_view_item: &TreeViewItem) -> bool {
349    if tree_view_item.item_count() > 0 && tree_view_item.is_expanded() {
350        tree_view_item.set_current_value(TreeViewItem::is_expanded_property(), false);
351        return true;
352    }
353
354    false
355}
356
357fn focus_aware_collapse_item(tree_view_item: &TreeViewItem) -> bool {
358    if tree_view_item.item_count() > 0 && tree_view_item.is_expanded() {
359        if tree_view_item.is_focused() {
360            tree_view_item.set_current_value(TreeViewItem::is_expanded_property(), false);
361        } else {
362            tree_view_item.focus_with(NavigationMethod::Directional, KeyModifiers::NONE);
363        }
364
365        return true;
366    }
367
368    false
369}
370
371fn sub_tree(tree_view_item: &Ref<TreeViewItem>, result: &mut Vec<Ref<TreeViewItem>>) {
372    result.push(tree_view_item.clone());
373
374    for child in StyledElement::logical_children(tree_view_item).snapshot().iter() {
375        if let Some(child) = child.clone().cast::<TreeViewItem>() {
376            sub_tree(&child, result);
377        }
378    }
379}
380
381impl TreeViewItem {
382    /// The named parts expected in the control template.
383    pub const TEMPLATE_PARTS: &'static [TemplatePartAttribute] =
384        &[TemplatePartAttribute::new("PART_Header", <Control as StaticType>::TYPE)];
385
386    /// The pseudoclasses set by the class.
387    pub const PSEUDO_CLASSES: PseudoClassesAttribute = PseudoClassesAttribute::new(&[":pressed", ":selected"]);
388}
389
390ferroui_base::ferro_properties! { impl TreeViewItem {
391    ferro_property!(
392        /// Defines the `IsExpanded` property.
393        pub fn is_expanded_property() -> StyledProperty<bool> {
394            FerroProperty::register_with::<TreeViewItem, _>(
395                "IsExpanded",
396                StyledPropertyOptions::new(false).default_binding_mode(BindingMode::TwoWay),
397            )
398        }
399    );
400
401    ferro_property!(
402        /// Defines the `IsSelected` property.
403        pub fn is_selected_property() -> StyledProperty<bool> {
404            SelectingItemsControl::is_selected_property().add_owner::<TreeViewItem>()
405        }
406    );
407
408    ferro_property!(
409        /// Defines the `Level` property.
410        pub fn level_property() -> DirectProperty<TreeViewItem, i32> {
411            FerroProperty::register_direct::<TreeViewItem, _>("Level", |o| o.level(), None, 0)
412        }
413    );
414} }
415
416impl TreeViewItem {
417    ferro_routed_event!(
418        /// Defines the `Expanded` event.
419        pub fn expanded_event() -> RoutedEvent<RoutedEventArgs> {
420            RoutedEvent::register::<TreeViewItem, _>("Expanded", RoutingStrategies::BUBBLE | RoutingStrategies::TUNNEL)
421        }
422    );
423
424    ferro_routed_event!(
425        /// Defines the `Collapsed` event.
426        pub fn collapsed_event() -> RoutedEvent<RoutedEventArgs> {
427            RoutedEvent::register::<TreeViewItem, _>(
428                "Collapsed",
429                RoutingStrategies::BUBBLE | RoutingStrategies::TUNNEL,
430            )
431        }
432    );
433
434    pub(crate) fn static_constructor() {
435        register_selectable::<TreeViewItem>();
436        SelectableMixin::attach::<TreeViewItem>(Self::is_selected_property());
437        PressedMixin::attach::<TreeViewItem>();
438        InputElement::focusable_property().override_default_value::<TreeViewItem>(true);
439        let default_panel: Rc<dyn ITemplateOf<Option<Ref<Panel>>>> =
440            FuncTemplate::new(|| Some(StackPanel::new().upcast::<Panel>()));
441        ItemsControl::items_panel_property().override_default_value::<TreeViewItem>(default_panel);
442        crate::platform::PlatformFeedback::feedback_type_property()
443            .override_default_value::<TreeViewItem>(crate::platform::FeedbackType::Auto);
444        crate::automation::AutomationProperties::is_offscreen_behavior_property()
445            .override_default_value::<TreeViewItem>(crate::automation::IsOffscreenBehavior::FromClip);
446        Control::request_bring_into_view_event()
447            .add_class_handler::<TreeViewItem>(|x, e| x.on_request_bring_into_view(e));
448        Self::is_expanded_property().changed().add_class_handler::<TreeViewItem>(|x, e| x.on_is_expanded_changed(e));
449    }
450
451    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
452    pub fn construct() -> Self {
453        Self {
454            base: HeaderedItemsControl::construct(),
455            tree_view: RefCell::new(None),
456            header: RefCell::new(None),
457            header_presenter: RefCell::new(None),
458            header_double_tapped_token: Cell::new(None),
459            level: Cell::new(0),
460            template_applied: Cell::new(false),
461            deferred_bring_into_view_flag: Cell::new(false),
462            items_panel_pointer_pressed: Cell::new(false),
463        }
464    }
465
466    pub fn new() -> Ref<Self> {
467        instantiate(Self::construct())
468    }
469
470    fn on_is_expanded_changed(&self, args: &FerroPropertyChangedEventArgs<'_>) {
471        let routed_event = if args.get_new_value::<bool>() { Self::expanded_event() } else { Self::collapsed_event() };
472        let event_args = RoutedEventArgs::with_event_and_source(routed_event, self.to_ref());
473        self.raise_event(&event_args);
474    }
475
476    /// Gets or sets a value indicating whether the item is expanded to show
477    /// its children.
478    pub fn is_expanded(&self) -> bool {
479        self.get_value(Self::is_expanded_property())
480    }
481
482    pub fn set_is_expanded(&self, value: bool) {
483        self.set_value(Self::is_expanded_property(), value)
484    }
485
486    /// Gets or sets the selection state of the item.
487    pub fn is_selected(&self) -> bool {
488        self.get_value(Self::is_selected_property())
489    }
490
491    pub fn set_is_selected(&self, value: bool) {
492        self.set_value(Self::is_selected_property(), value)
493    }
494
495    /// Gets the level/indentation of the item.
496    pub fn level(&self) -> i32 {
497        self.level.get()
498    }
499
500    fn set_level(&self, value: i32) {
501        self.set_and_raise_cell(Self::level_property(), &self.level, value);
502    }
503
504    /// Occurs after the [`TreeViewItem`] has expanded to show its children.
505    pub fn expanded(
506        &self,
507        handler: impl Fn(&ferroui_base::interactivity::Interactive, &RoutedEventArgs) + 'static,
508    ) -> RoutedEventHandlerToken {
509        self.add_handler(Self::expanded_event(), handler)
510    }
511
512    /// Occurs after the [`TreeViewItem`] has collapsed to hide its children.
513    pub fn collapsed(
514        &self,
515        handler: impl Fn(&ferroui_base::interactivity::Interactive, &RoutedEventArgs) + 'static,
516    ) -> RoutedEventHandlerToken {
517        self.add_handler(Self::collapsed_event(), handler)
518    }
519
520    pub(crate) fn tree_view_owner(&self) -> Option<Ref<TreeView>> {
521        self.tree_view.borrow().as_ref().and_then(WeakRef::upgrade)
522    }
523
524    fn get_header_target_size(header: &Control) -> Size {
525        // Use the desired width, not the width of the bounds: the latter is
526        // stretched to the full width of the tree view's extent, preventing
527        // scrolling, whereas the desired size is what the header actually
528        // needs.
529        let margin = header.margin();
530        let desired_width = header.desired_size().width - margin.left - margin.right;
531        let bounds_size = header.bounds().size();
532
533        bounds_size.with_width(desired_width.clamp(0.0, bounds_size.width))
534    }
535
536    /// Due to the implicit pointer capture, the items panel receives no
537    /// events if swiped onto. So we need to perform our own hit test to
538    /// reliably determine whether the mouse event occurred over it.
539    fn is_items_panel_event(&self, e: &PointerEventArgs) -> bool {
540        match self.items_panel_root() {
541            Some(items_panel_root) => {
542                items_panel_root.input_hit_test(e.get_position(Some(&items_panel_root))).is_some()
543            }
544            None => false,
545        }
546    }
547
548    fn calculate_distance_from_logical_parent<T: ferroui_base::ObjectType>(
549        logical: Option<Ref<StyledElement>>,
550        default: i32,
551    ) -> i32 {
552        let mut result = 0;
553        let mut logical = logical;
554
555        while let Some(current) = logical.take() {
556            if current.is::<T>() {
557                return result;
558            }
559
560            result += 1;
561            logical = current.parent();
562        }
563
564        default
565    }
566
567    fn ensure_tree_view(&self) -> Ref<TreeView> {
568        match self.tree_view_owner() {
569            Some(tree_view) => tree_view,
570            None => panic!("The TreeViewItem is not part of a TreeView."),
571        }
572    }
573
574    fn header_double_tapped(&self, e: &TappedEventArgs) {
575        self.on_header_double_tapped(e);
576    }
577}