use std::{ mem, sync::{Arc, Mutex, MutexGuard}, }; use pathfinder_color::ColorU; use pathfinder_geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }; use crate::{ event::DispatchedEvent, platform::Cursor, AfterLayoutContext, AppContext, Element, EventContext, PaintContext, SizeConstraint, }; use super::{Fill, Point, ZIndex}; const DRAGBAR_WIDTH: f32 = 5.0; /// A UI element with internal resizing ability. /// /// This element takes a ResizableStateHandle to receive a starting size and /// manage dimensions as the element is resized. /// /// Supports both horizontal and vertical resizing via the ResizeDirection. /// /// TODO: /// - Take a configurable dragbar size instead of always using 5.0 pub struct Resizable { child: Box, origin: Option, dragbar: Dragbar, state_handle: ResizableStateHandle, size: Option, bounds_callback: Option, resize_handler: Option, start_resize_handler: Option, end_resize_handler: Option, hovering_dragbar: bool, direction: ResizeDirection, origin_delta: Vector2F, dragbar_offset: f32, } type Handler = Box; pub type BoundsCallback = Box (f32, f32)>; /// Similar to MouseStateHandle, the view that incorporates the owner can instantiate, /// read, and set the state. pub type ResizableStateHandle = Arc>; pub fn resizable_state_handle(size: f32) -> ResizableStateHandle { Arc::new(Mutex::new(ResizableState::new(size))) } pub struct ResizableState { size: f32, bounds: Option<(f32, f32)>, mode: ResizableMode, } #[derive(Default)] pub enum ResizableMode { Dragging { last_position: Vector2F, }, #[default] Stationary, } impl ResizableState { pub fn new(size: f32) -> Self { Self { size, bounds: None, mode: Default::default(), } } pub fn size(&self) -> f32 { self.size } pub fn clamp_size(&mut self) { if let Some((min, max)) = self.bounds { self.size = self.size.clamp(min, max); } } fn check_for_resize( &mut self, position: Vector2F, origin: Option, dragbar_side: DragBarSide, ) -> Option { if let ResizableMode::Dragging { last_position } = self.mode { self.resize(last_position, position, origin, dragbar_side) } else { None } } fn is_resizing(&self) -> bool { matches!(self.mode, ResizableMode::Dragging { .. }) } fn resize( &mut self, old_position: Vector2F, new_position: Vector2F, origin: Option, dragbar_side: DragBarSide, ) -> Option { let mut resized = false; if let Some(origin) = origin { let delta = match dragbar_side { DragBarSide::Right => new_position.x() - old_position.x(), DragBarSide::Left => old_position.x() - new_position.x(), DragBarSide::Bottom => new_position.y() - old_position.y(), DragBarSide::Top => old_position.y() - new_position.y(), }; let old_size = self.size; if delta.abs() >= f32::EPSILON { resized = true; self.size += delta; self.clamp_size(); } let size = self.size; // The last position should reflect the latest position of the dragbar. let last_position = match dragbar_side { // With a right-side dragbar, the latest position of the dragbar will // be the old origin of the element plus the new width/height. DragBarSide::Right => origin + vec2f(size, 0.), // With a left-side dragbar, the latest position of the dragbar will // be the old origin of the element minus the bounded delta of the drag. DragBarSide::Left => origin - vec2f(size - old_size, 0.), // With a bottom-side dragbar, the latest position of the dragbar will // be the old origin of the element plus the new height. DragBarSide::Bottom => origin + vec2f(0., size), // With a top-side dragbar, the latest position of the dragbar will // be the old origin of the element minus the bounded delta of the drag. DragBarSide::Top => origin - vec2f(0., size - old_size), }; let origin_delta = match dragbar_side { DragBarSide::Right => Vector2F::zero(), DragBarSide::Left => vec2f(old_size - size, 0.), DragBarSide::Bottom => Vector2F::zero(), DragBarSide::Top => vec2f(0., old_size - size), }; self.mode = ResizableMode::Dragging { last_position }; if resized { Some(origin_delta) } else { None } } else { None } } pub fn begin_resizing(&mut self, position: Vector2F) { self.mode = ResizableMode::Dragging { last_position: position, }; } pub fn end_resizing(&mut self) { self.mode = ResizableMode::Stationary; } pub fn set_size(&mut self, new_size: f32) { self.size = new_size; } } struct Dragbar { bounds: Option, origin: Option, size: Option, z_index: Option, color: Fill, side: DragBarSide, } #[derive(Copy, Clone, Default)] pub enum DragBarSide { Left, #[default] Right, Top, Bottom, } #[derive(Copy, Clone, Default)] pub enum ResizeDirection { #[default] Horizontal, Vertical, } impl Dragbar { pub fn new() -> Self { let color = Fill::Solid(ColorU::transparent_black()); Self { bounds: None, origin: None, size: None, z_index: None, color, side: Default::default(), } } } impl Resizable { pub fn new(state_handle: ResizableStateHandle, child: Box) -> Self { Self { child, origin: None, state_handle, size: None, bounds_callback: None, resize_handler: None, start_resize_handler: None, end_resize_handler: None, dragbar: Dragbar::new(), hovering_dragbar: false, direction: ResizeDirection::Horizontal, origin_delta: Vector2F::zero(), dragbar_offset: 0.0, } } /// Adds a callback which will be called on a resize. /// Generally, this should trigger a re-render in the parent. pub fn on_resize(mut self, callback: F) -> Self where F: FnMut(&mut EventContext, &AppContext) + 'static, { self.resize_handler = Some(Box::new(callback)); self } pub fn on_start_resizing(mut self, callback: F) -> Self where F: FnMut(&mut EventContext, &AppContext) + 'static, { self.start_resize_handler = Some(Box::new(callback)); self } pub fn on_end_resizing(mut self, callback: F) -> Self where F: FnMut(&mut EventContext, &AppContext) + 'static, { self.end_resize_handler = Some(Box::new(callback)); self } /// Sets a function that computes the (min, max) bounds on the width/height /// of the resizable. The bounds are updated at paint time. pub fn with_bounds_callback(mut self, callback: BoundsCallback) -> Self { self.bounds_callback = Some(callback); self } pub fn with_dragbar_color(mut self, color: Fill) -> Self { self.dragbar.color = color; self } pub fn with_dragbar_side(mut self, side: DragBarSide) -> Self { self.dragbar.side = side; // Automatically set direction based on side self.direction = match side { DragBarSide::Left | DragBarSide::Right => ResizeDirection::Horizontal, DragBarSide::Top | DragBarSide::Bottom => ResizeDirection::Vertical, }; self } /// Sets an offset for the dragbar position. /// Positive values move the dragbar outwards (away from the center of the element). /// Negative values move the dragbar inwards (towards the center of the element). pub fn with_dragbar_offset(mut self, offset: f32) -> Self { self.dragbar_offset = offset; self } fn state(&mut self) -> MutexGuard<'_, ResizableState> { self.state_handle .lock() .expect("Resizable state should be accessible") } /// Determine if the mouse is hovering over the dragbar /// /// If there is another element above this one at the cursor position, then we treat that as /// outside the element for purposes of MouseState fn is_mouse_hovering_dragbar(&self, ctx: &EventContext, position: Vector2F) -> bool { let Some(dragbar_origin) = self.dragbar.origin else { log::warn!("self.origin was None in `Hoverable::is_mouse_in`"); return false; }; let Some(dragbar_size) = self.dragbar.size else { log::warn!("self.size() was None in `Hoverable::is_mouse_in`"); return false; }; let Some(z_index) = self.dragbar.z_index else { log::warn!("self.child_max_z_index was None in `Hoverable::is_mouse_in`"); return false; }; let is_hovering = ctx .visible_rect(dragbar_origin, dragbar_size) .is_some_and(|bound| bound.contains_point(position)); let point = Point::from_vec2f(position, z_index); let is_covered = ctx.is_covered(point); is_hovering && !is_covered } } impl Element for Resizable { fn layout( &mut self, constraint: crate::SizeConstraint, ctx: &mut crate::LayoutContext, app: &AppContext, ) -> Vector2F { // Use the window size to set bounds on the width/height if let Some(bounds_callback) = self.bounds_callback.as_mut() { let mut new_bounds = bounds_callback(ctx.window_size); if new_bounds.0 > new_bounds.1 { log::error!("Resizable: min bound is greater than max bound"); new_bounds = (new_bounds.0, new_bounds.0); } self.state().bounds = Some(new_bounds); // With new bounds, we should also clamp the current width/height. self.state().clamp_size(); } let size = self.state().size; // We set the child constraints to never be greater than the current width/height constraint. let child_constraint = match self.direction { ResizeDirection::Horizontal => SizeConstraint { min: (constraint.min) .max(Vector2F::zero()) .min(Vector2F::new(size, f32::MAX)), max: (constraint.max) .max(Vector2F::zero()) .min(Vector2F::new(size, f32::MAX)), }, ResizeDirection::Vertical => SizeConstraint { min: (constraint.min) .max(Vector2F::zero()) .min(Vector2F::new(f32::MAX, size)), max: (constraint.max) .max(Vector2F::zero()) .min(Vector2F::new(f32::MAX, size)), }, }; let child_size = self.child.layout(child_constraint, ctx, app); let size = child_size; self.size = Some(size); size } fn after_layout(&mut self, ctx: &mut AfterLayoutContext, app: &AppContext) { self.child.after_layout(ctx, app) } fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) { self.child.paint(origin, ctx, app); // Draw the dragbar and record its size and position let child_size = self.child.size().unwrap(); let (dragbar_origin, dragbar_size) = match self.dragbar.side { DragBarSide::Left => ( origin - vec2f(self.dragbar_offset, 0.), vec2f(DRAGBAR_WIDTH, child_size.y()), ), DragBarSide::Right => ( origin + vec2f(child_size.x() - DRAGBAR_WIDTH + self.dragbar_offset, 0.), vec2f(DRAGBAR_WIDTH, child_size.y()), ), DragBarSide::Top => ( origin - vec2f(0., self.dragbar_offset), vec2f(child_size.x(), DRAGBAR_WIDTH), ), DragBarSide::Bottom => ( origin + vec2f(0., child_size.y() - DRAGBAR_WIDTH + self.dragbar_offset), vec2f(child_size.x(), DRAGBAR_WIDTH), ), }; ctx.scene .draw_rect_with_hit_recording(RectF::new(dragbar_origin, dragbar_size)) .with_background(self.dragbar.color); self.dragbar.bounds = Some(RectF::new(dragbar_origin, dragbar_size)); self.dragbar.origin = Some(Point::from_vec2f(dragbar_origin, ctx.scene.z_index())); self.dragbar.size = Some(dragbar_size); self.dragbar.z_index = Some(ctx.scene.max_active_z_index()); self.origin = Some(origin); self.origin_delta = Vector2F::zero(); } fn dispatch_event( &mut self, event: &DispatchedEvent, ctx: &mut EventContext, app: &AppContext, ) -> bool { let child_handled = self.child.dispatch_event(event, ctx, app); match event.raw_event() { crate::Event::LeftMouseDown { position, .. } => { // If a mouse-down on the dragbar element occurred, put the view into resizing mode if self .dragbar .bounds .is_some_and(|bounds| bounds.contains_point(*position)) { self.state().begin_resizing(*position); dispatch_callback(self.resize_handler.as_mut(), ctx, app); return true; } } crate::Event::LeftMouseUp { .. } => { // If a mouse-up occurs, take the view out of resizing mode if self.state().is_resizing() { ctx.reset_cursor(); self.state().end_resizing(); dispatch_callback(self.end_resize_handler.as_mut(), ctx, app); return true; } } crate::Event::LeftMouseDragged { position, .. } => { if self.state().is_resizing() { let dragbar_side = self.dragbar.side; let origin = self.origin.map(|origin| origin + self.origin_delta); let resized = self .state() .check_for_resize(*position, origin, dragbar_side); self.origin_delta += resized.unwrap_or_default(); if resized.is_some() { dispatch_callback(self.resize_handler.as_mut(), ctx, app) } return true; } } crate::Event::MouseMoved { position, .. } => { // A mouse event over the dragbar should set the cursor let Some(z_index) = self.z_index() else { log::warn!("self.z_index() was None in `Resizable`"); return false; }; let hovering_dragbar = self.is_mouse_hovering_dragbar(ctx, *position); let was_already_hovering = mem::replace(&mut self.hovering_dragbar, hovering_dragbar); if hovering_dragbar && !was_already_hovering { let cursor = match self.direction { ResizeDirection::Horizontal => Cursor::ResizeLeftRight, ResizeDirection::Vertical => Cursor::ResizeUpDown, }; ctx.set_cursor(cursor, z_index); } else if !hovering_dragbar && was_already_hovering { ctx.reset_cursor(); } return true; } _ => {} } child_handled } fn size(&self) -> Option { self.child.size() } fn origin(&self) -> Option { self.child.origin() } } fn dispatch_callback(callback: Option<&mut Handler>, ctx: &mut EventContext, app: &AppContext) { if let Some(callback) = callback { callback(ctx, app); } }