use nalgebra_glm::Vec2;
use crate::ecs::ui::components::*;
use crate::ecs::ui::layout_types::FlowDirection;
use crate::ecs::ui::state::UiBase;
use super::date_utils::{format_month_year, populate_calendar_grid};
impl crate::ecs::world::World {
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.ui.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.ui.get_ui_widget_state(entity) {
data.header_entities
.first()
.and_then(|header| self.core.get_parent(*header).and_then(|p| p.0))
} else {
None
};
let header_parent = if let Some(he) = header_entity {
self.core.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.get_ui_widget_state_mut(entity) {
data.year = year;
data.month = month;
data.day = day;
}
}
}
}