resizable.rs

  1use std::{cell::Cell, rc::Rc};
  2
  3use pathfinder_geometry::vector::Vector2F;
  4use serde_json::json;
  5
  6use crate::{
  7    color::Color, scene::DragRegionEvent, Axis, Border, CursorStyle, Element, ElementBox,
  8    ElementStateHandle, MouseButton, RenderContext, View, MouseRegion,
  9};
 10
 11use super::{ConstrainedBox, Empty, Flex, Hook, MouseEventHandler, Padding, 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 resize_padding(&self, padding_size: f32) -> Padding {
 39        match self.axis() {
 40            Axis::Horizontal => Padding::horizontal(padding_size),
 41            Axis::Vertical => Padding::vertical(padding_size),
 42        }
 43    }
 44
 45    fn relevant_component(&self, vector: Vector2F) -> f32 {
 46        match self.axis() {
 47            Axis::Horizontal => vector.x(),
 48            Axis::Vertical => vector.y(),
 49        }
 50    }
 51
 52    fn compute_delta(&self, e: DragRegionEvent) -> f32 {
 53        if self.before_content() {
 54            self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
 55        } else {
 56            self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
 57        }
 58    }
 59}
 60
 61struct ResizeHandleState {
 62    actual_dimension: Cell<f32>,
 63    custom_dimension: Cell<f32>,
 64}
 65
 66pub struct Resizable {
 67    side: Side,
 68    child: ElementBox,
 69    state: Rc<ResizeHandleState>,
 70    _state_handle: ElementStateHandle<Rc<ResizeHandleState>>,
 71}
 72
 73impl Resizable {
 74    pub fn new<Tag: 'static, T: View>(
 75        child: ElementBox,
 76        element_id: usize,
 77        side: Side,
 78        handle_size: f32,
 79        initial_size: f32,
 80        cx: &mut RenderContext<T>,
 81    ) -> Self {
 82        let state_handle = cx.element_state::<Tag, Rc<ResizeHandleState>>(
 83            element_id,
 84            Rc::new(ResizeHandleState {
 85                actual_dimension: Cell::new(initial_size),
 86                custom_dimension: Cell::new(initial_size),
 87            }),
 88        );
 89
 90        let state = state_handle.read(cx).clone();
 91
 92        let mut flex = Flex::new(side.axis());
 93
 94        if side.before_content() {
 95            dbg!("HANDLE BEING RENDERED BEFORE");
 96            flex.add_child(render_resize_handle(state.clone(), side, handle_size, cx))
 97        }
 98
 99        flex.add_child(
100            Hook::new({
101                let constrained = ConstrainedBox::new(child);
102                match side.axis() {
103                    Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()),
104                    Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()),
105                }
106                .boxed()
107            })
108            .on_after_layout({
109                let state = state.clone();
110                move |size, _| {
111                    state.actual_dimension.set(side.relevant_component(size));
112                }
113            })
114            .boxed(),
115        );
116
117        if !side.before_content() {
118            dbg!("HANDLE BEING RENDERED AFTER");
119            flex.add_child(render_resize_handle(state.clone(), side, handle_size, cx))
120        }
121
122        let child = flex.boxed();
123
124        Self {
125            side,
126            child,
127            state,
128            _state_handle: state_handle,
129        }
130    }
131}
132
133fn render_resize_handle<T: View>(
134    state: Rc<ResizeHandleState>,
135    side: Side,
136    padding_size: f32,
137    cx: &mut RenderContext<T>,
138) -> ElementBox {
139    enum ResizeHandle {}
140    MouseEventHandler::<ResizeHandle>::new(side as usize, cx, |_, _| {
141        Empty::new()
142            // Border necessary to properly add a MouseRegion
143            .contained()
144            .with_border(Border {
145                width: 4.,
146                left: true,
147                color: Color::red(),
148                ..Default::default()
149            })
150            .boxed()
151    })
152    .with_padding(side.resize_padding(padding_size))
153    .with_cursor_style(match side.axis() {
154        Axis::Horizontal => CursorStyle::ResizeLeftRight,
155        Axis::Vertical => CursorStyle::ResizeUpDown,
156    })
157    .on_down(MouseButton::Left, |_, _| {}) // This prevents the mouse down event from being propagated elsewhere
158    .on_drag(MouseButton::Left, move |e, cx| {
159        let prev_width = state.actual_dimension.get();
160        state
161            .custom_dimension
162            .set(0f32.max(prev_width + side.compute_delta(e)).round());
163        cx.notify();
164    })
165    .boxed()
166}
167
168impl Element for Resizable {
169    type LayoutState = Vector2F;
170    type PaintState = ();
171
172    fn layout(
173        &mut self,
174        constraint: crate::SizeConstraint,
175        cx: &mut crate::LayoutContext,
176    ) -> (Vector2F, Self::LayoutState) {
177        let child_size = self.child.layout(constraint, cx);
178        (child_size, child_size)
179    }
180
181    fn paint(
182        &mut self,
183        bounds: pathfinder_geometry::rect::RectF,
184        visible_bounds: pathfinder_geometry::rect::RectF,
185        child_size: &mut Self::LayoutState,
186        cx: &mut crate::PaintContext,
187    ) -> Self::PaintState {
188        cx.scene.push_stacking_context(None);
189        
190        // Render a mouse region on the appropriate border (likely just bounds)
191        // Use the padding in the above code to decide the size of the rect to pass to the mouse region
192        // Add handlers for Down and Drag like above
193        
194        // Maybe try pushing a quad to visually inspect where the region gets placed
195        // Push a cursor region
196        cx.scene.push_mouse_region(MouseRegion::)
197        
198        cx.scene.pop_stacking_context();
199
200        self.child.paint(bounds.origin(), visible_bounds, cx);
201    }
202
203    fn dispatch_event(
204        &mut self,
205        event: &crate::Event,
206        _bounds: pathfinder_geometry::rect::RectF,
207        _visible_bounds: pathfinder_geometry::rect::RectF,
208        _layout: &mut Self::LayoutState,
209        _paint: &mut Self::PaintState,
210        cx: &mut crate::EventContext,
211    ) -> bool {
212        self.child.dispatch_event(event, cx)
213    }
214
215    fn rect_for_text_range(
216        &self,
217        range_utf16: std::ops::Range<usize>,
218        _bounds: pathfinder_geometry::rect::RectF,
219        _visible_bounds: pathfinder_geometry::rect::RectF,
220        _layout: &Self::LayoutState,
221        _paint: &Self::PaintState,
222        cx: &crate::MeasurementContext,
223    ) -> Option<pathfinder_geometry::rect::RectF> {
224        self.child.rect_for_text_range(range_utf16, cx)
225    }
226
227    fn debug(
228        &self,
229        _bounds: pathfinder_geometry::rect::RectF,
230        _layout: &Self::LayoutState,
231        _paint: &Self::PaintState,
232        cx: &crate::DebugContext,
233    ) -> serde_json::Value {
234        json!({
235            "child": self.child.debug(cx),
236        })
237    }
238}