use crate::core::element::{Element, ElementKind};
use crate::layout::axis::Axis;
use crate::style::Rect;
use crate::widgets::containers::layout::{frame_join_enabled, measure_stack};
use super::{Splitter, SplitterHandleMode, sizes_from_weights};
fn rides_border(splitter: &Splitter) -> bool {
matches!(splitter.handle_mode, SplitterHandleMode::Border)
}
fn handle_gap_size(splitter: &Splitter) -> u16 {
if rides_border(splitter) {
0
} else {
splitter.handle_size.max(1)
}
}
fn border_on_seam_edge(el: &Element, axis: Axis, leading: bool) -> bool {
let ElementKind::Frame(frame) = &el.kind else {
return false;
};
if !frame.props.has_border() {
return false;
}
let edges = frame.props.border_edges;
match (axis, leading) {
(Axis::Horizontal, true) => edges.has_left(),
(Axis::Horizontal, false) => edges.has_right(),
(Axis::Vertical, true) => edges.has_top(),
(Axis::Vertical, false) => edges.has_bottom(),
}
}
fn seam_geometry(left: &Element, right: &Element, axis: Axis) -> (u16, u16) {
if frame_join_enabled(left) && frame_join_enabled(right) {
return (1, 1);
}
let left_border = border_on_seam_edge(left, axis, false);
let right_border = border_on_seam_edge(right, axis, true);
match (left_border, right_border) {
(true, true) => (1, 2),
(true, false) => (1, 1),
(false, true) => (0, 1),
(false, false) => (1, 1),
}
}
pub(crate) struct SplitterLayout {
pub pane_rects: Vec<Rect>,
pub handle_rects: Vec<Rect>,
pub pane_sizes: Vec<u16>,
}
pub(crate) fn axis_for_splitter(splitter: &Splitter) -> Axis {
match splitter.orientation {
crate::widgets::Orientation::Horizontal => Axis::Vertical,
crate::widgets::Orientation::Vertical => Axis::Horizontal,
}
}
pub(crate) fn measure_splitter(
splitter: &Splitter,
max_w: Option<u16>,
max_h: Option<u16>,
) -> (u16, u16) {
let axis = axis_for_splitter(splitter);
let (main_max, cross_max) = match axis {
Axis::Horizontal => (max_w, max_h),
Axis::Vertical => (max_h, max_w),
};
let (mut w, mut h) = measure_stack(
&crate::widgets::internal::StackProps::default(),
&splitter.children,
axis,
main_max,
cross_max,
);
let handle_count = splitter.children.len().saturating_sub(1) as u16;
let handle_total = handle_gap_size(splitter).saturating_mul(handle_count);
match axis {
Axis::Horizontal => {
w = w.saturating_add(handle_total);
}
Axis::Vertical => {
h = h.saturating_add(handle_total);
}
}
(w, h)
}
pub(crate) fn layout_splitter(
splitter: &Splitter,
weights: &[f32],
bounds: Rect,
) -> SplitterLayout {
let axis = axis_for_splitter(splitter);
let handle_count = splitter.children.len().saturating_sub(1) as u16;
let handle_total = handle_gap_size(splitter).saturating_mul(handle_count);
let available_main = match axis {
Axis::Horizontal => bounds.w,
Axis::Vertical => bounds.h,
};
let available_main = available_main.saturating_sub(handle_total);
let pane_sizes = sizes_from_weights(weights, available_main, splitter.min_size);
let mut pane_rects = Vec::with_capacity(splitter.children.len());
let mut handle_rects = Vec::with_capacity(splitter.children.len().saturating_sub(1));
let mut cursor = match axis {
Axis::Horizontal => bounds.x,
Axis::Vertical => bounds.y,
};
for (idx, size) in pane_sizes.iter().enumerate() {
let rect = match axis {
Axis::Horizontal => Rect {
x: cursor,
y: bounds.y,
w: *size,
h: bounds.h,
},
Axis::Vertical => Rect {
x: bounds.x,
y: cursor,
w: bounds.w,
h: *size,
},
};
pane_rects.push(rect);
cursor = cursor.saturating_add(*size as i16);
if idx + 1 < pane_sizes.len() {
let handle_rect = if rides_border(splitter) {
let (offset, thickness) =
match (splitter.children.get(idx), splitter.children.get(idx + 1)) {
(Some(left), Some(right)) => seam_geometry(left, right, axis),
_ => (1, 1),
};
match axis {
Axis::Horizontal => {
let seam_x = cursor.saturating_sub(offset as i16).max(bounds.x);
let end = (bounds.x as i32 + bounds.w as i32)
.min(seam_x as i32 + thickness as i32);
Rect {
x: seam_x,
y: bounds.y,
w: (end - seam_x as i32).max(0) as u16,
h: bounds.h,
}
}
Axis::Vertical => {
let seam_y = cursor.saturating_sub(offset as i16).max(bounds.y);
let end = (bounds.y as i32 + bounds.h as i32)
.min(seam_y as i32 + thickness as i32);
Rect {
x: bounds.x,
y: seam_y,
w: bounds.w,
h: (end - seam_y as i32).max(0) as u16,
}
}
}
} else {
match axis {
Axis::Horizontal => Rect {
x: cursor,
y: bounds.y,
w: splitter.handle_size,
h: bounds.h,
},
Axis::Vertical => Rect {
x: bounds.x,
y: cursor,
w: bounds.w,
h: splitter.handle_size,
},
}
};
handle_rects.push(handle_rect);
cursor = cursor.saturating_add(handle_gap_size(splitter) as i16);
}
}
SplitterLayout {
pane_rects,
handle_rects,
pane_sizes,
}
}
#[cfg(test)]
mod tests {
use super::{layout_splitter, measure_splitter};
use crate::style::Rect;
use crate::widgets::{Frame, Spacer, Splitter, SplitterHandleMode};
fn base_vertical() -> Splitter {
Splitter::vertical()
.weights(vec![0.5, 0.5])
.handle_size(1)
.child(Spacer::new())
.child(Spacer::new())
}
fn base_horizontal() -> Splitter {
Splitter::horizontal()
.weights(vec![0.5, 0.5])
.handle_size(1)
.child(Spacer::new())
.child(Spacer::new())
}
#[test]
fn border_mode_vertical_removes_gutter_and_places_seam_handle() {
let bounds = Rect {
x: 0,
y: 0,
w: 20,
h: 5,
};
let regular = base_vertical();
let joined = base_vertical().handle_mode(SplitterHandleMode::Border);
let regular_layout = layout_splitter(®ular, &[0.5, 0.5], bounds);
let joined_layout = layout_splitter(&joined, &[0.5, 0.5], bounds);
assert_eq!(regular_layout.pane_rects[1].x, 11);
assert_eq!(regular_layout.handle_rects[0].x, 10);
assert_eq!(joined_layout.pane_rects[1].x, 10);
assert_eq!(joined_layout.handle_rects[0].x, 9);
assert_eq!(joined_layout.handle_rects[0].w, 1);
}
#[test]
fn border_mode_horizontal_removes_gutter_and_places_seam_handle() {
let bounds = Rect {
x: 0,
y: 0,
w: 10,
h: 20,
};
let regular = base_horizontal();
let joined = base_horizontal().handle_mode(SplitterHandleMode::Border);
let regular_layout = layout_splitter(®ular, &[0.5, 0.5], bounds);
let joined_layout = layout_splitter(&joined, &[0.5, 0.5], bounds);
assert_eq!(regular_layout.pane_rects[1].y, 11);
assert_eq!(regular_layout.handle_rects[0].y, 10);
assert_eq!(joined_layout.pane_rects[1].y, 10);
assert_eq!(joined_layout.handle_rects[0].y, 9);
assert_eq!(joined_layout.handle_rects[0].h, 1);
}
#[test]
fn border_mode_measure_excludes_gutter() {
let regular = base_vertical();
let joined = base_vertical().handle_mode(SplitterHandleMode::Border);
let (regular_w, regular_h) = measure_splitter(®ular, None, None);
let (joined_w, joined_h) = measure_splitter(&joined, None, None);
assert_eq!(regular_h, joined_h);
assert_eq!(regular_w.saturating_sub(joined_w), 1);
}
#[test]
fn separate_bordered_panes_grab_both_walls() {
let bounds = Rect {
x: 0,
y: 0,
w: 20,
h: 5,
};
let splitter = Splitter::vertical()
.weights(vec![0.5, 0.5])
.handle_mode(SplitterHandleMode::Border)
.child(Frame::new().border(true))
.child(Frame::new().border(true));
let layout = layout_splitter(&splitter, &[0.5, 0.5], bounds);
assert_eq!(layout.pane_rects[1].x, 10);
assert_eq!(layout.handle_rects[0].x, 9);
assert_eq!(layout.handle_rects[0].w, 2);
}
#[test]
fn collapsed_pane_seam_handle_stays_within_bounds() {
let bounds = Rect {
x: 0,
y: 0,
w: 20,
h: 5,
};
let splitter = Splitter::vertical()
.min_size(0)
.handle_mode(SplitterHandleMode::Border)
.child(Frame::new().border(true))
.child(Frame::new().border(true));
let layout = layout_splitter(&splitter, &[1.0, 0.0], bounds);
assert_eq!(layout.pane_sizes, vec![20, 0]);
let handle = layout.handle_rects[0];
assert_eq!(handle.x, 19);
assert_eq!(handle.w, 1);
assert!(handle.x.saturating_add(handle.w as i16) <= bounds.x + bounds.w as i16);
}
#[test]
fn merged_bordered_panes_share_one_wall() {
let bounds = Rect {
x: 0,
y: 0,
w: 20,
h: 5,
};
let splitter = Splitter::vertical()
.weights(vec![0.5, 0.5])
.handle_mode(SplitterHandleMode::Border)
.child(Frame::new().border(true).join_frame(true))
.child(Frame::new().border(true).join_frame(true));
let layout = layout_splitter(&splitter, &[0.5, 0.5], bounds);
assert_eq!(layout.pane_rects[1].x, 10);
assert_eq!(layout.handle_rects[0].x, 9);
assert_eq!(layout.handle_rects[0].w, 1);
}
}