use crate::compat::{Box, HashMap, Vec};
use core::cell::RefCell;
use crate::core::{ObjectId, Rect};
use crate::layout::Layout;
pub const SPACER_ID: ObjectId = ObjectId::MAX;
thread_local! {
#[allow(clippy::missing_const_for_thread_local)]
static LAYOUT_MAP: RefCell<HashMap<ObjectId, Box<dyn Layout>>> =
RefCell::new(HashMap::new());
}
pub fn store_layout(parent_id: ObjectId, layout: Box<dyn Layout>) {
LAYOUT_MAP.with(|map| {
map.borrow_mut().insert(parent_id, layout);
});
}
pub fn forget_layout(parent_id: ObjectId) -> bool {
LAYOUT_MAP.with(|map| map.borrow_mut().remove(&parent_id).is_some())
}
pub fn has_layout(parent_id: ObjectId) -> bool {
LAYOUT_MAP.with(|map| map.borrow().contains_key(&parent_id))
}
pub fn layout_count() -> usize {
LAYOUT_MAP.with(|map| map.borrow().len())
}
pub fn add_widget_to_layout(child_id: ObjectId, stretch: u32, parent_id: ObjectId) -> bool {
LAYOUT_MAP.with(|map| {
let mut map = map.borrow_mut();
match map.get_mut(&parent_id) {
Some(layout) => {
layout.add_widget(child_id, stretch);
true
}
None => false,
}
})
}
pub fn remove_widget_from_layout(child_id: ObjectId, parent_id: ObjectId) -> bool {
LAYOUT_MAP.with(|map| {
let mut map = map.borrow_mut();
match map.get_mut(&parent_id) {
Some(layout) => {
layout.remove_widget(child_id);
true
}
None => false,
}
})
}
pub fn add_spacer_to_layout(stretch: u32, parent_id: ObjectId) -> bool {
add_widget_to_layout(SPACER_ID, stretch, parent_id)
}
pub fn apply_layout(parent_id: ObjectId, rect: Rect) -> Vec<(ObjectId, Rect)> {
let geometries = LAYOUT_MAP.with(|map| {
let map = map.borrow();
let Some(layout) = map.get(&parent_id) else {
return Vec::new();
};
let mut geometries = Vec::new();
layout.update(rect, &mut |child_id, child_rect| {
if child_id != SPACER_ID {
geometries.push((child_id, child_rect));
}
});
geometries
});
#[cfg(not(alloc_frugal))]
for (child_id, child_rect) in &geometries {
crate::set_widget_geometry(
*child_id,
child_rect.x,
child_rect.y,
child_rect.width,
child_rect.height,
);
}
geometries
}
pub fn preview_layout(parent_id: ObjectId, rect: Rect) -> Vec<(ObjectId, Rect)> {
LAYOUT_MAP.with(|map| {
let map = map.borrow();
let Some(layout) = map.get(&parent_id) else {
return Vec::new();
};
let mut geometries = Vec::new();
layout.update(rect, &mut |child_id, child_rect| {
if child_id != SPACER_ID {
geometries.push((child_id, child_rect));
}
});
geometries
})
}
pub fn layout_kind_name(kind: LayoutKindName) -> &'static str {
match kind {
LayoutKindName::HorizontalBox => "hbox",
LayoutKindName::VerticalBox => "vbox",
LayoutKindName::Grid => "grid",
LayoutKindName::Form => "form",
LayoutKindName::Stack => "stack",
LayoutKindName::HBox => "hbox",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutKindName {
HorizontalBox,
VerticalBox,
Grid,
Form,
Stack,
HBox,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::{BoxLayout, Orientation};
fn box_layout() -> Box<dyn Layout> {
Box::new(BoxLayout::new(Orientation::Horizontal, 0, 0))
}
#[test]
fn layouts_are_thread_local() {
const PARENT: ObjectId = 0x5EED;
store_layout(PARENT, box_layout());
assert!(has_layout(PARENT));
let seen = std::thread::spawn(move || has_layout(PARENT))
.join()
.expect("the probe thread must not panic");
assert!(!seen, "a layout stored on one thread must not be visible on another");
assert!(forget_layout(PARENT));
}
#[test]
fn storing_replaces_rather_than_accumulates() {
const PARENT: ObjectId = 0x5EEF;
let before = layout_count();
store_layout(PARENT, box_layout());
store_layout(PARENT, box_layout());
assert_eq!(layout_count(), before + 1, "one parent holds one layout");
assert!(forget_layout(PARENT));
assert_eq!(layout_count(), before);
}
#[test]
fn forgetting_an_unknown_parent_is_false() {
assert!(!forget_layout(0xDEAD_BEEF));
}
#[test]
fn adding_a_child_without_a_layout_reports_failure() {
assert!(!add_widget_to_layout(1, 0, 0xDEAD_BEEF));
assert!(!add_spacer_to_layout(1, 0xDEAD_BEEF));
}
#[test]
fn applying_a_layout_returns_child_geometries_and_skips_the_spacer() {
const PARENT: ObjectId = 0x5EF0;
store_layout(PARENT, box_layout());
assert!(add_widget_to_layout(101, 0, PARENT));
assert!(add_widget_to_layout(102, 0, PARENT));
assert!(add_spacer_to_layout(1, PARENT));
let geometries = preview_layout(PARENT, Rect::new(0, 0, 300, 100));
assert_eq!(geometries.len(), 2, "the spacer must not appear as a widget");
let ids: Vec<ObjectId> = geometries.iter().map(|(id, _)| *id).collect();
assert_eq!(ids, vec![101, 102]);
assert!(forget_layout(PARENT));
}
#[test]
fn a_horizontal_box_places_children_side_by_side() {
const PARENT: ObjectId = 0x5EF1;
store_layout(PARENT, box_layout());
add_widget_to_layout(201, 0, PARENT);
add_widget_to_layout(202, 0, PARENT);
let geometries = preview_layout(PARENT, Rect::new(0, 0, 300, 100));
let first = geometries.iter().find(|(id, _)| *id == 201).expect("first child").1;
let second = geometries.iter().find(|(id, _)| *id == 202).expect("second child").1;
assert!(
second.x > first.x,
"the second child must start after the first: {first:?} then {second:?}"
);
assert_eq!(first.y, second.y, "a horizontal box keeps one row");
assert!(forget_layout(PARENT));
}
#[test]
fn applying_an_unknown_parent_yields_nothing() {
assert!(apply_layout(0xDEAD_BEEF, Rect::new(0, 0, 10, 10)).is_empty());
assert!(preview_layout(0xDEAD_BEEF, Rect::new(0, 0, 10, 10)).is_empty());
}
#[test]
fn removing_a_child_reaches_the_layout() {
const PARENT: ObjectId = 0x5EF2;
store_layout(PARENT, box_layout());
add_widget_to_layout(301, 0, PARENT);
assert!(remove_widget_from_layout(301, PARENT));
let geometries = preview_layout(PARENT, Rect::new(0, 0, 100, 50));
assert!(
geometries.iter().all(|(id, _)| *id != 301),
"a removed child must not be laid out"
);
assert!(!remove_widget_from_layout(301, 0xDEAD_BEEF));
assert!(forget_layout(PARENT));
}
}