resizable.rs

  1use std::{cell::RefCell, 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, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
 11    SizeConstraint, 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    /// 'before' is in reference to the standard english document ordering of left-to-right
 31    /// then top-to-bottom
 32    fn before_content(self) -> bool {
 33        match self {
 34            HandleSide::Left | HandleSide::Top => true,
 35            HandleSide::Right | HandleSide::Bottom => false,
 36        }
 37    }
 38
 39    fn relevant_component(&self, vector: Vector2F) -> f32 {
 40        match self.axis() {
 41            Axis::Horizontal => vector.x(),
 42            Axis::Vertical => vector.y(),
 43        }
 44    }
 45
 46    fn compute_delta(&self, e: MouseDrag) -> f32 {
 47        if self.before_content() {
 48            self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
 49        } else {
 50            self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
 51        }
 52    }
 53
 54    fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
 55        match self {
 56            HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
 57            HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
 58            HandleSide::Bottom => {
 59                let mut origin = bounds.lower_left();
 60                origin.set_y(origin.y() - handle_size);
 61                RectF::new(origin, vec2f(bounds.width(), handle_size))
 62            }
 63            HandleSide::Right => {
 64                let mut origin = bounds.upper_right();
 65                origin.set_x(origin.x() - handle_size);
 66                RectF::new(origin, vec2f(handle_size, bounds.height()))
 67            }
 68        }
 69    }
 70}
 71
 72pub struct Resizable<V: View> {
 73    child: AnyElement<V>,
 74    handle_side: HandleSide,
 75    handle_size: f32,
 76    on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
 77}
 78
 79const DEFAULT_HANDLE_SIZE: f32 = 4.0;
 80
 81impl<V: View> Resizable<V> {
 82    pub fn new(
 83        child: AnyElement<V>,
 84        handle_side: HandleSide,
 85        size: f32,
 86        on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
 87    ) -> Self {
 88        let child = match handle_side.axis() {
 89            Axis::Horizontal => child.constrained().with_max_width(size),
 90            Axis::Vertical => child.constrained().with_max_height(size),
 91        }
 92        .into_any();
 93
 94        Self {
 95            child,
 96            handle_side,
 97            handle_size: DEFAULT_HANDLE_SIZE,
 98            on_resize: Rc::new(RefCell::new(on_resize)),
 99        }
100    }
101
102    pub fn with_handle_size(mut self, handle_size: f32) -> Self {
103        self.handle_size = handle_size;
104        self
105    }
106}
107
108impl<V: View> Element<V> for Resizable<V> {
109    type LayoutState = SizeConstraint;
110    type PaintState = ();
111
112    fn layout(
113        &mut self,
114        constraint: crate::SizeConstraint,
115        view: &mut V,
116        cx: &mut LayoutContext<V>,
117    ) -> (Vector2F, Self::LayoutState) {
118        (self.child.layout(constraint, view, cx), constraint)
119    }
120
121    fn paint(
122        &mut self,
123        scene: &mut SceneBuilder,
124        bounds: pathfinder_geometry::rect::RectF,
125        visible_bounds: pathfinder_geometry::rect::RectF,
126        constraint: &mut SizeConstraint,
127        view: &mut V,
128        cx: &mut PaintContext<V>,
129    ) -> Self::PaintState {
130        scene.push_stacking_context(None, None);
131
132        let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
133
134        enum ResizeHandle {}
135        scene.push_mouse_region(
136            MouseRegion::new::<ResizeHandle>(
137                cx.view_id(),
138                self.handle_side as usize,
139                handle_region,
140            )
141            .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
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                move |event, view: &mut V, cx| {
150                    if event.end {
151                        return;
152                    }
153                    let new_size = min_size
154                        .max(prev_size + side.compute_delta(event))
155                        .min(max_size)
156                        .round();
157                    if new_size != prev_size {
158                        on_resize.borrow_mut()(view, new_size, cx);
159                    }
160                }
161            }),
162        );
163
164        scene.push_cursor_region(crate::CursorRegion {
165            bounds: handle_region,
166            style: match self.handle_side.axis() {
167                Axis::Horizontal => CursorStyle::ResizeLeftRight,
168                Axis::Vertical => CursorStyle::ResizeUpDown,
169            },
170        });
171
172        scene.pop_stacking_context();
173
174        self.child
175            .paint(scene, bounds.origin(), visible_bounds, view, cx);
176    }
177
178    fn rect_for_text_range(
179        &self,
180        range_utf16: std::ops::Range<usize>,
181        _bounds: pathfinder_geometry::rect::RectF,
182        _visible_bounds: pathfinder_geometry::rect::RectF,
183        _layout: &Self::LayoutState,
184        _paint: &Self::PaintState,
185        view: &V,
186        cx: &ViewContext<V>,
187    ) -> Option<pathfinder_geometry::rect::RectF> {
188        self.child.rect_for_text_range(range_utf16, view, cx)
189    }
190
191    fn debug(
192        &self,
193        _bounds: pathfinder_geometry::rect::RectF,
194        _layout: &Self::LayoutState,
195        _paint: &Self::PaintState,
196        view: &V,
197        cx: &ViewContext<V>,
198    ) -> serde_json::Value {
199        json!({
200            "child": self.child.debug(view, cx),
201        })
202    }
203}