1use std::{cell::RefCell, rc::Rc};
2
3use pathfinder_geometry::vector::{vec2f, Vector2F};
4use serde_json::json;
5
6use crate::{
7 geometry::rect::RectF,
8 platform::{CursorStyle, MouseButton},
9 scene::MouseDrag,
10 AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
11 SizeConstraint, View, ViewContext,
12};
13
14#[derive(Copy, Clone, Debug)]
15pub enum HandleSide {
16 Top,
17 Bottom,
18 Left,
19 Right,
20}
21
22impl HandleSide {
23 fn axis(&self) -> Axis {
24 match self {
25 HandleSide::Left | HandleSide::Right => Axis::Horizontal,
26 HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
27 }
28 }
29
30 /// 'before' is in reference to the standard english document ordering of left-to-right
31 /// then top-to-bottom
32 fn before_content(self) -> bool {
33 match self {
34 HandleSide::Left | HandleSide::Top => true,
35 HandleSide::Right | HandleSide::Bottom => false,
36 }
37 }
38
39 fn relevant_component(&self, vector: Vector2F) -> f32 {
40 match self.axis() {
41 Axis::Horizontal => vector.x(),
42 Axis::Vertical => vector.y(),
43 }
44 }
45
46 fn compute_delta(&self, e: MouseDrag) -> f32 {
47 if self.before_content() {
48 self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
49 } else {
50 self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
51 }
52 }
53
54 fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
55 match self {
56 HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
57 HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
58 HandleSide::Bottom => {
59 let mut origin = bounds.lower_left();
60 origin.set_y(origin.y() - handle_size);
61 RectF::new(origin, vec2f(bounds.width(), handle_size))
62 }
63 HandleSide::Right => {
64 let mut origin = bounds.upper_right();
65 origin.set_x(origin.x() - handle_size);
66 RectF::new(origin, vec2f(handle_size, bounds.height()))
67 }
68 }
69 }
70}
71
72pub struct Resizable<V: View> {
73 child: AnyElement<V>,
74 handle_side: HandleSide,
75 handle_size: f32,
76 on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
77}
78
79const DEFAULT_HANDLE_SIZE: f32 = 4.0;
80
81impl<V: View> Resizable<V> {
82 pub fn new(
83 child: AnyElement<V>,
84 handle_side: HandleSide,
85 size: f32,
86 on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
87 ) -> Self {
88 let child = match handle_side.axis() {
89 Axis::Horizontal => child.constrained().with_max_width(size),
90 Axis::Vertical => child.constrained().with_max_height(size),
91 }
92 .into_any();
93
94 Self {
95 child,
96 handle_side,
97 handle_size: DEFAULT_HANDLE_SIZE,
98 on_resize: Rc::new(RefCell::new(on_resize)),
99 }
100 }
101
102 pub fn with_handle_size(mut self, handle_size: f32) -> Self {
103 self.handle_size = handle_size;
104 self
105 }
106}
107
108impl<V: View> Element<V> for Resizable<V> {
109 type LayoutState = SizeConstraint;
110 type PaintState = ();
111
112 fn layout(
113 &mut self,
114 constraint: crate::SizeConstraint,
115 view: &mut V,
116 cx: &mut LayoutContext<V>,
117 ) -> (Vector2F, Self::LayoutState) {
118 (self.child.layout(constraint, view, cx), constraint)
119 }
120
121 fn paint(
122 &mut self,
123 scene: &mut SceneBuilder,
124 bounds: pathfinder_geometry::rect::RectF,
125 visible_bounds: pathfinder_geometry::rect::RectF,
126 constraint: &mut SizeConstraint,
127 view: &mut V,
128 cx: &mut PaintContext<V>,
129 ) -> Self::PaintState {
130 scene.push_stacking_context(None, None);
131
132 let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
133
134 enum ResizeHandle {}
135 scene.push_mouse_region(
136 MouseRegion::new::<ResizeHandle>(
137 cx.view_id(),
138 self.handle_side as usize,
139 handle_region,
140 )
141 .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
142 .on_drag(MouseButton::Left, {
143 let bounds = bounds.clone();
144 let side = self.handle_side;
145 let prev_size = side.relevant_component(bounds.size());
146 let min_size = side.relevant_component(constraint.min);
147 let max_size = side.relevant_component(constraint.max);
148 let on_resize = self.on_resize.clone();
149 move |event, view: &mut V, cx| {
150 if event.end {
151 return;
152 }
153 let new_size = min_size
154 .max(prev_size + side.compute_delta(event))
155 .min(max_size)
156 .round();
157 if new_size != prev_size {
158 on_resize.borrow_mut()(view, new_size, cx);
159 }
160 }
161 }),
162 );
163
164 scene.push_cursor_region(crate::CursorRegion {
165 bounds: handle_region,
166 style: match self.handle_side.axis() {
167 Axis::Horizontal => CursorStyle::ResizeLeftRight,
168 Axis::Vertical => CursorStyle::ResizeUpDown,
169 },
170 });
171
172 scene.pop_stacking_context();
173
174 self.child
175 .paint(scene, bounds.origin(), visible_bounds, view, cx);
176 }
177
178 fn rect_for_text_range(
179 &self,
180 range_utf16: std::ops::Range<usize>,
181 _bounds: pathfinder_geometry::rect::RectF,
182 _visible_bounds: pathfinder_geometry::rect::RectF,
183 _layout: &Self::LayoutState,
184 _paint: &Self::PaintState,
185 view: &V,
186 cx: &ViewContext<V>,
187 ) -> Option<pathfinder_geometry::rect::RectF> {
188 self.child.rect_for_text_range(range_utf16, view, cx)
189 }
190
191 fn debug(
192 &self,
193 _bounds: pathfinder_geometry::rect::RectF,
194 _layout: &Self::LayoutState,
195 _paint: &Self::PaintState,
196 view: &V,
197 cx: &ViewContext<V>,
198 ) -> serde_json::Value {
199 json!({
200 "child": self.child.debug(view, cx),
201 })
202 }
203}