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    link widgets;
17    use link::theme::*;
18    use crate::scroll_bars::ScrollBars;
19    use link::shaders::*;
20    
21    DrawBgQuad = {{DrawBgQuad}} {}
22    DrawNameText = {{DrawNameText}} {}
23    DrawIconQuad = {{DrawIconQuad}} {}
24    
25    pub FileTreeNodeBase = {{FileTreeNode}} {}
26    pub FileTreeBase = {{FileTree}} {}
27    
28    pub FileTreeNode = <FileTreeNodeBase> {
29        align: { y: 0.5 }
30        padding: { left: (THEME_SPACE_2) },
31        is_folder: false,
32        indent_width: (THEME_SPACE_2)
33        min_drag_distance: 10.0
34        
35        draw_bg: {
36            uniform color_1: (THEME_COLOR_BG_EVEN)
37            uniform color_2: (THEME_COLOR_BG_ODD)
38            uniform color_active: (THEME_COLOR_HIGHLIGHT)
39
40            fn pixel(self) -> vec4 {
41                let sdf = Sdf2d::viewport(self.pos * self.rect_size);
42                sdf.box(
43                    0.,
44                    -2.,
45                    self.rect_size.x,
46                    self.rect_size.y + 3.0,
47                    1.
48                )
49                sdf.fill_keep(
50                    mix(
51                        mix(
52                            self.color_1,
53                            self.color_2,
54                            self.is_even
55                        ),
56                        self.color_active,
57                        self.active
58                    )
59                )
60                return sdf.result
61            }
62        }
63        
64        draw_icon: {
65            uniform color: (THEME_COLOR_LABEL_INNER)
66            uniform color_active: (THEME_COLOR_LABEL_INNER_ACTIVE)
67
68            fn pixel(self) -> vec4 {
69                let sdf = Sdf2d::viewport(self.pos * self.rect_size);
70                let w = self.rect_size.x;
71                let h = self.rect_size.y;
72                sdf.box(0. * w, 0.35 * h, 0.87 * w, 0.39 * h, 0.75);
73                sdf.box(0. * w, 0.28 * h, 0.5 * w, 0.3 * h, 1.);
74                sdf.union();
75                return sdf.fill(
76                    mix(
77                        self.color * self.scale,
78                        self.color_active,
79                        self.active
80                    )
81                );
82            }
83        }
84        
85        draw_text: {
86            uniform color: (THEME_COLOR_LABEL_INNER)
87            uniform color_active: (THEME_COLOR_LABEL_INNER_ACTIVE)
88            
89            fn get_color(self) -> vec4 {
90                return mix(
91                    self.color * self.scale,
92                    self.color_active,
93                    self.active
94                )
95            }
96            
97            text_style: <THEME_FONT_REGULAR> {
98                font_size: (THEME_FONT_SIZE_P)
99            }
100        }
101        
102        icon_walk: {
103            width: (THEME_DATA_ICON_WIDTH - 2), height: (THEME_DATA_ICON_HEIGHT),
104            margin: { right: (THEME_SPACE_1) }
105        }
106        
107        animator: {
108            hover = {
109                default: off
110                off = {
111                    from: {all: Forward {duration: 0.2}}
112                    apply: {
113                        hover: 0.0
114                        draw_bg: {hover: 0.0}
115                        draw_text: {hover: 0.0}
116                        draw_icon: {hover: 0.0}
117                    }
118                }
119                
120                on = {
121                    cursor: Hand
122                    from: {all: Snap}
123                    apply: {
124                        hover: 1.0
125                        draw_bg: {hover: 1.0}
126                        draw_text: {hover: 1.0}
127                        draw_icon: {hover: 1.0}
128                    },
129                }
130            }
131            
132            focus = {
133                default: on
134                on = {
135                    from: {all: Snap}
136                    apply: {focussed: 1.0}
137                }
138                
139                off = {
140                    from: {all: Forward {duration: 0.1}}
141                    apply: {focussed: 0.0}
142                }
143            }
144            
145            select = {
146                default: off
147                off = {
148                    from: {all: Forward {duration: 0.1}}
149                    apply: {
150                        active: 0.0
151                        draw_bg: {active: 0.0}
152                        draw_text: {active: 0.0}
153                        draw_icon: {active: 0.0}
154                    }
155                }
156                on = {
157                    from: {all: Snap}
158                    apply: {
159                        active: 1.0
160                        draw_bg: {active: 1.0}
161                        draw_text: {active: 1.0}
162                        draw_icon: {active: 1.0}
163                    }
164                }
165                
166            }
167            
168            open = {
169                default: off
170                off = {
171                    //from: {all: Exp {speed1: 0.80, speed2: 0.97}}
172                    //duration: 0.2
173                    redraw: true
174                    
175                    from: {all: Forward {duration: 0.2}}
176                    ease: ExpDecay {d1: 0.80, d2: 0.97}
177                    
178                    //ease: Ease::OutExp
179                    apply: {
180                        opened: [{time: 0.0, value: 1.0}, {time: 1.0, value: 0.0}]
181                        draw_bg: {opened: [{time: 0.0, value: 1.0}, {time: 1.0, value: 0.0}]}
182                        draw_text: {opened: [{time: 0.0, value: 1.0}, {time: 1.0, value: 0.0}]}
183                        draw_icon: {opened: [{time: 0.0, value: 1.0}, {time: 1.0, value: 0.0}]}
184                    }
185                }
186                
187                on = {
188                    //from: {all: Exp {speed1: 0.82, speed2: 0.95}}
189                    
190                    from: {all: Forward {duration: 0.2}}
191                    ease: ExpDecay {d1: 0.82, d2: 0.95}
192                    
193                    //from: {all: Exp {speed1: 0.82, speed2: 0.95}}
194                    redraw: true
195                    apply: {
196                        opened: 1.0
197                        draw_bg: {opened: 1.0}
198                        draw_text: {opened: 1.0}
199                        draw_icon: {opened: 1.0}
200                    }
201                }
202            }
203        }
204    }
205     
206    pub FileTree = <FileTreeBase> {
207        flow: Down,
208        
209        scroll_bars: <ScrollBars> {}
210        scroll_bars: {}
211        node_height: (THEME_DATA_ITEM_HEIGHT),
212        clip_x: true,
213        clip_y: true
214        
215        file_node: <FileTreeNode> {
216            is_folder: false,
217            draw_bg: {is_folder: 0.0}
218            draw_text: {is_folder: 0.0}
219        }
220        
221        folder_node: <FileTreeNode> {
222            is_folder: true,
223            draw_bg: {is_folder: 1.0}
224            draw_text: {is_folder: 1.0}
225        }
226        
227        filler: {
228            fn pixel(self) -> vec4 {
229                return mix(
230                    mix(
231                        THEME_COLOR_BG_EVEN,
232                        THEME_COLOR_BG_ODD,
233                        self.is_even
234                    ),
235                    mix(
236                        THEME_COLOR_OUTSET_INACTIVE,
237                        THEME_COLOR_OUTSET_ACTIVE,
238                        self.focussed
239                    ),
240                    self.active
241                );
242            }
243        }
244    }
245}
246
247// TODO support a shared 'inputs' struct on drawshaders
248#[derive(Live, LiveHook, LiveRegister)]#[repr(C)]
249struct DrawBgQuad {
250    #[deref] draw_super: DrawQuad,
251    #[live] is_even: f32,
252    #[live] scale: f32,
253    #[live] is_folder: f32,
254    #[live] focussed: f32,
255    #[live] active: f32,
256    #[live] hover: f32,
257    #[live] opened: f32,
258}
259
260#[derive(Live, LiveHook, LiveRegister)]#[repr(C)]
261struct DrawNameText {
262    #[deref] draw_super: DrawText,
263    #[live] is_even: f32,
264    #[live] scale: f32,
265    #[live] is_folder: f32,
266    #[live] focussed: f32,
267    #[live] active: f32,
268    #[live] hover: f32,
269    #[live] opened: f32,
270}
271
272#[derive(Live, LiveHook, LiveRegister)]#[repr(C)]
273struct DrawIconQuad {
274    #[deref] draw_super: DrawQuad,
275    #[live] is_even: f32,
276    #[live] scale: f32,
277    #[live] is_folder: f32,
278    #[live] focussed: f32,
279    #[live] active: f32,
280    #[live] hover: f32,
281    #[live] opened: f32,
282}
283
284#[derive(Live, LiveHook, LiveRegister)]
285pub struct FileTreeNode {
286    #[live] draw_bg: DrawBgQuad,
287    #[live] draw_icon: DrawIconQuad,
288    #[live] draw_text: DrawNameText,
289    #[live] check_box: CheckBox,
290    #[layout] layout: Layout,
291    
292    #[animator] animator: Animator,
293    
294    #[live] indent_width: f64,
295    #[live] indent_shift: f64,
296    
297    #[live] icon_walk: Walk,
298    
299    #[live] is_folder: bool,
300    #[live] min_drag_distance: f64,
301    
302    #[live] opened: f32,
303    #[live] focussed: f32,
304    #[live] hover: f32,
305    #[live] active: f32,
306}
307
308#[derive(Live, Widget)]
309pub struct FileTree {
310    #[redraw] #[live] scroll_bars: ScrollBars,
311    #[live] file_node: Option<LivePtr>,
312    #[live] folder_node: Option<LivePtr>,
313    #[walk] walk: Walk,
314    #[layout] layout: Layout,
315    #[live] filler: DrawBgQuad,
316    
317    #[live] node_height: f64,
318    
319    #[live] draw_scroll_shadow: DrawScrollShadow,
320    
321    #[rust] draw_state: DrawStateWrap<()>,
322    
323    #[rust] dragging_node_id: Option<LiveId>,
324    #[rust] selected_node_id: Option<LiveId>,
325    #[rust] open_nodes: HashSet<LiveId>,
326    
327    #[rust] tree_nodes: ComponentMap<LiveId, (FileTreeNode, LiveId)>,
328    
329    #[rust] count: usize,
330    #[rust] stack: Vec<f64>,
331}
332
333impl LiveHook for FileTree {
334    fn after_apply(&mut self, cx: &mut Cx, apply: &mut Apply, index: usize, nodes: &[LiveNode]) {
335        for (_, (tree_node, id)) in self.tree_nodes.iter_mut() {
336            if let Some(index) = nodes.child_by_name(index, id.as_field()) {
337                tree_node.apply(cx, apply, index, nodes);
338            }
339        }
340        self.scroll_bars.redraw(cx);
341    }
342}
343
344#[derive(Clone, Debug, DefaultNone)]
345pub enum FileTreeAction {
346    None,
347    FileClicked(LiveId),
348    FolderClicked(LiveId),
349    ShouldFileStartDrag(LiveId),
350}
351
352pub enum FileTreeNodeAction {
353    WasClicked,
354    Opening,
355    Closing,
356    ShouldStartDrag
357}
358
359impl FileTreeNode {
360    pub fn set_draw_state(&mut self, is_even: f32, scale: f64) {
361        self.draw_bg.scale = scale as f32;
362        self.draw_bg.is_even = is_even;
363        self.draw_text.scale = scale as f32;
364        self.draw_text.is_even = is_even;
365        self.draw_icon.scale = scale as f32;
366        self.draw_icon.is_even = is_even;
367        self.draw_text.font_scale = scale as f32;
368    }
369    
370    pub fn draw_folder(&mut self, cx: &mut Cx2d, name: &str, is_even: f32, node_height: f64, depth: usize, scale: f64) {
371        self.set_draw_state(is_even, scale);
372        
373        self.draw_bg.begin(cx, Walk::size(Size::Fill, Size::Fixed(scale * node_height)), self.layout);
374        
375        cx.walk_turtle(self.indent_walk(depth));
376        
377        self.draw_icon.draw_walk(cx, self.icon_walk);
378        
379        self.draw_text.draw_walk(cx, Walk::fit(), Align::default(), name);
380        self.draw_bg.end(cx);
381    }
382    
383    pub fn draw_file(&mut self, cx: &mut Cx2d, name: &str, is_even: f32, node_height: f64, depth: usize, scale: f64) {
384        self.set_draw_state(is_even, scale);
385        
386        self.draw_bg.begin(cx, Walk::size(Size::Fill, Size::Fixed(scale * node_height)), self.layout);
387        
388        cx.walk_turtle(self.indent_walk(depth));
389        
390        self.draw_text.draw_walk(cx, Walk::fit(), Align::default(), name);
391        self.draw_bg.end(cx);
392    }
393    
394    fn indent_walk(&self, depth: usize) -> Walk {
395        Walk {
396            abs_pos: None,
397            width: Size::Fixed(depth as f64 * self.indent_width + self.indent_shift),
398            height: Size::Fixed(0.0),
399            margin: Margin {
400                left: depth as f64 * 1.0,
401                top: 0.0,
402                right: depth as f64 * 4.0,
403                bottom: 0.0,
404            },
405        }
406    }
407    
408    fn set_is_selected(&mut self, cx: &mut Cx, is: bool, animate: Animate) {
409        self.animator_toggle(cx, is, animate, id!(select.on), id!(select.off))
410    }
411    
412    fn set_is_focussed(&mut self, cx: &mut Cx, is: bool, animate: Animate) {
413        self.animator_toggle(cx, is, animate, id!(focus.on), id!(focus.off))
414    }
415    
416    pub fn set_folder_is_open(&mut self, cx: &mut Cx, is: bool, animate: Animate) {
417        self.animator_toggle(cx, is, animate, id!(open.on), id!(open.off));
418    }
419    
420    pub fn handle_event(
421        &mut self,
422        cx: &mut Cx,
423        event: &Event,
424        node_id: LiveId,
425        _scope: &mut Scope,
426        actions: &mut Vec<(LiveId, FileTreeNodeAction)>,
427    ) {
428        if self.animator_handle_event(cx, event).must_redraw() {
429            self.draw_bg.redraw(cx);
430        }
431        match event.hits(cx, self.draw_bg.area()) {
432            Hit::FingerHoverIn(_) => {
433                self.animator_play(cx, id!(hover.on));
434            }
435            Hit::FingerHoverOut(_) => {
436                self.animator_play(cx, id!(hover.off));
437            }
438            Hit::FingerMove(f) => {
439                if f.abs.distance(&f.abs_start) >= self.min_drag_distance {
440                    actions.push((node_id, FileTreeNodeAction::ShouldStartDrag));
441                }
442            }
443            Hit::FingerDown(_) => {
444                self.animator_play(cx, id!(select.on));
445                if self.is_folder {
446                    if self.animator_in_state(cx, id!(open.on)) {
447                        self.animator_play(cx, id!(open.off));
448                        actions.push((node_id, FileTreeNodeAction::Closing));
449                    }
450                    else {
451                        self.animator_play(cx, id!(open.on));
452                        actions.push((node_id, FileTreeNodeAction::Opening));
453                    }
454                }
455                actions.push((node_id, FileTreeNodeAction::WasClicked));
456            }
457            _ => {}
458        }
459    }
460}
461
462impl FileTree {
463    
464    pub fn begin(&mut self, cx: &mut Cx2d, walk: Walk) {
465        self.scroll_bars.begin(cx, walk, self.layout);
466        self.count = 0;
467    }
468    
469    pub fn end(&mut self, cx: &mut Cx2d) {
470        // lets fill the space left with blanks
471        let height_left = cx.turtle().height_left();
472        let mut walk = 0.0;
473        while walk < height_left {
474            self.count += 1;
475            self.filler.is_even = Self::is_even(self.count);
476            let height = self.node_height.min(height_left - walk);
477            self.filler.draw_walk(cx, Walk::size(Size::Fill, Size::Fixed(height)));
478            walk += height.max(1.0);
479        }
480        
481        self.draw_scroll_shadow.draw(cx, dvec2(0., 0.));
482        self.scroll_bars.end(cx);
483        
484        let selected_node_id = self.selected_node_id;
485        self.tree_nodes.retain_visible_and( | node_id, _ | Some(*node_id) == selected_node_id);
486    }
487    
488    pub fn is_even(count: usize) -> f32 {
489        if count % 2 == 1 {0.0}else {1.0}
490    }
491    
492    pub fn should_node_draw(&mut self, cx: &mut Cx2d) -> bool {
493        let scale = self.stack.last().cloned().unwrap_or(1.0);
494        let height = self.node_height * scale;
495        let walk = Walk::size(Size::Fill, Size::Fixed(height));
496        if scale > 0.01 && cx.walk_turtle_would_be_visible(walk) {
497            return true
498        }
499        else {
500            cx.walk_turtle(walk);
501            return false
502        }
503    }
504    
505    pub fn begin_folder(
506        &mut self,
507        cx: &mut Cx2d,
508        node_id: LiveId,
509        name: &str,
510    ) -> Result<(), ()> {
511        let scale = self.stack.last().cloned().unwrap_or(1.0);
512        
513        if scale > 0.2 {
514            self.count += 1;
515        }
516        
517        let is_open = self.open_nodes.contains(&node_id);
518        
519        if self.should_node_draw(cx) {
520            let folder_node = self.folder_node;
521            let (tree_node, _) = self.tree_nodes.get_or_insert(cx, node_id, | cx | {
522                let mut tree_node = FileTreeNode::new_from_ptr(cx, folder_node);
523                if is_open {
524                    tree_node.set_folder_is_open(cx, true, Animate::No)
525                }
526                (tree_node, live_id!(folder_node))
527            });
528            tree_node.draw_folder(cx, name, Self::is_even(self.count), self.node_height, self.stack.len(), scale);
529            self.stack.push(tree_node.opened as f64 * scale);
530            if tree_node.opened <= 0.001 {
531                self.end_folder();
532                return Err(());
533            }
534        }
535        else {
536            if is_open {
537                self.stack.push(scale * 1.0);
538            }
539            else {
540                return Err(());
541            }
542        }
543        Ok(())
544    }
545    
546    pub fn end_folder(&mut self) {
547        self.stack.pop();
548    }
549    
550    pub fn file(&mut self, cx: &mut Cx2d, node_id: LiveId, name: &str) {
551        let scale = self.stack.last().cloned().unwrap_or(1.0);
552        
553        if scale > 0.2 {
554            self.count += 1;
555        }
556        if self.should_node_draw(cx) {
557            let file_node = self.file_node;
558            let (tree_node, _) = self.tree_nodes.get_or_insert(cx, node_id, | cx | {
559                (FileTreeNode::new_from_ptr(cx, file_node), live_id!(file_node))
560            });
561            tree_node.draw_file(cx, name, Self::is_even(self.count), self.node_height, self.stack.len(), scale);
562        }
563    }
564    
565    pub fn forget(&mut self) {
566        self.tree_nodes.clear();
567    }
568    
569    pub fn forget_node(&mut self, file_node_id: LiveId) {
570        self.tree_nodes.remove(&file_node_id);
571    }
572    
573    pub fn is_folder(&mut self, file_node_id: LiveId)->bool {
574        if let Some((node,_)) = self.tree_nodes.get(&file_node_id){
575            node.is_folder
576        }
577        else{
578            false
579        }
580    }
581    
582    pub fn set_folder_is_open(
583        &mut self,
584        cx: &mut Cx,
585        node_id: LiveId,
586        is_open: bool,
587        animate: Animate,
588    ) {
589        if is_open {
590            self.open_nodes.insert(node_id);
591        }
592        else {
593            self.open_nodes.remove(&node_id);
594        }
595        if let Some((tree_node, _)) = self.tree_nodes.get_mut(&node_id) {
596            tree_node.set_folder_is_open(cx, is_open, animate);
597        }
598    }
599    
600    pub fn start_dragging_file_node(
601        &mut self,
602        cx: &mut Cx,
603        node_id: LiveId,
604        items: Vec<DragItem>,
605    ) {
606        self.dragging_node_id = Some(node_id);
607
608        log!("makepad: start_dragging_file_node");
609
610        cx.start_dragging(items);
611    }
612}
613
614//pub type LiveId = LiveId;
615//#[derive(Clone, Debug, Default, Eq, Hash, Copy, PartialEq, FromLiveId)]
616//pub struct LiveId(pub LiveId);
617
618impl Widget for FileTree {
619
620    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
621        let uid = self.widget_uid();
622        
623        self.scroll_bars.handle_event(cx, event, scope);
624                
625        match event {
626            Event::DragEnd => self.dragging_node_id = None,
627            _ => ()
628        }
629        
630        let mut node_actions = Vec::new();
631                
632        for (node_id, (node, _)) in self.tree_nodes.iter_mut() {
633            node.handle_event(cx, event, *node_id, scope, &mut node_actions);
634        }
635                
636        for (node_id, node_action) in node_actions {
637            match node_action {
638                FileTreeNodeAction::Opening => {
639                    self.open_nodes.insert(node_id);
640                }
641                FileTreeNodeAction::Closing => {
642                    self.open_nodes.remove(&node_id);
643                }
644                FileTreeNodeAction::WasClicked => {
645                    cx.set_key_focus(self.scroll_bars.area());
646                    if let Some(last_selected) = self.selected_node_id {
647                        if last_selected != node_id {
648                            self.tree_nodes.get_mut(&last_selected).unwrap().0.set_is_selected(cx, false, Animate::Yes);
649                        }
650                    }
651                    self.selected_node_id = Some(node_id);
652                    if self.is_folder(node_id){
653                        cx.widget_action(uid, &scope.path, FileTreeAction::FolderClicked(node_id));
654                    }
655                    else{
656                        cx.widget_action(uid, &scope.path, FileTreeAction::FileClicked(node_id));
657                    }
658                }
659                FileTreeNodeAction::ShouldStartDrag => {
660                    if self.dragging_node_id.is_none() {
661                        cx.widget_action(uid, &scope.path, FileTreeAction::ShouldFileStartDrag(node_id));
662                    }
663                }
664            }
665        }
666                
667        match event.hits(cx, self.scroll_bars.area()) {
668            Hit::KeyFocus(_) => {
669                if let Some(node_id) = self.selected_node_id {
670                    self.tree_nodes.get_mut(&node_id).unwrap().0.set_is_focussed(cx, true, Animate::Yes);
671                }
672            }
673            Hit::KeyFocusLost(_) => {
674                if let Some(node_id) = self.selected_node_id {
675                    self.tree_nodes.get_mut(&node_id).unwrap().0.set_is_focussed(cx, false, Animate::Yes);
676                }
677            }
678            _ => ()
679        }
680    }
681    
682    fn draw_walk(&mut self, cx: &mut Cx2d, _scope:&mut Scope,walk: Walk) -> DrawStep {
683        if self.draw_state.begin(cx, ()) {
684            self.begin(cx, walk);
685            return DrawStep::make_step()
686        }
687        if let Some(()) = self.draw_state.get() {
688            self.end(cx);
689            self.draw_state.end();
690        }
691        DrawStep::done()
692    }
693}
694
695impl FileTreeRef{
696    pub fn should_file_start_drag(&self, actions: &Actions) -> Option<LiveId> {
697        if let Some(item) = actions.find_widget_action(self.widget_uid()) {
698            if let FileTreeAction::ShouldFileStartDrag(file_id) = item.cast() {
699                return Some(file_id)
700            }
701        }
702        None
703    }
704    
705    pub fn file_clicked(&self, actions: &Actions) -> Option<LiveId> {
706        if let Some(item) = actions.find_widget_action(self.widget_uid()) {
707            if let FileTreeAction::FileClicked(file_id) = item.cast() {
708                return Some(file_id)
709            }
710        }
711        None
712    }
713    
714    pub fn folder_clicked(&self, actions: &Actions) -> Option<LiveId> {
715        if let Some(item) = actions.find_widget_action(self.widget_uid()) {
716            if let FileTreeAction::FolderClicked(file_id) = item.cast() {
717                return Some(file_id)
718            }
719        }
720        None
721    }
722    
723        
724    pub fn set_folder_is_open(
725        &self,
726        cx: &mut Cx,
727        node_id: LiveId,
728        is_open: bool,
729        animate: Animate,
730    ) {
731        if let Some(mut inner) = self.borrow_mut(){
732            inner.set_folder_is_open(cx, node_id, is_open, animate);
733        }
734    }
735    
736    pub fn file_start_drag(&self, cx: &mut Cx, _file_id: LiveId, item: DragItem) {
737        cx.start_dragging(vec![item]);
738    }
739}