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/drag_resize.rs
StratoSDK / crates / strato-ui-core / src / elements / drag_resize.rs
1use super::Point;
2use super::ZIndex;
3use pathfinder_geometry::vector::Vector2F;
4use std::sync::{Arc, Mutex};
5 
6use crate::{
7 event::DispatchedEvent, platform::Cursor, AfterLayoutContext, AppContext, Element, Event,
8 EventContext, LayoutContext, PaintContext, SizeConstraint,
9};
10 
11/// Shared handle for drag-to-resize state, following the same `Arc<Mutex<_>>`
12/// pattern as `ResizableStateHandle`. The view creates the handle once at
13/// construction and passes it into `DragResizeElement` each render.
14pub type DragResizeHandle = Arc<Mutex<DragResizeState>>;
15 
16pub fn drag_resize_handle() -> DragResizeHandle {
17 Arc::new(Mutex::new(DragResizeState::default()))
18}
19 
20/// Tracks whether a drag-to-resize operation is in progress.
21#[derive(Default)]
22pub struct DragResizeState {
23 is_dragging: bool,
24 last_y: f32,
25}
26 
27impl DragResizeState {
28 fn begin(&mut self, y: f32) {
29 self.is_dragging = true;
30 self.last_y = y;
31 }
32 
33 fn end(&mut self) {
34 self.is_dragging = false;
35 }
36 
37 fn is_dragging(&self) -> bool {
38 self.is_dragging
39 }
40 
41 /// Compute the vertical delta since the last event and update `last_y`.
42 fn consume_delta(&mut self, y: f32) -> f32 {
43 let delta = y - self.last_y;
44 self.last_y = y;
45 delta
46 }
47}
48 
49/// Callback invoked during a resize drag. Receives the vertical delta (pixels).
50type ResizeUpdateFn = Box<dyn Fn(f32, &mut EventContext, &AppContext)>;
51 
52/// Callback invoked when a resize drag finishes.
53pub type ResizeEndFn = Box<dyn Fn(&mut EventContext, &AppContext)>;
54 
55/// An element that enables drag-to-resize on its entire surface area.
56///
57/// The element dispatches events to its child first. If the child does not
58/// handle a `LeftMouseDown`, the element begins a resize operation. Subsequent
59/// `LeftMouseDragged` / `LeftMouseUp` events are captured via `raw_event()` so
60/// they work regardless of cursor position (same pattern used by `Resizable`).
61pub struct DragResizeElement {
62 child: Box<dyn Element>,
63 handle: DragResizeHandle,
64 on_resize_update: ResizeUpdateFn,
65 on_resize_end: Option<ResizeEndFn>,
66 origin: Option<Point>,
67 child_max_z_index: Option<ZIndex>,
68}
69 
70impl DragResizeElement {
71 pub fn new(
72 handle: DragResizeHandle,
73 child: Box<dyn Element>,
74 on_resize_update: impl Fn(f32, &mut EventContext, &AppContext) + 'static,
75 on_resize_end: Option<ResizeEndFn>,
76 ) -> Self {
77 Self {
78 child,
79 handle,
80 on_resize_update: Box::new(on_resize_update),
81 on_resize_end,
82 origin: None,
83 child_max_z_index: None,
84 }
85 }
86 
87 fn state(&self) -> std::sync::MutexGuard<'_, DragResizeState> {
88 // This is the same (slightly scary) pattern as `Resizable::state()`.
89 // Poisoning should only occur after a prior panic (already in a bad state).
90 self.handle.lock().expect("DragResizeState lock poisoned")
91 }
92}
93 
94impl Element for DragResizeElement {
95 fn layout(
96 &mut self,
97 constraint: SizeConstraint,
98 ctx: &mut LayoutContext,
99 app: &AppContext,
100 ) -> Vector2F {
101 self.child.layout(constraint, ctx, app)
102 }
103 
104 fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) {
105 self.child.after_layout(ctx, app);
106 }
107 
108 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
109 self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
110 self.child.paint(origin, ctx, app);
111 self.child_max_z_index = Some(ctx.scene.max_active_z_index());
112 }
113 
114 fn dispatch_event(
115 &mut self,
116 event: &DispatchedEvent,
117 ctx: &mut EventContext,
118 app: &AppContext,
119 ) -> bool {
120 // Always let the child see the event first.
121 let child_handled = self.child.dispatch_event(event, ctx, app);
122 
123 // Use raw_event() for drag/up so they are position-independent.
124 match event.raw_event() {
125 Event::LeftMouseDown { position, .. } => {
126 if child_handled {
127 return true;
128 }
129 // Check if the click is within our bounds.
130 if let (Some(origin), Some(size)) = (self.origin, self.size()) {
131 if let Some(rect) = ctx.visible_rect(origin, size) {
132 if rect.contains_point(*position) {
133 self.state().begin(position.y());
134 return true;
135 }
136 }
137 }
138 false
139 }
140 Event::LeftMouseDragged { position, .. } => {
141 if self.state().is_dragging() {
142 if let Some(z_index) = self.child_max_z_index {
143 ctx.set_cursor(Cursor::ResizeUpDown, z_index);
144 }
145 let delta = self.state().consume_delta(position.y());
146 (self.on_resize_update)(delta, ctx, app);
147 return true;
148 }
149 child_handled
150 }
151 Event::LeftMouseUp { .. } => {
152 if self.state().is_dragging() {
153 self.state().end();
154 if let Some(on_end) = &self.on_resize_end {
155 (on_end)(ctx, app);
156 }
157 ctx.reset_cursor();
158 return true;
159 }
160 child_handled
161 }
162 Event::MouseMoved { .. } => {
163 if self.state().is_dragging() {
164 if let Some(z_index) = self.child_max_z_index {
165 ctx.set_cursor(Cursor::ResizeUpDown, z_index);
166 }
167 return true;
168 }
169 child_handled
170 }
171 _ => child_handled,
172 }
173 }
174 
175 fn size(&self) -> Option<Vector2F> {
176 self.child.size()
177 }
178 
179 fn origin(&self) -> Option<Point> {
180 self.child.origin()
181 }
182}
183