makepad_widgets/
file_tree.rs

1use {
2    std::{
3        collections::{HashSet},
4    },
5    crate::{
6        makepad_derive_widget::*,
7        check_box::*,
8        makepad_draw::*,
9        widget::*,
10        scroll_shadow::DrawScrollShadow,
11        scroll_bars::ScrollBars
12    }
13};
14
15live_design!{
16    DrawBgQuad = {{DrawBgQuad}} {}
17    DrawNameText = {{DrawNameText}} {}
18    DrawIconQuad = {{DrawIconQuad}} {}
19    FileTreeNodeBase = {{FileTreeNode}} {}
20    FileTreeBase = {{FileTree}} {}
21}
22
23// TODO support a shared 'inputs' struct on drawshaders
24#[derive(Live, LiveHook)]#[repr(C)]
25struct DrawBgQuad {
26    #[deref] draw_super: DrawQuad,
27    #[live] is_even: f32,
28    #[live] scale: f32,
29    #[live] is_folder: f32,
30    #[live] focussed: f32,
31    #[live] selected: f32,
32    #[live] hover: f32,
33    #[live] opened: f32,
34}
35
36#[derive(Live, LiveHook)]#[repr(C)]
37struct DrawNameText {
38    #[deref] draw_super: DrawText,
39    #[live] is_even: f32,
40    #[live] scale: f32,
41    #[live] is_folder: f32,
42    #[live] focussed: f32,
43    #[live] selected: f32,
44    #[live] hover: f32,
45    #[live] opened: f32,
46}
47
48#[derive(Live, LiveHook)]#[repr(C)]
49struct DrawIconQuad {
50    #[deref] draw_super: DrawQuad,
51    #[live] is_even: f32,
52    #[live] scale: f32,
53    #[live] is_folder: f32,
54    #[live] focussed: f32,
55    #[live] selected: f32,
56    #[live] hover: f32,
57    #[live] opened: f32,
58}
59
60#[derive(Live, LiveHook)]
61pub struct FileTreeNode {
62    #[live] draw_bg: DrawBgQuad,
63    #[live] draw_icon: DrawIconQuad,
64    #[live] draw_name: DrawNameText,
65    #[live] check_box: CheckBox,
66    #[layout] layout: Layout,
67    
68    #[animator] animator: Animator,
69    
70    #[live] indent_width: f64,
71    
72    #[live] icon_walk: Walk,
73    
74    #[live] is_folder: bool,
75    #[live] min_drag_distance: f64,
76    
77    #[live] opened: f32,
78    #[live] focussed: f32,
79    #[live] hover: f32,
80    #[live] selected: f32,
81}
82
83#[derive(Live)]
84pub struct FileTree {
85    #[live] scroll_bars: ScrollBars,
86    #[live] file_node: Option<LivePtr>,
87    #[live] folder_node: Option<LivePtr>,
88    #[walk] walk: Walk,
89    #[layout] layout: Layout,
90    #[live] filler: DrawBgQuad,
91    
92    #[live] node_height: f64,
93    
94    #[live] draw_scroll_shadow: DrawScrollShadow,
95    
96    #[rust] draw_state: DrawStateWrap<()>,
97    
98    #[rust] dragging_node_id: Option<FileNodeId>,
99    #[rust] selected_node_id: Option<FileNodeId>,
100    #[rust] open_nodes: HashSet<FileNodeId>,
101    
102    #[rust] tree_nodes: ComponentMap<FileNodeId, (FileTreeNode, LiveId)>,
103    
104    #[rust] count: usize,
105    #[rust] stack: Vec<f64>,
106}
107
108impl LiveHook for FileTree {
109    fn before_live_design(cx:&mut Cx){
110        register_widget!(cx, FileTree)
111    }
112
113    fn after_apply(&mut self, cx: &mut Cx, from: ApplyFrom, index: usize, nodes: &[LiveNode]) {
114        for (_, (tree_node, id)) in self.tree_nodes.iter_mut() {
115            if let Some(index) = nodes.child_by_name(index, id.as_field()) {
116                tree_node.apply(cx, from, index, nodes);
117            }
118        }
119        self.scroll_bars.redraw(cx);
120    }
121}
122
123#[derive(Clone, WidgetAction)]
124pub enum FileTreeAction {
125    None,
126    FileClicked(FileNodeId),
127    FolderClicked(FileNodeId),
128    ShouldFileStartDrag(FileNodeId),
129}
130
131pub enum FileTreeNodeAction {
132    None,
133    WasClicked,
134    Opening,
135    Closing,
136    ShouldStartDrag
137}
138
139impl FileTreeNode {
140    pub fn set_draw_state(&mut self, is_even: f32, scale: f64) {
141        self.draw_bg.scale = scale as f32;
142        self.draw_bg.is_even = is_even;
143        self.draw_name.scale = scale as f32;
144        self.draw_name.is_even = is_even;
145        self.draw_icon.scale = scale as f32;
146        self.draw_icon.is_even = is_even;
147        self.draw_name.font_scale = scale;
148    }
149    
150    pub fn draw_folder(&mut self, cx: &mut Cx2d, name: &str, is_even: f32, node_height: f64, depth: usize, scale: f64) {
151        self.set_draw_state(is_even, scale);
152        
153        self.draw_bg.begin(cx, Walk::size(Size::Fill, Size::Fixed(scale * node_height)), self.layout);
154        
155        cx.walk_turtle(self.indent_walk(depth));
156        
157        self.draw_icon.draw_walk(cx, self.icon_walk);
158        
159        self.draw_name.draw_walk(cx, Walk::fit(), Align::default(), name);
160        self.draw_bg.end(cx);
161    }
162    
163    pub fn draw_file(&mut self, cx: &mut Cx2d, name: &str, is_even: f32, node_height: f64, depth: usize, scale: f64) {
164        self.set_draw_state(is_even, scale);
165        
166        self.draw_bg.begin(cx, Walk::size(Size::Fill, Size::Fixed(scale * node_height)), self.layout);
167        
168        cx.walk_turtle(self.indent_walk(depth));
169        
170        self.draw_name.draw_walk(cx, Walk::fit(), Align::default(), name);
171        self.draw_bg.end(cx);
172    }
173    
174    fn indent_walk(&self, depth: usize) -> Walk {
175        Walk {
176            abs_pos: None,
177            width: Size::Fixed(depth as f64 * self.indent_width),
178            height: Size::Fixed(0.0),
179            margin: Margin {
180                left: depth as f64 * 1.0,
181                top: 0.0,
182                right: depth as f64 * 4.0,
183                bottom: 0.0,
184            },
185        }
186    }
187    
188    fn set_is_selected(&mut self, cx: &mut Cx, is: bool, animate: Animate) {
189        self.animator_toggle(cx, is, animate, id!(select.on), id!(select.off))
190    }
191    
192    fn set_is_focussed(&mut self, cx: &mut Cx, is: bool, animate: Animate) {
193        self.animator_toggle(cx, is, animate, id!(focus.on), id!(focus.off))
194    }
195    
196    pub fn set_folder_is_open(&mut self, cx: &mut Cx, is: bool, animate: Animate) {
197        self.animator_toggle(cx, is, animate, id!(open.on), id!(open.off));
198    }
199    
200    pub fn handle_event_with(
201        &mut self,
202        cx: &mut Cx,
203        event: &Event,
204        dispatch_action: &mut dyn FnMut(&mut Cx, FileTreeNodeAction),
205    ) {
206        if self.animator_handle_event(cx, event).must_redraw() {
207            self.draw_bg.redraw(cx);
208        }
209        match event.hits(cx, self.draw_bg.area()) {
210            Hit::FingerHoverIn(_) => {
211                self.animator_play(cx, id!(hover.on));
212            }
213            Hit::FingerHoverOut(_) => {
214                self.animator_play(cx, id!(hover.off));
215            }
216            Hit::FingerMove(f) => {
217                if f.abs.distance(&f.abs_start) >= self.min_drag_distance {
218                    dispatch_action(cx, FileTreeNodeAction::ShouldStartDrag);
219                }
220            }
221            Hit::FingerDown(_) => {
222                self.animator_play(cx, id!(select.on));
223                if self.is_folder {
224                    if self.animator_in_state(cx, id!(open.on)) {
225                        self.animator_play(cx, id!(open.off));
226                        dispatch_action(cx, FileTreeNodeAction::Closing);
227                    }
228                    else {
229                        self.animator_play(cx, id!(open.on));
230                        dispatch_action(cx, FileTreeNodeAction::Opening);
231                    }
232                    
233                }
234                dispatch_action(cx, FileTreeNodeAction::WasClicked);
235            }
236            _ => {}
237        }
238    }
239}
240
241
242impl FileTree {
243    
244    pub fn begin(&mut self, cx: &mut Cx2d, walk: Walk) {
245        self.scroll_bars.begin(cx, walk, self.layout);
246        self.count = 0;
247    }
248    
249    pub fn end(&mut self, cx: &mut Cx2d) {
250        // lets fill the space left with blanks
251        let height_left = cx.turtle().height_left();
252        let mut walk = 0.0;
253        while walk < height_left {
254            self.count += 1;
255            self.filler.is_even = Self::is_even(self.count);
256            self.filler.draw_walk(cx, Walk::size(Size::Fill, Size::Fixed(self.node_height.min(height_left - walk))));
257            walk += self.node_height.max(1.0);
258        }
259        
260        self.draw_scroll_shadow.draw(cx, dvec2(0., 0.));
261        self.scroll_bars.end(cx);
262        
263        let selected_node_id = self.selected_node_id;
264        self.tree_nodes.retain_visible_and( | node_id, _ | Some(*node_id) == selected_node_id);
265    }
266    
267    pub fn is_even(count: usize) -> f32 {
268        if count % 2 == 1 {0.0}else {1.0}
269    }
270    
271    pub fn should_node_draw(&mut self, cx: &mut Cx2d) -> bool {
272        let scale = self.stack.last().cloned().unwrap_or(1.0);
273        let height = self.node_height * scale;
274        let walk = Walk::size(Size::Fill, Size::Fixed(height));
275        if scale > 0.01 && cx.walk_turtle_would_be_visible(walk) {
276            return true
277        }
278        else {
279            cx.walk_turtle(walk);
280            return false
281        }
282    }
283    
284    pub fn begin_folder(
285        &mut self,
286        cx: &mut Cx2d,
287        node_id: FileNodeId,
288        name: &str,
289    ) -> Result<(), ()> {
290        let scale = self.stack.last().cloned().unwrap_or(1.0);
291        
292        if scale > 0.2 {
293            self.count += 1;
294        }
295        
296        let is_open = self.open_nodes.contains(&node_id);
297        
298        if self.should_node_draw(cx) {
299            let folder_node = self.folder_node;
300            let (tree_node, _) = self.tree_nodes.get_or_insert(cx, node_id, | cx | {
301                let mut tree_node = FileTreeNode::new_from_ptr(cx, folder_node);
302                if is_open {
303                    tree_node.set_folder_is_open(cx, true, Animate::No)
304                }
305                (tree_node, live_id!(folder_node))
306            });
307            
308            tree_node.draw_folder(cx, name, Self::is_even(self.count), self.node_height, self.stack.len(), scale);
309            self.stack.push(tree_node.opened as f64 * scale);
310            if tree_node.opened == 0.0 {
311                self.end_folder();
312                return Err(());
313            }
314        }
315        else {
316            if is_open {
317                self.stack.push(scale * 1.0);
318            }
319            else {
320                return Err(());
321            }
322        }
323        Ok(())
324    }
325    
326    pub fn end_folder(&mut self) {
327        self.stack.pop();
328    }
329    
330    pub fn file(&mut self, cx: &mut Cx2d, node_id: FileNodeId, name: &str) {
331        let scale = self.stack.last().cloned().unwrap_or(1.0);
332        
333        if scale > 0.2 {
334            self.count += 1;
335        }
336        if self.should_node_draw(cx) {
337            let file_node = self.file_node;
338            let (tree_node, _) = self.tree_nodes.get_or_insert(cx, node_id, | cx | {
339                (FileTreeNode::new_from_ptr(cx, file_node), live_id!(file_node))
340            });
341            tree_node.draw_file(cx, name, Self::is_even(self.count), self.node_height, self.stack.len(), scale);
342        }
343    }
344    
345    pub fn forget(&mut self) {
346        self.tree_nodes.clear();
347    }
348    
349    pub fn forget_node(&mut self, file_node_id: FileNodeId) {
350        self.tree_nodes.remove(&file_node_id);
351    }
352    
353    pub fn is_folder(&mut self, file_node_id: FileNodeId)->bool {
354        if let Some((node,_)) = self.tree_nodes.get(&file_node_id){
355            node.is_folder
356        }
357        else{
358            false
359        }
360    }
361    
362    pub fn set_folder_is_open(
363        &mut self,
364        cx: &mut Cx,
365        node_id: FileNodeId,
366        is_open: bool,
367        animate: Animate,
368    ) {
369        if is_open {
370            self.open_nodes.insert(node_id);
371        }
372        else {
373            self.open_nodes.remove(&node_id);
374        }
375        if let Some((tree_node, _)) = self.tree_nodes.get_mut(&node_id) {
376            tree_node.set_folder_is_open(cx, is_open, animate);
377        }
378    }
379    
380    pub fn start_dragging_file_node(
381        &mut self,
382        cx: &mut Cx,
383        node_id: FileNodeId,
384        items: Vec<DragItem>,
385    ) {
386        self.dragging_node_id = Some(node_id);
387
388        log!("makepad: start_dragging_file_node");
389
390        cx.start_dragging(items);
391    }
392    
393    pub fn handle_event(&mut self, cx: &mut Cx, event: &Event) -> Vec<FileTreeAction> {
394        let mut a = Vec::new();
395        self.handle_event_with(cx, event, &mut | _, v | a.push(v));
396        a
397    }
398    
399    pub fn handle_event_with(
400        &mut self,
401        cx: &mut Cx,
402        event: &Event,
403        dispatch_action: &mut dyn FnMut(&mut Cx, FileTreeAction),
404    ) {
405        self.scroll_bars.handle_event_with(cx, event, &mut | _, _ | {});
406        
407        match event {
408            Event::DragEnd => self.dragging_node_id = None,
409            _ => ()
410        }
411        
412        let mut actions = Vec::new();
413        for (node_id, (node, _)) in self.tree_nodes.iter_mut() {
414            node.handle_event_with(cx, event, &mut | _, e | actions.push((*node_id, e)));
415        }
416        
417        for (node_id, action) in actions {
418            match action {
419                FileTreeNodeAction::Opening => {
420                    self.open_nodes.insert(node_id);
421                }
422                FileTreeNodeAction::Closing => {
423                    self.open_nodes.remove(&node_id);
424                }
425                FileTreeNodeAction::WasClicked => {
426                    cx.set_key_focus(self.scroll_bars.area());
427                    if let Some(last_selected) = self.selected_node_id {
428                        if last_selected != node_id {
429                            self.tree_nodes.get_mut(&last_selected).unwrap().0.set_is_selected(cx, false, Animate::Yes);
430                        }
431                    }
432                    self.selected_node_id = Some(node_id);
433                    if self.is_folder(node_id){
434                        dispatch_action(cx, FileTreeAction::FolderClicked(node_id));
435                    }
436                    else{
437                        dispatch_action(cx, FileTreeAction::FileClicked(node_id));
438                    }
439                }
440                FileTreeNodeAction::ShouldStartDrag => {
441                    if self.dragging_node_id.is_none() {
442                        dispatch_action(cx, FileTreeAction::ShouldFileStartDrag(node_id));
443                    }
444                }
445                _ => ()
446            }
447        }
448        
449        match event.hits(cx, self.scroll_bars.area()) {
450            Hit::KeyFocus(_) => {
451                if let Some(node_id) = self.selected_node_id {
452                    self.tree_nodes.get_mut(&node_id).unwrap().0.set_is_focussed(cx, true, Animate::Yes);
453                }
454            }
455            Hit::KeyFocusLost(_) => {
456                if let Some(node_id) = self.selected_node_id {
457                    self.tree_nodes.get_mut(&node_id).unwrap().0.set_is_focussed(cx, false, Animate::Yes);
458                }
459            }
460            _ => ()
461        }
462    }
463}
464
465#[derive(Clone, Debug, Default, Eq, Hash, Copy, PartialEq, FromLiveId)]
466pub struct FileNodeId(pub LiveId);
467
468impl Widget for FileTree {
469    fn redraw(&mut self, cx: &mut Cx) {
470        self.scroll_bars.redraw(cx);
471    }
472    
473    fn handle_widget_event_with(&mut self, cx: &mut Cx, event: &Event, dispatch_action: &mut dyn FnMut(&mut Cx, WidgetActionItem)) {
474        let uid = self.widget_uid();
475        self.handle_event_with(cx, event, &mut | cx, action | {
476            dispatch_action(cx, WidgetActionItem::new(action.into(), uid))
477        });
478    }
479    
480    fn walk(&mut self, _cx:&mut Cx) -> Walk {self.walk}
481    
482    fn draw_walk_widget(&mut self, cx: &mut Cx2d, walk: Walk) -> WidgetDraw {
483        if self.draw_state.begin(cx, ()) {
484            self.begin(cx, walk);
485            return WidgetDraw::hook_above()
486        }
487        if let Some(()) = self.draw_state.get() {
488            self.end(cx);
489            self.draw_state.end();
490        }
491        WidgetDraw::done()
492    }
493}
494
495#[derive(Debug, Clone, Default, PartialEq, WidgetRef)]
496pub struct FileTreeRef(WidgetRef);
497
498impl FileTreeRef{
499    pub fn should_file_start_drag(&self, actions: &WidgetActions) -> Option<FileNodeId> {
500        if let Some(item) = actions.find_single_action(self.widget_uid()) {
501            if let FileTreeAction::ShouldFileStartDrag(file_id) = item.action() {
502                return Some(file_id)
503            }
504        }
505        None
506    }
507    
508    pub fn file_clicked(&self, actions: &WidgetActions) -> Option<FileNodeId> {
509        if let Some(item) = actions.find_single_action(self.widget_uid()) {
510            if let FileTreeAction::FileClicked(file_id) = item.action() {
511                return Some(file_id)
512            }
513        }
514        None
515    }
516    
517    
518    pub fn file_start_drag(&self, cx: &mut Cx, _file_id: FileNodeId, item: DragItem) {
519        cx.start_dragging(vec![item]);
520    }
521}
522
523#[derive(Clone, Default, WidgetSet)]
524pub struct FileTreeSet(WidgetSet);