vizia_core/views/
resizable.rs1use crate::prelude::*;
2
3#[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
26pub struct Resizable {
33 is_dragging: bool,
35
36 on_drag: Box<dyn Fn(&mut EventContext, f32)>,
40
41 on_reset: Option<Box<dyn Fn(&mut EventContext)>>,
45
46 direction: Memo<ResizeStackDirection>,
48
49 drag_start: f32,
51
52 start_size: f32,
54}
55
56impl Resizable {
57 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 }
113}
114
115pub enum ResizableEvent {
117 StartDrag {
121 cursor_x: f32, cursor_y: f32, },
124
125 StopDrag,
129
130 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 cx.with_current(Entity::root(), |cx| {
154 cx.set_pointer_events(false);
155 });
156
157 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 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 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 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}