use crate::ecs::world::World;
use nalgebra_glm::Vec2;
use crate::ecs::ui::state::{UiBase, UiStateTrait};
use crate::prelude::*;
pub fn ui_clipboard_text(world: &World) -> &str {
&world.resources.retained_ui.clipboard_text
}
pub fn ui_set_clipboard_text(world: &mut World, text: impl Into<String>) {
let text = text.into();
world.resources.retained_ui.clipboard_text = text.clone();
sync_to_system_clipboard(&text);
}
pub fn ui_read_system_clipboard(world: &mut World) -> String {
if let Some(text) = read_from_system_clipboard() {
world.resources.retained_ui.clipboard_text = text.clone();
text
} else {
world.resources.retained_ui.clipboard_text.clone()
}
}
pub fn ui_events(world: &World) -> &[crate::ecs::ui::resources::UiEvent] {
world.resources.retained_ui.events_for_active()
}
pub fn ui_button_clicks(world: &World) -> impl Iterator<Item = freecs::Entity> + '_ {
ui_events(world).iter().filter_map(|event| match event {
crate::ecs::ui::resources::UiEvent::ButtonClicked(entity) => Some(*entity),
_ => None,
})
}
pub fn ui_events_for(
world: &World,
entity: freecs::Entity,
) -> impl Iterator<Item = &crate::ecs::ui::resources::UiEvent> {
use crate::ecs::ui::resources::UiEvent;
world
.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::VirtualListItemRightClicked {
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(world: &mut World, entity: freecs::Entity) {
if !world.resources.transform_state.children_cache_valid {
validate_and_rebuild_children_cache(world);
}
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) = world.resources.transform_state.children_cache.get(¤t) {
for child in children {
stack.push(*child);
}
}
}
for descendant in &all_entities {
if let Some(content) = world.ui.get_ui_node_content(*descendant)
&& let crate::ecs::ui::components::UiNodeContent::Text { text_slot, .. } = content
{
let slot = *text_slot;
world.resources.text.cache.remove_text(slot);
}
if let Some(data) = world.ui.get_ui_radio(*descendant).cloned()
&& let Some(group) = world
.resources
.retained_ui
.groups
.radio
.get_mut(&data.group_id)
{
group.retain(|e| *e != *descendant);
}
if let Some(data) = world.ui.get_ui_selectable_label(*descendant).cloned()
&& let Some(gid) = data.group_id
&& let Some(group) = world
.resources
.retained_ui
.groups
.selectable_labels
.get_mut(&gid)
{
group.retain(|e| *e != *descendant);
}
let mut slots: Vec<usize> = Vec::new();
if let Some(data) = world.ui.get_ui_button(*descendant) {
slots.push(data.text_slot);
}
if let Some(data) = world.ui.get_ui_slider(*descendant) {
slots.push(data.text_slot);
}
if let Some(data) = world.ui.get_ui_text_input(*descendant) {
slots.push(data.text_slot);
}
if let Some(data) = world.ui.get_ui_collapsing_header(*descendant) {
slots.push(data.arrow_text_slot);
}
if let Some(data) = world.ui.get_ui_dropdown(*descendant) {
slots.push(data.header_text_slot);
}
if let Some(data) = world.ui.get_ui_menu(*descendant) {
slots.push(data.label_text_slot);
}
if let Some(data) = world.ui.get_ui_panel(*descendant) {
slots.push(data.title_text_slot);
if let Some(slot) = data.collapse_button_text_slot {
slots.push(slot);
}
}
if let Some(data) = world.ui.get_ui_selectable_label(*descendant) {
slots.push(data.text_slot);
}
if let Some(data) = world.ui.get_ui_drag_value(*descendant) {
slots.push(data.text_slot);
}
if let Some(data) = world.ui.get_ui_tree_node(*descendant) {
slots.push(data.text_slot);
slots.push(data.arrow_text_slot);
}
if let Some(data) = world.ui.get_ui_modal_dialog(*descendant) {
slots.push(data.title_text_slot);
}
if let Some(data) = world.ui.get_ui_tab_bar(*descendant) {
slots.extend_from_slice(&data.tab_text_slots);
}
if let Some(data) = world.ui.get_ui_text_area(*descendant) {
slots.push(data.text_slot);
}
if let Some(data) = world.ui.get_ui_rich_text(*descendant) {
slots.extend_from_slice(&data.span_text_slots);
}
if let Some(data) = world.ui.get_ui_data_grid(*descendant) {
slots.extend_from_slice(&data.header_text_slots);
for row in &data.pool_rows {
slots.extend_from_slice(&row.cell_text_slots);
}
}
if let Some(data) = world.ui.get_ui_command_palette(*descendant) {
slots.extend_from_slice(&data.result_text_slots);
}
if let Some(data) = world.ui.get_ui_rich_text_editor(*descendant) {
slots.push(data.text_slot);
}
if let Some(data) = world.ui.get_ui_breadcrumb(*descendant) {
slots.extend_from_slice(&data.segment_text_slots);
}
if let Some(data) = world.ui.get_ui_multi_select(*descendant) {
slots.push(data.header_text_slot);
}
if let Some(data) = world.ui.get_ui_date_picker(*descendant) {
slots.push(data.header_text_slot);
slots.push(data.month_label_slot);
slots.extend_from_slice(&data.day_text_slots);
}
for slot in slots {
world.resources.text.cache.remove_text(slot);
}
}
crate::ecs::world::commands::despawn_recursive_immediate(world, entity);
ui_mark_children_dirty(world);
}
pub fn ui_clear_children(world: &mut World, parent: freecs::Entity) {
if !world.resources.transform_state.children_cache_valid {
validate_and_rebuild_children_cache(world);
}
let children: Vec<freecs::Entity> = world
.resources
.transform_state
.children_cache
.get(&parent)
.cloned()
.unwrap_or_default();
for child in children {
ui_despawn_node(world, child);
}
ui_mark_layout_dirty(world);
}
pub fn ui_rebuild_children(
world: &mut World,
parent: freecs::Entity,
build: impl FnOnce(&mut crate::ecs::ui::builder::UiTreeBuilder),
) {
ui_clear_children(world, parent);
let content = ui_widget_content(world, parent).unwrap_or(parent);
let mut builder = crate::ecs::ui::builder::UiTreeBuilder::from_parent(world, content);
build(&mut builder);
builder.finish_subtree();
}
pub fn ui_focus(world: &mut World, entity: freecs::Entity) {
world
.resources
.retained_ui
.interaction_for_active_mut()
.focused_entity = Some(entity);
}
pub fn ui_set_disabled(world: &mut World, entity: freecs::Entity, disabled: bool) {
if let Some(interaction) = world.ui.get_ui_node_interaction_mut(entity) {
interaction.disabled = disabled;
}
if disabled {
let base_color = world
.ui
.get_ui_node_color(entity)
.and_then(|color| color.colors[UiBase::INDEX]);
if let Some(base) = base_color
&& let Some(color_comp) = world.ui.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) = world.ui.get_ui_node_color_mut(entity) {
color_comp.colors[crate::ecs::ui::state::UiDisabled::INDEX] = None;
}
}
pub fn ui_set_disabled_recursive(world: &mut World, entity: freecs::Entity, disabled: bool) {
if !world.resources.transform_state.children_cache_valid {
validate_and_rebuild_children_cache(world);
}
let mut stack = vec![entity];
while let Some(current) = stack.pop() {
ui_set_disabled(world, current, disabled);
if let Some(children) = world.resources.transform_state.children_cache.get(¤t) {
for child in children {
stack.push(*child);
}
}
}
}
pub fn ui_is_disabled(world: &World, entity: freecs::Entity) -> bool {
world
.ui
.get_ui_node_interaction(entity)
.is_some_and(|interaction| interaction.disabled)
}
pub fn ui_set_named(world: &mut World, entity: freecs::Entity, name: &str) {
world
.resources
.retained_ui
.accessibility
.named_entities
.insert(name.to_string(), entity);
}
pub fn ui_named_entity(world: &World, name: &str) -> Option<freecs::Entity> {
world
.resources
.retained_ui
.accessibility
.named_entities
.get(name)
.copied()
}
pub fn ui_set_test_id(world: &mut World, entity: freecs::Entity, test_id: &str) {
if world.ui.get_ui_node_interaction(entity).is_none() {
world.ui.set_ui_node_interaction(
entity,
crate::ecs::ui::components::UiNodeInteraction::default(),
);
if let Some(node) = world.ui.get_ui_layout_node_mut(entity) {
node.pointer_events = true;
}
}
if let Some(interaction) = world.ui.get_ui_node_interaction_mut(entity) {
interaction.test_id = Some(test_id.to_string());
}
world
.resources
.retained_ui
.accessibility
.test_id_map
.insert(test_id.to_string(), entity);
}
pub fn ui_set_visible(world: &mut World, entity: freecs::Entity, visible: bool) {
if let Some(node) = world.ui.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(
world: &mut World,
entities: &[freecs::Entity],
active: freecs::Entity,
) {
for &entity in entities {
ui_set_visible(world, entity, entity == active);
}
}
pub fn ui_node_visible(world: &World, entity: freecs::Entity) -> bool {
world
.ui
.get_ui_layout_node(entity)
.is_some_and(|node| node.visible)
}
pub fn ui_node_effectively_visible(world: &World, entity: freecs::Entity) -> bool {
let Some(node) = world.ui.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))) =
world.core.get_parent(current)
{
let parent = *parent;
if let Some(parent_node) = world.ui.get_ui_layout_node(parent)
&& !parent_node.visible
{
return false;
}
current = parent;
}
true
}
pub fn ui_rect(world: &World, entity: freecs::Entity) -> Option<crate::ecs::ui::types::Rect> {
world
.ui
.get_ui_layout_node(entity)
.map(|node| node.computed_rect)
}
pub fn ui_size(world: &World, entity: freecs::Entity) -> Option<Vec2> {
world
.ui
.get_ui_layout_node(entity)
.map(|node| node.computed_rect.size())
}
pub fn ui_set_text(world: &mut World, entity: freecs::Entity, text: &str) {
let slot = ui_text_slot_for_entity(world, entity);
if let Some(text_slot) = slot {
world.resources.text.cache.set_text(text_slot, text);
}
}
pub fn ui_set_icon(
world: &mut World,
entity: freecs::Entity,
icon: crate::ecs::ui::icons::IconGlyph,
) {
let slot = ui_text_slot_for_entity(world, entity);
if let Some(text_slot) = slot {
let mut buf = [0u8; 4];
let icon_str: &str = icon.codepoint.encode_utf8(&mut buf);
world.resources.text.cache.set_text(text_slot, icon_str);
}
if let Some(content) = world.ui.get_ui_node_content_mut(entity)
&& let crate::ecs::ui::components::UiNodeContent::Text { font_kind, .. } = content
{
*font_kind = icon.font_kind;
}
}
pub fn ui_set_label_text(world: &mut World, entity: freecs::Entity, text: &str) {
ui_set_text(world, entity, text);
}
pub fn ui_label_text(world: &World, entity: freecs::Entity) -> Option<String> {
ui_text_slot_for_entity(world, entity)
.and_then(|slot| world.resources.text.cache.get_text(slot).map(String::from))
}
pub fn ui_text_slot_for_entity(world: &World, entity: freecs::Entity) -> Option<usize> {
if let Some(crate::ecs::ui::components::UiNodeContent::Text { text_slot, .. }) =
world.ui.get_ui_node_content(entity)
{
return Some(*text_slot);
}
if let Some(data) = world.ui.get_ui_button(entity) {
return Some(data.text_slot);
}
if let Some(data) = world.ui.get_ui_slider(entity) {
return Some(data.text_slot);
}
if let Some(data) = world.ui.get_ui_text_input(entity) {
return Some(data.text_slot);
}
if let Some(data) = world.ui.get_ui_selectable_label(entity) {
return Some(data.text_slot);
}
if let Some(data) = world.ui.get_ui_drag_value(entity) {
return Some(data.text_slot);
}
None
}
pub fn ui_set_tooltip_entity(
world: &mut World,
entity: freecs::Entity,
tooltip_entity: Option<freecs::Entity>,
) {
if let Some(interaction) = world.ui.get_ui_node_interaction_mut(entity) {
interaction.tooltip_entity = tooltip_entity;
}
}
pub fn ui_set_tooltip_text(world: &mut World, entity: freecs::Entity, text: Option<&str>) {
if let Some(interaction) = world.ui.get_ui_node_interaction_mut(entity) {
interaction.tooltip_text = text.map(String::from);
}
}
#[cfg(feature = "assets")]
pub fn ui_upload_image(
world: &mut World,
rgba: &[u8],
width: u32,
height: u32,
) -> Option<UiImageUpload> {
use crate::render::wgpu::ui_texture_array::UI_TEXTURE_LAYER_SIZE;
use nalgebra_glm::Vec2;
if width == 0 || height == 0 {
return None;
}
let target = UI_TEXTURE_LAYER_SIZE;
let scale_factor = (target as f32 / width as f32).min(target as f32 / height as f32);
let scaled_width = ((width as f32 * scale_factor).round() as u32).clamp(1, target);
let scaled_height = ((height as f32 * scale_factor).round() as u32).clamp(1, target);
let scaled_pixels = if scaled_width == width && scaled_height == height {
rgba.to_vec()
} else {
let source: image::RgbaImage = image::ImageBuffer::from_raw(width, height, rgba.to_vec())?;
image::imageops::resize(
&source,
scaled_width,
scaled_height,
image::imageops::FilterType::Triangle,
)
.into_raw()
};
let mut padded = vec![0u8; (target * target * 4) as usize];
let row_bytes = (scaled_width * 4) as usize;
let target_row_bytes = (target * 4) as usize;
for row in 0..scaled_height as usize {
let source_offset = row * row_bytes;
let target_offset = row * target_row_bytes;
padded[target_offset..target_offset + row_bytes]
.copy_from_slice(&scaled_pixels[source_offset..source_offset + row_bytes]);
}
let layer = world.resources.ui_image_allocator.allocate()?;
world
.resources
.commands
.render
.push(crate::ecs::world::RenderCommand::UploadUiImageLayer {
layer,
rgba_data: padded,
width: target,
height: target,
});
let uv_max = Vec2::new(
scaled_width as f32 / target as f32,
scaled_height as f32 / target as f32,
);
Some(UiImageUpload {
layer,
uv_min: Vec2::new(0.0, 0.0),
uv_max,
content_width: scaled_width,
content_height: scaled_height,
})
}
pub fn ui_release_image_layer(world: &mut World, layer: u32) {
world.resources.ui_image_allocator.release(layer);
}
#[cfg(feature = "assets")]
#[derive(Clone, Copy, Debug)]
pub struct UiImageUpload {
pub layer: u32,
pub uv_min: nalgebra_glm::Vec2,
pub uv_max: nalgebra_glm::Vec2,
pub content_width: u32,
pub content_height: u32,
}
#[cfg(not(target_arch = "wasm32"))]
fn sync_to_system_clipboard(text: &str) {
if let Ok(mut clipboard) = arboard::Clipboard::new() {
let _ = clipboard.set_text(text.to_string());
}
}
#[cfg(not(target_arch = "wasm32"))]
fn read_from_system_clipboard() -> Option<String> {
let mut clipboard = arboard::Clipboard::new().ok()?;
clipboard.get_text().ok()
}
#[cfg(target_arch = "wasm32")]
fn sync_to_system_clipboard(_text: &str) {}
#[cfg(target_arch = "wasm32")]
fn read_from_system_clipboard() -> Option<String> {
None
}
#[cfg(not(target_arch = "wasm32"))]
pub fn open_url_in_browser(url: &str) {
let _ = webbrowser::open(url);
}
#[cfg(target_arch = "wasm32")]
pub fn open_url_in_browser(_url: &str) {}