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, SceneBuilder, SizeConstraint, View,
11 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 ViewContext<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 let new_size = min_size
151 .max(prev_size + side.compute_delta(event))
152 .min(max_size)
153 .round();
154 if new_size != prev_size {
155 on_resize.borrow_mut()(view, new_size, cx);
156 }
157 }
158 }),
159 );
160
161 scene.push_cursor_region(crate::CursorRegion {
162 bounds: handle_region,
163 style: match self.handle_side.axis() {
164 Axis::Horizontal => CursorStyle::ResizeLeftRight,
165 Axis::Vertical => CursorStyle::ResizeUpDown,
166 },
167 });
168
169 scene.pop_stacking_context();
170
171 self.child
172 .paint(scene, bounds.origin(), visible_bounds, view, cx);
173 }
174
175 fn rect_for_text_range(
176 &self,
177 range_utf16: std::ops::Range<usize>,
178 _bounds: pathfinder_geometry::rect::RectF,
179 _visible_bounds: pathfinder_geometry::rect::RectF,
180 _layout: &Self::LayoutState,
181 _paint: &Self::PaintState,
182 view: &V,
183 cx: &ViewContext<V>,
184 ) -> Option<pathfinder_geometry::rect::RectF> {
185 self.child.rect_for_text_range(range_utf16, view, cx)
186 }
187
188 fn debug(
189 &self,
190 _bounds: pathfinder_geometry::rect::RectF,
191 _layout: &Self::LayoutState,
192 _paint: &Self::PaintState,
193 view: &V,
194 cx: &ViewContext<V>,
195 ) -> serde_json::Value {
196 json!({
197 "child": self.child.debug(view, cx),
198 })
199 }
200}