use super::{Layout, LayoutContext};
use crate::compat::{Any, Vec};
use crate::core::{ObjectId, Rect};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BorderRegion {
North,
South,
West,
East,
Center,
}
impl BorderRegion {
pub const fn token(self) -> &'static str {
match self {
Self::North => "north",
Self::South => "south",
Self::West => "west",
Self::East => "east",
Self::Center => "center",
}
}
pub fn from_token(token: &str) -> Option<Self> {
match token {
"north" | "North" => Some(Self::North),
"south" | "South" => Some(Self::South),
"west" | "West" => Some(Self::West),
"east" | "East" => Some(Self::East),
"center" | "Center" | "centre" | "Centre" => Some(Self::Center),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy)]
struct BorderItem {
widget_id: ObjectId,
extent: u32,
stretch: u32,
}
struct TrackAllocation {
lengths: Vec<u32>,
spacing: u32,
total: u32,
}
#[derive(Debug, Default)]
pub struct BorderLayout {
spacing: u32,
margin: u32,
north: Vec<BorderItem>,
south: Vec<BorderItem>,
west: Vec<BorderItem>,
east: Vec<BorderItem>,
center: Vec<BorderItem>,
}
impl BorderLayout {
pub fn new(spacing: u32, margin: u32) -> Self {
Self { spacing, margin, ..Self::default() }
}
pub fn spacing(&self) -> u32 {
self.spacing
}
pub fn set_spacing(&mut self, spacing: u32) {
self.spacing = spacing;
}
pub fn margin(&self) -> u32 {
self.margin
}
pub fn set_margin(&mut self, margin: u32) {
self.margin = margin;
}
pub fn add(&mut self, region: BorderRegion, widget_id: ObjectId, extent: u32) -> &mut Self {
self.add_with_stretch(region, widget_id, extent, 1)
}
pub fn add_with_stretch(
&mut self,
region: BorderRegion,
widget_id: ObjectId,
extent: u32,
stretch: u32,
) -> &mut Self {
self.take(widget_id);
self.region_mut(region).push(BorderItem { widget_id, extent, stretch: stretch.max(1) });
self
}
pub fn set_region(&mut self, widget_id: ObjectId, region: BorderRegion, extent: u32) -> bool {
let Some(existing) = self.take(widget_id) else {
return false;
};
self.region_mut(region).push(BorderItem { widget_id, extent, stretch: existing.stretch });
true
}
pub fn set_extent(&mut self, widget_id: ObjectId, extent: u32) -> bool {
for item in self
.north
.iter_mut()
.chain(self.south.iter_mut())
.chain(self.west.iter_mut())
.chain(self.east.iter_mut())
.chain(self.center.iter_mut())
{
if item.widget_id == widget_id {
item.extent = extent;
return true;
}
}
false
}
pub fn extent(&self, widget_id: ObjectId) -> Option<u32> {
self.item(widget_id).map(|item| item.extent)
}
pub fn region_of(&self, widget_id: ObjectId) -> Option<BorderRegion> {
[
(BorderRegion::North, &self.north),
(BorderRegion::South, &self.south),
(BorderRegion::West, &self.west),
(BorderRegion::East, &self.east),
(BorderRegion::Center, &self.center),
]
.into_iter()
.find(|(_, items)| items.iter().any(|item| item.widget_id == widget_id))
.map(|(region, _)| region)
}
pub fn track_count(&self, region: BorderRegion) -> usize {
self.region(region).len()
}
pub fn children(&self) -> Vec<ObjectId> {
self.north
.iter()
.chain(self.south.iter())
.chain(self.west.iter())
.chain(self.east.iter())
.chain(self.center.iter())
.map(|item| item.widget_id)
.collect()
}
fn region(&self, region: BorderRegion) -> &Vec<BorderItem> {
match region {
BorderRegion::North => &self.north,
BorderRegion::South => &self.south,
BorderRegion::West => &self.west,
BorderRegion::East => &self.east,
BorderRegion::Center => &self.center,
}
}
fn region_mut(&mut self, region: BorderRegion) -> &mut Vec<BorderItem> {
match region {
BorderRegion::North => &mut self.north,
BorderRegion::South => &mut self.south,
BorderRegion::West => &mut self.west,
BorderRegion::East => &mut self.east,
BorderRegion::Center => &mut self.center,
}
}
fn item(&self, widget_id: ObjectId) -> Option<&BorderItem> {
self.north
.iter()
.chain(self.south.iter())
.chain(self.west.iter())
.chain(self.east.iter())
.chain(self.center.iter())
.find(|item| item.widget_id == widget_id)
}
fn take(&mut self, widget_id: ObjectId) -> Option<BorderItem> {
for region in [
BorderRegion::North,
BorderRegion::South,
BorderRegion::West,
BorderRegion::East,
BorderRegion::Center,
] {
let items = self.region_mut(region);
if let Some(index) = items.iter().position(|item| item.widget_id == widget_id) {
return Some(items.remove(index));
}
}
None
}
fn arrange(
&self,
rect: Rect,
spacing: u32,
margin: u32,
scale: f32,
widgets: &mut dyn FnMut(ObjectId, Rect),
) {
if rect.width == 0 || rect.height == 0 {
return;
}
let margin = margin.min((rect.width / 2).min(rect.height / 2));
let inner = Rect::new(
rect.x + margin as i32,
rect.y + margin as i32,
rect.width.saturating_sub(margin * 2),
rect.height.saturating_sub(margin * 2),
);
let north = allocate_tracks(&self.north, inner.height, spacing, scale);
let south =
allocate_tracks(&self.south, inner.height.saturating_sub(north.total), spacing, scale);
let west = allocate_tracks(&self.west, inner.width, spacing, scale);
let east =
allocate_tracks(&self.east, inner.width.saturating_sub(west.total), spacing, scale);
let middle_top = inner.y.saturating_add(north.total as i32);
let middle_bottom = inner
.y
.saturating_add(inner.height as i32)
.saturating_sub(south.total as i32)
.max(middle_top);
let middle_height = (middle_bottom - middle_top).max(0) as u32;
place_vertical(&self.north, &north, inner.x, inner.y, inner.width, widgets);
place_vertical(
&self.south,
&south,
inner.x,
inner.y + inner.height.saturating_sub(south.total) as i32,
inner.width,
widgets,
);
place_horizontal(&self.west, &west, inner.x, middle_top, middle_height, widgets);
place_horizontal(
&self.east,
&east,
inner.x + inner.width.saturating_sub(east.total) as i32,
middle_top,
middle_height,
widgets,
);
let center_x = inner.x.saturating_add(west.total as i32);
let center_right = inner
.x
.saturating_add(inner.width as i32)
.saturating_sub(east.total as i32)
.max(center_x);
let center_width = (center_right - center_x).max(0) as u32;
place_center(
&self.center,
Rect::new(center_x, middle_top, center_width, middle_height),
spacing,
widgets,
);
}
}
fn allocate_tracks(
items: &[BorderItem],
available: u32,
spacing: u32,
scale: f32,
) -> TrackAllocation {
if items.is_empty() {
return TrackAllocation { lengths: Vec::new(), spacing: 0, total: 0 };
}
let gaps = items.len().saturating_sub(1) as u32;
let spacing = spacing.min(available.checked_div(gaps).unwrap_or(0));
let budget = available.saturating_sub(gaps * spacing);
let mut remaining = budget;
let mut lengths = Vec::with_capacity(items.len());
for item in items {
let extent = scale_extent(item.extent, scale).min(remaining);
remaining -= extent;
lengths.push(extent);
}
let total = lengths.iter().copied().sum::<u32>().saturating_add(gaps * spacing);
TrackAllocation { lengths, spacing, total }
}
fn scale_extent(extent: u32, scale: f32) -> u32 {
(extent as f32 * scale).round().max(0.0) as u32
}
fn place_vertical(
items: &[BorderItem],
allocation: &TrackAllocation,
x: i32,
y: i32,
width: u32,
widgets: &mut dyn FnMut(ObjectId, Rect),
) {
let mut cursor = y;
for (item, length) in items.iter().zip(allocation.lengths.iter().copied()) {
widgets(item.widget_id, Rect::new(x, cursor, width, length));
cursor = cursor.saturating_add(length as i32 + allocation.spacing as i32);
}
}
fn place_horizontal(
items: &[BorderItem],
allocation: &TrackAllocation,
x: i32,
y: i32,
height: u32,
widgets: &mut dyn FnMut(ObjectId, Rect),
) {
let mut cursor = x;
for (item, length) in items.iter().zip(allocation.lengths.iter().copied()) {
widgets(item.widget_id, Rect::new(cursor, y, length, height));
cursor = cursor.saturating_add(length as i32 + allocation.spacing as i32);
}
}
fn place_center(
items: &[BorderItem],
rect: Rect,
spacing: u32,
widgets: &mut dyn FnMut(ObjectId, Rect),
) {
if items.is_empty() || rect.width == 0 || rect.height == 0 {
return;
}
let gaps = items.len().saturating_sub(1) as u32;
let spacing = spacing.min(rect.width.checked_div(gaps).unwrap_or(0));
let available = rect.width.saturating_sub(gaps * spacing);
let total_stretch = items.iter().map(|item| u64::from(item.stretch)).sum::<u64>().max(1);
let mut widths = Vec::with_capacity(items.len());
let mut assigned = 0u32;
for item in items {
let width = (u64::from(available) * u64::from(item.stretch) / total_stretch) as u32;
assigned = assigned.saturating_add(width);
widths.push(width);
}
if let Some(first) = widths.first_mut() {
*first = first.saturating_add(available.saturating_sub(assigned));
}
let mut cursor = rect.x;
for (item, width) in items.iter().zip(widths.iter().copied()) {
widgets(item.widget_id, Rect::new(cursor, rect.y, width, rect.height));
cursor = cursor.saturating_add(width as i32 + spacing as i32);
}
}
impl Layout for BorderLayout {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn add_widget(&mut self, widget_id: ObjectId, stretch: u32) {
self.add_with_stretch(BorderRegion::Center, widget_id, 0, stretch);
}
fn remove_widget(&mut self, widget_id: ObjectId) {
self.take(widget_id);
}
fn child_ids(&self) -> Vec<ObjectId> {
self.children()
}
fn has_child(&self, id: ObjectId) -> bool {
self.item(id).is_some()
}
fn clear(&mut self) {
self.north.clear();
self.south.clear();
self.west.clear();
self.east.clear();
self.center.clear();
}
fn update(&self, rect: Rect, widgets: &mut dyn FnMut(ObjectId, Rect)) {
self.arrange(rect, self.spacing, self.margin, 1.0, widgets);
}
fn update_with_context(
&self,
rect: Rect,
context: &LayoutContext,
widgets: &mut dyn FnMut(ObjectId, Rect),
) {
let scale = context.layout_scale.max(context.font_scale);
let spacing = (self.spacing as f32 * scale).round() as u32;
let margin = (self.margin as f32 * scale).round() as u32;
self.arrange(rect, spacing, margin, scale, widgets);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::HashMap;
fn rects(layout: &BorderLayout, rect: Rect) -> HashMap<ObjectId, Rect> {
let mut out = HashMap::new();
layout.update(rect, &mut |id, geometry| {
out.insert(id, geometry);
});
out
}
#[test]
fn edge_tracks_are_fixed_and_the_center_takes_the_rest() {
let mut layout = BorderLayout::new(0, 0);
layout.add(BorderRegion::North, 1, 40);
layout.add(BorderRegion::South, 2, 30);
layout.add(BorderRegion::West, 3, 100);
layout.add(BorderRegion::East, 4, 50);
layout.add(BorderRegion::Center, 5, 0);
let out = rects(&layout, Rect::new(0, 0, 400, 300));
assert_eq!(out.get(&1), Some(&Rect::new(0, 0, 400, 40)), "north");
assert_eq!(out.get(&2), Some(&Rect::new(0, 270, 400, 30)), "south");
assert_eq!(out.get(&3), Some(&Rect::new(0, 40, 100, 230)), "west");
assert_eq!(out.get(&4), Some(&Rect::new(350, 40, 50, 230)), "east");
assert_eq!(out.get(&5), Some(&Rect::new(100, 40, 250, 230)), "center");
}
#[test]
fn several_tracks_in_one_region_stack_with_spacing() {
let mut layout = BorderLayout::new(5, 0);
layout.add(BorderRegion::North, 1, 20);
layout.add(BorderRegion::North, 2, 30);
layout.add(BorderRegion::Center, 3, 0);
let out = rects(&layout, Rect::new(0, 0, 100, 200));
assert_eq!(out.get(&1), Some(&Rect::new(0, 0, 100, 20)));
assert_eq!(out.get(&2), Some(&Rect::new(0, 25, 100, 30)));
assert_eq!(out.get(&3), Some(&Rect::new(0, 55, 100, 145)));
assert_eq!(layout.track_count(BorderRegion::North), 2);
}
#[test]
fn an_over_subscribed_edge_stays_inside_the_parent() {
let mut layout = BorderLayout::new(10, 4);
layout.add(BorderRegion::North, 1, 90);
layout.add(BorderRegion::North, 2, 90);
layout.add(BorderRegion::South, 3, 90);
layout.add(BorderRegion::West, 4, 90);
layout.add(BorderRegion::East, 5, 90);
layout.add(BorderRegion::Center, 6, 0);
let parent = Rect::new(0, 0, 20, 16);
let out = rects(&layout, parent);
for (id, rect) in &out {
assert!(
rect.x >= parent.x
&& rect.y >= parent.y
&& rect.x + rect.width as i32 <= parent.x + parent.width as i32
&& rect.y + rect.height as i32 <= parent.y + parent.height as i32,
"track {id} escaped {parent:?}: {rect:?}"
);
}
}
#[test]
fn context_scaling_grows_extents_spacing_and_margin() {
let mut layout = BorderLayout::new(4, 2);
layout.add(BorderRegion::North, 1, 20);
layout.add(BorderRegion::Center, 2, 0);
let context =
LayoutContext { layout_scale: 2.0, font_scale: 2.0, ..LayoutContext::default() };
let mut out = HashMap::new();
layout.update_with_context(Rect::new(0, 0, 200, 100), &context, &mut |id, geometry| {
out.insert(id, geometry);
});
assert_eq!(out.get(&1), Some(&Rect::new(4, 4, 192, 40)));
assert_eq!(out.get(&2), Some(&Rect::new(4, 44, 192, 52)));
}
#[test]
fn moving_a_widget_to_a_region_does_not_duplicate_it() {
let mut layout = BorderLayout::new(0, 0);
layout.add_widget(7, 1);
assert_eq!(layout.region_of(7), Some(BorderRegion::Center));
assert!(layout.set_region(7, BorderRegion::North, 36));
assert_eq!(layout.region_of(7), Some(BorderRegion::North));
assert_eq!(layout.track_count(BorderRegion::Center), 0);
assert_eq!(layout.child_ids().as_slice(), &[7]);
}
#[test]
fn center_tracks_share_the_remaining_width_by_stretch() {
let mut layout = BorderLayout::new(0, 0);
layout.add(BorderRegion::West, 1, 40);
layout.add_with_stretch(BorderRegion::Center, 2, 0, 1);
layout.add_with_stretch(BorderRegion::Center, 3, 0, 3);
let out = rects(&layout, Rect::new(0, 0, 360, 100));
assert_eq!(out.get(&2), Some(&Rect::new(40, 0, 80, 100)));
assert_eq!(out.get(&3), Some(&Rect::new(120, 0, 240, 100)));
}
#[test]
fn removing_a_widget_removes_it_from_every_region() {
let mut layout = BorderLayout::new(0, 0);
layout.add(BorderRegion::South, 9, 20);
assert!(layout.has_child(9));
layout.remove_widget(9);
assert!(!layout.has_child(9));
assert!(rects(&layout, Rect::new(0, 0, 10, 10)).is_empty());
}
}