use nalgebra_glm::Vec4;
use crate::ecs::text::components::{TextAlignment, TextProperties, VerticalAlignment};
use crate::ecs::text::resources::FontAtlasData;
use crate::ecs::ui::canvas_render::emit_canvas_commands;
use crate::ecs::ui::components::UiNodeContent;
use crate::ecs::ui::text_wrapping::wrap_text;
use crate::ecs::ui::tile_render::emit_tile_containers;
use crate::ecs::ui::types::Rect;
use crate::ecs::ui::types::UiTextInstance;
use crate::ecs::world::World;
use crate::render::wgpu::passes::geometry::{UiImage, UiLayer, UiRect};
use crate::render::wgpu::text_mesh::generate_text_mesh;
pub(crate) fn select_best_font(
fonts: &[FontAtlasData],
physical_size: f32,
) -> Option<(usize, &FontAtlasData)> {
select_best_font_in_group(
fonts,
physical_size,
crate::render::wgpu::font_atlas::BITMAP_ROBOTO_RANGE,
)
}
pub(crate) fn select_best_font_in_group(
fonts: &[FontAtlasData],
physical_size: f32,
group: std::ops::Range<usize>,
) -> Option<(usize, &FontAtlasData)> {
if fonts.is_empty() || group.is_empty() {
return None;
}
let start = group.start.min(fonts.len());
let end = group.end.min(fonts.len());
if start >= end {
return None;
}
let mut best_index = start;
let mut best_deviation = f32::INFINITY;
for (offset, font) in fonts[start..end].iter().enumerate() {
let deviation = (physical_size / font.font_size - 1.0).abs();
if deviation < best_deviation {
best_deviation = deviation;
best_index = start + offset;
}
}
Some((best_index, &fonts[best_index]))
}
struct RenderEntry {
computed_rect: Rect,
computed_depth: f32,
computed_color: Vec4,
content: UiNodeContent,
clip_rect: Option<Rect>,
font_size: f32,
layer: UiLayer,
z_index_override: Option<i32>,
}
pub fn ui_layout_render_sync_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
let viewport_size = world
.resources
.window
.cached_viewport_size
.map(|(width, height)| nalgebra_glm::Vec2::new(width as f32, height as f32))
.unwrap_or(nalgebra_glm::Vec2::new(800.0, 600.0));
let bitmap_fonts: Vec<FontAtlasData> = world
.resources
.text_cache
.font_manager
.bitmap_fonts
.iter()
.map(|arc| arc.as_ref().clone())
.collect();
let has_bitmap_font = !bitmap_fonts.is_empty();
let dpi_scale = world.resources.window.cached_scale_factor;
let render_dirty = world.resources.retained_ui.render_dirty;
if render_dirty {
world.resources.retained_ui.frame_rects.clear();
world.resources.retained_ui.frame_ui_images.clear();
world.resources.retained_ui.frame_text_meshes.clear();
if let Some(bg_color) = world.resources.retained_ui.background_color {
world.resources.retained_ui.frame_rects.push(UiRect {
position: nalgebra_glm::Vec2::new(0.0, 0.0),
size: viewport_size,
color: bg_color,
corner_radius: 0.0,
border_width: 0.0,
border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
rotation: 0.0,
clip_rect: None,
layer: UiLayer::Background,
z_index: 0,
});
}
let sorted_nodes = std::mem::take(&mut world.resources.retained_ui.z_sorted_nodes);
let dpi_changed =
(dpi_scale - world.resources.retained_ui.text_mesh_cache_dpi).abs() > f32::EPSILON;
if dpi_changed {
world.resources.retained_ui.text_mesh_cache.clear();
world.resources.retained_ui.text_mesh_cache_dpi = dpi_scale;
}
let mut render_entries: Vec<RenderEntry> = Vec::new();
for &(entity, _) in &sorted_nodes {
let node = match world.ui.get_ui_layout_node(entity) {
Some(node) => node,
None => continue,
};
let computed_rect = node.computed_rect;
if computed_rect.max.x < 0.0
|| computed_rect.min.x > viewport_size.x
|| computed_rect.max.y < 0.0
|| computed_rect.min.y > viewport_size.y
{
continue;
}
let computed_depth = node.computed_depth;
let font_size = node.font_size.unwrap_or(16.0);
let node_layer = node.computed_layer.unwrap_or(UiLayer::Background);
let z_index_override = node.z_index;
let computed_color = world
.ui
.get_ui_node_color(entity)
.map(|color| color.computed_color)
.unwrap_or(Vec4::new(1.0, 1.0, 1.0, 1.0));
let content = match world.ui.get_ui_node_content(entity) {
Some(content) => *content,
None => continue,
};
let clip_rect = node.computed_clip_rect;
render_entries.push(RenderEntry {
computed_rect,
computed_depth,
computed_color,
content,
clip_rect,
font_size,
layer: node_layer,
z_index_override,
});
}
let mut rects = std::mem::take(&mut world.resources.retained_ui.frame_rects);
let mut ui_images = std::mem::take(&mut world.resources.retained_ui.frame_ui_images);
let mut text_meshes = Vec::new();
for entry in &render_entries {
let z_index = entry
.z_index_override
.unwrap_or((entry.computed_depth * 100.0) as i32);
match &entry.content {
UiNodeContent::None => {}
UiNodeContent::Rect {
corner_radius,
border_width,
border_color,
} => {
rects.push(UiRect {
position: entry.computed_rect.min,
size: entry.computed_rect.size(),
color: entry.computed_color,
corner_radius: *corner_radius * dpi_scale,
border_width: *border_width * dpi_scale,
border_color: *border_color,
rotation: 0.0,
clip_rect: entry.clip_rect,
layer: entry.layer,
z_index,
});
}
UiNodeContent::Text {
text_slot,
font_index,
font_size_override,
outline_color,
outline_width,
alignment,
vertical_alignment,
overflow,
monospace_width,
} => {
let text = world
.resources
.text_cache
.get_text(*text_slot)
.map(|s| s.to_string());
if let Some(text) = text {
let actual_font_size =
font_size_override.unwrap_or(entry.font_size) * dpi_scale;
let group = if *font_index
>= crate::render::wgpu::font_atlas::BITMAP_MONO_RANGE.start
{
crate::render::wgpu::font_atlas::BITMAP_MONO_RANGE
} else {
crate::render::wgpu::font_atlas::BITMAP_ROBOTO_RANGE
};
let Some((font_idx, font_data)) =
select_best_font_in_group(&bitmap_fonts, actual_font_size, group)
else {
continue;
};
let slot_gen = world
.resources
.text_cache
.slot_generations
.get(*text_slot)
.copied()
.unwrap_or(0);
let wrap_width_key: Option<f32> =
if *overflow == crate::ecs::ui::components::TextOverflow::Wrap {
Some(entry.computed_rect.width().round())
} else {
None
};
let (render_text, use_cache) = if *overflow
== crate::ecs::ui::components::TextOverflow::Wrap
{
let available = entry.computed_rect.width();
(
wrap_text(font_data, &text, actual_font_size, available),
true,
)
} else if *overflow == crate::ecs::ui::components::TextOverflow::Ellipsis {
let full_width = crate::ecs::ui::widget_systems::measure_text_width(
font_data,
&text,
actual_font_size,
);
let available = entry.computed_rect.width();
if full_width > available {
let ellipsis = "...";
let ellipsis_width =
crate::ecs::ui::widget_systems::measure_text_width(
font_data,
ellipsis,
actual_font_size,
);
let target_width = (available - ellipsis_width).max(0.0);
let mut accumulated = 0.0f32;
let mut truncate_at = 0;
let scale = actual_font_size / font_data.font_size;
let mut prev_char: Option<char> = None;
for (index, character) in text.chars().enumerate() {
if let Some(glyph) = font_data.glyphs.get(&character) {
if let Some(previous) = prev_char
&& let Some(&kern) =
font_data.kerning.get(&(previous, character))
{
accumulated += kern * scale;
}
accumulated += glyph.advance * scale;
if accumulated > target_width {
truncate_at = index;
break;
}
}
prev_char = Some(character);
truncate_at = index + 1;
}
let truncated: String =
text.chars().take(truncate_at).collect::<String>() + ellipsis;
(truncated, false)
} else {
(text, true)
}
} else {
(text, true)
};
let mesh = if use_cache {
let cache = &world.resources.retained_ui.text_mesh_cache;
let cached =
cache.get(*text_slot).and_then(|c| c.as_ref()).filter(|c| {
c.slot_generation == slot_gen
&& (c.font_size - actual_font_size).abs() < f32::EPSILON
&& c.wrap_width == wrap_width_key
&& c.monospace_width == *monospace_width
});
if let Some(hit) = cached {
hit.mesh.clone()
} else {
let properties = TextProperties {
font_size: actual_font_size,
color: entry.computed_color,
alignment: *alignment,
vertical_alignment: *vertical_alignment,
monospace_width: *monospace_width,
..TextProperties::default()
};
let new_mesh =
generate_text_mesh(&render_text, font_data, &properties);
let mesh_cache = &mut world.resources.retained_ui.text_mesh_cache;
if mesh_cache.len() <= *text_slot {
mesh_cache.resize_with(*text_slot + 1, || None);
}
mesh_cache[*text_slot] =
Some(crate::ecs::ui::resources::CachedTextMesh {
mesh: new_mesh,
slot_generation: slot_gen,
font_size: actual_font_size,
wrap_width: wrap_width_key,
monospace_width: *monospace_width,
});
mesh_cache[*text_slot].as_ref().unwrap().mesh.clone()
}
} else {
let properties = TextProperties {
font_size: actual_font_size,
color: entry.computed_color,
alignment: *alignment,
vertical_alignment: *vertical_alignment,
monospace_width: *monospace_width,
..TextProperties::default()
};
generate_text_mesh(&render_text, font_data, &properties)
};
let position_x = match alignment {
TextAlignment::Left => entry.computed_rect.min.x,
TextAlignment::Center => entry.computed_rect.center().x,
TextAlignment::Right => entry.computed_rect.max.x,
};
let position_y = match vertical_alignment {
VerticalAlignment::Top => entry.computed_rect.min.y,
VerticalAlignment::Middle => entry.computed_rect.center().y,
VerticalAlignment::Bottom => entry.computed_rect.max.y,
VerticalAlignment::Baseline => entry.computed_rect.center().y,
};
let snapped_position =
nalgebra_glm::Vec2::new(position_x.round(), position_y.round());
let effective_clip = if matches!(
*overflow,
crate::ecs::ui::components::TextOverflow::Clip
| crate::ecs::ui::components::TextOverflow::Wrap
) {
let content_clip = Some(entry.computed_rect);
match entry.clip_rect {
Some(existing) => entry.computed_rect.intersect(&existing),
None => content_clip,
}
} else {
entry.clip_rect
};
let character_colors = world
.resources
.retained_ui
.text_slot_character_colors
.get(text_slot)
.cloned();
let character_background_colors = world
.resources
.retained_ui
.text_slot_character_background_colors
.get(text_slot)
.cloned();
text_meshes.push(UiTextInstance {
mesh,
position: snapped_position,
color: entry.computed_color,
outline_color: *outline_color,
outline_width: *outline_width,
font_size: actual_font_size,
font_index: font_idx,
clip_rect: effective_clip,
layer: entry.layer,
z_index,
character_colors,
character_background_colors,
});
}
}
UiNodeContent::Image {
texture_index,
uv_min,
uv_max,
} => {
ui_images.push(UiImage {
position: entry.computed_rect.min,
size: entry.computed_rect.size(),
texture_index: *texture_index,
uv_min: *uv_min,
uv_max: *uv_max,
color: entry.computed_color,
clip_rect: entry.clip_rect,
layer: entry.layer,
z_index,
});
}
}
}
world.resources.retained_ui.z_sorted_nodes = sorted_nodes;
world.resources.retained_ui.frame_rects = rects;
world.resources.retained_ui.frame_ui_images = ui_images;
world.resources.retained_ui.frame_text_meshes = text_meshes;
world.resources.retained_ui.cached_node_rect_count =
world.resources.retained_ui.frame_rects.len();
world.resources.retained_ui.cached_node_image_count =
world.resources.retained_ui.frame_ui_images.len();
world.resources.retained_ui.cached_node_text_count =
world.resources.retained_ui.frame_text_meshes.len();
let secondary_window_indices: Vec<usize> = world
.resources
.retained_ui
.secondary_buffers
.keys()
.copied()
.collect();
for window_index in secondary_window_indices {
let nodes = world
.resources
.retained_ui
.secondary_buffers
.get_mut(&window_index)
.map(|b| std::mem::take(&mut b.z_sorted_nodes))
.unwrap_or_default();
if nodes.is_empty() {
if let Some(buffers) = world
.resources
.retained_ui
.secondary_buffers
.get_mut(&window_index)
{
buffers.z_sorted_nodes = nodes;
}
continue;
}
let window_viewport = world
.resources
.secondary_windows
.states
.iter()
.find(|s| s.index == window_index)
.map(|s| nalgebra_glm::Vec2::new(s.size.0 as f32, s.size.1 as f32))
.unwrap_or(viewport_size);
let (sec_rects, sec_images, sec_text) = render_sorted_nodes_to_output(
world,
&nodes,
window_viewport,
dpi_scale,
&bitmap_fonts,
);
if let Some(buffers) = world
.resources
.retained_ui
.secondary_buffers
.get_mut(&window_index)
{
buffers.z_sorted_nodes = nodes;
buffers.frame_rects = sec_rects;
buffers.frame_ui_images = sec_images;
buffers.frame_text_meshes = sec_text;
}
}
world.resources.retained_ui.render_dirty = false;
} else {
let rect_count = world.resources.retained_ui.cached_node_rect_count;
let image_count = world.resources.retained_ui.cached_node_image_count;
let text_count = world.resources.retained_ui.cached_node_text_count;
world.resources.retained_ui.frame_rects.truncate(rect_count);
world
.resources
.retained_ui
.frame_ui_images
.truncate(image_count);
world
.resources
.retained_ui
.frame_text_meshes
.truncate(text_count);
}
emit_canvas_commands(world, dpi_scale);
if let Some(tooltip) = &world.resources.retained_ui.tooltip_state {
let delay = 0.5;
if world.resources.retained_ui.current_time - tooltip.hover_start >= delay {
let mouse_pos = world.resources.input.mouse.position;
let tooltip_entity = tooltip.tooltip_entity;
let tooltip_text = tooltip.text.clone();
if let Some(tooltip_entity) = tooltip_entity {
let mut tooltip_x = mouse_pos.x + 12.0;
let mut tooltip_y = mouse_pos.y + 16.0;
if let Some(node) = world.ui.get_ui_layout_node(tooltip_entity) {
let tooltip_w = node.computed_rect.width();
let tooltip_h = node.computed_rect.height();
if tooltip_x + tooltip_w > viewport_size.x {
tooltip_x = viewport_size.x - tooltip_w;
}
if tooltip_y + tooltip_h > viewport_size.y {
tooltip_y = mouse_pos.y - tooltip_h - 4.0;
}
}
if let Some(node) = world.ui.get_ui_layout_node_mut(tooltip_entity) {
node.visible = true;
for layout in &mut node.layouts {
if let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
layout
{
window.position = crate::ecs::ui::units::Ab(nalgebra_glm::Vec2::new(
tooltip_x, tooltip_y,
))
.into();
break;
}
}
}
} else if let Some((tooltip_font_idx, font_data)) =
select_best_font(&bitmap_fonts, 13.0 * dpi_scale)
{
let theme = world.resources.retained_ui.theme_state.active_theme();
let tooltip_bg = Vec4::new(
theme.panel_color.x,
theme.panel_color.y,
theme.panel_color.z,
0.95,
);
let tooltip_border = theme.border_color;
let tooltip_text_color = theme.text_color;
let tooltip_corner = theme.corner_radius;
let font_size = 13.0 * dpi_scale;
let padding = 8.0 * dpi_scale;
let max_tooltip_width = 300.0 * dpi_scale;
let max_text_width = max_tooltip_width - padding * 2.0;
let properties = TextProperties {
font_size,
color: tooltip_text_color,
alignment: TextAlignment::Left,
vertical_alignment: VerticalAlignment::Top,
..TextProperties::default()
};
let initial_mesh = generate_text_mesh(&tooltip_text, font_data, &properties);
let initial_text_width = initial_mesh.bounds.x * 100.0;
let (mesh, text_width, line_count) = if initial_text_width > max_text_width {
let wrapped = crate::ecs::ui::text_wrapping::wrap_text(
font_data,
&tooltip_text,
font_size,
max_text_width,
);
let lines = wrapped.lines().count().max(1);
let wrapped_mesh = generate_text_mesh(&wrapped, font_data, &properties);
let wrapped_width = wrapped_mesh.bounds.x * 100.0;
(wrapped_mesh, wrapped_width, lines)
} else {
(initial_mesh, initial_text_width, 1)
};
let line_height = font_size * 1.3;
let tooltip_height = if line_count > 1 {
line_height * line_count as f32 + padding * 2.0
} else {
28.0 * dpi_scale
};
let tooltip_width = text_width + padding * 2.0;
let mut tooltip_x = mouse_pos.x + 12.0;
let mut tooltip_y = mouse_pos.y + 16.0;
if tooltip_x + tooltip_width > viewport_size.x {
tooltip_x = viewport_size.x - tooltip_width;
}
if tooltip_y + tooltip_height > viewport_size.y {
tooltip_y = mouse_pos.y - tooltip_height - 4.0;
}
world.resources.retained_ui.frame_rects.push(UiRect {
position: nalgebra_glm::Vec2::new(tooltip_x, tooltip_y),
size: nalgebra_glm::Vec2::new(tooltip_width, tooltip_height),
color: tooltip_bg,
corner_radius: tooltip_corner * dpi_scale,
border_width: 1.0 * dpi_scale,
border_color: tooltip_border,
rotation: 0.0,
clip_rect: None,
layer: UiLayer::Tooltips,
z_index: 0,
});
world
.resources
.retained_ui
.frame_text_meshes
.push(UiTextInstance {
mesh,
position: nalgebra_glm::Vec2::new(
tooltip_x + padding,
if line_count > 1 {
tooltip_y + padding + font_size * 0.5
} else {
tooltip_y + tooltip_height * 0.5
},
),
color: tooltip_text_color,
outline_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
outline_width: 0.0,
font_size,
font_index: tooltip_font_idx,
clip_rect: None,
layer: UiLayer::Tooltips,
character_colors: None,
character_background_colors: None,
z_index: 1,
});
}
}
}
if world.resources.retained_ui.focus_ring_visible
&& let Some(focused) = world.resources.retained_ui.focused_entity
&& let Some(node) = world.ui.get_ui_layout_node(focused)
&& node.visible
{
let rect = node.computed_rect;
let accent_color = world
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
let ring_color = Vec4::new(accent_color.x, accent_color.y, accent_color.z, 0.8);
let inset = 1.0 * dpi_scale;
world.resources.retained_ui.frame_rects.push(UiRect {
position: nalgebra_glm::Vec2::new(rect.min.x - inset, rect.min.y - inset),
size: nalgebra_glm::Vec2::new(rect.width() + inset * 2.0, rect.height() + inset * 2.0),
color: Vec4::new(0.0, 0.0, 0.0, 0.0),
corner_radius: 4.0 * dpi_scale,
border_width: 2.0 * dpi_scale,
border_color: ring_color,
rotation: 0.0,
clip_rect: None,
layer: UiLayer::Tooltips,
z_index: 0,
});
}
if world.resources.retained_ui.dock_indicator_active {
let theme = world.resources.retained_ui.theme_state.active_theme();
let theme_panel_color = theme.panel_color;
let theme_accent = theme.accent_color;
let theme_bg_hovered = theme.background_color_hovered;
let theme_border = theme.border_color;
let theme_text_color = theme.text_color;
let indicator_bg = Vec4::new(
theme_panel_color.x,
theme_panel_color.y,
theme_panel_color.z,
0.95,
);
let preview_color = Vec4::new(theme_accent.x, theme_accent.y, theme_accent.z, 0.3);
let indicator_size = 40.0;
let indicator_spacing = 4.0;
let center = viewport_size * 0.5;
let total_size = indicator_size * 3.0 + indicator_spacing * 2.0;
let bg_rect_pos =
center - nalgebra_glm::Vec2::new(total_size + 16.0, total_size + 16.0) * 0.5;
world.resources.retained_ui.frame_rects.push(UiRect {
position: bg_rect_pos,
size: nalgebra_glm::Vec2::new(total_size + 16.0, total_size + 16.0),
color: indicator_bg,
corner_radius: 8.0,
border_width: 1.0,
border_color: theme_border,
rotation: 0.0,
clip_rect: None,
layer: UiLayer::DockIndicator,
z_index: 0,
});
let mouse_pos = world.resources.input.mouse.position;
let buttons = [
(
nalgebra_glm::Vec2::new(center.x, center.y - indicator_size - indicator_spacing),
"T",
),
(
nalgebra_glm::Vec2::new(center.x, center.y + indicator_size + indicator_spacing),
"B",
),
(
nalgebra_glm::Vec2::new(center.x - indicator_size - indicator_spacing, center.y),
"L",
),
(
nalgebra_glm::Vec2::new(center.x + indicator_size + indicator_spacing, center.y),
"R",
),
(center, "+"),
];
let mut hovered_index: Option<usize> = None;
for (index, (btn_center, symbol)) in buttons.iter().enumerate() {
let half = indicator_size * 0.5;
let btn_min = *btn_center - nalgebra_glm::Vec2::new(half, half);
let btn_max = *btn_center + nalgebra_glm::Vec2::new(half, half);
let is_hovered = mouse_pos.x >= btn_min.x
&& mouse_pos.x <= btn_max.x
&& mouse_pos.y >= btn_min.y
&& mouse_pos.y <= btn_max.y;
if is_hovered {
hovered_index = Some(index);
}
let btn_color = if is_hovered {
theme_accent
} else {
theme_bg_hovered
};
world.resources.retained_ui.frame_rects.push(UiRect {
position: btn_min,
size: nalgebra_glm::Vec2::new(indicator_size, indicator_size),
color: btn_color,
corner_radius: 4.0,
border_width: 1.0,
border_color: theme_border,
rotation: 0.0,
clip_rect: None,
layer: UiLayer::DockIndicator,
z_index: 1,
});
let symbol_font_size = indicator_size * 0.4 * dpi_scale;
if let Some((dock_font_idx, font_data)) =
select_best_font(&bitmap_fonts, symbol_font_size)
{
let properties = TextProperties {
font_size: symbol_font_size,
color: theme_text_color,
alignment: TextAlignment::Center,
vertical_alignment: VerticalAlignment::Middle,
..TextProperties::default()
};
let mesh = generate_text_mesh(symbol, font_data, &properties);
let text_pos =
nalgebra_glm::Vec2::new(btn_center.x, btn_center.y + symbol_font_size * 0.15);
world
.resources
.retained_ui
.frame_text_meshes
.push(UiTextInstance {
mesh,
position: text_pos,
color: theme_text_color,
outline_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
outline_width: 0.0,
font_size: symbol_font_size,
font_index: dock_font_idx,
clip_rect: None,
layer: UiLayer::DockIndicator,
character_colors: None,
character_background_colors: None,
z_index: 2,
});
}
}
if let Some(index) = hovered_index
&& index < 4
{
let reserved = &world.resources.retained_ui.reserved_areas;
let dock_size = 300.0;
let (preview_pos, preview_size) = match index {
0 => {
let w = viewport_size.x - reserved.left - reserved.right;
(
nalgebra_glm::Vec2::new(reserved.left, reserved.top),
nalgebra_glm::Vec2::new(w, dock_size),
)
}
1 => {
let w = viewport_size.x - reserved.left - reserved.right;
(
nalgebra_glm::Vec2::new(
reserved.left,
viewport_size.y - reserved.bottom - dock_size,
),
nalgebra_glm::Vec2::new(w, dock_size),
)
}
2 => {
let h = viewport_size.y - reserved.top - reserved.bottom;
(
nalgebra_glm::Vec2::new(reserved.left, reserved.top),
nalgebra_glm::Vec2::new(dock_size, h),
)
}
3 => {
let h = viewport_size.y - reserved.top - reserved.bottom;
(
nalgebra_glm::Vec2::new(
viewport_size.x - reserved.right - dock_size,
reserved.top,
),
nalgebra_glm::Vec2::new(dock_size, h),
)
}
_ => (
nalgebra_glm::Vec2::new(0.0, 0.0),
nalgebra_glm::Vec2::new(0.0, 0.0),
),
};
world.resources.retained_ui.frame_rects.push(UiRect {
position: preview_pos,
size: preview_size,
color: preview_color,
corner_radius: 0.0,
border_width: 2.0,
border_color: Vec4::new(theme_accent.x, theme_accent.y, theme_accent.z, 0.8),
rotation: 0.0,
clip_rect: None,
layer: UiLayer::DockIndicator,
z_index: 0,
});
}
}
emit_tile_containers(world, dpi_scale);
let overlay_rects: Vec<_> = world
.resources
.retained_ui
.overlay_rects
.drain(..)
.collect();
world
.resources
.retained_ui
.frame_rects
.extend(overlay_rects);
let overlay_text: Vec<_> = world.resources.retained_ui.overlay_text.drain(..).collect();
if !overlay_text.is_empty() && has_bitmap_font {
for entry in overlay_text {
if let Some((overlay_font_idx, overlay_font)) =
select_best_font(&bitmap_fonts, entry.properties.font_size)
{
let mesh = generate_text_mesh(&entry.text, overlay_font, &entry.properties);
world
.resources
.retained_ui
.frame_text_meshes
.push(UiTextInstance {
mesh,
position: entry.position,
color: entry.properties.color,
outline_color: entry.properties.outline_color,
outline_width: entry.properties.outline_width,
font_size: entry.properties.font_size,
font_index: overlay_font_idx,
clip_rect: entry.clip_rect,
layer: entry.layer,
z_index: entry.z_index,
character_colors: None,
character_background_colors: None,
});
}
}
}
}
fn render_sorted_nodes_to_output(
world: &mut World,
sorted_nodes: &[(freecs::Entity, f32)],
viewport_size: nalgebra_glm::Vec2,
dpi_scale: f32,
bitmap_fonts: &[FontAtlasData],
) -> (Vec<UiRect>, Vec<UiImage>, Vec<UiTextInstance>) {
let mut render_entries: Vec<RenderEntry> = Vec::new();
for &(entity, _) in sorted_nodes {
let node = match world.ui.get_ui_layout_node(entity) {
Some(node) => node,
None => continue,
};
let computed_rect = node.computed_rect;
if computed_rect.max.x < 0.0
|| computed_rect.min.x > viewport_size.x
|| computed_rect.max.y < 0.0
|| computed_rect.min.y > viewport_size.y
{
continue;
}
let computed_depth = node.computed_depth;
let font_size = node.font_size.unwrap_or(16.0);
let node_layer = node.computed_layer.unwrap_or(UiLayer::Background);
let z_index_override = node.z_index;
let computed_color = world
.ui
.get_ui_node_color(entity)
.map(|color| color.computed_color)
.unwrap_or(Vec4::new(1.0, 1.0, 1.0, 1.0));
let content = match world.ui.get_ui_node_content(entity) {
Some(content) => *content,
None => continue,
};
let clip_rect = node.computed_clip_rect;
render_entries.push(RenderEntry {
computed_rect,
computed_depth,
computed_color,
content,
clip_rect,
font_size,
layer: node_layer,
z_index_override,
});
}
let mut rects = Vec::new();
let mut ui_images = Vec::new();
let mut text_meshes = Vec::new();
for entry in &render_entries {
let z_index = entry
.z_index_override
.unwrap_or((entry.computed_depth * 100.0) as i32);
match &entry.content {
UiNodeContent::None => {}
UiNodeContent::Rect {
corner_radius,
border_width,
border_color,
} => {
rects.push(UiRect {
position: entry.computed_rect.min,
size: entry.computed_rect.size(),
color: entry.computed_color,
corner_radius: *corner_radius * dpi_scale,
border_width: *border_width * dpi_scale,
border_color: *border_color,
rotation: 0.0,
clip_rect: entry.clip_rect,
layer: entry.layer,
z_index,
});
}
UiNodeContent::Text {
text_slot,
font_index: _,
font_size_override,
outline_color,
outline_width,
alignment,
vertical_alignment,
overflow: _,
monospace_width,
} => {
let text = world
.resources
.text_cache
.get_text(*text_slot)
.map(|s| s.to_string());
if let Some(text) = text {
let actual_font_size =
font_size_override.unwrap_or(entry.font_size) * dpi_scale;
let Some((font_idx, font_data)) =
select_best_font(bitmap_fonts, actual_font_size)
else {
continue;
};
let properties = TextProperties {
font_size: actual_font_size,
color: entry.computed_color,
alignment: *alignment,
vertical_alignment: *vertical_alignment,
monospace_width: *monospace_width,
..TextProperties::default()
};
let mesh = generate_text_mesh(&text, font_data, &properties);
let position_x = match alignment {
TextAlignment::Left => entry.computed_rect.min.x,
TextAlignment::Center => entry.computed_rect.center().x,
TextAlignment::Right => entry.computed_rect.max.x,
};
let position_y = match vertical_alignment {
VerticalAlignment::Top => entry.computed_rect.min.y,
VerticalAlignment::Middle => entry.computed_rect.center().y,
VerticalAlignment::Bottom => entry.computed_rect.max.y,
VerticalAlignment::Baseline => entry.computed_rect.center().y,
};
let snapped_position =
nalgebra_glm::Vec2::new(position_x.round(), position_y.round());
let character_colors = world
.resources
.retained_ui
.text_slot_character_colors
.get(text_slot)
.cloned();
let character_background_colors = world
.resources
.retained_ui
.text_slot_character_background_colors
.get(text_slot)
.cloned();
text_meshes.push(UiTextInstance {
mesh,
position: snapped_position,
color: entry.computed_color,
outline_color: *outline_color,
outline_width: *outline_width,
font_size: actual_font_size,
font_index: font_idx,
clip_rect: entry.clip_rect,
layer: entry.layer,
z_index,
character_colors,
character_background_colors,
});
}
}
UiNodeContent::Image {
texture_index,
uv_min,
uv_max,
} => {
ui_images.push(UiImage {
position: entry.computed_rect.min,
size: entry.computed_rect.size(),
texture_index: *texture_index,
uv_min: *uv_min,
uv_max: *uv_max,
color: entry.computed_color,
clip_rect: entry.clip_rect,
layer: entry.layer,
z_index,
});
}
}
}
(rects, ui_images, text_meshes)
}