506 lines
16 KiB
Rust
506 lines
16 KiB
Rust
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<dyn Element>,
|
|
origin: Option<Vector2F>,
|
|
dragbar: Dragbar,
|
|
state_handle: ResizableStateHandle,
|
|
size: Option<Vector2F>,
|
|
bounds_callback: Option<BoundsCallback>,
|
|
resize_handler: Option<Handler>,
|
|
start_resize_handler: Option<Handler>,
|
|
end_resize_handler: Option<Handler>,
|
|
hovering_dragbar: bool,
|
|
direction: ResizeDirection,
|
|
origin_delta: Vector2F,
|
|
dragbar_offset: f32,
|
|
}
|
|
|
|
type Handler = Box<dyn FnMut(&mut EventContext, &AppContext)>;
|
|
pub type BoundsCallback = Box<dyn FnMut(Vector2F) -> (f32, f32)>;
|
|
|
|
/// Similar to MouseStateHandle, the view that incorporates the owner can instantiate,
|
|
/// read, and set the state.
|
|
pub type ResizableStateHandle = Arc<Mutex<ResizableState>>;
|
|
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<Vector2F>,
|
|
dragbar_side: DragBarSide,
|
|
) -> Option<Vector2F> {
|
|
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<Vector2F>,
|
|
dragbar_side: DragBarSide,
|
|
) -> Option<Vector2F> {
|
|
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<RectF>,
|
|
origin: Option<Point>,
|
|
size: Option<Vector2F>,
|
|
z_index: Option<ZIndex>,
|
|
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<dyn Element>) -> 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<F>(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<F>(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<F>(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<Vector2F> {
|
|
self.child.size()
|
|
}
|
|
|
|
fn origin(&self) -> Option<Point> {
|
|
self.child.origin()
|
|
}
|
|
}
|
|
|
|
fn dispatch_callback(callback: Option<&mut Handler>, ctx: &mut EventContext, app: &AppContext) {
|
|
if let Some(callback) = callback {
|
|
callback(ctx, app);
|
|
}
|
|
}
|