use crate::ecs::world::World;
use nalgebra_glm::{Vec2, Vec4};
use crate::ecs::ui::state::{UiBase, UiStateTrait};
use crate::ecs::ui::units::Rl;
use super::range_slider_visuals::{RangeSliderVisualUpdate, update_range_slider_visuals};
use crate::prelude::*;
pub fn measure_ui_text_width(world: &mut World, text: &str, font_size: f32) -> f32 {
crate::ecs::ui::widget_systems::measure_text_width(
&mut world.resources.text.font_engine,
text,
font_size,
)
}
pub fn widget<T: crate::ecs::ui::components::WidgetData>(
world: &World,
entity: freecs::Entity,
) -> Option<&T> {
T::get(world, entity)
}
pub fn ui_changed(world: &World, entity: freecs::Entity) -> bool {
if let Some(d) = world.ui.get_ui_slider(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_range_slider(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_toggle(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_checkbox(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_radio(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_tab_bar(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_breadcrumb(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_splitter(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_text_input(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_dropdown(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_multi_select(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_date_picker(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_color_picker(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_selectable_label(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_drag_value(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_tree_view(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_text_area(entity) {
return d.changed;
}
if let Some(d) = world.ui.get_ui_rich_text_editor(entity) {
return d.changed;
}
false
}
pub fn ui_slider_value(world: &World, entity: freecs::Entity) -> Option<f32> {
world.ui.get_ui_slider(entity).map(|data| data.value)
}
pub fn ui_slider_value_changed(world: &World, entity: freecs::Entity) -> Option<f32> {
if let Some(data) = world.ui.get_ui_slider(entity)
&& data.changed
{
Some(data.value)
} else {
None
}
}
pub fn ui_drag_value(world: &World, entity: freecs::Entity) -> Option<f32> {
world.ui.get_ui_drag_value(entity).map(|data| data.value)
}
pub fn ui_drag_value_changed(world: &World, entity: freecs::Entity) -> Option<f32> {
if let Some(data) = world.ui.get_ui_drag_value(entity)
&& data.changed
{
Some(data.value)
} else {
None
}
}
pub fn ui_toggle_value(world: &World, entity: freecs::Entity) -> Option<bool> {
world.ui.get_ui_toggle(entity).map(|data| data.value)
}
pub fn ui_toggle_changed(world: &World, entity: freecs::Entity) -> Option<bool> {
if let Some(data) = world.ui.get_ui_toggle(entity)
&& data.changed
{
Some(data.value)
} else {
None
}
}
pub fn ui_checkbox_value(world: &World, entity: freecs::Entity) -> Option<bool> {
world.ui.get_ui_checkbox(entity).map(|data| data.value)
}
pub fn ui_checkbox_changed(world: &World, entity: freecs::Entity) -> Option<bool> {
if let Some(data) = world.ui.get_ui_checkbox(entity)
&& data.changed
{
Some(data.value)
} else {
None
}
}
pub fn ui_text_input_text(world: &World, entity: freecs::Entity) -> Option<&str> {
if let Some(data) = world.ui.get_ui_text_input(entity) {
Some(data.text.as_str())
} else {
None
}
}
pub fn ui_text_input_changed(world: &World, entity: freecs::Entity) -> Option<&str> {
if let Some(data) = world.ui.get_ui_text_input(entity)
&& data.changed
{
Some(data.text.as_str())
} else {
None
}
}
pub fn ui_dropdown_selected(world: &World, entity: freecs::Entity) -> Option<usize> {
world
.ui
.get_ui_dropdown(entity)
.map(|data| data.selected_index)
}
pub fn ui_dropdown_selected_changed(world: &World, entity: freecs::Entity) -> Option<usize> {
if let Some(data) = world.ui.get_ui_dropdown(entity)
&& data.changed
{
Some(data.selected_index)
} else {
None
}
}
pub fn ui_color_picker_color(world: &World, entity: freecs::Entity) -> Option<nalgebra_glm::Vec4> {
world.ui.get_ui_color_picker(entity).map(|data| data.color)
}
pub fn ui_color_picker_changed(
world: &World,
entity: freecs::Entity,
) -> Option<nalgebra_glm::Vec4> {
if let Some(data) = world.ui.get_ui_color_picker(entity)
&& data.changed
{
Some(data.color)
} else {
None
}
}
pub fn ui_radio_selected(world: &World, group_id: u32) -> Option<usize> {
let entities = world
.resources
.retained_ui
.groups
.radio
.get(&group_id)?
.clone();
for entity in entities {
if let Some(data) = world.ui.get_ui_radio(entity)
&& data.selected
{
return Some(data.option_index);
}
}
None
}
pub fn ui_tree_view_selected(world: &World, entity: freecs::Entity) -> Vec<freecs::Entity> {
if let Some(data) = world.ui.get_ui_tree_view(entity) {
data.selected_nodes.clone()
} else {
Vec::new()
}
}
pub fn ui_collapsing_header_open(world: &World, entity: freecs::Entity) -> Option<bool> {
world
.ui
.get_ui_collapsing_header(entity)
.map(|data| data.open)
}
pub fn ui_tab_bar_selected(world: &World, entity: freecs::Entity) -> Option<usize> {
world
.ui
.get_ui_tab_bar(entity)
.map(|data| data.selected_tab)
}
pub fn ui_button_set_text(world: &mut World, entity: freecs::Entity, text: &str) {
if let Some(data) = world.ui.get_ui_button(entity) {
let slot = data.text_slot;
world.resources.text.cache.set_text(slot, text);
}
}
pub fn ui_slider_set_value(world: &mut World, entity: freecs::Entity, value: f32) {
let update = if let Some(data) = world.ui.get_ui_slider(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) = world.ui.get_ui_layout_node_mut(fill_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
fill_node.base_layout.as_mut()
{
boundary.position_2 = Rl(Vec2::new(normalized * 100.0, 100.0)).into();
}
let precision = if logarithmic { 3 } else { 1 };
world
.resources
.text
.cache
.set_text(text_slot, format!("{:.prec$}", clamped, prec = precision));
if let Some(data) = world.ui.get_ui_slider_mut(entity) {
data.value = clamped;
}
}
}
pub fn ui_range_slider_set_values(world: &mut World, entity: freecs::Entity, low: f32, high: f32) {
let update = if let Some(data) = world.ui.get_ui_range_slider(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(
world,
&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(data) = world.ui.get_ui_range_slider_mut(entity) {
data.low_value = clamped_low;
data.high_value = clamped_high;
}
}
}
pub fn ui_breadcrumb_set_segments(world: &mut World, entity: freecs::Entity, segments: &[&str]) {
let text_slots: Vec<usize> = if let Some(data) = world.ui.get_ui_breadcrumb_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() {
world
.resources
.text
.cache
.set_text(*text_slot, segments[index]);
}
}
}
pub fn ui_toggle_set_value(world: &mut World, entity: freecs::Entity, value: bool) {
if let Some(data) = world.ui.get_ui_toggle_mut(entity) {
data.value = value;
}
}
pub fn ui_radio_group_value(world: &World, group_id: u32) -> Option<usize> {
let entities: Vec<freecs::Entity> = world
.ui
.query_entities(crate::ecs::world::UI_RADIO)
.collect();
for entity in entities {
if let Some(data) = world.ui.get_ui_radio(entity)
&& data.group_id == group_id
&& data.selected
{
return Some(data.option_index);
}
}
None
}
pub fn ui_progress_bar_set_value(world: &mut World, entity: freecs::Entity, value: f32) {
let update = world
.ui
.get_ui_progress_bar(entity)
.map(|data| data.fill_entity);
if let Some(fill_entity) = update {
let normalized = value.clamp(0.0, 1.0);
if let Some(fill_node) = world.ui.get_ui_layout_node_mut(fill_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
fill_node.base_layout.as_mut()
{
boundary.position_2 = Rl(Vec2::new(normalized * 100.0, 100.0)).into();
}
if let Some(data) = world.ui.get_ui_progress_bar_mut(entity) {
data.value = normalized;
}
}
}
pub fn ui_scroll_area_set_offset(world: &mut World, entity: freecs::Entity, offset: f32) {
if let Some(data) = world.ui.get_ui_scroll_area_mut(entity) {
data.scroll_offset = offset;
}
}
pub fn ui_scroll_area_set_snap(world: &mut World, entity: freecs::Entity, interval: Option<f32>) {
if let Some(data) = world.ui.get_ui_scroll_area_mut(entity) {
data.snap_interval = interval;
}
}
pub fn ui_virtual_list_set_count(world: &mut World, entity: freecs::Entity, count: usize) {
if let Some(data) = world.ui.get_ui_virtual_list_mut(entity) {
data.total_items = count;
}
}
pub fn ui_virtual_list_selection_changed(world: &World, entity: freecs::Entity) -> bool {
if let Some(data) = world.ui.get_ui_virtual_list(entity) {
data.selection_changed
} else {
false
}
}
pub fn ui_virtual_list_populate(
world: &mut World,
entity: freecs::Entity,
source: &dyn crate::ecs::ui::components::VirtualListDataSource,
) {
let count = source.item_count();
ui_virtual_list_set_count(world, entity, count);
let (visible_start, pool_items) = match world.ui.get_ui_virtual_list(entity) {
Some(data) => (data.visible_start, data.pool_items.clone()),
None => return,
};
for (pool_index, pool_item) in pool_items.iter().enumerate() {
let item_index = visible_start + pool_index;
if item_index < count {
source.bind_item(world, item_index, pool_item.container_entity);
}
}
}
pub fn ui_virtual_list_populate_fn(
world: &mut World,
entity: freecs::Entity,
count: usize,
bind: impl Fn(&mut crate::ecs::world::World, usize, freecs::Entity),
) {
ui_virtual_list_set_count(world, entity, count);
let (visible_start, pool_items) = match world.ui.get_ui_virtual_list(entity) {
Some(data) => (data.visible_start, data.pool_items.clone()),
None => return,
};
for (pool_index, pool_item) in pool_items.iter().enumerate() {
let item_index = visible_start + pool_index;
if item_index < count {
bind(world, item_index, pool_item.container_entity);
}
}
}
pub fn ui_tab_bar_set_value(world: &mut World, entity: freecs::Entity, index: usize) {
let update = if let Some(data) = world.ui.get_ui_tab_bar(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 = world.resources.retained_ui.theme_state.active_theme();
let active_bg = theme.accent_color;
let inactive_bg = theme.background_color;
if let Some(color) = world.ui.get_ui_node_color_mut(old_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(inactive_bg);
}
if let Some(color) = world.ui.get_ui_node_color_mut(new_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(active_bg);
}
if let Some(data) = world.ui.get_ui_tab_bar_mut(entity) {
data.selected_tab = index;
}
}
}
pub fn ui_text_input_set_value(world: &mut World, entity: freecs::Entity, text: &str) {
let (text_slot, placeholder_entity, password) =
if let Some(data) = world.ui.get_ui_text_input(entity) {
(Some(data.text_slot), data.placeholder_entity, data.password)
} else {
(None, None, false)
};
if let Some(slot) = text_slot {
world
.resources
.text
.cache
.set_text(slot, masked_input_display(text, password));
if let Some(data) = world.ui.get_ui_text_input_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) = world.ui.get_ui_layout_node_mut(ph_entity)
{
node.visible = text.is_empty();
}
}
}
pub fn ui_text_input_set_mask(
world: &mut World,
entity: freecs::Entity,
mask: crate::ecs::ui::components::InputMask,
) {
if let Some(data) = world.ui.get_ui_text_input_mut(entity) {
data.input_mask = mask;
}
}
pub fn ui_text_input_set_password(world: &mut World, entity: freecs::Entity, password: bool) {
let update = if let Some(data) = world.ui.get_ui_text_input_mut(entity) {
if data.password == password {
return;
}
data.password = password;
Some((data.text_slot, data.text.clone()))
} else {
None
};
if let Some((slot, text)) = update {
world
.resources
.text
.cache
.set_text(slot, masked_input_display(&text, password));
}
}
pub fn ui_text_input_submitted(world: &World, entity: freecs::Entity) -> Option<String> {
for event in &world.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(world: &mut World, entity: freecs::Entity, index: usize) {
let update = if let Some(data) = world.ui.get_ui_dropdown(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 {
world
.resources
.text
.cache
.set_text(text_slot, selected_text);
if let Some(data) = world.ui.get_ui_dropdown_mut(entity) {
data.selected_index = index;
}
}
}
pub fn ui_panel_set_pinned(world: &mut World, entity: freecs::Entity, pinned: bool) {
if let Some(data) = world.ui.get_ui_panel_mut(entity) {
data.pinned = pinned;
}
}
pub fn ui_panel_set_header_visible(world: &mut World, entity: freecs::Entity, visible: bool) {
if let Some(data) = world.ui.get_ui_panel_mut(entity) {
let header = data.header_entity;
let content = data.content_entity;
data.header_visible = visible;
ui_set_visible(world, header, visible);
if let Some(node) = world.ui.get_ui_layout_node_mut(content)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(ref mut boundary)) =
node.base_layout
{
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(world: &World) -> &crate::ecs::ui::resources::ReservedAreas {
&world
.resources
.retained_ui
.dock_state
.primary
.reserved_areas
}
pub fn ui_dock_layout(world: &World) -> &crate::ecs::ui::resources::WindowDockLayout {
&world.resources.retained_ui.dock_state.primary
}
pub fn ui_dock_state(world: &World) -> &crate::ecs::ui::resources::DockState {
&world.resources.retained_ui.dock_state
}
pub fn ui_capture_dock_state(world: &World) -> crate::ecs::ui::resources::DockState {
world.resources.retained_ui.dock_state.clone()
}
pub fn ui_save_dock_state_to_str(world: &World) -> Result<String, String> {
serde_json::to_string_pretty(&world.resources.retained_ui.dock_state)
.map_err(|err| err.to_string())
}
pub fn ui_load_dock_state_from_str(world: &mut World, json: &str) -> Result<(), String> {
let state: crate::ecs::ui::resources::DockState =
serde_json::from_str(json).map_err(|err| err.to_string())?;
ui_apply_dock_state(world, &state);
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn ui_save_dock_state_to_path(
world: &World,
path: impl AsRef<std::path::Path>,
) -> Result<(), String> {
let json = ui_save_dock_state_to_str(world)?;
std::fs::write(path, json).map_err(|err| err.to_string())
}
#[cfg(not(target_arch = "wasm32"))]
pub fn ui_load_dock_state_from_path(
world: &mut World,
path: impl AsRef<std::path::Path>,
) -> Result<bool, String> {
let path = path.as_ref();
if !path.exists() {
return Ok(false);
}
let json = std::fs::read_to_string(path).map_err(|err| err.to_string())?;
ui_load_dock_state_from_str(world, &json)?;
Ok(true)
}
pub fn ui_set_window_menu_bar(world: &mut World, entity: freecs::Entity) {
world.resources.retained_ui.window_chrome.menu_bar = Some(entity);
}
pub fn ui_set_window_toolbar(world: &mut World, entity: freecs::Entity) {
world.resources.retained_ui.window_chrome.toolbar = Some(entity);
}
pub fn ui_set_window_status_bar(world: &mut World, entity: freecs::Entity) {
world.resources.retained_ui.window_chrome.status_bar = Some(entity);
}
pub fn ui_clear_window_chrome(world: &mut World) {
world.resources.retained_ui.window_chrome = Default::default();
}
pub fn ui_window_menu_bar(world: &World) -> Option<freecs::Entity> {
world.resources.retained_ui.window_chrome.menu_bar
}
pub fn ui_window_toolbar(world: &World) -> Option<freecs::Entity> {
world.resources.retained_ui.window_chrome.toolbar
}
pub fn ui_window_status_bar(world: &World) -> Option<freecs::Entity> {
world.resources.retained_ui.window_chrome.status_bar
}
pub fn ui_register_window_shortcut(
world: &mut World,
binding: crate::ecs::ui::components::ShortcutBinding,
command_index: usize,
) {
world.resources.retained_ui.window_chrome.shortcuts.push(
crate::ecs::ui::resources::WindowShortcut {
binding,
command_index,
},
);
}
pub fn ui_apply_dock_state(world: &mut World, state: &crate::ecs::ui::resources::DockState) {
let panel_entities: Vec<freecs::Entity> = world
.ui
.query_entities(crate::ecs::world::UI_PANEL)
.collect();
for entity in panel_entities {
let Some(data) = world.ui.get_ui_panel(entity) else {
continue;
};
let layout = &state.primary;
let id = data.id.clone();
if id.is_empty() {
continue;
}
let Some(config) = layout.panels.iter().find(|panel| panel.id == id) else {
continue;
};
let new_dock_size = config.size;
let new_collapsed = config.collapsed;
let new_visible = config.visible;
let new_kind = config.kind;
let new_floating_position = config.floating_position;
let new_floating_size = config.floating_size;
if let Some(panel) = world.ui.get_ui_panel_mut(entity) {
panel.default_dock_size = new_dock_size;
panel.collapsed = new_collapsed;
panel.panel_kind = new_kind;
}
if let Some(node) = world.ui.get_ui_layout_node_mut(entity) {
node.visible = new_visible;
if new_kind == crate::ecs::ui::components::UiPanelKind::Floating
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.base_layout.as_mut()
{
if let Some(position) = new_floating_position {
window.position = crate::ecs::ui::units::Ab(nalgebra_glm::Vec2::new(
position[0],
position[1],
))
.into();
}
if let Some(size) = new_floating_size {
window.size =
crate::ecs::ui::units::Ab(nalgebra_glm::Vec2::new(size[0], size[1])).into();
}
}
}
}
}
pub fn ui_pointer_over_widget(world: &World) -> bool {
world
.resources
.retained_ui
.interaction
.hovered_entity
.is_some()
|| world
.resources
.retained_ui
.interaction
.focused_entity
.is_some()
}
pub fn ui_color_picker_set_value(world: &mut World, entity: freecs::Entity, color: Vec4) {
let update = if let Some(data) = world.ui.get_ui_color_picker(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 => {
ui_slider_set_value(world, sliders[0], color.x);
ui_slider_set_value(world, sliders[1], color.y);
ui_slider_set_value(world, sliders[2], color.z);
ui_slider_set_value(world, sliders[3], color.w);
}
crate::ecs::ui::components::ColorPickerMode::Hsv => {
let hsv = crate::ecs::ui::color::Hsva::from_rgba(color);
ui_slider_set_value(world, sliders[0], hsv.hue);
ui_slider_set_value(world, sliders[1], hsv.saturation);
ui_slider_set_value(world, sliders[2], hsv.value);
ui_slider_set_value(world, sliders[3], color.w);
}
}
if let Some(swatch_color) = world.ui.get_ui_node_color_mut(swatch) {
swatch_color.colors[UiBase::INDEX] = Some(color);
}
if let Some(data) = world.ui.get_ui_color_picker_mut(entity) {
data.color = color;
}
}
}
pub(crate) fn masked_input_display(text: &str, password: bool) -> String {
if password {
"*".repeat(text.chars().count())
} else {
text.to_string()
}
}