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}