use crate::event::{Point, Rect};
use crate::types::WidgetId;
use serde::{Deserialize, Serialize};
pub use crate::primitives::split::SplitDirection;
pub const MIN_RATIO: f32 = 0.1;
pub const MAX_RATIO: f32 = 0.9;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum SplitTree {
Leaf(WidgetId),
Split {
direction: SplitDirection,
ratio: f32,
first: Box<SplitTree>,
second: Box<SplitTree>,
},
}
impl SplitTree {
pub fn leaf(id: WidgetId) -> Self {
SplitTree::Leaf(id)
}
pub fn split(
direction: SplitDirection,
ratio: f32,
first: SplitTree,
second: SplitTree,
) -> Self {
SplitTree::Split {
direction,
ratio,
first: Box::new(first),
second: Box::new(second),
}
}
pub fn is_leaf(&self) -> bool {
matches!(self, SplitTree::Leaf(_))
}
pub fn leaf_ids(&self) -> Vec<WidgetId> {
let mut out = Vec::new();
self.collect_leaf_ids(&mut out);
out
}
fn collect_leaf_ids(&self, out: &mut Vec<WidgetId>) {
match self {
SplitTree::Leaf(id) => out.push(*id),
SplitTree::Split { first, second, .. } => {
first.collect_leaf_ids(out);
second.collect_leaf_ids(out);
}
}
}
pub fn leaf_count(&self) -> usize {
match self {
SplitTree::Leaf(_) => 1,
SplitTree::Split { first, second, .. } => first.leaf_count() + second.leaf_count(),
}
}
pub fn split_count(&self) -> usize {
match self {
SplitTree::Leaf(_) => 0,
SplitTree::Split { first, second, .. } => {
1 + first.split_count() + second.split_count()
}
}
}
pub fn layout(&self, bounds: Rect, measure: SplitTreeMeasure) -> SplitTreeLayout {
let mut leaves = Vec::new();
let mut dividers = Vec::new();
let mut counter = 0usize;
self.layout_into(bounds, measure, &mut counter, &mut leaves, &mut dividers);
SplitTreeLayout {
bounds,
leaves,
dividers,
}
}
fn layout_into(
&self,
bounds: Rect,
measure: SplitTreeMeasure,
counter: &mut usize,
leaves: &mut Vec<(WidgetId, Rect)>,
dividers: &mut Vec<SplitTreeDivider>,
) {
match self {
SplitTree::Leaf(id) => leaves.push((*id, bounds)),
SplitTree::Split {
direction,
ratio,
first,
second,
} => {
let idx = *counter;
*counter += 1;
let clamped = ratio.clamp(0.0, 1.0);
let thickness = measure.divider_thickness;
let (first_bounds, second_bounds, divider) = match direction {
SplitDirection::Horizontal => {
let available = (bounds.width - thickness).max(0.0);
let first_w = available * clamped;
let position = bounds.x + first_w;
let divider = SplitTreeDivider {
split_index: idx,
direction: *direction,
position,
axis_start: bounds.x,
axis_size: bounds.width,
cross_start: bounds.y,
cross_size: bounds.height,
thickness,
};
let first_bounds = Rect::new(bounds.x, bounds.y, first_w, bounds.height);
let second_bounds = Rect::new(
position + thickness,
bounds.y,
(bounds.width - first_w - thickness).max(0.0),
bounds.height,
);
(first_bounds, second_bounds, divider)
}
SplitDirection::Vertical => {
let available = (bounds.height - thickness).max(0.0);
let first_h = available * clamped;
let position = bounds.y + first_h;
let divider = SplitTreeDivider {
split_index: idx,
direction: *direction,
position,
axis_start: bounds.y,
axis_size: bounds.height,
cross_start: bounds.x,
cross_size: bounds.width,
thickness,
};
let first_bounds = Rect::new(bounds.x, bounds.y, bounds.width, first_h);
let second_bounds = Rect::new(
bounds.x,
position + thickness,
bounds.width,
(bounds.height - first_h - thickness).max(0.0),
);
(first_bounds, second_bounds, divider)
}
};
dividers.push(divider);
first.layout_into(first_bounds, measure, counter, leaves, dividers);
second.layout_into(second_bounds, measure, counter, leaves, dividers);
}
}
}
pub fn set_ratio_at_index(&mut self, split_index: usize, ratio: f32) -> bool {
self.set_ratio_at_index_impl(split_index, ratio, &mut 0)
}
fn set_ratio_at_index_impl(&mut self, target: usize, ratio: f32, counter: &mut usize) -> bool {
match self {
SplitTree::Leaf(_) => false,
SplitTree::Split {
ratio: r,
first,
second,
..
} => {
let idx = *counter;
*counter += 1;
if idx == target {
*r = ratio.clamp(MIN_RATIO, MAX_RATIO);
return true;
}
first.set_ratio_at_index_impl(target, ratio, counter)
|| second.set_ratio_at_index_impl(target, ratio, counter)
}
}
}
pub fn adjust_ratio_at_index(&mut self, split_index: usize, delta: f32) -> bool {
self.adjust_ratio_at_index_impl(split_index, delta, &mut 0)
}
fn adjust_ratio_at_index_impl(
&mut self,
target: usize,
delta: f32,
counter: &mut usize,
) -> bool {
match self {
SplitTree::Leaf(_) => false,
SplitTree::Split {
ratio,
first,
second,
..
} => {
let idx = *counter;
*counter += 1;
if idx == target {
*ratio = (*ratio + delta).clamp(MIN_RATIO, MAX_RATIO);
return true;
}
first.adjust_ratio_at_index_impl(target, delta, counter)
|| second.adjust_ratio_at_index_impl(target, delta, counter)
}
}
}
pub fn set_all_ratios(&mut self, ratio: f32) {
match self {
SplitTree::Leaf(_) => {}
SplitTree::Split {
ratio: r,
first,
second,
..
} => {
*r = ratio.clamp(MIN_RATIO, MAX_RATIO);
first.set_all_ratios(ratio);
second.set_all_ratios(ratio);
}
}
}
pub fn parent_split_of(&self, target: &WidgetId) -> Option<(usize, SplitDirection, bool)> {
self.parent_split_of_impl(target, &mut 0)
}
pub fn ratio_at_index(&self, split_index: usize) -> Option<f32> {
self.ratio_at_index_impl(split_index, &mut 0)
}
fn ratio_at_index_impl(&self, target: usize, counter: &mut usize) -> Option<f32> {
match self {
SplitTree::Leaf(_) => None,
SplitTree::Split {
ratio,
first,
second,
..
} => {
let idx = *counter;
*counter += 1;
if idx == target {
return Some(*ratio);
}
first
.ratio_at_index_impl(target, counter)
.or_else(|| second.ratio_at_index_impl(target, counter))
}
}
}
fn parent_split_of_impl(
&self,
target: &WidgetId,
counter: &mut usize,
) -> Option<(usize, SplitDirection, bool)> {
match self {
SplitTree::Leaf(_) => None,
SplitTree::Split {
direction,
first,
second,
..
} => {
let idx = *counter;
*counter += 1;
if let SplitTree::Leaf(id) = first.as_ref() {
if id == target {
return Some((idx, *direction, true));
}
}
if let SplitTree::Leaf(id) = second.as_ref() {
if id == target {
return Some((idx, *direction, false));
}
}
first
.parent_split_of_impl(target, counter)
.or_else(|| second.parent_split_of_impl(target, counter))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SplitTreeMeasure {
pub divider_thickness: f32,
}
impl SplitTreeMeasure {
pub fn new(divider_thickness: f32) -> Self {
Self { divider_thickness }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SplitTreeDivider {
pub split_index: usize,
pub direction: SplitDirection,
pub position: f32,
pub axis_start: f32,
pub axis_size: f32,
pub cross_start: f32,
pub cross_size: f32,
pub thickness: f32,
}
impl SplitTreeDivider {
pub fn cell_position(&self) -> u16 {
self.position.max(0.0) as u16
}
pub fn hit_tolerant(&self, axis_point: f32, tolerance: f32) -> bool {
axis_point >= self.position - tolerance
&& axis_point < self.position + self.thickness + tolerance
}
pub fn cross_contains(&self, cross_point: f32) -> bool {
cross_point >= self.cross_start && cross_point < self.cross_start + self.cross_size
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SplitTreeLayout {
pub bounds: Rect,
pub leaves: Vec<(WidgetId, Rect)>,
pub dividers: Vec<SplitTreeDivider>,
}
impl SplitTreeLayout {
pub fn hit_test_divider(&self, point: Point, tolerance: f32) -> Option<usize> {
self.dividers.iter().find_map(|d| {
let (axis, cross) = match d.direction {
SplitDirection::Horizontal => (point.x, point.y),
SplitDirection::Vertical => (point.y, point.x),
};
(d.hit_tolerant(axis, tolerance) && d.cross_contains(cross)).then_some(d.split_index)
})
}
pub fn hit_test_divider_cell(&self, axis_cell: u16, cross_cell: u16) -> Option<usize> {
self.dividers.iter().find_map(|d| {
let cross_ok = d.cross_contains(cross_cell as f32);
(d.cell_position() == axis_cell && cross_ok).then_some(d.split_index)
})
}
pub fn hit_test_leaf(&self, point: Point) -> Option<&WidgetId> {
self.leaves.iter().find_map(|(id, rect)| {
(point.x >= rect.x
&& point.x < rect.x + rect.width
&& point.y >= rect.y
&& point.y < rect.y + rect.height)
.then_some(id)
})
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(dead_code)]
pub(crate) mod native_surface_paint {
use super::{SplitDirection, SplitTreeLayout};
use crate::paint_surface::PaintSurface;
use crate::theme::Theme;
use crate::Rect;
pub(crate) fn paint(layout: &SplitTreeLayout, surface: &mut dyn PaintSurface, theme: &Theme) {
for div in &layout.dividers {
let rect = match div.direction {
SplitDirection::Horizontal => {
Rect::new(div.position, div.cross_start, div.thickness, div.cross_size)
}
SplitDirection::Vertical => {
Rect::new(div.cross_start, div.position, div.cross_size, div.thickness)
}
};
surface.surface_fill_rect(rect, theme.separator);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::ImagePaintResult;
use crate::event::{Rect as QRect, Viewport};
use crate::primitives::split_tree::{SplitTree, SplitTreeMeasure};
use crate::types::{Color, WidgetId};
use crate::Image;
#[derive(Default)]
struct RecordingSurface {
fills: Vec<(Rect, Color)>,
}
impl PaintSurface for RecordingSurface {
fn surface_begin_frame(&mut self, _viewport: Viewport) {}
fn surface_end_frame(&mut self) {}
fn surface_viewport(&self) -> Viewport {
Viewport::new(200.0, 100.0, 1.0)
}
fn surface_line_height(&self) -> f32 {
16.0
}
fn surface_char_width(&self) -> f32 {
8.0
}
fn surface_measure_text(&self, _text: &str) -> (f32, f32) {
(0.0, 0.0)
}
fn surface_fill_rect(&mut self, rect: Rect, color: Color) {
self.fills.push((rect, color));
}
fn surface_fill_rounded_rect(&mut self, rect: Rect, _radius: f32, color: Color) {
self.fills.push((rect, color));
}
fn surface_stroke_rect(&mut self, _rect: Rect, _color: Color, _stroke_width: f32) {}
fn surface_stroke_rounded_rect(
&mut self,
_rect: Rect,
_radius: f32,
_color: Color,
_stroke_width: f32,
) {
}
fn surface_draw_text_run(&mut self, _rect: Rect, _text: &str, _color: Color) {}
fn surface_draw_line(
&mut self,
_from: crate::Point,
_to: crate::Point,
_color: Color,
_stroke_width: f32,
) {
}
fn surface_push_clip(&mut self, _rect: Rect) {}
fn surface_pop_clip(&mut self) {}
fn surface_draw_image(&mut self, _rect: Rect, _image: &Image) -> ImagePaintResult {
ImagePaintResult::Unsupported
}
}
fn wid(s: &str) -> WidgetId {
WidgetId::new(s)
}
fn two_pane() -> SplitTree {
SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
)
}
fn nested() -> SplitTree {
SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::split(
SplitDirection::Vertical,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("c")),
),
SplitTree::leaf(wid("b")),
)
}
#[test]
fn one_fill_per_divider_in_theme_separator() {
let layout = nested().layout(
QRect::new(0.0, 0.0, 400.0, 300.0),
SplitTreeMeasure::new(4.0),
);
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&layout, &mut surface, &theme);
assert_eq!(surface.fills.len(), 2, "one fill per divider node");
for (_, color) in &surface.fills {
assert_eq!(
*color, theme.separator,
"dividers paint the opaque theme separator colour"
);
}
}
#[test]
fn horizontal_divider_rect_matches_geometry() {
let layout =
two_pane().layout(QRect::new(0.0, 0.0, 41.0, 10.0), SplitTreeMeasure::new(1.0));
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&layout, &mut surface, &theme);
let d = &layout.dividers[0];
assert_eq!(
surface.fills[0].0,
Rect::new(d.position, d.cross_start, d.thickness, d.cross_size)
);
}
#[test]
fn vertical_divider_rect_swaps_axes() {
let tree = SplitTree::split(
SplitDirection::Vertical,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
let layout = tree.layout(QRect::new(0.0, 0.0, 10.0, 21.0), SplitTreeMeasure::new(1.0));
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&layout, &mut surface, &theme);
let d = &layout.dividers[0];
assert_eq!(
surface.fills[0].0,
Rect::new(d.cross_start, d.position, d.cross_size, d.thickness)
);
}
#[test]
fn leaf_only_tree_paints_nothing() {
let layout = SplitTree::leaf(wid("a"))
.layout(QRect::new(0.0, 0.0, 10.0, 10.0), SplitTreeMeasure::new(1.0));
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&layout, &mut surface, &theme);
assert!(surface.fills.is_empty());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn wid(s: &str) -> WidgetId {
WidgetId::new(s)
}
fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect::new(x, y, w, h)
}
fn measure(t: f32) -> SplitTreeMeasure {
SplitTreeMeasure::new(t)
}
#[test]
fn single_leaf() {
let tree = SplitTree::leaf(wid("a"));
assert!(tree.is_leaf());
assert_eq!(tree.leaf_ids(), vec![wid("a")]);
assert_eq!(tree.leaf_count(), 1);
assert_eq!(tree.split_count(), 0);
}
#[test]
fn single_split() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
assert!(!tree.is_leaf());
assert_eq!(tree.leaf_ids(), vec![wid("a"), wid("b")]);
assert_eq!(tree.leaf_count(), 2);
assert_eq!(tree.split_count(), 1);
}
#[test]
fn nested_split_pre_order() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::split(
SplitDirection::Vertical,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("c")),
),
SplitTree::leaf(wid("b")),
);
assert_eq!(tree.leaf_ids(), vec![wid("a"), wid("c"), wid("b")]);
assert_eq!(tree.leaf_count(), 3);
assert_eq!(tree.split_count(), 2);
}
#[test]
fn layout_leaf_gets_full_bounds() {
let tree = SplitTree::leaf(wid("a"));
let layout = tree.layout(rect(0.0, 0.0, 100.0, 50.0), measure(1.0));
assert_eq!(layout.leaves, vec![(wid("a"), rect(0.0, 0.0, 100.0, 50.0))]);
assert!(layout.dividers.is_empty());
}
#[test]
fn layout_horizontal_split_side_by_side() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
let layout = tree.layout(rect(0.0, 0.0, 41.0, 10.0), measure(1.0));
assert_eq!(layout.leaves.len(), 2);
assert_eq!(layout.leaves[0], (wid("a"), rect(0.0, 0.0, 20.0, 10.0)));
assert_eq!(layout.leaves[1], (wid("b"), rect(21.0, 0.0, 20.0, 10.0)));
assert_eq!(layout.dividers.len(), 1);
let d = &layout.dividers[0];
assert_eq!(d.split_index, 0);
assert_eq!(d.direction, SplitDirection::Horizontal);
assert!((d.position - 20.0).abs() < 0.001);
assert!((d.axis_start - 0.0).abs() < 0.001);
assert!((d.axis_size - 41.0).abs() < 0.001);
assert!((d.cross_start - 0.0).abs() < 0.001);
assert!((d.cross_size - 10.0).abs() < 0.001);
}
#[test]
fn layout_vertical_split_stacked() {
let tree = SplitTree::split(
SplitDirection::Vertical,
0.25,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
let layout = tree.layout(rect(0.0, 0.0, 10.0, 21.0), measure(1.0));
assert_eq!(layout.leaves[0], (wid("a"), rect(0.0, 0.0, 10.0, 5.0)));
assert_eq!(layout.leaves[1], (wid("b"), rect(0.0, 6.0, 10.0, 15.0)));
let d = &layout.dividers[0];
assert_eq!(d.direction, SplitDirection::Vertical);
assert!((d.position - 5.0).abs() < 0.001);
}
#[test]
fn dividers_pre_order_nested() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::split(
SplitDirection::Vertical,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("c")),
),
SplitTree::leaf(wid("b")),
);
let layout = tree.layout(rect(0.0, 0.0, 800.0, 600.0), measure(0.0));
assert_eq!(layout.dividers.len(), 2);
assert_eq!(layout.dividers[0].split_index, 0);
assert_eq!(layout.dividers[0].direction, SplitDirection::Horizontal);
assert!((layout.dividers[0].position - 400.0).abs() < 0.001);
assert_eq!(layout.dividers[1].split_index, 1);
assert_eq!(layout.dividers[1].direction, SplitDirection::Vertical);
assert!((layout.dividers[1].position - 300.0).abs() < 0.001); }
#[test]
fn set_ratio_at_index_updates_layout_and_clamps() {
let mut tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
assert!(tree.set_ratio_at_index(0, 0.7));
let layout = tree.layout(rect(0.0, 0.0, 1000.0, 600.0), measure(0.0));
assert!((layout.leaves[0].1.width - 700.0).abs() < 0.001);
assert!((layout.leaves[1].1.width - 300.0).abs() < 0.001);
assert!(tree.set_ratio_at_index(0, 0.99));
let layout = tree.layout(rect(0.0, 0.0, 1000.0, 600.0), measure(0.0));
assert!((layout.leaves[0].1.width - 900.0).abs() < 0.001);
assert!(tree.set_ratio_at_index(0, 0.01));
let layout = tree.layout(rect(0.0, 0.0, 1000.0, 600.0), measure(0.0));
assert!((layout.leaves[0].1.width - 100.0).abs() < 0.001);
assert!(!tree.set_ratio_at_index(5, 0.5));
}
#[test]
fn ratio_at_index_reads_back_current_value() {
let mut tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::split(
SplitDirection::Vertical,
0.25,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("c")),
),
SplitTree::leaf(wid("b")),
);
assert_eq!(tree.ratio_at_index(0), Some(0.5));
assert_eq!(tree.ratio_at_index(1), Some(0.25));
assert_eq!(tree.ratio_at_index(2), None); tree.set_ratio_at_index(1, 0.6);
assert_eq!(tree.ratio_at_index(1), Some(0.6));
}
#[test]
fn adjust_ratio_at_index_deltas_and_clamps() {
let mut tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
assert!(tree.adjust_ratio_at_index(0, 0.2));
match &tree {
SplitTree::Split { ratio, .. } => assert!((ratio - 0.7).abs() < 0.001),
_ => panic!(),
}
assert!(tree.adjust_ratio_at_index(0, 0.5));
match &tree {
SplitTree::Split { ratio, .. } => assert!((ratio - MAX_RATIO).abs() < 0.001),
_ => panic!(),
}
}
#[test]
fn set_all_ratios_applies_to_every_split() {
let mut tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::split(
SplitDirection::Vertical,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("c")),
),
SplitTree::leaf(wid("b")),
);
tree.set_all_ratios(0.3);
let layout = tree.layout(rect(0.0, 0.0, 800.0, 600.0), measure(0.0));
assert!((layout.dividers[0].position - 240.0).abs() < 0.001); assert!((layout.dividers[1].position - 180.0).abs() < 0.001); }
#[test]
fn parent_split_of_finds_direct_children() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::split(
SplitDirection::Vertical,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("c")),
),
SplitTree::leaf(wid("b")),
);
let pa = tree.parent_split_of(&wid("a")).unwrap();
assert_eq!(pa.0, 1);
assert_eq!(pa.1, SplitDirection::Vertical);
assert!(pa.2);
let pc = tree.parent_split_of(&wid("c")).unwrap();
assert_eq!(pc.0, 1);
assert!(!pc.2);
let pb = tree.parent_split_of(&wid("b")).unwrap();
assert_eq!(pb.0, 0);
assert_eq!(pb.1, SplitDirection::Horizontal);
assert!(!pb.2);
assert!(tree.parent_split_of(&wid("nope")).is_none());
}
#[test]
fn cell_position_truncates_toward_zero() {
let d = SplitTreeDivider {
split_index: 0,
direction: SplitDirection::Horizontal,
position: 20.9,
axis_start: 0.0,
axis_size: 41.0,
cross_start: 0.0,
cross_size: 10.0,
thickness: 1.0,
};
assert_eq!(d.cell_position(), 20);
}
#[test]
fn hit_test_divider_tolerant_matches_gtk_convention() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
let layout = tree.layout(rect(0.0, 0.0, 100.0, 100.0), measure(4.0));
let d = &layout.dividers[0];
assert_eq!(
layout.hit_test_divider(
Point {
x: d.position + 1.0,
y: 50.0
},
3.0
),
Some(0)
);
assert_eq!(
layout.hit_test_divider(
Point {
x: d.position - 2.0,
y: 50.0
},
3.0
),
Some(0)
);
assert_eq!(
layout.hit_test_divider(
Point {
x: d.position - 20.0,
y: 50.0
},
3.0
),
None
);
assert_eq!(
layout.hit_test_divider(
Point {
x: d.position,
y: 500.0
},
3.0
),
None
);
}
#[test]
fn hit_test_divider_cell_requires_exact_cell_match() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
let layout = tree.layout(rect(0.0, 0.0, 41.0, 10.0), measure(1.0));
let d = &layout.dividers[0];
let cell = d.cell_position();
assert_eq!(layout.hit_test_divider_cell(cell, 5), Some(0));
assert_eq!(layout.hit_test_divider_cell(cell + 1, 5), None);
assert_eq!(
layout.hit_test_divider_cell(cell.saturating_sub(1), 5),
None
);
assert_eq!(layout.hit_test_divider_cell(cell, 50), None);
}
#[test]
fn hit_test_leaf_resolves_pane_and_ignores_divider_gap() {
let tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
SplitTree::leaf(wid("a")),
SplitTree::leaf(wid("b")),
);
let layout = tree.layout(rect(0.0, 0.0, 41.0, 10.0), measure(1.0));
assert_eq!(
layout.hit_test_leaf(Point { x: 5.0, y: 5.0 }),
Some(&wid("a"))
);
assert_eq!(
layout.hit_test_leaf(Point { x: 30.0, y: 5.0 }),
Some(&wid("b"))
);
assert_eq!(layout.hit_test_leaf(Point { x: 20.0, y: 5.0 }), None);
assert_eq!(layout.hit_test_leaf(Point { x: 500.0, y: 5.0 }), None);
}
#[test]
fn deep_nesting_leaf_and_divider_counts() {
let mut tree = SplitTree::leaf(wid("0"));
for i in 1..7 {
tree = SplitTree::split(
SplitDirection::Horizontal,
0.5,
tree,
SplitTree::leaf(wid(&i.to_string())),
);
}
assert_eq!(tree.leaf_count(), 7);
assert_eq!(tree.split_count(), 6);
let layout = tree.layout(rect(0.0, 0.0, 1400.0, 100.0), measure(0.0));
assert_eq!(layout.leaves.len(), 7);
assert_eq!(layout.dividers.len(), 6);
for (_, r) in &layout.leaves {
assert!(r.width > 0.0);
}
}
}