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