use std::panic::Location;
use crate::cursor::Cursor;
use crate::event::{UiEvent, UiEventKind, UiKey};
use crate::tokens;
use crate::tree::*;
pub const HANDLE_THICKNESS: f32 = 8.0;
const HAIRLINE_THICKNESS: f32 = 2.0;
pub const KEYBOARD_STEP_PX: f32 = 8.0;
pub const KEYBOARD_PAGE_STEP_PX: f32 = 40.0;
#[track_caller]
pub fn resize_handle(axis: Axis) -> El {
let (width, height) = match axis {
Axis::Row => (Size::Fixed(HANDLE_THICKNESS), Size::Fill(1.0)),
Axis::Column => (Size::Fill(1.0), Size::Fixed(HANDLE_THICKNESS)),
Axis::Overlay => (Size::Fixed(HANDLE_THICKNESS), Size::Fixed(HANDLE_THICKNESS)),
};
let hairline = match axis {
Axis::Row => El::new(Kind::Custom("resize-handle-hairline"))
.width(Size::Fixed(HAIRLINE_THICKNESS))
.height(Size::Fill(1.0))
.fill(tokens::BORDER)
.state_follows_interactive_ancestor(),
Axis::Column | Axis::Overlay => El::new(Kind::Custom("resize-handle-hairline"))
.width(Size::Fill(1.0))
.height(Size::Fixed(HAIRLINE_THICKNESS))
.fill(tokens::BORDER)
.state_follows_interactive_ancestor(),
};
let cursor = match axis {
Axis::Row => Cursor::EwResize,
Axis::Column => Cursor::NsResize,
Axis::Overlay => Cursor::EwResize,
};
stack([hairline])
.at_loc(Location::caller())
.align(Align::Center)
.justify(Justify::Center)
.focusable()
.paint_overflow(Sides::all(tokens::RING_WIDTH))
.cursor(cursor)
.width(width)
.height(height)
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ResizeDrag {
pub anchor: Option<f32>,
pub initial: f32,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Side {
#[default]
Start,
End,
}
impl Side {
fn sign(self) -> f32 {
match self {
Side::Start => 1.0,
Side::End => -1.0,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct ResizeWeightsDrag {
pub anchor: Option<f32>,
pub initial: [f32; 2],
}
fn project(pos: (f32, f32), axis: Axis) -> f32 {
match axis {
Axis::Row | Axis::Overlay => pos.0,
Axis::Column => pos.1,
}
}
pub fn delta_from_event(drag: &ResizeDrag, event: &UiEvent, axis: Axis) -> Option<f32> {
let anchor = drag.anchor?;
let pos = event.pointer?;
Some(project(pos, axis) - anchor)
}
#[allow(clippy::too_many_arguments)]
pub fn apply_event_fixed(
value: &mut f32,
drag: &mut ResizeDrag,
event: &UiEvent,
key: &str,
axis: Axis,
side: Side,
min: f32,
max: f32,
) -> bool {
if event.route() != Some(key) {
return false;
}
match event.kind {
UiEventKind::PointerDown => {
if let Some(pos) = event.pointer {
drag.anchor = Some(project(pos, axis));
drag.initial = *value;
}
false
}
UiEventKind::Drag => {
let Some(anchor) = drag.anchor else {
return false;
};
let Some(pos) = event.pointer else {
return false;
};
let pixel_delta = (project(pos, axis) - anchor) * side.sign();
let next = (drag.initial + pixel_delta).clamp(min, max);
let changed = (next - *value).abs() > f32::EPSILON;
*value = next;
changed
}
UiEventKind::PointerUp => {
drag.anchor = None;
false
}
UiEventKind::KeyDown => apply_key(value, event, side, min, max),
_ => false,
}
}
pub fn apply_event_weights(
weights: &mut [f32; 2],
drag: &mut ResizeWeightsDrag,
event: &UiEvent,
key: &str,
axis: Axis,
parent_main_extent: f32,
min_weight: f32,
) -> bool {
if event.route() != Some(key) {
return false;
}
match event.kind {
UiEventKind::PointerDown => {
if let Some(pos) = event.pointer {
drag.anchor = Some(project(pos, axis));
drag.initial = *weights;
}
false
}
UiEventKind::Drag => {
let Some(anchor) = drag.anchor else {
return false;
};
let Some(pos) = event.pointer else {
return false;
};
if parent_main_extent <= 0.0 {
return false;
}
let total = drag.initial[0] + drag.initial[1];
if total <= 0.0 {
return false;
}
let pixel_delta = project(pos, axis) - anchor;
let weight_delta = pixel_delta * (total / parent_main_extent);
let lo = min_weight.max(0.0);
let hi = (total - lo).max(lo);
let next_left = (drag.initial[0] + weight_delta).clamp(lo, hi);
let next_right = total - next_left;
let changed = (next_left - weights[0]).abs() > f32::EPSILON
|| (next_right - weights[1]).abs() > f32::EPSILON;
weights[0] = next_left;
weights[1] = next_right;
changed
}
UiEventKind::PointerUp => {
drag.anchor = None;
false
}
_ => false,
}
}
fn apply_key(value: &mut f32, event: &UiEvent, side: Side, min: f32, max: f32) -> bool {
let Some(press) = event.key_press.as_ref() else {
return false;
};
let prev = *value;
let step = KEYBOARD_STEP_PX * side.sign();
let page_step = KEYBOARD_PAGE_STEP_PX * side.sign();
let (home_target, end_target) = match side {
Side::Start => (min, max),
Side::End => (max, min),
};
let next = match press.key {
UiKey::ArrowRight | UiKey::ArrowDown => *value + step,
UiKey::ArrowLeft | UiKey::ArrowUp => *value - step,
UiKey::PageUp => *value + page_step,
UiKey::PageDown => *value - page_step,
UiKey::Home => home_target,
UiKey::End => end_target,
_ => return false,
};
*value = next.clamp(min, max);
(*value - prev).abs() > f32::EPSILON
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{KeyModifiers, KeyPress, UiTarget};
fn pointer_event(kind: UiEventKind, key: &str, x: f32) -> UiEvent {
let click_count = match kind {
UiEventKind::PointerDown | UiEventKind::PointerUp | UiEventKind::Click => 1,
_ => 0,
};
UiEvent {
path: None,
key: Some(key.to_string()),
target: Some(UiTarget {
key: key.to_string(),
node_id: format!("/{key}"),
rect: Rect::new(0.0, 0.0, 6.0, 400.0),
}),
pointer: Some((x, 100.0)),
key_press: None,
text: None,
selection: None,
modifiers: KeyModifiers::default(),
click_count,
kind,
}
}
fn key_event(key: &str, ui_key: UiKey) -> UiEvent {
UiEvent {
path: None,
key: Some(key.to_string()),
target: Some(UiTarget {
key: key.to_string(),
node_id: format!("/{key}"),
rect: Rect::new(0.0, 0.0, 6.0, 400.0),
}),
pointer: None,
key_press: Some(KeyPress {
key: ui_key,
modifiers: KeyModifiers::default(),
repeat: false,
}),
text: None,
selection: None,
modifiers: KeyModifiers::default(),
click_count: 0,
kind: UiEventKind::KeyDown,
}
}
#[test]
fn handle_is_focusable_and_thin_in_its_resize_axis() {
let row_handle = resize_handle(Axis::Row);
assert!(row_handle.focusable);
assert_eq!(row_handle.width, Size::Fixed(HANDLE_THICKNESS));
assert_eq!(row_handle.height, Size::Fill(1.0));
let col_handle = resize_handle(Axis::Column);
assert_eq!(col_handle.width, Size::Fill(1.0));
assert_eq!(col_handle.height, Size::Fixed(HANDLE_THICKNESS));
}
#[test]
fn handle_cursor_matches_drag_axis() {
assert_eq!(
resize_handle(Axis::Row).cursor,
Some(crate::cursor::Cursor::EwResize),
);
assert_eq!(
resize_handle(Axis::Column).cursor,
Some(crate::cursor::Cursor::NsResize),
);
}
#[test]
fn handle_does_not_capture_keys() {
assert!(!resize_handle(Axis::Row).capture_keys);
assert!(!resize_handle(Axis::Column).capture_keys);
}
#[test]
fn fixed_drag_uses_absolute_anchor_so_no_drift() {
let mut value = 256.0;
let mut drag = ResizeDrag::default();
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::PointerDown, "h", 300.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert_eq!(drag.anchor, Some(300.0));
assert_eq!(drag.initial, 256.0);
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::Drag, "h", 350.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert!((value - 306.0).abs() < 1e-3);
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::Drag, "h", 380.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert!((value - 336.0).abs() < 1e-3);
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::PointerUp, "h", 380.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert_eq!(drag.anchor, None, "anchor cleared on PointerUp");
}
#[test]
fn fixed_drag_clamps_to_min_max() {
let mut value = 256.0;
let mut drag = ResizeDrag::default();
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::PointerDown, "h", 300.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::Drag, "h", 1000.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert_eq!(value, 480.0);
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::Drag, "h", 0.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert_eq!(value, 180.0);
}
#[test]
fn fixed_ignores_unrouted_events() {
let mut value = 256.0;
let mut drag = ResizeDrag::default();
let changed = apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::PointerDown, "other", 300.0),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert!(!changed);
assert_eq!(drag.anchor, None);
assert_eq!(value, 256.0);
}
#[test]
fn fixed_arrow_keys_nudge_within_bounds() {
let mut value = 256.0;
let mut drag = ResizeDrag::default();
apply_event_fixed(
&mut value,
&mut drag,
&key_event("h", UiKey::ArrowRight),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert!((value - (256.0 + KEYBOARD_STEP_PX)).abs() < 1e-3);
apply_event_fixed(
&mut value,
&mut drag,
&key_event("h", UiKey::Home),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert_eq!(value, 180.0);
let unchanged = apply_event_fixed(
&mut value,
&mut drag,
&key_event("h", UiKey::ArrowLeft),
"h",
Axis::Row,
Side::Start,
180.0,
480.0,
);
assert!(!unchanged);
assert_eq!(value, 180.0);
}
#[test]
fn weights_drag_redistributes_proportionally_to_parent_extent() {
let mut weights = [1.0, 1.0];
let mut drag = ResizeWeightsDrag::default();
apply_event_weights(
&mut weights,
&mut drag,
&pointer_event(UiEventKind::PointerDown, "split", 400.0),
"split",
Axis::Row,
800.0,
0.15,
);
apply_event_weights(
&mut weights,
&mut drag,
&pointer_event(UiEventKind::Drag, "split", 500.0),
"split",
Axis::Row,
800.0,
0.15,
);
assert!((weights[0] - 1.25).abs() < 1e-3, "left = {}", weights[0]);
assert!((weights[1] - 0.75).abs() < 1e-3, "right = {}", weights[1]);
assert!(
(weights[0] + weights[1] - 2.0).abs() < 1e-3,
"total weight is conserved"
);
}
#[test]
fn weights_drag_clamps_each_side_to_min_weight() {
let mut weights = [1.0, 1.0];
let mut drag = ResizeWeightsDrag::default();
apply_event_weights(
&mut weights,
&mut drag,
&pointer_event(UiEventKind::PointerDown, "split", 400.0),
"split",
Axis::Row,
800.0,
0.5, );
apply_event_weights(
&mut weights,
&mut drag,
&pointer_event(UiEventKind::Drag, "split", 10_000.0),
"split",
Axis::Row,
800.0,
0.5,
);
assert!((weights[0] - 1.5).abs() < 1e-3);
assert!((weights[1] - 0.5).abs() < 1e-3);
apply_event_weights(
&mut weights,
&mut drag,
&pointer_event(UiEventKind::Drag, "split", -10_000.0),
"split",
Axis::Row,
800.0,
0.5,
);
assert!((weights[0] - 0.5).abs() < 1e-3);
assert!((weights[1] - 1.5).abs() < 1e-3);
}
#[test]
fn delta_from_event_returns_none_until_pointerdown() {
let drag = ResizeDrag::default();
let drag_event = pointer_event(UiEventKind::Drag, "h", 350.0);
assert!(delta_from_event(&drag, &drag_event, Axis::Row).is_none());
}
#[test]
fn fixed_drag_with_end_side_inverts_direction() {
let mut value = 256.0;
let mut drag = ResizeDrag::default();
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::PointerDown, "h", 800.0),
"h",
Axis::Row,
Side::End,
180.0,
480.0,
);
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::Drag, "h", 750.0),
"h",
Axis::Row,
Side::End,
180.0,
480.0,
);
assert!(
(value - 306.0).abs() < 1e-3,
"drag-left on End side should grow value, got {value}",
);
apply_event_fixed(
&mut value,
&mut drag,
&pointer_event(UiEventKind::Drag, "h", 830.0),
"h",
Axis::Row,
Side::End,
180.0,
480.0,
);
assert!(
(value - 226.0).abs() < 1e-3,
"drag-right on End side should shrink value, got {value}",
);
}
#[test]
fn fixed_arrow_keys_with_end_side_invert_direction() {
let mut value = 256.0;
let mut drag = ResizeDrag::default();
apply_event_fixed(
&mut value,
&mut drag,
&key_event("h", UiKey::ArrowLeft),
"h",
Axis::Row,
Side::End,
180.0,
480.0,
);
assert!((value - (256.0 + KEYBOARD_STEP_PX)).abs() < 1e-3);
apply_event_fixed(
&mut value,
&mut drag,
&key_event("h", UiKey::ArrowRight),
"h",
Axis::Row,
Side::End,
180.0,
480.0,
);
assert!((value - 256.0).abs() < 1e-3);
apply_event_fixed(
&mut value,
&mut drag,
&key_event("h", UiKey::Home),
"h",
Axis::Row,
Side::End,
180.0,
480.0,
);
assert_eq!(value, 480.0);
apply_event_fixed(
&mut value,
&mut drag,
&key_event("h", UiKey::End),
"h",
Axis::Row,
Side::End,
180.0,
480.0,
);
assert_eq!(value, 180.0);
}
}