1use std::{cell::Cell, rc::Rc};
2
3use pathfinder_geometry::vector::Vector2F;
4use serde_json::json;
5
6use crate::{
7 color::Color, scene::DragRegionEvent, Axis, Border, CursorStyle, Element, ElementBox,
8 ElementStateHandle, MouseButton, RenderContext, View, MouseRegion,
9};
10
11use super::{ConstrainedBox, Empty, Flex, Hook, MouseEventHandler, Padding, 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 resize_padding(&self, padding_size: f32) -> Padding {
39 match self.axis() {
40 Axis::Horizontal => Padding::horizontal(padding_size),
41 Axis::Vertical => Padding::vertical(padding_size),
42 }
43 }
44
45 fn relevant_component(&self, vector: Vector2F) -> f32 {
46 match self.axis() {
47 Axis::Horizontal => vector.x(),
48 Axis::Vertical => vector.y(),
49 }
50 }
51
52 fn compute_delta(&self, e: DragRegionEvent) -> f32 {
53 if self.before_content() {
54 self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
55 } else {
56 self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
57 }
58 }
59}
60
61struct ResizeHandleState {
62 actual_dimension: Cell<f32>,
63 custom_dimension: Cell<f32>,
64}
65
66pub struct Resizable {
67 side: Side,
68 child: ElementBox,
69 state: Rc<ResizeHandleState>,
70 _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
71}
72
73impl Resizable {
74 pub fn new<Tag: 'static, T: View>(
75 child: ElementBox,
76 element_id: usize,
77 side: Side,
78 handle_size: f32,
79 initial_size: f32,
80 cx: &mut RenderContext<T>,
81 ) -> Self {
82 let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
83 element_id,
84 Rc::new(ResizeHandleState {
85 actual_dimension: Cell::new(initial_size),
86 custom_dimension: Cell::new(initial_size),
87 }),
88 );
89
90 let state = state_handle.read(cx).clone();
91
92 let mut flex = Flex::new(side.axis());
93
94 if side.before_content() {
95 dbg!("HANDLE BEING RENDERED BEFORE");
96 flex.add_child(render_resize_handle(state.clone(), side, handle_size, cx))
97 }
98
99 flex.add_child(
100 Hook::new({
101 let constrained = ConstrainedBox::new(child);
102 match side.axis() {
103 Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
104 Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
105 }
106 .boxed()
107 })
108 .on_after_layout({
109 let state = state.clone();
110 move |size, _| {
111 state.actual_dimension.set(side.relevant_component(size));
112 }
113 })
114 .boxed(),
115 );
116
117 if !side.before_content() {
118 dbg!("HANDLE BEING RENDERED AFTER");
119 flex.add_child(render_resize_handle(state.clone(), side, handle_size, cx))
120 }
121
122 let child = flex.boxed();
123
124 Self {
125 side,
126 child,
127 state,
128 _state_handle: state_handle,
129 }
130 }
131}
132
133fn render_resize_handle<T: View>(
134 state: Rc<ResizeHandleState>,
135 side: Side,
136 padding_size: f32,
137 cx: &mut RenderContext<T>,
138) -> ElementBox {
139 enum ResizeHandle {}
140 MouseEventHandler::<ResizeHandle>::new(side as usize, cx, |_, _| {
141 Empty::new()
142 // Border necessary to properly add a MouseRegion
143 .contained()
144 .with_border(Border {
145 width: 4.,
146 left: true,
147 color: Color::red(),
148 ..Default::default()
149 })
150 .boxed()
151 })
152 .with_padding(side.resize_padding(padding_size))
153 .with_cursor_style(match side.axis() {
154 Axis::Horizontal => CursorStyle::ResizeLeftRight,
155 Axis::Vertical => CursorStyle::ResizeUpDown,
156 })
157 .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
158 .on_drag(MouseButton::Left, move |e, cx| {
159 let prev_width = state.actual_dimension.get();
160 state
161 .custom_dimension
162 .set(0f32.max(prev_width + side.compute_delta(e)).round());
163 cx.notify();
164 })
165 .boxed()
166}
167
168impl Element for Resizable {
169 type LayoutState = Vector2F;
170 type PaintState = ();
171
172 fn layout(
173 &mut self,
174 constraint: crate::SizeConstraint,
175 cx: &mut crate::LayoutContext,
176 ) -> (Vector2F, Self::LayoutState) {
177 let child_size = self.child.layout(constraint, cx);
178 (child_size, child_size)
179 }
180
181 fn paint(
182 &mut self,
183 bounds: pathfinder_geometry::rect::RectF,
184 visible_bounds: pathfinder_geometry::rect::RectF,
185 child_size: &mut Self::LayoutState,
186 cx: &mut crate::PaintContext,
187 ) -> Self::PaintState {
188 cx.scene.push_stacking_context(None);
189
190 // Render a mouse region on the appropriate border (likely just bounds)
191 // Use the padding in the above code to decide the size of the rect to pass to the mouse region
192 // Add handlers for Down and Drag like above
193
194 // Maybe try pushing a quad to visually inspect where the region gets placed
195 // Push a cursor region
196 cx.scene.push_mouse_region(MouseRegion::)
197
198 cx.scene.pop_stacking_context();
199
200 self.child.paint(bounds.origin(), visible_bounds, cx);
201 }
202
203 fn dispatch_event(
204 &mut self,
205 event: &crate::Event,
206 _bounds: pathfinder_geometry::rect::RectF,
207 _visible_bounds: pathfinder_geometry::rect::RectF,
208 _layout: &mut Self::LayoutState,
209 _paint: &mut Self::PaintState,
210 cx: &mut crate::EventContext,
211 ) -> bool {
212 self.child.dispatch_event(event, cx)
213 }
214
215 fn rect_for_text_range(
216 &self,
217 range_utf16: std::ops::Range<usize>,
218 _bounds: pathfinder_geometry::rect::RectF,
219 _visible_bounds: pathfinder_geometry::rect::RectF,
220 _layout: &Self::LayoutState,
221 _paint: &Self::PaintState,
222 cx: &crate::MeasurementContext,
223 ) -> Option<pathfinder_geometry::rect::RectF> {
224 self.child.rect_for_text_range(range_utf16, cx)
225 }
226
227 fn debug(
228 &self,
229 _bounds: pathfinder_geometry::rect::RectF,
230 _layout: &Self::LayoutState,
231 _paint: &Self::PaintState,
232 cx: &crate::DebugContext,
233 ) -> serde_json::Value {
234 json!({
235 "child": self.child.debug(cx),
236 })
237 }
238}