makepad_widgets/
stack_navigation.rs

1use crate::{
2    makepad_derive_widget::*,
3    makepad_draw::*,
4    widget::*,
5    label::*,
6    button::*,
7    view::*,
8    WidgetMatchEvent,
9    WindowAction,
10};
11
12live_design!{
13    link widgets;
14    use link::widgets::*;
15    use link::theme::*;
16    use makepad_draw::shader::std::*;
17    
18    pub StackNavigationViewBase = {{StackNavigationView}} {}
19    pub StackNavigationBase = {{StackNavigation}} {}
20    
21    // StackView DSL begin
22    
23    HEADER_HEIGHT = 80.0
24    
25    pub StackViewHeader = <View> {
26        width: Fill, height: (HEADER_HEIGHT),
27        padding: {bottom: 10., top: 50.}
28        show_bg: true
29        draw_bg: {
30            color: (THEME_COLOR_APP_CAPTION_BAR)
31        }
32        
33        content = <View> {
34            width: Fill, height: Fit,
35            flow: Overlay,
36            
37            title_container = <View> {
38                width: Fill, height: Fit,
39                align: {x: 0.5, y: 0.5}
40                
41                title = <H4> {
42                    width: Fit, height: Fit,
43                    margin: 0,
44                    text: "Stack View Title"
45                }
46            }
47            
48            button_container = <View> {
49                left_button = <Button> {
50                    width: Fit, height: 68,
51                    icon_walk: {width: 10, height: 68}
52                    draw_bg: {
53                        fn pixel(self) -> vec4 {
54                            let sdf = Sdf2d::viewport(self.pos * self.rect_size);
55                            return sdf.result
56                        }
57                    }
58                    draw_icon: {
59                        svg_file: dep("crate://self/resources/icons/back.svg"),
60                        color: (THEME_COLOR_LABEL_INNER);
61                        brightness: 0.8;
62                    }
63                }
64            }
65        }
66    }
67    
68    pub StackNavigationView = <StackNavigationViewBase> {
69        visible: false
70        width: Fill, height: Fill,
71        flow: Overlay
72        
73        show_bg: true
74        draw_bg: {
75            color: (THEME_COLOR_WHITE)
76        }
77        
78        // Empty slot to place a generic full-screen background
79        background = <View> {
80            width: Fill, height: Fill,
81            visible: false
82        }
83        
84        body = <View> {
85            width: Fill, height: Fill,
86            flow: Down,
87            
88            // THEME_SPACE between body and header can be adjusted overriding this margin
89            margin: {top: (HEADER_HEIGHT)},
90        }
91        
92        header = <StackViewHeader> {}
93        
94        offset: 4000.0
95        
96        animator: {
97            slide = {
98                default: hide,
99                hide = {
100                    redraw: true
101                    ease: ExpDecay {d1: 0.80, d2: 0.97}
102                    from: {all: Forward {duration: 5.0}}
103                    // Large enough number to cover several screens,
104                    // but we need a way to parametrize it
105                    apply: {offset: 4000.0}
106                }
107                
108                show = {
109                    redraw: true
110                    ease: ExpDecay {d1: 0.82, d2: 0.95}
111                    from: {all: Forward {duration: 0.5}}
112                    apply: {offset: 0.0}
113                }
114            }
115        }
116    }
117    
118    pub StackNavigation = <StackNavigationBase> {
119        width: Fill, height: Fill
120        flow: Overlay
121        
122        root_view = <View> {}
123    }
124    
125}
126
127#[derive(Clone, DefaultNone, Eq, Hash, PartialEq, Debug)]
128pub enum StackNavigationAction {
129    None,
130    NavigateTo(LiveId)
131}
132
133#[derive(Clone, Default, Eq, Hash, PartialEq, Debug)]
134pub enum StackNavigationViewState {
135    #[default] Inactive,
136    Active,
137}
138
139/// Actions that are delivered to an incoming or outgoing "active" widget/view
140/// within a stack navigation container.
141#[derive(Clone, DefaultNone, Eq, Hash, PartialEq, Debug)]
142pub enum StackNavigationTransitionAction {
143    None,
144    ShowBegin,
145    ShowDone,
146    HideBegin,
147    HideEnd,
148}
149
150#[derive(Live, LiveHook, Widget)]
151pub struct StackNavigationView {
152    #[deref]
153    view: View,
154
155    #[live]
156    offset: f64,
157
158    #[rust(10000.0)]
159    offset_to_hide: f64,
160
161    #[animator]
162    animator: Animator,
163
164    #[rust]
165    state: StackNavigationViewState,
166}
167
168impl Widget for StackNavigationView {
169    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
170        if self.animator_handle_event(cx, event).must_redraw() {
171            self.view.redraw(cx);
172        }
173        self.view.handle_event(cx, event, scope);
174
175        self.handle_stack_view_closure_request(cx, event, scope);
176        self.trigger_action_post_opening_if_done(cx);
177        self.finish_closure_animation_if_done(cx);
178    }
179
180    fn draw_walk(&mut self, cx:&mut Cx2d, scope:&mut Scope, walk:Walk) -> DrawStep{
181        self.view.draw_walk(
182            cx,
183            scope,
184            walk.with_abs_pos(DVec2 {
185                x: self.offset,
186                y: 0.,
187            }),
188        )
189    }
190}
191
192impl StackNavigationView {
193    fn hide_stack_view(&mut self, cx: &mut Cx) {
194        self.animator_play(cx, id!(slide.hide));
195        cx.widget_action(
196            self.widget_uid(),
197            &HeapLiveIdPath::default(),
198            StackNavigationTransitionAction::HideBegin,
199        );
200    }
201
202    fn handle_stack_view_closure_request(&mut self, cx: &mut Cx, event: &Event, _scope: &mut Scope) {
203        // Hide the active stack view if:
204        // * the back navigation button/gesture occurred,
205        // * the left_button was clicked,
206        // * the "back" button on the mouse was clicked.
207        // TODO: in the future, handle a swipe right gesture on touchscreen, or two-finger swipe on trackpad
208        if matches!(self.state, StackNavigationViewState::Active) {
209            if event.back_pressed()
210                || matches!(event, Event::Actions(actions) if self.button(id!(left_button)).clicked(&actions))
211                || matches!(event, Event::MouseUp(mouse) if mouse.button.is_back())
212            {
213                self.hide_stack_view(cx);
214            }
215        }
216    }
217
218    fn finish_closure_animation_if_done(&mut self, cx: &mut Cx) {
219        if self.state == StackNavigationViewState::Active
220            && self.animator.animator_in_state(cx, id!(slide.hide))
221        {
222            if self.offset > self.offset_to_hide {
223                self.apply_over(cx, live! { visible: false });
224
225                cx.widget_action(
226                    self.widget_uid(),
227                    &HeapLiveIdPath::default(),
228                    StackNavigationTransitionAction::HideEnd,
229                );
230
231                self.animator_cut(cx, id!(slide.hide));
232                self.state = StackNavigationViewState::Inactive;
233            }
234        }
235    }
236
237    fn trigger_action_post_opening_if_done(&mut self, cx: &mut Cx) {
238        if self.state == StackNavigationViewState::Inactive &&
239            self.animator.animator_in_state(cx, id!(slide.show))
240        {
241            const OPENING_OFFSET_THRESHOLD: f64 = 0.5;
242            if self.offset < OPENING_OFFSET_THRESHOLD {
243                cx.widget_action(
244                    self.widget_uid(),
245                    &HeapLiveIdPath::default(),
246                    StackNavigationTransitionAction::ShowDone,
247                );
248                self.state = StackNavigationViewState::Active;
249            }
250        }
251    }
252}
253
254impl StackNavigationViewRef {
255    pub fn show(&self, cx: &mut Cx, root_width: f64) {
256        if let Some(mut inner) = self.borrow_mut() {
257            inner.apply_over(cx, live! {offset: (root_width), visible: true});
258            inner.offset_to_hide = root_width;
259            inner.animator_play(cx, id!(slide.show));
260        }
261    }
262
263    pub fn is_showing(&self, cx: &mut Cx) -> bool {
264        if let Some(inner) = self.borrow() {
265            inner.animator.animator_in_state(cx, id!(slide.show))
266                || inner.animator.is_track_animating(cx, id!(slide))
267        } else {
268            false
269        }
270    }
271
272    pub fn is_animating(&self, cx: &mut Cx) -> bool {
273        if let Some(inner) = self.borrow() {
274            inner.animator.is_track_animating(cx, id!(slide))
275        } else {
276            false
277        }
278    }
279
280    pub fn set_offset_to_hide(&self, offset_to_hide: f64) {
281        if let Some(mut inner) = self.borrow_mut() {
282            inner.offset_to_hide = offset_to_hide;
283        }
284    }
285}
286
287#[derive(Default)]
288enum ActiveStackView {
289    #[default]
290    None,
291    Active(LiveId),
292}
293
294#[derive(Live, LiveRegisterWidget, WidgetRef)]
295pub struct StackNavigation {
296    #[deref]
297    view: View,
298
299    #[rust]
300    screen_width: f64,
301
302    #[rust]
303    active_stack_view: ActiveStackView,
304}
305
306impl LiveHook for StackNavigation {
307    fn after_apply_from(&mut self, cx: &mut Cx, apply: &mut Apply) {
308        if apply.from.is_new_from_doc() {
309            self.active_stack_view = ActiveStackView::None;
310        } else {
311            if let ActiveStackView::Active(stack_view_id) = self.active_stack_view {
312                // Make sure current stack view is visible when code reloads
313                let stack_view_ref = self.stack_navigation_view(&[stack_view_id]);
314                stack_view_ref.apply_over(cx, live! {visible: true, offset: 0.0});
315            }
316        }
317    }
318}
319
320impl Widget for StackNavigation {
321    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
322        // If the event requires visibility, only forward it to the visible views.
323        // If the event does not require visibility, forward it to all views,
324        // ensuring that we don't forward it to the root view twice.
325        let mut visible_views = self.get_visible_views(cx);
326        if !event.requires_visibility() {
327            let root_view = self.view.widget(id!(root_view));
328            if !visible_views.contains(&root_view) {
329                visible_views.insert(0, root_view);
330            }
331        }
332        for widget_ref in visible_views {
333            widget_ref.handle_event(cx, event, scope);
334        }
335
336        // Leaving this to the final step, so that the active stack view can handle the event first.
337        // It is relevant when the active stack view is animating out and wants to handle
338        // the StackNavigationTransitionAction::HideEnd action.
339        self.widget_match_event(cx, event, scope);
340    }
341
342    fn draw_walk(&mut self, cx: &mut Cx2d, scope: &mut Scope, walk: Walk) -> DrawStep  {
343        for widget_ref in self.get_visible_views(cx.cx).iter() {
344            widget_ref.draw_walk(cx, scope, walk) ?;
345        }
346        DrawStep::done()
347    }
348}
349
350impl WidgetNode for StackNavigation {
351    fn walk(&mut self, cx:&mut Cx) -> Walk{
352        self.view.walk(cx)
353    }
354    fn area(&self)->Area{self.view.area()}
355    
356    fn redraw(&mut self, cx: &mut Cx) {
357        for widget_ref in self.get_visible_views(cx).iter() {
358            widget_ref.redraw(cx);
359        }
360    }
361
362    fn find_widgets(&self, path: &[LiveId], cached: WidgetCache, results: &mut WidgetSet) {
363        self.view.find_widgets(path, cached, results);
364    }
365    
366    fn uid_to_widget(&self, uid:WidgetUid)->WidgetRef{
367        self.view.uid_to_widget(uid)
368    }
369    
370}
371
372impl WidgetMatchEvent for StackNavigation {
373    fn handle_actions(&mut self, _cx: &mut Cx, actions: &Actions, _scope: &mut Scope) {
374        for action in actions {
375            // If the window is resized, we need to record the new screen width to
376            // fit the transition animation for the new dimensions.
377            if let WindowAction::WindowGeomChange(ce) = action.as_widget_action().cast() {
378                self.screen_width = ce.new_geom.inner_size.x * ce.new_geom.dpi_factor;
379                if let ActiveStackView::Active(stack_view_id) = self.active_stack_view {
380                    let stack_view_ref = self.stack_navigation_view(&[stack_view_id]);
381                    stack_view_ref.set_offset_to_hide(self.screen_width);
382                }
383            }
384
385            // If the active stack view is already hidden, we need to reset the active stack view.
386            if let StackNavigationTransitionAction::HideEnd = action.as_widget_action().cast() {
387                self.active_stack_view = ActiveStackView::None;
388            }
389        }
390    }
391}
392
393
394impl StackNavigation {
395    pub fn show_stack_view_by_id(&mut self, stack_view_id: LiveId, cx: &mut Cx) {
396        if let ActiveStackView::None = self.active_stack_view {
397            let stack_view_ref = self.stack_navigation_view(&[stack_view_id]);
398            stack_view_ref.show(cx, self.screen_width);
399            self.active_stack_view = ActiveStackView::Active(stack_view_id);
400
401            // Send a `Show` action to the view being shown so it can be aware of the transition.
402            cx.widget_action(
403                stack_view_ref.widget_uid(),
404                &HeapLiveIdPath::default(),
405                StackNavigationTransitionAction::ShowBegin,
406            );
407
408            self.redraw(cx);
409        }
410    }
411
412    /// Returns the views that are currently visible.
413    ///
414    /// This includes up to two views, in this order:
415    /// 1. The root_view, if it is animating and partially showing,
416    /// 2. The active stack view, if it exists and is partially or fully showing.
417    ///   or if there is no active stack view at all.
418    fn get_visible_views(&mut self, cx: &mut Cx) -> Vec<WidgetRef> {
419        match self.active_stack_view {
420            ActiveStackView::None => {
421                vec![self.view.widget(id!(root_view))]
422            },
423            ActiveStackView::Active(stack_view_id) => {
424                let stack_view_ref = self.stack_navigation_view(&[stack_view_id]);
425                let mut views = vec![];
426
427                if stack_view_ref.is_showing(cx) {
428                    if stack_view_ref.is_animating(cx) {
429                        views.push(self.view.widget(id!(root_view)));
430                    }
431                }
432
433                views.push(stack_view_ref.0.clone());
434                views
435            }
436        }
437    }
438}
439
440impl StackNavigationRef {
441    pub fn show_stack_view_by_id(&self, stack_view_id: LiveId, cx: &mut Cx) {
442        if let Some(mut inner) = self.borrow_mut() {
443            inner.show_stack_view_by_id(stack_view_id, cx);
444        }
445    }
446
447    pub fn handle_stack_view_actions(&self, cx: &mut Cx, actions: &Actions) {
448        for action in actions {
449            if let StackNavigationAction::NavigateTo(stack_view_id) = action.as_widget_action().cast() {
450                self.show_stack_view_by_id(stack_view_id, cx);
451                break;
452            }
453        }
454    }
455
456    pub fn set_title(&self, cx:&mut Cx, stack_view_id: LiveId, title: &str) {
457        if let Some(inner) = self.borrow_mut() {
458            let stack_view_ref = inner.stack_navigation_view(&[stack_view_id]);
459            stack_view_ref.label(id!(title)).set_text(cx, title);
460        }
461    }
462}