use nalgebra_glm::Vec2;
use crate::ecs::ui::components::UiDepthMode;
use crate::ecs::ui::layout_types::{FlowLayout, compute_layout_rect};
use crate::ecs::ui::types::Rect;
use crate::ecs::ui::units::UiEvalContext;
use crate::ecs::world::World;
use crate::render::wgpu::passes::geometry::UiLayer;
use super::color_blend::apply_animation_to_rect;
struct StackEntry {
entity: freecs::Entity,
parent_rect: Rect,
parent_depth: f32,
parent_font_size: f32,
parent_clip_rect: Option<Rect>,
parent_layer: Option<UiLayer>,
flow_override_rect: Option<Rect>,
}
fn compute_window_dpi_signature(world: &World) -> f32 {
world.resources.window.cached_scale_factor
}
pub fn ui_docked_panel_layout_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
let primary_viewport_size = world
.resources
.window
.cached_viewport_size
.map(|(width, height)| Vec2::new(width as f32, height as f32))
.unwrap_or(Vec2::new(800.0, 600.0));
let primary_dpi_scale = world.resources.window.cached_scale_factor;
world.resources.retained_ui.dock_state = crate::ecs::ui::resources::DockState::default();
let panel_entities: Vec<freecs::Entity> = world
.ui
.query_entities(crate::ecs::world::UI_PANEL)
.collect();
type PanelEntry = (
freecs::Entity,
crate::ecs::ui::components::UiPanelKind,
f32,
i32,
bool,
bool,
String,
String,
Option<[f32; 2]>,
Option<[f32; 2]>,
);
let mut entries: Vec<PanelEntry> = Vec::new();
for entity in &panel_entities {
let visible = world
.ui
.get_ui_layout_node(*entity)
.map(|node| node.visible)
.unwrap_or(true);
let Some(data) = world.ui.get_ui_panel(*entity) else {
continue;
};
let floating_position =
if data.panel_kind == crate::ecs::ui::components::UiPanelKind::Floating {
world
.ui
.get_ui_layout_node(*entity)
.and_then(|node| node.base_layout)
.and_then(|layout| match layout {
crate::ecs::ui::layout_types::UiLayoutType::Window(window) => {
window.position.absolute
}
_ => None,
})
.map(|ab| [ab.x, ab.y])
} else {
None
};
let floating_size = if data.panel_kind == crate::ecs::ui::components::UiPanelKind::Floating
{
world
.ui
.get_ui_layout_node(*entity)
.and_then(|node| node.base_layout)
.and_then(|layout| match layout {
crate::ecs::ui::layout_types::UiLayoutType::Window(window) => {
window.size.absolute
}
_ => None,
})
.map(|ab| [ab.x, ab.y])
} else {
None
};
entries.push((
*entity,
data.panel_kind,
data.default_dock_size,
data.focus_order,
visible,
data.collapsed,
data.id.clone(),
data.title.clone(),
floating_position,
floating_size,
));
}
entries.sort_by_key(|entry| entry.3);
let window_viewport_size = primary_viewport_size;
let window_dpi_scale = primary_dpi_scale;
let mut reserved = crate::ecs::ui::resources::ReservedAreas::default();
let mut configs: Vec<crate::ecs::ui::resources::PanelDockConfig> = Vec::new();
for entry in &entries {
let (
entity,
panel_kind,
dock_size,
focus_order,
visible,
collapsed,
id,
title,
floating_position,
floating_size,
) = entry;
configs.push(crate::ecs::ui::resources::PanelDockConfig {
id: id.clone(),
title: title.clone(),
kind: *panel_kind,
focus_order: *focus_order,
size: *dock_size,
visible: *visible,
collapsed: *collapsed,
floating_position: *floating_position,
floating_size: *floating_size,
});
if !visible {
continue;
}
let (position, size) = match panel_kind {
crate::ecs::ui::components::UiPanelKind::DockedLeft => {
let h = window_viewport_size.y - reserved.top - reserved.bottom;
(
Vec2::new(reserved.left, reserved.top),
Vec2::new(dock_size * window_dpi_scale, h),
)
}
crate::ecs::ui::components::UiPanelKind::DockedRight => {
let h = window_viewport_size.y - reserved.top - reserved.bottom;
(
Vec2::new(
window_viewport_size.x - reserved.right - dock_size * window_dpi_scale,
reserved.top,
),
Vec2::new(dock_size * window_dpi_scale, h),
)
}
crate::ecs::ui::components::UiPanelKind::DockedTop => {
let w = window_viewport_size.x - reserved.left - reserved.right;
(
Vec2::new(reserved.left, reserved.top),
Vec2::new(w, dock_size * window_dpi_scale),
)
}
crate::ecs::ui::components::UiPanelKind::DockedBottom => {
let w = window_viewport_size.x - reserved.left - reserved.right;
(
Vec2::new(
reserved.left,
window_viewport_size.y - reserved.bottom - dock_size * window_dpi_scale,
),
Vec2::new(w, dock_size * window_dpi_scale),
)
}
crate::ecs::ui::components::UiPanelKind::Floating => continue,
};
if let Some(node) = world.ui.get_ui_layout_node_mut(*entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.base_layout.as_mut()
{
let current_ab = window.position.absolute.unwrap_or(Vec2::new(0.0, 0.0));
let parent_min = node.computed_rect.min - current_ab * window_dpi_scale;
window.position =
crate::ecs::ui::units::Ab((position - parent_min) / window_dpi_scale).into();
window.size = crate::ecs::ui::units::Ab(size / window_dpi_scale).into();
}
match panel_kind {
crate::ecs::ui::components::UiPanelKind::DockedLeft => {
reserved.left += dock_size * window_dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::DockedRight => {
reserved.right += dock_size * window_dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::DockedTop => {
reserved.top += dock_size * window_dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::DockedBottom => {
reserved.bottom += dock_size * window_dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::Floating => {}
}
}
world.resources.retained_ui.dock_state.primary = crate::ecs::ui::resources::WindowDockLayout {
reserved_areas: reserved,
panels: configs,
};
}
pub fn ui_layout_compute_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
let viewport_raw = world.resources.window.cached_viewport_size;
let dpi_scale = world.resources.window.cached_scale_factor;
let text_generation = world.resources.text.cache.generation;
let combined_window_dpi_signature = compute_window_dpi_signature(world);
let viewport_changed = world.resources.retained_ui.dirty.last_compute_viewport != viewport_raw;
let dpi_changed =
(combined_window_dpi_signature - world.resources.retained_ui.dirty.last_compute_dpi).abs()
> f32::EPSILON;
let text_changed = text_generation
!= world
.resources
.retained_ui
.dirty
.last_compute_text_generation;
let layout_dirty = world.resources.retained_ui.dirty.layout_dirty;
if !viewport_changed && !dpi_changed && !text_changed && !layout_dirty {
return;
}
world.resources.retained_ui.dirty.last_compute_viewport = viewport_raw;
world.resources.retained_ui.dirty.last_compute_dpi = combined_window_dpi_signature;
world
.resources
.retained_ui
.dirty
.last_compute_text_generation = text_generation;
world.resources.retained_ui.dirty.layout_dirty = false;
world.resources.retained_ui.dirty.render_dirty = true;
world.resources.retained_ui.dirty.global_version += 1;
prune_taffy(world);
let viewport_size = viewport_raw
.map(|(width, height)| Vec2::new(width as f32, height as f32))
.unwrap_or(Vec2::new(800.0, 600.0));
let root_entities: Vec<freecs::Entity> = world
.ui
.query_entities(crate::ecs::world::UI_LAYOUT_ROOT)
.collect();
let mut region_rects = std::mem::take(&mut world.resources.retained_ui.taffy_region_rects);
region_rects.clear();
let mut children_scratch: Vec<freecs::Entity> = Vec::new();
let mut all_sorted_nodes: Vec<(freecs::Entity, f32)> = Vec::new();
for root_entity in root_entities {
let (root_intrinsic_scale, default_font_size) = {
match world.ui.get_ui_layout_root(root_entity) {
Some(root) => (root.absolute_scale, root.default_font_size),
None => continue,
}
};
let (root_viewport_size, root_window_dpi) = (viewport_size, dpi_scale);
let absolute_scale = root_intrinsic_scale * root_window_dpi;
let root_rect = Rect::new(0.0, 0.0, root_viewport_size.x, root_viewport_size.y);
let root_children: &[freecs::Entity] = world
.resources
.transform_state
.children_cache
.get(&root_entity)
.map(|v| v.as_slice())
.unwrap_or(&[]);
let mut stack: Vec<StackEntry> = Vec::new();
for child in root_children.iter().rev() {
if world.ui.get_ui_layout_node(*child).is_some() {
stack.push(StackEntry {
entity: *child,
parent_rect: root_rect,
parent_depth: 0.0,
parent_font_size: default_font_size,
parent_clip_rect: None,
parent_layer: None,
flow_override_rect: None,
});
}
}
while let Some(entry) = stack.pop() {
let (
base_layout,
flow_layout,
responsive_flow,
grid_layout,
depth_mode,
node_font_size,
visible,
clip_content,
node_layer,
node_min_size,
node_max_size,
) = {
match world.ui.get_ui_layout_node(entry.entity) {
Some(node) => (
node.base_layout,
node.flow_layout,
node.responsive_flow,
node.grid_layout,
node.depth,
node.font_size,
node.visible,
node.clip_content,
node.layer,
node.min_size,
node.max_size,
),
None => continue,
}
};
if !visible {
continue;
}
let font_size = node_font_size.unwrap_or(entry.parent_font_size);
let context = UiEvalContext {
parent_width: entry.parent_rect.width(),
parent_height: entry.parent_rect.height(),
viewport_width: root_viewport_size.x,
viewport_height: root_viewport_size.y,
font_size,
absolute_scale,
};
let computed_rect = if let Some(flow_rect) = entry.flow_override_rect {
flow_rect
} else if let Some(base_layout) = &base_layout {
compute_layout_rect(base_layout, &context, &entry.parent_rect)
} else {
entry.parent_rect
};
let computed_rect = if node_min_size.is_some() || node_max_size.is_some() {
let mut width = computed_rect.width();
let mut height = computed_rect.height();
if let Some(min) = node_min_size {
width = width.max(min.x);
height = height.max(min.y);
}
if let Some(max) = node_max_size {
width = width.min(max.x);
height = height.min(max.y);
}
Rect::from_min_max(
computed_rect.min,
Vec2::new(computed_rect.min.x + width, computed_rect.min.y + height),
)
} else {
computed_rect
};
let depth = match depth_mode {
UiDepthMode::Add(delta) => entry.parent_depth + delta,
UiDepthMode::Set(absolute) => absolute,
};
let effective_layer = node_layer.or(entry.parent_layer);
let is_popup_layer = matches!(
effective_layer,
Some(UiLayer::Popups) | Some(UiLayer::Tooltips)
);
let (node_clip_rect, child_clip_rect) = if is_popup_layer {
(None, None)
} else if clip_content {
let clipped = match &entry.parent_clip_rect {
Some(parent_clip) => {
Some(computed_rect.intersect(parent_clip).unwrap_or(*parent_clip))
}
None => Some(computed_rect),
};
(clipped, clipped)
} else {
(entry.parent_clip_rect, entry.parent_clip_rect)
};
let computed_rect = if let Some(node) = world.ui.get_ui_layout_node(entry.entity)
&& let Some(animation) = &node.animation
{
apply_animation_to_rect(computed_rect, animation)
} else {
computed_rect
};
let prev_rect_data = world
.ui
.get_ui_layout_node(entry.entity)
.map(|n| (n.computed_rect.min, n.computed_rect.max));
let new_rect_data = (computed_rect.min, computed_rect.max);
if let Some(node) = world.ui.get_ui_layout_node_mut(entry.entity) {
node.computed_rect = computed_rect;
node.computed_depth = depth;
node.computed_font_scale = root_intrinsic_scale;
node.computed_clip_rect = node_clip_rect;
node.computed_layer = effective_layer;
}
if prev_rect_data != Some(new_rect_data) {
let global = world.resources.retained_ui.dirty.global_version;
world
.resources
.retained_ui
.dirty
.node_versions
.insert(entry.entity, global);
}
all_sorted_nodes.push((entry.entity, depth));
children_scratch.clear();
if let Some(cached_children) = world
.resources
.transform_state
.children_cache
.get(&entry.entity)
{
children_scratch.extend_from_slice(cached_children);
}
let scroll_offset = world
.ui
.get_ui_layout_node(entry.entity)
.map(|n| n.scroll_offset * root_window_dpi)
.unwrap_or(Vec2::new(0.0, 0.0));
let scroll_shift = scroll_offset != Vec2::new(0.0, 0.0);
let scrolled_parent_rect = if scroll_shift {
Rect::from_min_max(
computed_rect.min - scroll_offset,
computed_rect.max - scroll_offset,
)
} else {
computed_rect
};
if flow_layout.is_some() || grid_layout.is_some() {
let is_region_root = entry.flow_override_rect.is_none();
let responsive_repass = flow_layout.is_some() && responsive_flow.is_some();
let child_rects: Vec<(freecs::Entity, Option<Rect>)> =
if is_region_root || responsive_repass {
let parent_style = if let Some(flow) = &flow_layout {
let effective_flow = if let Some(resp) = responsive_flow
&& computed_rect.width() < resp.max_width * absolute_scale
{
FlowLayout {
direction: resp.direction,
..*flow
}
} else {
*flow
};
crate::ecs::ui::taffy_layout::flow_to_style(
world.ui.get_ui_layout_node(entry.entity).unwrap(),
&effective_flow,
scrolled_parent_rect.size(),
absolute_scale,
)
} else {
crate::ecs::ui::taffy_layout::grid_to_style(
world.ui.get_ui_layout_node(entry.entity).unwrap(),
grid_layout.as_ref().unwrap(),
scrolled_parent_rect.size(),
absolute_scale,
)
};
compute_taffy_children(
world,
entry.entity,
parent_style,
&children_scratch,
&scrolled_parent_rect,
absolute_scale,
&mut region_rects,
)
} else {
read_region_children(world, &children_scratch, ®ion_rects)
};
for (child, child_rect) in child_rects.iter().rev() {
stack.push(StackEntry {
entity: *child,
parent_rect: scrolled_parent_rect,
parent_depth: depth,
parent_font_size: font_size,
parent_clip_rect: child_clip_rect,
parent_layer: effective_layer,
flow_override_rect: *child_rect,
});
}
} else {
for child in children_scratch.iter().rev() {
if world.ui.get_ui_layout_node(*child).is_some() {
stack.push(StackEntry {
entity: *child,
parent_rect: scrolled_parent_rect,
parent_depth: depth,
parent_font_size: font_size,
parent_clip_rect: child_clip_rect,
parent_layer: effective_layer,
flow_override_rect: None,
});
}
}
}
}
}
world.resources.retained_ui.taffy_region_rects = region_rects;
all_sorted_nodes.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let primary_viewport = (viewport_size.x, viewport_size.y);
let primary_grid =
crate::ecs::ui::picking::build_picking_grid(world, &all_sorted_nodes, primary_viewport);
world.resources.retained_ui.z_sorted_nodes = all_sorted_nodes;
world.resources.retained_ui.picking_grid = Some(primary_grid);
}
fn compute_taffy_children(
world: &mut World,
parent_entity: freecs::Entity,
parent_style: taffy::Style,
children: &[freecs::Entity],
parent_rect: &Rect,
dpi_scale: f32,
region_rects: &mut std::collections::HashMap<freecs::Entity, Rect>,
) -> Vec<(freecs::Entity, Option<Rect>)> {
use crate::ecs::ui::taffy_layout::available_size_for;
let parent_size = parent_rect.size();
world
.resources
.retained_ui
.taffy
.ensure_leaf(parent_entity, parent_style);
let mut child_node_ids: Vec<taffy::NodeId> = Vec::with_capacity(children.len());
let mut flowed_children: Vec<freecs::Entity> = Vec::with_capacity(children.len());
let mut absolute_children: Vec<freecs::Entity> = Vec::new();
for child in children {
let Some(child_node) = world.ui.get_ui_layout_node(*child) else {
continue;
};
if !child_node.visible {
continue;
}
if child_node.base_layout.is_some() {
absolute_children.push(*child);
continue;
}
let child_node_id = build_taffy_subtree(world, *child, parent_size, dpi_scale);
child_node_ids.push(child_node_id);
flowed_children.push(*child);
}
world
.resources
.retained_ui
.taffy
.set_children(parent_entity, &child_node_ids);
let resources = &mut world.resources;
resources.retained_ui.taffy.compute_layout(
parent_entity,
available_size_for(parent_size),
&mut resources.text.font_engine,
);
for child in &flowed_children {
collect_region_rects(world, *child, parent_rect.min, dpi_scale, region_rects);
}
let mut rects = Vec::with_capacity(flowed_children.len() + absolute_children.len());
for child in absolute_children {
rects.push((child, None));
}
for child in flowed_children {
rects.push((child, region_rects.get(&child).copied()));
}
rects
}
fn collect_region_rects(
world: &World,
entity: freecs::Entity,
parent_abs_min: Vec2,
dpi_scale: f32,
region_rects: &mut std::collections::HashMap<freecs::Entity, Rect>,
) {
let Some((location, size)) = world.resources.retained_ui.taffy.rect_for(entity) else {
return;
};
let abs_min = parent_abs_min + location;
region_rects.insert(entity, Rect::from_min_max(abs_min, abs_min + size));
let Some(node) = world.ui.get_ui_layout_node(entity) else {
return;
};
if node.flow_layout.is_none() && node.grid_layout.is_none() {
return;
}
let child_origin = abs_min - node.scroll_offset * dpi_scale;
if let Some(children) = world.resources.transform_state.children_cache.get(&entity) {
for &child in children {
let Some(child_node) = world.ui.get_ui_layout_node(child) else {
continue;
};
if !child_node.visible {
continue;
}
if child_node.base_layout.is_some() {
continue;
}
collect_region_rects(world, child, child_origin, dpi_scale, region_rects);
}
}
}
fn read_region_children(
world: &World,
children: &[freecs::Entity],
region_rects: &std::collections::HashMap<freecs::Entity, Rect>,
) -> Vec<(freecs::Entity, Option<Rect>)> {
let mut rects = Vec::with_capacity(children.len());
for child in children {
let Some(child_node) = world.ui.get_ui_layout_node(*child) else {
continue;
};
if !child_node.visible {
continue;
}
if child_node.base_layout.is_some() {
rects.push((*child, None));
} else {
rects.push((*child, region_rects.get(child).copied()));
}
}
rects
}
fn build_taffy_subtree(
world: &mut World,
entity: freecs::Entity,
parent_size: Vec2,
dpi_scale: f32,
) -> taffy::NodeId {
use crate::ecs::ui::taffy_layout::child_to_style;
let (mut style, is_container, font_size) = match world.ui.get_ui_layout_node(entity) {
Some(node) => (
child_to_style(node, parent_size, dpi_scale),
node.flow_layout.is_some() || node.grid_layout.is_some(),
node.font_size.unwrap_or(16.0),
),
None => (taffy::Style::default(), false, 16.0),
};
if is_container {
apply_absolute_child_floor(&mut style, world, entity, parent_size, font_size, dpi_scale);
}
let node_id = world.resources.retained_ui.taffy.ensure_leaf(entity, style);
if !is_container {
let measured = measure_leaf_mut(world, entity, dpi_scale);
world
.resources
.retained_ui
.taffy
.set_measured(entity, measured);
update_wrap_text_cache(world, entity, dpi_scale);
return node_id;
}
let child_count = world
.resources
.transform_state
.children_cache
.get(&entity)
.map(|children| children.len())
.unwrap_or(0);
let entity_size = world
.resources
.retained_ui
.taffy
.rect_for(entity)
.map(|(_, size)| size)
.unwrap_or(parent_size);
let mut child_node_ids: Vec<taffy::NodeId> = Vec::new();
for index in 0..child_count {
let Some(child) = world
.resources
.transform_state
.children_cache
.get(&entity)
.and_then(|children| children.get(index).copied())
else {
continue;
};
let Some(child_node) = world.ui.get_ui_layout_node(child) else {
continue;
};
if !child_node.visible {
continue;
}
if child_node.base_layout.is_some() {
continue;
}
let cnode = build_taffy_subtree(world, child, entity_size, dpi_scale);
child_node_ids.push(cnode);
}
world
.resources
.retained_ui
.taffy
.set_children(entity, &child_node_ids);
node_id
}
fn apply_absolute_child_floor(
style: &mut taffy::Style,
world: &World,
container: freecs::Entity,
parent_size: Vec2,
font_size: f32,
dpi_scale: f32,
) {
use taffy::Dimension;
let context = UiEvalContext {
parent_width: 0.0,
parent_height: 0.0,
viewport_width: parent_size.x,
viewport_height: parent_size.y,
font_size,
absolute_scale: dpi_scale,
};
let zero_parent = Rect::new(0.0, 0.0, 0.0, 0.0);
let mut extent = Vec2::new(0.0, 0.0);
if let Some(children) = world
.resources
.transform_state
.children_cache
.get(&container)
{
for &child in children {
let Some(node) = world.ui.get_ui_layout_node(child) else {
continue;
};
if !node.visible {
continue;
}
let Some(base_layout) = node.base_layout.as_ref() else {
continue;
};
let rect = compute_layout_rect(base_layout, &context, &zero_parent);
extent.x = extent.x.max(rect.max.x);
extent.y = extent.y.max(rect.max.y);
}
}
if extent.x > 0.0 && matches!(style.size.width, Dimension::Auto) {
style.min_size.width = dimension_floor(style.min_size.width, extent.x);
}
if extent.y > 0.0 && matches!(style.size.height, Dimension::Auto) {
style.min_size.height = dimension_floor(style.min_size.height, extent.y);
}
}
fn dimension_floor(dimension: taffy::Dimension, floor: f32) -> taffy::Dimension {
use taffy::prelude::FromLength;
match dimension {
taffy::Dimension::Length(value) => taffy::Dimension::from_length(value.max(floor)),
_ => taffy::Dimension::from_length(floor),
}
}
fn update_wrap_text_cache(world: &mut World, entity: freecs::Entity, dpi_scale: f32) {
use crate::ecs::ui::components::{TextOverflow, UiNodeContent};
let info = (|| {
let content = world.ui.get_ui_node_content(entity)?;
let layout_node = world.ui.get_ui_layout_node(entity)?;
let UiNodeContent::Text {
text_slot,
font_size_override,
overflow,
..
} = content
else {
return None;
};
if !matches!(overflow, TextOverflow::Wrap) {
return None;
}
let base_font = layout_node.font_size.unwrap_or(16.0);
let font_size = font_size_override.unwrap_or(base_font) * dpi_scale;
let text = world.resources.text.cache.get_text(*text_slot)?.to_string();
Some(crate::ecs::ui::taffy_layout::WrapTextMeasure { text, font_size })
})();
match info {
Some(info) => world
.resources
.retained_ui
.taffy
.set_wrap_text(entity, info),
None => world.resources.retained_ui.taffy.clear_wrap_text(entity),
}
}
fn measure_leaf_mut(world: &mut World, entity: freecs::Entity, dpi_scale: f32) -> taffy::Size<f32> {
use crate::ecs::ui::components::UiNodeContent;
let (text_slot, font_size_override, base_font) = {
let Some(content) = world.ui.get_ui_node_content(entity) else {
return taffy::Size::ZERO;
};
let Some(node) = world.ui.get_ui_layout_node(entity) else {
return taffy::Size::ZERO;
};
let UiNodeContent::Text {
text_slot,
font_size_override,
..
} = content
else {
return taffy::Size::ZERO;
};
(
*text_slot,
*font_size_override,
node.font_size.unwrap_or(16.0),
)
};
let font_size = font_size_override.unwrap_or(base_font) * dpi_scale;
let text = match world.resources.text.cache.get_text(text_slot) {
Some(text) => text.to_string(),
None => return taffy::Size::ZERO,
};
let width = crate::ecs::ui::widget_systems::measure_text_width(
&mut world.resources.text.font_engine,
&text,
font_size,
);
let line_height = font_size * 1.2;
taffy::Size {
width,
height: line_height,
}
}
fn prune_taffy(world: &mut World) {
let mut live: std::collections::HashSet<freecs::Entity> = std::collections::HashSet::new();
let mut stack: Vec<freecs::Entity> = world
.ui
.query_entities(crate::ecs::world::UI_LAYOUT_ROOT)
.collect();
while let Some(entity) = stack.pop() {
if !live.insert(entity) {
continue;
}
if let Some(children) = world.resources.transform_state.children_cache.get(&entity) {
stack.extend(children.iter().copied());
}
}
let stale: Vec<freecs::Entity> = world
.resources
.retained_ui
.taffy
.entity_to_node
.keys()
.copied()
.filter(|entity| !live.contains(entity))
.collect();
for entity in stale {
world.resources.retained_ui.taffy.release(entity);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ecs::transform::systems::validate_and_rebuild_children_cache;
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::layout_types::FlowDirection;
use crate::ecs::ui::types::Anchor;
use crate::ecs::ui::units::{Ab, Rl};
fn setup_world(width: u32, height: u32) -> World {
let mut world = World::default();
world.resources.window.cached_viewport_size = Some((width, height));
world.resources.window.cached_scale_factor = 1.0;
world.resources.retained_ui.enabled = true;
world
}
fn compute_layout(world: &mut World) {
validate_and_rebuild_children_cache(world);
world.resources.retained_ui.dirty.layout_dirty = true;
ui_layout_compute_system(world);
}
fn rect_of(world: &World, entity: freecs::Entity) -> Rect {
world
.ui
.get_ui_layout_node(entity)
.expect("layout node present")
.computed_rect
}
fn assert_rect(actual: Rect, min: Vec2, max: Vec2) {
let epsilon = 0.05;
let close = (actual.min.x - min.x).abs() < epsilon
&& (actual.min.y - min.y).abs() < epsilon
&& (actual.max.x - max.x).abs() < epsilon
&& (actual.max.y - max.y).abs() < epsilon;
assert!(
close,
"expected min {:?} max {:?}, got min {:?} max {:?}",
min, max, actual.min, actual.max
);
}
#[test]
fn vertical_flow_stacks_children_with_gap() {
let mut world = setup_world(1000, 800);
let container;
let first;
let second;
{
let mut tree = UiTreeBuilder::new(&mut world);
container = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 10.0)
.entity();
(first, second) = tree.in_parent(container, |tree| {
let first = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
.entity();
let second = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
.entity();
(first, second)
});
tree.finish();
}
compute_layout(&mut world);
assert_rect(
rect_of(&world, container),
Vec2::new(0.0, 0.0),
Vec2::new(1000.0, 800.0),
);
assert_rect(
rect_of(&world, first),
Vec2::new(0.0, 0.0),
Vec2::new(1000.0, 100.0),
);
assert_rect(
rect_of(&world, second),
Vec2::new(0.0, 110.0),
Vec2::new(1000.0, 210.0),
);
}
#[test]
fn absolute_scale_shrinks_flow_children_and_gap() {
let mut world = setup_world(1000, 800);
let left;
let right;
{
let mut tree = UiTreeBuilder::new(&mut world).with_absolute_scale(0.5);
let container = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Horizontal, 0.0, 20.0)
.entity();
(left, right) = tree.in_parent(container, |tree| {
let left = tree
.add_node()
.flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.entity();
let right = tree
.add_node()
.flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.entity();
(left, right)
});
tree.finish();
}
compute_layout(&mut world);
assert_rect(
rect_of(&world, left),
Vec2::new(0.0, 0.0),
Vec2::new(100.0, 800.0),
);
assert_rect(
rect_of(&world, right),
Vec2::new(110.0, 0.0),
Vec2::new(210.0, 800.0),
);
}
#[test]
fn nested_flow_region_resolves_grandchildren() {
let mut world = setup_world(1000, 800);
let outer;
let inner;
let left;
let right;
{
let mut tree = UiTreeBuilder::new(&mut world);
outer = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
(inner, left, right) = tree.in_parent(outer, |tree| {
let inner = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 200.0)))
.flow(FlowDirection::Horizontal, 0.0, 20.0)
.entity();
let (left, right) = tree.in_parent(inner, |tree| {
let left = tree
.add_node()
.flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.entity();
let right = tree
.add_node()
.flow_child(Ab(Vec2::new(200.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.entity();
(left, right)
});
(inner, left, right)
});
tree.finish();
}
compute_layout(&mut world);
assert_rect(
rect_of(&world, inner),
Vec2::new(0.0, 0.0),
Vec2::new(1000.0, 200.0),
);
assert_rect(
rect_of(&world, left),
Vec2::new(0.0, 0.0),
Vec2::new(200.0, 200.0),
);
assert_rect(
rect_of(&world, right),
Vec2::new(220.0, 0.0),
Vec2::new(420.0, 200.0),
);
}
#[test]
fn scroll_offset_shifts_flow_children() {
let mut world = setup_world(1000, 800);
let container;
let first;
let second;
{
let mut tree = UiTreeBuilder::new(&mut world);
container = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 10.0)
.entity();
(first, second) = tree.in_parent(container, |tree| {
let first = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
.entity();
let second = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 100.0)))
.entity();
(first, second)
});
tree.finish();
}
if let Some(node) = world.ui.get_ui_layout_node_mut(container) {
node.scroll_offset = Vec2::new(0.0, 50.0);
}
compute_layout(&mut world);
assert_rect(
rect_of(&world, first),
Vec2::new(0.0, -50.0),
Vec2::new(1000.0, 50.0),
);
assert_rect(
rect_of(&world, second),
Vec2::new(0.0, 60.0),
Vec2::new(1000.0, 160.0),
);
}
#[test]
fn auto_container_grows_to_fit_absolute_child() {
let mut world = setup_world(1000, 800);
let inner;
{
let mut tree = UiTreeBuilder::new(&mut world);
let outer = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
inner = tree.in_parent(outer, |tree| {
let inner = tree
.add_node()
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
tree.in_parent(inner, |tree| {
tree.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(300.0, 150.0)),
Anchor::TopLeft,
)
.entity();
});
inner
});
tree.finish();
}
compute_layout(&mut world);
assert_rect(
rect_of(&world, inner),
Vec2::new(0.0, 0.0),
Vec2::new(300.0, 150.0),
);
}
#[test]
fn property_grid_rows_stack_without_overlap() {
let mut world = setup_world(1000, 800);
let grid;
{
let mut tree = UiTreeBuilder::new(&mut world);
let container = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
grid = tree.in_parent(container, |tree| {
let grid = tree.add_property_grid(96.0);
let section = tree.add_property_section(grid, "Transform");
tree.add_property_drag_value(grid, section, "X", -100.0, 100.0, 1.0);
tree.add_property_drag_value(grid, section, "Y", -100.0, 100.0, 2.0);
tree.add_property_drag_value(grid, section, "Z", -100.0, 100.0, 3.0);
grid
});
tree.finish();
}
compute_layout(&mut world);
let rows = world
.ui
.get_ui_property_grid(grid)
.expect("property grid data")
.row_entities
.clone();
assert_eq!(rows.len(), 3, "three drag-value rows expected");
let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
for rect in &rects {
let height = rect.height();
assert!(
height > 1.0 && height < 80.0,
"a property row should be a single line tall, got {height}"
);
}
rects.sort_by(|a, b| {
a.min
.y
.partial_cmp(&b.min.y)
.unwrap_or(std::cmp::Ordering::Equal)
});
for pair in rects.windows(2) {
assert!(
pair[1].min.y >= pair[0].max.y - 0.5,
"rows overlap: {:?} then {:?}",
pair[0],
pair[1]
);
}
}
#[test]
fn docked_panel_scroll_grid_diagnostic() {
let mut world = setup_world(1920, 1080);
world.resources.schedules.retained_ui =
crate::schedule::build_default_retained_ui_schedule();
let grid;
let panel;
{
let mut tree = UiTreeBuilder::new(&mut world);
panel = tree.add_docked_panel_right("inspector", "Inspector", 320.0);
let content = tree
.world_mut()
.ui
.get_ui_panel(panel)
.map(|data| data.content_entity)
.expect("panel content");
grid = tree.in_parent(content, |tree| {
let scroll = tree.add_scroll_area_fill(0.0, 0.0);
let body = tree
.world_mut()
.ui
.get_ui_scroll_area(scroll)
.map(|data| data.content_entity)
.unwrap_or(scroll);
tree.in_parent(body, |tree| {
let grid = tree.add_property_grid(96.0);
let section = tree.add_property_section(grid, "Transform");
tree.add_property_drag_value(grid, section, "X", -100.0, 100.0, 1.0);
tree.add_property_drag_value(grid, section, "Y", -100.0, 100.0, 2.0);
tree.add_property_drag_value(grid, section, "Z", -100.0, 100.0, 3.0);
grid
})
});
tree.finish();
}
for _ in 0..2 {
validate_and_rebuild_children_cache(&mut world);
crate::schedule::run_retained_ui_schedule(&mut world);
}
let panel_rect = rect_of(&world, panel);
assert!(
(panel_rect.min.x - 1600.0).abs() < 1.0,
"right-docked panel should start at viewport width minus its size, got {}",
panel_rect.min.x
);
let rows = world
.ui
.get_ui_property_grid(grid)
.expect("property grid data")
.row_entities
.clone();
let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
for rect in &rects {
assert!(
rect.height() > 1.0 && rect.height() < 80.0,
"row inside the docked panel should be one line tall, got {}",
rect.height()
);
assert!(
rect.min.x >= panel_rect.min.x && rect.max.x <= panel_rect.max.x + 0.5,
"row should sit within the panel bounds"
);
}
rects.sort_by(|a, b| {
a.min
.y
.partial_cmp(&b.min.y)
.unwrap_or(std::cmp::Ordering::Equal)
});
for pair in rects.windows(2) {
assert!(
pair[1].min.y >= pair[0].max.y - 0.5,
"rows overlap inside the docked panel: {:?} then {:?}",
pair[0],
pair[1]
);
}
}
#[test]
fn property_grid_rows_stack_at_high_dpi() {
let mut world = setup_world(1280, 1600);
world.resources.window.cached_scale_factor = 2.0;
let grid;
{
let mut tree = UiTreeBuilder::new(&mut world);
let container = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
grid = tree.in_parent(container, |tree| {
let grid = tree.add_property_grid(96.0);
let section = tree.add_property_section(grid, "Transform");
tree.add_property_drag_value(grid, section, "X", -100.0, 100.0, 1.0);
tree.add_property_drag_value(grid, section, "Y", -100.0, 100.0, 2.0);
tree.add_property_drag_value(grid, section, "Z", -100.0, 100.0, 3.0);
grid
});
tree.finish();
}
compute_layout(&mut world);
let rows = world
.ui
.get_ui_property_grid(grid)
.expect("property grid data")
.row_entities
.clone();
let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
for rect in &rects {
let height = rect.height();
assert!(
height > 1.0 && height < 160.0,
"at dpi 2 a property row should be one scaled line, got {height}"
);
}
rects.sort_by(|a, b| {
a.min
.y
.partial_cmp(&b.min.y)
.unwrap_or(std::cmp::Ordering::Equal)
});
for pair in rects.windows(2) {
assert!(
pair[1].min.y >= pair[0].max.y - 0.5,
"rows overlap at dpi 2: {:?} then {:?}",
pair[0],
pair[1]
);
}
}
#[test]
fn property_grid_mixed_widgets_stack_without_overlap() {
let mut world = setup_world(1000, 800);
let grid;
{
let mut tree = UiTreeBuilder::new(&mut world);
let container = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
grid = tree.in_parent(container, |tree| {
let grid = tree.add_property_grid(96.0);
let section = tree.add_property_section(grid, "Mixed");
tree.add_property_drag_value(grid, section, "Drag", -100.0, 100.0, 1.0);
tree.add_property_checkbox(grid, section, "Check", true);
tree.add_property_dropdown(grid, section, "Pick", &["A", "B", "C"], 0);
tree.add_property_text_input(grid, section, "Text", "value");
grid
});
tree.finish();
}
compute_layout(&mut world);
let rows = world
.ui
.get_ui_property_grid(grid)
.expect("property grid data")
.row_entities
.clone();
assert_eq!(rows.len(), 4, "four widget rows expected");
let mut rects: Vec<Rect> = rows.iter().map(|row| rect_of(&world, *row)).collect();
for rect in &rects {
let height = rect.height();
assert!(
height > 1.0 && height < 80.0,
"a property row should be a single line tall, got {height}"
);
assert!(
rect.width() > 200.0,
"a property row should span the grid width, got {}",
rect.width()
);
}
rects.sort_by(|a, b| {
a.min
.y
.partial_cmp(&b.min.y)
.unwrap_or(std::cmp::Ordering::Equal)
});
for pair in rects.windows(2) {
assert!(
pair[1].min.y >= pair[0].max.y - 0.5,
"rows overlap: {:?} then {:?}",
pair[0],
pair[1]
);
}
}
#[test]
fn explicit_size_container_ignores_absolute_child() {
let mut world = setup_world(1000, 800);
let inner;
{
let mut tree = UiTreeBuilder::new(&mut world);
let outer = tree
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
inner = tree.in_parent(outer, |tree| {
let inner = tree
.add_node()
.flow_child(Ab(Vec2::new(120.0, 40.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
tree.in_parent(inner, |tree| {
tree.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(300.0, 150.0)),
Anchor::TopLeft,
)
.entity();
});
inner
});
tree.finish();
}
compute_layout(&mut world);
assert_rect(
rect_of(&world, inner),
Vec2::new(0.0, 0.0),
Vec2::new(120.0, 40.0),
);
}
}