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