Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/resizable.rs
StratoSDK / crates / strato-ui-core / src / elements / resizable.rs
1use std::{
2 mem,
3 sync::{Arc, Mutex, MutexGuard},
4};
5 
6use pathfinder_color::ColorU;
7use pathfinder_geometry::{
8 rect::RectF,
9 vector::{vec2f, Vector2F},
10};
11 
12use crate::{
13 event::DispatchedEvent, platform::Cursor, AfterLayoutContext, AppContext, Element,
14 EventContext, PaintContext, SizeConstraint,
15};
16 
17use super::{Fill, Point, ZIndex};
18 
19const DRAGBAR_WIDTH: f32 = 5.0;
20 
21/// A UI element with internal resizing ability.
22///
23/// This element takes a ResizableStateHandle to receive a starting size and
24/// manage dimensions as the element is resized.
25///
26/// Supports both horizontal and vertical resizing via the ResizeDirection.
27///
28/// TODO:
29/// - Take a configurable dragbar size instead of always using 5.0
30pub struct Resizable {
31 child: Box<dyn Element>,
32 origin: Option<Vector2F>,
33 dragbar: Dragbar,
34 state_handle: ResizableStateHandle,
35 size: Option<Vector2F>,
36 bounds_callback: Option<BoundsCallback>,
37 resize_handler: Option<Handler>,
38 start_resize_handler: Option<Handler>,
39 end_resize_handler: Option<Handler>,
40 hovering_dragbar: bool,
41 direction: ResizeDirection,
42 origin_delta: Vector2F,
43 dragbar_offset: f32,
44}
45 
46type Handler = Box<dyn FnMut(&mut EventContext, &AppContext)>;
47pub type BoundsCallback = Box<dyn FnMut(Vector2F) -> (f32, f32)>;
48 
49/// Similar to MouseStateHandle, the view that incorporates the owner can instantiate,
50/// read, and set the state.
51pub type ResizableStateHandle = Arc<Mutex<ResizableState>>;
52pub fn resizable_state_handle(size: f32) -> ResizableStateHandle {
53 Arc::new(Mutex::new(ResizableState::new(size)))
54}
55 
56pub struct ResizableState {
57 size: f32,
58 bounds: Option<(f32, f32)>,
59 mode: ResizableMode,
60}
61 
62#[derive(Default)]
63pub enum ResizableMode {
64 Dragging {
65 last_position: Vector2F,
66 },
67 #[default]
68 Stationary,
69}
70 
71impl ResizableState {
72 pub fn new(size: f32) -> Self {
73 Self {
74 size,
75 bounds: None,
76 mode: Default::default(),
77 }
78 }
79 pub fn size(&self) -> f32 {
80 self.size
81 }
82 
83 pub fn clamp_size(&mut self) {
84 if let Some((min, max)) = self.bounds {
85 self.size = self.size.clamp(min, max);
86 }
87 }
88 
89 fn check_for_resize(
90 &mut self,
91 position: Vector2F,
92 origin: Option<Vector2F>,
93 dragbar_side: DragBarSide,
94 ) -> Option<Vector2F> {
95 if let ResizableMode::Dragging { last_position } = self.mode {
96 self.resize(last_position, position, origin, dragbar_side)
97 } else {
98 None
99 }
100 }
101 
102 fn is_resizing(&self) -> bool {
103 matches!(self.mode, ResizableMode::Dragging { .. })
104 }
105 
106 fn resize(
107 &mut self,
108 old_position: Vector2F,
109 new_position: Vector2F,
110 origin: Option<Vector2F>,
111 dragbar_side: DragBarSide,
112 ) -> Option<Vector2F> {
113 let mut resized = false;
114 
115 if let Some(origin) = origin {
116 let delta = match dragbar_side {
117 DragBarSide::Right => new_position.x() - old_position.x(),
118 DragBarSide::Left => old_position.x() - new_position.x(),
119 DragBarSide::Bottom => new_position.y() - old_position.y(),
120 DragBarSide::Top => old_position.y() - new_position.y(),
121 };
122 
123 let old_size = self.size;
124 if delta.abs() >= f32::EPSILON {
125 resized = true;
126 self.size += delta;
127 self.clamp_size();
128 }
129 let size = self.size;
130 
131 // The last position should reflect the latest position of the dragbar.
132 let last_position = match dragbar_side {
133 // With a right-side dragbar, the latest position of the dragbar will
134 // be the old origin of the element plus the new width/height.
135 DragBarSide::Right => origin + vec2f(size, 0.),
136 // With a left-side dragbar, the latest position of the dragbar will
137 // be the old origin of the element minus the bounded delta of the drag.
138 DragBarSide::Left => origin - vec2f(size - old_size, 0.),
139 // With a bottom-side dragbar, the latest position of the dragbar will
140 // be the old origin of the element plus the new height.
141 DragBarSide::Bottom => origin + vec2f(0., size),
142 // With a top-side dragbar, the latest position of the dragbar will
143 // be the old origin of the element minus the bounded delta of the drag.
144 DragBarSide::Top => origin - vec2f(0., size - old_size),
145 };
146 
147 let origin_delta = match dragbar_side {
148 DragBarSide::Right => Vector2F::zero(),
149 DragBarSide::Left => vec2f(old_size - size, 0.),
150 DragBarSide::Bottom => Vector2F::zero(),
151 DragBarSide::Top => vec2f(0., old_size - size),
152 };
153 
154 self.mode = ResizableMode::Dragging { last_position };
155 
156 if resized {
157 Some(origin_delta)
158 } else {
159 None
160 }
161 } else {
162 None
163 }
164 }
165 
166 pub fn begin_resizing(&mut self, position: Vector2F) {
167 self.mode = ResizableMode::Dragging {
168 last_position: position,
169 };
170 }
171 
172 pub fn end_resizing(&mut self) {
173 self.mode = ResizableMode::Stationary;
174 }
175 
176 pub fn set_size(&mut self, new_size: f32) {
177 self.size = new_size;
178 }
179}
180 
181struct Dragbar {
182 bounds: Option<RectF>,
183 origin: Option<Point>,
184 size: Option<Vector2F>,
185 z_index: Option<ZIndex>,
186 color: Fill,
187 side: DragBarSide,
188}
189 
190#[derive(Copy, Clone, Default)]
191pub enum DragBarSide {
192 Left,
193 #[default]
194 Right,
195 Top,
196 Bottom,
197}
198 
199#[derive(Copy, Clone, Default)]
200pub enum ResizeDirection {
201 #[default]
202 Horizontal,
203 Vertical,
204}
205 
206impl Dragbar {
207 pub fn new() -> Self {
208 let color = Fill::Solid(ColorU::transparent_black());
209 Self {
210 bounds: None,
211 origin: None,
212 size: None,
213 z_index: None,
214 color,
215 side: Default::default(),
216 }
217 }
218}
219 
220impl Resizable {
221 pub fn new(state_handle: ResizableStateHandle, child: Box<dyn Element>) -> Self {
222 Self {
223 child,
224 origin: None,
225 state_handle,
226 size: None,
227 bounds_callback: None,
228 resize_handler: None,
229 start_resize_handler: None,
230 end_resize_handler: None,
231 dragbar: Dragbar::new(),
232 hovering_dragbar: false,
233 direction: ResizeDirection::Horizontal,
234 origin_delta: Vector2F::zero(),
235 dragbar_offset: 0.0,
236 }
237 }
238 
239 /// Adds a callback which will be called on a resize.
240 /// Generally, this should trigger a re-render in the parent.
241 pub fn on_resize<F>(mut self, callback: F) -> Self
242 where
243 F: FnMut(&mut EventContext, &AppContext) + 'static,
244 {
245 self.resize_handler = Some(Box::new(callback));
246 self
247 }
248 
249 pub fn on_start_resizing<F>(mut self, callback: F) -> Self
250 where
251 F: FnMut(&mut EventContext, &AppContext) + 'static,
252 {
253 self.start_resize_handler = Some(Box::new(callback));
254 self
255 }
256 
257 pub fn on_end_resizing<F>(mut self, callback: F) -> Self
258 where
259 F: FnMut(&mut EventContext, &AppContext) + 'static,
260 {
261 self.end_resize_handler = Some(Box::new(callback));
262 self
263 }
264 
265 /// Sets a function that computes the (min, max) bounds on the width/height
266 /// of the resizable. The bounds are updated at paint time.
267 pub fn with_bounds_callback(mut self, callback: BoundsCallback) -> Self {
268 self.bounds_callback = Some(callback);
269 self
270 }
271 
272 pub fn with_dragbar_color(mut self, color: Fill) -> Self {
273 self.dragbar.color = color;
274 self
275 }
276 
277 pub fn with_dragbar_side(mut self, side: DragBarSide) -> Self {
278 self.dragbar.side = side;
279 // Automatically set direction based on side
280 self.direction = match side {
281 DragBarSide::Left | DragBarSide::Right => ResizeDirection::Horizontal,
282 DragBarSide::Top | DragBarSide::Bottom => ResizeDirection::Vertical,
283 };
284 self
285 }
286 
287 /// Sets an offset for the dragbar position.
288 /// Positive values move the dragbar outwards (away from the center of the element).
289 /// Negative values move the dragbar inwards (towards the center of the element).
290 pub fn with_dragbar_offset(mut self, offset: f32) -> Self {
291 self.dragbar_offset = offset;
292 self
293 }
294 
295 fn state(&mut self) -> MutexGuard<'_, ResizableState> {
296 self.state_handle
297 .lock()
298 .expect("Resizable state should be accessible")
299 }
300 
301 /// Determine if the mouse is hovering over the dragbar
302 ///
303 /// If there is another element above this one at the cursor position, then we treat that as
304 /// outside the element for purposes of MouseState
305 fn is_mouse_hovering_dragbar(&self, ctx: &EventContext, position: Vector2F) -> bool {
306 let Some(dragbar_origin) = self.dragbar.origin else {
307 log::warn!("self.origin was None in `Hoverable::is_mouse_in`");
308 return false;
309 };
310 let Some(dragbar_size) = self.dragbar.size else {
311 log::warn!("self.size() was None in `Hoverable::is_mouse_in`");
312 return false;
313 };
314 let Some(z_index) = self.dragbar.z_index else {
315 log::warn!("self.child_max_z_index was None in `Hoverable::is_mouse_in`");
316 return false;
317 };
318 
319 let is_hovering = ctx
320 .visible_rect(dragbar_origin, dragbar_size)
321 .is_some_and(|bound| bound.contains_point(position));
322 
323 let point = Point::from_vec2f(position, z_index);
324 let is_covered = ctx.is_covered(point);
325 
326 is_hovering && !is_covered
327 }
328}
329 
330impl Element for Resizable {
331 fn layout(
332 &mut self,
333 constraint: crate::SizeConstraint,
334 ctx: &mut crate::LayoutContext,
335 app: &AppContext,
336 ) -> Vector2F {
337 // Use the window size to set bounds on the width/height
338 if let Some(bounds_callback) = self.bounds_callback.as_mut() {
339 let mut new_bounds = bounds_callback(ctx.window_size);
340 if new_bounds.0 > new_bounds.1 {
341 log::error!("Resizable: min bound is greater than max bound");
342 new_bounds = (new_bounds.0, new_bounds.0);
343 }
344 self.state().bounds = Some(new_bounds);
345 
346 // With new bounds, we should also clamp the current width/height.
347 self.state().clamp_size();
348 }
349 
350 let size = self.state().size;
351 
352 // We set the child constraints to never be greater than the current width/height constraint.
353 let child_constraint = match self.direction {
354 ResizeDirection::Horizontal => SizeConstraint {
355 min: (constraint.min)
356 .max(Vector2F::zero())
357 .min(Vector2F::new(size, f32::MAX)),
358 max: (constraint.max)
359 .max(Vector2F::zero())
360 .min(Vector2F::new(size, f32::MAX)),
361 },
362 ResizeDirection::Vertical => SizeConstraint {
363 min: (constraint.min)
364 .max(Vector2F::zero())
365 .min(Vector2F::new(f32::MAX, size)),
366 max: (constraint.max)
367 .max(Vector2F::zero())
368 .min(Vector2F::new(f32::MAX, size)),
369 },
370 };
371 let child_size = self.child.layout(child_constraint, ctx, app);
372 
373 let size = child_size;
374 self.size = Some(size);
375 size
376 }
377 
378 fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
379 self.child.after_layout(ctx, app)
380 }
381 
382 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
383 self.child.paint(origin, ctx, app);
384 
385 // Draw the dragbar and record its size and position
386 let child_size = self.child.size().unwrap();
387 let (dragbar_origin, dragbar_size) = match self.dragbar.side {
388 DragBarSide::Left => (
389 origin - vec2f(self.dragbar_offset, 0.),
390 vec2f(DRAGBAR_WIDTH, child_size.y()),
391 ),
392 DragBarSide::Right => (
393 origin + vec2f(child_size.x() - DRAGBAR_WIDTH + self.dragbar_offset, 0.),
394 vec2f(DRAGBAR_WIDTH, child_size.y()),
395 ),
396 DragBarSide::Top => (
397 origin - vec2f(0., self.dragbar_offset),
398 vec2f(child_size.x(), DRAGBAR_WIDTH),
399 ),
400 DragBarSide::Bottom => (
401 origin + vec2f(0., child_size.y() - DRAGBAR_WIDTH + self.dragbar_offset),
402 vec2f(child_size.x(), DRAGBAR_WIDTH),
403 ),
404 };
405 
406 ctx.scene
407 .draw_rect_with_hit_recording(RectF::new(dragbar_origin, dragbar_size))
408 .with_background(self.dragbar.color);
409 
410 self.dragbar.bounds = Some(RectF::new(dragbar_origin, dragbar_size));
411 self.dragbar.origin = Some(Point::from_vec2f(dragbar_origin, ctx.scene.z_index()));
412 self.dragbar.size = Some(dragbar_size);
413 self.dragbar.z_index = Some(ctx.scene.max_active_z_index());
414 
415 self.origin = Some(origin);
416 self.origin_delta = Vector2F::zero();
417 }
418 
419 fn dispatch_event(
420 &mut self,
421 event: &DispatchedEvent,
422 ctx: &mut EventContext,
423 app: &AppContext,
424 ) -> bool {
425 let child_handled = self.child.dispatch_event(event, ctx, app);
426 
427 match event.raw_event() {
428 crate::Event::LeftMouseDown { position, .. } => {
429 // If a mouse-down on the dragbar element occurred, put the view into resizing mode
430 if self
431 .dragbar
432 .bounds
433 .is_some_and(|bounds| bounds.contains_point(*position))
434 {
435 self.state().begin_resizing(*position);
436 dispatch_callback(self.resize_handler.as_mut(), ctx, app);
437 return true;
438 }
439 }
440 
441 crate::Event::LeftMouseUp { .. } => {
442 // If a mouse-up occurs, take the view out of resizing mode
443 if self.state().is_resizing() {
444 ctx.reset_cursor();
445 self.state().end_resizing();
446 dispatch_callback(self.end_resize_handler.as_mut(), ctx, app);
447 return true;
448 }
449 }
450 
451 crate::Event::LeftMouseDragged { position, .. } => {
452 if self.state().is_resizing() {
453 let dragbar_side = self.dragbar.side;
454 let origin = self.origin.map(|origin| origin + self.origin_delta);
455 let resized = self
456 .state()
457 .check_for_resize(*position, origin, dragbar_side);
458 self.origin_delta += resized.unwrap_or_default();
459 if resized.is_some() {
460 dispatch_callback(self.resize_handler.as_mut(), ctx, app)
461 }
462 return true;
463 }
464 }
465 crate::Event::MouseMoved { position, .. } => {
466 // A mouse event over the dragbar should set the cursor
467 let Some(z_index) = self.z_index() else {
468 log::warn!("self.z_index() was None in `Resizable`");
469 return false;
470 };
471 let hovering_dragbar = self.is_mouse_hovering_dragbar(ctx, *position);
472 let was_already_hovering =
473 mem::replace(&mut self.hovering_dragbar, hovering_dragbar);
474 
475 if hovering_dragbar && !was_already_hovering {
476 let cursor = match self.direction {
477 ResizeDirection::Horizontal => Cursor::ResizeLeftRight,
478 ResizeDirection::Vertical => Cursor::ResizeUpDown,
479 };
480 ctx.set_cursor(cursor, z_index);
481 } else if !hovering_dragbar && was_already_hovering {
482 ctx.reset_cursor();
483 }
484 
485 return true;
486 }
487 _ => {}
488 }
489 child_handled
490 }
491 
492 fn size(&self) -> Option<Vector2F> {
493 self.child.size()
494 }
495 
496 fn origin(&self) -> Option<Point> {
497 self.child.origin()
498 }
499}
500 
501fn dispatch_callback(callback: Option<&mut Handler>, ctx: &mut EventContext, app: &AppContext) {
502 if let Some(callback) = callback {
503 callback(ctx, app);
504 }
505}
506