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