Skip to main content

ferroui_diagnostics/diagnostics/views/
tree_page_view.rs

1//! Port of `Diagnostics/Views/TreePageView.xaml.cs`: the class of the
2//! document `TreePageView.xaml`.
3//!
4//! The tree of the document is a TreeDataGrid, where upstream has a tree view
5//! (docs/porting/DEVIATIONS.md, Developer tools).
6
7use crate::diagnostics::controls::ControlHighlightAdorner;
8use crate::diagnostics::view_models::{TreeNode, TreePageViewModel};
9use crate::diagnostics::DevToolsDataFormats;
10use ferroui_base::data::model::INotifyPropertyChanged;
11use ferroui_base::input::{
12    DataTransfer, DataTransferItem, IAsyncDataTransfer, InputElementImpl, PointerEventArgs, TappedEventArgs,
13};
14use ferroui_base::interactivity::{IRoutedEventArgs, InteractiveImpl};
15use ferroui_base::layout::LayoutableImpl;
16use ferroui_base::metadata::from_markup_value;
17use ferroui_base::reactive::IDisposable;
18use ferroui_base::threading::{Dispatcher, DispatcherPriority};
19use ferroui_base::{
20    ferro_class, ferro_class_info, ferro_impl_classes, instantiate, BoxedValue, FerroObjectImpl, FerroObjectImplExt,
21    FerroPropertyChangedEventArgs, Ref, StyledElement, StyledElementImpl, Visual, VisualImpl,
22};
23use ferroui_controls::primitives::{TemplatedControlImpl, ToggleButton};
24use ferroui_controls::{ContentControlImpl, Control, ControlImpl, TopLevel, UserControl};
25use ferroui_controls_treedatagrid::TreeDataGrid;
26use std::cell::RefCell;
27use std::rc::Rc;
28
29/// The view of a tree page of the developer tools: the tree of the nodes,
30/// with a highlight of the control of the node under the pointer, and the
31/// details of the control of the selected node.
32#[repr(C)]
33pub struct TreePageView {
34    base: UserControl,
35    // Deviation (DEVIATIONS.md, Developer tools): the node of the row under the pointer, where
36    // upstream keeps the item container of the tree view (the rows of the TreeDataGrid are
37    // recycled for other nodes as the grid scrolls).
38    hovered: RefCell<Option<Rc<TreeNode>>>,
39    tree: RefCell<Option<Ref<TreeDataGrid>>>,
40    adorner: RefCell<Option<Rc<dyn IDisposable>>>,
41    clipboard_copy_requested: RefCell<Option<Rc<dyn IDisposable>>>,
42    selected_node_changed: RefCell<Option<(Rc<TreePageViewModel>, u64)>>,
43}
44
45ferro_class!(TreePageView: UserControl);
46ferro_impl_classes!(
47    TreePageView: StyledElementImpl,
48    VisualImpl,
49    LayoutableImpl,
50    InteractiveImpl,
51    InputElementImpl,
52    ControlImpl,
53    TemplatedControlImpl,
54    ContentControlImpl
55);
56ferro_class_info!(TreePageView {
57    new: TreePageView::new,
58    markup: {
59        methods: [
60            fn UpdateAdorner(Option<BoxedValue>, Rc<dyn IRoutedEventArgs>) =>
61                |this: &Ref<TreePageView>, sender: Option<BoxedValue>, e: Rc<dyn IRoutedEventArgs>| {
62                    if let Some(e) = e.downcast_ref::<PointerEventArgs>() {
63                        this.update_adorner(&sender, e)
64                    }
65                },
66            fn RemoveAdorner(Option<BoxedValue>, Rc<dyn IRoutedEventArgs>) =>
67                |this: &Ref<TreePageView>, sender: Option<BoxedValue>, e: Rc<dyn IRoutedEventArgs>| {
68                    if let Some(e) = e.downcast_ref::<PointerEventArgs>() {
69                        this.remove_adorner(&sender, e)
70                    }
71                },
72            fn ToggleExpanded(Option<BoxedValue>, Rc<dyn IRoutedEventArgs>) =>
73                |this: &Ref<TreePageView>, sender: Option<BoxedValue>, e: Rc<dyn IRoutedEventArgs>| {
74                    if let Some(e) = e.downcast_ref::<TappedEventArgs>() {
75                        this.toggle_expanded(&sender, e)
76                    }
77                },
78        ],
79    },
80});
81
82impl FerroObjectImpl for TreePageView {
83    fn on_property_changed(this: &Self, change: &FerroPropertyChangedEventArgs<'_>) {
84        Self::parent_on_property_changed(this, change);
85
86        if change.property() == StyledElement::data_context_property().as_property() {
87            // `oldViewModel.ClipboardCopyRequested -= OnClipboardCopyRequested`: the handler was
88            // added to the old view model, through the subscription kept here.
89            let old_view_model = change
90                .get_old_value::<Option<BoxedValue>>()
91                .flatten()
92                .and_then(|old| from_markup_value::<Rc<TreePageViewModel>>(&Some(old)));
93            if old_view_model.is_some() {
94                let subscription = this.clipboard_copy_requested.borrow_mut().take();
95                if let Some(subscription) = subscription {
96                    subscription.dispose();
97                }
98            }
99
100            // Deviation (DEVIATIONS.md, Developer tools): the handler of the changes of the
101            // selected node, which brings its row into view, is removed from the old view model.
102            let selected_node_changed = this.selected_node_changed.borrow_mut().take();
103            if let Some((old_view_model, token)) = selected_node_changed {
104                old_view_model.property_changed().remove(token);
105            }
106
107            let new_view_model = from_markup_value::<Rc<TreePageViewModel>>(&change.get_new_value::<Option<BoxedValue>>());
108            if let Some(new_view_model) = new_view_model {
109                let weak = this.to_ref().downgrade();
110                let subscription = new_view_model.clipboard_copy_requested(move |selector: &str| {
111                    if let Some(this) = weak.upgrade() {
112                        this.on_clipboard_copy_requested(selector);
113                    }
114                });
115                *this.clipboard_copy_requested.borrow_mut() = Some(subscription);
116
117                // Deviation (DEVIATIONS.md, Developer tools): the row of a node the view model
118                // selects is brought into view (the source has selected it and expanded the rows
119                // of its ancestors), once the grid has its rows.
120                let weak = this.to_ref().downgrade();
121                let token = new_view_model.property_changed().add(Rc::new(move |property_name: &str| {
122                    if property_name != "SelectedNode" {
123                        return;
124                    }
125                    let weak = weak.clone();
126                    Dispatcher::ui_thread().post_local(
127                        move || {
128                            if let Some(this) = weak.upgrade() {
129                                this.bring_selected_row_into_view();
130                            }
131                        },
132                        DispatcherPriority::default(),
133                    );
134                }));
135                *this.selected_node_changed.borrow_mut() = Some((new_view_model, token));
136            }
137        }
138    }
139}
140
141impl TreePageView {
142    /// The URI of the document of the class.
143    pub const DOCUMENT_URI: &'static str = "ferres://FerroUI.Diagnostics/diagnostics/views/TreePageView.xaml";
144
145    pub fn construct() -> Self {
146        Self {
147            base: UserControl::construct(),
148            hovered: RefCell::new(None),
149            tree: RefCell::new(None),
150            adorner: RefCell::new(None),
151            clipboard_copy_requested: RefCell::new(None),
152            selected_node_changed: RefCell::new(None),
153        }
154    }
155
156    /// Creates the view and populates it from its document
157    /// (`InitializeComponent()`: the compiled markup of the document, which
158    /// the build of the crate generates).
159    ///
160    /// # Panics
161    /// Panics when the document fails to load (an exception of the
162    /// constructor in the original).
163    pub fn new() -> Ref<Self> {
164        crate::register_types();
165        let this = instantiate(Self::construct());
166        Self::initialize_component(&this);
167        *this.tree.borrow_mut() = Some(this.get_control::<TreeDataGrid>("tree"));
168        this
169    }
170
171    /// `InitializeComponent()`.
172    fn initialize_component(this: &Ref<Self>) {
173        if let Err(error) = crate::compiled_diagnostics_views_tree_page_view::populate(None, this) {
174            panic!("{}: {error}", Self::DOCUMENT_URI);
175        }
176    }
177
178    /// The node of the row of the grid that is `control` or contains it.
179    ///
180    /// Deviation (DEVIATIONS.md, Developer tools): the nodes are the rows of a TreeDataGrid, not
181    /// the items of a tree view.
182    fn node_of(&self, control: &Control) -> Option<Rc<TreeNode>> {
183        let tree = self.tree.borrow().clone()?;
184        tree.try_get_row_model::<TreeNode>(control)
185    }
186
187    /// `UpdateAdorner`: highlights the control of the node under the
188    /// pointer.
189    pub fn update_adorner(&self, _sender: &Option<BoxedValue>, e: &PointerEventArgs) {
190        let Some(source) = e.source().and_then(|source| source.cast::<Control>()) else {
191            return;
192        };
193
194        // Deviation (DEVIATIONS.md, Developer tools): the node of the row of the TreeDataGrid under
195        // the pointer, where upstream finds the item container of the tree view (and its owner).
196        let node = self.node_of(&source);
197        let same = match (&node, &*self.hovered.borrow()) {
198            (Some(node), Some(hovered)) => Rc::ptr_eq(node, hovered),
199            (None, None) => true,
200            _ => false,
201        };
202        if same {
203            return;
204        }
205
206        let adorner = self.adorner.borrow().clone();
207        if let Some(adorner) = adorner {
208            adorner.dispose();
209        }
210
211        let Some(node) = node else {
212            *self.hovered.borrow_mut() = None;
213            return;
214        };
215
216        *self.hovered.borrow_mut() = Some(node.clone());
217
218        let visual = node.visual().cast::<Visual>();
219        let should_visualize_margin_padding = from_markup_value::<Rc<TreePageViewModel>>(&self.data_context())
220            .and_then(|view_model| view_model.main_view())
221            .map(|main_view| main_view.should_visualize_margin_padding());
222        let (Some(visual), Some(should_visualize_margin_padding)) = (visual, should_visualize_margin_padding) else {
223            return;
224        };
225
226        *self.adorner.borrow_mut() = ControlHighlightAdorner::add_visual(&visual, should_visualize_margin_padding);
227    }
228
229    /// `RemoveAdorner`: removes the highlight when the pointer leaves the
230    /// tree.
231    fn remove_adorner(&self, _sender: &Option<BoxedValue>, _e: &PointerEventArgs) {
232        let adorner = self.adorner.borrow_mut().take();
233        if let Some(adorner) = adorner {
234            adorner.dispose();
235        }
236        // Deviation (DEVIATIONS.md, Developer tools): the row the pointer enters next is
237        // highlighted again (upstream keeps the item container, whose highlight it has removed).
238        *self.hovered.borrow_mut() = None;
239    }
240
241    /// Double tapping a node expands or collapses it, as double tapping the header of an item of
242    /// a tree view does; the expander of the row toggles it by itself.
243    ///
244    /// Deviation (DEVIATIONS.md, Developer tools): the nodes are the rows of a TreeDataGrid, which
245    /// has no such gesture.
246    fn toggle_expanded(&self, _sender: &Option<BoxedValue>, e: &TappedEventArgs) {
247        if e.handled() {
248            return;
249        }
250        let Some(source) = e.source().and_then(|source| source.cast::<Control>()) else {
251            return;
252        };
253        if source.find_ancestor_of_type::<ToggleButton>(true).is_some() {
254            return;
255        }
256        if let Some(node) = self.node_of(&source) {
257            if node.children().count() > 0 {
258                node.set_is_expanded(!node.is_expanded());
259                e.set_handled(true);
260            }
261        }
262    }
263
264    /// Brings the row of the selected node into view.
265    ///
266    /// Deviation (DEVIATIONS.md, Developer tools): the row of a node may not exist until the view
267    /// model selects it; upstream's tree view selects the item of a node only when it is shown.
268    fn bring_selected_row_into_view(&self) {
269        let Some(view_model) = from_markup_value::<Rc<TreePageViewModel>>(&self.data_context()) else {
270            return;
271        };
272        let row_index = view_model.selected_row_index();
273        if row_index < 0 {
274            return;
275        }
276        let tree = self.tree.borrow().clone();
277        if let Some(rows_presenter) = tree.and_then(|tree| tree.rows_presenter()) {
278            rows_presenter.bring_into_view(row_index, None);
279        }
280    }
281
282    /// Copies a selector to the clipboard: as text, without the parts in
283    /// braces, and in the format of selectors of the developer tools.
284    fn on_clipboard_copy_requested(&self, selector: &str) {
285        let Some(clipboard) = TopLevel::get_top_level(Some(self)).and_then(|top_level| top_level.clipboard()) else {
286            return;
287        };
288
289        let data_transfer_item = DataTransferItem::new();
290        let text = Self::to_text(selector);
291        data_transfer_item.set_text(Some(text.as_str()));
292        data_transfer_item.set(&DevToolsDataFormats::selector(), Some(selector.to_string()));
293
294        let data_transfer = DataTransfer::new();
295        data_transfer.add(data_transfer_item);
296        let data_transfer: Rc<dyn IAsyncDataTransfer> = data_transfer;
297
298        // The original does not await the task.
299        let _ = Dispatcher::ui_thread().invoke_async_task_local(move || async move {
300            let _ = clipboard.set_data_async(Some(data_transfer)).await;
301        });
302    }
303
304    /// The text of a selector: the selector without the parts in braces
305    /// (the assemblies of the types).
306    pub(crate) fn to_text(text: &str) -> String {
307        let mut sb = String::new();
308        let mut buffer_start_index: Option<usize> = None;
309        for c in text.chars() {
310            match c {
311                '{' => {
312                    buffer_start_index = Some(sb.len());
313                }
314                '}' if buffer_start_index.is_some() => {
315                    let start = buffer_start_index.unwrap_or_default();
316                    sb.truncate(start);
317                    buffer_start_index = Some(sb.len());
318                }
319                _ => sb.push(c),
320            }
321        }
322        sb
323    }
324}