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}