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::MouseDrag, Axis, CursorStyle, Element, ElementBox,
8 ElementStateHandle, MouseButton, MouseRegion, RenderContext, View,
9};
10
11use super::{ConstrainedBox, Hook};
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: MouseDrag) -> 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::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
57 Side::Bottom => {
58 let mut origin = bounds.lower_left();
59 origin.set_y(origin.y() - handle_size);
60 RectF::new(origin, vec2f(bounds.width(), handle_size))
61 }
62 Side::Right => {
63 let mut origin = bounds.upper_right();
64 origin.set_x(origin.x() - handle_size);
65 RectF::new(origin, vec2f(handle_size, bounds.height()))
66 }
67 }
68 }
69}
70
71struct ResizeHandleState {
72 actual_dimension: Cell<f32>,
73 custom_dimension: Cell<f32>,
74}
75
76pub struct Resizable {
77 side: Side,
78 handle_size: f32,
79 child: ElementBox,
80 state: Rc<ResizeHandleState>,
81 _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
82}
83
84impl Resizable {
85 pub fn new<Tag: 'static, T: View>(
86 child: ElementBox,
87 element_id: usize,
88 side: Side,
89 handle_size: f32,
90 initial_size: f32,
91 cx: &mut RenderContext<T>,
92 ) -> Self {
93 let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
94 element_id,
95 Rc::new(ResizeHandleState {
96 actual_dimension: Cell::new(initial_size),
97 custom_dimension: Cell::new(initial_size),
98 }),
99 );
100
101 let state = state_handle.read(cx).clone();
102
103 let child = Hook::new({
104 let constrained = ConstrainedBox::new(child);
105 match side.axis() {
106 Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
107 Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
108 }
109 .boxed()
110 })
111 .on_after_layout({
112 let state = state.clone();
113 move |size, _| {
114 state.actual_dimension.set(side.relevant_component(size));
115 }
116 })
117 .boxed();
118
119 Self {
120 side,
121 child,
122 handle_size,
123 state,
124 _state_handle: state_handle,
125 }
126 }
127
128 pub fn current_size(&self) -> f32 {
129 self.state.actual_dimension.get()
130 }
131}
132
133impl Element for Resizable {
134 type LayoutState = ();
135 type PaintState = ();
136
137 fn layout(
138 &mut self,
139 constraint: crate::SizeConstraint,
140 cx: &mut crate::LayoutContext,
141 ) -> (Vector2F, Self::LayoutState) {
142 (self.child.layout(constraint, cx), ())
143 }
144
145 fn paint(
146 &mut self,
147 bounds: pathfinder_geometry::rect::RectF,
148 visible_bounds: pathfinder_geometry::rect::RectF,
149 _child_size: &mut Self::LayoutState,
150 cx: &mut crate::PaintContext,
151 ) -> Self::PaintState {
152 cx.scene.push_stacking_context(None, None);
153
154 let handle_region = self.side.of_rect(bounds, self.handle_size);
155
156 enum ResizeHandle {}
157 cx.scene.push_mouse_region(
158 MouseRegion::new::<ResizeHandle>(
159 cx.current_view_id(),
160 self.side as usize,
161 handle_region,
162 )
163 .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
164 .on_drag(MouseButton::Left, {
165 let state = self.state.clone();
166 let side = self.side;
167 move |e, cx| {
168 let prev_width = state.actual_dimension.get();
169 state
170 .custom_dimension
171 .set(0f32.max(prev_width + side.compute_delta(e)).round());
172 cx.notify();
173 }
174 }),
175 );
176
177 cx.scene.push_cursor_region(crate::CursorRegion {
178 bounds: handle_region,
179 style: match self.side.axis() {
180 Axis::Horizontal => CursorStyle::ResizeLeftRight,
181 Axis::Vertical => CursorStyle::ResizeUpDown,
182 },
183 });
184
185 cx.scene.pop_stacking_context();
186
187 self.child.paint(bounds.origin(), visible_bounds, cx);
188 }
189
190 fn rect_for_text_range(
191 &self,
192 range_utf16: std::ops::Range<usize>,
193 _bounds: pathfinder_geometry::rect::RectF,
194 _visible_bounds: pathfinder_geometry::rect::RectF,
195 _layout: &Self::LayoutState,
196 _paint: &Self::PaintState,
197 cx: &crate::MeasurementContext,
198 ) -> Option<pathfinder_geometry::rect::RectF> {
199 self.child.rect_for_text_range(range_utf16, cx)
200 }
201
202 fn debug(
203 &self,
204 _bounds: pathfinder_geometry::rect::RectF,
205 _layout: &Self::LayoutState,
206 _paint: &Self::PaintState,
207 cx: &crate::DebugContext,
208 ) -> serde_json::Value {
209 json!({
210 "child": self.child.debug(cx),
211 })
212 }
213}