use crate::core::{Align, Axis, BoxStyle, Cx, DragMotion, Element, KeyInput, Role, Track};
use crate::style::Theme;
const HANDLE: f64 = 7.0;
const KEY_STEP: f64 = 0.05;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SplitPane {
axis: Axis,
fraction: f64,
min_first: f64,
min_second: f64,
extent: f64,
}
impl SplitPane {
pub fn new(axis: Axis, fraction: f64) -> Self {
Self {
axis,
fraction: clamp_unit(fraction),
min_first: 0.0,
min_second: 0.0,
extent: 0.0,
}
}
pub fn horizontal(fraction: f64) -> Self {
Self::new(Axis::Horizontal, fraction)
}
pub fn vertical(fraction: f64) -> Self {
Self::new(Axis::Vertical, fraction)
}
pub fn min_sizes(mut self, first: f64, second: f64) -> Self {
self.min_first = first.max(0.0);
self.min_second = second.max(0.0);
self
}
pub fn axis(&self) -> Axis {
self.axis
}
pub fn fraction(&self) -> f64 {
self.fraction
}
pub fn set_fraction(&mut self, fraction: f64) {
self.fraction = clamp_unit(fraction);
}
fn clamp_px(&self, first: f64, extent: f64) -> f64 {
let (lo, hi) = (self.min_first, extent - self.min_second);
if lo <= hi {
first.clamp(lo, hi)
} else if self.min_first + self.min_second > 0.0 {
extent * self.min_first / (self.min_first + self.min_second)
} else {
extent / 2.0
}
}
pub fn drag(&mut self, motion: &DragMotion) {
let (pos, start, grab, total) = match self.axis {
Axis::Horizontal => (
motion.position.x,
motion.parent.min_x(),
motion.origin.x - motion.bounds.min_x(),
motion.parent.width(),
),
Axis::Vertical => (
motion.position.y,
motion.parent.min_y(),
motion.origin.y - motion.bounds.min_y(),
motion.parent.height(),
),
};
let extent = total - HANDLE;
if extent <= 0.0 {
return;
}
self.extent = extent;
let first = self.clamp_px(pos - start - grab, extent);
self.fraction = clamp_unit(first / extent);
}
pub fn apply(&mut self, key: &KeyInput) -> bool {
let before = self.fraction;
let target = match key {
KeyInput::Left | KeyInput::Up => self.fraction - KEY_STEP,
KeyInput::Right | KeyInput::Down => self.fraction + KEY_STEP,
KeyInput::Home => 0.0,
KeyInput::End => 1.0,
_ => return false,
};
self.fraction = if self.extent > 0.0 {
clamp_unit(self.clamp_px(target * self.extent, self.extent) / self.extent)
} else {
clamp_unit(target)
};
self.fraction != before
}
}
fn clamp_unit(v: f64) -> f64 {
if v.is_finite() {
v.clamp(0.0, 1.0)
} else {
0.5
}
}
pub fn split_pane<S>(
cx: &mut Cx<S>,
theme: &Theme,
state: &SplitPane,
field: impl Fn(&mut S) -> &mut SplitPane + Copy + 'static,
first: Element,
second: Element,
) -> Element {
let p = &theme.palette;
let rtl = theme.is_rtl();
let f = state.fraction;
let tracks = vec![Track::fr(f), Track::px(HANDLE), Track::fr(1.0 - f)];
let pane = |content: Element| {
Element::stack(Axis::Vertical, vec![content])
.align(Align::Start, Align::Stretch)
.clip()
};
let (cross, grip) = match state.axis {
Axis::Horizontal => (Axis::Vertical, (3.0, 24.0)),
Axis::Vertical => (Axis::Horizontal, (24.0, 3.0)),
};
let segment = || {
let el = Element::boxed(BoxStyle::default()).fill(p.border).grow(1.0);
match state.axis {
Axis::Horizontal => el.width(1.0),
Axis::Vertical => el.height(1.0),
}
};
let grip = Element::boxed(BoxStyle::default())
.fill(p.text_muted)
.radius(1.5)
.width(grip.0)
.height(grip.1);
let handle = Element::stack(cross, vec![segment(), grip, segment()])
.align(Align::Start, Align::Center)
.role(Role::Splitter)
.on_key(cx, move |s: &mut S, k| {
field(s).apply(&super::logical_key(rtl, k));
})
.on_drag_motion(cx, move |s: &mut S, m| field(s).drag(m));
let children = vec![pane(first), handle, pane(second)];
let grid = match state.axis {
Axis::Horizontal => Element::grid(tracks, children).rows(vec![Track::fr(1.0)]),
Axis::Vertical => Element::grid(vec![Track::fr(1.0)], children).rows(tracks),
};
grid.grow(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::App;
use crate::geometry::{Point, Rect, Size};
fn motion(x: f64, origin_x: f64, handle_x: f64) -> DragMotion {
DragMotion {
position: Point::new(x, 50.0),
origin: Point::new(origin_x, 50.0),
bounds: Rect::from_xywh(handle_x, 0.0, HANDLE, 100.0),
parent: Rect::from_xywh(0.0, 0.0, 400.0 + HANDLE, 100.0),
}
}
#[test]
fn drag_follows_the_grab_point_and_honors_minimums() {
let mut s = SplitPane::horizontal(0.5).min_sizes(100.0, 50.0);
s.drag(&motion(103.0, 203.0, 200.0));
assert_eq!(s.fraction(), 0.25);
s.drag(&motion(-50.0, 203.0, 200.0));
assert_eq!(s.fraction(), 0.25);
s.drag(&motion(1000.0, 203.0, 200.0));
assert_eq!(s.fraction(), 0.875);
}
#[test]
fn keys_move_the_divider_within_bounds() {
let mut s = SplitPane::vertical(0.5);
assert!(s.apply(&KeyInput::Down));
assert!((s.fraction() - 0.55).abs() < 1e-9);
assert!(s.apply(&KeyInput::Home));
assert_eq!(s.fraction(), 0.0);
assert!(!s.apply(&KeyInput::Up), "already at the start");
assert!(!s.apply(&KeyInput::Enter));
let mut h = SplitPane::horizontal(0.5).min_sizes(100.0, 100.0);
h.drag(&motion(203.0, 203.0, 200.0));
h.apply(&KeyInput::End);
assert_eq!(h.fraction(), 0.75);
}
#[test]
fn mins_that_cannot_fit_split_proportionally() {
let mut s = SplitPane::horizontal(0.5).min_sizes(300.0, 300.0);
s.drag(&motion(10.0, 203.0, 200.0));
assert_eq!(s.fraction(), 0.5);
}
struct State {
split: SplitPane,
}
#[test]
fn dragging_the_divider_resizes_the_panes() {
let mut app = App::new(
State {
split: SplitPane::horizontal(0.5),
},
|s: &State, cx: &mut Cx<State>| {
let theme = *cx.theme();
let a = Element::boxed(BoxStyle::default()).height(10.0);
let b = Element::boxed(BoxStyle::default()).height(10.0);
super::super::row(vec![split_pane(
cx,
&theme,
&s.split,
|s: &mut State| &mut s.split,
a,
b,
)])
.align(Align::Start, Align::Stretch)
},
)
.logical_size(Size::new(400.0 + HANDLE, 100.0));
app.render_once();
let tree = app.accessibility_tree().unwrap();
let handle = tree
.descendants()
.into_iter()
.find(|n| n.role == Role::Splitter)
.unwrap()
.bounds;
assert_eq!(handle.min_x(), 200.0, "the divider sits at half");
let grab = handle.center();
assert!(app.drag_at_point(grab));
assert!(app.drag_at_point(Point::new(grab.x - 100.0, grab.y)));
app.end_drag();
assert_eq!(app.state().split.fraction(), 0.25);
app.render_once();
let tree = app.accessibility_tree().unwrap();
let handle = tree
.descendants()
.into_iter()
.find(|n| n.role == Role::Splitter)
.unwrap()
.bounds;
assert_eq!(handle.min_x(), 100.0);
}
}