resizable.rs

  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, LayoutContext, MouseRegion, SceneBuilder, View,
 11    ViewContext,
 12};
 13
 14use super::{ConstrainedBox, Hook};
 15
 16#[derive(Copy, Clone, Debug)]
 17pub enum Side {
 18    Top,
 19    Bottom,
 20    Left,
 21    Right,
 22}
 23
 24impl Side {
 25    fn axis(&self) -> Axis {
 26        match self {
 27            Side::Left | Side::Right => Axis::Horizontal,
 28            Side::Top | Side::Bottom => Axis::Vertical,
 29        }
 30    }
 31
 32    /// 'before' is in reference to the standard english document ordering of left-to-right
 33    /// then top-to-bottom
 34    fn before_content(self) -> bool {
 35        match self {
 36            Side::Left | Side::Top => true,
 37            Side::Right | Side::Bottom => false,
 38        }
 39    }
 40
 41    fn relevant_component(&self, vector: Vector2F) -> f32 {
 42        match self.axis() {
 43            Axis::Horizontal => vector.x(),
 44            Axis::Vertical => vector.y(),
 45        }
 46    }
 47
 48    fn compute_delta(&self, e: MouseDrag) -> f32 {
 49        if self.before_content() {
 50            self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
 51        } else {
 52            self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
 53        }
 54    }
 55
 56    fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
 57        match self {
 58            Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
 59            Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
 60            Side::Bottom => {
 61                let mut origin = bounds.lower_left();
 62                origin.set_y(origin.y() - handle_size);
 63                RectF::new(origin, vec2f(bounds.width(), handle_size))
 64            }
 65            Side::Right => {
 66                let mut origin = bounds.upper_right();
 67                origin.set_x(origin.x() - handle_size);
 68                RectF::new(origin, vec2f(handle_size, bounds.height()))
 69            }
 70        }
 71    }
 72}
 73
 74struct ResizeHandleState {
 75    actual_dimension: Cell<f32>,
 76    custom_dimension: Cell<f32>,
 77}
 78
 79pub struct Resizable<V: View> {
 80    side: Side,
 81    handle_size: f32,
 82    child: AnyElement<V>,
 83    state: Rc<ResizeHandleState>,
 84    _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
 85}
 86
 87impl<V: View> Resizable<V> {
 88    pub fn new<Tag: 'static, T: View>(
 89        child: AnyElement<V>,
 90        element_id: usize,
 91        side: Side,
 92        handle_size: f32,
 93        initial_size: f32,
 94        cx: &mut ViewContext<V>,
 95    ) -> Self {
 96        let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
 97            element_id,
 98            Rc::new(ResizeHandleState {
 99                actual_dimension: Cell::new(initial_size),
100                custom_dimension: Cell::new(initial_size),
101            }),
102        );
103
104        let state = state_handle.read(cx).clone();
105
106        let child = Hook::new({
107            let constrained = ConstrainedBox::new(child);
108            match side.axis() {
109                Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
110                Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
111            }
112        })
113        .on_after_layout({
114            let state = state.clone();
115            move |size, _| {
116                state.actual_dimension.set(side.relevant_component(size));
117            }
118        })
119        .into_any();
120
121        Self {
122            side,
123            child,
124            handle_size,
125            state,
126            _state_handle: state_handle,
127        }
128    }
129
130    pub fn current_size(&self) -> f32 {
131        self.state.actual_dimension.get()
132    }
133}
134
135impl<V: View> Element<V> for Resizable<V> {
136    type LayoutState = ();
137    type PaintState = ();
138
139    fn layout(
140        &mut self,
141        constraint: crate::SizeConstraint,
142        view: &mut V,
143        cx: &mut LayoutContext<V>,
144    ) -> (Vector2F, Self::LayoutState) {
145        (self.child.layout(constraint, view, cx), ())
146    }
147
148    fn paint(
149        &mut self,
150        scene: &mut SceneBuilder,
151        bounds: pathfinder_geometry::rect::RectF,
152        visible_bounds: pathfinder_geometry::rect::RectF,
153        _child_size: &mut Self::LayoutState,
154        view: &mut V,
155        cx: &mut ViewContext<V>,
156    ) -> Self::PaintState {
157        scene.push_stacking_context(None, None);
158
159        let handle_region = self.side.of_rect(bounds, self.handle_size);
160
161        enum ResizeHandle {}
162        scene.push_mouse_region(
163            MouseRegion::new::<ResizeHandle>(cx.view_id(), self.side as usize, handle_region)
164                .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
165                .on_drag(MouseButton::Left, {
166                    let state = self.state.clone();
167                    let side = self.side;
168                    move |e, _: &mut V, cx| {
169                        let prev_width = state.actual_dimension.get();
170                        state
171                            .custom_dimension
172                            .set(0f32.max(prev_width + side.compute_delta(e)).round());
173                        cx.notify();
174                    }
175                }),
176        );
177
178        scene.push_cursor_region(crate::CursorRegion {
179            bounds: handle_region,
180            style: match self.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}