Skip to main content

vizia_core/views/
resizable.rs

1use crate::prelude::*;
2
3/// A direction for resizing a resizable stack from one of its edges.
4#[derive(PartialEq, Clone, Copy)]
5pub enum ResizeStackDirection {
6    Left,
7    Right,
8    Top,
9    Bottom,
10}
11
12impl ResizeStackDirection {
13    fn is_horizontal(self) -> bool {
14        matches!(self, Self::Left | Self::Right)
15    }
16
17    fn is_vertical(self) -> bool {
18        matches!(self, Self::Top | Self::Bottom)
19    }
20
21    fn resizes_from_leading_edge(self) -> bool {
22        matches!(self, Self::Left | Self::Top)
23    }
24}
25
26/// A view that can be resized by clicking and dragging from one of its edges.
27///
28/// The `Resizable` struct allows users to create a resizable container in a user interface.
29/// It supports resizing in either a horizontal or vertical direction, as specified
30/// by the `direction` field. The resizing behavior is controlled via the `on_drag` callback, which
31/// is triggered during a drag operation.
32pub struct Resizable {
33    /// Tracks whether the edge of the view is currently being dragged.
34    is_dragging: bool,
35
36    /// A callback function that is triggered when the view is being dragged.
37    /// The callback receives a mutable reference to the event context and the new size
38    /// of the stack as a floating-point value.
39    on_drag: Box<dyn Fn(&mut EventContext, f32)>,
40
41    /// An optional callback function that is called when the stack is reset.
42    /// This callback is triggered when the user double-clicks the resize handle,
43    /// allowing the stack to return to its default size.
44    on_reset: Option<Box<dyn Fn(&mut EventContext)>>,
45
46    /// Specifies the edge from which the stack can be resized.
47    direction: Memo<ResizeStackDirection>,
48
49    /// The mouse position on the active axis when dragging starts.
50    drag_start: f32,
51
52    /// The stack size when dragging starts, in logical pixels.
53    start_size: f32,
54}
55
56impl Resizable {
57    /// Creates a new `Resizable` view.
58    /// The `size` parameter is a `Res<Units>` source for the stack size, updated when the stack is resized.
59    /// The `direction` parameter specifies which edge of the stack is resizable.
60    /// The `on_drag` callback is called with the new size when the stack is being resized.
61    /// The `content` closure is called to build the content of the stack.
62    pub fn new<F>(
63        cx: &mut Context,
64        size: impl Res<Units> + Copy + 'static,
65        direction: ResizeStackDirection,
66        on_drag: impl Fn(&mut EventContext, f32) + 'static,
67        content: F,
68    ) -> Handle<Self>
69    where
70        F: FnOnce(&mut Context),
71    {
72        let text_direction = cx.environment().direction;
73        let direction = Memo::new(move |_| {
74            if text_direction.get() == Direction::RightToLeft {
75                match direction {
76                    ResizeStackDirection::Left => ResizeStackDirection::Right,
77                    ResizeStackDirection::Right => ResizeStackDirection::Left,
78                    other => other,
79                }
80            } else {
81                direction
82            }
83        });
84
85        Self {
86            is_dragging: false,
87            on_drag: Box::new(on_drag),
88            on_reset: None,
89            direction,
90            drag_start: 0.0,
91            start_size: 0.0,
92        }
93        .build(cx, |cx| {
94            ResizeHandle::new(cx);
95            (content)(cx);
96        })
97        .toggle_class("horizontal", direction.map(|dir| dir.is_vertical()))
98        .toggle_class("vertical", direction.map(|dir| dir.is_horizontal()))
99        .toggle_class("left", direction.map(|dir| *dir == ResizeStackDirection::Left))
100        .toggle_class("right", direction.map(|dir| *dir == ResizeStackDirection::Right))
101        .toggle_class("top", direction.map(|dir| *dir == ResizeStackDirection::Top))
102        .toggle_class("bottom", direction.map(|dir| *dir == ResizeStackDirection::Bottom))
103        .bind(direction, move |handle| {
104            if direction.get().is_horizontal() {
105                handle.width(size);
106            } else {
107                handle.height(size);
108            }
109        })
110
111        // if direction.get().is_horizontal() { handle.width(size) } else { handle.height(size) }
112    }
113}
114
115/// Events emitted by the `Resizable` view to indicate changes in dragging state.
116pub enum ResizableEvent {
117    /// Emitted when the user starts dragging the resizable edge of the stack.
118    /// This event is triggered when the user presses down on the resize handle.
119    /// It enables dragging behavior and locks the cursor.
120    StartDrag {
121        cursor_x: f32, // The x-position of the mouse cursor when dragging starts.
122        cursor_y: f32, // The y-position of the mouse cursor when dragging starts.
123    },
124
125    /// Emitted when the user stops dragging the resizable edge of the stack.
126    /// This event is triggered when the user releases the mouse button after dragging.
127    /// It disables dragging behavior and unlocks the cursor.
128    StopDrag,
129
130    /// Emitted when the user double-clicks the resize handle.
131    Reset,
132}
133
134impl View for Resizable {
135    fn element(&self) -> Option<&'static str> {
136        Some("resizable")
137    }
138
139    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
140        event.map(|resizable_event, event| match resizable_event {
141            ResizableEvent::StartDrag { cursor_x, cursor_y } => {
142                self.is_dragging = true;
143                cx.set_active(true);
144                cx.capture();
145                cx.lock_cursor_icon();
146                self.start_size = if self.direction.get().is_horizontal() {
147                    cx.bounds().w / cx.scale_factor()
148                } else {
149                    cx.bounds().h / cx.scale_factor()
150                };
151
152                // Disable pointer events for everything while dragging
153                cx.with_current(Entity::root(), |cx| {
154                    cx.set_pointer_events(false);
155                });
156
157                // Prevent propagation in case the resizable stack is within another resizable stack
158                event.consume();
159
160                if self.direction.get().is_horizontal() {
161                    self.drag_start = *cursor_x;
162                } else {
163                    self.drag_start = *cursor_y;
164                }
165            }
166
167            ResizableEvent::StopDrag => {
168                self.is_dragging = false;
169                cx.set_active(false);
170                cx.release();
171                cx.unlock_cursor_icon();
172
173                // Re-enable pointer events
174                cx.with_current(Entity::root(), |cx| {
175                    cx.set_pointer_events(true);
176                });
177
178                event.consume()
179            }
180
181            ResizableEvent::Reset => {
182                self.is_dragging = false;
183                cx.set_active(false);
184                cx.release();
185                cx.unlock_cursor_icon();
186
187                // Re-enable pointer events
188                cx.with_current(Entity::root(), |cx| {
189                    cx.set_pointer_events(true);
190                });
191
192                if let Some(on_reset) = &self.on_reset {
193                    on_reset(cx);
194                }
195
196                event.consume()
197            }
198        });
199
200        event.map(|window_event, _| match window_event {
201            WindowEvent::MouseMove(x, y) => {
202                let dpi = cx.scale_factor();
203                if self.is_dragging {
204                    let delta = if self.direction.get().is_horizontal() {
205                        (*x - self.drag_start) / dpi
206                    } else {
207                        (*y - self.drag_start) / dpi
208                    };
209
210                    let new_size = if self.direction.get().resizes_from_leading_edge() {
211                        self.start_size - delta
212                    } else {
213                        self.start_size + delta
214                    };
215
216                    if new_size.is_finite() && new_size > 5.0 {
217                        (self.on_drag)(cx, new_size);
218                    }
219                }
220            }
221
222            WindowEvent::MouseUp(button) if *button == MouseButton::Left => {
223                cx.emit(ResizableEvent::StopDrag);
224            }
225
226            _ => {}
227        });
228    }
229}
230
231impl Handle<'_, Resizable> {
232    /// Sets a callback to be called when the stack is reset, i.e. when the resize handle is double-clicked.
233    pub fn on_reset<F>(self, on_reset: F) -> Self
234    where
235        F: Fn(&mut EventContext) + 'static,
236    {
237        self.modify(|this| {
238            this.on_reset = Some(Box::new(on_reset));
239        })
240    }
241}
242
243pub struct ResizeHandle;
244
245impl ResizeHandle {
246    pub fn new(cx: &mut Context) -> Handle<Self> {
247        Self.build(cx, |_cx| {}).position_type(PositionType::Absolute).z_index(10)
248    }
249}
250
251impl View for ResizeHandle {
252    fn element(&self) -> Option<&'static str> {
253        Some("resize-handle")
254    }
255
256    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
257        event.map(|window_event, _| match window_event {
258            WindowEvent::PressDown { mouse } if *mouse => {
259                cx.emit(ResizableEvent::StartDrag {
260                    cursor_x: cx.mouse.cursor_x,
261                    cursor_y: cx.mouse.cursor_y,
262                });
263            }
264
265            WindowEvent::MouseDoubleClick(button) if *button == MouseButton::Left => {
266                cx.emit(ResizableEvent::Reset);
267            }
268
269            _ => {}
270        });
271    }
272}