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, 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}