makepad_widget/
splitter.rs

1use makepad_render::*;
2use serde::*;
3use crate::widgetstyle::*;
4
5#[derive(Clone)]
6pub struct Splitter {
7    pub axis: Axis,
8    pub align: SplitterAlign,
9    pub pos: f32,
10    
11    pub min_size: f32,
12    pub split_size: f32,
13    pub bg: Quad,
14    pub animator: Animator,
15    pub realign_dist: f32,
16    pub split_view: View,
17    pub _split_area: Area,
18    pub _calc_pos: f32,
19    pub _is_moving: bool,
20    pub _drag_point: f32,
21    pub _drag_pos_start: f32,
22    pub _drag_max_pos: f32,
23    pub _hit_state_margin: Option<Margin>,
24}
25
26#[derive(Clone, PartialEq, Serialize, Deserialize)]
27pub enum SplitterAlign {
28    First,
29    Last,
30    Weighted
31}
32
33#[derive(Clone, PartialEq)]
34pub enum SplitterEvent {
35    None,
36    Moving {new_pos: f32},
37    MovingEnd {new_align: SplitterAlign, new_pos: f32}
38}
39
40impl Splitter {
41    pub fn proto(cx: &mut Cx) -> Self {
42        Self {
43            axis: Axis::Vertical,
44            align: SplitterAlign::First,
45            pos: 0.0,
46            
47            _split_area: Area::Empty,
48            _calc_pos: 0.0,
49            _is_moving: false,
50            _drag_point: 0.,
51            _drag_pos_start: 0.,
52            _drag_max_pos: 0.0,
53            _hit_state_margin: None,
54            realign_dist: 30.,
55            split_size: 2.0,
56            min_size: 25.0,
57            split_view: View::proto(cx),
58            bg: Quad::proto(cx),
59            animator: Animator::default(),
60        }
61    }
62    
63    pub fn anim_default() -> AnimId {uid!()}
64    pub fn anim_over() -> AnimId {uid!()}
65    pub fn anim_down() -> AnimId {uid!()}
66    pub fn shader_bg() -> ShaderId {uid!()}
67    
68    pub fn style(cx: &mut Cx, _opt: &StyleOptions) {
69        
70        Self::anim_default().set(cx, Anim::new(Play::Cut {duration: 0.5}, vec![
71            Track::color(Quad::instance_color(), Ease::Lin, vec![(1.0, Theme::color_bg_splitter().get(cx))]),
72        ]));
73        
74        Self::anim_over().set(cx, Anim::new(Play::Cut {duration: 0.05}, vec![
75            Track::color(Quad::instance_color(), Ease::Lin, vec![(1.0, Theme::color_bg_splitter_over().get(cx))]),
76        ]));
77        
78        Self::anim_down().set(cx, Anim::new(Play::Cut {duration: 0.2}, vec![
79            Track::color(Quad::instance_color(), Ease::Lin, vec![
80                (0.0, Theme::color_bg_splitter_peak().get(cx)),
81                (1.0, Theme::color_bg_splitter_drag().get(cx))
82            ]),
83        ]));
84        
85        Self::shader_bg().set(cx, Quad::def_quad_shader().compose(shader_ast!({
86            
87            fn pixel() -> vec4 {
88                df_viewport(pos * vec2(w, h));
89                df_box(0., 0., w, h, 0.5);
90                return df_fill(color);
91            }
92        })));
93    }
94    
95    pub fn handle_splitter(&mut self, cx: &mut Cx, event: &mut Event) -> SplitterEvent {
96        match event.hits(cx, self._split_area, HitOpt {margin: self._hit_state_margin, ..Default::default()}) {
97            Event::Animate(ae) => {
98                self.animator.calc_area(cx, self._split_area, ae.time);
99            },
100            Event::AnimEnded(_) => self.animator.end(),
101            Event::FingerDown(fe) => {
102                self._is_moving = true;
103                self.animator.play_anim(cx, Self::anim_down().get(cx));
104                match self.axis {
105                    Axis::Horizontal => cx.set_down_mouse_cursor(MouseCursor::RowResize),
106                    Axis::Vertical => cx.set_down_mouse_cursor(MouseCursor::ColResize)
107                };
108                self._drag_pos_start = self.pos;
109                self._drag_point = match self.axis {
110                    Axis::Horizontal => {fe.rel.y},
111                    Axis::Vertical => {fe.rel.x}
112                }
113            },
114            Event::FingerHover(fe) => {
115                match self.axis {
116                    Axis::Horizontal => cx.set_hover_mouse_cursor(MouseCursor::RowResize),
117                    Axis::Vertical => cx.set_hover_mouse_cursor(MouseCursor::ColResize)
118                };
119                if !self._is_moving {
120                    match fe.hover_state {
121                        HoverState::In => {
122                            self.animator.play_anim(cx, Self::anim_over().get(cx));
123                        },
124                        HoverState::Out => {
125                            self.animator.play_anim(cx, Self::anim_default().get(cx));
126                        },
127                        _ => ()
128                    }
129                }
130            },
131            Event::FingerUp(fe) => {
132                self._is_moving = false;
133                if fe.is_over {
134                    if !fe.is_touch {
135                        self.animator.play_anim(cx, Self::anim_over().get(cx));
136                    }
137                    else {
138                        self.animator.play_anim(cx, Self::anim_default().get(cx));
139                    }
140                }
141                else {
142                    self.animator.play_anim(cx, Self::anim_default().get(cx));
143                }
144                // we should change our mode based on which edge we are closest to
145                // the rule is center - 30 + 30
146                let center = self._drag_max_pos * 0.5;
147                if self._calc_pos > center - self.realign_dist &&
148                self._calc_pos < center + self.realign_dist {
149                    self.align = SplitterAlign::Weighted;
150                    self.pos = self._calc_pos / self._drag_max_pos;
151                }
152                else if self._calc_pos < center - self.realign_dist {
153                    
154                    self.align = SplitterAlign::First;
155                    self.pos = self._calc_pos;
156                }
157                else {
158                    self.align = SplitterAlign::Last;
159                    self.pos = self._drag_max_pos - self._calc_pos;
160                }
161                
162                return SplitterEvent::MovingEnd {
163                    new_align: self.align.clone(),
164                    new_pos: self.pos
165                }
166            },
167            Event::FingerMove(fe) => {
168                let delta = match self.axis {
169                    Axis::Horizontal => {
170                        fe.abs_start.y - fe.abs.y
171                    },
172                    Axis::Vertical => {
173                        fe.abs_start.x - fe.abs.x
174                    }
175                };
176                let mut pos = match self.align {
177                    SplitterAlign::First => self._drag_pos_start - delta,
178                    SplitterAlign::Last => self._drag_pos_start + delta,
179                    SplitterAlign::Weighted => self._drag_pos_start * self._drag_max_pos - delta
180                };
181                if pos > self._drag_max_pos - self.min_size {
182                    pos = self._drag_max_pos - self.min_size
183                }
184                else if pos < self.min_size {
185                    pos = self.min_size
186                };
187                let calc_pos = match self.align {
188                    SplitterAlign::First => {
189                        self.pos = pos;
190                        pos
191                    },
192                    SplitterAlign::Last => {
193                        self.pos = pos;
194                        self._drag_max_pos - pos
195                    },
196                    SplitterAlign::Weighted => {
197                        self.pos = pos / self._drag_max_pos;
198                        pos
199                    }
200                };
201                // pixelsnap calc_pos
202                if calc_pos != self._calc_pos {
203                    self._calc_pos = calc_pos;
204                    cx.redraw_child_area(self._split_area);
205                    return SplitterEvent::Moving {new_pos: self.pos};
206                }
207            }
208            _ => ()
209        };
210        SplitterEvent::None
211    }
212    
213    pub fn set_splitter_state(&mut self, align: SplitterAlign, pos: f32, axis: Axis) {
214        self.axis = axis;
215        self.align = align;
216        self.pos = pos;
217        match self.axis {
218            Axis::Horizontal => {
219                self._hit_state_margin = Some(Margin {
220                    l: 0.,
221                    t: 3.,
222                    r: 0.,
223                    b: 7.,
224                })
225            },
226            Axis::Vertical => {
227                self._hit_state_margin = Some(Margin {
228                    l: 3.,
229                    t: 0.,
230                    r: 7.,
231                    b: 0.,
232                })
233            }
234        }
235    }
236    
237    pub fn begin_splitter(&mut self, cx: &mut Cx) {
238        self.animator.init(cx, | cx | Self::anim_default().get(cx));
239        let rect = cx.get_turtle_rect();
240        self._calc_pos = match self.align {
241            SplitterAlign::First => self.pos,
242            SplitterAlign::Last => match self.axis {
243                Axis::Horizontal => rect.h - self.pos,
244                Axis::Vertical => rect.w - self.pos
245            },
246            SplitterAlign::Weighted => self.pos * match self.axis {
247                Axis::Horizontal => rect.h,
248                Axis::Vertical => rect.w
249            }
250        };
251        let dpi_factor = cx.get_dpi_factor_of(&self._split_area);
252        self._calc_pos -= self._calc_pos % (1.0 / dpi_factor);
253        match self.axis {
254            Axis::Horizontal => {
255                cx.begin_turtle(Layout {
256                    walk: Walk::wh(Width::Fill, Height::Fix(self._calc_pos)),
257                    ..Layout::default()
258                }, Area::Empty)
259            },
260            Axis::Vertical => {
261                cx.begin_turtle(Layout {
262                    walk: Walk::wh(Width::Fix(self._calc_pos), Height::Fill),
263                    ..Layout::default()
264                }, Area::Empty)
265            }
266        }
267    }
268    
269    pub fn mid_splitter(&mut self, cx: &mut Cx) {
270        cx.end_turtle(Area::Empty);
271        let rect = cx.get_turtle_rect();
272        let origin = cx.get_turtle_origin();
273        self.bg.shader = Self::shader_bg().get(cx);
274        self.bg.color = self.animator.last_color(cx, Quad::instance_color());
275        match self.axis {
276            Axis::Horizontal => {
277                cx.set_turtle_pos(Vec2 {x: origin.x, y: origin.y + self._calc_pos});
278                if let Ok(_) = self.split_view.begin_view(cx, Layout {
279                    walk: Walk::wh(Width::Fix(rect.w), Height::Fix(self.split_size)),
280                    ..Layout::default()
281                }) {
282                    self._split_area = self.bg.draw_quad_rel(cx, Rect {x: 0., y: 0., w: rect.w, h: self.split_size}).into();
283                    self.split_view.end_view(cx);
284                }
285                cx.set_turtle_pos(Vec2 {x: origin.x, y: origin.y + self._calc_pos + self.split_size});
286            },
287            Axis::Vertical => {
288                cx.set_turtle_pos(Vec2 {x: origin.x + self._calc_pos, y: origin.y});
289                if let Ok(_) = self.split_view.begin_view(cx, Layout {
290                    walk: Walk::wh(Width::Fix(self.split_size), Height::Fix(rect.h)),
291                    ..Layout::default()
292                }) {
293                    self._split_area = self.bg.draw_quad_rel(cx, Rect {x: 0., y: 0., w: self.split_size, h: rect.h}).into();
294                    self.split_view.end_view(cx);
295                }
296            }
297        };
298        cx.begin_turtle(Layout::default(), Area::Empty);
299    }
300    
301    pub fn end_splitter(&mut self, cx: &mut Cx) {
302        cx.end_turtle(Area::Empty);
303        // draw the splitter in the middle of the turtle
304        let rect = cx.get_turtle_rect();
305        
306        match self.axis {
307            Axis::Horizontal => {
308                self._drag_max_pos = rect.h;
309            },
310            Axis::Vertical => {
311                self._drag_max_pos = rect.w;
312            }
313        };
314        
315        self.animator.set_area(cx, self._split_area);
316    }
317}