Skip to main content

vizia_core/views/
resizable_stack.rs

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