use alloc::boxed::Box;
use alloc::vec;
use alloc::vec::Vec;
use crate::invalidation::InvalidationList;
use crate::widget::Rect;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum Dimension {
Px(i32),
Pct(u16),
Content,
}
impl Default for Dimension {
fn default() -> Self {
Dimension::Px(0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum FlexFlow {
#[default]
Row,
Column,
RowWrap,
RowReverse,
RowWrapReverse,
ColumnWrap,
ColumnReverse,
ColumnWrapReverse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum FlexAlign {
#[default]
Start,
End,
Center,
SpaceEvenly,
SpaceAround,
SpaceBetween,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlexConfig {
pub flow: FlexFlow,
pub main_align: FlexAlign,
pub cross_align: FlexAlign,
pub track_cross_align: FlexAlign,
pub gap_main: i32,
pub gap_cross: i32,
}
impl Default for FlexConfig {
fn default() -> Self {
Self {
flow: FlexFlow::Row,
main_align: FlexAlign::Start,
cross_align: FlexAlign::Start,
track_cross_align: FlexAlign::Start,
gap_main: 0,
gap_cross: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum GridTrack {
Px(i32),
Fr(u8),
Content,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum GridAlign {
Start,
Center,
End,
#[default]
Stretch,
SpaceEvenly,
SpaceAround,
SpaceBetween,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GridConfig {
pub col_tracks: Vec<GridTrack>,
pub row_tracks: Vec<GridTrack>,
pub col_gap: i32,
pub row_gap: i32,
pub col_align: GridAlign,
pub row_align: GridAlign,
}
impl Default for GridConfig {
fn default() -> Self {
Self {
col_tracks: Vec::new(),
row_tracks: Vec::new(),
col_gap: 0,
row_gap: 0,
col_align: GridAlign::Stretch,
row_align: GridAlign::Stretch,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemHints {
pub width: Dimension,
pub height: Dimension,
pub flex_grow: u8,
pub self_align: Option<FlexAlign>,
pub col_pos: u16,
pub col_span: u16,
pub row_pos: u16,
pub row_span: u16,
pub col_align: GridAlign,
pub row_align: GridAlign,
pub min_width: Option<i32>,
pub max_width: Option<i32>,
pub min_height: Option<i32>,
pub max_height: Option<i32>,
}
impl Default for ItemHints {
fn default() -> Self {
Self {
width: Dimension::Px(0),
height: Dimension::Px(0),
flex_grow: 0,
self_align: None,
col_pos: 0,
col_span: 1,
row_pos: 0,
row_span: 1,
col_align: GridAlign::Stretch,
row_align: GridAlign::Stretch,
min_width: None,
max_width: None,
min_height: None,
max_height: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EngineConfig {
Flex(FlexConfig),
Grid(GridConfig),
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum LayoutRole {
#[default]
None,
Container(EngineConfig),
Item(ItemHints),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LayoutState {
pub role: LayoutRole,
pub computed: Option<Rect>,
pub layout_dirty: bool,
}
impl Default for LayoutState {
fn default() -> Self {
Self {
role: LayoutRole::None,
computed: None,
layout_dirty: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LayoutStyle {
pub padding_top: i32,
pub padding_bottom: i32,
pub padding_left: i32,
pub padding_right: i32,
pub margin_top: i32,
pub margin_bottom: i32,
pub margin_left: i32,
pub margin_right: i32,
pub gap_row: i32,
pub gap_col: i32,
}
pub fn resolve_layout_style(
style_state: Option<&crate::style_cascade::StyleState>,
part: crate::style_cascade::Part,
states: crate::object::ObjectStates,
) -> LayoutStyle {
let Some(ss) = style_state else {
return LayoutStyle::default();
};
let mut out = LayoutStyle::default();
macro_rules! apply_patch {
($patch:expr) => {{
let p: &crate::style_cascade::StylePatch = $patch;
if out.padding_top == 0 {
if let Some(v) = p.padding_top {
out.padding_top = v;
}
}
if out.padding_bottom == 0 {
if let Some(v) = p.padding_bottom {
out.padding_bottom = v;
}
}
if out.padding_left == 0 {
if let Some(v) = p.padding_left {
out.padding_left = v;
}
}
if out.padding_right == 0 {
if let Some(v) = p.padding_right {
out.padding_right = v;
}
}
if out.margin_top == 0 {
if let Some(v) = p.margin_top {
out.margin_top = v;
}
}
if out.margin_bottom == 0 {
if let Some(v) = p.margin_bottom {
out.margin_bottom = v;
}
}
if out.margin_left == 0 {
if let Some(v) = p.margin_left {
out.margin_left = v;
}
}
if out.margin_right == 0 {
if let Some(v) = p.margin_right {
out.margin_right = v;
}
}
if out.gap_row == 0 {
if let Some(v) = p.gap_row {
out.gap_row = v;
}
}
if out.gap_col == 0 {
if let Some(v) = p.gap_col {
out.gap_col = v;
}
}
}};
}
for (selector, patch) in ss.local_entries().iter().rev() {
if selector.matches(part, states) {
apply_patch!(patch);
}
}
for (selector, patch) in ss.added_entries().iter().rev() {
if selector.matches(part, states) {
apply_patch!(*patch);
}
}
out
}
fn resolve_dimension(
dim: Dimension,
parent_content: i32,
content_size: i32,
min: Option<i32>,
max: Option<i32>,
) -> i32 {
let raw = match dim {
Dimension::Px(n) => n,
Dimension::Pct(p) => (parent_content * p as i32) / 100,
Dimension::Content => content_size,
};
clamp_size(raw, min, max)
}
fn clamp_size(size: i32, min: Option<i32>, max: Option<i32>) -> i32 {
let mut s = size;
if let Some(lo) = min {
s = s.max(lo);
}
if let Some(hi) = max {
s = s.min(hi);
}
s
}
pub fn run_layout<const N: usize>(
root: &mut crate::object::ObjectNode,
planner: &mut InvalidationList<N>,
) {
let root_bounds = root.effective_bounds();
run_layout_node(root, root_bounds, planner);
}
fn run_layout_node<const N: usize>(
node: &mut crate::object::ObjectNode,
_parent_bounds: Rect,
planner: &mut InvalidationList<N>,
) {
let is_dirty_container = node
.layout
.as_ref()
.map(|ls| ls.layout_dirty && matches!(ls.role, LayoutRole::Container(_)))
.unwrap_or(false);
if is_dirty_container {
let engine = match node.layout.as_ref().unwrap().role.clone() {
LayoutRole::Container(cfg) => cfg,
_ => unreachable!(),
};
let layout_style = resolve_layout_style(
node.style.as_deref(),
crate::style_cascade::Part::MAIN,
node.meta().states(),
);
let container_bounds = node.effective_bounds();
let content_x = container_bounds.x + layout_style.padding_left;
let content_y = container_bounds.y + layout_style.padding_top;
let content_w =
(container_bounds.width - layout_style.padding_left - layout_style.padding_right)
.max(0);
let content_h =
(container_bounds.height - layout_style.padding_top - layout_style.padding_bottom)
.max(0);
let content_area = Rect {
x: content_x,
y: content_y,
width: content_w,
height: content_h,
};
match engine {
EngineConfig::Flex(cfg) => {
run_flex(node, content_area, &cfg, &layout_style, planner);
}
EngineConfig::Grid(cfg) => {
run_grid(node, content_area, &cfg, &layout_style, planner);
}
}
if let Some(ls) = node.layout.as_deref_mut() {
ls.layout_dirty = false;
}
use crate::object::ObjectEvent;
node.invoke_handlers_for(&ObjectEvent::LayoutChanged);
} else {
let children_count = node.children().len();
for i in 0..children_count {
let child_bounds = node.children()[i].effective_bounds();
run_layout_node(&mut node.children_mut()[i], child_bounds, planner);
}
}
}
fn run_flex<const N: usize>(
container: &mut crate::object::ObjectNode,
content_area: Rect,
cfg: &FlexConfig,
layout_style: &LayoutStyle,
planner: &mut InvalidationList<N>,
) {
use FlexFlow::*;
let is_row_flow = matches!(cfg.flow, Row | RowWrap | RowReverse | RowWrapReverse);
let is_reverse = matches!(
cfg.flow,
RowReverse | RowWrapReverse | ColumnReverse | ColumnWrapReverse
);
let _is_wrap = matches!(
cfg.flow,
RowWrap | RowWrapReverse | ColumnWrap | ColumnWrapReverse
);
let gap_main = if layout_style.gap_col != 0 && is_row_flow {
layout_style.gap_col
} else if layout_style.gap_row != 0 && !is_row_flow {
layout_style.gap_row
} else {
cfg.gap_main
};
let _gap_cross = if layout_style.gap_row != 0 && is_row_flow {
layout_style.gap_row
} else if layout_style.gap_col != 0 && !is_row_flow {
layout_style.gap_col
} else {
cfg.gap_cross
};
let n = container.children().len();
let mut sizes: Vec<(i32, i32)> = Vec::with_capacity(n); let mut grows: Vec<u8> = Vec::with_capacity(n);
let mut self_aligns: Vec<Option<FlexAlign>> = Vec::with_capacity(n);
for i in 0..n {
let child = &container.children()[i];
let (hints, intrinsic) = child_hints_and_bounds(child);
let main_dim = if is_row_flow {
hints.width
} else {
hints.height
};
let cross_dim = if is_row_flow {
hints.height
} else {
hints.width
};
let parent_main = if is_row_flow {
content_area.width
} else {
content_area.height
};
let parent_cross = if is_row_flow {
content_area.height
} else {
content_area.width
};
let intrinsic_main = if is_row_flow {
intrinsic.width
} else {
intrinsic.height
};
let intrinsic_cross = if is_row_flow {
intrinsic.height
} else {
intrinsic.width
};
let main = resolve_main_dim(main_dim, parent_main, intrinsic_main, &hints);
let cross = resolve_cross_dim(cross_dim, parent_cross, intrinsic_cross, &hints);
sizes.push((main, cross));
grows.push(hints.flex_grow);
self_aligns.push(hints.self_align);
}
let gaps_total = if n > 1 { gap_main * (n as i32 - 1) } else { 0 };
let fixed_main: i32 = sizes.iter().map(|&(m, _)| m).sum();
let parent_main = if is_row_flow {
content_area.width
} else {
content_area.height
};
let free = (parent_main - fixed_main - gaps_total).max(0);
let total_grow: u32 = grows.iter().map(|&g| g as u32).sum();
if total_grow > 0 && free > 0 {
let mut distributed = 0i32;
for i in 0..n {
if grows[i] > 0 {
let share = free * grows[i] as i32 / total_grow as i32;
sizes[i].0 += share;
distributed += share;
}
}
let remainder = free - distributed;
if remainder > 0
&& let Some(last_grow_idx) = grows.iter().rposition(|&g| g > 0)
{
sizes[last_grow_idx].0 += remainder;
}
}
let total_main_used: i32 = sizes.iter().map(|&(m, _)| m).sum::<i32>() + gaps_total;
let start_offset = main_align_offset(
cfg.main_align,
parent_main,
total_main_used,
n as i32,
gap_main,
);
let parent_cross = if is_row_flow {
content_area.height
} else {
content_area.width
};
let indices: Vec<usize> = if is_reverse {
(0..n).rev().collect()
} else {
(0..n).collect()
};
let mut main_cursor = start_offset;
for (seq, &child_idx) in indices.iter().enumerate() {
let (item_main, item_cross_intrinsic) = sizes[child_idx];
let effective_cross_align = self_aligns[child_idx].unwrap_or(cfg.cross_align);
let item_cross = match effective_cross_align {
FlexAlign::Start
| FlexAlign::End
| FlexAlign::Center
| FlexAlign::SpaceEvenly
| FlexAlign::SpaceAround
| FlexAlign::SpaceBetween => item_cross_intrinsic,
};
let cross_offset = cross_align_offset(effective_cross_align, parent_cross, item_cross);
let (x, y, w, h) = if is_row_flow {
(
content_area.x + main_cursor,
content_area.y + cross_offset,
item_main,
item_cross,
)
} else {
(
content_area.x + cross_offset,
content_area.y + main_cursor,
item_cross,
item_main,
)
};
let new_rect = Rect {
x,
y,
width: w,
height: h,
};
write_computed(&mut container.children_mut()[child_idx], new_rect, planner);
main_cursor += item_main;
if seq < n.saturating_sub(1) {
main_cursor += gap_main;
}
}
}
fn main_align_offset(
align: FlexAlign,
parent_size: i32,
total_used: i32,
item_count: i32,
gap: i32,
) -> i32 {
let free = (parent_size - total_used).max(0);
match align {
FlexAlign::Start => 0,
FlexAlign::End => free,
FlexAlign::Center => free / 2,
FlexAlign::SpaceEvenly => {
if item_count > 0 {
free / (item_count + 1)
} else {
0
}
}
FlexAlign::SpaceAround => {
if item_count > 0 {
free / (2 * item_count)
} else {
0
}
}
FlexAlign::SpaceBetween => {
let _ = gap; 0
}
}
}
fn cross_align_offset(align: FlexAlign, parent_cross: i32, item_cross: i32) -> i32 {
let free = (parent_cross - item_cross).max(0);
match align {
FlexAlign::Start
| FlexAlign::SpaceEvenly
| FlexAlign::SpaceAround
| FlexAlign::SpaceBetween => 0,
FlexAlign::End => free,
FlexAlign::Center => free / 2,
}
}
fn resolve_main_dim(dim: Dimension, parent_main: i32, intrinsic: i32, hints: &ItemHints) -> i32 {
let (min, max) = intrinsic_min_max_for_dim(dim, hints, true);
match dim {
Dimension::Px(0) => clamp_size(intrinsic, min, max),
_ => resolve_dimension(dim, parent_main, intrinsic, min, max),
}
}
fn resolve_cross_dim(dim: Dimension, parent_cross: i32, intrinsic: i32, hints: &ItemHints) -> i32 {
let (min, max) = intrinsic_min_max_for_dim(dim, hints, false);
match dim {
Dimension::Px(0) => clamp_size(intrinsic, min, max),
_ => resolve_dimension(dim, parent_cross, intrinsic, min, max),
}
}
fn intrinsic_min_max_for_dim(
_dim: Dimension,
hints: &ItemHints,
is_width: bool,
) -> (Option<i32>, Option<i32>) {
if is_width {
(hints.min_width, hints.max_width)
} else {
(hints.min_height, hints.max_height)
}
}
fn run_grid<const N: usize>(
container: &mut crate::object::ObjectNode,
content_area: Rect,
cfg: &GridConfig,
layout_style: &LayoutStyle,
planner: &mut InvalidationList<N>,
) {
let n_cols = cfg.col_tracks.len();
let n_rows = cfg.row_tracks.len();
if n_cols == 0 || n_rows == 0 {
return;
}
let col_gap = if layout_style.gap_col != 0 {
layout_style.gap_col
} else {
cfg.col_gap
};
let row_gap = if layout_style.gap_row != 0 {
layout_style.gap_row
} else {
cfg.row_gap
};
let mut col_sizes: Vec<i32> = vec![0i32; n_cols];
let mut row_sizes: Vec<i32> = vec![0i32; n_rows];
for (i, track) in cfg.col_tracks.iter().enumerate() {
if let GridTrack::Px(px) = track {
col_sizes[i] = (*px).max(0);
}
}
for (i, track) in cfg.row_tracks.iter().enumerate() {
if let GridTrack::Px(px) = track {
row_sizes[i] = (*px).max(0);
}
}
let n_children = container.children().len();
for (track_idx, col_size) in col_sizes.iter_mut().enumerate() {
if !matches!(cfg.col_tracks[track_idx], GridTrack::Content) {
continue;
}
let mut max_w = 0i32;
for ci in 0..n_children {
let child = &container.children()[ci];
let (hints, intrinsic) = child_hints_and_bounds(child);
let col = hints.col_pos as usize;
let span = hints.col_span.max(1) as usize;
if col <= track_idx && track_idx < col + span {
let w = resolve_main_dim(hints.width, content_area.width, intrinsic.width, &hints);
if w > max_w {
max_w = w;
}
}
}
*col_size = max_w;
}
for (track_idx, row_size) in row_sizes.iter_mut().enumerate() {
if !matches!(cfg.row_tracks[track_idx], GridTrack::Content) {
continue;
}
let mut max_h = 0i32;
for ci in 0..n_children {
let child = &container.children()[ci];
let (hints, intrinsic) = child_hints_and_bounds(child);
let row = hints.row_pos as usize;
let span = hints.row_span.max(1) as usize;
if row <= track_idx && track_idx < row + span {
let h =
resolve_cross_dim(hints.height, content_area.height, intrinsic.height, &hints);
if h > max_h {
max_h = h;
}
}
}
*row_size = max_h;
}
let gaps_col_total = if n_cols > 1 {
col_gap * (n_cols as i32 - 1)
} else {
0
};
let gaps_row_total = if n_rows > 1 {
row_gap * (n_rows as i32 - 1)
} else {
0
};
let fixed_col_total: i32 = cfg
.col_tracks
.iter()
.zip(&col_sizes)
.filter(|(t, _)| !matches!(t, GridTrack::Fr(_)))
.map(|(_, &s)| s)
.sum();
let fixed_row_total: i32 = cfg
.row_tracks
.iter()
.zip(&row_sizes)
.filter(|(t, _)| !matches!(t, GridTrack::Fr(_)))
.map(|(_, &s)| s)
.sum();
let free_col = (content_area.width - fixed_col_total - gaps_col_total).max(0);
let free_row = (content_area.height - fixed_row_total - gaps_row_total).max(0);
let total_col_fr: u32 = cfg
.col_tracks
.iter()
.filter_map(|t| {
if let GridTrack::Fr(f) = t {
Some(*f as u32)
} else {
None
}
})
.sum();
let total_row_fr: u32 = cfg
.row_tracks
.iter()
.filter_map(|t| {
if let GridTrack::Fr(f) = t {
Some(*f as u32)
} else {
None
}
})
.sum();
if total_col_fr > 0 {
let mut distributed = 0i32;
let mut last_fr_idx = None;
for (i, track) in cfg.col_tracks.iter().enumerate() {
if let GridTrack::Fr(f) = track {
let share = free_col * (*f as i32) / total_col_fr as i32;
col_sizes[i] = share;
distributed += share;
last_fr_idx = Some(i);
}
}
if let Some(idx) = last_fr_idx {
col_sizes[idx] += free_col - distributed;
}
}
if total_row_fr > 0 {
let mut distributed = 0i32;
let mut last_fr_idx = None;
for (i, track) in cfg.row_tracks.iter().enumerate() {
if let GridTrack::Fr(f) = track {
let share = free_row * (*f as i32) / total_row_fr as i32;
row_sizes[i] = share;
distributed += share;
last_fr_idx = Some(i);
}
}
if let Some(idx) = last_fr_idx {
row_sizes[idx] += free_row - distributed;
}
}
let mut col_offsets: Vec<i32> = Vec::with_capacity(n_cols);
let mut acc = 0i32;
for (i, &s) in col_sizes.iter().enumerate() {
col_offsets.push(acc);
acc += s;
if i < n_cols - 1 {
acc += col_gap;
}
}
let mut row_offsets: Vec<i32> = Vec::with_capacity(n_rows);
acc = 0;
for (i, &s) in row_sizes.iter().enumerate() {
row_offsets.push(acc);
acc += s;
if i < n_rows - 1 {
acc += row_gap;
}
}
for ci in 0..n_children {
let child = &container.children()[ci];
let (hints, intrinsic) = child_hints_and_bounds(child);
let col = (hints.col_pos as usize).min(n_cols.saturating_sub(1));
let row = (hints.row_pos as usize).min(n_rows.saturating_sub(1));
let col_span = (hints.col_span.max(1) as usize).min(n_cols - col);
let row_span = (hints.row_span.max(1) as usize).min(n_rows - row);
let cell_x = content_area.x + col_offsets[col];
let cell_y = content_area.y + row_offsets[row];
let cell_w: i32 =
col_sizes[col..col + col_span].iter().sum::<i32>() + col_gap * (col_span as i32 - 1);
let cell_h: i32 =
row_sizes[row..row + row_span].iter().sum::<i32>() + row_gap * (row_span as i32 - 1);
let item_w = match hints.col_align {
GridAlign::Stretch => cell_w,
_ => {
let w = resolve_main_dim(hints.width, cell_w, intrinsic.width, &hints);
w.min(cell_w)
}
};
let item_h = match hints.row_align {
GridAlign::Stretch => cell_h,
_ => {
let h = resolve_cross_dim(hints.height, cell_h, intrinsic.height, &hints);
h.min(cell_h)
}
};
let item_x = cell_x + grid_align_offset(hints.col_align, cell_w, item_w);
let item_y = cell_y + grid_align_offset(hints.row_align, cell_h, item_h);
let new_rect = Rect {
x: item_x,
y: item_y,
width: item_w,
height: item_h,
};
write_computed(&mut container.children_mut()[ci], new_rect, planner);
}
}
fn grid_align_offset(align: GridAlign, cell_size: i32, item_size: i32) -> i32 {
let free = (cell_size - item_size).max(0);
match align {
GridAlign::Start
| GridAlign::Stretch
| GridAlign::SpaceEvenly
| GridAlign::SpaceAround
| GridAlign::SpaceBetween => 0,
GridAlign::Center => free / 2,
GridAlign::End => free,
}
}
fn child_hints_and_bounds(child: &crate::object::ObjectNode) -> (ItemHints, Rect) {
let intrinsic = child.effective_bounds();
let hints = if let Some(ls) = child.layout.as_deref() {
if let LayoutRole::Item(ref h) = ls.role {
let mut h = h.clone();
if h.width == Dimension::Px(0) {
h.width = Dimension::Px(intrinsic.width);
}
if h.height == Dimension::Px(0) {
h.height = Dimension::Px(intrinsic.height);
}
h
} else {
default_hints_for(intrinsic)
}
} else {
default_hints_for(intrinsic)
};
(hints, intrinsic)
}
fn default_hints_for(intrinsic: Rect) -> ItemHints {
ItemHints {
width: Dimension::Px(intrinsic.width),
height: Dimension::Px(intrinsic.height),
..ItemHints::default()
}
}
fn write_computed<const N: usize>(
child: &mut crate::object::ObjectNode,
new_rect: Rect,
planner: &mut InvalidationList<N>,
) {
use crate::object::ObjectEvent;
let old_rect = child.effective_bounds();
child.widget().borrow_mut().set_bounds(new_rect);
let slot = child
.layout
.get_or_insert_with(|| Box::new(LayoutState::default()));
slot.computed = Some(new_rect);
if old_rect != new_rect {
planner.push(old_rect.union(new_rect));
child.invoke_handlers_for(&ObjectEvent::SizeChanged);
}
}
#[cfg(test)]
mod tests {
use alloc::rc::Rc;
use alloc::vec;
use core::cell::RefCell;
use super::*;
use crate::event::Event;
use crate::invalidation::InvalidationList;
use crate::object::{ObjectEvent, ObjectNode};
use crate::renderer::Renderer;
use crate::widget::{Color, Widget};
struct ResizeWidget {
bounds: Rect,
set_bounds_log: Vec<Rect>,
}
impl ResizeWidget {
fn new(bounds: Rect) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
bounds,
set_bounds_log: Vec::new(),
}))
}
}
impl Widget for ResizeWidget {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, _r: &mut dyn Renderer) {}
fn handle_event(&mut self, _e: &Event) -> bool {
false
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
self.set_bounds_log.push(b);
}
}
struct StaticWidget {
bounds: Rect,
}
impl StaticWidget {
fn new(x: i32, y: i32, w: i32, h: i32) -> Rc<RefCell<Self>> {
Rc::new(RefCell::new(Self {
bounds: Rect {
x,
y,
width: w,
height: h,
},
}))
}
}
impl Widget for StaticWidget {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, _r: &mut dyn Renderer) {}
fn handle_event(&mut self, _e: &Event) -> bool {
false
}
}
#[allow(dead_code)]
struct NoopRenderer;
impl Renderer for NoopRenderer {
fn fill_rect(&mut self, _: Rect, _: Color) {}
fn draw_text(&mut self, _: (i32, i32), _: &str, _: Color) {}
}
fn make_node(x: i32, y: i32, w: i32, h: i32) -> ObjectNode {
ObjectNode::new(StaticWidget::new(x, y, w, h))
}
#[test]
fn set_bounds_default_noop() {
let widget = StaticWidget::new(0, 0, 100, 50);
let original = widget.borrow().bounds();
widget.borrow_mut().set_bounds(Rect {
x: 10,
y: 20,
width: 30,
height: 40,
});
assert_eq!(widget.borrow().bounds(), original);
}
#[test]
fn set_bounds_override_adopts_rect() {
let widget = ResizeWidget::new(Rect {
x: 0,
y: 0,
width: 50,
height: 50,
});
let new_bounds = Rect {
x: 10,
y: 20,
width: 80,
height: 60,
};
widget.borrow_mut().set_bounds(new_bounds);
assert_eq!(widget.borrow().bounds(), new_bounds);
assert_eq!(widget.borrow().set_bounds_log, vec![new_bounds]);
}
#[test]
fn effective_bounds_without_layout_returns_intrinsic() {
let node = make_node(5, 10, 100, 200);
let eb = node.effective_bounds();
assert_eq!(
eb,
Rect {
x: 5,
y: 10,
width: 100,
height: 200
}
);
}
#[test]
fn effective_bounds_with_computed_returns_computed() {
let mut node = make_node(5, 10, 100, 200);
node.layout = Some(Box::new(LayoutState {
computed: Some(Rect {
x: 100,
y: 150,
width: 80,
height: 60,
}),
..LayoutState::default()
}));
let eb = node.effective_bounds();
assert_eq!(
eb,
Rect {
x: 100,
y: 150,
width: 80,
height: 60
}
);
}
#[test]
fn hit_test_uses_effective_bounds() {
let mut node = make_node(0, 0, 50, 50);
node.meta_mut()
.flags_mut()
.insert(crate::object::ObjectFlags::CLICKABLE);
node.layout = Some(Box::new(LayoutState {
computed: Some(Rect {
x: 100,
y: 100,
width: 50,
height: 50,
}),
..LayoutState::default()
}));
assert!(node.hit_test(10, 10).is_none());
assert!(node.hit_test(110, 110).is_some());
}
#[test]
fn dimension_px_resolves_to_n() {
assert_eq!(resolve_dimension(Dimension::Px(42), 100, 0, None, None), 42);
}
#[test]
fn dimension_pct_resolves_against_parent_content() {
assert_eq!(
resolve_dimension(Dimension::Pct(50), 200, 0, None, None),
100
);
}
#[test]
fn dimension_content_uses_content_size() {
assert_eq!(
resolve_dimension(Dimension::Content, 200, 73, None, None),
73
);
}
#[test]
fn dimension_min_max_clamped() {
assert_eq!(
resolve_dimension(Dimension::Px(5), 100, 0, Some(10), None),
10
);
assert_eq!(
resolve_dimension(Dimension::Px(200), 100, 0, None, Some(50)),
50
);
}
#[test]
fn flex_row_places_children_in_order_with_gap() {
let container_w = StaticWidget::new(0, 0, 200, 50);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig {
flow: FlexFlow::Row,
gap_main: 10,
..FlexConfig::default()
});
container.append_child(make_node(0, 0, 60, 50));
container.append_child(make_node(0, 0, 60, 50));
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 200,
height: 50,
});
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let c0 = container.children()[0].effective_bounds();
let c1 = container.children()[1].effective_bounds();
assert_eq!(c0.x, 0);
assert_eq!(c0.width, 60);
assert_eq!(c1.x, 70);
assert_eq!(c1.width, 60);
}
#[test]
fn flex_column_places_children_vertically() {
let container_w = StaticWidget::new(0, 0, 50, 200);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig {
flow: FlexFlow::Column,
gap_main: 5,
..FlexConfig::default()
});
container.append_child(make_node(0, 0, 50, 40));
container.append_child(make_node(0, 0, 50, 40));
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 50,
height: 200,
});
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let c0 = container.children()[0].effective_bounds();
let c1 = container.children()[1].effective_bounds();
assert_eq!(c0.y, 0);
assert_eq!(c0.height, 40);
assert_eq!(c1.y, 45); assert_eq!(c1.height, 40);
}
#[test]
fn flex_grow_distributes_remaining_space() {
let container_w = StaticWidget::new(0, 0, 200, 50);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig {
flow: FlexFlow::Row,
gap_main: 0,
..FlexConfig::default()
});
container.append_child(make_node(0, 0, 40, 50));
let mut child1 = make_node(0, 0, 0, 50);
child1.set_item_hints(ItemHints {
flex_grow: 1,
width: Dimension::Px(0),
height: Dimension::Px(50),
..ItemHints::default()
});
container.append_child(child1);
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 200,
height: 50,
});
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let c0 = container.children()[0].effective_bounds();
let c1 = container.children()[1].effective_bounds();
assert_eq!(c0.width, 40);
assert_eq!(c1.x, 40);
assert_eq!(c1.width, 160);
}
#[test]
fn grid_px_tracks_and_explicit_placement() {
let container_w = StaticWidget::new(0, 0, 200, 100);
let mut container = ObjectNode::new(container_w);
container.set_layout_grid(GridConfig {
col_tracks: vec![GridTrack::Px(100), GridTrack::Px(100)],
row_tracks: vec![GridTrack::Px(50), GridTrack::Px(50)],
..GridConfig::default()
});
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 200,
height: 100,
});
let mut child = make_node(0, 0, 80, 40);
child.set_item_hints(ItemHints {
col_pos: 1,
row_pos: 0,
col_align: GridAlign::Stretch,
row_align: GridAlign::Stretch,
..ItemHints::default()
});
container.append_child(child);
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let c = container.children()[0].effective_bounds();
assert_eq!(c.x, 100);
assert_eq!(c.y, 0);
assert_eq!(c.width, 100);
assert_eq!(c.height, 50);
}
#[test]
fn grid_fr_tracks_distribute_remaining_space() {
let container_w = StaticWidget::new(0, 0, 300, 100);
let mut container = ObjectNode::new(container_w);
container.set_layout_grid(GridConfig {
col_tracks: vec![GridTrack::Fr(1), GridTrack::Fr(2)],
row_tracks: vec![GridTrack::Px(100)],
..GridConfig::default()
});
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 300,
height: 100,
});
let mut child0 = make_node(0, 0, 10, 10);
child0.set_item_hints(ItemHints {
col_pos: 0,
row_pos: 0,
col_align: GridAlign::Stretch,
row_align: GridAlign::Stretch,
..ItemHints::default()
});
let mut child1 = make_node(0, 0, 10, 10);
child1.set_item_hints(ItemHints {
col_pos: 1,
row_pos: 0,
col_align: GridAlign::Stretch,
row_align: GridAlign::Stretch,
..ItemHints::default()
});
container.append_child(child0);
container.append_child(child1);
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let c0 = container.children()[0].effective_bounds();
let c1 = container.children()[1].effective_bounds();
assert_eq!(c0.width, 100);
assert_eq!(c1.x, 100);
assert_eq!(c1.width, 200);
}
#[test]
fn grid_span_covers_multiple_tracks() {
let container_w = StaticWidget::new(0, 0, 150, 50);
let mut container = ObjectNode::new(container_w);
container.set_layout_grid(GridConfig {
col_tracks: vec![GridTrack::Px(50), GridTrack::Px(50), GridTrack::Px(50)],
row_tracks: vec![GridTrack::Px(50)],
..GridConfig::default()
});
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 150,
height: 50,
});
let mut child = make_node(0, 0, 10, 10);
child.set_item_hints(ItemHints {
col_pos: 0,
col_span: 2,
row_pos: 0,
col_align: GridAlign::Stretch,
row_align: GridAlign::Stretch,
..ItemHints::default()
});
container.append_child(child);
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let c = container.children()[0].effective_bounds();
assert_eq!(c.width, 100);
assert_eq!(c.height, 50);
}
#[test]
fn layout_pass_pushes_old_union_new_to_planner() {
let container_w = StaticWidget::new(0, 0, 200, 50);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig::default());
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 200,
height: 50,
});
container.append_child(make_node(0, 0, 50, 50));
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let plan = planner.plan();
let _ = plan;
}
#[test]
fn layout_pass_pushes_dirty_rect_on_move() {
let container_w = StaticWidget::new(0, 0, 200, 50);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig {
flow: FlexFlow::Row,
gap_main: 0,
..FlexConfig::default()
});
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 200,
height: 50,
});
container.append_child(make_node(0, 0, 80, 50));
container.append_child(make_node(0, 0, 60, 50));
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
let rects_pushed = planner.plan();
assert!(
!matches!(rects_pushed, crate::invalidation::PresentPlan::None),
"expected dirty rects to be pushed for displaced child"
);
}
#[test]
fn size_changed_dispatched_on_resize() {
let container_w = StaticWidget::new(0, 0, 200, 50);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig::default());
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 200,
height: 50,
});
container.append_child(make_node(99, 99, 50, 50));
use alloc::rc::Rc;
use core::cell::Cell;
let fired = Rc::new(Cell::new(false));
let fired2 = fired.clone();
container.children_mut()[0].add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::SizeChanged) {
fired2.set(true);
}
false
});
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
assert!(fired.get(), "SizeChanged should fire when child moves");
}
#[test]
fn layout_changed_dispatched_to_container() {
let container_w = StaticWidget::new(0, 0, 100, 100);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig::default());
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 100,
height: 100,
});
use alloc::rc::Rc;
use core::cell::Cell;
let fired = Rc::new(Cell::new(false));
let fired2 = fired.clone();
container.add_target_handler(move |ev, _ctx| {
if matches!(ev, ObjectEvent::LayoutChanged) {
fired2.set(true);
}
false
});
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
assert!(fired.get(), "LayoutChanged should fire on the container");
}
#[test]
fn dirty_flag_cleared_after_layout() {
let container_w = StaticWidget::new(0, 0, 100, 50);
let mut container = ObjectNode::new(container_w);
container.set_layout_flex(FlexConfig::default());
container.layout.as_deref_mut().unwrap().computed = Some(Rect {
x: 0,
y: 0,
width: 100,
height: 50,
});
assert!(container.layout.as_ref().unwrap().layout_dirty);
let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
run_layout(&mut container, &mut planner);
assert!(!container.layout.as_ref().unwrap().layout_dirty);
}
#[test]
fn set_layout_flex_marks_dirty() {
let mut node = make_node(0, 0, 100, 50);
node.set_layout_flex(FlexConfig::default());
assert!(node.layout.as_ref().unwrap().layout_dirty);
}
#[test]
fn set_item_hints_marks_dirty() {
let mut node = make_node(0, 0, 50, 50);
node.set_item_hints(ItemHints::default());
assert!(node.layout.as_ref().unwrap().layout_dirty);
}
#[test]
fn layout_style_defaults_to_zero() {
let ls = resolve_layout_style(
None,
crate::style_cascade::Part::MAIN,
crate::object::ObjectStates::DEFAULT,
);
assert_eq!(ls, LayoutStyle::default());
}
#[test]
fn layout_style_reads_padding_from_cascade() {
let patch = crate::style_cascade::StylePatch {
padding_top: Some(8),
padding_left: Some(4),
..crate::style_cascade::StylePatch::new()
};
let mut style_state = crate::style_cascade::StyleState::new();
crate::style_cascade::push_local(
&mut style_state,
patch,
crate::style_cascade::Selector::part(crate::style_cascade::Part::MAIN),
);
let ls = resolve_layout_style(
Some(&style_state),
crate::style_cascade::Part::MAIN,
crate::object::ObjectStates::DEFAULT,
);
assert_eq!(ls.padding_top, 8);
assert_eq!(ls.padding_left, 4);
assert_eq!(ls.padding_bottom, 0);
}
#[test]
fn core_style_struct_unchanged() {
let s = crate::style::Style::default();
let _ = s.bg_color;
let _ = s.border_color;
let _ = s.border_width;
let _ = s.alpha;
let _ = s.radius;
}
}