resizable.rs

  1use std::{cell::RefCell, rc::Rc};
  2
  3use collections::HashMap;
  4use pathfinder_geometry::vector::{vec2f, Vector2F};
  5use serde_json::json;
  6
  7use crate::{
  8    geometry::rect::RectF,
  9    platform::{CursorStyle, MouseButton},
 10    AnyElement, AppContext, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
 11    SizeConstraint, TypeTag, View, ViewContext,
 12};
 13
 14#[derive(Copy, Clone, Debug)]
 15pub enum HandleSide {
 16    Top,
 17    Bottom,
 18    Left,
 19    Right,
 20}
 21
 22impl HandleSide {
 23    fn axis(&self) -> Axis {
 24        match self {
 25            HandleSide::Left | HandleSide::Right => Axis::Horizontal,
 26            HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
 27        }
 28    }
 29
 30    fn relevant_component(&self, vector: Vector2F) -> f32 {
 31        match self.axis() {
 32            Axis::Horizontal => vector.x(),
 33            Axis::Vertical => vector.y(),
 34        }
 35    }
 36
 37    fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
 38        match self {
 39            HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
 40            HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
 41            HandleSide::Bottom => {
 42                let mut origin = bounds.lower_left();
 43                origin.set_y(origin.y() - handle_size);
 44                RectF::new(origin, vec2f(bounds.width(), handle_size))
 45            }
 46            HandleSide::Right => {
 47                let mut origin = bounds.upper_right();
 48                origin.set_x(origin.x() - handle_size);
 49                RectF::new(origin, vec2f(handle_size, bounds.height()))
 50            }
 51        }
 52    }
 53}
 54
 55fn get_bounds(tag: TypeTag, cx: &AppContext) -> Option<&(RectF, RectF)>
 56where
 57{
 58    cx.optional_global::<ProviderMap>()
 59        .and_then(|map| map.0.get(&tag))
 60}
 61
 62pub struct Resizable<V: 'static> {
 63    child: AnyElement<V>,
 64    tag: TypeTag,
 65    handle_side: HandleSide,
 66    handle_size: f32,
 67    on_resize: Rc<RefCell<dyn FnMut(&mut V, Option<f32>, &mut ViewContext<V>)>>,
 68}
 69
 70const DEFAULT_HANDLE_SIZE: f32 = 4.0;
 71
 72impl<V: 'static> Resizable<V> {
 73    pub fn new<Tag: 'static>(
 74        child: AnyElement<V>,
 75        handle_side: HandleSide,
 76        size: f32,
 77        on_resize: impl 'static + FnMut(&mut V, Option<f32>, &mut ViewContext<V>),
 78    ) -> Self {
 79        let child = match handle_side.axis() {
 80            Axis::Horizontal => child.constrained().with_max_width(size),
 81            Axis::Vertical => child.constrained().with_max_height(size),
 82        }
 83        .into_any();
 84
 85        Self {
 86            child,
 87            handle_side,
 88            tag: TypeTag::new::<Tag>(),
 89            handle_size: DEFAULT_HANDLE_SIZE,
 90            on_resize: Rc::new(RefCell::new(on_resize)),
 91        }
 92    }
 93
 94    pub fn with_handle_size(mut self, handle_size: f32) -> Self {
 95        self.handle_size = handle_size;
 96        self
 97    }
 98}
 99
100impl<V: 'static> Element<V> for Resizable<V> {
101    type LayoutState = SizeConstraint;
102    type PaintState = ();
103
104    fn layout(
105        &mut self,
106        constraint: crate::SizeConstraint,
107        view: &mut V,
108        cx: &mut LayoutContext<V>,
109    ) -> (Vector2F, Self::LayoutState) {
110        (self.child.layout(constraint, view, cx), constraint)
111    }
112
113    fn paint(
114        &mut self,
115        scene: &mut SceneBuilder,
116        bounds: pathfinder_geometry::rect::RectF,
117        visible_bounds: pathfinder_geometry::rect::RectF,
118        constraint: &mut SizeConstraint,
119        view: &mut V,
120        cx: &mut PaintContext<V>,
121    ) -> Self::PaintState {
122        scene.push_stacking_context(None, None);
123
124        let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
125
126        enum ResizeHandle {}
127        scene.push_mouse_region(
128            MouseRegion::new::<ResizeHandle>(
129                cx.view_id(),
130                self.handle_side as usize,
131                handle_region,
132            )
133            .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
134            .on_click(MouseButton::Left, {
135                let on_resize = self.on_resize.clone();
136                move |click, v, cx| {
137                    if click.click_count == 2 {
138                        on_resize.borrow_mut()(v, None, cx);
139                    }
140                }
141            })
142            .on_drag(MouseButton::Left, {
143                let bounds = bounds.clone();
144                let side = self.handle_side;
145                let prev_size = side.relevant_component(bounds.size());
146                let min_size = side.relevant_component(constraint.min);
147                let max_size = side.relevant_component(constraint.max);
148                let on_resize = self.on_resize.clone();
149                let tag = self.tag;
150                move |event, view: &mut V, cx| {
151                    if event.end {
152                        return;
153                    }
154
155                    let Some((bounds, _)) = get_bounds(tag, cx) else {
156                        return;
157                    };
158
159                    let new_size_raw = match side {
160                        // Handle on top side of element => Element is on bottom
161                        HandleSide::Top => bounds.height() + bounds.origin_y() - event.position.y(),
162                        // Handle on right side of element => Element is on left
163                        HandleSide::Right => event.position.x() - bounds.lower_left().x(),
164                        // Handle on left side of element => Element is on the right
165                        HandleSide::Left => bounds.width() + bounds.origin_x() - event.position.x(),
166                        // Handle on bottom side of element => Element is on the top
167                        HandleSide::Bottom => event.position.y() - bounds.lower_left().y(),
168                    };
169
170                    let new_size = min_size.max(new_size_raw).min(max_size).round();
171                    if new_size != prev_size {
172                        on_resize.borrow_mut()(view, Some(new_size), cx);
173                    }
174                }
175            }),
176        );
177
178        scene.push_cursor_region(crate::CursorRegion {
179            bounds: handle_region,
180            style: match self.handle_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}
218
219#[derive(Debug, Default)]
220struct ProviderMap(HashMap<TypeTag, (RectF, RectF)>);
221
222pub struct BoundsProvider<V: 'static, P> {
223    child: AnyElement<V>,
224    phantom: std::marker::PhantomData<P>,
225}
226
227impl<V: 'static, P: 'static> BoundsProvider<V, P> {
228    pub fn new(child: AnyElement<V>) -> Self {
229        Self {
230            child,
231            phantom: std::marker::PhantomData,
232        }
233    }
234}
235
236impl<V: View, P: 'static> Element<V> for BoundsProvider<V, P> {
237    type LayoutState = ();
238
239    type PaintState = ();
240
241    fn layout(
242        &mut self,
243        constraint: crate::SizeConstraint,
244        view: &mut V,
245        cx: &mut crate::LayoutContext<V>,
246    ) -> (pathfinder_geometry::vector::Vector2F, Self::LayoutState) {
247        (self.child.layout(constraint, view, cx), ())
248    }
249
250    fn paint(
251        &mut self,
252        scene: &mut crate::SceneBuilder,
253        bounds: pathfinder_geometry::rect::RectF,
254        visible_bounds: pathfinder_geometry::rect::RectF,
255        _: &mut Self::LayoutState,
256        view: &mut V,
257        cx: &mut crate::PaintContext<V>,
258    ) -> Self::PaintState {
259        cx.update_default_global::<ProviderMap, _, _>(|map, _| {
260            map.0.insert(TypeTag::new::<P>(), (bounds, visible_bounds));
261        });
262
263        self.child
264            .paint(scene, bounds.origin(), visible_bounds, view, cx)
265    }
266
267    fn rect_for_text_range(
268        &self,
269        range_utf16: std::ops::Range<usize>,
270        _: pathfinder_geometry::rect::RectF,
271        _: pathfinder_geometry::rect::RectF,
272        _: &Self::LayoutState,
273        _: &Self::PaintState,
274        view: &V,
275        cx: &crate::ViewContext<V>,
276    ) -> Option<pathfinder_geometry::rect::RectF> {
277        self.child.rect_for_text_range(range_utf16, view, cx)
278    }
279
280    fn debug(
281        &self,
282        _: pathfinder_geometry::rect::RectF,
283        _: &Self::LayoutState,
284        _: &Self::PaintState,
285        view: &V,
286        cx: &crate::ViewContext<V>,
287    ) -> serde_json::Value {
288        serde_json::json!({
289            "type": "Provider",
290            "providing": format!("{:?}", TypeTag::new::<P>()),
291            "child": self.child.debug(view, cx),
292        })
293    }
294}