makepad_widgets/
splitter.rs

1use crate::{
2    makepad_derive_widget::*,
3    makepad_micro_serde::*,
4    makepad_draw::*,
5    widget::*,
6};
7
8live_design!{
9    link widgets;
10    use link::theme::*;
11    use makepad_draw::shader::std::*;
12    
13    pub DrawSplitter= {{DrawSplitter}} {}
14    pub SplitterBase = {{Splitter}} {}
15    pub Splitter = <SplitterBase> {
16        draw_bg: {
17            instance drag: 0.0
18            instance hover: 0.0
19            
20            uniform size: 110.0
21
22            uniform color: (THEME_COLOR_D_HIDDEN),
23            uniform color_hover: (THEME_COLOR_OUTSET_HOVER),
24            uniform color_drag: (THEME_COLOR_OUTSET_DRAG)
25            
26            uniform border_radius: 1.0
27            uniform splitter_pad: 1.0
28            
29            fn pixel(self) -> vec4 {
30                let sdf = Sdf2d::viewport(self.pos * self.rect_size);
31                sdf.clear(THEME_COLOR_BG_APP); // TODO: This should be a transparent color instead.
32                
33                if self.is_vertical > 0.5 {
34                    sdf.box(
35                        self.splitter_pad,
36                        self.rect_size.y * 0.5 - self.size * 0.5,
37                        self.rect_size.x - 2.0 * self.splitter_pad,
38                        self.size,
39                        self.border_radius
40                    );
41                }
42                else {
43                    sdf.box(
44                        self.rect_size.x * 0.5 - self.size * 0.5,
45                        self.splitter_pad,
46                        self.size,
47                        self.rect_size.y - 2.0 * self.splitter_pad,
48                        self.border_radius
49                    );
50                }
51
52                return sdf.fill_keep(
53                    mix(
54                        self.color,
55                        mix(
56                            self.color_hover,
57                            self.color_drag,
58                            self.drag
59                        ),
60                        self.hover
61                    )
62                );
63            }
64        }
65
66        size: (THEME_SPLITTER_SIZE)
67        min_horizontal: (THEME_SPLITTER_MIN_HORIZONTAL)
68        max_horizontal: (THEME_SPLITTER_MAX_HORIZONTAL)
69        min_vertical: (THEME_SPLITTER_MIN_VERTICAL)
70        max_vertical: (THEME_SPLITTER_MAX_VERTICAL)
71        
72        animator: {
73            hover = {
74                default: off
75                off = {
76                    from: {all: Forward {duration: 0.1}}
77                    apply: {
78                        draw_bg: {drag: 0.0, hover: 0.0}
79                    }
80                }
81                
82                on = {
83                    from: {
84                        all: Forward {duration: 0.1}
85                        state_drag: Forward {duration: 0.01}
86                    }
87                    apply: {
88                        draw_bg: {
89                            drag: 0.0,
90                            hover: [{time: 0.0, value: 1.0}],
91                        }
92                    }
93                }
94                
95                drag = {
96                    from: { all: Forward { duration: 0.1 }}
97                    apply: {
98                        draw_bg: {
99                            drag: [{time: 0.0, value: 1.0}],
100                            hover: 1.0,
101                        }
102                    }
103                }
104            }
105        }
106    }
107    
108}
109
110
111#[derive(Live, LiveHook, LiveRegister)]
112#[repr(C)]
113pub struct DrawSplitter {
114    #[deref] draw_super: DrawQuad,
115    #[live] is_vertical: f32,
116}
117
118#[derive(Copy, Clone, Debug, Live, LiveHook, SerRon, DeRon)]
119#[live_ignore]
120pub enum SplitterAxis {
121    #[pick] Horizontal,
122    Vertical
123}
124
125impl Default for SplitterAxis {
126    fn default() -> Self {
127        SplitterAxis::Horizontal
128    }
129}
130
131
132#[derive(Clone, Copy, Debug, Live, LiveHook, SerRon, DeRon)]
133#[live_ignore]
134pub enum SplitterAlign {
135    #[live(50.0)] FromA(f64),
136    #[live(50.0)] FromB(f64),
137    #[pick(0.5)] Weighted(f64),
138}
139
140impl SplitterAlign {
141    fn to_position(self, axis: SplitterAxis, rect: Rect) -> f64 {
142        match axis {
143            SplitterAxis::Horizontal => match self {
144                Self::FromA(position) => position,
145                Self::FromB(position) => rect.size.x - position,
146                Self::Weighted(weight) => weight * rect.size.x,
147            },
148            SplitterAxis::Vertical => match self {
149                Self::FromA(position) => position,
150                Self::FromB(position) => rect.size.y - position,
151                Self::Weighted(weight) => weight * rect.size.y,
152            },
153        }
154    }
155}
156
157#[derive(Live, LiveHook, Widget)]
158pub struct Splitter {
159    #[live(SplitterAxis::Horizontal)] pub axis: SplitterAxis,
160    #[live(SplitterAlign::Weighted(0.5))] pub align: SplitterAlign,
161    #[rust] rect: Rect,
162    #[rust] position: f64,
163    #[rust] drag_start_align: Option<SplitterAlign>,
164    #[rust] area_a: Area,
165    #[rust] area_b: Area,
166    #[animator] animator: Animator,
167    
168    #[live] min_vertical: f64,
169    #[live] max_vertical: f64,
170    #[live] min_horizontal: f64,
171    #[live] max_horizontal: f64,
172    
173    #[redraw] #[live] draw_bg: DrawSplitter,
174    #[live] size: f64,
175    
176    // framecomponent mode
177    #[rust] draw_state: DrawStateWrap<DrawState>,
178    #[find] #[live] a: WidgetRef,
179    #[find] #[live] b: WidgetRef,
180    #[walk] walk: Walk,
181}
182
183#[derive(Clone)]
184enum DrawState {
185    DrawA,
186    DrawSplit,
187    DrawB
188}
189
190impl Widget for Splitter {
191    fn handle_event(&mut self, cx: &mut Cx, event: &Event, scope: &mut Scope) {
192        let uid = self.widget_uid();
193        
194        self.animator_handle_event(cx, event);
195        match event.hits_with_options(cx, self.draw_bg.area(), HitOptions::new().with_margin(self.margin())) {
196            Hit::FingerHoverIn(_) => {
197                match self.axis {
198                    SplitterAxis::Horizontal => cx.set_cursor(MouseCursor::ColResize),
199                    SplitterAxis::Vertical => cx.set_cursor(MouseCursor::RowResize),
200                }
201                self.animator_play(cx, id!(hover.on));
202            }
203            Hit::FingerHoverOut(_) => {
204                self.animator_play(cx, id!(hover.off));
205            },
206            Hit::FingerDown(_) => {
207                match self.axis {
208                    SplitterAxis::Horizontal => cx.set_cursor(MouseCursor::ColResize),
209                    SplitterAxis::Vertical => cx.set_cursor(MouseCursor::RowResize),
210                }
211                self.animator_play(cx, id!(hover.drag));
212                self.drag_start_align = Some(self.align);
213            }
214            Hit::FingerUp(f) => {
215                self.drag_start_align = None;
216                if f.is_over && f.device.has_hovers() {
217                    self.animator_play(cx, id!(hover.on));
218                }
219                else {
220                    self.animator_play(cx, id!(hover.off));
221                }
222            }
223            Hit::FingerMove(f) => {
224                if let Some(drag_start_align) = self.drag_start_align {
225                    let delta = match self.axis {
226                        SplitterAxis::Horizontal => f.abs.x - f.abs_start.x,
227                        SplitterAxis::Vertical => f.abs.y - f.abs_start.y,
228                    };
229                    let new_position =
230                    drag_start_align.to_position(self.axis, self.rect) + delta;
231                    self.align = match self.axis {
232                        SplitterAxis::Horizontal => {
233                            let center = self.rect.size.x / 2.0;
234                            if new_position < center - 30.0 {
235                                SplitterAlign::FromA(new_position.max(self.min_vertical))
236                            } else if new_position > center + 30.0 {
237                                SplitterAlign::FromB((self.rect.size.x - new_position).max(self.max_vertical))
238                            } else {
239                                SplitterAlign::Weighted(new_position / self.rect.size.x)
240                            }
241                        }
242                        SplitterAxis::Vertical => {
243                            let center = self.rect.size.y / 2.0;
244                            if new_position < center - 30.0 {
245                                SplitterAlign::FromA(new_position.max(self.min_horizontal))
246                            } else if new_position > center + 30.0 {
247                                SplitterAlign::FromB((self.rect.size.y - new_position).max(self.max_horizontal))
248                            } else {
249                                SplitterAlign::Weighted(new_position / self.rect.size.y)
250                            }
251                        }
252                    };
253                    self.draw_bg.redraw(cx);
254                    cx.widget_action(uid, &scope.path, SplitterAction::Changed {axis: self.axis, align: self.align});
255                    
256                    self.a.redraw(cx);
257                    self.b.redraw(cx);
258                }
259            }
260            _ => {}
261        }
262        self.a.handle_event(cx, event, scope);
263        self.b.handle_event(cx, event, scope);
264    }
265    
266    fn draw_walk(&mut self, cx: &mut Cx2d, scope:&mut Scope, walk: Walk) -> DrawStep {
267        if self.draw_state.begin(cx, DrawState::DrawA) {
268            self.begin(cx, walk);
269        }
270        if let Some(DrawState::DrawA) = self.draw_state.get() {
271            self.a.draw(cx, scope) ?;
272            self.draw_state.set(DrawState::DrawSplit);
273        }
274        if let Some(DrawState::DrawSplit) = self.draw_state.get() {
275            self.middle(cx);
276            self.draw_state.set(DrawState::DrawB)
277        }
278        if let Some(DrawState::DrawB) = self.draw_state.get() {
279            self.b.draw(cx, scope) ?;
280            self.end(cx);
281            self.draw_state.end();
282        }
283        DrawStep::done()
284    }
285}
286
287impl Splitter {
288    pub fn begin(&mut self, cx: &mut Cx2d, walk: Walk) {
289        // we should start a fill turtle in the layout direction of choice
290        match self.axis {
291            SplitterAxis::Horizontal => {
292                cx.begin_turtle(walk, Layout::flow_right());
293            }
294            SplitterAxis::Vertical => {
295                cx.begin_turtle(walk, Layout::flow_down());
296            }
297        }
298        
299        self.rect = cx.turtle().padded_rect();
300        self.position = self.align.to_position(self.axis, self.rect);
301        
302        let walk = match self.axis {
303            SplitterAxis::Horizontal => Walk::size(Size::Fixed(self.position), Size::Fill),
304            SplitterAxis::Vertical => Walk::size(Size::Fill, Size::Fixed(self.position)),
305        };
306        cx.begin_turtle(walk, Layout::flow_down());
307    }
308    
309    pub fn middle(&mut self, cx: &mut Cx2d) {
310        cx.end_turtle_with_area(&mut self.area_a);
311        match self.axis {
312            SplitterAxis::Horizontal => {
313                self.draw_bg.is_vertical = 1.0;
314                self.draw_bg.draw_walk(cx, Walk::size(Size::Fixed(self.size), Size::Fill));
315            }
316            SplitterAxis::Vertical => {
317                self.draw_bg.is_vertical = 0.0;
318                self.draw_bg.draw_walk(cx, Walk::size(Size::Fill, Size::Fixed(self.size)));
319            }
320        }
321        cx.begin_turtle(Walk::default(), Layout::flow_down());
322    }
323    
324    pub fn end(&mut self, cx: &mut Cx2d) {
325        cx.end_turtle_with_area(&mut self.area_b);
326        cx.end_turtle();
327    }
328    
329    pub fn axis(&self) -> SplitterAxis {
330        self.axis
331    }
332
333    pub fn area_a(&self) -> Area {
334        self.area_a
335    }
336    
337    pub fn area_b(&self) -> Area {
338        self.area_b
339    }
340    
341    pub fn set_axis(&mut self, axis: SplitterAxis) {
342        self.axis = axis;
343    }
344    
345    pub fn align(&self) -> SplitterAlign {
346        self.align
347    }
348    
349    pub fn set_align(&mut self, align: SplitterAlign) {
350        self.align = align;
351    }
352    
353    fn margin(&self) -> Margin {
354        match self.axis {
355            SplitterAxis::Horizontal => Margin {
356                left: 3.0,
357                top: 0.0,
358                right: 3.0,
359                bottom: 0.0,
360            },
361            SplitterAxis::Vertical => Margin {
362                left: 0.0,
363                top: 3.0,
364                right: 0.0,
365                bottom: 3.0,
366            },
367        }
368    }
369}
370
371#[derive(Clone, Debug, DefaultNone)]
372pub enum SplitterAction {
373    None,
374    Changed {axis: SplitterAxis, align: SplitterAlign},
375}