ferroui_diagnostics/diagnostics/views/
tree_page_view.rs1use 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#[repr(C)]
33pub struct TreePageView {
34 base: UserControl,
35 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 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 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 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 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 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 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 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 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 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 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 *self.hovered.borrow_mut() = None;
239 }
240
241 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 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 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 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 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}