use std::collections::HashMap;
use nalgebra_glm::{Vec2, Vec4};
use serde::{Deserialize, Serialize};
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::layout_types::{FlowAlignment, FlowDirection, FlowLayout};
use crate::ecs::ui::types::{Anchor, Rect};
use crate::ecs::ui::units::{Ab, Rl};
use crate::ecs::world::World;
use crate::prelude::*;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct UiSceneRecord {
pub root: Vec<UiSceneNode>,
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct FlowSpec {
pub direction: FlowDirection,
pub padding: f32,
pub spacing: f32,
#[serde(default)]
pub alignment: FlowAlignment,
#[serde(default)]
pub cross_alignment: FlowAlignment,
#[serde(default)]
pub wrap: bool,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum SizeSpec {
Absolute(f32, f32),
Relative(f32, f32),
Mixed { rel: (f32, f32), abs: (f32, f32) },
}
impl SizeSpec {
fn as_value(&self) -> crate::ecs::ui::units::UiValue<Vec2> {
match *self {
SizeSpec::Absolute(x, y) => Ab(Vec2::new(x, y)).into(),
SizeSpec::Relative(x, y) => Rl(Vec2::new(x, y)).into(),
SizeSpec::Mixed { rel, abs } => {
Rl(Vec2::new(rel.0, rel.1)) + Ab(Vec2::new(abs.0, abs.1))
}
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum UiSceneNode {
Boundary {
#[serde(default)]
id: Option<String>,
min: SizeSpec,
max: SizeSpec,
#[serde(default)]
flow: Option<FlowSpec>,
#[serde(default)]
children: Vec<UiSceneNode>,
},
Window {
#[serde(default)]
id: Option<String>,
position: SizeSpec,
size: SizeSpec,
anchor: Anchor,
#[serde(default)]
flow: Option<FlowSpec>,
#[serde(default)]
children: Vec<UiSceneNode>,
},
FlowChild {
#[serde(default)]
id: Option<String>,
size: SizeSpec,
#[serde(default)]
flow: Option<FlowSpec>,
#[serde(default)]
children: Vec<UiSceneNode>,
},
DockedPanel {
#[serde(default)]
id: Option<String>,
title: String,
side: DockSide,
size: f32,
#[serde(default)]
children: Vec<UiSceneNode>,
},
FloatingPanel {
#[serde(default)]
id: Option<String>,
title: String,
rect: [f32; 4],
#[serde(default)]
visible: Option<bool>,
#[serde(default)]
children: Vec<UiSceneNode>,
},
Heading {
#[serde(default)]
id: Option<String>,
text: String,
},
Label {
#[serde(default)]
id: Option<String>,
text: String,
#[serde(default)]
font_size: Option<f32>,
#[serde(default)]
color: Option<[f32; 4]>,
#[serde(default)]
alignment: Option<TextAlignment>,
#[serde(default)]
vertical_alignment: Option<VerticalAlignment>,
},
Separator,
Spacer {
height: f32,
},
Button {
#[serde(default)]
id: Option<String>,
text: String,
},
Checkbox {
#[serde(default)]
id: Option<String>,
label: String,
#[serde(default)]
checked: bool,
},
Toggle {
#[serde(default)]
id: Option<String>,
label: String,
#[serde(default)]
value: bool,
},
Slider {
#[serde(default)]
id: Option<String>,
label: String,
min: f32,
max: f32,
value: f32,
},
TextInput {
#[serde(default)]
id: Option<String>,
placeholder: String,
},
Dropdown {
#[serde(default)]
id: Option<String>,
options: Vec<String>,
#[serde(default)]
selected: usize,
},
ScrollArea {
#[serde(default)]
id: Option<String>,
#[serde(default = "default_padding")]
padding: f32,
#[serde(default = "default_spacing")]
spacing: f32,
#[serde(default)]
children: Vec<UiSceneNode>,
},
}
fn default_padding() -> f32 {
8.0
}
fn default_spacing() -> f32 {
4.0
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub enum DockSide {
Left,
Right,
Top,
Bottom,
}
pub fn apply_record(world: &mut World, record: &UiSceneRecord) -> HashMap<String, freecs::Entity> {
let mut handles: HashMap<String, freecs::Entity> = HashMap::new();
{
let mut tree = UiTreeBuilder::new(world);
for node in &record.root {
apply_node(&mut tree, node, &mut handles);
}
tree.finish();
}
handles
}
fn apply_node(
tree: &mut UiTreeBuilder,
node: &UiSceneNode,
handles: &mut HashMap<String, freecs::Entity>,
) {
match node {
UiSceneNode::Boundary {
id,
min,
max,
flow,
children,
} => {
let entity = tree
.add_node()
.boundary(min.as_value(), max.as_value())
.entity();
apply_flow(tree, entity, flow);
register(handles, id, entity);
let mut scope = tree.children_of(entity);
for child in children {
apply_node(&mut scope, child, handles);
}
}
UiSceneNode::Window {
id,
position,
size,
anchor,
flow,
children,
} => {
let entity = tree
.add_node()
.window(position.as_value(), size.as_value(), *anchor)
.entity();
apply_flow(tree, entity, flow);
register(handles, id, entity);
let mut scope = tree.children_of(entity);
for child in children {
apply_node(&mut scope, child, handles);
}
}
UiSceneNode::FlowChild {
id,
size,
flow,
children,
} => {
let entity = tree.add_node().flow_child(size.as_value()).entity();
apply_flow(tree, entity, flow);
register(handles, id, entity);
let mut scope = tree.children_of(entity);
for child in children {
apply_node(&mut scope, child, handles);
}
}
UiSceneNode::DockedPanel {
id,
title,
side,
size,
children,
} => {
let panel_id = id.as_deref().unwrap_or(title);
let entity = match side {
DockSide::Left => tree.add_docked_panel_left(panel_id, title, *size),
DockSide::Right => tree.add_docked_panel_right(panel_id, title, *size),
DockSide::Top => tree.add_docked_panel_top(panel_id, title, *size),
DockSide::Bottom => tree.add_docked_panel_bottom(panel_id, title, *size),
};
register(handles, id, entity);
let mut scope = tree.children_of_widget(entity);
for child in children {
apply_node(&mut scope, child, handles);
}
}
UiSceneNode::FloatingPanel {
id,
title,
rect,
visible,
children,
} => {
let panel_id = id.as_deref().unwrap_or(title);
let entity = tree.add_floating_panel(
panel_id,
title,
Rect {
min: Vec2::new(rect[0], rect[1]),
max: Vec2::new(rect[2], rect[3]),
},
);
if let Some(visible) = visible {
ui_set_visible(tree.world_mut(), entity, *visible);
}
register(handles, id, entity);
let mut scope = tree.children_of_widget(entity);
for child in children {
apply_node(&mut scope, child, handles);
}
}
UiSceneNode::Heading { id, text } => {
let entity = tree.add_heading(text);
register(handles, id, entity);
}
UiSceneNode::Label {
id,
text,
font_size,
color,
alignment,
vertical_alignment,
} => {
let theme_font = tree
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let mut builder = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 18.0)))
.with_text(text, font_size.unwrap_or(theme_font))
.with_text_alignment(
alignment.unwrap_or(TextAlignment::Left),
vertical_alignment.unwrap_or(VerticalAlignment::Middle),
);
if let Some(rgba) = color {
builder = builder.color_raw::<crate::ecs::ui::state::UiBase>(Vec4::new(
rgba[0], rgba[1], rgba[2], rgba[3],
));
}
let entity = builder.entity();
register(handles, id, entity);
}
UiSceneNode::Separator => {
tree.add_separator();
}
UiSceneNode::Spacer { height } => {
tree.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, *height)))
.entity();
}
UiSceneNode::Button { id, text } => {
let entity = tree.add_button(text);
register(handles, id, entity);
}
UiSceneNode::Checkbox { id, label, checked } => {
let entity = tree.add_checkbox(label, *checked);
register(handles, id, entity);
}
UiSceneNode::Toggle { id, label, value } => {
let _ = label;
let entity = tree.add_toggle(*value);
register(handles, id, entity);
}
UiSceneNode::Slider {
id,
label,
min,
max,
value,
} => {
let _ = label;
let entity = tree.add_slider(*min, *max, *value);
register(handles, id, entity);
}
UiSceneNode::TextInput { id, placeholder } => {
let entity = tree.add_text_input(placeholder);
register(handles, id, entity);
}
UiSceneNode::Dropdown {
id,
options,
selected,
} => {
let option_refs: Vec<&str> = options.iter().map(String::as_str).collect();
let entity = tree.add_dropdown(&option_refs, *selected);
register(handles, id, entity);
}
UiSceneNode::ScrollArea {
id,
padding,
spacing,
children,
} => {
let entity = tree.add_scroll_area_fill(*padding, *spacing);
register(handles, id, entity);
let mut scope = tree.children_of_widget(entity);
for child in children {
apply_node(&mut scope, child, handles);
}
}
}
}
fn apply_flow(tree: &mut UiTreeBuilder, entity: freecs::Entity, flow: &Option<FlowSpec>) {
let Some(spec) = flow else {
return;
};
if let Some(node) = tree.world_mut().ui.get_ui_layout_node_mut(entity) {
node.flow_layout = Some(FlowLayout {
direction: spec.direction,
padding: spec.padding,
spacing: spec.spacing,
alignment: spec.alignment,
cross_alignment: spec.cross_alignment,
wrap: spec.wrap,
});
}
}
fn register(
handles: &mut HashMap<String, freecs::Entity>,
id: &Option<String>,
entity: freecs::Entity,
) {
if let Some(id) = id {
handles.insert(id.clone(), entity);
}
}