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