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, scene::DragRegionEvent, Axis, CursorStyle, Element, ElementBox,
  8    ElementStateHandle, MouseButton, MouseRegion, RenderContext, View,
  9};
 10
 11use super::{ConstrainedBox, Flex, Hook, ParentElement};
 12
 13#[derive(Copy, Clone, Debug)]
 14pub enum Side {
 15    Top,
 16    Bottom,
 17    Left,
 18    Right,
 19}
 20
 21impl Side {
 22    fn axis(&self) -> Axis {
 23        match self {
 24            Side::Left | Side::Right => Axis::Horizontal,
 25            Side::Top | Side::Bottom => Axis::Vertical,
 26        }
 27    }
 28
 29    /// 'before' is in reference to the standard english document ordering of left-to-right
 30    /// then top-to-bottom
 31    fn before_content(self) -> bool {
 32        match self {
 33            Side::Left | Side::Top => true,
 34            Side::Right | Side::Bottom => false,
 35        }
 36    }
 37
 38    fn relevant_component(&self, vector: Vector2F) -> f32 {
 39        match self.axis() {
 40            Axis::Horizontal => vector.x(),
 41            Axis::Vertical => vector.y(),
 42        }
 43    }
 44
 45    fn compute_delta(&self, e: DragRegionEvent) -> f32 {
 46        if self.before_content() {
 47            self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
 48        } else {
 49            self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
 50        }
 51    }
 52
 53    fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
 54        match self {
 55            Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
 56            Side::Bottom => RectF::new(bounds.lower_left(), vec2f(bounds.width(), handle_size)),
 57            Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
 58            Side::Right => {
 59                let mut origin = bounds.upper_right();
 60                origin.set_x(origin.x() - handle_size);
 61                RectF::new(origin, vec2f(handle_size, bounds.height()))
 62            }
 63        }
 64    }
 65}
 66
 67struct ResizeHandleState {
 68    actual_dimension: Cell<f32>,
 69    custom_dimension: Cell<f32>,
 70}
 71
 72pub struct Resizable {
 73    side: Side,
 74    handle_size: f32,
 75    child: ElementBox,
 76    state: Rc<ResizeHandleState>,
 77    _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
 78}
 79
 80impl Resizable {
 81    pub fn new<Tag: 'static, T: View>(
 82        child: ElementBox,
 83        element_id: usize,
 84        side: Side,
 85        handle_size: f32,
 86        initial_size: f32,
 87        cx: &mut RenderContext<T>,
 88    ) -> Self {
 89        let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
 90            element_id,
 91            Rc::new(ResizeHandleState {
 92                actual_dimension: Cell::new(initial_size),
 93                custom_dimension: Cell::new(initial_size),
 94            }),
 95        );
 96
 97        let state = state_handle.read(cx).clone();
 98
 99        let mut flex = Flex::new(side.axis());
100
101        flex.add_child(
102            Hook::new({
103                let constrained = ConstrainedBox::new(child);
104                match side.axis() {
105                    Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
106                    Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
107                }
108                .boxed()
109            })
110            .on_after_layout({
111                let state = state.clone();
112                move |size, _| {
113                    state.actual_dimension.set(side.relevant_component(size));
114                }
115            })
116            .boxed(),
117        );
118
119        let child = flex.boxed();
120
121        Self {
122            side,
123            child,
124            handle_size,
125            state,
126            _state_handle: state_handle,
127        }
128    }
129}
130
131impl Element for Resizable {
132    type LayoutState = Vector2F;
133    type PaintState = ();
134
135    fn layout(
136        &mut self,
137        constraint: crate::SizeConstraint,
138        cx: &mut crate::LayoutContext,
139    ) -> (Vector2F, Self::LayoutState) {
140        let child_size = self.child.layout(constraint, cx);
141        (child_size, child_size)
142    }
143
144    fn paint(
145        &mut self,
146        bounds: pathfinder_geometry::rect::RectF,
147        visible_bounds: pathfinder_geometry::rect::RectF,
148        _child_size: &mut Self::LayoutState,
149        cx: &mut crate::PaintContext,
150    ) -> Self::PaintState {
151        cx.scene.push_stacking_context(None);
152
153        let handle_region = self.side.of_rect(bounds, self.handle_size);
154
155        enum ResizeHandle {}
156        cx.scene.push_mouse_region(
157            MouseRegion::new::<ResizeHandle>(
158                cx.current_view_id(),
159                self.side as usize,
160                handle_region,
161            )
162            .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
163            .on_drag(MouseButton::Left, {
164                let state = self.state.clone();
165                let side = self.side;
166                move |e, cx| {
167                    let prev_width = state.actual_dimension.get();
168                    state
169                        .custom_dimension
170                        .set(0f32.max(prev_width + side.compute_delta(e)).round());
171                    cx.notify();
172                }
173            }),
174        );
175
176        cx.scene.push_cursor_region(crate::CursorRegion {
177            bounds: handle_region,
178            style: match self.side.axis() {
179                Axis::Horizontal => CursorStyle::ResizeLeftRight,
180                Axis::Vertical => CursorStyle::ResizeUpDown,
181            },
182        });
183
184        cx.scene.pop_stacking_context();
185
186        self.child.paint(bounds.origin(), visible_bounds, cx);
187    }
188
189    fn dispatch_event(
190        &mut self,
191        event: &crate::Event,
192        _bounds: pathfinder_geometry::rect::RectF,
193        _visible_bounds: pathfinder_geometry::rect::RectF,
194        _layout: &mut Self::LayoutState,
195        _paint: &mut Self::PaintState,
196        cx: &mut crate::EventContext,
197    ) -> bool {
198        self.child.dispatch_event(event, cx)
199    }
200
201    fn rect_for_text_range(
202        &self,
203        range_utf16: std::ops::Range<usize>,
204        _bounds: pathfinder_geometry::rect::RectF,
205        _visible_bounds: pathfinder_geometry::rect::RectF,
206        _layout: &Self::LayoutState,
207        _paint: &Self::PaintState,
208        cx: &crate::MeasurementContext,
209    ) -> Option<pathfinder_geometry::rect::RectF> {
210        self.child.rect_for_text_range(range_utf16, cx)
211    }
212
213    fn debug(
214        &self,
215        _bounds: pathfinder_geometry::rect::RectF,
216        _layout: &Self::LayoutState,
217        _paint: &Self::PaintState,
218        cx: &crate::DebugContext,
219    ) -> serde_json::Value {
220        json!({
221            "child": self.child.debug(cx),
222        })
223    }
224}