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;
rebuild_layout_graph(world);
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 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_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));
let children: Vec<freecs::Entity> = world
.resources
.transform_state
.children_cache
.get(&entry.entity)
.cloned()
.unwrap_or_default();
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 let Some(flow) = &flow_layout {
let effective_flow = if let Some(resp) = responsive_flow
&& computed_rect.width() < resp.max_width * root_window_dpi
{
FlowLayout {
direction: resp.direction,
..*flow
}
} else {
*flow
};
let parent_style = crate::ecs::ui::taffy_layout::flow_to_style(
world.ui.get_ui_layout_node(entry.entity).unwrap(),
&effective_flow,
scrolled_parent_rect.size(),
root_window_dpi,
);
let flow_rects = compute_taffy_children(
world,
entry.entity,
parent_style,
&children,
&scrolled_parent_rect,
root_window_dpi,
);
for (child, child_rect) in flow_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 if let Some(grid) = &grid_layout {
let parent_style = crate::ecs::ui::taffy_layout::grid_to_style(
world.ui.get_ui_layout_node(entry.entity).unwrap(),
grid,
scrolled_parent_rect.size(),
root_window_dpi,
);
let grid_rects = compute_taffy_children(
world,
entry.entity,
parent_style,
&children,
&scrolled_parent_rect,
root_window_dpi,
);
for (child, child_rect) in grid_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.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,
});
}
}
}
}
}
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,
) -> 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,
);
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 {
if let Some((position, size)) = world.resources.retained_ui.taffy.rect_for(child) {
let rect = Rect::from_min_max(
parent_rect.min + position,
parent_rect.min + position + size,
);
rects.push((child, Some(rect)));
}
}
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 style = match world.ui.get_ui_layout_node(entity) {
Some(node) => child_to_style(node, parent_size, dpi_scale),
None => taffy::Style::default(),
};
let node_id = world.resources.retained_ui.taffy.ensure_leaf(entity, style);
let is_container = world
.ui
.get_ui_layout_node(entity)
.map(|node| node.flow_layout.is_some() || node.grid_layout.is_some())
.unwrap_or(false);
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 children: Vec<freecs::Entity> = world
.resources
.transform_state
.children_cache
.get(&entity)
.cloned()
.unwrap_or_default();
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 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;
}
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 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 rebuild_layout_graph(world: &mut World) {
let roots: Vec<freecs::Entity> = world
.ui
.query_entities(crate::ecs::world::UI_LAYOUT_ROOT)
.collect();
let resources = &mut world.resources;
let children_cache = &resources.transform_state.children_cache;
let graph = &mut resources.retained_ui.layout_graph;
graph.rebuild(&roots, |entity| children_cache.get(&entity).cloned());
}
fn prune_taffy(world: &mut World) {
let live: std::collections::HashSet<freecs::Entity> = world
.resources
.retained_ui
.layout_graph
.nodes
.iter()
.copied()
.collect();
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);
}
}