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