Skip to main content

sequoia_elements/
resize_handle.rs

1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Edges, Length, Point, Rect, Size};
5use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
6use sequoia_layout::{Axis, LayoutCtx, LayoutStyle};
7use sequoia_reactive::Signal;
8use sequoia_style::{ResizeHandleStyle, VisualState};
9use sequoia_view::{EventCtx, PaintCtx, Unit, View};
10
11const DEFAULT_HIT: f32 = 7.0;
12
13const LINE_IDLE: f32 = 1.0;
14
15const LINE_ACTIVE: f32 = 2.0;
16
17const IDLE_COLOR: Color = Color::rgb8(0x75, 0x75, 0x75);
18
19const ACTIVE_COLOR: Color = Color::rgb8(0x0D, 0x99, 0xFF);
20
21#[derive(Clone, Copy)]
22struct DragState {
23    origin: f32,
24    value: f32,
25}
26
27#[derive(Clone)]
28pub struct ResizeHandle {
29    value: Signal<f32>,
30    min: f32,
31    max: f32,
32    hit: f32,
33    axis: Axis,
34    invert: bool,
35    gutter: Color,
36    style: Option<ResizeHandleStyle>,
37    idle: Option<Color>,
38    active: Option<Color>,
39    idle_width: Option<f32>,
40    active_width: Option<f32>,
41    drag: Rc<Cell<Option<DragState>>>,
42    margin: Edges,
43}
44
45impl ResizeHandle {
46    pub fn new(value: Signal<f32>) -> Self {
47        Self {
48            value,
49            min: 0.0,
50            max: f32::INFINITY,
51            hit: DEFAULT_HIT,
52            axis: Axis::Horizontal,
53            invert: false,
54            gutter: Color::TRANSPARENT,
55            style: None,
56            idle: None,
57            active: None,
58            idle_width: None,
59            active_width: None,
60            drag: Rc::new(Cell::new(None)),
61            margin: Edges::ZERO,
62        }
63    }
64
65    pub fn min(mut self, min: f32) -> Self {
66        self.min = if min.is_finite() { min.max(0.0) } else { 0.0 };
67        self
68    }
69
70    pub fn max(mut self, max: f32) -> Self {
71        self.max = if max.is_nan() {
72            f32::INFINITY
73        } else {
74            max.max(0.0)
75        };
76        self
77    }
78
79    pub fn hit_width(mut self, hit: f32) -> Self {
80        self.hit = if hit.is_finite() {
81            hit.max(3.0)
82        } else {
83            DEFAULT_HIT
84        };
85        self
86    }
87
88    pub fn axis(mut self, axis: Axis) -> Self {
89        self.axis = axis;
90        self
91    }
92
93    pub fn invert(mut self, invert: bool) -> Self {
94        self.invert = invert;
95        self
96    }
97
98    pub fn gutter(mut self, color: Color) -> Self {
99        self.gutter = color;
100        self
101    }
102
103    pub fn style(mut self, style: ResizeHandleStyle) -> Self {
104        self.style = Some(style);
105        self
106    }
107
108    pub fn idle_color(mut self, color: Color) -> Self {
109        self.idle = Some(color);
110        self
111    }
112
113    pub fn active_color(mut self, color: Color) -> Self {
114        self.active = Some(color);
115        self
116    }
117
118    pub fn idle_width(mut self, width: f32) -> Self {
119        self.idle_width = Some(width);
120        self
121    }
122
123    pub fn active_width(mut self, width: f32) -> Self {
124        self.active_width = Some(width);
125        self
126    }
127
128    pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
129        let m = margin.into();
130        self.margin = Edges::only(
131            sanitize_margin(m.left),
132            sanitize_margin(m.top),
133            sanitize_margin(m.right),
134            sanitize_margin(m.bottom),
135        );
136        self
137    }
138
139    pub fn margin_xy(self, x: f32, y: f32) -> Self {
140        self.margin(Edges::xy(x, y))
141    }
142
143    pub fn margin_x(mut self, x: f32) -> Self {
144        let x = sanitize_margin(x);
145        self.margin.left = x;
146        self.margin.right = x;
147        self
148    }
149
150    pub fn margin_y(mut self, y: f32) -> Self {
151        let y = sanitize_margin(y);
152        self.margin.top = y;
153        self.margin.bottom = y;
154        self
155    }
156
157    pub fn margin_left(mut self, v: f32) -> Self {
158        self.margin.left = sanitize_margin(v);
159        self
160    }
161
162    pub fn margin_right(mut self, v: f32) -> Self {
163        self.margin.right = sanitize_margin(v);
164        self
165    }
166
167    pub fn margin_top(mut self, v: f32) -> Self {
168        self.margin.top = sanitize_margin(v);
169        self
170    }
171
172    pub fn margin_bottom(mut self, v: f32) -> Self {
173        self.margin.bottom = sanitize_margin(v);
174        self
175    }
176
177    fn resolved_gutter(&self) -> Color {
178        if self.gutter != Color::TRANSPARENT {
179            self.gutter
180        } else {
181            self.style
182                .unwrap_or_default()
183                .gutter
184                .unwrap_or(Color::TRANSPARENT)
185        }
186    }
187
188    fn resolved_idle(&self) -> Color {
189        let style = self.style.unwrap_or_default();
190        self.idle.or(style.idle).unwrap_or(IDLE_COLOR)
191    }
192
193    fn resolved_active(&self) -> Color {
194        let style = self.style.unwrap_or_default();
195        self.active.or(style.active).unwrap_or(ACTIVE_COLOR)
196    }
197
198    fn resolved_idle_width(&self) -> f32 {
199        let style = self.style.unwrap_or_default();
200        self.idle_width.or(style.idle_width).unwrap_or(LINE_IDLE)
201    }
202
203    fn resolved_active_width(&self) -> f32 {
204        let style = self.style.unwrap_or_default();
205        self.active_width
206            .or(style.active_width)
207            .unwrap_or(LINE_ACTIVE)
208    }
209
210    fn clamped(&self, v: f32) -> f32 {
211        v.clamp(self.min, self.max)
212    }
213
214    fn axis_pos(&self, pos: &Point) -> f32 {
215        match self.axis {
216            Axis::Horizontal => pos.x,
217            Axis::Vertical => pos.y,
218        }
219    }
220}
221
222impl View for ResizeHandle {
223    fn reuse_state(&self, old: &Self) {
224        self.drag.set(old.drag.get());
225    }
226
227    fn body(&self) -> impl View + 'static {
228        Unit
229    }
230
231    fn is_leaf(&self) -> bool {
232        true
233    }
234
235    fn captures_pointer(&self) -> bool {
236        true
237    }
238
239    fn has_visual_state(&self) -> bool {
240        true
241    }
242
243    fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
244        match self.axis {
245            Axis::Horizontal => CursorIcon::ColResize,
246            Axis::Vertical => CursorIcon::RowResize,
247        }
248    }
249
250    fn layout_style(&self) -> LayoutStyle {
251        match self.axis {
252            Axis::Horizontal => LayoutStyle {
253                width: Length::Px(self.hit),
254                height: Length::Fill(1.0),
255                margin: self.margin,
256                ..LayoutStyle::default()
257            },
258            Axis::Vertical => LayoutStyle {
259                width: Length::Fill(1.0),
260                height: Length::Px(self.hit),
261                margin: self.margin,
262                ..LayoutStyle::default()
263            },
264        }
265    }
266
267    fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
268        Some(match self.axis {
269            Axis::Horizontal => Size::new(self.hit, 0.0),
270            Axis::Vertical => Size::new(0.0, self.hit),
271        })
272    }
273
274    fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
275        if ctx.phase != EventPhase::Target {
276            return;
277        }
278        match event {
279            Event::Pointer(PointerEvent::Down {
280                button: PointerButton::Left,
281                pos,
282                ..
283            }) => {
284                let p = self.axis_pos(pos);
285                self.drag.set(Some(DragState {
286                    origin: p,
287                    value: self.value.get_untracked(),
288                }));
289                ctx.capture_pointer();
290                ctx.handled();
291            }
292            Event::Pointer(PointerEvent::Move { pos, .. }) => {
293                if let Some(st) = self.drag.get() {
294                    let p = self.axis_pos(pos);
295                    let delta = if self.invert {
296                        st.origin - p
297                    } else {
298                        p - st.origin
299                    };
300                    self.value.set(self.clamped(st.value + delta));
301                    ctx.handled();
302                }
303            }
304            Event::Pointer(PointerEvent::Up {
305                button: PointerButton::Left,
306                ..
307            }) => {
308                self.drag.set(None);
309            }
310            Event::Pointer(PointerEvent::Cancel) => {
311                self.drag.set(None);
312            }
313            _ => {}
314        }
315    }
316
317    fn paint_self(&self, ctx: &mut PaintCtx) {
318        let r = ctx.rect;
319        if r.is_empty() {
320            return;
321        }
322
323        let gutter = self.resolved_gutter();
324        if gutter.a > 0.0 {
325            ctx.quad(r, gutter, 0.0);
326        }
327        let active = ctx.state.hovered || ctx.state.pressed || self.drag.get().is_some();
328        let (w, c) = if active {
329            (self.resolved_active_width(), self.resolved_active())
330        } else {
331            (self.resolved_idle_width(), self.resolved_idle())
332        };
333        match self.axis {
334            Axis::Horizontal => {
335                let cx = r.left() + r.size.width * 0.5;
336                ctx.quad(
337                    Rect::from_xywh(cx - w * 0.5, r.top(), w, r.size.height),
338                    c,
339                    0.0,
340                );
341            }
342            Axis::Vertical => {
343                let cy = r.top() + r.size.height * 0.5;
344                ctx.quad(
345                    Rect::from_xywh(r.left(), cy - w * 0.5, r.size.width, w),
346                    c,
347                    0.0,
348                );
349            }
350        }
351    }
352}
353
354fn sanitize_margin(v: f32) -> f32 {
355    if v.is_finite() { v.max(0.0) } else { 0.0 }
356}