use std::collections::HashMap;
use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::components::{
AccessibleRole, CanvasCommand, CharStyle, CommandEntry, ContextMenuItem, ContextMenuItemDef,
ContextMenuItemKind, DataGridColumn, DataGridPoolRow, DragValueConfig, ShortcutBinding,
TextSpan, ThemeColor, UiButtonData, UiCanvasData, UiCheckboxData, UiCollapsingHeaderData,
UiColorPickerData, UiCommandPaletteData, UiContextMenuData, UiDataGridData, UiDatePickerData,
UiDepthMode, UiDragValueData, UiDropdownData, UiMenuData, UiModalDialogData, UiMultiSelectData,
UiNodeInteraction, UiPanelData, UiPanelKind, UiProgressBarData, UiPropertyGridData,
UiRadioData, UiRangeSliderData, UiRichTextData, UiRichTextEditorData, UiScrollAreaData,
UiSelectableLabelData, UiSliderData, UiTabBarData, UiTextAreaData, UiTextInputData,
UiTileContainerData, UiToggleData, UiTreeNodeData, UiTreeViewData, UiWidgetState, UndoStack,
};
use crate::ecs::ui::components::{SplitDirection, TileId, TileNode};
use crate::ecs::ui::layout_types::{FlowAlignment, FlowDirection};
use crate::ecs::ui::state::{UiBase, UiFocused, UiHover, UiPressed, UiStateTrait};
use crate::ecs::ui::types::{Anchor, Rect};
use crate::ecs::ui::units::{Ab, Rl};
use crate::render::wgpu::passes::geometry::UiLayer;
use std::collections::HashSet;
struct ContextMenuTheme {
font_size: f32,
corner_radius: f32,
button_height: f32,
}
enum ContextMenuBuilderEntry {
Item {
label: String,
shortcut: String,
},
Separator,
Submenu {
label: String,
children: Vec<ContextMenuBuilderEntry>,
},
WidgetRow {
label: String,
},
}
pub struct ContextMenuBuilder {
entries: Vec<ContextMenuBuilderEntry>,
}
impl ContextMenuBuilder {
pub fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub fn item(mut self, label: &str, shortcut: &str) -> Self {
self.entries.push(ContextMenuBuilderEntry::Item {
label: label.to_string(),
shortcut: shortcut.to_string(),
});
self
}
pub fn separator(mut self) -> Self {
self.entries.push(ContextMenuBuilderEntry::Separator);
self
}
pub fn submenu(
mut self,
label: &str,
f: impl FnOnce(ContextMenuBuilder) -> ContextMenuBuilder,
) -> Self {
let sub = f(ContextMenuBuilder::new());
self.entries.push(ContextMenuBuilderEntry::Submenu {
label: label.to_string(),
children: sub.entries,
});
self
}
pub fn widget_row(mut self, label: &str) -> Self {
self.entries.push(ContextMenuBuilderEntry::WidgetRow {
label: label.to_string(),
});
self
}
}
impl Default for ContextMenuBuilder {
fn default() -> Self {
Self::new()
}
}
impl<'a> UiTreeBuilder<'a> {
pub fn add_label(&mut self, text: &str) -> freecs::Entity {
let font_size = self.active_theme().font_size;
let text_slot = self.world_mut().resources.text_cache.add_text(text);
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.5)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done()
}
pub fn add_label_with_slot(&mut self, slot: usize) -> freecs::Entity {
let font_size = self.active_theme().font_size;
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.5)))
.with_text_slot(slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done()
}
pub fn add_label_wrapped(&mut self, text: &str) -> freecs::Entity {
let font_size = self.active_theme().font_size;
let text_slot = self.world_mut().resources.text_cache.add_text(text);
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Top)
.with_text_wrap()
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done()
}
pub fn add_dynamic_label(&mut self, text: &str) -> (freecs::Entity, usize) {
let slot = self.world_mut().resources.text_cache.add_text(text);
let entity = self.add_label_with_slot(slot);
(entity, slot)
}
pub fn add_label_colored(&mut self, text: &str, color: Vec4) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let text_slot = self.world_mut().resources.text_cache.add_text(text);
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.5)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_color::<UiBase>(color)
.without_pointer_events()
.done()
}
pub fn add_heading(&mut self, text: &str) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size * 1.4;
let text_slot = self.world_mut().resources.text_cache.add_text(text);
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.5)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::TextAccent)
.without_pointer_events()
.done()
}
pub fn add_separator(&mut self) -> freecs::Entity {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 1.0)))
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Border)
.without_pointer_events()
.done()
}
pub fn add_spacing(&mut self, amount: f32) -> freecs::Entity {
self.add_node()
.flow_child(Ab(Vec2::new(amount, amount)))
.without_pointer_events()
.done()
}
pub fn add_spring(&mut self) -> freecs::Entity {
self.add_node()
.flow_child(Ab(Vec2::new(0.0, 0.0)))
.flex_grow(1.0)
.without_pointer_events()
.done()
}
pub fn add_row(&mut self) -> freecs::Entity {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)))
.flow(FlowDirection::Horizontal, 0.0, 4.0)
.without_pointer_events()
.entity()
}
pub fn add_column(&mut self) -> freecs::Entity {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)))
.flow(FlowDirection::Vertical, 0.0, 4.0)
.without_pointer_events()
.entity()
}
pub fn add_button(&mut self, text: &str) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let button_height = theme.button_height;
let corner_radius = theme.corner_radius;
let border_width = theme.border_width;
let border_color = theme.border_color;
let text_slot = self.world_mut().resources.text_cache.add_text(text);
let button_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, border_width, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_transition::<UiPressed>(12.0, 8.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.with_text_slot(text_slot, font_size)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
button_entity,
UiWidgetState::Button(UiButtonData {
clicked: false,
text_slot,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(button_entity) {
interaction.accessible_role = Some(AccessibleRole::Button);
}
self.assign_tab_index(button_entity);
button_entity
}
pub fn add_button_colored(&mut self, text: &str, color: Vec4) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let button_height = theme.button_height;
let corner_radius = theme.corner_radius;
let border_width = theme.border_width;
let border_color = theme.border_color;
let hover_color = Vec4::new(
(color.x + 0.1).min(1.0),
(color.y + 0.1).min(1.0),
(color.z + 0.1).min(1.0),
color.w,
);
let active_color = Vec4::new(
(color.x - 0.1).max(0.0),
(color.y - 0.1).max(0.0),
(color.z - 0.1).max(0.0),
color.w,
);
let text_slot = self.world_mut().resources.text_cache.add_text(text);
let button_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, border_width, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_color::<UiBase>(color)
.with_color::<UiHover>(hover_color)
.with_color::<UiPressed>(active_color)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_transition::<UiPressed>(12.0, 8.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.with_text_slot(text_slot, font_size)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
button_entity,
UiWidgetState::Button(UiButtonData {
clicked: false,
text_slot,
}),
);
self.assign_tab_index(button_entity);
button_entity
}
pub fn add_slider(&mut self, min: f32, max: f32, initial: f32) -> freecs::Entity {
self.add_slider_configured(crate::ecs::ui::components::SliderConfig::new(
min, max, initial,
))
}
pub fn add_slider_logarithmic(&mut self, min: f32, max: f32, initial: f32) -> freecs::Entity {
self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(min, max, initial).logarithmic(),
)
}
pub fn add_slider_configured(
&mut self,
config: crate::ecs::ui::components::SliderConfig<'_>,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let slider_height = theme.slider_height;
let corner_radius = theme.corner_radius;
let font_size = theme.font_size;
let normalized = if config.logarithmic && config.min > 0.0 && config.max > config.min {
((config.initial / config.min).ln() / (config.max / config.min).ln()).clamp(0.0, 1.0)
} else {
((config.initial - config.min) / (config.max - config.min)).clamp(0.0, 1.0)
};
let text_slot = self.world_mut().resources.text_cache.add_text(format!(
"{}{:.prec$}{}",
config.prefix,
config.initial,
config.suffix,
prec = config.precision
));
let mut fill_entity = freecs::Entity::default();
let track_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, slider_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
fill_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(normalized * 100.0, 100.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::SliderFill)
.without_pointer_events()
.done();
tree.add_node()
.with_text_slot(text_slot, font_size * 0.85)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
track_entity,
UiWidgetState::Slider(UiSliderData {
min: config.min,
max: config.max,
value: config.initial,
changed: false,
fill_entity,
text_slot,
logarithmic: config.logarithmic,
precision: config.precision,
prefix: config.prefix.to_string(),
suffix: config.suffix.to_string(),
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(track_entity) {
interaction.accessible_role = Some(AccessibleRole::Slider);
}
self.assign_tab_index(track_entity);
track_entity
}
pub fn add_range_slider(
&mut self,
min: f32,
max: f32,
initial_low: f32,
initial_high: f32,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let slider_height = theme.slider_height;
let corner_radius = theme.corner_radius;
let font_size = theme.font_size;
let thumb_size = slider_height;
let range = max - min;
let low_normalized = if range.abs() > f32::EPSILON {
((initial_low - min) / range).clamp(0.0, 1.0)
} else {
0.0
};
let high_normalized = if range.abs() > f32::EPSILON {
((initial_high - min) / range).clamp(0.0, 1.0)
} else {
1.0
};
let text_slot = self
.world_mut()
.resources
.text_cache
.add_text(format!("{:.1} - {:.1}", initial_low, initial_high));
let mut fill_entity = freecs::Entity::default();
let mut low_thumb_entity = freecs::Entity::default();
let mut high_thumb_entity = freecs::Entity::default();
let track_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, slider_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
fill_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(low_normalized * 100.0, 0.0)),
Rl(Vec2::new(high_normalized * 100.0, 100.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::SliderFill)
.without_pointer_events()
.done();
low_thumb_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(low_normalized * 100.0, 0.0))
+ Ab(Vec2::new(-thumb_size * 0.5, 0.0)),
Rl(Vec2::new(low_normalized * 100.0, 100.0))
+ Ab(Vec2::new(thumb_size * 0.5, 0.0)),
)
.with_rect(thumb_size * 0.5, 1.0, Vec4::new(1.0, 1.0, 1.0, 0.3))
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
high_thumb_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(high_normalized * 100.0, 0.0))
+ Ab(Vec2::new(-thumb_size * 0.5, 0.0)),
Rl(Vec2::new(high_normalized * 100.0, 100.0))
+ Ab(Vec2::new(thumb_size * 0.5, 0.0)),
)
.with_rect(thumb_size * 0.5, 1.0, Vec4::new(1.0, 1.0, 1.0, 0.3))
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.with_text_slot(text_slot, font_size * 0.85)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
track_entity,
UiWidgetState::RangeSlider(UiRangeSliderData {
min,
max,
low_value: initial_low,
high_value: initial_high,
changed: false,
fill_entity,
low_thumb_entity,
high_thumb_entity,
text_slot,
active_thumb: None,
precision: 1,
thumb_half_size: thumb_size * 0.5,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(track_entity) {
interaction.accessible_role = Some(AccessibleRole::Slider);
}
self.assign_tab_index(track_entity);
track_entity
}
pub fn add_canvas(&mut self, size: Vec2) -> freecs::Entity {
let canvas_entity = self
.add_node()
.flow_child(Ab(size))
.with_clip()
.without_pointer_events()
.done();
self.world_mut().set_ui_widget_state(
canvas_entity,
UiWidgetState::Canvas(UiCanvasData {
commands: Vec::new(),
hit_test_enabled: false,
command_bounds: Vec::new(),
}),
);
canvas_entity
}
pub fn add_canvas_interactive(&mut self, size: Vec2) -> freecs::Entity {
let canvas_entity = self
.add_node()
.flow_child(Ab(size))
.with_clip()
.with_interaction()
.done();
self.world_mut().set_ui_widget_state(
canvas_entity,
UiWidgetState::Canvas(UiCanvasData {
commands: Vec::new(),
hit_test_enabled: true,
command_bounds: Vec::new(),
}),
);
canvas_entity
}
pub fn add_toggle(&mut self, initial: bool) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let bg_color = if initial {
theme.accent_color
} else {
theme.background_color
};
let toggle_width = theme.toggle_width;
let toggle_height = theme.toggle_height;
let knob_padding = 2.0;
let knob_size = toggle_height - knob_padding * 2.0;
let knob_travel = toggle_width - knob_size - knob_padding * 2.0;
let initial_x = if initial {
knob_padding + knob_travel
} else {
knob_padding
};
let mut knob_entity = freecs::Entity::default();
let toggle_entity = self
.add_node()
.flow_child(Ab(Vec2::new(toggle_width, toggle_height)))
.with_rect(toggle_height / 2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(bg_color)
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
knob_entity = tree
.add_node()
.window(
Ab(Vec2::new(initial_x, knob_padding)),
Ab(Vec2::new(knob_size, knob_size)),
Anchor::TopLeft,
)
.with_rect(knob_size / 2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
toggle_entity,
UiWidgetState::Toggle(UiToggleData {
value: initial,
changed: false,
animated_position: if initial { 1.0 } else { 0.0 },
knob_entity,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(toggle_entity) {
interaction.accessible_role = Some(AccessibleRole::Toggle);
}
self.assign_tab_index(toggle_entity);
toggle_entity
}
pub fn add_checkbox(&mut self, label: &str, initial: bool) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let text_slot = self.world_mut().resources.text_cache.add_text(label);
let mut inner_entity = freecs::Entity::default();
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.8)))
.flow(FlowDirection::Horizontal, 0.0, 8.0)
.entity();
self.push_parent(row_entity);
let box_entity = self
.add_node()
.flow_child(Ab(Vec2::new(20.0, 20.0)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
inner_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(3.0, 3.0)),
Ab(Vec2::new(-3.0, -3.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(corner_radius * 0.5, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Accent)
.with_visible(initial)
.without_pointer_events()
.done();
})
.done();
self.add_node()
.flow_child(Ab(Vec2::new(0.0, 20.0)))
.flex_grow(1.0)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
self.pop_parent();
self.world_mut().set_ui_widget_state(
box_entity,
UiWidgetState::Checkbox(UiCheckboxData {
value: initial,
changed: false,
inner_entity,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(box_entity) {
interaction.accessible_role = Some(AccessibleRole::Checkbox);
}
self.assign_tab_index(box_entity);
box_entity
}
pub fn add_radio(&mut self, label: &str, group_id: u32, option_index: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let border_color = theme.border_color;
let text_slot = self.world_mut().resources.text_cache.add_text(label);
let mut inner_entity = freecs::Entity::default();
let selected = option_index == 0;
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.8)))
.flow(FlowDirection::Horizontal, 0.0, 8.0)
.entity();
self.push_parent(row_entity);
let circle_entity = self
.add_node()
.flow_child(Ab(Vec2::new(20.0, 20.0)))
.with_rect(10.0, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
inner_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(5.0, 5.0)),
Ab(Vec2::new(-5.0, -5.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(5.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Accent)
.with_visible(selected)
.without_pointer_events()
.done();
})
.done();
self.add_node()
.flow_child(Ab(Vec2::new(0.0, 20.0)))
.flex_grow(1.0)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
self.pop_parent();
self.world_mut().set_ui_widget_state(
circle_entity,
UiWidgetState::Radio(UiRadioData {
group_id,
option_index,
selected,
changed: false,
inner_entity,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(circle_entity) {
interaction.accessible_role = Some(AccessibleRole::Button);
}
self.world_mut()
.resources
.retained_ui
.radio_groups
.entry(group_id)
.or_default()
.push(circle_entity);
self.assign_tab_index(circle_entity);
circle_entity
}
pub fn add_progress_bar(&mut self, initial: f32) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let slider_height = theme.slider_height;
let corner_radius = theme.corner_radius;
let normalized = initial.clamp(0.0, 1.0);
let mut fill_entity = freecs::Entity::default();
let bar_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, slider_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.without_pointer_events()
.with_children(|tree| {
fill_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(normalized * 100.0, 100.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Accent)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
bar_entity,
UiWidgetState::ProgressBar(UiProgressBarData {
value: initial,
fill_entity,
}),
);
self.world_mut().set_ui_node_interaction(
bar_entity,
UiNodeInteraction {
accessible_role: Some(AccessibleRole::ProgressBar),
..UiNodeInteraction::default()
},
);
bar_entity
}
pub fn add_collapsing_header(&mut self, label: &str, default_open: bool) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let arrow_text = if default_open { "\u{25BC}" } else { "\u{25B6}" };
let arrow_slot = self.world_mut().resources.text_cache.add_text(arrow_text);
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flex_grow(0.0)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
self.push_parent(root_entity);
let header_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 2.0)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 4.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Ab(Vec2::new(16.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(arrow_slot, font_size)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
let content_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flex_grow(0.0)
.flow(FlowDirection::Vertical, 8.0, 4.0)
.with_visible(default_open)
.entity();
self.pop_parent();
self.world_mut().set_ui_widget_state(
header_entity,
UiWidgetState::CollapsingHeader(UiCollapsingHeaderData {
open: default_open,
changed: false,
content_entity,
arrow_text_slot: arrow_slot,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(header_entity) {
interaction.accessible_role = Some(AccessibleRole::Button);
}
header_entity
}
pub fn add_scroll_area(&mut self, size: Vec2) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let corner_radius = theme.corner_radius;
let mut content_entity = freecs::Entity::default();
let mut thumb_entity = freecs::Entity::default();
let mut track_entity = freecs::Entity::default();
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, size.y)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_clip()
.with_interaction()
.with_children(|tree| {
content_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(-12.0, 0.0)) + Rl(Vec2::new(100.0, 0.0)),
)
.flow(FlowDirection::Vertical, 4.0, 4.0)
.without_pointer_events()
.entity();
track_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(-10.0, 2.0)) + Rl(Vec2::new(100.0, 0.0)),
Ab(Vec2::new(-2.0, -2.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.2))
.with_visible(false)
.without_pointer_events()
.entity();
tree.push_parent(track_entity);
thumb_entity = tree
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(8.0, 30.0)),
Anchor::TopLeft,
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Scrollbar)
.with_theme_color::<UiHover>(ThemeColor::ScrollbarHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.done();
tree.pop_parent();
})
.done();
self.world_mut().set_ui_widget_state(
root_entity,
UiWidgetState::ScrollArea(UiScrollAreaData {
scroll_offset: 0.0,
content_entity,
content_height: 0.0,
visible_height: size.y,
thumb_entity,
track_entity,
thumb_dragging: false,
thumb_drag_start_offset: 0.0,
snap_interval: None,
}),
);
root_entity
}
pub fn add_scroll_area_fill(&mut self, padding: f32, spacing: f32) -> freecs::Entity {
let mut content_entity = freecs::Entity::default();
let mut thumb_entity = freecs::Entity::default();
let mut track_entity = freecs::Entity::default();
let root_entity = self
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_clip()
.with_interaction()
.with_children(|tree| {
content_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(-12.0, 0.0)) + Rl(Vec2::new(100.0, 0.0)),
)
.flow(FlowDirection::Vertical, padding, spacing)
.without_pointer_events()
.entity();
track_entity = tree
.add_node()
.boundary(
Ab(Vec2::new(-10.0, 2.0)) + Rl(Vec2::new(100.0, 0.0)),
Ab(Vec2::new(-2.0, -2.0)) + Rl(Vec2::new(100.0, 100.0)),
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.2))
.with_visible(false)
.without_pointer_events()
.entity();
tree.push_parent(track_entity);
thumb_entity = tree
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(8.0, 30.0)),
Anchor::TopLeft,
)
.with_rect(4.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Scrollbar)
.with_theme_color::<UiHover>(ThemeColor::ScrollbarHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.done();
tree.pop_parent();
})
.done();
self.world_mut().set_ui_widget_state(
root_entity,
UiWidgetState::ScrollArea(UiScrollAreaData {
scroll_offset: 0.0,
content_entity,
content_height: 0.0,
visible_height: 0.0,
thumb_entity,
track_entity,
thumb_dragging: false,
thumb_drag_start_offset: 0.0,
snap_interval: None,
}),
);
root_entity
}
pub fn add_virtual_list(&mut self, item_height: f32, pool_size: usize) -> freecs::Entity {
use crate::ecs::ui::components::{UiVirtualListData, VirtualListPoolItem};
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flex_grow(1.0)
.entity();
self.push_parent(root_entity);
let scroll_entity = self.add_scroll_area_fill(0.0, 0.0);
let body_entity = self
.world_mut()
.widget::<UiScrollAreaData>(scroll_entity)
.map(|d| d.content_entity)
.unwrap();
self.push_parent(body_entity);
let top_spacer = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.entity();
let mut pool_items = Vec::with_capacity(pool_size);
for _ in 0..pool_size {
let container = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, item_height)))
.with_interaction()
.with_visible(false)
.entity();
pool_items.push(VirtualListPoolItem {
container_entity: container,
});
}
let bottom_spacer = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.entity();
self.pop_parent();
self.pop_parent();
self.world_mut().set_ui_widget_state(
root_entity,
UiWidgetState::VirtualList(UiVirtualListData {
item_height,
pool_size,
total_items: 0,
visible_start: 0,
scroll_entity,
body_entity,
top_spacer,
bottom_spacer,
pool_items,
selection: None,
selection_changed: false,
}),
);
root_entity
}
pub fn add_tab_bar(&mut self, labels: &[&str], initial: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let active_bg = theme.accent_color;
let inactive_bg = theme.background_color;
let corner_radius = theme.corner_radius;
let tab_height = theme.button_height;
let mut tab_entities = Vec::new();
let mut tab_text_slots = Vec::new();
let bar_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, tab_height)))
.flow(FlowDirection::Horizontal, 0.0, 2.0)
.entity();
self.push_parent(bar_entity);
for (index, label) in labels.iter().enumerate() {
let is_active = index == initial;
let bg = if is_active { active_bg } else { inactive_bg };
let text_slot = self.world_mut().resources.text_cache.add_text(*label);
tab_text_slots.push(text_slot);
let tab_entity = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, tab_height)))
.flex_grow(1.0)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(bg)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.with_text_slot(text_slot, font_size)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(tab_entity) {
interaction.accessible_role = Some(AccessibleRole::Tab);
}
tab_entities.push(tab_entity);
}
self.pop_parent();
self.world_mut().set_ui_widget_state(
bar_entity,
UiWidgetState::TabBar(UiTabBarData {
selected_tab: initial,
changed: false,
tab_entities,
tab_text_slots,
}),
);
bar_entity
}
pub fn add_breadcrumb(&mut self, segments: &[&str]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let text_color = theme.text_color;
let accent_color = theme.accent_color;
let separator_color = Vec4::new(
text_color.x * 0.5,
text_color.y * 0.5,
text_color.z * 0.5,
text_color.w,
);
let mut segment_entities = Vec::new();
let mut segment_text_slots = Vec::new();
let bar_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 28.0)))
.flow(FlowDirection::Horizontal, 0.0, 2.0)
.entity();
self.push_parent(bar_entity);
for (index, segment) in segments.iter().enumerate() {
if index > 0 {
let separator_slot = self.world_mut().resources.text_cache.add_text("\u{203A}");
self.add_node()
.with_text_slot(separator_slot, font_size)
.with_color::<UiBase>(separator_color)
.flow_child(Ab(Vec2::new(0.0, 28.0)))
.without_pointer_events()
.done();
}
let text_slot = self.world_mut().resources.text_cache.add_text(*segment);
segment_text_slots.push(text_slot);
let is_last = index == segments.len() - 1;
let color = if is_last { text_color } else { accent_color };
let segment_entity = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, 28.0)))
.with_children(|tree| {
tree.add_node()
.with_text_slot(text_slot, font_size)
.with_color::<UiBase>(color)
.without_pointer_events()
.done();
});
let segment_entity = if is_last {
segment_entity.done()
} else {
segment_entity
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.done()
};
segment_entities.push(segment_entity);
}
self.pop_parent();
self.world_mut().set_ui_widget_state(
bar_entity,
UiWidgetState::Breadcrumb(crate::ecs::ui::components::UiBreadcrumbData {
segments: segments.iter().map(|s| s.to_string()).collect(),
clicked_segment: None,
changed: false,
segment_entities,
segment_text_slots,
}),
);
bar_entity
}
pub fn add_splitter(
&mut self,
direction: crate::ecs::ui::components::SplitDirection,
initial_ratio: f32,
) -> freecs::Entity {
use crate::ecs::ui::components::SplitDirection;
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let divider_color = theme.border_color;
let divider_hover_color = theme.accent_color;
let divider_thickness = 4.0;
let ratio = initial_ratio.clamp(0.05, 0.95);
let container_entity = self
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.entity();
self.push_parent(container_entity);
let (p1_1, p1_2, d1, d2, p2_1, p2_2) = match direction {
SplitDirection::Horizontal => (
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(ratio * 100.0, 100.0)) + Ab(Vec2::new(-divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(ratio * 100.0, 0.0)) + Ab(Vec2::new(-divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(ratio * 100.0, 100.0)) + Ab(Vec2::new(divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(ratio * 100.0, 0.0)) + Ab(Vec2::new(divider_thickness * 0.5, 0.0)),
Rl(Vec2::new(100.0, 100.0)),
),
SplitDirection::Vertical => (
Rl(Vec2::new(0.0, 0.0)),
Rl(Vec2::new(100.0, ratio * 100.0)) + Ab(Vec2::new(0.0, -divider_thickness * 0.5)),
Rl(Vec2::new(0.0, ratio * 100.0)) + Ab(Vec2::new(0.0, -divider_thickness * 0.5)),
Rl(Vec2::new(100.0, ratio * 100.0)) + Ab(Vec2::new(0.0, divider_thickness * 0.5)),
Rl(Vec2::new(0.0, ratio * 100.0)) + Ab(Vec2::new(0.0, divider_thickness * 0.5)),
Rl(Vec2::new(100.0, 100.0)),
),
};
let first_pane = self.add_node().boundary(p1_1, p1_2).with_clip().entity();
let cursor_icon = match direction {
SplitDirection::Horizontal => winit::window::CursorIcon::ColResize,
SplitDirection::Vertical => winit::window::CursorIcon::RowResize,
};
let divider_entity = self
.add_node()
.boundary(d1, d2)
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(divider_color)
.with_color::<UiHover>(divider_hover_color)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(cursor_icon)
.done();
let second_pane = self.add_node().boundary(p2_1, p2_2).with_clip().entity();
self.pop_parent();
self.world_mut().set_ui_widget_state(
container_entity,
UiWidgetState::Splitter(crate::ecs::ui::components::UiSplitterData {
direction,
ratio,
changed: false,
first_pane,
second_pane,
divider_entity,
min_ratio: 0.05,
max_ratio: 0.95,
}),
);
container_entity
}
pub fn add_text_input(&mut self, placeholder: &str) -> freecs::Entity {
self.add_text_input_inner(placeholder, "")
}
pub fn add_text_input_with_value(
&mut self,
placeholder: &str,
initial_text: &str,
) -> freecs::Entity {
self.add_text_input_inner(placeholder, initial_text)
}
pub fn add_text_input_max_length(
&mut self,
placeholder: &str,
max_length: usize,
) -> freecs::Entity {
let entity = self.add_text_input_inner(placeholder, "");
if let Some(UiWidgetState::TextInput(data)) =
self.world_mut().get_ui_widget_state_mut(entity)
{
data.max_length = Some(max_length);
}
entity
}
fn add_text_input_inner(&mut self, placeholder: &str, initial_text: &str) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let accent_color = theme.accent_color;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let input_height = theme.button_height;
let text_slot = self.world_mut().resources.text_cache.add_text(initial_text);
let placeholder_text = placeholder.to_string();
let has_placeholder = !placeholder_text.is_empty();
let mut cursor_entity = freecs::Entity::default();
let mut selection_entity = freecs::Entity::default();
let mut placeholder_entity_out = None;
let input_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, input_height)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiFocused>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiFocused>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Text)
.with_clip()
.with_children(|tree| {
selection_entity = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 4.0)),
Ab(Vec2::new(0.0, input_height - 8.0)),
Anchor::TopLeft,
)
.with_rect(2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(
accent_color.x,
accent_color.y,
accent_color.z,
0.3,
))
.with_visible(false)
.without_pointer_events()
.done();
tree.add_node()
.window(
Ab(Vec2::new(8.0, 0.0)),
Ab(Vec2::new(0.0, input_height)) + Rl(Vec2::new(100.0, 0.0)),
Anchor::TopLeft,
)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
if has_placeholder {
let placeholder_slot = tree
.world_mut()
.resources
.text_cache
.add_text(&placeholder_text);
placeholder_entity_out = Some(
tree.add_node()
.window(
Ab(Vec2::new(8.0, 0.0)),
Ab(Vec2::new(0.0, input_height)) + Rl(Vec2::new(100.0, 0.0)),
Anchor::TopLeft,
)
.with_text_slot(placeholder_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done(),
);
}
cursor_entity = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 4.0)),
Ab(Vec2::new(2.0, input_height - 8.0)),
Anchor::TopLeft,
)
.with_rect(1.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Text)
.with_visible(false)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
input_entity,
UiWidgetState::TextInput(UiTextInputData {
text: initial_text.to_string(),
cursor_position: initial_text.chars().count(),
selection_start: None,
changed: false,
text_slot,
cursor_entity,
selection_entity,
scroll_offset: 0.0,
cursor_blink_timer: 0.0,
placeholder_entity: placeholder_entity_out,
undo_stack: UndoStack::new(100),
input_mask: crate::ecs::ui::components::InputMask::None,
max_length: None,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(input_entity) {
interaction.accessible_role = Some(AccessibleRole::TextInput);
}
if !initial_text.is_empty()
&& let Some(ph_entity) = placeholder_entity_out
&& let Some(node) = self.world_mut().get_ui_layout_node_mut(ph_entity)
{
node.visible = false;
}
self.assign_tab_index(input_entity);
input_entity
}
pub fn add_text_area(&mut self, placeholder: &str, rows: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let accent_color = theme.accent_color;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let line_height = font_size * 1.4;
let area_height = line_height * rows as f32 + 16.0;
let text_slot = self.world_mut().resources.text_cache.add_text("");
let placeholder_text = placeholder.to_string();
let has_placeholder = !placeholder_text.is_empty();
let mut cursor_entity = freecs::Entity::default();
let mut selection_pool = Vec::new();
let mut placeholder_entity_out = None;
let input_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, area_height)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiFocused>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiFocused>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Text)
.with_clip()
.with_children(|tree| {
for _ in 0..rows + 1 {
let sel = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 0.0)),
Ab(Vec2::new(0.0, line_height)),
Anchor::TopLeft,
)
.with_rect(2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(
accent_color.x,
accent_color.y,
accent_color.z,
0.3,
))
.with_visible(false)
.without_pointer_events()
.done();
selection_pool.push(sel);
}
tree.add_node()
.window(
Ab(Vec2::new(8.0, 8.0)),
Rl(Vec2::new(100.0, 100.0)) + Ab(Vec2::new(-16.0, 0.0)),
Anchor::TopLeft,
)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Top)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
if has_placeholder {
let placeholder_slot = tree
.world_mut()
.resources
.text_cache
.add_text(&placeholder_text);
placeholder_entity_out = Some(
tree.add_node()
.window(
Ab(Vec2::new(8.0, 8.0)),
Rl(Vec2::new(100.0, 100.0)) + Ab(Vec2::new(-16.0, 0.0)),
Anchor::TopLeft,
)
.with_text_slot(placeholder_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Top)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done(),
);
}
cursor_entity = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 8.0)),
Ab(Vec2::new(2.0, line_height)),
Anchor::TopLeft,
)
.with_rect(1.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Text)
.with_visible(false)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
input_entity,
UiWidgetState::TextArea(UiTextAreaData {
text: String::new(),
cursor_position: 0,
selection_start: None,
changed: false,
text_slot,
cursor_entity,
selection_pool,
scroll_offset_y: 0.0,
cursor_blink_timer: 0.0,
placeholder_entity: placeholder_entity_out,
line_height,
visible_rows: rows,
syntax_language: None,
undo_stack: UndoStack::new(100),
max_length: None,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(input_entity) {
interaction.accessible_role = Some(AccessibleRole::TextArea);
}
self.assign_tab_index(input_entity);
input_entity
}
pub fn add_text_area_with_value(
&mut self,
placeholder: &str,
rows: usize,
initial_text: &str,
) -> freecs::Entity {
let entity = self.add_text_area(placeholder, rows);
self.world_mut()
.ui_text_area_set_value(entity, initial_text);
entity
}
#[cfg(feature = "syntax_highlighting")]
pub fn add_text_area_with_syntax(
&mut self,
placeholder: &str,
rows: usize,
language: &str,
) -> freecs::Entity {
let entity = self.add_text_area(placeholder, rows);
if let Some(UiWidgetState::TextArea(data)) =
self.world_mut().get_ui_widget_state_mut(entity)
{
data.syntax_language = Some(language.to_string());
}
entity
}
#[cfg(feature = "syntax_highlighting")]
pub fn add_text_area_with_syntax_and_value(
&mut self,
placeholder: &str,
rows: usize,
language: &str,
initial_text: &str,
) -> freecs::Entity {
let entity = self.add_text_area_with_syntax(placeholder, rows, language);
self.world_mut()
.ui_text_area_set_value(entity, initial_text);
entity
}
pub fn add_rich_text_editor(&mut self, placeholder: &str, rows: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let accent_color = theme.accent_color;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let line_height = font_size * 1.4;
let area_height = line_height * rows as f32 + 16.0;
let text_slot = self.world_mut().resources.text_cache.add_text("");
let placeholder_text = placeholder.to_string();
let has_placeholder = !placeholder_text.is_empty();
let mut cursor_entity = freecs::Entity::default();
let mut selection_pool = Vec::new();
let mut placeholder_entity_out = None;
let input_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, area_height)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiFocused>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiFocused>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Text)
.with_clip()
.with_children(|tree| {
for _ in 0..rows + 1 {
let sel = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 0.0)),
Ab(Vec2::new(0.0, line_height)),
Anchor::TopLeft,
)
.with_rect(2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(
accent_color.x,
accent_color.y,
accent_color.z,
0.3,
))
.with_visible(false)
.without_pointer_events()
.done();
selection_pool.push(sel);
}
tree.add_node()
.window(
Ab(Vec2::new(8.0, 8.0)),
Rl(Vec2::new(100.0, 100.0)) + Ab(Vec2::new(-16.0, 0.0)),
Anchor::TopLeft,
)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Top)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
if has_placeholder {
let placeholder_slot = tree
.world_mut()
.resources
.text_cache
.add_text(&placeholder_text);
placeholder_entity_out = Some(
tree.add_node()
.window(
Ab(Vec2::new(8.0, 8.0)),
Rl(Vec2::new(100.0, 100.0)) + Ab(Vec2::new(-16.0, 0.0)),
Anchor::TopLeft,
)
.with_text_slot(placeholder_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Top)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done(),
);
}
cursor_entity = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 8.0)),
Ab(Vec2::new(2.0, line_height)),
Anchor::TopLeft,
)
.with_rect(1.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Text)
.with_visible(false)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
input_entity,
UiWidgetState::RichTextEditor(UiRichTextEditorData {
text: String::new(),
char_styles: Vec::new(),
current_style: CharStyle::default(),
cursor_position: 0,
selection_start: None,
changed: false,
text_slot,
cursor_entity,
selection_pool,
scroll_offset_y: 0.0,
cursor_blink_timer: 0.0,
line_height,
visible_rows: rows,
placeholder_entity: placeholder_entity_out,
undo_stack: UndoStack::new(100),
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(input_entity) {
interaction.accessible_role = Some(AccessibleRole::TextArea);
}
self.assign_tab_index(input_entity);
input_entity
}
pub fn add_rich_text_editor_with_value(
&mut self,
placeholder: &str,
rows: usize,
initial_text: &str,
) -> freecs::Entity {
let entity = self.add_rich_text_editor(placeholder, rows);
self.world_mut()
.ui_rich_text_editor_set_value(entity, initial_text);
entity
}
pub fn add_dropdown(&mut self, options: &[&str], initial: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let button_height = theme.button_height;
let accent_color = theme.accent_color;
let bg_color = theme.input_background_color;
let selected_text = options.get(initial).copied().unwrap_or("");
let header_text_slot = self
.world_mut()
.resources
.text_cache
.add_text(selected_text);
let arrow_slot = self.world_mut().resources.text_cache.add_text("\u{25BC}");
let mut popup_entities = Vec::new();
let option_strings: Vec<String> = options.iter().map(|s| s.to_string()).collect();
let popup_height = options.len() as f32 * button_height;
let wrapper_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.without_pointer_events()
.entity();
self.push_parent(wrapper_entity);
let header_entity = self
.add_node()
.boundary(Ab(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(header_text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Ab(Vec2::new(20.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(arrow_slot, font_size * 0.8)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
let popup_container_entity = self
.add_node()
.window(
Rl(Vec2::new(0.0, 100.0)),
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, popup_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(30.0))
.with_visible(false)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
self.push_parent(popup_container_entity);
for (index, option) in options.iter().enumerate() {
let option_slot = self.world_mut().resources.text_cache.add_text(*option);
let is_selected = index == initial;
let option_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(if is_selected { accent_color } else { bg_color })
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.with_text_slot(option_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
popup_entities.push(option_entity);
}
self.pop_parent();
self.pop_parent();
self.world_mut().set_ui_widget_state(
header_entity,
UiWidgetState::Dropdown(UiDropdownData {
options: option_strings,
selected_index: initial,
changed: false,
open: false,
header_text_slot,
popup_entities,
popup_container_entity,
hovered_index: None,
is_theme_dropdown: false,
searchable: false,
filter_text: String::new(),
filter_input_entity: None,
filtered_indices: Vec::new(),
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(header_entity) {
interaction.accessible_role = Some(AccessibleRole::Dropdown);
}
self.assign_tab_index(header_entity);
header_entity
}
pub fn add_dropdown_searchable(&mut self, options: &[&str], initial: usize) -> freecs::Entity {
let entity = self.add_dropdown(options, initial);
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let button_height = theme.button_height;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let popup_container = {
let widget = self.world_mut().get_ui_widget_state(entity).cloned();
if let Some(UiWidgetState::Dropdown(data)) = widget {
data.popup_container_entity
} else {
return entity;
}
};
let filter_text_slot = self.world_mut().resources.text_cache.add_text("");
let placeholder_slot = self.world_mut().resources.text_cache.add_text("Search...");
self.push_parent(popup_container);
let filter_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Text)
.with_children(|tree| {
tree.add_node()
.boundary(
Ab(Vec2::new(8.0, 0.0)),
Ab(Vec2::new(0.0, button_height)) + Rl(Vec2::new(100.0, 0.0)),
)
.with_text_slot(filter_text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.boundary(
Ab(Vec2::new(8.0, 0.0)),
Ab(Vec2::new(0.0, button_height)) + Rl(Vec2::new(100.0, 0.0)),
)
.with_text_slot(placeholder_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done();
self.pop_parent();
self.world_mut().resources.children_cache_valid = false;
if let Some(children) = self
.world_mut()
.resources
.children_cache
.get_mut(&popup_container)
&& let Some(pos) = children.iter().position(|e| *e == filter_entity)
{
children.remove(pos);
children.insert(0, filter_entity);
}
let all_indices: Vec<usize> = (0..options.len()).collect();
if let Some(UiWidgetState::Dropdown(data)) =
self.world_mut().get_ui_widget_state_mut(entity)
{
data.searchable = true;
data.filter_input_entity = Some(filter_entity);
data.filtered_indices = all_indices;
}
self.world_mut().set_ui_widget_state(
filter_entity,
UiWidgetState::TextInput(UiTextInputData {
text: String::new(),
cursor_position: 0,
selection_start: None,
changed: false,
text_slot: filter_text_slot,
cursor_entity: freecs::Entity::default(),
selection_entity: freecs::Entity::default(),
scroll_offset: 0.0,
cursor_blink_timer: 0.0,
placeholder_entity: None,
undo_stack: UndoStack::new(50),
input_mask: crate::ecs::ui::components::InputMask::None,
max_length: None,
}),
);
entity
}
pub fn add_multi_select(&mut self, options: &[&str]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let button_height = theme.button_height;
let bg_color = theme.input_background_color;
let header_text_slot = self.world_mut().resources.text_cache.add_text("0 selected");
let arrow_slot = self.world_mut().resources.text_cache.add_text("\u{25BC}");
let mut popup_entities = Vec::new();
let mut check_entities = Vec::new();
let option_strings: Vec<String> = options.iter().map(|s| s.to_string()).collect();
let popup_height = options.len() as f32 * button_height;
let wrapper_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.without_pointer_events()
.entity();
self.push_parent(wrapper_entity);
let header_entity = self
.add_node()
.boundary(Ab(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(header_text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Ab(Vec2::new(20.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(arrow_slot, font_size * 0.8)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
let popup_container_entity = self
.add_node()
.window(
Rl(Vec2::new(0.0, 100.0)),
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, popup_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(30.0))
.with_visible(false)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
self.push_parent(popup_container_entity);
for option in options.iter() {
let option_slot = self.world_mut().resources.text_cache.add_text(*option);
let check_slot = self.world_mut().resources.text_cache.add_text(" ");
let mut check_entity = freecs::Entity::default();
let option_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(bg_color)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 4.0, 0.0)
.with_children(|tree| {
check_entity = tree
.add_node()
.flow_child(Ab(Vec2::new(20.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(check_slot, font_size)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(option_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
popup_entities.push(option_entity);
check_entities.push(check_entity);
}
self.pop_parent();
self.pop_parent();
self.world_mut().set_ui_widget_state(
header_entity,
UiWidgetState::MultiSelect(UiMultiSelectData {
options: option_strings,
selected_indices: HashSet::new(),
changed: false,
open: false,
header_text_slot,
popup_entities,
popup_container_entity,
hovered_index: None,
check_entities,
}),
);
self.assign_tab_index(header_entity);
header_entity
}
pub fn add_date_picker(&mut self, year: i32, month: u32, day: u32) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let button_height = theme.button_height;
let accent_color = theme.accent_color;
let header_text = format!("{year:04}-{month:02}-{day:02}");
let header_text_slot = self.world_mut().resources.text_cache.add_text(&header_text);
let arrow_slot = self.world_mut().resources.text_cache.add_text("\u{1F4C5}");
let month_label = format_month_year(year, month);
let month_label_slot = self.world_mut().resources.text_cache.add_text(&month_label);
let cell_size = 30.0;
let grid_width = cell_size * 7.0;
let nav_height = button_height;
let grid_height = cell_size * 6.0;
let popup_height = nav_height + grid_height;
let wrapper_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.without_pointer_events()
.entity();
self.push_parent(wrapper_entity);
let header_entity = self
.add_node()
.boundary(Ab(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(header_text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Ab(Vec2::new(24.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(arrow_slot, font_size * 0.8)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
let popup_entity = self
.add_node()
.window(
Rl(Vec2::new(0.0, 100.0)),
Ab(Vec2::new(grid_width + 8.0, popup_height + 8.0)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(30.0))
.with_visible(false)
.flow(FlowDirection::Vertical, 4.0, 4.0)
.entity();
self.push_parent(popup_entity);
let nav_row = self
.add_node()
.flow_child(Ab(Vec2::new(grid_width, nav_height)))
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.without_pointer_events()
.entity();
self.push_parent(nav_row);
let prev_slot = self.world_mut().resources.text_cache.add_text("\u{25C0}");
let prev_month_entity = self
.add_node()
.flow_child(Ab(Vec2::new(cell_size, nav_height)))
.with_rect(corner_radius, 0.0, Vec4::zeros())
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(prev_slot, font_size)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(month_label_slot, font_size)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
let next_slot = self.world_mut().resources.text_cache.add_text("\u{25B6}");
let next_month_entity = self
.add_node()
.flow_child(Ab(Vec2::new(cell_size, nav_height)))
.with_rect(corner_radius, 0.0, Vec4::zeros())
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(next_slot, font_size)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.pop_parent();
let mut day_entities = Vec::with_capacity(42);
let mut day_text_slots = Vec::with_capacity(42);
let grid_entity = self
.add_node()
.flow_child(Ab(Vec2::new(grid_width, grid_height)))
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.flow_wrap()
.without_pointer_events()
.entity();
self.push_parent(grid_entity);
for _ in 0..42 {
let slot = self.world_mut().resources.text_cache.add_text("");
day_text_slots.push(slot);
let cell = self
.add_node()
.flow_child(Ab(Vec2::new(cell_size, cell_size)))
.with_rect(corner_radius, 0.0, Vec4::zeros())
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(slot, font_size * 0.85)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
day_entities.push(cell);
}
self.pop_parent();
self.pop_parent();
self.pop_parent();
populate_calendar_grid(
self.world_mut(),
year,
month,
day,
&day_text_slots,
&day_entities,
accent_color,
);
self.world_mut().set_ui_widget_state(
header_entity,
UiWidgetState::DatePicker(UiDatePickerData {
year,
month,
day,
changed: false,
open: false,
header_text_slot,
popup_entity,
day_entities,
day_text_slots,
month_label_slot,
prev_month_entity,
next_month_entity,
selected_day_entity: None,
}),
);
self.assign_tab_index(header_entity);
header_entity
}
pub fn add_menu(&mut self, label: &str, items: &[&str]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let button_height = theme.button_height;
let border_color = theme.border_color;
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let mut popup_entities = Vec::new();
let mut popup_container_entity = freecs::Entity::default();
let item_strings: Vec<String> = items.iter().map(|s| s.to_string()).collect();
let popup_height = items.len() as f32 * button_height;
let menu_button = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.with_text_slot(label_slot, font_size)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
popup_container_entity = tree
.add_node()
.window(
Rl(Vec2::new(0.0, 100.0)),
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, popup_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(30.0))
.with_visible(false)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.entity();
tree.push_parent(popup_container_entity);
for item in items {
let item_slot = tree.world_mut().resources.text_cache.add_text(*item);
let item_entity = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.with_text_slot(item_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
popup_entities.push(item_entity);
}
tree.pop_parent();
})
.done();
self.world_mut().set_ui_widget_state(
menu_button,
UiWidgetState::Menu(UiMenuData {
items: item_strings,
clicked_item: None,
open: false,
label_text_slot: label_slot,
popup_entities,
popup_container_entity,
}),
);
menu_button
}
pub fn add_floating_panel(&mut self, title: &str, rect: Rect) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let title_slot = self.world_mut().resources.text_cache.add_text(title);
let collapse_slot = self.world_mut().resources.text_cache.add_text("-");
let focus_order = self.world_mut().resources.retained_ui.next_focus_order;
self.world_mut().resources.retained_ui.next_focus_order += 1;
let mut header_entity = freecs::Entity::default();
let mut content_entity = freecs::Entity::default();
let mut collapse_button_entity = freecs::Entity::default();
let panel_entity = self
.add_node()
.window(
Ab(Vec2::new(rect.min.x, rect.min.y)),
Ab(Vec2::new(rect.width(), rect.height())),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::FloatingPanels)
.with_clip()
.with_depth(crate::ecs::ui::components::UiDepthMode::Set(
20.0 + focus_order as f32 * 10.0,
))
.with_children(|tree| {
header_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(0.0, 32.0)) + Rl(Vec2::new(100.0, 0.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::BackgroundActive)
.with_clip()
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Grab)
.with_children(|tree| {
tree.add_node()
.window(
Ab(Vec2::new(8.0, 16.0)),
Ab(Vec2::new(200.0, 20.0)),
Anchor::CenterLeft,
)
.with_text_slot(title_slot, font_size * 0.85)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Accent)
.without_pointer_events()
.done();
collapse_button_entity = tree
.add_node()
.window(
Rl(Vec2::new(100.0, 50.0)) + Ab(Vec2::new(-8.0, 0.0)),
Ab(Vec2::new(20.0, 20.0)),
Anchor::CenterRight,
)
.with_rect(2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::BackgroundHover)
.with_theme_color::<UiHover>(ThemeColor::Accent)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.window(
Rl(Vec2::new(50.0, 50.0)),
Ab(Vec2::new(20.0, 20.0)),
Anchor::Center,
)
.with_text_slot(collapse_slot, font_size * 0.8)
.with_text_alignment(
TextAlignment::Center,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
})
.done();
content_entity = tree
.add_node()
.boundary(Ab(Vec2::new(0.0, 32.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 8.0, 4.0)
.with_clip()
.without_pointer_events()
.entity();
})
.done();
self.world_mut().set_ui_widget_state(
panel_entity,
UiWidgetState::Panel(UiPanelData {
title: title.to_string(),
title_text_slot: title_slot,
content_entity,
header_entity,
collapsed: false,
panel_kind: UiPanelKind::Floating,
focus_order,
pinned: false,
min_size: Vec2::new(150.0, 100.0),
drag_offset: None,
resize_edge: None,
resize_start_rect: None,
resize_start_mouse: None,
undocked_rect: None,
default_dock_size: 300.0,
collapse_button_entity: Some(collapse_button_entity),
collapse_button_text_slot: Some(collapse_slot),
header_visible: true,
resizable: true,
}),
);
panel_entity
}
fn build_docked_panel(
&mut self,
title: &str,
panel_kind: UiPanelKind,
default_size: f32,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let border_color = theme.border_color;
let title_slot = self.world_mut().resources.text_cache.add_text(title);
let focus_order = self.world_mut().resources.retained_ui.next_focus_order;
self.world_mut().resources.retained_ui.next_focus_order += 1;
let mut header_entity = freecs::Entity::default();
let mut content_entity = freecs::Entity::default();
let panel_entity = self
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(default_size, default_size)),
Anchor::TopLeft,
)
.with_rect(0.0, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::DockedPanels)
.with_clip()
.with_depth(crate::ecs::ui::components::UiDepthMode::Set(
10.0 + focus_order as f32 * 10.0,
))
.with_children(|tree| {
header_entity = tree
.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(0.0, 32.0)) + Rl(Vec2::new(100.0, 0.0)),
)
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::BackgroundActive)
.with_clip()
.with_interaction()
.with_cursor_icon(winit::window::CursorIcon::Grab)
.with_children(|tree| {
tree.add_node()
.window(
Ab(Vec2::new(8.0, 16.0)),
Ab(Vec2::new(200.0, 20.0)),
Anchor::CenterLeft,
)
.with_text_slot(title_slot, font_size * 0.85)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Accent)
.without_pointer_events()
.done();
})
.done();
content_entity = tree
.add_node()
.boundary(Ab(Vec2::new(0.0, 32.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 8.0, 4.0)
.with_clip()
.without_pointer_events()
.entity();
})
.done();
self.world_mut().set_ui_widget_state(
panel_entity,
UiWidgetState::Panel(UiPanelData {
title: title.to_string(),
title_text_slot: title_slot,
content_entity,
header_entity,
collapsed: false,
panel_kind,
focus_order,
pinned: false,
min_size: Vec2::new(100.0, 60.0),
drag_offset: None,
resize_edge: None,
resize_start_rect: None,
resize_start_mouse: None,
undocked_rect: None,
default_dock_size: default_size,
collapse_button_entity: None,
collapse_button_text_slot: None,
header_visible: true,
resizable: true,
}),
);
panel_entity
}
pub fn add_docked_panel_left(&mut self, title: &str, default_width: f32) -> freecs::Entity {
self.build_docked_panel(title, UiPanelKind::DockedLeft, default_width)
}
pub fn add_docked_panel_right(&mut self, title: &str, default_width: f32) -> freecs::Entity {
self.build_docked_panel(title, UiPanelKind::DockedRight, default_width)
}
pub fn add_docked_panel_top(&mut self, title: &str, default_height: f32) -> freecs::Entity {
self.build_docked_panel(title, UiPanelKind::DockedTop, default_height)
}
pub fn add_docked_panel_bottom(&mut self, title: &str, default_height: f32) -> freecs::Entity {
self.build_docked_panel(title, UiPanelKind::DockedBottom, default_height)
}
pub fn add_color_picker(&mut self, initial_color: Vec4) -> freecs::Entity {
self.add_color_picker_with_mode(
initial_color,
crate::ecs::ui::components::ColorPickerMode::Rgb,
)
}
pub fn add_color_picker_hsv(&mut self, initial_color: Vec4) -> freecs::Entity {
self.add_color_picker_with_mode(
initial_color,
crate::ecs::ui::components::ColorPickerMode::Hsv,
)
}
pub fn add_color_picker_with_mode(
&mut self,
initial_color: Vec4,
mode: crate::ecs::ui::components::ColorPickerMode,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let corner_radius = theme.corner_radius;
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flow(FlowDirection::Horizontal, 4.0, 8.0)
.entity();
self.push_parent(root_entity);
let swatch_entity = self
.add_node()
.flow_child(Ab(Vec2::new(48.0, 48.0)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(initial_color)
.without_pointer_events()
.done();
let sliders_container = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, 0.0)))
.flex_grow(1.0)
.flow(FlowDirection::Vertical, 0.0, 4.0)
.entity();
self.push_parent(sliders_container);
let slider_entities = match mode {
crate::ecs::ui::components::ColorPickerMode::Rgb => {
let slider_r = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 1.0, initial_color.x)
.precision(2)
.prefix("R: "),
);
let slider_g = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 1.0, initial_color.y)
.precision(2)
.prefix("G: "),
);
let slider_b = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 1.0, initial_color.z)
.precision(2)
.prefix("B: "),
);
let slider_a = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 1.0, initial_color.w)
.precision(2)
.prefix("A: "),
);
[slider_r, slider_g, slider_b, slider_a]
}
crate::ecs::ui::components::ColorPickerMode::Hsv => {
let hsv = crate::ecs::ui::color::Hsva::from_rgba(initial_color);
let slider_h = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 360.0, hsv.hue)
.precision(0)
.prefix("H: ")
.suffix("\u{00b0}"),
);
let slider_s = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 1.0, hsv.saturation)
.precision(2)
.prefix("S: "),
);
let slider_v = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 1.0, hsv.value)
.precision(2)
.prefix("V: "),
);
let slider_a = self.add_slider_configured(
crate::ecs::ui::components::SliderConfig::new(0.0, 1.0, initial_color.w)
.precision(2)
.prefix("A: "),
);
[slider_h, slider_s, slider_v, slider_a]
}
};
self.pop_parent();
self.pop_parent();
self.world_mut().set_ui_widget_state(
root_entity,
UiWidgetState::ColorPicker(UiColorPickerData {
color: initial_color,
changed: false,
swatch_entity,
slider_entities,
mode,
}),
);
root_entity
}
pub fn add_icon_button(
&mut self,
texture_index: u32,
icon_size: Vec2,
text: &str,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let button_height = theme.button_height;
let corner_radius = theme.corner_radius;
let border_width = theme.border_width;
let border_color = theme.border_color;
let text_slot = self.world_mut().resources.text_cache.add_text(text);
let button_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, border_width, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_transition::<UiPressed>(12.0, 8.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 8.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Ab(icon_size))
.with_image(texture_index)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Ab(Vec2::new(0.0, button_height)))
.flex_grow(1.0)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
button_entity,
UiWidgetState::Button(UiButtonData {
clicked: false,
text_slot,
}),
);
self.assign_tab_index(button_entity);
button_entity
}
pub fn add_button_rich(&mut self, spans: &[TextSpan]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let button_height = theme.button_height;
let corner_radius = theme.corner_radius;
let border_width = theme.border_width;
let border_color = theme.border_color;
let text_color = theme.text_color;
let button_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, border_width, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_theme_color::<UiPressed>(ThemeColor::BackgroundActive)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_transition::<UiPressed>(12.0, 8.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.entity();
self.push_parent(button_entity);
let span_container = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.flow(FlowDirection::Horizontal, 0.0, 8.0)
.flow_wrap()
.without_pointer_events()
.entity();
self.push_parent(span_container);
for span in spans {
let text_slot = self.world_mut().resources.text_cache.add_text(&span.text);
let mut span_font_size = span.font_size_override.unwrap_or(font_size);
let span_color = span.color.unwrap_or(text_color);
let span_font_index = span.font_index.unwrap_or(0);
if span.bold {
span_font_size *= 1.05;
}
let outline_width = if span.bold { 0.4 } else { 0.0 };
let outline_color = if span.bold {
span_color
} else {
Vec4::new(0.0, 0.0, 0.0, 0.0)
};
let span_wrapper = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, button_height)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.without_pointer_events()
.entity();
self.push_parent(span_wrapper);
let entity = self.add_node().done();
if let Some(content) = self.world_mut().get_ui_node_content_mut(entity) {
*content = crate::ecs::ui::components::UiNodeContent::Text {
text_slot,
font_index: span_font_index,
font_size_override: Some(span_font_size),
outline_color,
outline_width,
alignment: TextAlignment::Left,
vertical_alignment: VerticalAlignment::Middle,
overflow: crate::ecs::ui::components::TextOverflow::default(),
};
}
if let Some(node) = self.world_mut().get_ui_layout_node_mut(entity) {
node.flow_child_size = Some(Ab(Vec2::new(0.0, button_height)).into());
node.auto_size = crate::ecs::ui::components::AutoSizeMode::Width;
node.pointer_events = false;
}
if let Some(color) = self.world_mut().get_ui_node_color_mut(entity) {
color.colors[UiBase::INDEX] = Some(span_color);
}
if span.color.is_none() {
let mut binding = crate::ecs::ui::components::UiThemeBinding::default();
binding.color_roles[UiBase::INDEX] = Some(ThemeColor::Text);
self.world_mut().set_ui_theme_binding(entity, binding);
}
self.pop_parent();
}
self.pop_parent();
self.pop_parent();
self.world_mut().set_ui_widget_state(
button_entity,
UiWidgetState::Button(UiButtonData {
clicked: false,
text_slot: 0,
}),
);
self.assign_tab_index(button_entity);
button_entity
}
pub fn add_image_node(&mut self, texture_index: u32, size: Vec2) -> freecs::Entity {
self.add_node()
.flow_child(Ab(size))
.with_image(texture_index)
.with_color::<UiBase>(Vec4::new(1.0, 1.0, 1.0, 1.0))
.without_pointer_events()
.done()
}
pub fn add_theme_dropdown(&mut self) -> freecs::Entity {
let preset_names: Vec<String> = self
.world_mut()
.resources
.retained_ui
.theme_state
.presets
.iter()
.map(|theme| theme.name.clone())
.collect();
let selected = self
.world_mut()
.resources
.retained_ui
.theme_state
.selected_preset_index;
let labels: Vec<&str> = preset_names.iter().map(|s| s.as_str()).collect();
let entity = self.add_dropdown(&labels, selected.unwrap_or(0));
if let Some(UiWidgetState::Dropdown(data)) =
self.world_mut().get_ui_widget_state_mut(entity)
{
data.is_theme_dropdown = true;
}
entity
}
pub fn add_selectable_label(&mut self, text: &str, group_id: Option<u32>) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let text_slot = self.world_mut().resources.text_cache.add_text(text);
let label_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, font_size * 1.5)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::new(8.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done();
self.world_mut().set_ui_widget_state(
label_entity,
UiWidgetState::SelectableLabel(UiSelectableLabelData {
selected: false,
changed: false,
text_slot,
group_id,
}),
);
if let Some(gid) = group_id {
self.world_mut()
.resources
.retained_ui
.selectable_label_groups
.entry(gid)
.or_default()
.push(label_entity);
}
label_entity
}
pub fn add_drag_value(&mut self, min: f32, max: f32, initial: f32) -> freecs::Entity {
self.add_drag_value_configured(DragValueConfig::new(min, max, initial))
}
pub fn add_drag_value_configured(&mut self, config: DragValueConfig<'_>) -> freecs::Entity {
let initial = config.initial;
let min = config.min;
let max = config.max;
let speed = config.speed;
let precision = config.precision;
let prefix = config.prefix;
let suffix = config.suffix;
let show_arrows = config.show_arrows;
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let accent_color = theme.accent_color;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let input_height = theme.button_height;
let display_text = format!("{prefix}{initial:.prec$}{suffix}", prec = precision);
let text_slot = self
.world_mut()
.resources
.text_cache
.add_text(&display_text);
let mut cursor_entity = freecs::Entity::default();
let mut selection_entity = freecs::Entity::default();
let wrapper_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, input_height)))
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.without_pointer_events()
.entity();
self.push_parent(wrapper_entity);
let root_entity = self
.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)) + Ab(Vec2::new(0.0, 0.0)))
.flex_grow(1.0)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiFocused>(ThemeColor::InputBackgroundFocused)
.with_interaction()
.with_transition::<UiFocused>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::EwResize)
.with_clip()
.with_children(|tree| {
selection_entity = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 4.0)),
Ab(Vec2::new(0.0, input_height - 8.0)),
Anchor::TopLeft,
)
.with_rect(2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(
accent_color.x,
accent_color.y,
accent_color.z,
0.3,
))
.with_visible(false)
.without_pointer_events()
.done();
tree.add_node()
.window(
Ab(Vec2::new(8.0, 0.0)),
Ab(Vec2::new(0.0, input_height)) + Rl(Vec2::new(100.0, 0.0)),
Anchor::TopLeft,
)
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
cursor_entity = tree
.add_node()
.window(
Ab(Vec2::new(8.0, 4.0)),
Ab(Vec2::new(2.0, input_height - 8.0)),
Anchor::TopLeft,
)
.with_rect(1.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Text)
.with_visible(false)
.without_pointer_events()
.done();
})
.done();
let mut up_entity = None;
let mut down_entity = None;
if show_arrows {
let arrow_width = 18.0;
let half_height = input_height / 2.0;
let arrow_col = self
.add_node()
.flow_child(Ab(Vec2::new(arrow_width, input_height)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.without_pointer_events()
.entity();
self.push_parent(arrow_col);
let up_slot = self.world_mut().resources.text_cache.add_text("\u{25B2}");
up_entity = Some(
self.add_node()
.flow_child(Ab(Vec2::new(arrow_width, half_height)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(up_slot, font_size * 0.5)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done(),
);
let down_slot = self.world_mut().resources.text_cache.add_text("\u{25BC}");
down_entity = Some(
self.add_node()
.flow_child(Ab(Vec2::new(arrow_width, half_height)))
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::InputBackground)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::zeros()), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(down_slot, font_size * 0.5)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done(),
);
self.pop_parent();
}
self.pop_parent();
let step_val = speed * 10.0;
self.world_mut().set_ui_widget_state(
root_entity,
UiWidgetState::DragValue(UiDragValueData {
value: initial,
min,
max,
speed,
precision,
prefix: prefix.to_string(),
suffix: suffix.to_string(),
changed: false,
text_slot,
editing: false,
cursor_entity,
selection_entity,
edit_text: String::new(),
cursor_position: 0,
selection_start: None,
cursor_blink_timer: 0.0,
scroll_offset: 0.0,
drag_start_value: initial,
undo_stack: UndoStack::new(100),
up_entity,
down_entity,
step: step_val,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(root_entity) {
interaction.accessible_role = Some(AccessibleRole::Slider);
}
self.assign_tab_index(root_entity);
root_entity
}
pub fn add_context_menu(&mut self, items: &[(&str, Option<&str>)]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let button_height = theme.button_height;
let mut item_entities = Vec::new();
let context_items: Vec<ContextMenuItem> = items
.iter()
.map(|(label, shortcut)| ContextMenuItem {
label: label.to_string(),
shortcut: shortcut.map(|s| s.to_string()),
separator: label.is_empty(),
})
.collect();
let popup_height = items.iter().fold(0.0f32, |acc, (label, _)| {
if label.is_empty() {
acc + 5.0
} else {
acc + button_height
}
});
let popup_entity = self
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(200.0, popup_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(35.0))
.with_visible(false)
.flow(FlowDirection::Vertical, 2.0, 0.0)
.entity();
self.push_parent(popup_entity);
for (label, shortcut) in items {
if label.is_empty() {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 1.0)))
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Border)
.without_pointer_events()
.done();
item_entities.push(freecs::Entity::default());
continue;
}
let label_slot = self.world_mut().resources.text_cache.add_text(*label);
let item_entity = if let Some(sc) = shortcut {
let sc_slot = self.world_mut().resources.text_cache.add_text(*sc);
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.auto_size_padding(Vec2::new(4.0, 0.0))
.with_text_slot(sc_slot, font_size * 0.85)
.with_text_alignment(TextAlignment::Right, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done()
} else {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::new(8.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done()
};
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(item_entity) {
interaction.accessible_role = Some(AccessibleRole::MenuItem);
}
item_entities.push(item_entity);
}
self.pop_parent();
let menu_entity = popup_entity;
self.world_mut()
.set_ui_node_interaction(menu_entity, UiNodeInteraction::default());
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(menu_entity) {
interaction.accessible_role = Some(AccessibleRole::Menu);
}
self.world_mut().set_ui_widget_state(
menu_entity,
UiWidgetState::ContextMenu(UiContextMenuData {
items: context_items,
open: false,
clicked_item: None,
popup_entity,
item_entities,
item_defs: Vec::new(),
}),
);
menu_entity
}
pub fn add_context_menu_from_builder(&mut self, builder: ContextMenuBuilder) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let button_height = theme.button_height;
let mut command_counter = 0usize;
let cm_theme = ContextMenuTheme {
font_size,
corner_radius,
button_height,
};
let popup_entity =
self.build_context_menu_popup(&builder.entries, &mut command_counter, &cm_theme, 0);
let item_defs = if let Some(UiWidgetState::ContextMenu(data)) =
self.world_mut().get_ui_widget_state(popup_entity)
{
data.item_defs.clone()
} else {
Vec::new()
};
let item_entities: Vec<freecs::Entity> =
item_defs.iter().map(|def| def.row_entity).collect();
self.world_mut()
.set_ui_node_interaction(popup_entity, UiNodeInteraction::default());
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(popup_entity) {
interaction.accessible_role = Some(AccessibleRole::Menu);
}
self.world_mut().set_ui_widget_state(
popup_entity,
UiWidgetState::ContextMenu(UiContextMenuData {
items: Vec::new(),
open: false,
clicked_item: None,
popup_entity,
item_entities,
item_defs,
}),
);
popup_entity
}
fn build_context_menu_popup(
&mut self,
entries: &[ContextMenuBuilderEntry],
command_counter: &mut usize,
theme: &ContextMenuTheme,
depth_level: u32,
) -> freecs::Entity {
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let button_height = theme.button_height;
let border_color = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme()
.border_color;
let popup_height = entries.iter().fold(0.0f32, |acc, entry| match entry {
ContextMenuBuilderEntry::Separator => acc + 5.0,
_ => acc + button_height,
});
let popup_entity = self
.add_node()
.window(
Ab(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(200.0, popup_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(36.0 + depth_level as f32))
.with_visible(false)
.flow(FlowDirection::Vertical, 2.0, 0.0)
.entity();
self.push_parent(popup_entity);
let mut item_defs = Vec::new();
for entry in entries {
match entry {
ContextMenuBuilderEntry::Separator => {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 1.0)))
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Border)
.without_pointer_events()
.done();
item_defs.push(ContextMenuItemDef {
label: String::new(),
shortcut: String::new(),
kind: ContextMenuItemKind::Separator,
row_entity: freecs::Entity::default(),
command_id: None,
binding: None,
});
}
ContextMenuBuilderEntry::Item { label, shortcut } => {
let cmd_id = *command_counter;
*command_counter += 1;
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let row_entity = if shortcut.is_empty() {
self.add_node()
.flow_child(
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.with_children(|tree| {
tree.add_node()
.boundary(Ab(Vec2::new(8.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_text_slot(label_slot, font_size)
.with_text_alignment(
TextAlignment::Left,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
})
.done()
} else {
let sc_slot = self.world_mut().resources.text_cache.add_text(shortcut);
self.add_node()
.flow_child(
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(
TextAlignment::Left,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.auto_size_padding(Vec2::new(4.0, 0.0))
.with_text_slot(sc_slot, font_size * 0.85)
.with_text_alignment(
TextAlignment::Right,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done()
};
item_defs.push(ContextMenuItemDef {
label: label.clone(),
shortcut: shortcut.clone(),
kind: ContextMenuItemKind::Action,
row_entity,
command_id: Some(cmd_id),
binding: ShortcutBinding::parse(shortcut),
});
}
ContextMenuBuilderEntry::Submenu { label, children } => {
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let arrow_slot = self.world_mut().resources.text_cache.add_text("\u{25B6}");
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Ab(Vec2::new(16.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)))
.with_text_slot(arrow_slot, font_size * 0.85)
.with_text_alignment(
TextAlignment::Center,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done();
self.pop_parent();
let child_popup = self.build_context_menu_popup(
children,
command_counter,
theme,
depth_level + 1,
);
self.push_parent(popup_entity);
let child_defs = if let Some(UiWidgetState::ContextMenu(data)) =
self.world_mut().get_ui_widget_state(child_popup)
{
data.item_defs.clone()
} else {
Vec::new()
};
item_defs.push(ContextMenuItemDef {
label: label.clone(),
shortcut: String::new(),
kind: ContextMenuItemKind::Submenu {
children: child_defs,
popup_entity: child_popup,
open: false,
},
row_entity,
command_id: None,
binding: None,
});
}
ContextMenuBuilderEntry::WidgetRow { label } => {
let cmd_id = *command_counter;
*command_counter += 1;
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let mut content_entity = freecs::Entity::default();
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, button_height)))
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_interaction()
.flow_with_alignment(
FlowDirection::Horizontal,
8.0,
0.0,
FlowAlignment::Start,
FlowAlignment::Center,
)
.with_children(|tree| {
tree.add_node()
.flow_child(Ab(Vec2::new(0.0, button_height)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
content_entity = tree
.add_node()
.flow_child(Ab(Vec2::new(0.0, button_height)))
.flow_with_alignment(
FlowDirection::Horizontal,
0.0,
4.0,
FlowAlignment::End,
FlowAlignment::Center,
)
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.entity();
})
.done();
item_defs.push(ContextMenuItemDef {
label: label.clone(),
shortcut: String::new(),
kind: ContextMenuItemKind::Widget { content_entity },
row_entity,
command_id: Some(cmd_id),
binding: None,
});
}
}
}
self.pop_parent();
self.world_mut().set_ui_widget_state(
popup_entity,
UiWidgetState::ContextMenu(UiContextMenuData {
items: Vec::new(),
open: false,
clicked_item: None,
popup_entity,
item_entities: item_defs.iter().map(|d| d.row_entity).collect(),
item_defs,
}),
);
popup_entity
}
pub fn add_tree_view(&mut self, multi_select: bool) -> freecs::Entity {
let scroll_entity = self.add_scroll_area_fill(2.0, 0.0);
let content_entity = self
.world_mut()
.widget::<UiScrollAreaData>(scroll_entity)
.map(|d| d.content_entity)
.unwrap_or(scroll_entity);
self.world_mut().set_ui_widget_state(
scroll_entity,
UiWidgetState::TreeView(UiTreeViewData {
selected_nodes: Vec::new(),
multi_select,
node_entities: Vec::new(),
changed: false,
content_entity,
context_menu_node: None,
filter_text: String::new(),
filter_active: false,
pre_filter_expanded: std::collections::HashMap::new(),
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(scroll_entity) {
interaction.accessible_role = Some(AccessibleRole::Tree);
}
scroll_entity
}
pub fn add_tree_node(
&mut self,
tree: freecs::Entity,
parent_container: freecs::Entity,
label: &str,
depth: usize,
user_data: u64,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let row_height = font_size * 1.4;
let indent = depth as f32 * 16.0;
let arrow_slot = self.world_mut().resources.text_cache.add_text("\u{25B6}");
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let wrapper_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.without_pointer_events()
.entity();
if let Some(parent) = self.world_mut().get_parent_mut(wrapper_entity) {
*parent = crate::ecs::transform::components::Parent(Some(parent_container));
}
self.world_mut().resources.children_cache_valid = false;
self.push_parent(wrapper_entity);
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, row_height)))
.with_rect(2.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiHover>(ThemeColor::BackgroundHover)
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 4.0, 2.0)
.entity();
self.push_parent(row_entity);
if indent > 0.0 {
self.add_node()
.flow_child(Ab(Vec2::new(indent, row_height)))
.without_pointer_events()
.done();
}
let arrow_entity = self
.add_node()
.flow_child(Ab(Vec2::new(16.0, row_height)))
.with_text_slot(arrow_slot, font_size * 0.8)
.with_text_alignment(TextAlignment::Center, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
self.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
self.pop_parent();
let children_container = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.with_visible(false)
.without_pointer_events()
.entity();
self.pop_parent();
let node_entity = row_entity;
let existing_nodes = if let Some(UiWidgetState::TreeView(tree_data)) =
self.world_mut().get_ui_widget_state(tree)
{
tree_data.node_entities.clone()
} else {
Vec::new()
};
let parent_node = existing_nodes.iter().find_map(|&candidate| {
if let Some(UiWidgetState::TreeNode(nd)) =
self.world_mut().get_ui_widget_state(candidate)
&& nd.children_container == parent_container
{
return Some(candidate);
}
None
});
self.world_mut().set_ui_widget_state(
node_entity,
UiWidgetState::TreeNode(UiTreeNodeData {
label: label.to_string(),
text_slot: label_slot,
depth,
expanded: false,
selected: false,
row_entity,
arrow_entity,
arrow_text_slot: arrow_slot,
children_container,
user_data,
parent_node,
wrapper_entity,
lazy: false,
lazy_loaded: false,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(node_entity) {
interaction.accessible_role = Some(AccessibleRole::TreeItem);
}
if let Some(UiWidgetState::TreeView(tree_data)) =
self.world_mut().get_ui_widget_state_mut(tree)
{
tree_data.node_entities.push(node_entity);
}
node_entity
}
pub fn add_tree_node_lazy(
&mut self,
tree: freecs::Entity,
parent_container: freecs::Entity,
label: &str,
depth: usize,
user_data: u64,
) -> freecs::Entity {
let node = self.add_tree_node(tree, parent_container, label, depth, user_data);
if let Some(UiWidgetState::TreeNode(data)) = self.world_mut().get_ui_widget_state_mut(node)
{
data.lazy = true;
}
node
}
pub fn add_modal_dialog(&mut self, title: &str, width: f32, height: f32) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let title_slot = self.world_mut().resources.text_cache.add_text(title);
let backdrop_entity = self
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.5))
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(40.0))
.with_interaction()
.with_visible(false)
.done();
let mut content_entity = freecs::Entity::default();
let dialog_entity = self
.add_node()
.window(
Rl(Vec2::new(50.0, 50.0)) + Ab(Vec2::new(-width / 2.0, -height / 2.0)),
Ab(Vec2::new(width, height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(41.0))
.with_visible(false)
.with_children(|tree| {
tree.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(0.0, 36.0)) + Rl(Vec2::new(100.0, 0.0)),
)
.with_rect(corner_radius, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::BackgroundActive)
.without_pointer_events()
.with_children(|tree| {
tree.add_node()
.window(
Ab(Vec2::new(12.0, 18.0)),
Ab(Vec2::new(200.0, 20.0)),
Anchor::CenterLeft,
)
.with_text_slot(title_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Accent)
.without_pointer_events()
.done();
})
.done();
content_entity = tree
.add_node()
.boundary(Ab(Vec2::new(0.0, 36.0)), Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 12.0, 8.0)
.without_pointer_events()
.entity();
})
.done();
self.world_mut().set_ui_widget_state(
dialog_entity,
UiWidgetState::ModalDialog(UiModalDialogData {
title_text_slot: title_slot,
content_entity,
backdrop_entity,
ok_button: None,
cancel_button: None,
result: None,
}),
);
dialog_entity
}
pub fn add_confirm_dialog(&mut self, title: &str, message: &str) -> freecs::Entity {
let dialog_entity = self.add_modal_dialog(title, 350.0, 180.0);
let content = self
.world_mut()
.widget::<UiModalDialogData>(dialog_entity)
.map(|d| d.content_entity)
.unwrap_or(dialog_entity);
let accent_color = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
let ok_button;
let cancel_button;
{
let mut subtree =
crate::ecs::ui::builder::UiTreeBuilder::from_parent(self.world_mut(), content);
subtree.add_label(message);
subtree.add_spacing(8.0);
let button_row = subtree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 36.0)))
.flow(FlowDirection::Horizontal, 0.0, 8.0)
.without_pointer_events()
.entity();
subtree.push_parent(button_row);
cancel_button = subtree.add_button("Cancel");
ok_button = subtree.add_button_colored("OK", accent_color);
subtree.pop_parent();
if let Some(node) = subtree.world_mut().get_ui_layout_node_mut(cancel_button) {
node.flow_child_size = Some(Rl(Vec2::new(50.0, 0.0)) + Ab(Vec2::new(-4.0, 36.0)));
}
if let Some(node) = subtree.world_mut().get_ui_layout_node_mut(ok_button) {
node.flow_child_size = Some(Rl(Vec2::new(50.0, 0.0)) + Ab(Vec2::new(-4.0, 36.0)));
}
subtree.finish_subtree();
}
if let Some(UiWidgetState::ModalDialog(data)) =
self.world_mut().get_ui_widget_state_mut(dialog_entity)
{
data.ok_button = Some(ok_button);
data.cancel_button = Some(cancel_button);
}
dialog_entity
}
pub fn add_command_palette(&mut self, pool_size: usize) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let accent_color = theme.accent_color;
let corner_radius = theme.corner_radius;
let border_color = theme.border_color;
let row_height = font_size * 1.8;
let backdrop_entity = self
.add_node()
.boundary(Rl(Vec2::new(0.0, 0.0)), Rl(Vec2::new(100.0, 100.0)))
.with_rect(0.0, 0.0, Vec4::zeros())
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.3))
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(42.0))
.with_interaction()
.with_visible(false)
.done();
let dialog_width = 500.0f32;
let dialog_height = (pool_size as f32 * row_height) + row_height + 8.0;
let mut text_input_entity = freecs::Entity::default();
let mut scroll_entity = freecs::Entity::default();
let mut result_entities = Vec::with_capacity(pool_size);
let mut result_text_slots = Vec::with_capacity(pool_size);
let dialog_entity = self
.add_node()
.window(
Rl(Vec2::new(50.0, 0.0)) + Ab(Vec2::new(-dialog_width / 2.0, 80.0)),
Ab(Vec2::new(dialog_width, dialog_height)),
Anchor::TopLeft,
)
.with_rect(corner_radius, 1.0, border_color)
.with_theme_border_color(ThemeColor::Border)
.with_theme_color::<UiBase>(ThemeColor::Background)
.with_layer(UiLayer::Popups)
.with_depth(UiDepthMode::Set(43.0))
.with_visible(false)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.with_children(|tree| {
text_input_entity = tree.add_text_input("Type a command...");
if let Some(node) = tree.world_mut().get_ui_layout_node_mut(text_input_entity) {
node.flow_child_size =
Some(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, row_height)));
}
scroll_entity = tree.add_scroll_area_fill(0.0, 0.0);
let content = tree
.world_mut()
.widget::<UiScrollAreaData>(scroll_entity)
.map(|d| d.content_entity)
.unwrap_or(scroll_entity);
tree.push_parent(content);
for _ in 0..pool_size {
let label_slot = tree.world_mut().resources.text_cache.add_text("");
let shortcut_slot = tree.world_mut().resources.text_cache.add_text("");
let row = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, row_height)))
.with_rect(2.0, 0.0, Vec4::zeros())
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiHover>(Vec4::new(
accent_color.x,
accent_color.y,
accent_color.z,
0.2,
))
.with_interaction()
.with_transition::<UiHover>(8.0, 6.0)
.with_cursor_icon(winit::window::CursorIcon::Pointer)
.flow(FlowDirection::Horizontal, 8.0, 0.0)
.with_visible(false)
.with_children(|tree| {
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.flex_grow(1.0)
.with_text_slot(label_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
tree.add_node()
.flow_child(Rl(Vec2::new(0.0, 100.0)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.auto_size_padding(Vec2::new(8.0, 0.0))
.with_text_slot(shortcut_slot, font_size * 0.85)
.with_text_alignment(
TextAlignment::Right,
VerticalAlignment::Middle,
)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
})
.done();
result_entities.push(row);
result_text_slots.push(label_slot);
result_text_slots.push(shortcut_slot);
}
tree.pop_parent();
})
.done();
let palette_entity = dialog_entity;
self.world_mut()
.set_ui_node_interaction(palette_entity, UiNodeInteraction::default());
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(palette_entity) {
interaction.accessible_role = Some(AccessibleRole::Dialog);
}
self.world_mut().set_ui_widget_state(
palette_entity,
UiWidgetState::CommandPalette(UiCommandPaletteData {
commands: Vec::new(),
filter_text: String::new(),
filtered_indices: Vec::new(),
selected_index: 0,
open: false,
executed_command: None,
text_input_entity,
result_entities,
result_text_slots,
backdrop_entity,
dialog_entity,
pool_size,
scroll_entity,
}),
);
palette_entity
}
pub fn add_property_grid(&mut self, label_width: f32) -> freecs::Entity {
let grid_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 0.0)))
.flow(FlowDirection::Vertical, 4.0, 2.0)
.with_interaction()
.entity();
self.world_mut().set_ui_widget_state(
grid_entity,
UiWidgetState::PropertyGrid(UiPropertyGridData {
label_width,
row_entities: Vec::new(),
label_entities: Vec::new(),
resize_active: false,
resize_start_x: 0.0,
resize_start_width: 0.0,
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(grid_entity) {
interaction.accessible_role = Some(AccessibleRole::Grid);
}
grid_entity
}
pub fn add_property_row(
&mut self,
grid: freecs::Entity,
parent: freecs::Entity,
label: &str,
) -> freecs::Entity {
let label_width = if let Some(UiWidgetState::PropertyGrid(data)) =
self.world_mut().get_ui_widget_state(grid)
{
data.label_width
} else {
100.0
};
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let row_height = theme.button_height;
let label_slot = self.world_mut().resources.text_cache.add_text(label);
let row_entity = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, row_height)))
.flow_with_alignment(
FlowDirection::Horizontal,
4.0,
4.0,
FlowAlignment::Start,
FlowAlignment::Center,
)
.without_pointer_events()
.entity();
if let Some(p) = self.world_mut().get_parent_mut(row_entity) {
*p = crate::ecs::transform::components::Parent(Some(parent));
}
self.world_mut().resources.children_cache_valid = false;
self.push_parent(row_entity);
let label_entity = self
.add_node()
.flow_child(Ab(Vec2::new(label_width, row_height)))
.with_text_slot(label_slot, font_size * 0.9)
.with_text_alignment(TextAlignment::Right, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::TextDisabled)
.without_pointer_events()
.done();
let widget_area = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, row_height)))
.flex_grow(1.0)
.flow_with_alignment(
FlowDirection::Horizontal,
0.0,
4.0,
FlowAlignment::Start,
FlowAlignment::Center,
)
.without_pointer_events()
.entity();
self.pop_parent();
if let Some(UiWidgetState::PropertyGrid(data)) =
self.world_mut().get_ui_widget_state_mut(grid)
{
data.row_entities.push(row_entity);
data.label_entities.push(label_entity);
}
widget_area
}
pub fn add_property_slider(
&mut self,
grid: freecs::Entity,
parent: freecs::Entity,
label: &str,
min: f32,
max: f32,
initial: f32,
) -> freecs::Entity {
let widget_area = self.add_property_row(grid, parent, label);
self.push_parent(widget_area);
let widget = self.add_slider(min, max, initial);
self.pop_parent();
widget
}
pub fn add_property_toggle(
&mut self,
grid: freecs::Entity,
parent: freecs::Entity,
label: &str,
initial: bool,
) -> freecs::Entity {
let widget_area = self.add_property_row(grid, parent, label);
self.push_parent(widget_area);
let widget = self.add_toggle(initial);
self.pop_parent();
widget
}
pub fn add_property_text_input(
&mut self,
grid: freecs::Entity,
parent: freecs::Entity,
label: &str,
placeholder: &str,
) -> freecs::Entity {
let widget_area = self.add_property_row(grid, parent, label);
self.push_parent(widget_area);
let widget = self.add_text_input(placeholder);
self.pop_parent();
widget
}
pub fn add_property_dropdown(
&mut self,
grid: freecs::Entity,
parent: freecs::Entity,
label: &str,
options: &[&str],
initial: usize,
) -> freecs::Entity {
let widget_area = self.add_property_row(grid, parent, label);
self.push_parent(widget_area);
let widget = self.add_dropdown(options, initial);
self.pop_parent();
widget
}
pub fn add_property_checkbox(
&mut self,
grid: freecs::Entity,
parent: freecs::Entity,
label: &str,
initial: bool,
) -> freecs::Entity {
let widget_area = self.add_property_row(grid, parent, label);
self.push_parent(widget_area);
let widget = self.add_checkbox(label, initial);
self.pop_parent();
widget
}
pub fn add_property_drag_value(
&mut self,
grid: freecs::Entity,
parent: freecs::Entity,
label: &str,
min: f32,
max: f32,
initial: f32,
) -> freecs::Entity {
let widget_area = self.add_property_row(grid, parent, label);
self.push_parent(widget_area);
let widget = self.add_drag_value(min, max, initial);
self.pop_parent();
widget
}
pub fn add_rich_text(&mut self, spans: &[TextSpan]) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let text_color = theme.text_color;
let container = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Height)
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.flow_wrap()
.without_pointer_events()
.entity();
self.push_parent(container);
let mut span_entities = Vec::with_capacity(spans.len());
let mut span_text_slots = Vec::with_capacity(spans.len());
for span in spans {
let text_slot = self.world_mut().resources.text_cache.add_text(&span.text);
let mut span_font_size = span.font_size_override.unwrap_or(font_size);
let span_color = span.color.unwrap_or(text_color);
let span_font_index = span.font_index.unwrap_or(0);
if span.bold {
span_font_size *= 1.05;
}
let outline_width = if span.bold { 0.4 } else { 0.0 };
let outline_color = if span.bold {
span_color
} else {
Vec4::new(0.0, 0.0, 0.0, 0.0)
};
let span_wrapper = self
.add_node()
.flow_child(Ab(Vec2::new(0.0, span_font_size * 1.5)))
.auto_size(crate::ecs::ui::components::AutoSizeMode::Width)
.flow(FlowDirection::Vertical, 0.0, 0.0)
.without_pointer_events()
.entity();
self.push_parent(span_wrapper);
let entity = self.add_node().done();
if let Some(content) = self.world_mut().get_ui_node_content_mut(entity) {
*content = crate::ecs::ui::components::UiNodeContent::Text {
text_slot,
font_index: span_font_index,
font_size_override: Some(span_font_size),
outline_color,
outline_width,
alignment: TextAlignment::Left,
vertical_alignment: VerticalAlignment::Middle,
overflow: crate::ecs::ui::components::TextOverflow::default(),
};
}
if let Some(node) = self.world_mut().get_ui_layout_node_mut(entity) {
node.flow_child_size = Some(Ab(Vec2::new(0.0, span_font_size * 1.5)).into());
node.auto_size = crate::ecs::ui::components::AutoSizeMode::Width;
node.pointer_events = false;
}
if let Some(color) = self.world_mut().get_ui_node_color_mut(entity) {
color.colors[UiBase::INDEX] = Some(span_color);
}
if span.color.is_none() {
let mut binding = crate::ecs::ui::components::UiThemeBinding::default();
binding.color_roles[UiBase::INDEX] = Some(ThemeColor::Text);
self.world_mut().set_ui_theme_binding(entity, binding);
}
if span.underline {
if span.color.is_none() {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 1.0)))
.with_rect(0.0, 0.0, Vec4::zeros())
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done();
} else {
self.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 1.0)))
.with_rect(0.0, 0.0, Vec4::zeros())
.with_color::<UiBase>(span_color)
.without_pointer_events()
.done();
}
}
self.pop_parent();
span_entities.push(span_wrapper);
span_text_slots.push(text_slot);
}
self.pop_parent();
self.world_mut().set_ui_widget_state(
container,
UiWidgetState::RichText(UiRichTextData {
span_entities,
span_text_slots,
}),
);
container
}
pub fn add_property_section(&mut self, parent: freecs::Entity, label: &str) -> freecs::Entity {
let header = self.add_collapsing_header(label, true);
let reparent_entity = self
.world_mut()
.get_parent(header)
.and_then(|p| p.0)
.unwrap_or(header);
if let Some(p) = self.world_mut().get_parent_mut(reparent_entity) {
*p = crate::ecs::transform::components::Parent(Some(parent));
}
self.world_mut().resources.children_cache_valid = false;
self.world_mut()
.widget::<UiCollapsingHeaderData>(header)
.map(|d| d.content_entity)
.unwrap_or(header)
}
pub fn add_table(&mut self, headers: &[&str], widths: &[f32]) -> freecs::Entity {
let columns: Vec<DataGridColumn> = headers
.iter()
.zip(widths.iter())
.map(|(header, width)| DataGridColumn::new(header, *width))
.collect();
self.add_data_grid(&columns, 20)
}
pub fn add_data_grid(
&mut self,
columns: &[DataGridColumn],
pool_size: usize,
) -> freecs::Entity {
let theme = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme();
let font_size = theme.font_size;
let row_height = font_size * 1.8;
let total_width: f32 = columns.iter().map(|c| c.width).sum();
let root = self
.add_node()
.flow_child(Rl(Vec2::new(100.0, 100.0)))
.flow(FlowDirection::Vertical, 0.0, 0.0)
.with_rect(0.0, 0.0, Vec4::zeros())
.with_theme_color::<UiBase>(ThemeColor::Panel)
.entity();
self.push_parent(root);
let header_row = self
.add_node()
.flow_child(Ab(Vec2::new(total_width, row_height)))
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.with_rect(0.0, 0.0, Vec4::zeros())
.with_theme_color::<UiBase>(ThemeColor::PanelHeader)
.without_pointer_events()
.entity();
self.push_parent(header_row);
let mut header_entities = Vec::with_capacity(columns.len());
let mut header_text_slots = Vec::with_capacity(columns.len());
for column in columns {
let text_slot = self
.world_mut()
.resources
.text_cache
.add_text(&column.label);
header_text_slots.push(text_slot);
let header_cell = if column.sortable {
let entity = self
.add_node()
.flow_child(Ab(Vec2::new(column.width, row_height)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.with_theme_color::<UiHover>(ThemeColor::TextAccent)
.with_interaction()
.entity();
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(entity) {
interaction.cursor_icon = Some(winit::window::CursorIcon::Pointer);
}
entity
} else {
self.add_node()
.flow_child(Ab(Vec2::new(column.width, row_height)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.done()
};
header_entities.push(header_cell);
}
self.pop_parent();
let scroll = self.add_scroll_area_fill(0.0, 0.0);
let scroll_entity = scroll;
let body_entity = self
.world_mut()
.widget::<UiScrollAreaData>(scroll)
.map(|d| d.content_entity)
.unwrap_or(scroll);
self.push_parent(body_entity);
let top_spacer = self
.add_node()
.flow_child(Ab(Vec2::new(total_width, 0.0)))
.without_pointer_events()
.done();
let mut pool_rows = Vec::with_capacity(pool_size);
let accent_color = self
.world_mut()
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
let selected_bg = Vec4::new(accent_color.x, accent_color.y, accent_color.z, 0.3);
let has_editable = columns.iter().any(|c| c.editable);
for _ in 0..pool_size {
let mut row_builder = self
.add_node()
.flow_child(Ab(Vec2::new(total_width, row_height)))
.flow(FlowDirection::Horizontal, 0.0, 0.0)
.with_rect(0.0, 0.0, Vec4::zeros())
.with_color::<UiBase>(Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<crate::ecs::ui::state::UiSelected>(selected_bg)
.with_interaction();
if has_editable {
row_builder = row_builder.with_cursor_icon(winit::window::CursorIcon::Cell);
}
let row_entity = row_builder.entity();
self.push_parent(row_entity);
let mut cell_text_slots = Vec::with_capacity(columns.len());
let mut cell_entities = Vec::with_capacity(columns.len());
for column in columns {
let text_slot = self.world_mut().resources.text_cache.add_text("");
let cell_entity = self
.add_node()
.flow_child(Ab(Vec2::new(column.width, row_height)))
.with_text_slot(text_slot, font_size)
.with_text_alignment(column.alignment, VerticalAlignment::Middle)
.with_theme_color::<UiBase>(ThemeColor::Text)
.without_pointer_events()
.entity();
cell_text_slots.push(text_slot);
cell_entities.push(cell_entity);
}
self.pop_parent();
if let Some(node) = self.world_mut().get_ui_layout_node_mut(row_entity) {
node.visible = false;
}
pool_rows.push(DataGridPoolRow {
row_entity,
cell_text_slots,
cell_entities,
});
}
let bottom_spacer = self
.add_node()
.flow_child(Ab(Vec2::new(total_width, 0.0)))
.without_pointer_events()
.done();
self.pop_parent();
self.pop_parent();
self.push_parent(root);
let editing_input_entity = self.add_text_input("");
if let Some(node) = self
.world_mut()
.get_ui_layout_node_mut(editing_input_entity)
{
node.visible = false;
node.flow_child_size = None;
node.layouts[crate::ecs::ui::state::UiBase::INDEX] =
Some(crate::ecs::ui::layout_types::UiLayoutType::Window(
crate::ecs::ui::layout_types::WindowLayout {
position: Ab(Vec2::new(0.0, 0.0)).into(),
size: Ab(Vec2::new(100.0, row_height)).into(),
anchor: crate::ecs::ui::types::Anchor::TopLeft,
},
));
}
self.pop_parent();
self.world_mut().set_ui_widget_state(
root,
UiWidgetState::DataGrid(UiDataGridData {
columns: columns.to_vec(),
row_height,
pool_size,
total_rows: 0,
scroll_entity,
body_entity,
header_entities,
header_text_slots,
top_spacer,
bottom_spacer,
pool_rows,
visible_start: 0,
sort_column: None,
sort_ascending: true,
sort_changed: false,
selected_rows: std::collections::HashSet::new(),
selection_anchor: None,
selection_changed: false,
focused: false,
resize_column: None,
resize_start_x: 0.0,
resize_start_width: 0.0,
filtered_indices: None,
header_divider_entities: Vec::new(),
filter_row_entity: None,
filter_input_entities: Vec::new(),
filter_texts: Vec::new(),
editing_cell: None,
editing_input_entity: Some(editing_input_entity),
}),
);
if let Some(interaction) = self.world_mut().get_ui_node_interaction_mut(root) {
interaction.accessible_role = Some(AccessibleRole::Grid);
}
root
}
pub fn add_tile_container(&mut self, size: Vec2) -> freecs::Entity {
let container = self
.add_node()
.window(Ab(Vec2::new(0.0, 0.0)), Ab(size), Anchor::TopLeft)
.with_rect(0.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_theme_color::<UiBase>(ThemeColor::Panel)
.with_clip()
.done();
let root_tabs = TileNode::Tabs {
panes: Vec::new(),
active: 0,
};
let tiles: Vec<Option<TileNode>> = vec![Some(root_tabs)];
let data = UiTileContainerData {
tiles,
root: TileId(0),
rects: vec![Rect::default()],
dragging_splitter: None,
pending_tab_drag: None,
dragging_tab: None,
drop_preview: None,
container_entity: container,
splitter_width: 4.0,
tab_bar_height: 26.0,
next_free: Vec::new(),
hovered_close: None,
};
self.world_mut()
.set_ui_widget_state(container, UiWidgetState::TileContainer(data));
container
}
pub fn build_tiles(
&mut self,
container: freecs::Entity,
f: impl FnOnce(&mut TileBuilder<'_, 'a>),
) {
let mut builder = TileBuilder {
tree: self,
container,
};
f(&mut builder);
}
}
pub struct TileBuilder<'t, 'a> {
tree: &'t mut UiTreeBuilder<'a>,
container: freecs::Entity,
}
impl<'t, 'a> TileBuilder<'t, 'a> {
pub fn add_text(&mut self, text: impl Into<String>) -> usize {
self.tree.world_mut().resources.text_cache.add_text(text)
}
pub fn pane(&mut self, title: &str) -> Option<(TileId, freecs::Entity)> {
self.tree
.world_mut()
.ui_tile_add_pane(self.container, title)
}
pub fn pane_sibling(
&mut self,
sibling: TileId,
title: &str,
) -> Option<(TileId, freecs::Entity)> {
let tabs_id = match self.tree.world_mut().get_ui_widget_state(self.container) {
Some(UiWidgetState::TileContainer(data)) => data.find_parent_tabs(sibling)?,
_ => return None,
};
self.tree
.world_mut()
.ui_tile_add_pane_to(self.container, tabs_id, title)
}
pub fn split_from(
&mut self,
pane: TileId,
direction: SplitDirection,
ratio: f32,
title: &str,
) -> Option<(TileId, freecs::Entity)> {
let target = match self.tree.world_mut().get_ui_widget_state(self.container) {
Some(UiWidgetState::TileContainer(data)) => {
data.find_parent_tabs(pane).unwrap_or(data.root)
}
_ => return None,
};
self.tree
.world_mut()
.ui_tile_split(self.container, target, direction, ratio, title)
}
pub fn content(
&mut self,
content_entity: freecs::Entity,
f: impl FnOnce(&mut UiTreeBuilder<'a>),
) {
self.tree.push_parent(content_entity);
f(self.tree);
self.tree.pop_parent();
}
}
pub(crate) struct RangeSliderVisualUpdate {
pub fill_entity: freecs::Entity,
pub low_thumb: freecs::Entity,
pub high_thumb: freecs::Entity,
pub text_slot: usize,
pub precision: usize,
pub low_normalized: f32,
pub high_normalized: f32,
pub low_value: f32,
pub high_value: f32,
pub thumb_half: f32,
}
pub(crate) fn update_range_slider_visuals(
world: &mut crate::ecs::world::World,
update: &RangeSliderVisualUpdate,
) {
let RangeSliderVisualUpdate {
fill_entity,
low_thumb,
high_thumb,
text_slot,
precision,
low_normalized,
high_normalized,
low_value,
high_value,
thumb_half,
} = *update;
use crate::ecs::ui::layout_types::UiLayoutType;
use crate::ecs::ui::state::UiBase;
use crate::ecs::ui::units::{Ab, Rl};
if let Some(fill_node) = world.get_ui_layout_node_mut(fill_entity)
&& let Some(UiLayoutType::Boundary(boundary)) = fill_node.layouts[UiBase::INDEX].as_mut()
{
boundary.position_1 = Rl(Vec2::new(low_normalized * 100.0, 0.0)).into();
boundary.position_2 = Rl(Vec2::new(high_normalized * 100.0, 100.0)).into();
}
if let Some(low_node) = world.get_ui_layout_node_mut(low_thumb)
&& let Some(UiLayoutType::Boundary(boundary)) = low_node.layouts[UiBase::INDEX].as_mut()
{
boundary.position_1 =
Rl(Vec2::new(low_normalized * 100.0, 0.0)) + Ab(Vec2::new(-thumb_half, 0.0));
boundary.position_2 =
Rl(Vec2::new(low_normalized * 100.0, 100.0)) + Ab(Vec2::new(thumb_half, 0.0));
}
if let Some(high_node) = world.get_ui_layout_node_mut(high_thumb)
&& let Some(UiLayoutType::Boundary(boundary)) = high_node.layouts[UiBase::INDEX].as_mut()
{
boundary.position_1 =
Rl(Vec2::new(high_normalized * 100.0, 0.0)) + Ab(Vec2::new(-thumb_half, 0.0));
boundary.position_2 =
Rl(Vec2::new(high_normalized * 100.0, 100.0)) + Ab(Vec2::new(thumb_half, 0.0));
}
world.resources.text_cache.set_text(
text_slot,
format!(
"{:.prec$} - {:.prec$}",
low_value,
high_value,
prec = precision,
),
);
}
fn find_widget_content_in_defs(
defs: &[ContextMenuItemDef],
command_id: usize,
) -> Option<freecs::Entity> {
for def in defs {
if def.command_id == Some(command_id)
&& let ContextMenuItemKind::Widget { content_entity } = &def.kind
{
return Some(*content_entity);
}
if let ContextMenuItemKind::Submenu { children, .. } = &def.kind
&& let Some(found) = find_widget_content_in_defs(children, command_id)
{
return Some(found);
}
}
None
}
impl crate::ecs::world::World {
pub fn measure_ui_text_width(&self, text: &str, font_size: f32) -> f32 {
let best_idx = self
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = self
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
crate::ecs::ui::widget_systems::measure_text_width(&atlas, text, font_size)
} else {
0.0
}
}
pub fn widget<T: crate::ecs::ui::components::FromWidgetState>(
&self,
entity: freecs::Entity,
) -> Option<&T> {
T::from_widget_state(self.get_ui_widget_state(entity)?)
}
pub fn ui_changed(&self, entity: freecs::Entity) -> bool {
self.get_ui_widget_state(entity)
.is_some_and(|s| s.changed())
}
pub fn ui_button_set_text(&mut self, entity: freecs::Entity, text: &str) {
if let Some(UiWidgetState::Button(data)) = self.get_ui_widget_state(entity) {
let slot = data.text_slot;
self.resources.text_cache.set_text(slot, text);
}
}
pub fn ui_slider_set_value(&mut self, entity: freecs::Entity, value: f32) {
let update = if let Some(UiWidgetState::Slider(data)) = self.get_ui_widget_state(entity) {
let normalized = if data.logarithmic && data.min > 0.0 && data.max > data.min {
((value / data.min).ln() / (data.max / data.min).ln()).clamp(0.0, 1.0)
} else {
((value - data.min) / (data.max - data.min)).clamp(0.0, 1.0)
};
Some((
data.fill_entity,
data.text_slot,
data.min,
data.max,
normalized,
data.logarithmic,
))
} else {
None
};
if let Some((fill_entity, text_slot, min, max, normalized, logarithmic)) = update {
let clamped = if logarithmic && min > 0.0 && max > min {
min * (max / min).powf(normalized)
} else {
min + normalized * (max - min)
};
if let Some(fill_node) = self.get_ui_layout_node_mut(fill_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
fill_node.layouts[UiBase::INDEX].as_mut()
{
boundary.position_2 = Rl(Vec2::new(normalized * 100.0, 100.0)).into();
}
let precision = if logarithmic { 3 } else { 1 };
self.resources
.text_cache
.set_text(text_slot, format!("{:.prec$}", clamped, prec = precision));
if let Some(UiWidgetState::Slider(data)) = self.get_ui_widget_state_mut(entity) {
data.value = clamped;
}
}
}
pub fn ui_range_slider_set_values(&mut self, entity: freecs::Entity, low: f32, high: f32) {
let update =
if let Some(UiWidgetState::RangeSlider(data)) = self.get_ui_widget_state(entity) {
let range = data.max - data.min;
if range.abs() > f32::EPSILON {
let low_n = ((low - data.min) / range).clamp(0.0, 1.0);
let high_n = ((high - data.min) / range).clamp(0.0, 1.0);
Some((
data.fill_entity,
data.low_thumb_entity,
data.high_thumb_entity,
data.text_slot,
data.precision,
data.thumb_half_size,
low_n,
high_n,
data.min + low_n * range,
data.min + high_n * range,
))
} else {
None
}
} else {
None
};
if let Some((
fill_entity,
low_thumb,
high_thumb,
text_slot,
precision,
thumb_half,
low_n,
high_n,
clamped_low,
clamped_high,
)) = update
{
update_range_slider_visuals(
self,
&RangeSliderVisualUpdate {
fill_entity,
low_thumb,
high_thumb,
text_slot,
precision,
low_normalized: low_n,
high_normalized: high_n,
low_value: clamped_low,
high_value: clamped_high,
thumb_half,
},
);
if let Some(UiWidgetState::RangeSlider(data)) = self.get_ui_widget_state_mut(entity) {
data.low_value = clamped_low;
data.high_value = clamped_high;
}
}
}
pub fn ui_breadcrumb_set_segments(&mut self, entity: freecs::Entity, segments: &[&str]) {
let text_slots: Vec<usize> =
if let Some(UiWidgetState::Breadcrumb(data)) = self.get_ui_widget_state_mut(entity) {
data.segments = segments.iter().map(|s| s.to_string()).collect();
data.segment_text_slots.clone()
} else {
return;
};
for (index, text_slot) in text_slots.iter().enumerate() {
if index < segments.len() {
self.resources
.text_cache
.set_text(*text_slot, segments[index]);
}
}
}
pub fn ui_toggle_set_value(&mut self, entity: freecs::Entity, value: bool) {
if let Some(UiWidgetState::Toggle(data)) = self.get_ui_widget_state_mut(entity) {
data.value = value;
}
}
pub fn ui_radio_group_value(&self, group_id: u32) -> Option<usize> {
let entities: Vec<freecs::Entity> = self
.query_entities(crate::ecs::world::UI_WIDGET_STATE)
.collect();
for entity in entities {
if let Some(UiWidgetState::Radio(data)) = self.get_ui_widget_state(entity)
&& data.group_id == group_id
&& data.selected
{
return Some(data.option_index);
}
}
None
}
pub fn ui_progress_bar_set_value(&mut self, entity: freecs::Entity, value: f32) {
let update =
if let Some(UiWidgetState::ProgressBar(data)) = self.get_ui_widget_state(entity) {
Some(data.fill_entity)
} else {
None
};
if let Some(fill_entity) = update {
let normalized = value.clamp(0.0, 1.0);
if let Some(fill_node) = self.get_ui_layout_node_mut(fill_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
fill_node.layouts[UiBase::INDEX].as_mut()
{
boundary.position_2 = Rl(Vec2::new(normalized * 100.0, 100.0)).into();
}
if let Some(UiWidgetState::ProgressBar(data)) = self.get_ui_widget_state_mut(entity) {
data.value = normalized;
}
}
}
pub fn ui_scroll_area_set_offset(&mut self, entity: freecs::Entity, offset: f32) {
if let Some(UiWidgetState::ScrollArea(data)) = self.get_ui_widget_state_mut(entity) {
data.scroll_offset = offset;
}
}
pub fn ui_scroll_area_set_snap(&mut self, entity: freecs::Entity, interval: Option<f32>) {
if let Some(UiWidgetState::ScrollArea(data)) = self.get_ui_widget_state_mut(entity) {
data.snap_interval = interval;
}
}
pub fn ui_virtual_list_set_count(&mut self, entity: freecs::Entity, count: usize) {
if let Some(UiWidgetState::VirtualList(data)) = self.get_ui_widget_state_mut(entity) {
data.total_items = count;
}
}
pub fn ui_virtual_list_selection_changed(&self, entity: freecs::Entity) -> bool {
if let Some(UiWidgetState::VirtualList(data)) = self.get_ui_widget_state(entity) {
data.selection_changed
} else {
false
}
}
pub fn ui_tab_bar_set_value(&mut self, entity: freecs::Entity, index: usize) {
let update = if let Some(UiWidgetState::TabBar(data)) = self.get_ui_widget_state(entity) {
if index < data.tab_entities.len() && index != data.selected_tab {
Some((
data.tab_entities[data.selected_tab],
data.tab_entities[index],
))
} else {
None
}
} else {
None
};
if let Some((old_entity, new_entity)) = update {
let theme = self.resources.retained_ui.theme_state.active_theme();
let active_bg = theme.accent_color;
let inactive_bg = theme.background_color;
if let Some(color) = self.get_ui_node_color_mut(old_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(inactive_bg);
}
if let Some(color) = self.get_ui_node_color_mut(new_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(active_bg);
}
if let Some(UiWidgetState::TabBar(data)) = self.get_ui_widget_state_mut(entity) {
data.selected_tab = index;
}
}
}
pub fn ui_text_input_set_value(&mut self, entity: freecs::Entity, text: &str) {
let (text_slot, placeholder_entity) =
if let Some(UiWidgetState::TextInput(data)) = self.get_ui_widget_state(entity) {
(Some(data.text_slot), data.placeholder_entity)
} else {
(None, None)
};
if let Some(slot) = text_slot {
self.resources.text_cache.set_text(slot, text);
if let Some(UiWidgetState::TextInput(data)) = self.get_ui_widget_state_mut(entity) {
data.text = text.to_string();
data.cursor_position = text.chars().count();
data.selection_start = None;
}
if let Some(ph_entity) = placeholder_entity
&& let Some(node) = self.get_ui_layout_node_mut(ph_entity)
{
node.visible = text.is_empty();
}
}
}
pub fn ui_text_input_set_mask(
&mut self,
entity: freecs::Entity,
mask: crate::ecs::ui::components::InputMask,
) {
if let Some(UiWidgetState::TextInput(data)) = self.get_ui_widget_state_mut(entity) {
data.input_mask = mask;
}
}
pub fn ui_text_input_submitted(&self, entity: freecs::Entity) -> Option<String> {
for event in &self.resources.retained_ui.frame_events {
if let crate::ecs::ui::resources::UiEvent::TextInputSubmitted {
entity: event_entity,
text,
} = event
&& *event_entity == entity
{
return Some(text.clone());
}
}
None
}
pub fn ui_dropdown_set_value(&mut self, entity: freecs::Entity, index: usize) {
let update = if let Some(UiWidgetState::Dropdown(data)) = self.get_ui_widget_state(entity) {
if index < data.options.len() && index != data.selected_index {
Some((data.header_text_slot, data.options[index].clone()))
} else {
None
}
} else {
None
};
if let Some((text_slot, selected_text)) = update {
self.resources.text_cache.set_text(text_slot, selected_text);
if let Some(UiWidgetState::Dropdown(data)) = self.get_ui_widget_state_mut(entity) {
data.selected_index = index;
}
}
}
pub fn ui_panel_set_pinned(&mut self, entity: freecs::Entity, pinned: bool) {
if let Some(UiWidgetState::Panel(data)) = self.get_ui_widget_state_mut(entity) {
data.pinned = pinned;
}
}
pub fn ui_panel_set_header_visible(&mut self, entity: freecs::Entity, visible: bool) {
if let Some(UiWidgetState::Panel(data)) = self.get_ui_widget_state_mut(entity) {
let header = data.header_entity;
let content = data.content_entity;
data.header_visible = visible;
self.ui_set_visible(header, visible);
if let Some(node) = self.get_ui_layout_node_mut(content)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(ref mut boundary)) =
node.layouts[UiBase::INDEX]
{
let offset_y = if visible { 32.0 } else { 0.0 };
boundary.position_1 = crate::ecs::ui::units::Ab(Vec2::new(0.0, offset_y)).into();
}
}
}
pub fn ui_reserved_areas(&self) -> &crate::ecs::ui::resources::ReservedAreas {
&self.resources.retained_ui.reserved_areas
}
pub fn ui_color_picker_set_value(&mut self, entity: freecs::Entity, color: Vec4) {
let update =
if let Some(UiWidgetState::ColorPicker(data)) = self.get_ui_widget_state(entity) {
let sliders = data.slider_entities;
let mode = data.mode;
let swatch = data.swatch_entity;
Some((sliders, mode, swatch))
} else {
None
};
if let Some((sliders, mode, swatch)) = update {
match mode {
crate::ecs::ui::components::ColorPickerMode::Rgb => {
self.ui_slider_set_value(sliders[0], color.x);
self.ui_slider_set_value(sliders[1], color.y);
self.ui_slider_set_value(sliders[2], color.z);
self.ui_slider_set_value(sliders[3], color.w);
}
crate::ecs::ui::components::ColorPickerMode::Hsv => {
let hsv = crate::ecs::ui::color::Hsva::from_rgba(color);
self.ui_slider_set_value(sliders[0], hsv.hue);
self.ui_slider_set_value(sliders[1], hsv.saturation);
self.ui_slider_set_value(sliders[2], hsv.value);
self.ui_slider_set_value(sliders[3], color.w);
}
}
if let Some(swatch_color) = self.get_ui_node_color_mut(swatch) {
swatch_color.colors[UiBase::INDEX] = Some(color);
}
if let Some(UiWidgetState::ColorPicker(data)) = self.get_ui_widget_state_mut(entity) {
data.color = color;
}
}
}
pub fn ui_clipboard_text(&self) -> &str {
&self.resources.retained_ui.clipboard_text
}
pub fn ui_set_clipboard_text(&mut self, text: impl Into<String>) {
self.resources.retained_ui.clipboard_text = text.into();
}
pub fn ui_events(&self) -> &[crate::ecs::ui::resources::UiEvent] {
&self.resources.retained_ui.frame_events
}
pub fn ui_events_for(
&self,
entity: freecs::Entity,
) -> impl Iterator<Item = &crate::ecs::ui::resources::UiEvent> {
use crate::ecs::ui::resources::UiEvent;
self.resources
.retained_ui
.frame_events
.iter()
.filter(move |event| match event {
UiEvent::ButtonClicked(event_entity)
| UiEvent::SliderChanged {
entity: event_entity,
..
}
| UiEvent::ToggleChanged {
entity: event_entity,
..
}
| UiEvent::CheckboxChanged {
entity: event_entity,
..
}
| UiEvent::RadioChanged {
entity: event_entity,
..
}
| UiEvent::TabChanged {
entity: event_entity,
..
}
| UiEvent::TextInputChanged {
entity: event_entity,
..
}
| UiEvent::TextInputSubmitted {
entity: event_entity,
..
}
| UiEvent::DropdownChanged {
entity: event_entity,
..
}
| UiEvent::MenuItemClicked {
entity: event_entity,
..
}
| UiEvent::ColorPickerChanged {
entity: event_entity,
..
}
| UiEvent::SelectableLabelClicked {
entity: event_entity,
..
}
| UiEvent::DragValueChanged {
entity: event_entity,
..
}
| UiEvent::ContextMenuItemClicked {
entity: event_entity,
..
}
| UiEvent::ModalClosed {
entity: event_entity,
..
}
| UiEvent::CommandPaletteExecuted {
entity: event_entity,
..
} => *event_entity == entity,
UiEvent::TreeNodeSelected {
tree: event_entity, ..
}
| UiEvent::TreeNodeToggled {
tree: event_entity, ..
}
| UiEvent::TreeNodeContextMenu {
tree: event_entity, ..
}
| UiEvent::TreeNodeExpandRequested {
tree: event_entity, ..
} => *event_entity == entity,
UiEvent::TileTabActivated {
container: event_entity,
..
}
| UiEvent::TileTabClosed {
container: event_entity,
..
}
| UiEvent::TileSplitterMoved {
container: event_entity,
..
} => *event_entity == entity,
UiEvent::DragStarted {
source: event_entity,
..
}
| UiEvent::DragCancelled {
source: event_entity,
..
} => *event_entity == entity,
UiEvent::DragDropped {
target: event_entity,
..
}
| UiEvent::CanvasClicked {
entity: event_entity,
..
} => *event_entity == entity,
UiEvent::DragEnter {
target: event_entity,
..
}
| UiEvent::DragOver {
target: event_entity,
..
}
| UiEvent::DragLeave {
target: event_entity,
..
} => *event_entity == entity,
UiEvent::ShortcutTriggered { .. } => false,
UiEvent::VirtualListItemClicked {
entity: event_entity,
..
}
| UiEvent::TextAreaChanged {
entity: event_entity,
..
}
| UiEvent::RichTextEditorChanged {
entity: event_entity,
..
}
| UiEvent::DataGridFilterChanged {
entity: event_entity,
..
}
| UiEvent::RangeSliderChanged {
entity: event_entity,
..
}
| UiEvent::DataGridCellEdited {
entity: event_entity,
..
}
| UiEvent::BreadcrumbClicked {
entity: event_entity,
..
}
| UiEvent::SplitterChanged {
entity: event_entity,
..
}
| UiEvent::MultiSelectChanged {
entity: event_entity,
..
}
| UiEvent::DatePickerChanged {
entity: event_entity,
..
} => *event_entity == entity,
})
}
pub fn ui_despawn_node(&mut self, entity: freecs::Entity) {
if !self.resources.children_cache_valid {
self.validate_and_rebuild_children_cache();
}
let mut stack = vec![entity];
let mut all_entities = Vec::new();
while let Some(current) = stack.pop() {
all_entities.push(current);
if let Some(children) = self.resources.children_cache.get(¤t) {
for child in children {
stack.push(*child);
}
}
}
for descendant in &all_entities {
if let Some(content) = self.get_ui_node_content(*descendant)
&& let crate::ecs::ui::components::UiNodeContent::Text { text_slot, .. } = content
{
let slot = *text_slot;
self.resources.text_cache.remove_text(slot);
}
if let Some(widget) = self.get_ui_widget_state(*descendant).cloned() {
match &widget {
UiWidgetState::Radio(data) => {
if let Some(group) = self
.resources
.retained_ui
.radio_groups
.get_mut(&data.group_id)
{
group.retain(|e| *e != *descendant);
}
}
UiWidgetState::SelectableLabel(data) => {
if let Some(gid) = data.group_id
&& let Some(group) = self
.resources
.retained_ui
.selectable_label_groups
.get_mut(&gid)
{
group.retain(|e| *e != *descendant);
}
}
_ => {}
}
let slots: Vec<usize> = match &widget {
UiWidgetState::Button(data) => vec![data.text_slot],
UiWidgetState::Slider(data) => vec![data.text_slot],
UiWidgetState::TextInput(data) => vec![data.text_slot],
UiWidgetState::CollapsingHeader(data) => vec![data.arrow_text_slot],
UiWidgetState::Dropdown(data) => vec![data.header_text_slot],
UiWidgetState::Menu(data) => vec![data.label_text_slot],
UiWidgetState::Panel(data) => {
let mut s = vec![data.title_text_slot];
if let Some(slot) = data.collapse_button_text_slot {
s.push(slot);
}
s
}
UiWidgetState::SelectableLabel(data) => vec![data.text_slot],
UiWidgetState::DragValue(data) => vec![data.text_slot],
UiWidgetState::TreeNode(data) => {
vec![data.text_slot, data.arrow_text_slot]
}
UiWidgetState::ModalDialog(data) => vec![data.title_text_slot],
UiWidgetState::TabBar(data) => data.tab_text_slots.clone(),
UiWidgetState::TextArea(data) => vec![data.text_slot],
UiWidgetState::RichText(data) => data.span_text_slots.clone(),
UiWidgetState::DataGrid(data) => {
let mut s = data.header_text_slots.clone();
for row in &data.pool_rows {
s.extend_from_slice(&row.cell_text_slots);
}
s
}
UiWidgetState::CommandPalette(data) => data.result_text_slots.clone(),
UiWidgetState::RichTextEditor(data) => vec![data.text_slot],
UiWidgetState::Breadcrumb(data) => data.segment_text_slots.clone(),
UiWidgetState::MultiSelect(data) => vec![data.header_text_slot],
UiWidgetState::DatePicker(data) => {
let mut s = vec![data.header_text_slot, data.month_label_slot];
s.extend_from_slice(&data.day_text_slots);
s
}
_ => vec![],
};
for slot in slots {
self.resources.text_cache.remove_text(slot);
}
}
}
crate::ecs::world::commands::despawn_recursive_immediate(self, entity);
self.resources.children_cache_valid = false;
self.resources.retained_ui.layout_dirty = true;
}
pub fn ui_clear_children(&mut self, parent: freecs::Entity) {
if !self.resources.children_cache_valid {
self.validate_and_rebuild_children_cache();
}
let children: Vec<freecs::Entity> = self
.resources
.children_cache
.get(&parent)
.cloned()
.unwrap_or_default();
for child in children {
self.ui_despawn_node(child);
}
self.resources.retained_ui.layout_dirty = true;
}
pub fn ui_rebuild_children(
&mut self,
parent: freecs::Entity,
build: impl FnOnce(&mut crate::ecs::ui::builder::UiTreeBuilder),
) {
self.ui_clear_children(parent);
let content = self.ui_widget_content(parent).unwrap_or(parent);
let mut builder = crate::ecs::ui::builder::UiTreeBuilder::from_parent(self, content);
build(&mut builder);
builder.finish_subtree();
}
pub fn ui_focus(&mut self, entity: freecs::Entity) {
self.resources.retained_ui.focused_entity = Some(entity);
}
pub fn ui_set_disabled(&mut self, entity: freecs::Entity, disabled: bool) {
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.disabled = disabled;
}
if disabled {
let base_color = self
.get_ui_node_color(entity)
.and_then(|color| color.colors[UiBase::INDEX]);
if let Some(base) = base_color
&& let Some(color_comp) = self.get_ui_node_color_mut(entity)
{
color_comp.colors[crate::ecs::ui::state::UiDisabled::INDEX] = Some(
nalgebra_glm::Vec4::new(base.x, base.y, base.z, base.w * 0.4),
);
}
} else if let Some(color_comp) = self.get_ui_node_color_mut(entity) {
color_comp.colors[crate::ecs::ui::state::UiDisabled::INDEX] = None;
}
}
pub fn ui_set_disabled_recursive(&mut self, entity: freecs::Entity, disabled: bool) {
if !self.resources.children_cache_valid {
self.validate_and_rebuild_children_cache();
}
let mut stack = vec![entity];
while let Some(current) = stack.pop() {
self.ui_set_disabled(current, disabled);
if let Some(children) = self.resources.children_cache.get(¤t) {
for child in children {
stack.push(*child);
}
}
}
}
pub fn ui_is_disabled(&self, entity: freecs::Entity) -> bool {
self.get_ui_node_interaction(entity)
.is_some_and(|interaction| interaction.disabled)
}
pub fn ui_set_visible(&mut self, entity: freecs::Entity, visible: bool) {
if let Some(node) = self.get_ui_layout_node_mut(entity) {
if let Some(animation) = &mut node.animation {
if visible {
if animation.intro.is_some() {
animation.phase =
crate::ecs::ui::components::UiAnimationPhase::IntroPlaying;
animation.progress = 0.0;
}
node.visible = true;
} else if animation.outro.is_some() {
animation.phase = crate::ecs::ui::components::UiAnimationPhase::OutroPlaying;
animation.progress = 0.0;
} else {
animation.phase = crate::ecs::ui::components::UiAnimationPhase::Idle;
node.visible = false;
}
} else {
node.visible = visible;
}
}
}
pub fn ui_set_visible_exclusive(
&mut self,
entities: &[freecs::Entity],
active: freecs::Entity,
) {
for &entity in entities {
self.ui_set_visible(entity, entity == active);
}
}
pub fn ui_node_visible(&self, entity: freecs::Entity) -> bool {
self.get_ui_layout_node(entity)
.is_some_and(|node| node.visible)
}
pub fn ui_node_effectively_visible(&self, entity: freecs::Entity) -> bool {
let Some(node) = self.get_ui_layout_node(entity) else {
return false;
};
if !node.visible {
return false;
}
let mut current = entity;
while let Some(crate::ecs::transform::components::Parent(Some(parent))) =
self.get_parent(current)
{
let parent = *parent;
if let Some(parent_node) = self.get_ui_layout_node(parent)
&& !parent_node.visible
{
return false;
}
current = parent;
}
true
}
pub fn ui_rect(&self, entity: freecs::Entity) -> Option<crate::ecs::ui::types::Rect> {
self.get_ui_layout_node(entity)
.map(|node| node.computed_rect)
}
pub fn ui_size(&self, entity: freecs::Entity) -> Option<Vec2> {
self.get_ui_layout_node(entity)
.map(|node| node.computed_rect.size())
}
pub fn ui_set_text(&mut self, entity: freecs::Entity, text: &str) {
let slot = self.ui_text_slot_for_entity(entity);
if let Some(text_slot) = slot {
self.resources.text_cache.set_text(text_slot, text);
}
}
pub fn ui_set_label_text(&mut self, entity: freecs::Entity, text: &str) {
self.ui_set_text(entity, text);
}
pub fn ui_label_text(&self, entity: freecs::Entity) -> Option<String> {
self.ui_text_slot_for_entity(entity)
.and_then(|slot| self.resources.text_cache.get_text(slot).map(String::from))
}
pub(crate) fn ui_text_slot_for_entity(&self, entity: freecs::Entity) -> Option<usize> {
if let Some(crate::ecs::ui::components::UiNodeContent::Text { text_slot, .. }) =
self.get_ui_node_content(entity)
{
return Some(*text_slot);
}
match self.get_ui_widget_state(entity) {
Some(UiWidgetState::Button(data)) => Some(data.text_slot),
Some(UiWidgetState::Slider(data)) => Some(data.text_slot),
Some(UiWidgetState::TextInput(data)) => Some(data.text_slot),
Some(UiWidgetState::SelectableLabel(data)) => Some(data.text_slot),
Some(UiWidgetState::DragValue(data)) => Some(data.text_slot),
_ => None,
}
}
pub fn ui_set_tooltip_entity(
&mut self,
entity: freecs::Entity,
tooltip_entity: Option<freecs::Entity>,
) {
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.tooltip_entity = tooltip_entity;
}
}
pub fn ui_set_tooltip_text(&mut self, entity: freecs::Entity, text: Option<&str>) {
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.tooltip_text = text.map(String::from);
}
}
pub fn ui_set_error(&mut self, entity: freecs::Entity, error: Option<&str>) {
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.error_text = error.map(String::from);
interaction.tooltip_text = error.map(String::from);
}
}
pub fn ui_has_error(&self, entity: freecs::Entity) -> bool {
self.get_ui_node_interaction(entity)
.is_some_and(|interaction| interaction.error_text.is_some())
}
pub fn ui_clear_error(&mut self, entity: freecs::Entity) {
self.ui_set_error(entity, None);
}
pub fn ui_add_validation_rule(
&mut self,
entity: freecs::Entity,
rule: crate::ecs::ui::components::ValidationRule,
) {
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.validation_rules.push(rule);
}
}
pub fn ui_set_validation_rules(
&mut self,
entity: freecs::Entity,
rules: Vec<crate::ecs::ui::components::ValidationRule>,
) {
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.validation_rules = rules;
}
}
pub fn ui_validate(&mut self, entity: freecs::Entity) -> bool {
let text = self
.widget::<UiTextInputData>(entity)
.map(|d| d.text.clone())
.unwrap_or_default();
let rules: Vec<crate::ecs::ui::components::ValidationRule> = self
.get_ui_node_interaction(entity)
.map(|interaction| interaction.validation_rules.clone())
.unwrap_or_default();
let mut error: Option<String> = None;
for rule in &rules {
if let Err(message) = rule.validate(&text) {
error = Some(message);
break;
}
}
let is_valid = error.is_none();
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.error_text = error.clone();
interaction.tooltip_text = error;
}
is_valid
}
pub fn ui_is_valid(&self, entity: freecs::Entity) -> bool {
self.get_ui_node_interaction(entity)
.is_some_and(|interaction| interaction.error_text.is_none())
}
pub fn ui_show_toast(
&mut self,
message: &str,
severity: crate::ecs::ui::resources::ToastSeverity,
duration: f32,
) {
use crate::ecs::ui::builder::UiTreeBuilder;
use crate::ecs::ui::components::{UiAnimationPhase, UiAnimationType};
use crate::ecs::ui::resources::{ToastEntry, ToastSeverity};
let container = if let Some(entity) = self.resources.retained_ui.toast_container {
entity
} else {
let mut tree = UiTreeBuilder::new(self);
let container = tree
.add_node()
.boundary(
Rl(Vec2::new(70.0, 0.0)) + Ab(Vec2::new(0.0, 60.0)),
Rl(Vec2::new(100.0, 100.0)) + Ab(Vec2::new(-12.0, -50.0)),
)
.flow(
crate::ecs::ui::layout_types::FlowDirection::Vertical,
0.0,
8.0,
)
.with_depth(UiDepthMode::Set(50.0))
.without_pointer_events()
.entity();
tree.finish();
self.resources.retained_ui.toast_container = Some(container);
container
};
let accent = match severity {
ToastSeverity::Info => Vec4::new(0.3, 0.6, 1.0, 1.0),
ToastSeverity::Success => Vec4::new(0.2, 0.8, 0.3, 1.0),
ToastSeverity::Warning => Vec4::new(1.0, 0.7, 0.1, 1.0),
ToastSeverity::Error => Vec4::new(1.0, 0.25, 0.25, 1.0),
};
let text_slot = self.resources.text_cache.add_text(message);
let spawn_time = self.resources.retained_ui.current_time;
let mut tree = UiTreeBuilder::from_parent(self, container);
let toast = tree
.add_node()
.flow_child(Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(0.0, 36.0)))
.with_rect(6.0, 1.0, Vec4::new(0.15, 0.15, 0.22, 0.5))
.with_color::<UiBase>(Vec4::new(0.06, 0.06, 0.1, 0.95))
.with_intro(UiAnimationType::SlideRight, 0.2)
.with_outro(UiAnimationType::Fade, 0.15)
.without_pointer_events()
.entity();
tree.push_parent(toast);
tree.add_node()
.boundary(
Rl(Vec2::new(0.0, 0.0)),
Ab(Vec2::new(4.0, 0.0)) + Rl(Vec2::new(0.0, 100.0)),
)
.with_rect(6.0, 0.0, Vec4::new(0.0, 0.0, 0.0, 0.0))
.with_color::<UiBase>(accent)
.without_pointer_events();
tree.add_node()
.window(
Ab(Vec2::new(14.0, 18.0)),
Rl(Vec2::new(100.0, 0.0)) + Ab(Vec2::new(-20.0, 20.0)),
Anchor::CenterLeft,
)
.with_text_slot(text_slot, 13.0)
.with_text_alignment(TextAlignment::Left, VerticalAlignment::Middle)
.with_color::<UiBase>(Vec4::new(0.9, 0.9, 0.95, 1.0))
.without_pointer_events();
tree.pop_parent();
tree.finish_subtree();
if let Some(node) = self.get_ui_layout_node_mut(toast) {
if let Some(animation) = &mut node.animation {
animation.phase = UiAnimationPhase::IntroPlaying;
animation.progress = 0.0;
}
node.visible = true;
}
self.resources.retained_ui.toast_entries.push(ToastEntry {
entity: toast,
spawn_time,
duration,
dismissing: false,
});
}
pub fn ui_tick_toasts(&mut self) {
let current_time = self.resources.retained_ui.current_time;
let entries = std::mem::take(&mut self.resources.retained_ui.toast_entries);
let mut kept = Vec::new();
let mut to_despawn = Vec::new();
for mut entry in entries {
let elapsed = (current_time - entry.spawn_time) as f32;
if entry.dismissing {
let phase = self
.get_ui_layout_node(entry.entity)
.and_then(|node| node.animation.as_ref())
.map(|anim| anim.phase);
if phase == Some(crate::ecs::ui::components::UiAnimationPhase::OutroComplete)
|| !self.ui_node_visible(entry.entity)
{
to_despawn.push(entry.entity);
} else {
kept.push(entry);
}
} else if elapsed >= entry.duration {
entry.dismissing = true;
self.ui_set_visible(entry.entity, false);
kept.push(entry);
} else {
kept.push(entry);
}
}
self.resources.retained_ui.toast_entries = kept;
for entity in to_despawn {
self.ui_despawn_node(entity);
}
}
pub fn ui_set_selected(&mut self, entity: freecs::Entity, selected: bool) {
let accent = self
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
let selected_bg = Vec4::new(accent.x, accent.y, accent.z, 0.3);
if let Some(UiWidgetState::SelectableLabel(data)) = self.get_ui_widget_state_mut(entity) {
data.selected = selected;
}
if let Some(color) = self.get_ui_node_color_mut(entity) {
color.colors[crate::ecs::ui::state::UiSelected::INDEX] =
if selected { Some(selected_bg) } else { None };
}
if let Some(weights) = self.get_ui_state_weights_mut(entity) {
weights.weights[crate::ecs::ui::state::UiSelected::INDEX] =
if selected { 1.0 } else { 0.0 };
}
}
pub fn ui_drag_value_set_value(&mut self, entity: freecs::Entity, value: f32) {
let update = if let Some(UiWidgetState::DragValue(data)) = self.get_ui_widget_state(entity)
{
let clamped = value.clamp(data.min, data.max);
Some((
data.text_slot,
data.prefix.clone(),
data.suffix.clone(),
data.precision,
clamped,
))
} else {
None
};
if let Some((text_slot, prefix, suffix, precision, clamped)) = update {
let display = format!("{prefix}{clamped:.prec$}{suffix}", prec = precision);
self.resources.text_cache.set_text(text_slot, &display);
if let Some(UiWidgetState::DragValue(data)) = self.get_ui_widget_state_mut(entity) {
data.value = clamped;
}
}
}
pub fn ui_show_context_menu(&mut self, entity: freecs::Entity, position: Vec2) {
if let Some(old_menu) = self.resources.retained_ui.active_context_menu
&& old_menu != entity
&& let Some(UiWidgetState::ContextMenu(old_data)) =
self.get_ui_widget_state(old_menu).cloned().as_ref()
{
if let Some(node) = self.get_ui_layout_node_mut(old_data.popup_entity) {
node.visible = false;
}
if let Some(UiWidgetState::ContextMenu(wd)) = self.get_ui_widget_state_mut(old_menu) {
wd.open = false;
}
}
let popup = if let Some(UiWidgetState::ContextMenu(data)) = self.get_ui_widget_state(entity)
{
Some(data.popup_entity)
} else {
None
};
if let Some(popup_entity) = popup {
let dpi_scale = self.resources.window.cached_scale_factor;
if let Some(node) = self.get_ui_layout_node_mut(popup_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position = crate::ecs::ui::units::Ab(position / dpi_scale).into();
node.visible = true;
}
if let Some(UiWidgetState::ContextMenu(data)) = self.get_ui_widget_state_mut(entity) {
data.open = true;
data.clicked_item = None;
}
self.resources.retained_ui.active_context_menu = Some(entity);
}
}
pub fn ui_context_menu_widget_content(
&self,
entity: freecs::Entity,
command_id: usize,
) -> Option<freecs::Entity> {
if let Some(UiWidgetState::ContextMenu(data)) = self.get_ui_widget_state(entity) {
find_widget_content_in_defs(&data.item_defs, command_id)
} else {
None
}
}
pub fn ui_close_context_menus(&mut self) {
if let Some(menu_entity) = self.resources.retained_ui.active_context_menu {
let popup = if let Some(UiWidgetState::ContextMenu(data)) =
self.get_ui_widget_state(menu_entity)
{
Some(data.popup_entity)
} else {
None
};
if let Some(popup_entity) = popup
&& let Some(node) = self.get_ui_layout_node_mut(popup_entity)
{
node.visible = false;
}
if let Some(UiWidgetState::ContextMenu(data)) =
self.get_ui_widget_state_mut(menu_entity)
{
data.open = false;
}
self.resources.retained_ui.active_context_menu = None;
}
}
pub fn ui_tree_view_set_filter(&mut self, tree: freecs::Entity, text: &str) {
let tree_data = if let Some(UiWidgetState::TreeView(data)) = self.get_ui_widget_state(tree)
{
data.clone()
} else {
return;
};
let filter_lower = text.to_lowercase();
let filtering = !filter_lower.is_empty();
if filtering && !tree_data.filter_active {
let mut pre_filter = std::collections::HashMap::new();
for &node_entity in &tree_data.node_entities {
if let Some(UiWidgetState::TreeNode(nd)) = self.get_ui_widget_state(node_entity) {
pre_filter.insert(node_entity, nd.expanded);
}
}
if let Some(UiWidgetState::TreeView(data)) = self.get_ui_widget_state_mut(tree) {
data.pre_filter_expanded = pre_filter;
}
}
if filtering {
let mut matching_nodes: std::collections::HashSet<freecs::Entity> =
std::collections::HashSet::new();
for &node_entity in &tree_data.node_entities {
if let Some(UiWidgetState::TreeNode(nd)) = self.get_ui_widget_state(node_entity)
&& nd.label.to_lowercase().contains(&filter_lower)
{
matching_nodes.insert(node_entity);
let mut ancestor = nd.parent_node;
while let Some(anc) = ancestor {
matching_nodes.insert(anc);
ancestor = if let Some(UiWidgetState::TreeNode(anc_data)) =
self.get_ui_widget_state(anc)
{
anc_data.parent_node
} else {
None
};
}
}
}
for &node_entity in &tree_data.node_entities {
let node_info = if let Some(UiWidgetState::TreeNode(nd)) =
self.get_ui_widget_state(node_entity)
{
Some((nd.wrapper_entity, nd.parent_node.is_some()))
} else {
None
};
if let Some((wrapper, _has_parent)) = node_info {
let visible = matching_nodes.contains(&node_entity);
if let Some(node) = self.get_ui_layout_node_mut(wrapper) {
node.visible = visible;
}
if visible {
self.ui_tree_node_set_expanded(node_entity, true);
}
}
}
} else if tree_data.filter_active {
let pre_filter = tree_data.pre_filter_expanded.clone();
for &node_entity in &tree_data.node_entities {
let wrapper = if let Some(UiWidgetState::TreeNode(nd)) =
self.get_ui_widget_state(node_entity)
{
Some(nd.wrapper_entity)
} else {
None
};
if let Some(wrapper) = wrapper
&& let Some(node) = self.get_ui_layout_node_mut(wrapper)
{
node.visible = true;
}
let was_expanded = pre_filter.get(&node_entity).copied().unwrap_or(false);
self.ui_tree_node_set_expanded(node_entity, was_expanded);
}
}
if let Some(UiWidgetState::TreeView(data)) = self.get_ui_widget_state_mut(tree) {
data.filter_text = text.to_string();
data.filter_active = filtering;
if !filtering {
data.pre_filter_expanded.clear();
}
}
}
pub fn ui_tree_view_clear_filter(&mut self, tree: freecs::Entity) {
self.ui_tree_view_set_filter(tree, "");
}
pub fn ui_tree_node_set_expanded(&mut self, entity: freecs::Entity, expanded: bool) {
let update = if let Some(UiWidgetState::TreeNode(data)) = self.get_ui_widget_state(entity) {
Some((data.arrow_text_slot, data.children_container))
} else {
None
};
if let Some((arrow_slot, children)) = update {
self.resources
.text_cache
.set_text(arrow_slot, if expanded { "\u{25BC}" } else { "\u{25B6}" });
if let Some(node) = self.get_ui_layout_node_mut(children) {
node.visible = expanded;
}
if let Some(UiWidgetState::TreeNode(data)) = self.get_ui_widget_state_mut(entity) {
data.expanded = expanded;
}
}
}
pub fn ui_tree_node_mark_loaded(&mut self, entity: freecs::Entity) {
if let Some(UiWidgetState::TreeNode(data)) = self.get_ui_widget_state_mut(entity) {
data.lazy_loaded = true;
}
}
pub fn ui_show_modal(&mut self, entity: freecs::Entity) {
let backdrop =
if let Some(UiWidgetState::ModalDialog(data)) = self.get_ui_widget_state(entity) {
Some(data.backdrop_entity)
} else {
None
};
if let Some(backdrop_entity) = backdrop {
if let Some(node) = self.get_ui_layout_node_mut(backdrop_entity) {
node.visible = true;
}
if let Some(node) = self.get_ui_layout_node_mut(entity) {
node.visible = true;
}
if let Some(UiWidgetState::ModalDialog(data)) = self.get_ui_widget_state_mut(entity) {
data.result = None;
}
self.resources.retained_ui.active_modal = Some(entity);
}
}
pub fn ui_widget_content(&self, entity: freecs::Entity) -> Option<freecs::Entity> {
match self.get_ui_widget_state(entity) {
Some(UiWidgetState::CollapsingHeader(data)) => Some(data.content_entity),
Some(UiWidgetState::ScrollArea(data)) => Some(data.content_entity),
Some(UiWidgetState::Panel(data)) => Some(data.content_entity),
Some(UiWidgetState::ModalDialog(data)) => Some(data.content_entity),
Some(UiWidgetState::TreeView(data)) => Some(data.content_entity),
Some(UiWidgetState::TileContainer(data)) => Some(data.container_entity),
_ => Some(entity),
}
}
pub fn ui_show_command_palette(&mut self, entity: freecs::Entity) {
let (backdrop, text_input) =
if let Some(UiWidgetState::CommandPalette(data)) = self.get_ui_widget_state(entity) {
(data.backdrop_entity, data.text_input_entity)
} else {
return;
};
if let Some(node) = self.get_ui_layout_node_mut(backdrop) {
node.visible = true;
}
if let Some(node) = self.get_ui_layout_node_mut(entity) {
node.visible = true;
}
self.ui_text_input_set_value(text_input, "");
self.resources.retained_ui.focused_entity = Some(text_input);
if let Some(UiWidgetState::CommandPalette(data)) = self.get_ui_widget_state_mut(entity) {
data.open = true;
data.executed_command = None;
data.filter_text.clear();
data.selected_index = 0;
data.filtered_indices = (0..data.commands.len()).collect();
}
self.ui_command_palette_rebuild_results(entity);
}
pub fn ui_command_palette_register(
&mut self,
entity: freecs::Entity,
label: &str,
shortcut: &str,
category: &str,
) {
if let Some(UiWidgetState::CommandPalette(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CommandEntry {
label: label.to_string(),
shortcut: shortcut.to_string(),
category: category.to_string(),
enabled: true,
});
data.filtered_indices = (0..data.commands.len()).collect();
}
}
pub fn ui_command_palette_clear(&mut self, entity: freecs::Entity) {
if let Some(UiWidgetState::CommandPalette(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.clear();
data.filtered_indices.clear();
data.selected_index = 0;
}
}
pub(crate) fn ui_command_palette_rebuild_results(&mut self, entity: freecs::Entity) {
let data =
if let Some(UiWidgetState::CommandPalette(data)) = self.get_ui_widget_state(entity) {
data.clone()
} else {
return;
};
for (pool_index, &row_entity) in data.result_entities.iter().enumerate() {
if pool_index < data.filtered_indices.len() {
let cmd_index = data.filtered_indices[pool_index];
let cmd = &data.commands[cmd_index];
let label_slot = data.result_text_slots[pool_index * 2];
let shortcut_slot = data.result_text_slots[pool_index * 2 + 1];
let display = if cmd.category.is_empty() {
cmd.label.clone()
} else {
format!("{}: {}", cmd.category, cmd.label)
};
self.resources.text_cache.set_text(label_slot, &display);
self.resources
.text_cache
.set_text(shortcut_slot, &cmd.shortcut);
if let Some(node) = self.get_ui_layout_node_mut(row_entity) {
node.visible = true;
}
} else if let Some(node) = self.get_ui_layout_node_mut(row_entity) {
node.visible = false;
}
}
}
pub fn ui_rich_text_set_span_text(
&mut self,
entity: freecs::Entity,
span_index: usize,
text: &str,
) {
if let Some(UiWidgetState::RichText(data)) = self.get_ui_widget_state(entity)
&& let Some(&text_slot) = data.span_text_slots.get(span_index)
{
self.resources.text_cache.set_text(text_slot, text);
}
}
pub fn ui_rich_text_set_span_color(
&mut self,
entity: freecs::Entity,
span_index: usize,
color: Vec4,
) {
if let Some(UiWidgetState::RichText(data)) = self.get_ui_widget_state(entity)
&& let Some(&span_entity) = data.span_entities.get(span_index)
&& let Some(node_color) = self.get_ui_node_color_mut(span_entity)
{
node_color.colors[0] = Some(color);
}
}
pub fn ui_bubbled_events_for(
&self,
entity: freecs::Entity,
) -> Vec<crate::ecs::ui::resources::BubbledUiEvent> {
self.resources
.retained_ui
.bubbled_events
.iter()
.filter(|event| event.ancestor == entity && !event.stopped)
.cloned()
.collect()
}
pub fn ui_data_grid_set_row_count(&mut self, entity: freecs::Entity, count: usize) {
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state_mut(entity) {
data.total_rows = count;
data.selected_rows.retain(|&row| row < count);
}
}
pub fn ui_data_grid_set_cell(
&mut self,
entity: freecs::Entity,
data_row: usize,
column: usize,
text: &str,
) {
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state(entity) {
if data_row < data.visible_start {
return;
}
let pool_index = data_row - data.visible_start;
if let Some(pool_row) = data.pool_rows.get(pool_index)
&& let Some(&text_slot) = pool_row.cell_text_slots.get(column)
{
self.resources.text_cache.set_text(text_slot, text);
}
}
}
pub fn ui_data_grid_sort_changed(&self, entity: freecs::Entity) -> bool {
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state(entity) {
data.sort_changed
} else {
false
}
}
pub fn ui_data_grid_selection_changed(&self, entity: freecs::Entity) -> bool {
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state(entity) {
data.selection_changed
} else {
false
}
}
pub fn ui_data_grid_set_filter(&mut self, entity: freecs::Entity, indices: &[usize]) {
if let Some(UiWidgetState::DataGrid(grid)) = self.get_ui_widget_state_mut(entity) {
grid.filtered_indices = Some(indices.to_vec());
grid.selected_rows.clear();
grid.selection_anchor = None;
grid.selection_changed = true;
}
}
pub fn ui_data_grid_clear_filter(&mut self, entity: freecs::Entity) {
if let Some(UiWidgetState::DataGrid(grid)) = self.get_ui_widget_state_mut(entity) {
grid.filtered_indices = None;
grid.selected_rows.clear();
grid.selection_anchor = None;
grid.selection_changed = true;
}
}
pub fn ui_data_grid_populate(
&mut self,
entity: freecs::Entity,
source: &dyn crate::ecs::ui::components::DataGridDataSource,
) {
let row_count = source.row_count();
self.ui_data_grid_set_row_count(entity, row_count);
let Some(data) = self.widget::<UiDataGridData>(entity) else {
return;
};
let end = (data.visible_start + data.pool_size).min(data.total_rows);
let range = data.visible_start..end;
let col_count = data.columns.len();
let filtered = data.filtered_indices.clone();
for visible_row in range {
let data_row = filtered
.as_ref()
.and_then(|indices| indices.get(visible_row).copied())
.unwrap_or(visible_row);
for column in 0..col_count {
let text = source.cell_text(data_row, column);
self.ui_data_grid_set_cell(entity, visible_row, column, &text);
}
}
}
pub fn ui_data_grid_populate_fn(
&mut self,
entity: freecs::Entity,
row_count: usize,
cell_fn: impl Fn(usize, usize) -> String,
) {
self.ui_data_grid_set_row_count(entity, row_count);
let Some(data) = self.widget::<UiDataGridData>(entity) else {
return;
};
let end = (data.visible_start + data.pool_size).min(data.total_rows);
let range = data.visible_start..end;
let col_count = data.columns.len();
let filtered = data.filtered_indices.clone();
for visible_row in range {
let data_row = filtered
.as_ref()
.and_then(|indices| indices.get(visible_row).copied())
.unwrap_or(visible_row);
for column in 0..col_count {
let text = cell_fn(data_row, column);
self.ui_data_grid_set_cell(entity, visible_row, column, &text);
}
}
}
pub fn ui_data_grid_start_edit(&mut self, entity: freecs::Entity, row: usize, column: usize) {
let info = if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state(entity) {
if column < data.columns.len()
&& data.columns[column].editable
&& let Some(input_entity) = data.editing_input_entity
{
let pool_row_idx = if row >= data.visible_start {
row - data.visible_start
} else {
return;
};
if pool_row_idx >= data.pool_rows.len() {
return;
}
let cell_entity = data.pool_rows[pool_row_idx].cell_entities[column];
let cell_text_slot = data.pool_rows[pool_row_idx].cell_text_slots[column];
let cell_text = self
.resources
.text_cache
.get_text(cell_text_slot)
.unwrap_or_default()
.to_string();
Some((input_entity, cell_entity, cell_text))
} else {
None
}
} else {
None
};
if let Some((input_entity, cell_entity, cell_text)) = info {
self.ui_text_input_set_value(input_entity, &cell_text);
if let Some(UiWidgetState::TextInput(input_data)) =
self.get_ui_widget_state_mut(input_entity)
{
input_data.selection_start = Some(0);
}
let root_rect = self
.get_ui_layout_node(entity)
.map(|n| n.computed_rect)
.unwrap_or_default();
let cell_rect = self
.get_ui_layout_node(cell_entity)
.map(|n| n.computed_rect)
.unwrap_or_default();
let rel_x = cell_rect.min.x - root_rect.min.x;
let rel_y = cell_rect.min.y - root_rect.min.y;
let width = cell_rect.width();
let height = cell_rect.height();
if let Some(node) = self.get_ui_layout_node_mut(input_entity) {
node.visible = true;
if let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.layouts[UiBase::INDEX].as_mut()
{
window.position = Ab(Vec2::new(rel_x, rel_y)).into();
window.size = Ab(Vec2::new(width, height)).into();
}
}
self.resources.retained_ui.focused_entity = Some(input_entity);
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state_mut(entity) {
data.editing_cell = Some((row, column));
}
}
}
pub fn ui_data_grid_stop_edit(&mut self, entity: freecs::Entity, commit: bool) {
let info = if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state(entity) {
if let Some((row, column)) = data.editing_cell
&& let Some(input_entity) = data.editing_input_entity
{
Some((input_entity, row, column))
} else {
None
}
} else {
None
};
if let Some((input_entity, row, column)) = info {
if commit {
let text = self
.widget::<UiTextInputData>(input_entity)
.map(|d| d.text.clone())
.unwrap_or_default();
self.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::DataGridCellEdited {
entity,
row,
column,
text,
},
);
}
if let Some(node) = self.get_ui_layout_node_mut(input_entity) {
node.visible = false;
}
if self.resources.retained_ui.focused_entity == Some(input_entity) {
self.resources.retained_ui.focused_entity = None;
}
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state_mut(entity) {
data.editing_cell = None;
}
}
}
pub fn ui_text_area_set_value(&mut self, entity: freecs::Entity, text: &str) {
let extract = if let Some(UiWidgetState::TextArea(data)) = self.get_ui_widget_state(entity)
{
Some((
data.text_slot,
data.placeholder_entity,
data.syntax_language.clone(),
))
} else {
None
};
if let Some((slot, placeholder_entity, syntax_language)) = extract {
self.resources.text_cache.set_text(slot, text);
if let Some(UiWidgetState::TextArea(data)) = self.get_ui_widget_state_mut(entity) {
data.text = text.to_string();
data.cursor_position = text.chars().count();
data.selection_start = None;
}
if let Some(ph_entity) = placeholder_entity
&& let Some(node) = self.get_ui_layout_node_mut(ph_entity)
{
node.visible = text.is_empty();
}
#[cfg(feature = "syntax_highlighting")]
if let Some(language) = &syntax_language {
crate::ecs::ui::syntax::highlight_text_area(self, text, language, slot);
}
#[cfg(not(feature = "syntax_highlighting"))]
let _ = syntax_language;
}
}
pub fn ui_text_area_set_syntax(&mut self, entity: freecs::Entity, language: Option<&str>) {
let info = if let Some(UiWidgetState::TextArea(data)) = self.get_ui_widget_state(entity) {
Some((data.text_slot, data.text.clone()))
} else {
None
};
if let Some(UiWidgetState::TextArea(data)) = self.get_ui_widget_state_mut(entity) {
data.syntax_language = language.map(|s| s.to_string());
}
if language.is_some()
&& let Some((text_slot, text)) = info
{
#[cfg(feature = "syntax_highlighting")]
crate::ecs::ui::syntax::highlight_text_area(self, &text, language.unwrap(), text_slot);
#[cfg(not(feature = "syntax_highlighting"))]
{
let _ = (text_slot, text);
}
} else if let Some((text_slot, _)) = info {
self.resources
.retained_ui
.text_slot_character_colors
.remove(&text_slot);
}
}
pub fn ui_rich_text_editor_set_value(&mut self, entity: freecs::Entity, text: &str) {
let slot =
if let Some(UiWidgetState::RichTextEditor(data)) = self.get_ui_widget_state(entity) {
Some(data.text_slot)
} else {
None
};
if let Some(slot) = slot {
self.resources.text_cache.set_text(slot, text);
let char_count = text.chars().count();
if let Some(UiWidgetState::RichTextEditor(data)) = self.get_ui_widget_state_mut(entity)
{
data.text = text.to_string();
data.char_styles = vec![CharStyle::default(); char_count];
data.cursor_position = char_count;
data.selection_start = None;
}
}
}
pub fn ui_rich_text_editor_toggle_bold(&mut self, entity: freecs::Entity) {
self.ui_rich_text_editor_toggle_style(entity, |style| &mut style.bold);
}
pub fn ui_rich_text_editor_toggle_italic(&mut self, entity: freecs::Entity) {
self.ui_rich_text_editor_toggle_style(entity, |style| &mut style.italic);
}
pub fn ui_rich_text_editor_toggle_underline(&mut self, entity: freecs::Entity) {
self.ui_rich_text_editor_toggle_style(entity, |style| &mut style.underline);
}
pub fn ui_rich_text_editor_set_color(&mut self, entity: freecs::Entity, color: Option<Vec4>) {
let update_info = if let Some(UiWidgetState::RichTextEditor(data)) =
self.get_ui_widget_state_mut(entity)
{
if let Some(sel_start) = data.selection_start {
let start = sel_start.min(data.cursor_position);
let end = sel_start.max(data.cursor_position);
for index in start..end {
if index < data.char_styles.len() {
data.char_styles[index].color = color;
}
}
}
data.current_style.color = color;
data.changed = true;
Some((data.char_styles.clone(), data.text_slot))
} else {
None
};
if let Some((char_styles, text_slot)) = update_info {
Self::update_rich_text_char_colors(
&char_styles,
text_slot,
&mut self.resources.retained_ui.text_slot_character_colors,
);
}
}
fn ui_rich_text_editor_toggle_style(
&mut self,
entity: freecs::Entity,
accessor: fn(&mut CharStyle) -> &mut bool,
) {
let update_info = if let Some(UiWidgetState::RichTextEditor(data)) =
self.get_ui_widget_state_mut(entity)
{
if let Some(sel_start) = data.selection_start {
let start = sel_start.min(data.cursor_position);
let end = sel_start.max(data.cursor_position);
let all_set = (start..end).all(|index| {
data.char_styles
.get(index)
.map(|s| *accessor(&mut s.clone()))
.unwrap_or(false)
});
let new_value = !all_set;
for index in start..end {
if index < data.char_styles.len() {
*accessor(&mut data.char_styles[index]) = new_value;
}
}
*accessor(&mut data.current_style) = new_value;
} else {
let field = accessor(&mut data.current_style);
*field = !*field;
}
data.changed = true;
Some((data.char_styles.clone(), data.text_slot))
} else {
None
};
if let Some((char_styles, text_slot)) = update_info {
Self::update_rich_text_char_colors(
&char_styles,
text_slot,
&mut self.resources.retained_ui.text_slot_character_colors,
);
}
}
fn update_rich_text_char_colors(
char_styles: &[CharStyle],
text_slot: usize,
slot_colors: &mut HashMap<usize, Vec<Option<Vec4>>>,
) {
let has_colors = char_styles.iter().any(|s| s.color.is_some());
if has_colors {
let colors: Vec<Option<Vec4>> = char_styles.iter().map(|s| s.color).collect();
slot_colors.insert(text_slot, colors);
} else {
slot_colors.remove(&text_slot);
}
}
pub fn ui_canvas_hit_test(&self, entity: freecs::Entity, position: Vec2) -> Option<u32> {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state(entity) {
for &(command_id, ref rect) in data.command_bounds.iter().rev() {
if rect.contains(position) {
return Some(command_id);
}
}
}
None
}
pub fn ui_canvas_clear(&mut self, entity: freecs::Entity) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.clear();
}
}
pub fn ui_canvas_rect(
&mut self,
entity: freecs::Entity,
position: Vec2,
size: Vec2,
color: Vec4,
corner_radius: f32,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::Rect {
position,
size,
color,
corner_radius,
id: None,
});
}
}
pub fn ui_canvas_circle(
&mut self,
entity: freecs::Entity,
center: Vec2,
radius: f32,
color: Vec4,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::Circle {
center,
radius,
color,
id: None,
});
}
}
pub fn ui_canvas_line(
&mut self,
entity: freecs::Entity,
from: Vec2,
to: Vec2,
thickness: f32,
color: Vec4,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::Line {
from,
to,
thickness,
color,
id: None,
});
}
}
pub fn ui_canvas_text(
&mut self,
entity: freecs::Entity,
text: &str,
position: Vec2,
font_size: f32,
color: Vec4,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::Text {
text: text.to_string(),
position,
font_size,
color,
id: None,
});
}
}
pub fn ui_canvas_polyline(
&mut self,
entity: freecs::Entity,
points: &[Vec2],
thickness: f32,
color: Vec4,
closed: bool,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::Polyline {
points: points.to_vec(),
thickness,
color,
closed,
id: None,
});
}
}
pub fn ui_canvas_rect_stroke(
&mut self,
entity: freecs::Entity,
position: Vec2,
size: Vec2,
color: Vec4,
thickness: f32,
corner_radius: f32,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::RectStroke {
position,
size,
color,
thickness,
corner_radius,
id: None,
});
}
}
pub fn ui_canvas_circle_stroke(
&mut self,
entity: freecs::Entity,
center: Vec2,
radius: f32,
color: Vec4,
thickness: f32,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::CircleStroke {
center,
radius,
color,
thickness,
id: None,
});
}
}
pub fn ui_canvas_arc(
&mut self,
entity: freecs::Entity,
center: Vec2,
radius: f32,
angle_range: (f32, f32),
thickness: f32,
color: Vec4,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::Arc {
center,
radius,
start_angle: angle_range.0,
end_angle: angle_range.1,
thickness,
color,
id: None,
});
}
}
pub fn ui_canvas_quadratic_bezier(
&mut self,
entity: freecs::Entity,
start: Vec2,
control: Vec2,
end: Vec2,
thickness: f32,
color: Vec4,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::QuadraticBezier {
start,
control,
end,
thickness,
color,
id: None,
});
}
}
pub fn ui_canvas_cubic_bezier(
&mut self,
entity: freecs::Entity,
start: Vec2,
controls: (Vec2, Vec2),
end: Vec2,
thickness: f32,
color: Vec4,
) {
if let Some(UiWidgetState::Canvas(data)) = self.get_ui_widget_state_mut(entity) {
data.commands.push(CanvasCommand::CubicBezier {
start,
control1: controls.0,
control2: controls.1,
end,
thickness,
color,
id: None,
});
}
}
pub fn ui_tile_add_pane(
&mut self,
container: freecs::Entity,
title: &str,
) -> Option<(TileId, freecs::Entity)> {
let content_entity = self.spawn_tile_pane_entity(container);
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state_mut(container)
else {
return None;
};
let pane = TileNode::Pane {
content_entity,
title: title.to_string(),
};
let pane_id = data.alloc(pane);
let mut current = data.root;
let target_tabs = loop {
match data.get(current) {
Some(TileNode::Tabs { .. }) => break Some(current),
Some(TileNode::Split { children, .. }) => current = children[0],
_ => break None,
}
};
if let Some(tabs_id) = target_tabs
&& let Some(TileNode::Tabs { panes, .. }) = data.get_mut(tabs_id)
{
panes.push(pane_id);
}
Some((pane_id, content_entity))
}
pub fn ui_tile_add_pane_to(
&mut self,
container: freecs::Entity,
tabs_id: TileId,
title: &str,
) -> Option<(TileId, freecs::Entity)> {
let (pane_id, content_entity) = self.ui_tile_add_pane(container, title)?;
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state_mut(container)
else {
return None;
};
if let Some(parent_tabs_id) = data.find_parent_tabs(pane_id)
&& parent_tabs_id != tabs_id
&& let Some(TileNode::Tabs { panes, active }) = data.get_mut(parent_tabs_id)
{
panes.retain(|id| *id != pane_id);
if *active >= panes.len() && !panes.is_empty() {
*active = panes.len() - 1;
}
}
if let Some(TileNode::Tabs { panes, .. }) = data.get_mut(tabs_id)
&& !panes.contains(&pane_id)
{
panes.push(pane_id);
}
Some((pane_id, content_entity))
}
pub fn ui_tile_split(
&mut self,
container: freecs::Entity,
target: TileId,
direction: SplitDirection,
ratio: f32,
title: &str,
) -> Option<(TileId, freecs::Entity)> {
let (pane_id, content_entity) = self.ui_tile_add_pane(container, title)?;
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state_mut(container)
else {
return None;
};
if let Some(parent_tabs_id) = data.find_parent_tabs(pane_id)
&& let Some(TileNode::Tabs { panes, .. }) = data.get_mut(parent_tabs_id)
{
panes.retain(|id| *id != pane_id);
}
let new_tabs = TileNode::Tabs {
panes: vec![pane_id],
active: 0,
};
let new_tabs_id = data.alloc(new_tabs);
let old_node = data.tiles[target.0].take()?;
let old_id = data.alloc(old_node);
let split = TileNode::Split {
direction,
ratio,
children: [old_id, new_tabs_id],
};
data.tiles[target.0] = Some(split);
Some((pane_id, content_entity))
}
pub fn ui_tile_remove(&mut self, container: freecs::Entity, tile_id: TileId) {
let content_to_hide =
if let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state(container) {
let pane_count = data
.tiles
.iter()
.filter(|t| matches!(t, Some(TileNode::Pane { .. })))
.count();
if pane_count < 2 {
return;
}
if let Some(TileNode::Pane { content_entity, .. }) = data.get(tile_id) {
Some(*content_entity)
} else {
None
}
} else {
return;
};
if let Some(content) = content_to_hide
&& let Some(node) = self.get_ui_layout_node_mut(content)
{
node.visible = false;
}
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state_mut(container)
else {
return;
};
if let Some(tabs_id) = data.find_parent_tabs(tile_id) {
if let Some(TileNode::Tabs { panes, active }) = data.get_mut(tabs_id) {
panes.retain(|id| *id != tile_id);
if *active >= panes.len() && !panes.is_empty() {
*active = panes.len() - 1;
}
}
data.free(tile_id);
let remaining = if let Some(TileNode::Tabs { panes, .. }) = data.get(tabs_id) {
panes.len()
} else {
0
};
if remaining == 0 {
if let Some((parent_split_id, child_index)) = data.find_parent_split(tabs_id) {
let sibling_index = 1 - child_index;
let sibling_id =
if let Some(TileNode::Split { children, .. }) = data.get(parent_split_id) {
children[sibling_index]
} else {
return;
};
let sibling_node = data.tiles[sibling_id.0].take();
data.tiles[parent_split_id.0] = sibling_node;
data.free(tabs_id);
data.free(sibling_id);
} else {
data.free(tabs_id);
}
}
}
}
pub fn ui_tile_pane_content(
&self,
container: freecs::Entity,
tile_id: TileId,
) -> Option<freecs::Entity> {
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state(container) else {
return None;
};
if let Some(TileNode::Pane { content_entity, .. }) = data.get(tile_id) {
Some(*content_entity)
} else {
None
}
}
pub fn ui_tile_pane_title(&self, container: freecs::Entity, pane_id: TileId) -> Option<String> {
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state(container) else {
return None;
};
if let Some(TileNode::Pane { title, .. }) = data.get(pane_id) {
Some(title.clone())
} else {
None
}
}
pub fn ui_tile_active_pane(&self, container: freecs::Entity, pane_id: TileId) -> bool {
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state(container) else {
return false;
};
if let Some(tabs_id) = data.find_parent_tabs(pane_id)
&& let Some(TileNode::Tabs { panes, active }) = data.get(tabs_id)
&& panes.get(*active) == Some(&pane_id)
{
return true;
}
false
}
pub fn ui_tile_tab_activated(&self, container: freecs::Entity) -> Option<TileId> {
for event in &self.resources.retained_ui.frame_events {
if let crate::ecs::ui::resources::UiEvent::TileTabActivated {
container: event_container,
pane_id,
} = event
&& *event_container == container
{
return Some(*pane_id);
}
}
None
}
pub fn ui_tile_tab_closed(&self, container: freecs::Entity) -> Option<(TileId, String)> {
for event in &self.resources.retained_ui.frame_events {
if let crate::ecs::ui::resources::UiEvent::TileTabClosed {
container: event_container,
pane_id,
title,
} = event
&& *event_container == container
{
return Some((*pane_id, title.clone()));
}
}
None
}
pub fn ui_tile_splitter_moved(&self, container: freecs::Entity) -> Option<(TileId, f32)> {
for event in &self.resources.retained_ui.frame_events {
if let crate::ecs::ui::resources::UiEvent::TileSplitterMoved {
container: event_container,
split_id,
ratio,
} = event
&& *event_container == container
{
return Some((*split_id, *ratio));
}
}
None
}
pub fn ui_tile_set_active(&mut self, container: freecs::Entity, pane_id: TileId) {
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state_mut(container)
else {
return;
};
if let Some(tabs_id) = data.find_parent_tabs(pane_id)
&& let Some(TileNode::Tabs { panes, active }) = data.get_mut(tabs_id)
&& let Some(index) = panes.iter().position(|id| *id == pane_id)
{
*active = index;
}
}
pub fn ui_tile_save_layout(
&self,
container: freecs::Entity,
) -> Option<crate::ecs::ui::components::TileLayout> {
use crate::ecs::ui::components::{TileLayout, TileLayoutNode};
let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state(container) else {
return None;
};
let nodes = data
.tiles
.iter()
.map(|tile| {
tile.as_ref().map(|node| match node {
TileNode::Pane { title, .. } => TileLayoutNode::Pane {
title: title.clone(),
},
TileNode::Split {
direction,
ratio,
children,
} => TileLayoutNode::Split {
direction: *direction,
ratio: *ratio,
children: *children,
},
TileNode::Tabs { panes, active } => TileLayoutNode::Tabs {
panes: panes.clone(),
active: *active,
},
})
})
.collect();
Some(TileLayout {
nodes,
root: data.root,
})
}
pub fn ui_tile_load_layout(
&mut self,
container: freecs::Entity,
layout: &crate::ecs::ui::components::TileLayout,
) -> Vec<(TileId, freecs::Entity)> {
use crate::ecs::ui::components::TileLayoutNode;
let old_pane_entities: Vec<freecs::Entity> =
if let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state(container) {
data.tiles
.iter()
.filter_map(|tile| {
if let Some(TileNode::Pane { content_entity, .. }) = tile {
Some(*content_entity)
} else {
None
}
})
.collect()
} else {
return Vec::new();
};
for entity in &old_pane_entities {
if let Some(node) = self.get_ui_layout_node_mut(*entity) {
node.visible = false;
}
}
let mut pane_mappings = Vec::new();
let mut new_tiles: Vec<Option<TileNode>> = Vec::with_capacity(layout.nodes.len());
for layout_node in &layout.nodes {
match layout_node {
Some(TileLayoutNode::Pane { title }) => {
let content_entity = self.spawn_tile_pane_entity(container);
let tile_id = TileId(new_tiles.len());
pane_mappings.push((tile_id, content_entity));
new_tiles.push(Some(TileNode::Pane {
content_entity,
title: title.clone(),
}));
}
Some(TileLayoutNode::Split {
direction,
ratio,
children,
}) => {
new_tiles.push(Some(TileNode::Split {
direction: *direction,
ratio: *ratio,
children: *children,
}));
}
Some(TileLayoutNode::Tabs { panes, active }) => {
new_tiles.push(Some(TileNode::Tabs {
panes: panes.clone(),
active: *active,
}));
}
None => {
new_tiles.push(None);
}
}
}
let mut next_free = Vec::new();
for (index, tile) in new_tiles.iter().enumerate() {
if tile.is_none() {
next_free.push(index);
}
}
let rects_len = new_tiles.len();
if let Some(UiWidgetState::TileContainer(data)) = self.get_ui_widget_state_mut(container) {
data.tiles = new_tiles;
data.root = layout.root;
data.next_free = next_free;
data.rects
.resize(rects_len, crate::ecs::ui::types::Rect::default());
data.dragging_splitter = None;
data.pending_tab_drag = None;
data.dragging_tab = None;
data.drop_preview = None;
data.hovered_close = None;
}
self.resources.children_cache_valid = false;
self.resources.retained_ui.layout_dirty = true;
pane_mappings
}
fn spawn_tile_pane_entity(&mut self, container: freecs::Entity) -> freecs::Entity {
use crate::ecs::ui::layout_types::{FlowAlignment, FlowDirection, FlowLayout};
use crate::ecs::ui::state::UiBase;
let theme = self.resources.retained_ui.theme_state.active_theme();
let panel_color = theme.panel_color;
let entities = self.spawn_entities(
crate::ecs::world::UI_LAYOUT_NODE
| crate::ecs::world::UI_NODE_COLOR
| crate::ecs::world::UI_NODE_CONTENT
| crate::ecs::world::UI_STATE_WEIGHTS
| crate::ecs::world::PARENT,
1,
);
let content_entity = entities[0];
if let Some(parent) = self.get_parent_mut(content_entity) {
*parent = crate::ecs::transform::components::Parent(Some(container));
}
if let Some(node) = self.get_ui_layout_node_mut(content_entity) {
*node = crate::ecs::ui::components::UiLayoutNode::default();
node.layouts[UiBase::INDEX] = Some(crate::ecs::ui::layout_types::UiLayoutType::Window(
crate::ecs::ui::layout_types::WindowLayout {
position: Ab(Vec2::new(0.0, 0.0)).into(),
size: Ab(Vec2::new(100.0, 100.0)).into(),
anchor: Anchor::TopLeft,
},
));
node.depth = crate::ecs::ui::components::UiDepthMode::Add(0.0);
node.clip_content = true;
node.flow_layout = Some(FlowLayout {
direction: FlowDirection::Vertical,
padding: 4.0,
spacing: 4.0,
alignment: FlowAlignment::Start,
cross_alignment: FlowAlignment::Start,
wrap: false,
});
}
if let Some(color) = self.get_ui_node_color_mut(content_entity) {
color.colors[UiBase::INDEX] = Some(panel_color);
}
if let Some(content) = self.get_ui_node_content_mut(content_entity) {
*content = crate::ecs::ui::components::UiNodeContent::Rect {
corner_radius: 0.0,
border_width: 0.0,
border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
};
}
self.resources.children_cache_valid = false;
self.resources.retained_ui.layout_dirty = true;
content_entity
}
pub fn build_tiles(
&mut self,
container: freecs::Entity,
f: impl FnOnce(&mut TileBuilder<'_, '_>),
) {
let mut tree = UiTreeBuilder::from_parent(self, container);
tree.build_tiles(container, f);
}
pub fn ui_on<F>(&mut self, entity: freecs::Entity, handler: F)
where
F: FnMut(&mut crate::ecs::world::World, &crate::ecs::ui::resources::UiEvent) + 'static,
{
self.resources
.retained_ui
.event_handlers
.entry(entity)
.or_default()
.push(Box::new(handler));
}
pub fn ui_off(&mut self, entity: freecs::Entity) {
self.resources.retained_ui.event_handlers.remove(&entity);
}
pub fn ui_data_grid_enable_filters(&mut self, entity: freecs::Entity) {
let columns = if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state(entity)
{
data.columns.clone()
} else {
return;
};
let theme = self.resources.retained_ui.theme_state.active_theme();
let font_size = theme.font_size;
let input_bg = theme.input_background_color;
let text_color = theme.text_color;
let border_color = theme.border_color;
let corner_radius = theme.corner_radius;
let row_height = 28.0;
let header_entity =
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state(entity) {
data.header_entities
.first()
.and_then(|header| self.get_parent(*header).and_then(|p| p.0))
} else {
None
};
let header_parent = if let Some(he) = header_entity {
self.get_parent(he).and_then(|p| p.0)
} else {
None
};
let filter_row_parent = header_parent.unwrap_or(entity);
let mut filter_input_entities = Vec::new();
let mut filter_texts = Vec::new();
let filter_row = {
let mut tree = crate::ecs::ui::builder::UiTreeBuilder::new(self);
tree.push_parent(filter_row_parent);
let row_entity = tree
.add_node()
.flow_child(
crate::ecs::ui::units::Rl(Vec2::new(100.0, 0.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, row_height)),
)
.flow(FlowDirection::Horizontal, 0.0, 2.0)
.entity();
tree.push_parent(row_entity);
for column in &columns {
let text_slot = tree.world_mut().resources.text_cache.add_text("");
let input = tree
.add_node()
.flow_child(crate::ecs::ui::units::Ab(Vec2::new(
column.width,
row_height,
)))
.with_rect(corner_radius, 1.0, border_color)
.with_color::<UiBase>(input_bg)
.with_interaction()
.with_children(|inner| {
inner
.add_node()
.with_text_slot(text_slot, font_size)
.with_color::<UiBase>(text_color)
.without_pointer_events()
.done();
})
.done();
filter_input_entities.push(input);
filter_texts.push(String::new());
}
tree.pop_parent();
tree.pop_parent();
row_entity
};
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state_mut(entity) {
data.filter_row_entity = Some(filter_row);
data.filter_input_entities = filter_input_entities;
data.filter_texts = filter_texts;
}
}
pub fn ui_data_grid_set_filtered_indices(
&mut self,
entity: freecs::Entity,
indices: Option<Vec<usize>>,
) {
if let Some(UiWidgetState::DataGrid(data)) = self.get_ui_widget_state_mut(entity) {
data.filtered_indices = indices;
}
self.resources.retained_ui.layout_dirty = true;
}
pub fn ui_set_state_active(
&mut self,
entity: freecs::Entity,
state_index: usize,
active: bool,
) {
if let Some(weights) = self.get_ui_state_weights_mut(entity) {
weights.ensure_state_capacity(state_index + 1);
weights.targets[state_index] = if active { 1.0 } else { 0.0 };
weights.start_weights[state_index] = weights.weights[state_index];
weights.progress[state_index] = 0.0;
}
}
pub(crate) fn ui_create_property<T: crate::ecs::ui::resources::IntoPropertyValue>(
&mut self,
entity: freecs::Entity,
initial: T,
) -> crate::ecs::ui::resources::PropertyId {
let id = crate::ecs::ui::resources::PropertyId(self.resources.retained_ui.next_property_id);
self.resources.retained_ui.next_property_id += 1;
self.resources.retained_ui.bound_properties.insert(
id,
crate::ecs::ui::resources::BoundProperty {
value: initial.into_property_value(),
entity,
dirty_from_widget: false,
dirty_from_code: false,
},
);
id
}
pub fn ui_register_named<T: crate::ecs::ui::resources::IntoPropertyValue>(
&mut self,
name: &str,
entity: freecs::Entity,
initial: T,
) {
let property_id = self.ui_create_property(entity, initial);
self.resources
.retained_ui
.register_named_property(name, property_id);
}
pub(crate) fn ui_property<T: crate::ecs::ui::resources::FromPropertyValue>(
&self,
id: crate::ecs::ui::resources::PropertyId,
) -> T {
self.resources
.retained_ui
.bound_properties
.get(&id)
.and_then(|prop| T::from_property_value(&prop.value))
.unwrap_or_else(T::default_value)
}
pub(crate) fn ui_set_property<T: crate::ecs::ui::resources::IntoPropertyValue>(
&mut self,
id: crate::ecs::ui::resources::PropertyId,
value: T,
) {
if let Some(prop) = self.resources.retained_ui.bound_properties.get_mut(&id) {
prop.value = value.into_property_value();
prop.dirty_from_code = true;
}
}
pub fn ui_prop<T: crate::ecs::ui::resources::FromPropertyValue>(&self, name: &str) -> T {
if let Some(&property_id) = self.resources.retained_ui.named_properties.get(name) {
self.ui_property(property_id)
} else {
#[cfg(debug_assertions)]
panic!("Unknown UI property: {name}");
#[cfg(not(debug_assertions))]
T::default_value()
}
}
pub fn ui_set_prop<T: crate::ecs::ui::resources::IntoPropertyValue>(
&mut self,
name: &str,
value: T,
) {
if let Some(&property_id) = self.resources.retained_ui.named_properties.get(name) {
self.ui_set_property(property_id, value);
} else {
#[cfg(debug_assertions)]
panic!("Unknown UI property: {name}");
}
}
pub fn ui_prop_entity(&self, name: &str) -> Option<freecs::Entity> {
self.resources
.retained_ui
.named_properties
.get(name)
.and_then(|id| self.resources.retained_ui.bound_properties.get(id))
.map(|bp| bp.entity)
}
pub fn ui_clicked(&self, entity: freecs::Entity) -> bool {
self.widget::<crate::ecs::ui::components::UiButtonData>(entity)
.is_some_and(|d| d.clicked)
}
pub fn ui_react<T, F>(&mut self, name: &str, mut handler: F)
where
T: crate::ecs::ui::resources::FromPropertyValue + 'static,
F: FnMut(T, &mut Self) + 'static,
{
let wrapped: crate::ecs::ui::resources::PropertyReaction = Box::new(move |value, world| {
if let Some(typed) = T::from_property_value(value) {
handler(typed, world);
}
});
self.resources
.retained_ui
.property_reactions
.entry(name.to_string())
.or_default()
.push(wrapped);
}
pub fn ui_react_clicked<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(&mut Self) + 'static,
{
self.resources
.retained_ui
.click_reactions
.entry(entity)
.or_default()
.push(Box::new(move |world| handler(world)));
}
pub fn ui_react_submitted<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(String, &mut Self) + 'static,
{
self.resources
.retained_ui
.submit_reactions
.entry(entity)
.or_default()
.push(Box::new(move |text, world| handler(text, world)));
}
pub fn ui_react_confirmed<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(bool, &mut Self) + 'static,
{
self.resources
.retained_ui
.confirm_reactions
.entry(entity)
.or_default()
.push(Box::new(move |confirmed, world| {
handler(confirmed, world);
}));
}
pub fn ui_react_menu_selected<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(usize, &mut Self) + 'static,
{
self.resources
.retained_ui
.menu_select_reactions
.entry(entity)
.or_default()
.push(Box::new(move |index, world| handler(index, world)));
}
pub fn ui_react_command<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(usize, &mut Self) + 'static,
{
self.resources
.retained_ui
.command_reactions
.entry(entity)
.or_default()
.push(Box::new(move |index, world| handler(index, world)));
}
pub fn ui_react_tree_selected<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(freecs::Entity, &mut Self) + 'static,
{
self.resources
.retained_ui
.tree_select_reactions
.entry(entity)
.or_default()
.push(Box::new(move |node, world| handler(node, world)));
}
pub fn ui_react_tree_context_menu<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(freecs::Entity, nalgebra_glm::Vec2, &mut Self) + 'static,
{
self.resources
.retained_ui
.tree_context_menu_reactions
.entry(entity)
.or_default()
.push(Box::new(move |node, position, world| {
handler(node, position, world);
}));
}
pub fn ui_react_multi_select_changed<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(Vec<usize>, &mut Self) + 'static,
{
self.resources
.retained_ui
.multi_select_reactions
.entry(entity)
.or_default()
.push(Box::new(move |indices, world| handler(indices, world)));
}
pub fn ui_react_date_changed<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(i32, u32, u32, &mut Self) + 'static,
{
self.resources
.retained_ui
.date_changed_reactions
.entry(entity)
.or_default()
.push(Box::new(move |year, month, day, world| {
handler(year, month, day, world);
}));
}
pub fn ui_react_changed<F>(&mut self, entity: freecs::Entity, mut handler: F)
where
F: FnMut(&mut Self) + 'static,
{
self.resources
.retained_ui
.changed_reactions
.entry(entity)
.or_default()
.push(Box::new(move |world| handler(world)));
}
pub fn ui_react_any<F>(&mut self, names: &[&str], handler: F)
where
F: FnMut(&mut Self) + 'static,
{
let handler = std::rc::Rc::new(std::cell::RefCell::new(handler));
for name in names {
let handler = handler.clone();
self.resources
.retained_ui
.property_reactions
.entry(name.to_string())
.or_default()
.push(Box::new(move |_value, world| {
(handler.borrow_mut())(world);
}));
}
}
pub fn ui_cleanup_entity(&mut self, entity: freecs::Entity) {
let ui = &mut self.resources.retained_ui;
ui.click_reactions.remove(&entity);
ui.submit_reactions.remove(&entity);
ui.confirm_reactions.remove(&entity);
ui.menu_select_reactions.remove(&entity);
ui.command_reactions.remove(&entity);
ui.tree_select_reactions.remove(&entity);
ui.tree_context_menu_reactions.remove(&entity);
ui.multi_select_reactions.remove(&entity);
ui.date_changed_reactions.remove(&entity);
ui.changed_reactions.remove(&entity);
ui.event_handlers.remove(&entity);
let stale_ids: Vec<crate::ecs::ui::resources::PropertyId> = ui
.bound_properties
.iter()
.filter(|(_, prop)| prop.entity == entity)
.map(|(&id, _)| id)
.collect();
for id in &stale_ids {
ui.bound_properties.remove(id);
}
let stale_names: Vec<String> = ui
.named_properties
.iter()
.filter(|(_, id)| stale_ids.contains(id))
.map(|(name, _)| name.clone())
.collect();
for name in &stale_names {
ui.named_properties.remove(name);
ui.property_reactions.remove(name);
}
}
pub fn ui_remove_property(&mut self, name: &str) {
let ui = &mut self.resources.retained_ui;
if let Some(property_id) = ui.named_properties.remove(name) {
ui.bound_properties.remove(&property_id);
}
ui.property_reactions.remove(name);
}
pub fn ui_set_reduced_motion(&mut self, enabled: bool) {
self.resources.retained_ui.reduced_motion = enabled;
}
pub fn ui_announce(&mut self, message: &str) {
self.resources
.retained_ui
.announce_queue
.push(message.to_string());
}
pub fn ui_announcements(&self) -> &[String] {
&self.resources.retained_ui.announce_queue
}
pub fn ui_set_accessible_label(&mut self, entity: freecs::Entity, label: &str) {
if let Some(interaction) = self.get_ui_node_interaction_mut(entity) {
interaction.accessible_label = Some(label.to_string());
}
}
pub fn ui_multi_select_set_selected(&mut self, entity: freecs::Entity, indices: &[usize]) {
let update =
if let Some(UiWidgetState::MultiSelect(data)) = self.get_ui_widget_state_mut(entity) {
data.selected_indices = indices.iter().copied().collect();
let count = data.selected_indices.len();
let header_slot = data.header_text_slot;
let header_text = format!("{count} selected");
let check_updates: Vec<(freecs::Entity, bool)> = data
.check_entities
.iter()
.enumerate()
.map(|(index, &e)| (e, data.selected_indices.contains(&index)))
.collect();
Some((header_slot, header_text, check_updates))
} else {
None
};
if let Some((header_slot, header_text, check_updates)) = update {
for (check_entity, selected) in check_updates {
let check_text = if selected { "\u{2713}" } else { " " };
if let Some(crate::ecs::ui::components::UiNodeContent::Text { text_slot, .. }) =
self.get_ui_node_content(check_entity)
{
let slot = *text_slot;
self.resources.text_cache.set_text(slot, check_text);
}
}
self.resources
.text_cache
.set_text(header_slot, &header_text);
}
}
pub fn ui_date_picker_set_value(
&mut self,
entity: freecs::Entity,
year: i32,
month: u32,
day: u32,
) {
let header_text = format!("{year:04}-{month:02}-{day:02}");
let update = if let Some(UiWidgetState::DatePicker(data)) = self.get_ui_widget_state(entity)
{
Some((
data.header_text_slot,
data.month_label_slot,
data.day_text_slots.clone(),
data.day_entities.clone(),
))
} else {
None
};
if let Some((header_slot, month_slot, day_slots, day_ents)) = update {
self.resources
.text_cache
.set_text(header_slot, &header_text);
let month_label = format_month_year(year, month);
self.resources.text_cache.set_text(month_slot, &month_label);
let theme = self.resources.retained_ui.theme_state.active_theme();
let accent_color = theme.accent_color;
populate_calendar_grid(self, year, month, day, &day_slots, &day_ents, accent_color);
if let Some(UiWidgetState::DatePicker(data)) = self.get_ui_widget_state_mut(entity) {
data.year = year;
data.month = month;
data.day = day;
}
}
}
}
pub(crate) fn format_month_year(year: i32, month: u32) -> String {
let month_name = match month {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
_ => "???",
};
format!("{month_name} {year}")
}
pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
29
} else {
28
}
}
_ => 30,
}
}
pub(crate) fn day_of_week(year: i32, month: u32, day: u32) -> u32 {
let (adjusted_year, adjusted_month) = if month <= 2 {
(year - 1, month + 12)
} else {
(year, month)
};
let q = day as i32;
let m = adjusted_month as i32;
let k = adjusted_year % 100;
let j = adjusted_year / 100;
let h = (q + (13 * (m + 1)) / 5 + k + k / 4 + j / 4 - 2 * j).rem_euclid(7);
((h + 6) % 7) as u32
}
pub(crate) fn populate_calendar_grid(
world: &mut crate::ecs::world::World,
year: i32,
month: u32,
selected_day: u32,
day_text_slots: &[usize],
day_entities: &[freecs::Entity],
accent_color: nalgebra_glm::Vec4,
) {
let dim_color = nalgebra_glm::Vec4::new(0.5, 0.5, 0.5, 0.3);
let clear_color = nalgebra_glm::Vec4::new(0.0, 0.0, 0.0, 0.0);
let first_dow = day_of_week(year, month, 1);
let days = days_in_month(year, month);
let (prev_year, prev_month) = if month == 1 {
(year - 1, 12)
} else {
(year, month - 1)
};
let prev_days = days_in_month(prev_year, prev_month);
for cell_index in 0..42 {
let (day_num, is_current_month) = if (cell_index as u32) < first_dow {
let d = prev_days - first_dow + cell_index as u32 + 1;
(d, false)
} else {
let d = cell_index as u32 - first_dow + 1;
if d <= days {
(d, true)
} else {
let d = d - days;
(d, false)
}
};
world
.resources
.text_cache
.set_text(day_text_slots[cell_index], day_num.to_string());
let is_selected = is_current_month && day_num == selected_day;
let base_color = if is_selected {
accent_color
} else if is_current_month {
clear_color
} else {
dim_color
};
if let Some(color) = world.get_ui_node_color_mut(day_entities[cell_index]) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(base_color);
}
}
}