use crate::compat::{Box, HashMap, Vec};
use core::cell::RefCell;
use crate::core::{ObjectId, Rect};
use crate::layout::{ChildInfo, 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());
#[allow(clippy::missing_const_for_thread_local)]
static DIRTY_LAYOUTS: RefCell<Vec<ObjectId>> = const { RefCell::new(Vec::new()) };
}
pub fn invalidate_layout(parent_id: ObjectId) -> bool {
if !has_layout(parent_id) {
return false;
}
DIRTY_LAYOUTS.with(|dirty| {
let mut dirty = dirty.borrow_mut();
if !dirty.contains(&parent_id) {
dirty.push(parent_id);
}
});
true
}
pub fn is_layout_dirty(parent_id: ObjectId) -> bool {
DIRTY_LAYOUTS.with(|dirty| dirty.borrow().contains(&parent_id))
}
pub fn apply_layout_if_dirty(parent_id: ObjectId, rect: Rect) -> Option<Vec<(ObjectId, Rect)>> {
if !is_layout_dirty(parent_id) {
return None;
}
DIRTY_LAYOUTS.with(|dirty| dirty.borrow_mut().retain(|id| *id != parent_id));
Some(apply_layout(parent_id, rect))
}
#[derive(Default)]
pub struct SharedLayoutRegistry {
layouts: std::sync::Mutex<HashMap<ObjectId, Box<dyn Layout + Send>>>,
dirty: std::sync::Mutex<Vec<ObjectId>>,
}
impl SharedLayoutRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn store(&self, parent_id: ObjectId, layout: Box<dyn Layout + Send>) {
if let Ok(mut layouts) = self.layouts.lock() {
layouts.insert(parent_id, layout);
}
if let Ok(mut dirty) = self.dirty.lock() {
if !dirty.contains(&parent_id) {
dirty.push(parent_id);
}
}
}
pub fn forget(&self, parent_id: ObjectId) -> bool {
let removed = self
.layouts
.lock()
.map(|mut layouts| layouts.remove(&parent_id).is_some())
.unwrap_or(false);
if let Ok(mut dirty) = self.dirty.lock() {
dirty.retain(|id| *id != parent_id);
}
removed
}
pub fn has(&self, parent_id: ObjectId) -> bool {
self.layouts.lock().map(|layouts| layouts.contains_key(&parent_id)).unwrap_or(false)
}
pub fn len(&self) -> usize {
self.layouts.lock().map(|layouts| layouts.len()).unwrap_or(0)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn invalidate(&self, parent_id: ObjectId) -> bool {
if !self.has(parent_id) {
return false;
}
if let Ok(mut dirty) = self.dirty.lock() {
if !dirty.contains(&parent_id) {
dirty.push(parent_id);
}
}
true
}
pub fn apply(&self, parent_id: ObjectId, rect: Rect) -> Vec<(ObjectId, Rect)> {
let geometries = self.run(parent_id, |layout| {
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
});
if let Ok(mut dirty) = self.dirty.lock() {
dirty.retain(|id| *id != parent_id);
}
geometries
}
pub fn apply_with_hints(
&self,
parent_id: ObjectId,
rect: Rect,
children: &[ChildInfo],
) -> Vec<(ObjectId, Rect)> {
let geometries = self.run(parent_id, |layout| {
let mut geometries = Vec::new();
layout.arrange(rect, children, &mut |child_id, child_rect| {
if child_id != SPACER_ID {
geometries.push((child_id, child_rect));
}
});
geometries
});
if let Ok(mut dirty) = self.dirty.lock() {
dirty.retain(|id| *id != parent_id);
}
geometries
}
fn run<R>(&self, parent_id: ObjectId, read: impl FnOnce(&dyn Layout) -> R) -> R
where
R: Default,
{
match self.layouts.lock() {
Ok(layouts) => match layouts.get(&parent_id) {
Some(layout) => read(layout.as_ref()),
None => R::default(),
},
Err(_) => R::default(),
}
}
pub fn apply_if_dirty(&self, parent_id: ObjectId, rect: Rect) -> Option<Vec<(ObjectId, Rect)>> {
let dirty = self.dirty.lock().map(|dirty| dirty.contains(&parent_id)).unwrap_or(false);
if !dirty {
return None;
}
Some(self.apply(parent_id, rect))
}
}
pub fn store_layout(parent_id: ObjectId, layout: Box<dyn Layout>) {
LAYOUT_MAP.with(|map| {
map.borrow_mut().insert(parent_id, layout);
});
let _ = invalidate_layout(parent_id);
}
pub fn forget_layout(parent_id: ObjectId) -> bool {
let removed = LAYOUT_MAP.with(|map| map.borrow_mut().remove(&parent_id).is_some());
DIRTY_LAYOUTS.with(|dirty| dirty.borrow_mut().retain(|id| *id != parent_id));
removed
}
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,
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChildPlacement {
pub col: u32,
pub row: u32,
pub col_span: u32,
pub row_span: u32,
}
pub fn place_widget_in_layout(
child_id: ObjectId,
placement: ChildPlacement,
parent_id: ObjectId,
) -> bool {
LAYOUT_MAP.with(|map| {
let mut map = map.borrow_mut();
let Some(layout) = map.get_mut(&parent_id) else {
return false;
};
if let Some(grid) = layout.as_any_mut().downcast_mut::<crate::layout::GridLayout>() {
grid.set_widget_spanning(
placement.row,
placement.col,
placement.col_span,
placement.row_span,
child_id,
);
return grid.has_child(child_id);
}
if let Some(grid) = layout.as_any_mut().downcast_mut::<crate::layout::UniformGridLayout>() {
return grid.set_widget_spanning(
placement.row,
placement.col,
placement.col_span,
placement.row_span,
child_id,
);
}
false
})
}
pub fn place_widget_in_region_layout(
child_id: ObjectId,
region: crate::layout::BorderRegion,
extent: u32,
parent_id: ObjectId,
) -> bool {
LAYOUT_MAP.with(|map| {
let mut map = map.borrow_mut();
let Some(layout) = map.get_mut(&parent_id) else {
return false;
};
match layout.as_any_mut().downcast_mut::<crate::layout::BorderLayout>() {
Some(border) => border.set_region(child_id, region, extent),
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)> {
const MAX_LAYOUT_DEPTH: u32 = 64;
apply_layout_bounded(parent_id, rect, 0, MAX_LAYOUT_DEPTH)
}
fn apply_layout_bounded(
parent_id: ObjectId,
rect: Rect,
depth: u32,
max_depth: u32,
) -> Vec<(ObjectId, Rect)> {
#[cfg(alloc_frugal)]
let _ = (depth, max_depth);
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,
);
if depth < max_depth && has_layout(*child_id) {
apply_layout_bounded(*child_id, *child_rect, depth + 1, max_depth);
} else if depth >= max_depth && has_layout(*child_id) {
log::warn!(
"declarative layout for widget {parent_id} exceeded the {max_depth}-level nesting \
bound; child {child_id}'s layout was not applied"
);
}
}
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 apply_layout_with_hints(
parent_id: ObjectId,
rect: Rect,
children: &[ChildInfo],
) -> 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.arrange(rect, children, &mut |child_id, child_rect| {
if child_id != SPACER_ID {
geometries.push((child_id, child_rect));
}
});
geometries
})
}
pub fn layout_child_count(parent_id: ObjectId) -> usize {
LAYOUT_MAP.with(|map| {
map.borrow().get(&parent_id).map_or(0, |layout| {
layout.child_ids().iter().filter(|child| **child != SPACER_ID).count()
})
})
}
pub fn layout_kind_name(kind: LayoutKindName) -> &'static str {
match kind {
LayoutKindName::HorizontalBox => "hbox",
LayoutKindName::VerticalBox => "vbox",
LayoutKindName::Grid => "grid",
LayoutKindName::Border => "border",
LayoutKindName::Form => "form",
LayoutKindName::Stack => "stack",
LayoutKindName::HBox => "hbox",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LayoutKindName {
HorizontalBox,
VerticalBox,
Grid,
Border,
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 applying_with_hints_runs_the_arrange_path() {
use crate::layout::{GridLayout, Hints};
const PARENT: ObjectId = 0x5EF3;
let mut grid = GridLayout::new(1, 2, 0, 0);
grid.set_widget(0, 0, 201);
grid.set_widget(0, 1, 202);
store_layout(PARENT, Box::new(grid));
let children =
[ChildInfo::new(201, Hints::fixed(150, 10)), ChildInfo::new(202, Hints::fixed(50, 10))];
let without = preview_layout(PARENT, Rect::new(0, 0, 200, 20));
let without_first = without.iter().find(|(id, _)| *id == 201).expect("child").1;
assert_eq!(without_first.width, 100, "the id-only path splits evenly");
let with = apply_layout_with_hints(PARENT, Rect::new(0, 0, 200, 20), &children);
let with_first = with.iter().find(|(id, _)| *id == 201).expect("child").1;
assert_eq!(with_first.width, 150, "the hint-aware path honours the preferred width");
assert!(forget_layout(PARENT));
}
#[test]
fn the_shared_registry_applies_with_hints() {
use crate::layout::{GridLayout, Hints};
const PARENT: ObjectId = 0x5EF4;
let mut grid = GridLayout::new(1, 2, 0, 0);
grid.set_widget(0, 0, 301);
grid.set_widget(0, 1, 302);
let registry = SharedLayoutRegistry::new();
registry.store(PARENT, Box::new(grid));
let children =
[ChildInfo::new(301, Hints::fixed(70, 10)), ChildInfo::new(302, Hints::fixed(130, 10))];
let out = registry.apply_with_hints(PARENT, Rect::new(0, 0, 200, 20), &children);
assert_eq!(out.iter().find(|(id, _)| *id == 301).expect("child").1.width, 70);
assert_eq!(out.iter().find(|(id, _)| *id == 302).expect("child").1.width, 130);
}
#[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));
}
#[test]
fn dirty_layouts_apply_once_and_then_stay_clean() {
const PARENT: ObjectId = 0x5EF3;
store_layout(PARENT, box_layout());
add_widget_to_layout(401, 0, PARENT);
assert!(is_layout_dirty(PARENT));
assert!(invalidate_layout(PARENT));
let first = apply_layout_if_dirty(PARENT, Rect::new(0, 0, 80, 20));
assert_eq!(first.map(|entries| entries.len()), Some(1));
assert!(!is_layout_dirty(PARENT));
assert!(apply_layout_if_dirty(PARENT, Rect::new(0, 0, 80, 20)).is_none());
assert!(!invalidate_layout(0xDEAD_BEEF));
assert!(forget_layout(PARENT));
}
#[test]
fn the_shared_registry_is_usable_from_another_thread() {
struct OneChild;
impl Layout for OneChild {
fn add_widget(&mut self, _widget_id: ObjectId, _stretch: u32) {}
fn remove_widget(&mut self, _widget_id: ObjectId) {}
fn update(&self, rect: Rect, out: &mut dyn FnMut(ObjectId, Rect)) {
out(501, rect);
}
fn as_any(&self) -> &dyn crate::compat::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn crate::compat::Any {
self
}
}
let registry = std::sync::Arc::new(SharedLayoutRegistry::new());
registry.store(0x5EF4, Box::new(OneChild));
let other = std::sync::Arc::clone(®istry);
let placed =
std::thread::spawn(move || other.apply_if_dirty(0x5EF4, Rect::new(1, 2, 30, 40)))
.join()
.expect("the worker thread finishes");
assert_eq!(placed, Some(vec![(501, Rect::new(1, 2, 30, 40))]));
assert!(registry.apply_if_dirty(0x5EF4, Rect::new(0, 0, 1, 1)).is_none());
}
}