I want to use this on another branch, but it's a sweeping change, so this prepares the ground for it. This can always be reverted if it doesn't work out.
201 lines
6.2 KiB
Rust
201 lines
6.2 KiB
Rust
use std::{cell::RefCell, rc::Rc};
|
|
|
|
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
|
use serde_json::json;
|
|
|
|
use crate::{
|
|
geometry::rect::RectF,
|
|
platform::{CursorStyle, MouseButton},
|
|
scene::MouseDrag,
|
|
AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder,
|
|
SizeConstraint, View, ViewContext,
|
|
};
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
pub enum HandleSide {
|
|
Top,
|
|
Bottom,
|
|
Left,
|
|
Right,
|
|
}
|
|
|
|
impl HandleSide {
|
|
fn axis(&self) -> Axis {
|
|
match self {
|
|
HandleSide::Left | HandleSide::Right => Axis::Horizontal,
|
|
HandleSide::Top | HandleSide::Bottom => Axis::Vertical,
|
|
}
|
|
}
|
|
|
|
/// 'before' is in reference to the standard english document ordering of left-to-right
|
|
/// then top-to-bottom
|
|
fn before_content(self) -> bool {
|
|
match self {
|
|
HandleSide::Left | HandleSide::Top => true,
|
|
HandleSide::Right | HandleSide::Bottom => false,
|
|
}
|
|
}
|
|
|
|
fn relevant_component(&self, vector: Vector2F) -> f32 {
|
|
match self.axis() {
|
|
Axis::Horizontal => vector.x(),
|
|
Axis::Vertical => vector.y(),
|
|
}
|
|
}
|
|
|
|
fn compute_delta(&self, e: MouseDrag) -> f32 {
|
|
if self.before_content() {
|
|
self.relevant_component(e.prev_mouse_position) - self.relevant_component(e.position)
|
|
} else {
|
|
self.relevant_component(e.position) - self.relevant_component(e.prev_mouse_position)
|
|
}
|
|
}
|
|
|
|
fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF {
|
|
match self {
|
|
HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)),
|
|
HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())),
|
|
HandleSide::Bottom => {
|
|
let mut origin = bounds.lower_left();
|
|
origin.set_y(origin.y() - handle_size);
|
|
RectF::new(origin, vec2f(bounds.width(), handle_size))
|
|
}
|
|
HandleSide::Right => {
|
|
let mut origin = bounds.upper_right();
|
|
origin.set_x(origin.x() - handle_size);
|
|
RectF::new(origin, vec2f(handle_size, bounds.height()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Resizable<V: View> {
|
|
child: AnyElement<V>,
|
|
handle_side: HandleSide,
|
|
handle_size: f32,
|
|
on_resize: Rc<RefCell<dyn FnMut(&mut V, f32, &mut ViewContext<V>)>>,
|
|
}
|
|
|
|
const DEFAULT_HANDLE_SIZE: f32 = 4.0;
|
|
|
|
impl<V: View> Resizable<V> {
|
|
pub fn new(
|
|
child: AnyElement<V>,
|
|
handle_side: HandleSide,
|
|
size: f32,
|
|
on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext<V>),
|
|
) -> Self {
|
|
let child = match handle_side.axis() {
|
|
Axis::Horizontal => child.constrained().with_max_width(size),
|
|
Axis::Vertical => child.constrained().with_max_height(size),
|
|
}
|
|
.into_any();
|
|
|
|
Self {
|
|
child,
|
|
handle_side,
|
|
handle_size: DEFAULT_HANDLE_SIZE,
|
|
on_resize: Rc::new(RefCell::new(on_resize)),
|
|
}
|
|
}
|
|
|
|
pub fn with_handle_size(mut self, handle_size: f32) -> Self {
|
|
self.handle_size = handle_size;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<V: View> Element<V> for Resizable<V> {
|
|
type LayoutState = SizeConstraint;
|
|
type PaintState = ();
|
|
|
|
fn layout(
|
|
&mut self,
|
|
constraint: crate::SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut LayoutContext<V>,
|
|
) -> (Vector2F, Self::LayoutState) {
|
|
(self.child.layout(constraint, view, cx), constraint)
|
|
}
|
|
|
|
fn paint(
|
|
&mut self,
|
|
scene: &mut SceneBuilder,
|
|
bounds: pathfinder_geometry::rect::RectF,
|
|
visible_bounds: pathfinder_geometry::rect::RectF,
|
|
constraint: &mut SizeConstraint,
|
|
view: &mut V,
|
|
cx: &mut PaintContext<V>,
|
|
) -> Self::PaintState {
|
|
scene.push_stacking_context(None, None);
|
|
|
|
let handle_region = self.handle_side.of_rect(bounds, self.handle_size);
|
|
|
|
enum ResizeHandle {}
|
|
scene.push_mouse_region(
|
|
MouseRegion::new::<ResizeHandle>(
|
|
cx.view_id(),
|
|
self.handle_side as usize,
|
|
handle_region,
|
|
)
|
|
.on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere
|
|
.on_drag(MouseButton::Left, {
|
|
let bounds = bounds.clone();
|
|
let side = self.handle_side;
|
|
let prev_size = side.relevant_component(bounds.size());
|
|
let min_size = side.relevant_component(constraint.min);
|
|
let max_size = side.relevant_component(constraint.max);
|
|
let on_resize = self.on_resize.clone();
|
|
move |event, view: &mut V, cx| {
|
|
let new_size = min_size
|
|
.max(prev_size + side.compute_delta(event))
|
|
.min(max_size)
|
|
.round();
|
|
if new_size != prev_size {
|
|
on_resize.borrow_mut()(view, new_size, cx);
|
|
}
|
|
}
|
|
}),
|
|
);
|
|
|
|
scene.push_cursor_region(crate::CursorRegion {
|
|
bounds: handle_region,
|
|
style: match self.handle_side.axis() {
|
|
Axis::Horizontal => CursorStyle::ResizeLeftRight,
|
|
Axis::Vertical => CursorStyle::ResizeUpDown,
|
|
},
|
|
});
|
|
|
|
scene.pop_stacking_context();
|
|
|
|
self.child
|
|
.paint(scene, bounds.origin(), visible_bounds, view, cx);
|
|
}
|
|
|
|
fn rect_for_text_range(
|
|
&self,
|
|
range_utf16: std::ops::Range<usize>,
|
|
_bounds: pathfinder_geometry::rect::RectF,
|
|
_visible_bounds: pathfinder_geometry::rect::RectF,
|
|
_layout: &Self::LayoutState,
|
|
_paint: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> Option<pathfinder_geometry::rect::RectF> {
|
|
self.child.rect_for_text_range(range_utf16, view, cx)
|
|
}
|
|
|
|
fn debug(
|
|
&self,
|
|
_bounds: pathfinder_geometry::rect::RectF,
|
|
_layout: &Self::LayoutState,
|
|
_paint: &Self::PaintState,
|
|
view: &V,
|
|
cx: &ViewContext<V>,
|
|
) -> serde_json::Value {
|
|
json!({
|
|
"child": self.child.debug(view, cx),
|
|
})
|
|
}
|
|
}
|