use crate::event::Rect;
use crate::types::WidgetId;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Split {
pub id: WidgetId,
pub direction: SplitDirection,
pub ratio: f32,
#[serde(default)]
pub first_min: f32,
#[serde(default)]
pub second_min: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SplitDirection {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SplitMeasure {
pub divider_thickness: f32,
}
impl SplitMeasure {
pub fn new(divider_thickness: f32) -> Self {
Self { divider_thickness }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SplitHit {
FirstPane(WidgetId),
SecondPane(WidgetId),
Divider(WidgetId),
Outside,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SplitLayout {
pub bounds: Rect,
pub first_bounds: Rect,
pub divider_bounds: Rect,
pub second_bounds: Rect,
pub hit_regions: Vec<(Rect, SplitHit)>,
pub resolved_ratio: f32,
}
impl SplitLayout {
pub fn hit_test(&self, x: f32, y: f32) -> SplitHit {
for (rect, hit) in &self.hit_regions {
if x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height {
return hit.clone();
}
}
SplitHit::Outside
}
}
impl Split {
pub fn layout(&self, bounds: Rect, measure: SplitMeasure) -> SplitLayout {
let (total, cross_start) = match self.direction {
SplitDirection::Horizontal => (bounds.width, bounds.x),
SplitDirection::Vertical => (bounds.height, bounds.y),
};
let available = (total - measure.divider_thickness).max(0.0);
let clamped = self.ratio.clamp(0.0, 1.0);
let first_size_raw = available * clamped;
let (first_size, second_size) = if self.first_min + self.second_min <= available {
let fs = first_size_raw.max(self.first_min);
let fs = fs.min(available - self.second_min);
(fs, available - fs)
} else if available > 0.0 {
let total_min = self.first_min + self.second_min;
let f = available * (self.first_min / total_min);
(f, available - f)
} else {
(0.0, 0.0)
};
let resolved_ratio = if available > 0.0 {
first_size / available
} else {
clamped
};
let (first_bounds, divider_bounds, second_bounds) = match self.direction {
SplitDirection::Horizontal => (
Rect::new(cross_start, bounds.y, first_size, bounds.height),
Rect::new(
cross_start + first_size,
bounds.y,
measure.divider_thickness,
bounds.height,
),
Rect::new(
cross_start + first_size + measure.divider_thickness,
bounds.y,
second_size,
bounds.height,
),
),
SplitDirection::Vertical => (
Rect::new(bounds.x, cross_start, bounds.width, first_size),
Rect::new(
bounds.x,
cross_start + first_size,
bounds.width,
measure.divider_thickness,
),
Rect::new(
bounds.x,
cross_start + first_size + measure.divider_thickness,
bounds.width,
second_size,
),
),
};
let hit_regions: Vec<(Rect, SplitHit)> = vec![
(divider_bounds, SplitHit::Divider(self.id)),
(first_bounds, SplitHit::FirstPane(self.id)),
(second_bounds, SplitHit::SecondPane(self.id)),
];
SplitLayout {
bounds,
first_bounds,
divider_bounds,
second_bounds,
hit_regions,
resolved_ratio,
}
}
}
#[cfg(any(
feature = "gtk",
feature = "win",
all(feature = "macos", target_os = "macos")
))]
#[allow(dead_code)]
pub(crate) mod native_surface_paint {
use super::SplitLayout;
use crate::paint_surface::PaintSurface;
use crate::theme::Theme;
pub(crate) fn paint(layout: &SplitLayout, surface: &mut dyn PaintSurface, theme: &Theme) {
surface.surface_fill_rect(layout.divider_bounds, theme.separator);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::ImagePaintResult;
use crate::event::{Rect as QRect, Viewport};
use crate::primitives::split::{Split, SplitDirection, SplitMeasure};
use crate::types::{Color, WidgetId};
use crate::Image;
#[derive(Default)]
struct RecordingSurface {
fills: Vec<(QRect, 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: QRect, color: Color) {
self.fills.push((rect, color));
}
fn surface_fill_rounded_rect(&mut self, rect: QRect, _radius: f32, color: Color) {
self.fills.push((rect, color));
}
fn surface_stroke_rect(&mut self, _rect: QRect, _color: Color, _stroke_width: f32) {}
fn surface_stroke_rounded_rect(
&mut self,
_rect: QRect,
_radius: f32,
_color: Color,
_stroke_width: f32,
) {
}
fn surface_draw_text_run(&mut self, _rect: QRect, _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: QRect) {}
fn surface_pop_clip(&mut self) {}
fn surface_draw_image(&mut self, _rect: QRect, _image: &Image) -> ImagePaintResult {
ImagePaintResult::Unsupported
}
}
fn two_pane(direction: SplitDirection) -> Split {
Split {
id: WidgetId::new("s"),
direction,
ratio: 0.5,
first_min: 0.0,
second_min: 0.0,
}
}
#[test]
fn paints_exactly_the_divider_rect_in_theme_separator() {
let split = two_pane(SplitDirection::Horizontal);
let layout = split.layout(QRect::new(0.0, 0.0, 200.0, 100.0), SplitMeasure::new(4.0));
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&layout, &mut surface, &theme);
assert_eq!(surface.fills.len(), 1, "split paints chrome only");
assert_eq!(surface.fills[0], (layout.divider_bounds, theme.separator));
}
#[test]
fn vertical_direction_paints_the_same_divider_bounds() {
let split = two_pane(SplitDirection::Vertical);
let layout = split.layout(QRect::new(0.0, 0.0, 100.0, 200.0), SplitMeasure::new(4.0));
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&layout, &mut surface, &theme);
assert_eq!(surface.fills.len(), 1);
assert_eq!(surface.fills[0].0, layout.divider_bounds);
}
#[test]
fn non_zero_origin_divider_bounds_are_painted_absolute() {
let split = two_pane(SplitDirection::Horizontal);
let layout = split.layout(QRect::new(7.0, 13.0, 200.0, 100.0), SplitMeasure::new(4.0));
let theme = Theme::default();
let mut surface = RecordingSurface::default();
paint(&layout, &mut surface, &theme);
assert_eq!(surface.fills[0].0, layout.divider_bounds);
assert!(surface.fills[0].0.x >= 7.0);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_layout_horizontal_even() {
let s = Split {
id: WidgetId::new("s"),
direction: SplitDirection::Horizontal,
ratio: 0.5,
first_min: 0.0,
second_min: 0.0,
};
let bounds = Rect::new(0.0, 0.0, 202.0, 100.0);
let layout = s.layout(bounds, SplitMeasure::new(2.0));
assert_eq!(layout.first_bounds.width, 100.0);
assert_eq!(layout.divider_bounds.x, 100.0);
assert_eq!(layout.divider_bounds.width, 2.0);
assert_eq!(layout.second_bounds.x, 102.0);
assert_eq!(layout.second_bounds.width, 100.0);
}
#[test]
fn split_layout_vertical_with_min() {
let s = Split {
id: WidgetId::new("s"),
direction: SplitDirection::Vertical,
ratio: 0.1, first_min: 30.0,
second_min: 20.0,
};
let bounds = Rect::new(0.0, 0.0, 100.0, 101.0);
let layout = s.layout(bounds, SplitMeasure::new(1.0));
assert_eq!(layout.first_bounds.height, 30.0);
assert_eq!(layout.second_bounds.height, 70.0);
assert_eq!(layout.divider_bounds.y, 30.0);
}
#[test]
fn split_hit_test_regions() {
let s = Split {
id: WidgetId::new("s"),
direction: SplitDirection::Horizontal,
ratio: 0.5,
first_min: 0.0,
second_min: 0.0,
};
let bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
let layout = s.layout(bounds, SplitMeasure::new(2.0));
match layout.hit_test(50.0, 50.0) {
SplitHit::FirstPane(id) => assert_eq!(id.as_str(), "s"),
_ => panic!(),
}
match layout.hit_test(99.5, 50.0) {
SplitHit::Divider(id) => assert_eq!(id.as_str(), "s"),
_ => panic!(),
}
match layout.hit_test(150.0, 50.0) {
SplitHit::SecondPane(id) => assert_eq!(id.as_str(), "s"),
_ => panic!(),
}
}
#[test]
fn split_layout_resolved_ratio() {
let s = Split {
id: WidgetId::new("s"),
direction: SplitDirection::Horizontal,
ratio: 0.0, first_min: 50.0,
second_min: 0.0,
};
let bounds = Rect::new(0.0, 0.0, 201.0, 100.0);
let layout = s.layout(bounds, SplitMeasure::new(1.0));
assert!((layout.resolved_ratio - 0.25).abs() < 0.001);
}
}