use nalgebra_glm::Vec2;
use crate::ecs::ui::components::{AutoSizeMode, UiDepthMode, UiNodeContent};
use crate::ecs::ui::layout_types::{
FlowAlignment, FlowDirection, FlowLayout, GridLayout, compute_layout_rect,
};
use crate::ecs::ui::state::{STATE_COUNT, UiBase, UiStateTrait};
use crate::ecs::ui::types::Rect;
use crate::ecs::ui::units::UiEvalContext;
use crate::ecs::ui::widget_systems::measure_text_width;
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>,
}
pub fn ui_docked_panel_layout_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
let 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 dpi_scale = world.resources.window.cached_scale_factor;
world.resources.retained_ui.reserved_areas =
crate::ecs::ui::resources::ReservedAreas::default();
let entities: Vec<freecs::Entity> = world
.ui
.query_entities(crate::ecs::world::UI_WIDGET_STATE)
.collect();
let mut docked_panels: Vec<(
freecs::Entity,
crate::ecs::ui::components::UiPanelKind,
f32,
i32,
)> = Vec::new();
for entity in &entities {
if let Some(crate::ecs::ui::components::UiWidgetState::Panel(data)) =
world.ui.get_ui_widget_state(*entity)
&& data.panel_kind != crate::ecs::ui::components::UiPanelKind::Floating
{
docked_panels.push((
*entity,
data.panel_kind,
data.default_dock_size,
data.focus_order,
));
}
}
docked_panels.sort_by_key(|(_, _, _, order)| *order);
for (entity, panel_kind, dock_size, _) in &docked_panels {
let reserved = &world.resources.retained_ui.reserved_areas;
let (position, size, resize_edge_kind) = match panel_kind {
crate::ecs::ui::components::UiPanelKind::DockedLeft => {
let h = viewport_size.y - reserved.top - reserved.bottom;
(
Vec2::new(reserved.left, reserved.top),
Vec2::new(dock_size * dpi_scale, h),
Some(crate::ecs::ui::components::ResizeEdge::Right),
)
}
crate::ecs::ui::components::UiPanelKind::DockedRight => {
let h = viewport_size.y - reserved.top - reserved.bottom;
(
Vec2::new(
viewport_size.x - reserved.right - dock_size * dpi_scale,
reserved.top,
),
Vec2::new(dock_size * dpi_scale, h),
Some(crate::ecs::ui::components::ResizeEdge::Left),
)
}
crate::ecs::ui::components::UiPanelKind::DockedTop => {
let w = viewport_size.x - reserved.left - reserved.right;
(
Vec2::new(reserved.left, reserved.top),
Vec2::new(w, dock_size * dpi_scale),
Some(crate::ecs::ui::components::ResizeEdge::Bottom),
)
}
crate::ecs::ui::components::UiPanelKind::DockedBottom => {
let w = viewport_size.x - reserved.left - reserved.right;
(
Vec2::new(
reserved.left,
viewport_size.y - reserved.bottom - dock_size * dpi_scale,
),
Vec2::new(w, dock_size * dpi_scale),
Some(crate::ecs::ui::components::ResizeEdge::Top),
)
}
crate::ecs::ui::components::UiPanelKind::Floating => continue,
};
let _ = resize_edge_kind;
if let Some(node) = world.ui.get_ui_layout_node_mut(*entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.layouts[UiBase::INDEX].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 * dpi_scale;
window.position = crate::ecs::ui::units::Ab((position - parent_min) / dpi_scale).into();
window.size = crate::ecs::ui::units::Ab(size / dpi_scale).into();
}
match panel_kind {
crate::ecs::ui::components::UiPanelKind::DockedLeft => {
world.resources.retained_ui.reserved_areas.left += dock_size * dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::DockedRight => {
world.resources.retained_ui.reserved_areas.right += dock_size * dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::DockedTop => {
world.resources.retained_ui.reserved_areas.top += dock_size * dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::DockedBottom => {
world.resources.retained_ui.reserved_areas.bottom += dock_size * dpi_scale;
}
crate::ecs::ui::components::UiPanelKind::Floating => {}
}
}
}
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 viewport_changed = world.resources.retained_ui.last_compute_viewport != viewport_raw;
let dpi_changed =
(dpi_scale - world.resources.retained_ui.last_compute_dpi).abs() > f32::EPSILON;
let text_changed = text_generation != world.resources.retained_ui.last_compute_text_generation;
let layout_dirty = world.resources.retained_ui.layout_dirty;
if !viewport_changed && !dpi_changed && !text_changed && !layout_dirty {
world.resources.retained_ui.render_dirty = false;
return;
}
world.resources.retained_ui.last_compute_viewport = viewport_raw;
world.resources.retained_ui.last_compute_dpi = dpi_scale;
world.resources.retained_ui.last_compute_text_generation = text_generation;
world.resources.retained_ui.layout_dirty = false;
world.resources.retained_ui.render_dirty = true;
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();
let mut secondary_sorted: std::collections::HashMap<usize, Vec<(freecs::Entity, f32)>> =
std::collections::HashMap::new();
for root_entity in root_entities {
let (absolute_scale, default_font_size, target_window) = {
match world.ui.get_ui_layout_root(root_entity) {
Some(root) => (
root.absolute_scale * dpi_scale,
root.default_font_size,
root.target_window,
),
None => continue,
}
};
let root_viewport_size = if let Some(window_index) = target_window {
world
.resources
.secondary_windows
.states
.iter()
.find(|s| s.index == window_index)
.map(|s| Vec2::new(s.size.0 as f32, s.size.1 as f32))
.unwrap_or(viewport_size)
} else {
viewport_size
};
let flow_ctx = FlowContext {
absolute_scale,
viewport_size: root_viewport_size,
dpi_scale,
};
let root_rect = Rect::new(0.0, 0.0, root_viewport_size.x, root_viewport_size.y);
let root_children: Vec<freecs::Entity> = world
.resources
.children_cache
.get(&root_entity)
.map(|v| v.to_vec())
.unwrap_or_default();
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 (
layouts,
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.layouts.clone(),
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 has_non_base_layouts = layouts[1..].iter().any(Option::is_some);
let computed_rect = if let Some(flow_rect) = entry.flow_override_rect {
flow_rect
} else if !has_non_base_layouts {
if let Some(base_layout) = &layouts[UiBase::INDEX] {
compute_layout_rect(base_layout, &context, &entry.parent_rect)
} else {
entry.parent_rect
}
} else {
let weights = world
.ui
.get_ui_state_weights(entry.entity)
.map(|state_weights| state_weights.weights.clone())
.unwrap_or_else(|| vec![0.0; STATE_COUNT]);
let total_weight: f32 = weights.iter().sum();
if total_weight < f32::EPSILON {
if let Some(base_layout) = &layouts[UiBase::INDEX] {
compute_layout_rect(base_layout, &context, &entry.parent_rect)
} else {
entry.parent_rect
}
} else {
let mut blended_min = Vec2::new(0.0, 0.0);
let mut blended_max = Vec2::new(0.0, 0.0);
let blend_count = weights.len().min(layouts.len());
for (index, weight) in weights[..blend_count].iter().enumerate() {
let layout = layouts[index].as_ref().or(layouts[UiBase::INDEX].as_ref());
if let Some(layout) = layout {
let rect = compute_layout_rect(layout, &context, &entry.parent_rect);
let normalized = *weight / total_weight;
blended_min += rect.min * normalized;
blended_max += rect.max * normalized;
}
}
Rect::from_min_max(blended_min, blended_max)
}
};
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) => computed_rect.intersect(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
};
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 let Some(window_index) = target_window {
secondary_sorted
.entry(window_index)
.or_default()
.push((entry.entity, depth));
} else {
all_sorted_nodes.push((entry.entity, depth));
}
let children: Vec<freecs::Entity> = world
.resources
.children_cache
.get(&entry.entity)
.map(|v| v.to_vec())
.unwrap_or_default();
if let Some(flow) = &flow_layout {
let effective_flow = if let Some(resp) = responsive_flow
&& computed_rect.width() < resp.max_width * dpi_scale
{
FlowLayout {
direction: resp.direction,
..*flow
}
} else {
*flow
};
let flow_rects = compute_flow_children(
world,
&children,
&effective_flow,
&computed_rect,
font_size,
&flow_ctx,
);
for (child, child_rect) in flow_rects.iter().rev() {
stack.push(StackEntry {
entity: *child,
parent_rect: computed_rect,
parent_depth: depth,
parent_font_size: font_size,
parent_clip_rect: child_clip_rect,
parent_layer: effective_layer,
flow_override_rect: Some(*child_rect),
});
}
} else if let Some(grid) = &grid_layout {
let grid_rects =
compute_grid_children(world, &children, grid, &computed_rect, dpi_scale);
for (child, child_rect) in grid_rects.iter().rev() {
stack.push(StackEntry {
entity: *child,
parent_rect: computed_rect,
parent_depth: depth,
parent_font_size: font_size,
parent_clip_rect: child_clip_rect,
parent_layer: effective_layer,
flow_override_rect: Some(*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: computed_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));
world.resources.retained_ui.z_sorted_nodes = all_sorted_nodes;
world.resources.retained_ui.picking_grid =
Some(crate::ecs::ui::picking::build_picking_grid(world));
for (window_index, mut nodes) in secondary_sorted {
nodes.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
let buffers = world
.resources
.retained_ui
.secondary_buffers
.entry(window_index)
.or_insert_with(|| crate::ecs::ui::resources::SecondaryWindowUiBuffers {
z_sorted_nodes: Vec::new(),
frame_rects: Vec::new(),
frame_ui_images: Vec::new(),
frame_text_meshes: Vec::new(),
});
buffers.z_sorted_nodes = nodes;
}
}
struct FlowContext {
absolute_scale: f32,
viewport_size: Vec2,
dpi_scale: f32,
}
fn compute_natural_height(
world: &World,
entity: freecs::Entity,
available_width: f32,
font_size: f32,
flow_ctx: &FlowContext,
) -> f32 {
let node = match world.ui.get_ui_layout_node(entity) {
Some(node) => node,
None => return 0.0,
};
if !node.visible {
return 0.0;
}
let flow = match node.flow_layout {
Some(flow) => flow,
None => return 0.0,
};
let children: Vec<freecs::Entity> = world
.resources
.children_cache
.get(&entity)
.map(|v| v.to_vec())
.unwrap_or_default();
let scaled_padding = flow.padding * flow_ctx.dpi_scale;
let scaled_spacing = flow.spacing * flow_ctx.dpi_scale;
let inner_width = (available_width - scaled_padding * 2.0).max(0.0);
let context = UiEvalContext {
parent_width: inner_width,
parent_height: 0.0,
viewport_width: flow_ctx.viewport_size.x,
viewport_height: flow_ctx.viewport_size.y,
font_size,
absolute_scale: flow_ctx.absolute_scale,
};
let mut child_heights: Vec<f32> = Vec::new();
for child_entity in &children {
let child_node = match world.ui.get_ui_layout_node(*child_entity) {
Some(node) => node,
None => continue,
};
if !child_node.visible {
continue;
}
let child_font_size = child_node.font_size.unwrap_or(font_size);
let height = if let Some(ref child_size) = child_node.flow_child_size {
let evaluated = child_size.evaluate(&context);
if evaluated.y.abs() < f32::EPSILON && child_node.flow_layout.is_some() {
compute_natural_height(world, *child_entity, inner_width, child_font_size, flow_ctx)
} else {
evaluated.y
}
} else {
0.0
};
child_heights.push(height);
}
let visible_count = child_heights.len();
match flow.direction {
FlowDirection::Vertical => {
let total: f32 = child_heights.iter().sum();
let spacing = if visible_count > 1 {
scaled_spacing * (visible_count - 1) as f32
} else {
0.0
};
total + spacing + scaled_padding * 2.0
}
FlowDirection::Horizontal => {
let max_height = child_heights.iter().cloned().fold(0.0f32, f32::max);
max_height + scaled_padding * 2.0
}
}
}
fn compute_grid_children(
world: &World,
children: &[freecs::Entity],
grid: &GridLayout,
parent_rect: &Rect,
dpi_scale: f32,
) -> Vec<(freecs::Entity, Rect)> {
let scaled_padding = grid.padding * dpi_scale;
let scaled_col_spacing = grid.column_spacing * dpi_scale;
let scaled_row_spacing = grid.row_spacing * dpi_scale;
let scaled_row_height = grid.row_height * dpi_scale;
let inner_width = (parent_rect.width() - scaled_padding * 2.0).max(0.0);
let columns = if let Some(min_width) = grid.min_column_width {
let scaled_min = min_width * dpi_scale;
if scaled_min > 0.0 {
((inner_width + scaled_col_spacing) / (scaled_min + scaled_col_spacing))
.floor()
.max(1.0) as usize
} else {
grid.columns.max(1)
}
} else {
grid.columns.max(1)
};
let col_width =
(inner_width - scaled_col_spacing * (columns as f32 - 1.0).max(0.0)) / columns as f32;
let visible_children: Vec<freecs::Entity> = children
.iter()
.filter(|child| {
world
.ui
.get_ui_layout_node(**child)
.is_some_and(|node| node.visible)
})
.copied()
.collect();
let mut result = Vec::with_capacity(visible_children.len());
for (index, entity) in visible_children.iter().enumerate() {
let col = index % columns;
let row = index / columns;
let x = parent_rect.min.x + scaled_padding + col as f32 * (col_width + scaled_col_spacing);
let y = parent_rect.min.y
+ scaled_padding
+ row as f32 * (scaled_row_height + scaled_row_spacing);
let rect = Rect::from_min_max(
Vec2::new(x, y),
Vec2::new(x + col_width, y + scaled_row_height),
);
result.push((*entity, rect));
}
result
}
fn measure_auto_size(
world: &World,
entity: freecs::Entity,
auto_size: AutoSizeMode,
padding: Vec2,
font_size: f32,
dpi_scale: f32,
) -> Option<(Option<f32>, Option<f32>)> {
if auto_size == AutoSizeMode::None {
return None;
}
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
let atlas = font_arc?;
let (text, text_font_size) = if let Some(content) = world.ui.get_ui_node_content(entity)
&& let UiNodeContent::Text {
text_slot,
font_size_override,
..
} = content
{
let resolved_size = font_size_override.unwrap_or(font_size);
if let Some(text) = world.resources.text_cache.get_text(*text_slot) {
(text.to_string(), resolved_size)
} else {
return None;
}
} else {
let children: Vec<freecs::Entity> = world
.resources
.children_cache
.get(&entity)
.map(|v| v.to_vec())
.unwrap_or_default();
let mut found_text = None;
for child in &children {
if let Some(content) = world.ui.get_ui_node_content(*child)
&& let UiNodeContent::Text {
text_slot,
font_size_override,
..
} = content
{
let child_font_size = world
.ui
.get_ui_layout_node(*child)
.and_then(|n| n.font_size)
.unwrap_or(font_size);
let resolved_size = font_size_override.unwrap_or(child_font_size);
if let Some(text) = world.resources.text_cache.get_text(*text_slot) {
found_text = Some((text.to_string(), resolved_size));
break;
}
}
}
if let Some(found) = found_text {
found
} else if let Some(flow) = world
.ui
.get_ui_layout_node(entity)
.and_then(|n| n.flow_layout)
{
let mut total_width = 0.0f32;
let mut total_height = 0.0f32;
let mut max_cross_width = 0.0f32;
let mut max_cross_height = 0.0f32;
let visible_count = children
.iter()
.filter(|c| world.ui.get_ui_layout_node(**c).is_some_and(|n| n.visible))
.count();
let total_spacing = flow.spacing * visible_count.saturating_sub(1) as f32;
for child in &children {
let child_node = match world.ui.get_ui_layout_node(*child) {
Some(n) if n.visible => n,
_ => continue,
};
if let Some(ref child_size) = child_node.flow_child_size {
if child_size.relative.is_some()
|| child_size.relative_width.is_some()
|| child_size.relative_height.is_some()
{
continue;
}
let abs = child_size.absolute.unwrap_or(Vec2::zeros());
match flow.direction {
FlowDirection::Vertical => {
total_height += abs.y;
max_cross_width = max_cross_width.max(abs.x);
}
FlowDirection::Horizontal => {
total_width += abs.x;
max_cross_height = max_cross_height.max(abs.y);
}
}
}
}
let (content_w, content_h) = match flow.direction {
FlowDirection::Vertical => (
max_cross_width + flow.padding * 2.0,
total_height + total_spacing + flow.padding * 2.0,
),
FlowDirection::Horizontal => (
total_width + total_spacing + flow.padding * 2.0,
max_cross_height + flow.padding * 2.0,
),
};
let auto_width = matches!(auto_size, AutoSizeMode::Width | AutoSizeMode::Both)
.then_some(content_w + padding.x * 2.0);
let auto_height = matches!(auto_size, AutoSizeMode::Height | AutoSizeMode::Both)
.then_some(content_h + padding.y * 2.0);
return Some((auto_width, auto_height));
} else {
return None;
}
};
let measured_width = measure_text_width(&atlas, &text, text_font_size) / dpi_scale;
let auto_width = matches!(auto_size, AutoSizeMode::Width | AutoSizeMode::Both)
.then_some(measured_width + padding.x * 2.0);
let auto_height = matches!(auto_size, AutoSizeMode::Height | AutoSizeMode::Both)
.then_some(text_font_size * 1.5 + padding.y * 2.0);
Some((auto_width, auto_height))
}
fn compute_flow_children(
world: &World,
children: &[freecs::Entity],
flow: &FlowLayout,
parent_rect: &Rect,
parent_font_size: f32,
flow_ctx: &FlowContext,
) -> Vec<(freecs::Entity, Rect)> {
let scaled_padding = flow.padding * flow_ctx.dpi_scale;
let scaled_spacing = flow.spacing * flow_ctx.dpi_scale;
let inner_rect = Rect::from_min_max(
parent_rect.min + Vec2::new(scaled_padding, scaled_padding),
parent_rect.max - Vec2::new(scaled_padding, scaled_padding),
);
let flow_children: Vec<freecs::Entity> = children
.iter()
.filter(|child| {
world
.ui
.get_ui_layout_node(**child)
.is_some_and(|node| node.visible)
})
.copied()
.collect();
if flow_children.is_empty() {
return Vec::new();
}
struct ChildMeasure {
entity: freecs::Entity,
width: f32,
height: f32,
flex_grow: Option<f32>,
flex_shrink: Option<f32>,
}
let context = UiEvalContext {
parent_width: inner_rect.width(),
parent_height: inner_rect.height(),
viewport_width: flow_ctx.viewport_size.x,
viewport_height: flow_ctx.viewport_size.y,
font_size: parent_font_size,
absolute_scale: flow_ctx.absolute_scale,
};
let mut measures: Vec<ChildMeasure> = Vec::new();
for entity in &flow_children {
let node = match world.ui.get_ui_layout_node(*entity) {
Some(node) => node,
None => continue,
};
let child_font_size = node.font_size.unwrap_or(parent_font_size);
let auto_size = node.auto_size;
let auto_size_padding = node.auto_size_padding;
let min_size = node.min_size;
let max_size = node.max_size;
let flex_shrink = node.flex_shrink;
let (width, height) = if let Some(ref child_size) = node.flow_child_size {
let evaluated = child_size.evaluate(&context);
let final_height = if evaluated.y.abs() < f32::EPSILON && node.flow_layout.is_some() {
compute_natural_height(world, *entity, evaluated.x, child_font_size, flow_ctx)
} else {
evaluated.y
};
(evaluated.x, final_height)
} else if let Some(base_layout) = &node.layouts[UiBase::INDEX] {
let rect = compute_layout_rect(base_layout, &context, &inner_rect);
(rect.width(), rect.height())
} else {
(inner_rect.width(), 0.0)
};
let (width, height) = if auto_size != AutoSizeMode::None {
if let Some((auto_w, auto_h)) = measure_auto_size(
world,
*entity,
auto_size,
auto_size_padding,
child_font_size,
flow_ctx.dpi_scale,
) {
(auto_w.unwrap_or(width), auto_h.unwrap_or(height))
} else {
(width, height)
}
} else {
(width, height)
};
let mut width = width;
let mut height = height;
if let Some(min) = min_size {
width = width.max(min.x);
height = height.max(min.y);
}
if let Some(max) = max_size {
width = width.min(max.x);
height = height.min(max.y);
}
measures.push(ChildMeasure {
entity: *entity,
width,
height,
flex_grow: node.flex_grow,
flex_shrink,
});
}
let total_spacing = scaled_spacing * (measures.len().saturating_sub(1)) as f32;
let fixed_size_sum: f32 = match flow.direction {
FlowDirection::Vertical => measures
.iter()
.filter(|measure| measure.flex_grow.is_none())
.map(|measure| measure.height)
.sum(),
FlowDirection::Horizontal => measures
.iter()
.filter(|measure| measure.flex_grow.is_none())
.map(|measure| measure.width)
.sum(),
};
let available_for_flex = match flow.direction {
FlowDirection::Vertical => (inner_rect.height() - total_spacing - fixed_size_sum).max(0.0),
FlowDirection::Horizontal => (inner_rect.width() - total_spacing - fixed_size_sum).max(0.0),
};
let total_flex: f32 = measures
.iter()
.filter_map(|measure| measure.flex_grow)
.sum();
let total_content_size: f32 = match flow.direction {
FlowDirection::Vertical => {
measures
.iter()
.map(|measure| {
if let Some(grow) = measure.flex_grow {
if total_flex > 0.0 {
available_for_flex * (grow / total_flex)
} else {
measure.height
}
} else {
measure.height
}
})
.sum::<f32>()
+ total_spacing
}
FlowDirection::Horizontal => {
measures
.iter()
.map(|measure| {
if let Some(grow) = measure.flex_grow {
if total_flex > 0.0 {
available_for_flex * (grow / total_flex)
} else {
measure.width
}
} else {
measure.width
}
})
.sum::<f32>()
+ total_spacing
}
};
let main_axis_start_offset = match flow.direction {
FlowDirection::Vertical => match flow.alignment {
FlowAlignment::Start => 0.0,
FlowAlignment::Center => (inner_rect.height() - total_content_size) * 0.5,
FlowAlignment::End => inner_rect.height() - total_content_size,
},
FlowDirection::Horizontal => match flow.alignment {
FlowAlignment::Start => 0.0,
FlowAlignment::Center => (inner_rect.width() - total_content_size) * 0.5,
FlowAlignment::End => inner_rect.width() - total_content_size,
},
};
if flow.wrap && flow.direction == FlowDirection::Horizontal {
let entities: Vec<freecs::Entity> = measures.iter().map(|m| m.entity).collect();
let widths: Vec<f32> = measures.iter().map(|m| m.width).collect();
let heights: Vec<f32> = measures.iter().map(|m| m.height).collect();
return compute_flow_children_wrap(
&entities,
&widths,
&heights,
flow.cross_alignment,
&inner_rect,
scaled_spacing,
);
}
let mut result = Vec::with_capacity(measures.len());
let mut cursor = main_axis_start_offset;
let total_fixed_size: f32 = match flow.direction {
FlowDirection::Vertical => measures.iter().map(|m| m.height).sum::<f32>(),
FlowDirection::Horizontal => measures.iter().map(|m| m.width).sum::<f32>(),
};
let available_main = match flow.direction {
FlowDirection::Vertical => inner_rect.height(),
FlowDirection::Horizontal => inner_rect.width(),
};
let overflow_amount = (total_fixed_size + total_spacing - available_main).max(0.0);
let total_shrink: f32 = measures.iter().filter_map(|m| m.flex_shrink).sum();
for measure in &measures {
let shrink_delta = if overflow_amount > 0.0 && total_shrink > 0.0 {
if let Some(shrink) = measure.flex_shrink {
overflow_amount * (shrink / total_shrink)
} else {
0.0
}
} else {
0.0
};
let (final_width, final_height) = if let Some(grow) = measure.flex_grow {
if total_flex > 0.0 {
let flex_amount = available_for_flex * (grow / total_flex);
match flow.direction {
FlowDirection::Vertical => (measure.width, flex_amount),
FlowDirection::Horizontal => (flex_amount, measure.height),
}
} else {
(measure.width, measure.height)
}
} else {
match flow.direction {
FlowDirection::Vertical => {
(measure.width, (measure.height - shrink_delta).max(0.0))
}
FlowDirection::Horizontal => {
((measure.width - shrink_delta).max(0.0), measure.height)
}
}
};
let child_rect = match flow.direction {
FlowDirection::Vertical => {
let cross_offset = match flow.cross_alignment {
FlowAlignment::Start => 0.0,
FlowAlignment::Center => (inner_rect.width() - final_width) * 0.5,
FlowAlignment::End => inner_rect.width() - final_width,
};
let rect = Rect::from_min_max(
Vec2::new(inner_rect.min.x + cross_offset, inner_rect.min.y + cursor),
Vec2::new(
inner_rect.min.x + cross_offset + final_width,
inner_rect.min.y + cursor + final_height,
),
);
cursor += final_height + scaled_spacing;
rect
}
FlowDirection::Horizontal => {
let cross_offset = match flow.cross_alignment {
FlowAlignment::Start => 0.0,
FlowAlignment::Center => (inner_rect.height() - final_height) * 0.5,
FlowAlignment::End => inner_rect.height() - final_height,
};
let rect = Rect::from_min_max(
Vec2::new(inner_rect.min.x + cursor, inner_rect.min.y + cross_offset),
Vec2::new(
inner_rect.min.x + cursor + final_width,
inner_rect.min.y + cross_offset + final_height,
),
);
cursor += final_width + scaled_spacing;
rect
}
};
result.push((measure.entity, child_rect));
}
result
}
fn compute_flow_children_wrap(
entities: &[freecs::Entity],
widths: &[f32],
heights: &[f32],
cross_alignment: FlowAlignment,
inner_rect: &Rect,
scaled_spacing: f32,
) -> Vec<(freecs::Entity, Rect)> {
let available_width = inner_rect.width();
let mut result = Vec::with_capacity(entities.len());
let mut cursor_x = 0.0f32;
let mut cursor_y = 0.0f32;
let mut line_height = 0.0f32;
for index in 0..entities.len() {
let width = widths[index];
let height = heights[index];
if cursor_x > 0.0 && cursor_x + width > available_width {
cursor_y += line_height + scaled_spacing;
cursor_x = 0.0;
line_height = 0.0;
}
if height > line_height {
line_height = height;
}
let cross_offset = match cross_alignment {
FlowAlignment::Start => 0.0,
FlowAlignment::Center => (line_height - height).max(0.0) * 0.5,
FlowAlignment::End => (line_height - height).max(0.0),
};
let rect = Rect::from_min_max(
Vec2::new(
inner_rect.min.x + cursor_x,
inner_rect.min.y + cursor_y + cross_offset,
),
Vec2::new(
inner_rect.min.x + cursor_x + width,
inner_rect.min.y + cursor_y + cross_offset + height,
),
);
result.push((entities[index], rect));
cursor_x += width + scaled_spacing;
}
result
}