use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::text::components::{TextAlignment, VerticalAlignment};
use crate::ecs::ui::components::*;
use crate::ecs::ui::state::{UiBase, UiStateTrait};
use crate::ecs::ui::types::Anchor;
use crate::ecs::ui::units::{Ab, Rl};
use super::range_slider_visuals::find_widget_content_in_defs;
impl crate::ecs::world::World {
pub fn ui_set_error(&mut self, entity: freecs::Entity, error: Option<&str>) {
if let Some(interaction) = self.ui.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.ui
.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.ui.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.ui.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
.ui
.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.ui.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.ui
.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.ui.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
.ui
.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.ui.get_ui_widget_state_mut(entity)
{
data.selected = selected;
}
if let Some(color) = self.ui.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.ui.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.ui.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.ui.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.ui.get_ui_widget_state(old_menu).cloned().as_ref()
{
if let Some(node) = self.ui.get_ui_layout_node_mut(old_data.popup_entity) {
node.visible = false;
}
if let Some(UiWidgetState::ContextMenu(wd)) = self.ui.get_ui_widget_state_mut(old_menu)
{
wd.open = false;
}
}
let popup =
if let Some(UiWidgetState::ContextMenu(data)) = self.ui.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.ui.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.ui.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.ui.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.ui.get_ui_widget_state(menu_entity)
{
Some(data.popup_entity)
} else {
None
};
if let Some(popup_entity) = popup
&& let Some(node) = self.ui.get_ui_layout_node_mut(popup_entity)
{
node.visible = false;
}
if let Some(UiWidgetState::ContextMenu(data)) =
self.ui.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.ui.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.ui.get_ui_widget_state(node_entity)
{
pre_filter.insert(node_entity, nd.expanded);
}
}
if let Some(UiWidgetState::TreeView(data)) = self.ui.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.ui.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.ui.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.ui.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.ui.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.ui.get_ui_widget_state(node_entity)
{
Some(nd.wrapper_entity)
} else {
None
};
if let Some(wrapper) = wrapper
&& let Some(node) = self.ui.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.ui.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.ui.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.ui.get_ui_layout_node_mut(children) {
node.visible = expanded;
}
if let Some(UiWidgetState::TreeNode(data)) = self.ui.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.ui.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.ui.get_ui_widget_state(entity) {
Some(data.backdrop_entity)
} else {
None
};
if let Some(backdrop_entity) = backdrop {
if let Some(node) = self.ui.get_ui_layout_node_mut(backdrop_entity) {
node.visible = true;
}
if let Some(node) = self.ui.get_ui_layout_node_mut(entity) {
node.visible = true;
}
if let Some(UiWidgetState::ModalDialog(data)) = self.ui.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.ui.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.ui.get_ui_widget_state(entity)
{
(data.backdrop_entity, data.text_input_entity)
} else {
return;
};
if let Some(node) = self.ui.get_ui_layout_node_mut(backdrop) {
node.visible = true;
}
if let Some(node) = self.ui.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.ui.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.ui.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.ui.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.ui.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.ui.get_ui_layout_node_mut(row_entity) {
node.visible = true;
}
} else if let Some(node) = self.ui.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.ui.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.ui.get_ui_widget_state(entity)
&& let Some(&span_entity) = data.span_entities.get(span_index)
&& let Some(node_color) = self.ui.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()
}
}