use serde::{Deserialize, Serialize};
use teksilo_canvas::Rect;
const EPS: f32 = 0.01;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DockSide {
Leading,
Trailing,
Top,
Bottom,
}
impl DockSide {
pub const ALL: [DockSide; 4] = [
DockSide::Leading,
DockSide::Trailing,
DockSide::Top,
DockSide::Bottom,
];
pub fn is_horizontal_axis(self) -> bool {
matches!(self, DockSide::Leading | DockSide::Trailing)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DockCorner {
TopLeading,
TopTrailing,
BottomLeading,
BottomTrailing,
}
impl DockCorner {
pub const ALL: [DockCorner; 4] = [
DockCorner::TopLeading,
DockCorner::TopTrailing,
DockCorner::BottomLeading,
DockCorner::BottomTrailing,
];
pub fn adjacent_sides(self) -> (DockSide, DockSide) {
match self {
DockCorner::TopLeading => (DockSide::Leading, DockSide::Top),
DockCorner::TopTrailing => (DockSide::Trailing, DockSide::Top),
DockCorner::BottomLeading => (DockSide::Leading, DockSide::Bottom),
DockCorner::BottomTrailing => (DockSide::Trailing, DockSide::Bottom),
}
}
fn other(self, side: DockSide) -> DockSide {
let (h, v) = self.adjacent_sides();
if side == h { v } else { h }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct CornerOwners {
pub top_leading: DockSide,
pub top_trailing: DockSide,
pub bottom_leading: DockSide,
pub bottom_trailing: DockSide,
}
impl Default for CornerOwners {
fn default() -> Self {
Self {
top_leading: DockSide::Top,
top_trailing: DockSide::Top,
bottom_leading: DockSide::Bottom,
bottom_trailing: DockSide::Bottom,
}
}
}
impl CornerOwners {
pub fn owner(&self, corner: DockCorner) -> DockSide {
match corner {
DockCorner::TopLeading => self.top_leading,
DockCorner::TopTrailing => self.top_trailing,
DockCorner::BottomLeading => self.bottom_leading,
DockCorner::BottomTrailing => self.bottom_trailing,
}
}
pub fn set(&mut self, corner: DockCorner, owner: DockSide) {
match corner {
DockCorner::TopLeading => self.top_leading = owner,
DockCorner::TopTrailing => self.top_trailing = owner,
DockCorner::BottomLeading => self.bottom_leading = owner,
DockCorner::BottomTrailing => self.bottom_trailing = owner,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SideLayout {
pub size: f32,
pub visible_progress: f32,
pub gutter: f32,
pub min_size: f32,
pub rail_thickness: f32,
pub has_rail: bool,
}
impl SideLayout {
pub fn empty() -> Self {
Self {
size: 0.0,
visible_progress: 0.0,
gutter: 0.0,
min_size: 0.0,
rail_thickness: 0.0,
has_rail: false,
}
}
fn rail_extent(&self) -> f32 {
if self.has_rail {
self.rail_thickness.max(0.0)
} else {
0.0
}
}
fn content_extent(&self) -> f32 {
(self.size * self.visible_progress.clamp(0.0, 1.0)).max(0.0)
}
fn gutter_extent(&self) -> f32 {
if self.content_extent() > EPS {
self.gutter.max(0.0)
} else {
0.0
}
}
fn total_extent(&self) -> f32 {
self.rail_extent() + self.content_extent() + self.gutter_extent()
}
fn band_depth(&self) -> f32 {
self.content_extent() + self.gutter_extent()
}
fn present(&self) -> bool {
self.total_extent() > EPS
}
fn band_present(&self) -> bool {
self.band_depth() > EPS
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SideRects {
pub rail: Rect,
pub content: Rect,
pub handle: Rect,
}
impl SideRects {
const ZERO: SideRects = SideRects {
rail: Rect::ZERO,
content: Rect::ZERO,
handle: Rect::ZERO,
};
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DockingRects {
pub leading: SideRects,
pub trailing: SideRects,
pub top: SideRects,
pub bottom: SideRects,
pub center: Rect,
}
fn effective_owner(
corner: DockCorner,
owners: &CornerOwners,
present: impl Fn(DockSide) -> bool,
) -> DockSide {
let declared = owners.owner(corner);
if present(declared) {
declared
} else {
let other = corner.other(declared);
if present(other) { other } else { declared }
}
}
pub fn compute_rects(
container: Rect,
leading: SideLayout,
trailing: SideLayout,
top: SideLayout,
bottom: SideLayout,
owners: CornerOwners,
rtl: bool,
) -> DockingRects {
let x = container.x;
let y = container.y;
let w = container.width.max(0.0);
let h = container.height.max(0.0);
let mut l = leading.total_extent();
let mut r = trailing.total_extent();
let mut t = top.band_depth();
let mut b = bottom.band_depth();
if l + r > w {
let scale = if l + r > 0.0 { w / (l + r) } else { 0.0 };
l *= scale;
r *= scale;
}
if t + b > h {
let scale = if t + b > 0.0 { h / (t + b) } else { 0.0 };
t *= scale;
b *= scale;
}
let present = |side: DockSide| match side {
DockSide::Leading => leading.present(),
DockSide::Trailing => trailing.present(),
DockSide::Top => top.band_present(),
DockSide::Bottom => bottom.band_present(),
};
let eff = |corner: DockCorner| effective_owner(corner, &owners, present);
let top_x_left = if eff(DockCorner::TopLeading) == DockSide::Top {
x
} else {
x + l
};
let top_x_right = if eff(DockCorner::TopTrailing) == DockSide::Top {
x + w
} else {
x + w - r
};
let bottom_x_left = if eff(DockCorner::BottomLeading) == DockSide::Bottom {
x
} else {
x + l
};
let bottom_x_right = if eff(DockCorner::BottomTrailing) == DockSide::Bottom {
x + w
} else {
x + w - r
};
let leading_y_top = if eff(DockCorner::TopLeading) == DockSide::Leading {
y
} else {
y + t
};
let leading_y_bottom = if eff(DockCorner::BottomLeading) == DockSide::Leading {
y + h
} else {
y + h - b
};
let trailing_y_top = if eff(DockCorner::TopTrailing) == DockSide::Trailing {
y
} else {
y + t
};
let trailing_y_bottom = if eff(DockCorner::BottomTrailing) == DockSide::Trailing {
y + h
} else {
y + h - b
};
let leading_region = Rect::new(
x,
leading_y_top,
l,
(leading_y_bottom - leading_y_top).max(0.0),
);
let trailing_region = Rect::new(
x + w - r,
trailing_y_top,
r,
(trailing_y_bottom - trailing_y_top).max(0.0),
);
let top_region = Rect::new(top_x_left, y, (top_x_right - top_x_left).max(0.0), t);
let bottom_region = Rect::new(
bottom_x_left,
y + h - b,
(bottom_x_right - bottom_x_left).max(0.0),
b,
);
let center = Rect::new(x + l, y + t, (w - l - r).max(0.0), (h - t - b).max(0.0));
DockingRects {
leading: split_side(DockSide::Leading, leading_region, &leading, l, rtl),
trailing: split_side(DockSide::Trailing, trailing_region, &trailing, r, rtl),
top: split_side(DockSide::Top, top_region, &top, t, rtl),
bottom: split_side(DockSide::Bottom, bottom_region, &bottom, b, rtl),
center,
}
}
fn split_side(
side: DockSide,
region: Rect,
layout: &SideLayout,
total: f32,
rtl: bool,
) -> SideRects {
if total <= EPS || region.width <= 0.0 || region.height <= 0.0 {
if layout.rail_extent() > EPS && region.width > 0.0 && region.height > 0.0 {
return rail_only(
side,
region,
layout.rail_extent().min(extent_along(side, region)),
rtl,
);
}
return SideRects::ZERO;
}
match side {
DockSide::Leading | DockSide::Trailing => {
let raw = layout.total_extent();
let scale = if raw > 0.0 { total / raw } else { 0.0 };
let rail = layout.rail_extent() * scale;
let content = layout.content_extent() * scale;
let gutter = layout.gutter_extent() * scale;
match side {
DockSide::Leading => SideRects {
rail: Rect::new(region.x, region.y, rail, region.height),
content: Rect::new(region.x + rail, region.y, content, region.height),
handle: Rect::new(region.x + rail + content, region.y, gutter, region.height),
},
_ => SideRects {
handle: Rect::new(region.x, region.y, gutter, region.height),
content: Rect::new(region.x + gutter, region.y, content, region.height),
rail: Rect::new(region.x + gutter + content, region.y, rail, region.height),
},
}
}
DockSide::Top | DockSide::Bottom => {
let rail_w = layout.rail_extent().min(region.width);
let body_w = (region.width - rail_w).max(0.0);
let (rail_x, body_x) = if rtl {
(region.x + body_w, region.x)
} else {
(region.x, region.x + rail_w)
};
let raw_depth = layout.content_extent() + layout.gutter_extent();
let scale = if raw_depth > 0.0 {
total / raw_depth
} else {
0.0
};
let content = layout.content_extent() * scale;
let gutter = layout.gutter_extent() * scale;
let rail = Rect::new(rail_x, region.y, rail_w, region.height);
match side {
DockSide::Top => SideRects {
rail,
content: Rect::new(body_x, region.y, body_w, content),
handle: Rect::new(body_x, region.y + content, body_w, gutter),
},
_ => SideRects {
rail,
handle: Rect::new(body_x, region.y, body_w, gutter),
content: Rect::new(body_x, region.y + gutter, body_w, content),
},
}
}
}
}
fn extent_along(side: DockSide, region: Rect) -> f32 {
if side.is_horizontal_axis() {
region.width
} else {
region.height
}
}
fn rail_only(side: DockSide, region: Rect, rail: f32, rtl: bool) -> SideRects {
let mut rects = SideRects::ZERO;
rects.rail = match side {
DockSide::Leading => Rect::new(region.x, region.y, rail, region.height),
DockSide::Trailing => Rect::new(
region.x + region.width - rail,
region.y,
rail,
region.height,
),
DockSide::Top | DockSide::Bottom => {
let rail_x = if rtl {
region.x + region.width - rail
} else {
region.x
};
Rect::new(rail_x, region.y, rail, region.height)
}
};
rects
}
#[cfg(test)]
mod tests {
use super::*;
fn container() -> Rect {
Rect::new(0.0, 0.0, 1000.0, 800.0)
}
fn shown(size: f32) -> SideLayout {
SideLayout {
size,
visible_progress: 1.0,
gutter: 6.0,
min_size: 50.0,
rail_thickness: 0.0,
has_rail: false,
}
}
fn hidden(size: f32) -> SideLayout {
SideLayout {
visible_progress: 0.0,
..shown(size)
}
}
#[test]
fn all_hidden_center_fills() {
let r = compute_rects(
container(),
SideLayout::empty(),
SideLayout::empty(),
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert_eq!(r.center, container());
}
#[test]
fn single_leading_side() {
let r = compute_rects(
container(),
shown(200.0),
SideLayout::empty(),
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert_eq!(r.leading.content.x, 0.0);
assert!((r.leading.content.width - 200.0).abs() < 0.01);
assert!(
(r.center.x - 206.0).abs() < 0.01,
"center after content+gutter"
);
assert!((r.center.width - (1000.0 - 206.0)).abs() < 0.01);
assert_eq!(r.trailing, SideRects::ZERO);
}
#[test]
fn four_sides_default_corners_inset_center() {
let r = compute_rects(
container(),
shown(200.0),
shown(150.0),
shown(100.0),
shown(120.0),
CornerOwners::default(),
false,
);
assert_eq!(r.top.content.x, 0.0);
assert!((r.top.content.width - 1000.0).abs() < 0.01);
assert_eq!(r.bottom.content.x, 0.0);
assert!((r.leading.content.y - 106.0).abs() < 0.01);
assert!((r.center.x - 206.0).abs() < 0.01);
assert!((r.center.y - 106.0).abs() < 0.01);
}
#[test]
fn corner_owned_by_leading_extends_column_up() {
let mut owners = CornerOwners::default();
owners.set(DockCorner::TopLeading, DockSide::Leading);
let r = compute_rects(
container(),
shown(200.0),
SideLayout::empty(),
shown(100.0),
SideLayout::empty(),
owners,
false,
);
assert_eq!(r.leading.content.y, 0.0);
assert!(
(r.top.content.x - 206.0).abs() < 0.01,
"top pushed right of leading"
);
}
#[test]
fn corner_degrades_when_owner_hidden() {
let mut owners = CornerOwners::default();
owners.set(DockCorner::TopLeading, DockSide::Leading);
let r = compute_rects(
container(),
hidden(200.0),
SideLayout::empty(),
shown(100.0),
SideLayout::empty(),
owners,
false,
);
assert_eq!(
r.top.content.x, 0.0,
"top fills since its corner-owner is gone"
);
}
#[test]
fn visible_progress_half_scales_content() {
let mut s = shown(200.0);
s.visible_progress = 0.5;
let r = compute_rects(
container(),
s,
SideLayout::empty(),
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert!((r.leading.content.width - 100.0).abs() < 0.01);
assert!(
r.leading.handle.width > 0.0,
"gutter present while expanding"
);
}
#[test]
fn hidden_side_with_rail_keeps_rail_drops_handle() {
let s = SideLayout {
size: 240.0,
visible_progress: 0.0,
gutter: 6.0,
min_size: 60.0,
rail_thickness: 48.0,
has_rail: true,
};
let r = compute_rects(
container(),
s,
SideLayout::empty(),
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert!((r.leading.rail.width - 48.0).abs() < 0.01, "rail persists");
assert!(r.leading.content.width.abs() < 0.01, "content hidden");
assert!(r.leading.handle.width.abs() < 0.01, "no handle when hidden");
assert!(
(r.center.x - 48.0).abs() < 0.01,
"center inset only by rail"
);
}
#[test]
fn shown_side_with_rail_orders_rail_content_handle() {
let s = SideLayout {
size: 200.0,
visible_progress: 1.0,
gutter: 6.0,
min_size: 60.0,
rail_thickness: 48.0,
has_rail: true,
};
let r = compute_rects(
container(),
s,
SideLayout::empty(),
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert_eq!(r.leading.rail.x, 0.0);
assert!((r.leading.rail.width - 48.0).abs() < 0.01);
assert!((r.leading.content.x - 48.0).abs() < 0.01);
assert!((r.leading.content.width - 200.0).abs() < 0.01);
assert!((r.leading.handle.x - 248.0).abs() < 0.01);
assert!((r.center.x - 254.0).abs() < 0.01);
}
fn band_with_rail(progress: f32) -> SideLayout {
SideLayout {
size: 100.0,
visible_progress: progress,
gutter: 6.0,
min_size: 80.0,
rail_thickness: 48.0,
has_rail: true,
}
}
#[test]
fn top_rail_is_a_leading_column_not_a_band() {
let r = compute_rects(
container(),
SideLayout::empty(),
SideLayout::empty(),
band_with_rail(1.0),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert_eq!(r.top.rail.x, 0.0);
assert!((r.top.rail.width - 48.0).abs() < 0.01);
assert!((r.top.rail.height - 106.0).abs() < 0.01);
assert!((r.top.content.x - 48.0).abs() < 0.01);
assert!((r.top.content.width - (1000.0 - 48.0)).abs() < 0.01);
assert!((r.top.content.height - 100.0).abs() < 0.01);
assert!((r.center.y - 106.0).abs() < 0.01);
}
#[test]
fn top_rail_column_mirrors_to_the_right_in_rtl() {
let r = compute_rects(
container(),
SideLayout::empty(),
SideLayout::empty(),
band_with_rail(1.0),
SideLayout::empty(),
CornerOwners::default(),
true,
);
assert!(
(r.top.rail.x - (1000.0 - 48.0)).abs() < 0.01,
"rail on the right in RTL"
);
assert_eq!(r.top.content.x, 0.0, "content on the left in RTL");
}
#[test]
fn hidden_top_with_rail_fully_collapses() {
let r = compute_rects(
container(),
SideLayout::empty(),
SideLayout::empty(),
band_with_rail(0.0),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert!(r.top.rail.height.abs() < 0.01, "no rail column when hidden");
assert!(r.top.content.height.abs() < 0.01, "no content when hidden");
assert_eq!(r.center.y, 0.0, "centre fills — no top inset");
assert!((r.center.height - 800.0).abs() < 0.01);
}
#[test]
fn bottom_rail_column_keeps_handle_inboard() {
let r = compute_rects(
container(),
SideLayout::empty(),
SideLayout::empty(),
SideLayout::empty(),
band_with_rail(1.0),
CornerOwners::default(),
false,
);
assert_eq!(r.bottom.rail.x, 0.0);
assert!((r.bottom.rail.width - 48.0).abs() < 0.01);
assert!((r.bottom.handle.x - 48.0).abs() < 0.01);
assert!((r.bottom.handle.y - (800.0 - 106.0)).abs() < 0.01);
assert!(
r.bottom.content.y > r.bottom.handle.y,
"content below the inboard handle"
);
}
#[test]
fn trailing_rail_sits_on_the_outer_edge() {
let s = SideLayout {
size: 200.0,
visible_progress: 1.0,
gutter: 6.0,
min_size: 60.0,
rail_thickness: 48.0,
has_rail: true,
};
let r = compute_rects(
container(),
SideLayout::empty(),
s,
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert!((r.trailing.rail.right() - 1000.0).abs() < 0.01);
assert!((r.trailing.rail.width - 48.0).abs() < 0.01);
assert!(r.trailing.handle.x < r.trailing.content.x);
assert!(r.trailing.content.x < r.trailing.rail.x);
}
#[test]
fn handle_spans_same_cross_extent_as_content() {
let r = compute_rects(
container(),
shown(200.0),
SideLayout::empty(),
shown(100.0),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert!((r.leading.handle.height - r.leading.content.height).abs() < 0.01);
assert!((r.leading.handle.y - r.leading.content.y).abs() < 0.01);
}
#[test]
fn center_never_negative_under_over_constraint() {
let big = shown(900.0);
let r = compute_rects(
container(),
big,
big,
big,
big,
CornerOwners::default(),
false,
);
assert!(r.center.width >= 0.0);
assert!(r.center.height >= 0.0);
assert!(r.leading.content.width + r.trailing.content.width <= 1000.0 + 0.01);
}
#[test]
fn zero_by_zero_container_no_panic() {
let r = compute_rects(
Rect::new(0.0, 0.0, 0.0, 0.0),
shown(200.0),
shown(200.0),
shown(100.0),
shown(100.0),
CornerOwners::default(),
false,
);
assert_eq!(r.center, Rect::new(0.0, 0.0, 0.0, 0.0));
}
#[test]
fn degenerate_corner_clamps_to_zero() {
let mut owners = CornerOwners::default();
owners.set(DockCorner::TopLeading, DockSide::Leading);
owners.set(DockCorner::BottomLeading, DockSide::Leading);
let r = compute_rects(
Rect::new(0.0, 0.0, 1000.0, 100.0),
shown(200.0),
SideLayout::empty(),
shown(80.0),
shown(80.0),
owners,
false,
);
assert!(r.leading.content.height >= 0.0);
}
#[test]
fn idempotent() {
let a = compute_rects(
container(),
shown(200.0),
shown(150.0),
shown(100.0),
shown(120.0),
CornerOwners::default(),
false,
);
let b = compute_rects(
container(),
shown(200.0),
shown(150.0),
shown(100.0),
shown(120.0),
CornerOwners::default(),
false,
);
assert_eq!(a, b);
}
#[test]
fn corner_other_side_helper() {
assert_eq!(
DockCorner::TopLeading.other(DockSide::Leading),
DockSide::Top
);
assert_eq!(
DockCorner::TopLeading.other(DockSide::Top),
DockSide::Leading
);
}
#[test]
fn rtl_mirror_is_caller_swap() {
let ltr = compute_rects(
container(),
shown(200.0),
shown(150.0),
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
let rtl = compute_rects(
container(),
shown(150.0),
shown(200.0),
SideLayout::empty(),
SideLayout::empty(),
CornerOwners::default(),
false,
);
assert!((ltr.leading.content.width - rtl.trailing.content.width).abs() < 0.01);
}
}