use nalgebra_glm::{Vec2, Vec4};
use winit::keyboard::KeyCode;
use crate::ecs::text::resources::FontAtlasData;
use crate::ecs::ui::components::{
DropPreview, DropZone, SplitDirection, TileId, TileNode, UiPanelKind, UiTileContainerData,
UiWidgetState,
};
use crate::ecs::ui::state::UiStateTrait as _;
use crate::ecs::world::World;
fn validate_widget_text(world: &mut World, entity: freecs::Entity, text: &str) {
let rules: Vec<crate::ecs::ui::components::ValidationRule> = world
.get_ui_node_interaction(entity)
.map(|interaction| interaction.validation_rules.clone())
.unwrap_or_default();
if rules.is_empty() {
return;
}
let mut error: Option<String> = None;
for rule in &rules {
if let Err(message) = rule.validate(text) {
error = Some(message);
break;
}
}
if let Some(interaction) = world.get_ui_node_interaction_mut(entity) {
interaction.error_text = error.clone();
interaction.tooltip_text = error;
}
}
pub fn ui_retained_input_sync_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
world.resources.retained_ui.frame_events.clear();
world.resources.retained_ui.delta_time = world.resources.window.timing.delta_time;
world.resources.retained_ui.current_time =
world.resources.window.timing.uptime_milliseconds as f64 / 1000.0;
let focused_secondary = world
.resources
.secondary_windows
.focused_index
.and_then(|idx| {
world
.resources
.secondary_windows
.states
.iter()
.find(|s| s.index == idx)
.map(|s| {
let keystates = &s.input.keyboard_keystates;
let ctrl = matches!(
keystates.get(&KeyCode::ControlLeft),
Some(winit::event::ElementState::Pressed)
) || matches!(
keystates.get(&KeyCode::ControlRight),
Some(winit::event::ElementState::Pressed)
);
let shift = matches!(
keystates.get(&KeyCode::ShiftLeft),
Some(winit::event::ElementState::Pressed)
) || matches!(
keystates.get(&KeyCode::ShiftRight),
Some(winit::event::ElementState::Pressed)
);
(
s.input.mouse_wheel_delta,
ctrl,
shift,
s.input.frame_keys.clone(),
s.input.frame_chars.clone(),
)
})
});
if let Some((scroll, ctrl, shift, frame_keys, frame_chars)) = focused_secondary {
world.resources.retained_ui.frame_chars = frame_chars;
world.resources.retained_ui.frame_keys = frame_keys;
world.resources.retained_ui.scroll_delta = scroll;
world.resources.retained_ui.ctrl_held = ctrl;
world.resources.retained_ui.shift_held = shift;
} else {
world.resources.retained_ui.frame_chars =
world.resources.input.keyboard.frame_chars.clone();
world.resources.retained_ui.frame_keys = world.resources.input.keyboard.frame_keys.clone();
world.resources.retained_ui.scroll_delta = world.resources.input.mouse.wheel_delta;
let ctrl_held = world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ControlLeft)
|| world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ControlRight);
let shift_held = world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftLeft)
|| world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::ShiftRight);
world.resources.retained_ui.ctrl_held = ctrl_held;
world.resources.retained_ui.shift_held = shift_held;
}
if world.resources.retained_ui.scroll_delta != Vec2::zeros()
|| !world.resources.retained_ui.frame_chars.is_empty()
{
world.resources.retained_ui.layout_dirty = true;
}
world.resources.retained_ui.cleanup_stale_animations();
}
pub fn measure_text_width(atlas: &FontAtlasData, text: &str, font_size: f32) -> f32 {
let scale = font_size / atlas.font_size;
let mut width = 0.0f32;
let mut prev_char: Option<char> = None;
for character in text.chars() {
if let Some(glyph) = atlas.glyphs.get(&character) {
if let Some(previous) = prev_char
&& let Some(&kern) = atlas.kerning.get(&(previous, character))
{
width += kern * scale;
}
width += glyph.advance * scale;
}
prev_char = Some(character);
}
width
}
pub fn byte_index_at_x(atlas: &FontAtlasData, text: &str, font_size: f32, target_x: f32) -> usize {
let scale = font_size / atlas.font_size;
let mut cursor_x = 0.0f32;
let mut prev_char: Option<char> = None;
for (index, character) in text.chars().enumerate() {
if let Some(glyph) = atlas.glyphs.get(&character) {
if let Some(previous) = prev_char
&& let Some(&kern) = atlas.kerning.get(&(previous, character))
{
cursor_x += kern * scale;
}
let advance = glyph.advance * scale;
if target_x < cursor_x + advance * 0.5 {
return index;
}
cursor_x += advance;
}
prev_char = Some(character);
}
text.chars().count()
}
pub fn prev_word_boundary(text: &str, position: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
if position == 0 {
return 0;
}
let mut index = position - 1;
while index > 0 && chars[index].is_whitespace() {
index -= 1;
}
while index > 0 && !chars[index - 1].is_whitespace() {
index -= 1;
}
index
}
pub fn next_word_boundary(text: &str, position: usize) -> usize {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
if position >= len {
return len;
}
let mut index = position;
while index < len && !chars[index].is_whitespace() {
index += 1;
}
while index < len && chars[index].is_whitespace() {
index += 1;
}
index
}
fn line_col_from_char_position(text: &str, position: usize) -> (usize, usize) {
let mut line = 0;
let mut col = 0;
for (index, character) in text.chars().enumerate() {
if index == position {
return (line, col);
}
if character == '\n' {
line += 1;
col = 0;
} else {
col += 1;
}
}
(line, col)
}
fn char_position_from_line_col(text: &str, target_line: usize, target_col: usize) -> usize {
let mut line = 0;
let mut col = 0;
for (index, character) in text.chars().enumerate() {
if line == target_line && col == target_col {
return index;
}
if line > target_line {
return index.saturating_sub(1);
}
if character == '\n' {
if line == target_line {
return index;
}
line += 1;
col = 0;
} else {
col += 1;
}
}
text.chars().count()
}
fn line_count(text: &str) -> usize {
text.chars().filter(|c| *c == '\n').count() + 1
}
fn line_start_char_index(text: &str, target_line: usize) -> usize {
let mut line = 0;
for (index, character) in text.chars().enumerate() {
if line == target_line {
return index;
}
if character == '\n' {
line += 1;
}
}
text.chars().count()
}
fn line_text(text: &str, target_line: usize) -> &str {
text.split('\n').nth(target_line).unwrap_or("")
}
struct InteractionSnapshot {
clicked: bool,
pressed: bool,
dragging: bool,
hovered: bool,
double_clicked: bool,
right_clicked: bool,
drag_start: Option<Vec2>,
}
fn snapshot_interaction(world: &World, entity: freecs::Entity) -> InteractionSnapshot {
world
.get_ui_node_interaction(entity)
.map(|interaction| InteractionSnapshot {
clicked: interaction.clicked,
pressed: interaction.pressed,
dragging: interaction.dragging,
hovered: interaction.hovered,
double_clicked: interaction.double_clicked,
right_clicked: interaction.right_clicked,
drag_start: interaction.drag_start,
})
.unwrap_or(InteractionSnapshot {
clicked: false,
pressed: false,
dragging: false,
hovered: false,
double_clicked: false,
right_clicked: false,
drag_start: None,
})
}
pub fn ui_widget_interaction_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
let delta_time = world.resources.retained_ui.delta_time;
let current_time = world.resources.retained_ui.current_time;
let mouse_position = world.resources.input.mouse.position;
let frame_chars = std::mem::take(&mut world.resources.retained_ui.frame_chars);
let frame_keys = std::mem::take(&mut world.resources.retained_ui.frame_keys);
let ctrl_held = world.resources.retained_ui.ctrl_held;
let shift_held = world.resources.retained_ui.shift_held;
let scroll_delta = world.resources.retained_ui.scroll_delta;
let focused_entity = world.resources.retained_ui.focused_entity;
let mouse_state = world.resources.input.mouse.state;
let mouse_just_pressed =
mouse_state.contains(crate::ecs::input::resources::MouseState::LEFT_JUST_PRESSED);
let mouse_just_released =
mouse_state.contains(crate::ecs::input::resources::MouseState::LEFT_JUST_RELEASED);
let mouse_down = mouse_state.contains(crate::ecs::input::resources::MouseState::LEFT_CLICKED);
let dpi_scale = world.resources.window.cached_scale_factor;
let viewport_size = world
.resources
.window
.cached_viewport_size
.map(|(width, height)| Vec2::new(width as f32, height as f32))
.unwrap_or(Vec2::new(800.0, 600.0));
let entities: Vec<freecs::Entity> = world
.query_entities(crate::ecs::world::UI_WIDGET_STATE)
.collect();
let has_text_focus = focused_entity.is_some_and(|fe| {
matches!(
world.get_ui_widget_state(fe),
Some(UiWidgetState::TextInput(_))
| Some(UiWidgetState::TextArea(_))
| Some(UiWidgetState::DragValue(_))
)
});
if !has_text_focus {
let bindings = world.resources.retained_ui.shortcut_bindings.clone();
let alt_held = world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::AltLeft)
|| world
.resources
.input
.keyboard
.is_key_pressed(KeyCode::AltRight);
for &(ref key, pressed) in &frame_keys {
if !pressed {
continue;
}
for (binding, command_index) in &bindings {
if binding.key == *key
&& binding.ctrl == ctrl_held
&& binding.shift == shift_held
&& binding.alt == alt_held
{
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::ShortcutTriggered {
command_index: *command_index,
},
);
}
}
}
}
let mut panel_rects: Vec<(freecs::Entity, crate::ecs::ui::types::Rect, f32)> = Vec::new();
for entity in &entities {
if let Some(UiWidgetState::Panel(_)) = world.get_ui_widget_state(*entity)
&& let Some(node) = world.get_ui_layout_node(*entity)
&& node.visible
{
panel_rects.push((*entity, node.computed_rect, node.computed_depth));
}
}
for entity in entities {
let interaction = snapshot_interaction(world, entity);
let widget_clone = match world.get_ui_widget_state(entity) {
Some(widget) => widget.clone(),
None => continue,
};
match widget_clone {
UiWidgetState::Button(_) => {
if let Some(UiWidgetState::Button(data)) = world.get_ui_widget_state_mut(entity) {
data.clicked = interaction.clicked;
}
if interaction.clicked {
world
.resources
.retained_ui
.frame_events
.push(crate::ecs::ui::resources::UiEvent::ButtonClicked(entity));
}
}
UiWidgetState::Slider(data) => {
handle_slider(
world,
entity,
&interaction,
&data,
mouse_position,
focused_entity,
&frame_keys,
);
}
UiWidgetState::Toggle(data) => {
handle_toggle(world, entity, &interaction, &data, delta_time);
}
UiWidgetState::Checkbox(data) => {
handle_checkbox(world, entity, &interaction, &data);
}
UiWidgetState::Radio(data) => {
handle_radio(world, entity, &interaction, &data);
}
UiWidgetState::ProgressBar(_) => {}
UiWidgetState::CollapsingHeader(data) => {
handle_collapsing_header(world, entity, &interaction, &data);
}
UiWidgetState::ScrollArea(data) => {
handle_scroll_area(
world,
entity,
&interaction,
&data,
scroll_delta,
mouse_position,
dpi_scale,
);
}
UiWidgetState::TabBar(data) => {
handle_tab_bar(world, entity, &data, &frame_keys, focused_entity);
}
UiWidgetState::TextInput(data) => {
handle_text_input(
world,
entity,
&interaction,
&data,
focused_entity,
&frame_chars,
&frame_keys,
ctrl_held,
shift_held,
mouse_position,
current_time,
dpi_scale,
);
}
UiWidgetState::Dropdown(data) => {
handle_dropdown(
world,
entity,
&interaction,
&data,
&frame_keys,
focused_entity,
);
}
UiWidgetState::Menu(data) => {
handle_menu(world, entity, &interaction, &data);
}
UiWidgetState::Panel(data) => {
let panel_depth = world
.get_ui_layout_node(entity)
.map(|n| n.computed_depth)
.unwrap_or(0.0);
let mouse_occluded =
panel_rects
.iter()
.any(|(other_entity, other_rect, other_depth)| {
*other_entity != entity
&& *other_depth > panel_depth
&& other_rect.contains(mouse_position)
});
handle_panel(
world,
entity,
&data,
&PanelContext {
mouse_position,
mouse_just_pressed,
mouse_just_released,
mouse_down,
dpi_scale,
viewport_size,
mouse_occluded,
},
);
}
UiWidgetState::ColorPicker(data) => {
handle_color_picker(world, entity, &data);
}
UiWidgetState::SelectableLabel(data) => {
handle_selectable_label(world, entity, &interaction, &data);
}
UiWidgetState::DragValue(data) => {
handle_drag_value(
world,
entity,
&interaction,
&data,
focused_entity,
&frame_chars,
&frame_keys,
ctrl_held,
shift_held,
mouse_position,
current_time,
dpi_scale,
);
}
UiWidgetState::ContextMenu(data) => {
handle_context_menu(world, entity, &data);
}
UiWidgetState::TreeView(data) => {
handle_tree_view(
world,
entity,
&data,
ctrl_held,
shift_held,
&frame_keys,
focused_entity,
);
}
UiWidgetState::TreeNode(_) => {}
UiWidgetState::ModalDialog(data) => {
handle_modal_dialog(world, entity, &data, &frame_keys);
}
UiWidgetState::RichText(_) => {}
UiWidgetState::DataGrid(data) => {
handle_data_grid(
world,
entity,
&data,
&DataGridContext {
ctrl_held,
shift_held,
frame_keys: &frame_keys,
mouse_position,
mouse_just_pressed,
mouse_down,
},
);
}
UiWidgetState::PropertyGrid(data) => {
handle_property_grid(
world,
entity,
&data,
mouse_position,
mouse_just_pressed,
mouse_down,
);
}
UiWidgetState::CommandPalette(data) => {
handle_command_palette(world, entity, &data, &frame_keys);
}
UiWidgetState::Canvas(data) => {
if interaction.clicked && data.hit_test_enabled {
let mouse_pos = world.resources.input.mouse.position;
if let Some(node) = world.get_ui_layout_node(entity) {
let local_pos = mouse_pos - node.computed_rect.min;
for &(command_id, ref rect) in data.command_bounds.iter().rev() {
if rect.contains(local_pos) {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::CanvasClicked {
entity,
command_id,
position: local_pos,
},
);
break;
}
}
}
}
}
UiWidgetState::TextArea(data) => {
handle_text_area(
world,
entity,
&interaction,
&data,
focused_entity,
&frame_chars,
&frame_keys,
ctrl_held,
shift_held,
mouse_position,
current_time,
dpi_scale,
);
}
UiWidgetState::TileContainer(data) => {
handle_tile_container(
world,
entity,
&data,
&TileContainerContext {
mouse_position,
mouse_just_pressed,
mouse_just_released,
mouse_down,
},
);
}
UiWidgetState::VirtualList(data) => {
handle_virtual_list(world, entity, &data, dpi_scale, &frame_keys, focused_entity);
}
UiWidgetState::RangeSlider(data) => {
handle_range_slider(
world,
entity,
&interaction,
&data,
mouse_position,
focused_entity,
&frame_keys,
);
}
UiWidgetState::Breadcrumb(data) => {
handle_breadcrumb(world, entity, &data);
}
UiWidgetState::Splitter(data) => {
handle_splitter(world, entity, &data, mouse_position);
}
UiWidgetState::RichTextEditor(data) => {
handle_rich_text_editor(
world,
RichTextEditorContext {
entity,
interaction: &interaction,
data: &data,
focused_entity,
frame_chars: &frame_chars,
frame_keys: &frame_keys,
ctrl_held,
shift_held,
mouse_position,
current_time,
dpi_scale,
},
);
}
UiWidgetState::MultiSelect(data) => {
handle_multi_select(world, entity, &interaction, &data);
}
UiWidgetState::DatePicker(data) => {
handle_date_picker(world, entity, &interaction, &data);
}
}
}
if scroll_delta.y.abs() > f32::EPSILON && !is_mouse_over_popup(world, mouse_position) {
close_open_popups(world);
}
let error_color = world
.resources
.retained_ui
.theme_state
.active_theme()
.error_color;
let error_entities: Vec<(freecs::Entity, bool)> = world
.query_entities(crate::ecs::world::UI_NODE_INTERACTION)
.map(|entity| {
let has_error = world
.get_ui_node_interaction(entity)
.is_some_and(|interaction| interaction.error_text.is_some());
(entity, has_error)
})
.filter(|(entity, _)| {
world.get_ui_node_content(*entity).is_some_and(|c| {
matches!(c, crate::ecs::ui::components::UiNodeContent::Rect { .. })
})
})
.collect();
for (entity, has_error) in error_entities {
if has_error
&& let Some(crate::ecs::ui::components::UiNodeContent::Rect {
border_color,
border_width,
..
}) = world.get_ui_node_content_mut(entity)
{
*border_color = error_color;
if *border_width < 1.0 {
*border_width = 1.0;
}
}
}
world.resources.retained_ui.frame_chars = frame_chars;
world.resources.retained_ui.frame_keys = frame_keys;
}
fn is_mouse_over_popup(world: &World, mouse_position: Vec2) -> bool {
for entity in world.query_entities(crate::ecs::world::UI_WIDGET_STATE) {
let popup_container = match world.get_ui_widget_state(entity) {
Some(UiWidgetState::Dropdown(data)) if data.open => Some(data.popup_container_entity),
Some(UiWidgetState::Menu(data)) if data.open => Some(data.popup_container_entity),
Some(UiWidgetState::ContextMenu(data)) if data.open => Some(data.popup_entity),
_ => None,
};
if let Some(popup) = popup_container
&& let Some(node) = world.get_ui_layout_node(popup)
&& node.computed_rect.contains(mouse_position)
{
return true;
}
}
false
}
fn close_open_popups(world: &mut World) {
let entities: Vec<freecs::Entity> = world
.query_entities(crate::ecs::world::UI_WIDGET_STATE)
.collect();
for entity in entities {
let widget = match world.get_ui_widget_state(entity) {
Some(widget) => widget.clone(),
None => continue,
};
match widget {
UiWidgetState::Dropdown(data) if data.open => {
if let Some(node) = world.get_ui_layout_node_mut(data.popup_container_entity) {
node.visible = false;
}
if let Some(UiWidgetState::Dropdown(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.open = false;
}
}
UiWidgetState::Menu(data) if data.open => {
if let Some(node) = world.get_ui_layout_node_mut(data.popup_container_entity) {
node.visible = false;
}
if let Some(UiWidgetState::Menu(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.open = false;
}
}
UiWidgetState::ContextMenu(data) if data.open => {
if let Some(node) = world.get_ui_layout_node_mut(data.popup_entity) {
node.visible = false;
}
close_submenu_popups(world, &data.item_defs);
if let Some(UiWidgetState::ContextMenu(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.open = false;
close_submenu_defs_state(&mut widget_data.item_defs);
}
world.resources.retained_ui.active_context_menu = None;
}
_ => {}
}
}
}
fn handle_slider(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiSliderData,
mouse_position: Vec2,
focused_entity: Option<freecs::Entity>,
frame_keys: &[(KeyCode, bool)],
) {
let mut new_value = data.value;
let mut changed = false;
if focused_entity == Some(entity) {
let range = data.max - data.min;
let step = if data.logarithmic {
range * 0.01
} else {
let natural_step = 10.0_f32.powf((range.log10() - 1.0).floor());
natural_step.max(0.01)
};
for (key, is_pressed) in frame_keys {
if !is_pressed {
continue;
}
match key {
KeyCode::ArrowRight | KeyCode::ArrowUp => {
new_value = (new_value + step).min(data.max);
changed = true;
}
KeyCode::ArrowLeft | KeyCode::ArrowDown => {
new_value = (new_value - step).max(data.min);
changed = true;
}
KeyCode::Home => {
new_value = data.min;
changed = true;
}
KeyCode::End => {
new_value = data.max;
changed = true;
}
_ => {}
}
}
}
if interaction.pressed || interaction.dragging {
let track_rect = world
.get_ui_layout_node(entity)
.map(|node| node.computed_rect);
if let Some(rect) = track_rect {
let normalized = ((mouse_position.x - rect.min.x) / rect.width()).clamp(0.0, 1.0);
new_value = if data.logarithmic && data.min > 0.0 && data.max > data.min {
data.min * (data.max / data.min).powf(normalized)
} else {
data.min + normalized * (data.max - data.min)
};
if (new_value - data.value).abs() > f32::EPSILON {
changed = true;
}
let fill_percent = normalized * 100.0;
if let Some(fill_node) = world.get_ui_layout_node_mut(data.fill_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
fill_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
boundary.position_2 =
crate::ecs::ui::units::Rl(Vec2::new(fill_percent, 100.0)).into();
}
world.resources.text_cache.set_text(
data.text_slot,
format!(
"{}{:.prec$}{}",
data.prefix,
new_value,
data.suffix,
prec = data.precision
),
);
}
}
if changed && !(interaction.pressed || interaction.dragging) {
let range = data.max - data.min;
let normalized = if data.logarithmic && data.min > 0.0 && range > 0.0 {
((new_value / data.min).ln() / (data.max / data.min).ln()).clamp(0.0, 1.0)
} else if range.abs() > f32::EPSILON {
((new_value - data.min) / range).clamp(0.0, 1.0)
} else {
0.0
};
let fill_percent = normalized * 100.0;
if let Some(fill_node) = world.get_ui_layout_node_mut(data.fill_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
fill_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
boundary.position_2 = crate::ecs::ui::units::Rl(Vec2::new(fill_percent, 100.0)).into();
}
world.resources.text_cache.set_text(
data.text_slot,
format!(
"{}{:.prec$}{}",
data.prefix,
new_value,
data.suffix,
prec = data.precision
),
);
}
if let Some(UiWidgetState::Slider(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.value = new_value;
widget_data.changed = changed;
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::SliderChanged {
entity,
value: new_value,
},
);
}
}
fn handle_splitter(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiSplitterData,
mouse_position: Vec2,
) {
use crate::ecs::ui::components::SplitDirection;
let divider_dragging = world
.get_ui_node_interaction(data.divider_entity)
.map(|interaction| interaction.dragging)
.unwrap_or(false);
if !divider_dragging {
if let Some(crate::ecs::ui::components::UiWidgetState::Splitter(splitter_data)) =
world.get_ui_widget_state_mut(entity)
{
splitter_data.changed = false;
}
return;
}
let container_rect = world
.get_ui_layout_node(entity)
.map(|node| node.computed_rect);
let Some(container_rect) = container_rect else {
return;
};
let new_ratio = match data.direction {
SplitDirection::Horizontal => {
let width = container_rect.width();
if width > 0.0 {
((mouse_position.x - container_rect.min.x) / width)
.clamp(data.min_ratio, data.max_ratio)
} else {
data.ratio
}
}
SplitDirection::Vertical => {
let height = container_rect.height();
if height > 0.0 {
((mouse_position.y - container_rect.min.y) / height)
.clamp(data.min_ratio, data.max_ratio)
} else {
data.ratio
}
}
};
if (new_ratio - data.ratio).abs() < 0.001 {
return;
}
let divider_thickness = 4.0;
let (p1_1, p1_2, d1, d2, p2_1, p2_2) = match data.direction {
SplitDirection::Horizontal => {
let half = divider_thickness * 0.5;
(
crate::ecs::ui::units::Rl(Vec2::new(0.0, 0.0)),
crate::ecs::ui::units::Rl(Vec2::new(new_ratio * 100.0, 100.0))
+ crate::ecs::ui::units::Ab(Vec2::new(-half, 0.0)),
crate::ecs::ui::units::Rl(Vec2::new(new_ratio * 100.0, 0.0))
+ crate::ecs::ui::units::Ab(Vec2::new(-half, 0.0)),
crate::ecs::ui::units::Rl(Vec2::new(new_ratio * 100.0, 100.0))
+ crate::ecs::ui::units::Ab(Vec2::new(half, 0.0)),
crate::ecs::ui::units::Rl(Vec2::new(new_ratio * 100.0, 0.0))
+ crate::ecs::ui::units::Ab(Vec2::new(half, 0.0)),
crate::ecs::ui::units::Rl(Vec2::new(100.0, 100.0)),
)
}
SplitDirection::Vertical => {
let half = divider_thickness * 0.5;
(
crate::ecs::ui::units::Rl(Vec2::new(0.0, 0.0)),
crate::ecs::ui::units::Rl(Vec2::new(100.0, new_ratio * 100.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, -half)),
crate::ecs::ui::units::Rl(Vec2::new(0.0, new_ratio * 100.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, -half)),
crate::ecs::ui::units::Rl(Vec2::new(100.0, new_ratio * 100.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, half)),
crate::ecs::ui::units::Rl(Vec2::new(0.0, new_ratio * 100.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, half)),
crate::ecs::ui::units::Rl(Vec2::new(100.0, 100.0)),
)
}
};
let first_pane = data.first_pane;
let second_pane = data.second_pane;
let divider_entity = data.divider_entity;
if let Some(node) = world.get_ui_layout_node_mut(first_pane)
&& let Some(Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary))) =
node.layouts.get_mut(0)
{
boundary.position_1 = p1_1.into();
boundary.position_2 = p1_2;
}
if let Some(node) = world.get_ui_layout_node_mut(divider_entity)
&& let Some(Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary))) =
node.layouts.get_mut(0)
{
boundary.position_1 = d1;
boundary.position_2 = d2;
}
if let Some(node) = world.get_ui_layout_node_mut(second_pane)
&& let Some(Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary))) =
node.layouts.get_mut(0)
{
boundary.position_1 = p2_1;
boundary.position_2 = p2_2.into();
}
if let Some(crate::ecs::ui::components::UiWidgetState::Splitter(splitter_data)) =
world.get_ui_widget_state_mut(entity)
{
splitter_data.ratio = new_ratio;
splitter_data.changed = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::SplitterChanged {
entity,
ratio: new_ratio,
},
);
}
fn handle_breadcrumb(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiBreadcrumbData,
) {
let mut clicked_segment = None;
for (index, segment_entity) in data.segment_entities.iter().enumerate() {
let clicked = world
.get_ui_node_interaction(*segment_entity)
.map(|interaction| interaction.clicked)
.unwrap_or(false);
if clicked {
clicked_segment = Some(index);
break;
}
}
if let Some(index) = clicked_segment {
if let Some(UiWidgetState::Breadcrumb(breadcrumb_data)) =
world.get_ui_widget_state_mut(entity)
{
breadcrumb_data.clicked_segment = Some(index);
breadcrumb_data.changed = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::BreadcrumbClicked {
entity,
segment_index: index,
},
);
} else if let Some(UiWidgetState::Breadcrumb(breadcrumb_data)) =
world.get_ui_widget_state_mut(entity)
{
breadcrumb_data.clicked_segment = None;
breadcrumb_data.changed = false;
}
}
fn handle_range_slider(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiRangeSliderData,
mouse_position: Vec2,
focused_entity: Option<freecs::Entity>,
frame_keys: &[(KeyCode, bool)],
) {
let mut low_value = data.low_value;
let mut high_value = data.high_value;
let mut changed = false;
let mut active_thumb = data.active_thumb;
let range = data.max - data.min;
if range.abs() < f32::EPSILON {
if let Some(UiWidgetState::RangeSlider(widget_data)) = world.get_ui_widget_state_mut(entity)
{
widget_data.changed = false;
}
return;
}
if interaction.pressed {
let track_rect = world
.get_ui_layout_node(entity)
.map(|node| node.computed_rect);
if let Some(rect) = track_rect {
let normalized = ((mouse_position.x - rect.min.x) / rect.width()).clamp(0.0, 1.0);
let mouse_value = data.min + normalized * range;
let dist_to_low = (mouse_value - low_value).abs();
let dist_to_high = (mouse_value - high_value).abs();
active_thumb = Some(if dist_to_low <= dist_to_high {
crate::ecs::ui::components::RangeSliderThumb::Low
} else {
crate::ecs::ui::components::RangeSliderThumb::High
});
}
}
if (interaction.pressed || interaction.dragging)
&& let Some(thumb) = active_thumb
{
let track_rect = world
.get_ui_layout_node(entity)
.map(|node| node.computed_rect);
if let Some(rect) = track_rect {
let normalized = ((mouse_position.x - rect.min.x) / rect.width()).clamp(0.0, 1.0);
let new_value = data.min + normalized * range;
match thumb {
crate::ecs::ui::components::RangeSliderThumb::Low => {
low_value = new_value.min(high_value);
}
crate::ecs::ui::components::RangeSliderThumb::High => {
high_value = new_value.max(low_value);
}
}
if (low_value - data.low_value).abs() > f32::EPSILON
|| (high_value - data.high_value).abs() > f32::EPSILON
{
changed = true;
}
let low_n = ((low_value - data.min) / range).clamp(0.0, 1.0);
let high_n = ((high_value - data.min) / range).clamp(0.0, 1.0);
crate::ecs::ui::widgets::update_range_slider_visuals(
world,
&crate::ecs::ui::widgets::RangeSliderVisualUpdate {
fill_entity: data.fill_entity,
low_thumb: data.low_thumb_entity,
high_thumb: data.high_thumb_entity,
text_slot: data.text_slot,
precision: data.precision,
low_normalized: low_n,
high_normalized: high_n,
low_value,
high_value,
thumb_half: data.thumb_half_size,
},
);
}
}
if !interaction.pressed && !interaction.dragging {
active_thumb = None;
}
if focused_entity == Some(entity) {
let step = range * 0.01;
for &(key, pressed) in frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::Tab => {
active_thumb = match active_thumb {
Some(crate::ecs::ui::components::RangeSliderThumb::Low) => {
Some(crate::ecs::ui::components::RangeSliderThumb::High)
}
_ => Some(crate::ecs::ui::components::RangeSliderThumb::Low),
};
}
KeyCode::ArrowLeft | KeyCode::ArrowDown => {
let thumb =
active_thumb.unwrap_or(crate::ecs::ui::components::RangeSliderThumb::Low);
match thumb {
crate::ecs::ui::components::RangeSliderThumb::Low => {
low_value = (low_value - step).max(data.min).min(high_value);
}
crate::ecs::ui::components::RangeSliderThumb::High => {
high_value = (high_value - step).max(low_value).max(data.min);
}
}
if active_thumb.is_none() {
active_thumb = Some(thumb);
}
changed = true;
}
KeyCode::ArrowRight | KeyCode::ArrowUp => {
let thumb =
active_thumb.unwrap_or(crate::ecs::ui::components::RangeSliderThumb::High);
match thumb {
crate::ecs::ui::components::RangeSliderThumb::Low => {
low_value = (low_value + step).min(high_value).min(data.max);
}
crate::ecs::ui::components::RangeSliderThumb::High => {
high_value = (high_value + step).min(data.max);
}
}
if active_thumb.is_none() {
active_thumb = Some(thumb);
}
changed = true;
}
_ => {}
}
}
if changed && !interaction.pressed && !interaction.dragging {
let low_n = ((low_value - data.min) / range).clamp(0.0, 1.0);
let high_n = ((high_value - data.min) / range).clamp(0.0, 1.0);
crate::ecs::ui::widgets::update_range_slider_visuals(
world,
&crate::ecs::ui::widgets::RangeSliderVisualUpdate {
fill_entity: data.fill_entity,
low_thumb: data.low_thumb_entity,
high_thumb: data.high_thumb_entity,
text_slot: data.text_slot,
precision: data.precision,
low_normalized: low_n,
high_normalized: high_n,
low_value,
high_value,
thumb_half: data.thumb_half_size,
},
);
}
}
if let Some(UiWidgetState::RangeSlider(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.low_value = low_value;
widget_data.high_value = high_value;
widget_data.changed = changed;
widget_data.active_thumb = active_thumb;
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::RangeSliderChanged {
entity,
low: low_value,
high: high_value,
},
);
}
}
fn handle_toggle(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiToggleData,
delta_time: f32,
) {
let mut value = data.value;
let mut changed = false;
let mut animated_position = data.animated_position;
if interaction.clicked {
value = !value;
changed = true;
}
let target = if value { 1.0 } else { 0.0 };
let speed = 8.0;
let diff = target - animated_position;
if diff.abs() > 0.001 {
animated_position += diff.signum() * speed * delta_time;
animated_position = animated_position.clamp(0.0, 1.0);
} else {
animated_position = target;
}
let theme = world.resources.retained_ui.theme_state.active_theme();
let toggle_height = theme.toggle_height;
let toggle_width = theme.toggle_width;
let on_color = theme.accent_color;
let off_color = theme.background_color;
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 knob_x = knob_padding + animated_position * knob_travel;
if let Some(knob_node) = world.get_ui_layout_node_mut(data.knob_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
knob_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position = crate::ecs::ui::units::Ab(Vec2::new(knob_x, knob_padding)).into();
}
let blended = off_color + (on_color - off_color) * animated_position;
if let Some(color) = world.get_ui_node_color_mut(entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(blended);
}
if let Some(UiWidgetState::Toggle(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.value = value;
widget_data.changed = changed;
widget_data.animated_position = animated_position;
}
if changed {
world
.resources
.retained_ui
.frame_events
.push(crate::ecs::ui::resources::UiEvent::ToggleChanged { entity, value });
}
}
fn handle_checkbox(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiCheckboxData,
) {
if interaction.clicked {
let new_value = !data.value;
if let Some(node) = world.get_ui_layout_node_mut(data.inner_entity) {
node.visible = new_value;
}
if let Some(UiWidgetState::Checkbox(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.value = new_value;
widget_data.changed = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::CheckboxChanged {
entity,
value: new_value,
},
);
} else if let Some(UiWidgetState::Checkbox(widget_data)) = world.get_ui_widget_state_mut(entity)
{
widget_data.changed = false;
}
}
fn handle_radio(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiRadioData,
) {
if let Some(UiWidgetState::Radio(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.changed = false;
}
if interaction.clicked && !data.selected {
if let Some(node) = world.get_ui_layout_node_mut(data.inner_entity) {
node.visible = true;
}
let siblings = world
.resources
.retained_ui
.radio_groups
.get(&data.group_id)
.cloned()
.unwrap_or_default();
for other_entity in siblings {
if other_entity == entity {
continue;
}
let other_data_clone = world.get_ui_widget_state(other_entity).cloned();
if let Some(UiWidgetState::Radio(other_radio)) = other_data_clone
&& other_radio.selected
{
if let Some(node) = world.get_ui_layout_node_mut(other_radio.inner_entity) {
node.visible = false;
}
if let Some(UiWidgetState::Radio(wd)) = world.get_ui_widget_state_mut(other_entity)
{
wd.selected = false;
}
}
}
if let Some(UiWidgetState::Radio(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.selected = true;
widget_data.changed = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::RadioChanged {
entity,
group_id: data.group_id,
option_index: data.option_index,
},
);
}
}
fn handle_collapsing_header(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiCollapsingHeaderData,
) {
if let Some(UiWidgetState::CollapsingHeader(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.changed = false;
}
if interaction.clicked {
let new_open = !data.open;
if let Some(node) = world.get_ui_layout_node_mut(data.content_entity) {
node.visible = new_open;
}
world.resources.text_cache.set_text(
data.arrow_text_slot,
if new_open { "\u{25BC}" } else { "\u{25B6}" },
);
if let Some(UiWidgetState::CollapsingHeader(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.open = new_open;
widget_data.changed = true;
}
}
}
fn handle_scroll_area(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiScrollAreaData,
scroll_delta: Vec2,
mouse_position: Vec2,
dpi_scale: f32,
) {
let visible_height = world
.get_ui_layout_node(entity)
.map(|node| node.computed_rect.height())
.unwrap_or(0.0);
let children: Vec<freecs::Entity> = world
.resources
.children_cache
.get(&data.content_entity)
.map(|v| v.to_vec())
.unwrap_or_default();
let content_flow = world
.get_ui_layout_node(data.content_entity)
.and_then(|node| node.flow_layout);
let mut total_content_height = 0.0f32;
let mut visible_count = 0usize;
for child in &children {
if let Some(node) = world.get_ui_layout_node(*child)
&& node.visible
{
total_content_height += node.computed_rect.height();
visible_count += 1;
}
}
if let Some(flow) = content_flow {
if visible_count > 1 {
total_content_height += flow.spacing * dpi_scale * (visible_count - 1) as f32;
}
total_content_height += flow.padding * dpi_scale * 2.0;
}
let max_scroll = (total_content_height - visible_height).max(0.0) / dpi_scale;
let mut scroll_offset = data.scroll_offset;
let mut thumb_dragging = data.thumb_dragging;
let mut thumb_drag_start_offset = data.thumb_drag_start_offset;
if interaction.hovered && scroll_delta.y.abs() > 0.0 {
scroll_offset -= scroll_delta.y * 40.0;
scroll_offset = scroll_offset.clamp(0.0, max_scroll);
}
let thumb_interaction = snapshot_interaction(world, data.thumb_entity);
if thumb_interaction.pressed && !thumb_dragging {
thumb_dragging = true;
thumb_drag_start_offset = scroll_offset;
}
if thumb_dragging
&& thumb_interaction.pressed
&& let Some(drag_start) = thumb_interaction.drag_start
{
let track_rect = world
.get_ui_layout_node(data.track_entity)
.map(|n| n.computed_rect);
if let Some(track) = track_rect {
let track_height = track.height();
let thumb_ratio = if total_content_height > 0.0 {
visible_height / total_content_height
} else {
1.0
};
let scrollable_track = track_height * (1.0 - thumb_ratio);
if scrollable_track > 0.0 {
let mouse_delta = mouse_position.y - drag_start.y;
let scroll_per_pixel = max_scroll / scrollable_track;
scroll_offset = (thumb_drag_start_offset + mouse_delta * scroll_per_pixel)
.clamp(0.0, max_scroll);
}
}
}
if !thumb_interaction.pressed {
thumb_dragging = false;
}
if let Some(snap) = data.snap_interval
&& snap > 0.0
&& !thumb_dragging
&& scroll_delta.y.abs() < f32::EPSILON
{
let target = (scroll_offset / snap).round() * snap;
let target = target.clamp(0.0, max_scroll);
let delta_time = world.resources.retained_ui.delta_time;
let diff = target - scroll_offset;
if diff.abs() > 0.1 {
scroll_offset += diff * (10.0 * delta_time).min(1.0);
} else {
scroll_offset = target;
}
}
if let Some(content_node) = world.get_ui_layout_node_mut(data.content_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Boundary(boundary)) =
content_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
boundary.position_1 = crate::ecs::ui::units::Ab(Vec2::new(0.0, -scroll_offset)).into();
}
let show_scrollbar = total_content_height > visible_height;
if let Some(track_node) = world.get_ui_layout_node_mut(data.track_entity) {
track_node.visible = show_scrollbar;
}
if show_scrollbar {
let thumb_ratio = (visible_height / total_content_height).clamp(0.05, 1.0);
let track_rect = world
.get_ui_layout_node(data.track_entity)
.map(|n| n.computed_rect);
if let Some(track) = track_rect {
let track_height = track.height();
let thumb_height_physical = (track_height * thumb_ratio).max(20.0 * dpi_scale);
let scrollable_track = track_height - thumb_height_physical;
let scroll_ratio = if max_scroll > 0.0 {
scroll_offset / max_scroll
} else {
0.0
};
let thumb_y = scroll_ratio * scrollable_track / dpi_scale;
let thumb_height = thumb_height_physical / dpi_scale;
if let Some(thumb_node) = world.get_ui_layout_node_mut(data.thumb_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
thumb_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position = crate::ecs::ui::units::Ab(Vec2::new(0.0, thumb_y)).into();
window.size = crate::ecs::ui::units::Ab(Vec2::new(8.0, thumb_height)).into();
}
}
}
if let Some(UiWidgetState::ScrollArea(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.scroll_offset = scroll_offset;
widget_data.content_height = total_content_height;
widget_data.visible_height = visible_height;
widget_data.thumb_dragging = thumb_dragging;
widget_data.thumb_drag_start_offset = thumb_drag_start_offset;
}
}
fn handle_virtual_list(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiVirtualListData,
dpi_scale: f32,
frame_keys: &[(KeyCode, bool)],
focused_entity: Option<freecs::Entity>,
) {
let scroll_offset = if let Some(UiWidgetState::ScrollArea(scroll_data)) =
world.get_ui_widget_state(data.scroll_entity)
{
scroll_data.scroll_offset
} else {
0.0
};
let visible_start = if data.item_height > 0.0 {
(scroll_offset / data.item_height).floor() as usize
} else {
0
};
let visible_start = visible_start.min(data.total_items.saturating_sub(data.pool_size));
let visible_end = data.total_items.min(visible_start + data.pool_size);
let top_height = visible_start as f32 * data.item_height;
let bottom_height = data.total_items.saturating_sub(visible_end) as f32 * data.item_height;
if let Some(top_node) = world.get_ui_layout_node_mut(data.top_spacer) {
top_node.flow_child_size = Some(
crate::ecs::ui::units::Rl(Vec2::new(100.0, 0.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, top_height)),
);
}
if let Some(bottom_node) = world.get_ui_layout_node_mut(data.bottom_spacer) {
bottom_node.flow_child_size = Some(
crate::ecs::ui::units::Rl(Vec2::new(100.0, 0.0))
+ crate::ecs::ui::units::Ab(Vec2::new(0.0, bottom_height)),
);
}
let mut selection = data.selection;
let mut selection_changed = false;
for (pool_index, item) in data.pool_items.iter().enumerate() {
let item_index = visible_start + pool_index;
let is_visible = item_index < data.total_items;
if let Some(node) = world.get_ui_layout_node_mut(item.container_entity) {
node.visible = is_visible;
}
if is_visible {
let interaction = snapshot_interaction(world, item.container_entity);
if interaction.clicked {
selection = Some(item_index);
selection_changed = true;
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::VirtualListItemClicked {
entity,
item_index,
},
);
}
let accent_color = world
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
let is_selected = selection == Some(item_index);
if let Some(crate::ecs::ui::components::UiNodeContent::Rect {
border_width,
border_color,
..
}) = world.get_ui_node_content_mut(item.container_entity)
{
if is_selected {
*border_width = 1.0 / dpi_scale;
*border_color = accent_color;
} else {
*border_width = 0.0;
}
}
}
}
let is_focused = focused_entity == Some(entity)
|| data
.pool_items
.iter()
.any(|item| focused_entity == Some(item.container_entity));
if is_focused && data.total_items > 0 {
for &(key, pressed) in frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::ArrowUp => {
let current = selection.unwrap_or(0);
if current > 0 {
selection = Some(current - 1);
selection_changed = true;
}
}
KeyCode::ArrowDown => {
let current = selection.map_or(0, |s| s + 1);
if current < data.total_items {
selection = Some(current);
selection_changed = true;
}
}
KeyCode::Home => {
selection = Some(0);
selection_changed = true;
}
KeyCode::End => {
selection = Some(data.total_items - 1);
selection_changed = true;
}
_ => {}
}
}
}
if let Some(UiWidgetState::VirtualList(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.visible_start = visible_start;
widget_data.selection = selection;
widget_data.selection_changed = selection_changed;
}
}
fn handle_tab_bar(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiTabBarData,
frame_keys: &[(KeyCode, bool)],
focused_entity: Option<freecs::Entity>,
) {
let mut new_selected = data.selected_tab;
let mut changed = false;
for (index, tab_entity) in data.tab_entities.iter().enumerate() {
let tab_clicked = world
.get_ui_node_interaction(*tab_entity)
.map(|i| i.clicked)
.unwrap_or(false);
if tab_clicked && data.selected_tab != index {
new_selected = index;
changed = true;
break;
}
}
let any_tab_focused = data
.tab_entities
.iter()
.any(|tab| focused_entity == Some(*tab));
if any_tab_focused {
let tab_count = data.tab_entities.len();
for &(key, pressed) in frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::ArrowLeft => {
if new_selected > 0 {
new_selected -= 1;
changed = true;
}
}
KeyCode::ArrowRight => {
if new_selected + 1 < tab_count {
new_selected += 1;
changed = true;
}
}
_ => {}
}
}
}
if changed {
let theme = world.resources.retained_ui.theme_state.active_theme();
let active_bg = theme.accent_color;
let inactive_bg = theme.background_color;
let old_entity = data.tab_entities[data.selected_tab];
let new_entity = data.tab_entities[new_selected];
if let Some(color) = world.get_ui_node_color_mut(old_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(inactive_bg);
}
if let Some(color) = world.get_ui_node_color_mut(new_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(active_bg);
}
}
if let Some(UiWidgetState::TabBar(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.selected_tab = new_selected;
widget_data.changed = changed;
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TabChanged {
entity,
tab_index: new_selected,
},
);
}
}
#[allow(clippy::too_many_arguments)]
fn handle_text_input(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiTextInputData,
focused_entity: Option<freecs::Entity>,
frame_chars: &[char],
frame_keys: &[(KeyCode, bool)],
ctrl_held: bool,
shift_held: bool,
mouse_position: Vec2,
current_time: f64,
dpi_scale: f32,
) {
let is_focused = focused_entity == Some(entity);
let mut text = data.text.clone();
let mut cursor_position = data.cursor_position;
let mut selection_start = data.selection_start;
let mut changed = false;
let mut cursor_blink_timer = data.cursor_blink_timer;
let mut scroll_offset = data.scroll_offset;
let mut clear_focus = false;
let mut undo_stack = data.undo_stack.clone();
let mut needs_snapshot = false;
let input_mask = &data.input_mask;
let max_length = data.max_length;
if is_focused {
for character in frame_chars {
if *character >= ' ' && input_mask.accepts(*character) {
if let Some(max) = max_length {
let result_len = if let Some(sel) = selection_start {
let selected = sel.max(cursor_position) - sel.min(cursor_position);
text.chars().count() - selected + 1
} else {
text.chars().count() + 1
};
if result_len > max {
continue;
}
}
if !needs_snapshot {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
needs_snapshot = true;
}
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.push(*character);
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min + 1;
selection_start = None;
} else {
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.push(*character);
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position += 1;
}
changed = true;
cursor_blink_timer = current_time;
}
}
if needs_snapshot {
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
for (key, is_pressed) in frame_keys {
if !is_pressed {
continue;
}
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
match key {
KeyCode::Backspace => {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
let mut did_change = false;
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min;
selection_start = None;
did_change = true;
} else if cursor_position > 0 {
if ctrl_held {
let new_pos = prev_word_boundary(&text, cursor_position);
let mut new_text: String = chars[..new_pos].iter().collect();
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position = new_pos;
} else {
let mut new_text: String =
chars[..cursor_position - 1].iter().collect();
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position -= 1;
}
did_change = true;
}
if did_change {
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
changed = true;
}
cursor_blink_timer = current_time;
}
KeyCode::Delete => {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
let mut did_change = false;
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min;
selection_start = None;
did_change = true;
} else if cursor_position < len {
if ctrl_held {
let end_pos = next_word_boundary(&text, cursor_position);
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[end_pos..].iter());
text = new_text;
} else {
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[cursor_position + 1..].iter());
text = new_text;
}
did_change = true;
}
if did_change {
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
changed = true;
}
cursor_blink_timer = current_time;
}
KeyCode::ArrowLeft => {
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let min = selection_start.unwrap().min(cursor_position);
cursor_position = min;
selection_start = None;
cursor_blink_timer = current_time;
continue;
}
if ctrl_held {
cursor_position = prev_word_boundary(&text, cursor_position);
} else {
cursor_position = cursor_position.saturating_sub(1);
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::ArrowRight => {
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let max = selection_start.unwrap().max(cursor_position);
cursor_position = max;
selection_start = None;
cursor_blink_timer = current_time;
continue;
}
if ctrl_held {
cursor_position = next_word_boundary(&text, cursor_position);
} else if cursor_position < len {
cursor_position += 1;
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::Home => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
cursor_position = 0;
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::End => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
cursor_position = len;
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::KeyA if ctrl_held => {
selection_start = Some(0);
cursor_position = len;
cursor_blink_timer = current_time;
}
KeyCode::KeyZ if ctrl_held => {
if let Some(snapshot) = undo_stack.undo() {
text = snapshot.text.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
changed = true;
cursor_blink_timer = current_time;
}
}
KeyCode::KeyY if ctrl_held => {
if let Some(snapshot) = undo_stack.redo() {
text = snapshot.text.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
changed = true;
cursor_blink_timer = current_time;
}
}
KeyCode::KeyC if ctrl_held => {
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let selected: String = text.chars().skip(min).take(max - min).collect();
world.resources.retained_ui.clipboard_text = selected;
}
}
KeyCode::KeyX if ctrl_held => {
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let selected: String = text.chars().skip(min).take(max - min).collect();
world.resources.retained_ui.clipboard_text = selected;
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min;
selection_start = None;
changed = true;
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
}
KeyCode::KeyV if ctrl_held => {
let mut paste_text = world.resources.retained_ui.clipboard_text.clone();
if !paste_text.is_empty() {
if let Some(max) = max_length {
let existing = text.chars().count();
let removed = selection_start
.map(|s| s.max(cursor_position) - s.min(cursor_position))
.unwrap_or(0);
let available = max.saturating_sub(existing - removed);
paste_text = paste_text.chars().take(available).collect();
}
if !paste_text.is_empty() {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.push_str(&paste_text);
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min + paste_text.chars().count();
selection_start = None;
} else {
let chars: Vec<char> = text.chars().collect();
let mut new_text: String =
chars[..cursor_position].iter().collect();
new_text.push_str(&paste_text);
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position += paste_text.chars().count();
}
changed = true;
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
}
}
KeyCode::Escape => {
selection_start = None;
clear_focus = true;
}
KeyCode::Enter => {
clear_focus = true;
}
_ => {}
}
}
if interaction.clicked {
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let local_x = (mouse_position.x - rect.min.x) / dpi_scale - 8.0 + scroll_offset;
let new_pos = byte_index_at_x(&atlas, &text, font_size, local_x);
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else {
selection_start = None;
}
cursor_position = new_pos;
cursor_blink_timer = current_time;
}
}
}
if interaction.double_clicked {
let len = text.chars().count();
let pos = cursor_position.min(len);
let word_start = prev_word_boundary(&text, pos);
let word_end = next_word_boundary(&text, pos).min(len);
selection_start = Some(word_start);
cursor_position = word_end;
}
} else {
selection_start = None;
}
if clear_focus {
world.resources.retained_ui.focused_entity = None;
}
if changed {
world.resources.text_cache.set_text(data.text_slot, &text);
}
let cursor_visible = is_focused && ((current_time - cursor_blink_timer) % 1.0) < 0.5;
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity) {
cursor_node.visible = cursor_visible;
}
let has_selection =
is_focused && selection_start.is_some() && selection_start != Some(cursor_position);
if let Some(sel_node) = world.get_ui_layout_node_mut(data.selection_entity) {
sel_node.visible = has_selection;
}
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let text_before_cursor: String = text.chars().take(cursor_position).collect();
let cursor_x = measure_text_width(&atlas, &text_before_cursor, font_size);
let visible_width = rect.width() / dpi_scale - 16.0;
if cursor_x - scroll_offset > visible_width {
scroll_offset = cursor_x - visible_width;
} else if cursor_x - scroll_offset < 0.0 {
scroll_offset = cursor_x;
}
let cursor_screen_x = cursor_x - scroll_offset;
let mut sel_start_x = 0.0f32;
let mut sel_end_x = 0.0f32;
if has_selection {
let sel_start = selection_start.unwrap();
let sel_min = sel_start.min(cursor_position);
let sel_max = sel_start.max(cursor_position);
let text_before_sel: String = text.chars().take(sel_min).collect();
let text_to_sel_end: String = text.chars().take(sel_max).collect();
sel_start_x =
measure_text_width(&atlas, &text_before_sel, font_size) - scroll_offset;
sel_end_x = measure_text_width(&atlas, &text_to_sel_end, font_size) - scroll_offset;
}
drop(atlas);
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
cursor_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + cursor_screen_x, 4.0)).into();
}
if has_selection
&& let Some(sel_node) = world.get_ui_layout_node_mut(data.selection_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
sel_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + sel_start_x, 4.0)).into();
window.size = crate::ecs::ui::units::Ab(Vec2::new(
sel_end_x - sel_start_x,
rect.height() / dpi_scale - 8.0,
))
.into();
}
}
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TextInputChanged {
entity,
text: text.clone(),
},
);
validate_widget_text(world, entity, &text);
}
if clear_focus {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TextInputSubmitted {
entity,
text: text.clone(),
},
);
}
let text_is_empty = text.is_empty();
let placeholder_entity = if let Some(UiWidgetState::TextInput(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.text = text;
widget_data.cursor_position = cursor_position;
widget_data.selection_start = selection_start;
widget_data.changed = changed;
widget_data.cursor_blink_timer = cursor_blink_timer;
widget_data.scroll_offset = scroll_offset;
widget_data.undo_stack = undo_stack;
widget_data.placeholder_entity
} else {
None
};
if let Some(ph_entity) = placeholder_entity
&& let Some(node) = world.get_ui_layout_node_mut(ph_entity)
{
node.visible = text_is_empty;
}
}
fn handle_dropdown(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiDropdownData,
frame_keys: &[(KeyCode, bool)],
focused_entity: Option<freecs::Entity>,
) {
let mut selected_index = data.selected_index;
let mut changed = false;
let mut open = data.open;
let mut hovered_index = data.hovered_index;
let is_theme_dropdown = data.is_theme_dropdown;
let was_open = data.open;
let option_count = data.options.len();
let searchable = data.searchable;
let filter_input_entity = data.filter_input_entity;
let mut filtered_indices = data.filtered_indices.clone();
let mut filter_text = data.filter_text.clone();
if interaction.clicked {
open = !open;
}
if !open && focused_entity == Some(entity) {
for &(key, pressed) in frame_keys {
if pressed
&& (key == KeyCode::ArrowDown || key == KeyCode::Space || key == KeyCode::Enter)
{
open = true;
hovered_index = Some(selected_index);
break;
}
}
}
if open && !was_open && searchable {
if let Some(filter_entity) = filter_input_entity {
if let Some(UiWidgetState::TextInput(input_data)) =
world.get_ui_widget_state_mut(filter_entity)
{
input_data.text.clear();
input_data.cursor_position = 0;
input_data.selection_start = None;
input_data.changed = false;
}
if let Some(UiWidgetState::TextInput(input_data)) =
world.get_ui_widget_state(filter_entity)
{
world
.resources
.text_cache
.set_text(input_data.text_slot, "");
}
world.resources.retained_ui.focused_entity = Some(filter_entity);
}
filter_text.clear();
filtered_indices = (0..option_count).collect();
}
if open
&& searchable
&& let Some(filter_entity) = filter_input_entity
{
let current_filter = world
.get_ui_widget_state(filter_entity)
.and_then(|ws| {
if let UiWidgetState::TextInput(input_data) = ws {
Some(input_data.text.clone())
} else {
None
}
})
.unwrap_or_default();
if current_filter != filter_text {
filter_text = current_filter;
let lower_filter = filter_text.to_lowercase();
filtered_indices = (0..option_count)
.filter(|idx| {
lower_filter.is_empty()
|| data.options[*idx].to_lowercase().contains(&lower_filter)
})
.collect();
hovered_index = None;
for (index, popup_entity) in data.popup_entities.iter().enumerate() {
let visible = filtered_indices.contains(&index);
if let Some(node) = world.get_ui_layout_node_mut(*popup_entity) {
node.visible = visible;
}
}
}
}
if open {
let visible_count = if searchable {
filtered_indices.len()
} else {
option_count
};
for &(key, pressed) in frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::ArrowDown => {
if visible_count > 0 {
let current_visible_pos = hovered_index
.and_then(|hi| {
if searchable {
filtered_indices.iter().position(|&idx| idx == hi)
} else {
Some(hi)
}
})
.unwrap_or(0);
let next_pos = if current_visible_pos + 1 >= visible_count {
0
} else {
current_visible_pos + 1
};
hovered_index = Some(if searchable {
filtered_indices[next_pos]
} else {
next_pos
});
}
}
KeyCode::ArrowUp => {
if visible_count > 0 {
let current_visible_pos = hovered_index
.and_then(|hi| {
if searchable {
filtered_indices.iter().position(|&idx| idx == hi)
} else {
Some(hi)
}
})
.unwrap_or(0);
let prev_pos = if current_visible_pos == 0 {
visible_count.saturating_sub(1)
} else {
current_visible_pos - 1
};
hovered_index = Some(if searchable {
filtered_indices[prev_pos]
} else {
prev_pos
});
}
}
KeyCode::Enter => {
if let Some(idx) = hovered_index {
selected_index = idx;
changed = true;
open = false;
let selected_text = data.options[idx].clone();
world
.resources
.text_cache
.set_text(data.header_text_slot, selected_text);
}
}
KeyCode::Escape => {
open = false;
}
_ => {}
}
}
for (index, popup_entity) in data.popup_entities.iter().enumerate() {
if searchable && !filtered_indices.contains(&index) {
continue;
}
let popup_interaction = world.get_ui_node_interaction(*popup_entity);
let popup_clicked = popup_interaction.map(|i| i.clicked).unwrap_or(false);
let popup_hovered = popup_interaction.map(|i| i.hovered).unwrap_or(false);
if popup_hovered {
hovered_index = Some(index);
}
if popup_clicked {
selected_index = index;
changed = true;
open = false;
let selected_text = data.options[index].clone();
world
.resources
.text_cache
.set_text(data.header_text_slot, selected_text);
break;
}
}
for (index, popup_entity) in data.popup_entities.iter().enumerate() {
let is_highlighted = hovered_index == Some(index);
if let Some(weights) = world.get_ui_state_weights_mut(*popup_entity) {
weights.weights[crate::ecs::ui::state::UiHover::INDEX] =
if is_highlighted { 1.0 } else { 0.0 };
}
}
let mouse_just_pressed = world
.resources
.input
.mouse
.state
.contains(crate::ecs::input::resources::MouseState::LEFT_JUST_PRESSED);
let filter_hovered = filter_input_entity
.and_then(|fe| world.get_ui_node_interaction(fe))
.map(|i| i.hovered)
.unwrap_or(false);
if mouse_just_pressed && !interaction.hovered && hovered_index.is_none() && !filter_hovered
{
open = false;
}
}
if searchable && was_open && !open {
if let Some(filter_entity) = filter_input_entity
&& world.resources.retained_ui.focused_entity == Some(filter_entity)
{
world.resources.retained_ui.focused_entity = None;
}
for popup_entity in &data.popup_entities {
if let Some(node) = world.get_ui_layout_node_mut(*popup_entity) {
node.visible = true;
}
}
}
if is_theme_dropdown {
if open && hovered_index != data.hovered_index {
world
.resources
.retained_ui
.theme_state
.set_preview(hovered_index);
}
if was_open && !open {
world.resources.retained_ui.theme_state.clear_preview();
}
if changed {
world
.resources
.retained_ui
.theme_state
.select_theme(selected_index);
}
}
if let Some(node) = world.get_ui_layout_node_mut(data.popup_container_entity) {
node.visible = open;
}
if let Some(UiWidgetState::Dropdown(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.selected_index = selected_index;
widget_data.changed = changed;
widget_data.open = open;
widget_data.hovered_index = hovered_index;
widget_data.filter_text = filter_text;
widget_data.filtered_indices = filtered_indices;
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::DropdownChanged {
entity,
selected_index,
},
);
}
}
fn handle_multi_select(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiMultiSelectData,
) {
let mut selected_indices = data.selected_indices.clone();
let mut changed = false;
let mut open = data.open;
let mut hovered_index = data.hovered_index;
let popup_container_entity = data.popup_container_entity;
let popup_entities = data.popup_entities.clone();
let check_entities = data.check_entities.clone();
if interaction.clicked {
open = !open;
}
if open {
for (index, &popup_entity) in popup_entities.iter().enumerate() {
if let Some(popup_interaction) = world.get_ui_node_interaction(popup_entity) {
if popup_interaction.clicked {
if selected_indices.contains(&index) {
selected_indices.remove(&index);
} else {
selected_indices.insert(index);
}
changed = true;
}
if popup_interaction.hovered {
hovered_index = Some(index);
}
}
}
let mouse_state = world.resources.input.mouse.state;
let mouse_just_pressed =
mouse_state.contains(crate::ecs::input::MouseState::LEFT_JUST_PRESSED);
if mouse_just_pressed
&& !interaction.hovered
&& !world
.get_ui_node_interaction(popup_container_entity)
.is_some_and(|i| i.hovered)
{
open = false;
}
}
let header_text = if selected_indices.is_empty() {
"None selected".to_string()
} else if selected_indices.len() == 1 {
let idx = *selected_indices.iter().next().unwrap();
data.options[idx].clone()
} else {
format!("{} selected", selected_indices.len())
};
world
.resources
.text_cache
.set_text(data.header_text_slot, &header_text);
for (index, &check_entity) in check_entities.iter().enumerate() {
let check_text = if selected_indices.contains(&index) {
"\u{2713}"
} else {
" "
};
if let Some(crate::ecs::ui::components::UiNodeContent::Text { text_slot, .. }) =
world.get_ui_node_content(check_entity)
{
let slot = *text_slot;
world.resources.text_cache.set_text(slot, check_text);
}
}
if let Some(node) = world.get_ui_layout_node_mut(popup_container_entity) {
node.visible = open;
}
if let Some(UiWidgetState::MultiSelect(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.selected_indices = selected_indices.clone();
widget_data.changed = changed;
widget_data.open = open;
widget_data.hovered_index = hovered_index;
}
if changed {
let selected_vec: Vec<usize> = selected_indices.into_iter().collect();
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::MultiSelectChanged {
entity,
selected_indices: selected_vec,
},
);
}
}
fn handle_date_picker(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiDatePickerData,
) {
let mut year = data.year;
let mut month = data.month;
let mut day = data.day;
let mut changed = false;
let mut open = data.open;
let popup_entity = data.popup_entity;
let day_entities = data.day_entities.clone();
let day_text_slots = data.day_text_slots.clone();
let prev_month_entity = data.prev_month_entity;
let next_month_entity = data.next_month_entity;
let month_label_slot = data.month_label_slot;
let header_text_slot = data.header_text_slot;
if interaction.clicked {
open = !open;
}
let mut repopulate = false;
if open {
if let Some(prev_interaction) = world.get_ui_node_interaction(prev_month_entity)
&& prev_interaction.clicked
{
if month == 1 {
month = 12;
year -= 1;
} else {
month -= 1;
}
repopulate = true;
}
if let Some(next_interaction) = world.get_ui_node_interaction(next_month_entity)
&& next_interaction.clicked
{
if month == 12 {
month = 1;
year += 1;
} else {
month += 1;
}
repopulate = true;
}
let first_dow = crate::ecs::ui::widgets::day_of_week(year, month, 1);
let days = crate::ecs::ui::widgets::days_in_month(year, month);
for (cell_index, &day_entity) in day_entities.iter().enumerate() {
if let Some(day_interaction) = world.get_ui_node_interaction(day_entity)
&& day_interaction.clicked
{
let cell_u32 = cell_index as u32;
if cell_u32 >= first_dow && cell_u32 < first_dow + days {
let clicked_day = cell_u32 - first_dow + 1;
day = clicked_day;
changed = true;
open = false;
}
}
}
let mouse_state = world.resources.input.mouse.state;
let mouse_just_pressed =
mouse_state.contains(crate::ecs::input::MouseState::LEFT_JUST_PRESSED);
if mouse_just_pressed
&& !interaction.hovered
&& !world
.get_ui_node_interaction(popup_entity)
.is_some_and(|i| i.hovered)
{
open = false;
}
}
if repopulate {
let accent_color = world
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
crate::ecs::ui::widgets::populate_calendar_grid(
world,
year,
month,
day,
&day_text_slots,
&day_entities,
accent_color,
);
world.resources.text_cache.set_text(
month_label_slot,
crate::ecs::ui::widgets::format_month_year(year, month),
);
}
if changed {
let accent_color = world
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
crate::ecs::ui::widgets::populate_calendar_grid(
world,
year,
month,
day,
&day_text_slots,
&day_entities,
accent_color,
);
world
.resources
.text_cache
.set_text(header_text_slot, format!("{year:04}-{month:02}-{day:02}"));
}
if let Some(node) = world.get_ui_layout_node_mut(popup_entity) {
node.visible = open;
}
if let Some(UiWidgetState::DatePicker(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.year = year;
widget_data.month = month;
widget_data.day = day;
widget_data.changed = changed;
widget_data.open = open;
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::DatePickerChanged {
entity,
year,
month,
day,
},
);
}
}
fn handle_menu(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiMenuData,
) {
let mut clicked_item = None;
let mut open = data.open;
if interaction.clicked {
open = !open;
}
if open {
let mut any_popup_hovered = false;
for (index, popup_entity) in data.popup_entities.iter().enumerate() {
let popup_interaction = world.get_ui_node_interaction(*popup_entity);
let popup_clicked = popup_interaction.map(|i| i.clicked).unwrap_or(false);
if popup_interaction.map(|i| i.hovered).unwrap_or(false) {
any_popup_hovered = true;
}
if popup_clicked {
clicked_item = Some(index);
open = false;
break;
}
}
let mouse_just_pressed = world
.resources
.input
.mouse
.state
.contains(crate::ecs::input::resources::MouseState::LEFT_JUST_PRESSED);
if mouse_just_pressed && !interaction.hovered && !any_popup_hovered {
open = false;
}
}
if let Some(node) = world.get_ui_layout_node_mut(data.popup_container_entity) {
node.visible = open;
}
if let Some(UiWidgetState::Menu(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.clicked_item = clicked_item;
widget_data.open = open;
}
if let Some(item_index) = clicked_item {
world
.resources
.retained_ui
.frame_events
.push(crate::ecs::ui::resources::UiEvent::MenuItemClicked { entity, item_index });
}
}
struct PanelContext {
mouse_position: Vec2,
mouse_just_pressed: bool,
mouse_just_released: bool,
mouse_down: bool,
dpi_scale: f32,
viewport_size: Vec2,
mouse_occluded: bool,
}
fn handle_panel(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiPanelData,
ctx: &PanelContext,
) {
use crate::ecs::ui::components::ResizeEdge;
use crate::ecs::ui::units::Ab;
let mouse_position = ctx.mouse_position;
let mouse_just_pressed = ctx.mouse_just_pressed;
let mouse_just_released = ctx.mouse_just_released;
let mouse_down = ctx.mouse_down;
let dpi_scale = ctx.dpi_scale;
let viewport_size = ctx.viewport_size;
let header_interaction = snapshot_interaction(world, data.header_entity);
let mut drag_offset = data.drag_offset;
let mut resize_edge = data.resize_edge;
let mut resize_start_rect = data.resize_start_rect;
let mut resize_start_mouse = data.resize_start_mouse;
let mut panel_kind = data.panel_kind;
let mut undocked_rect = data.undocked_rect;
let mut default_dock_size = data.default_dock_size;
let resizable = data.resizable;
if let Some(collapse_entity) = data.collapse_button_entity {
let collapse_interaction = snapshot_interaction(world, collapse_entity);
if collapse_interaction.clicked {
if panel_kind == UiPanelKind::Floating {
if let Some(node) = world.get_ui_layout_node_mut(entity) {
node.visible = false;
}
} else {
let new_collapsed = !data.collapsed;
if let Some(node) = world.get_ui_layout_node_mut(data.content_entity) {
node.visible = !new_collapsed;
}
if let Some(text_slot) = data.collapse_button_text_slot {
world
.resources
.text_cache
.set_text(text_slot, if new_collapsed { "+" } else { "-" });
}
if let Some(UiWidgetState::Panel(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.collapsed = new_collapsed;
}
}
}
}
if header_interaction.dragging && panel_kind == UiPanelKind::Floating {
if drag_offset.is_none() {
let panel_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = panel_rect
&& let Some(drag_start) = header_interaction.drag_start
{
drag_offset = Some(drag_start - rect.min);
}
}
if let Some(offset) = drag_offset
&& let Some(node) = world.get_ui_layout_node_mut(entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
let current_ab = window.position.absolute.unwrap_or(Vec2::new(0.0, 0.0));
let parent_min = node.computed_rect.min - current_ab * dpi_scale;
let new_pos = (mouse_position - offset - parent_min) / dpi_scale;
let rounded_pos = Vec2::new(new_pos.x.round(), new_pos.y.round());
window.position = Ab(rounded_pos).into();
}
world.resources.retained_ui.dock_indicator_active = true;
world.resources.retained_ui.dock_indicator_panel = Some(entity);
}
if header_interaction.dragging
&& panel_kind != UiPanelKind::Floating
&& !data.pinned
&& let Some(drag_start) = header_interaction.drag_start
{
let drag_distance = (mouse_position - drag_start).magnitude();
if drag_distance > 50.0 {
panel_kind = UiPanelKind::Floating;
let restored_size = undocked_rect
.map(|r| r.size())
.unwrap_or(Vec2::new(300.0, 400.0));
let new_pos = mouse_position - Vec2::new(restored_size.x * 0.5, 14.0);
if let Some(node) = world.get_ui_layout_node_mut(entity) {
node.layer = Some(crate::render::wgpu::passes::geometry::UiLayer::FloatingPanels);
if let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position = Ab(new_pos / dpi_scale).into();
window.size = Ab(restored_size / dpi_scale).into();
}
}
drag_offset = Some(Vec2::new(restored_size.x * 0.5, 14.0));
world.resources.retained_ui.dock_indicator_active = true;
world.resources.retained_ui.dock_indicator_panel = Some(entity);
}
}
if mouse_just_released
&& world.resources.retained_ui.dock_indicator_panel == Some(entity)
&& panel_kind == UiPanelKind::Floating
{
let indicator_size = 40.0;
let indicator_spacing = 4.0;
let center = viewport_size * 0.5;
let dock_buttons = [
(
crate::ecs::ui::types::Rect::from_center_size(
Vec2::new(center.x, center.y - indicator_size - indicator_spacing),
Vec2::new(indicator_size, indicator_size),
),
UiPanelKind::DockedTop,
),
(
crate::ecs::ui::types::Rect::from_center_size(
Vec2::new(center.x, center.y + indicator_size + indicator_spacing),
Vec2::new(indicator_size, indicator_size),
),
UiPanelKind::DockedBottom,
),
(
crate::ecs::ui::types::Rect::from_center_size(
Vec2::new(center.x - indicator_size - indicator_spacing, center.y),
Vec2::new(indicator_size, indicator_size),
),
UiPanelKind::DockedLeft,
),
(
crate::ecs::ui::types::Rect::from_center_size(
Vec2::new(center.x + indicator_size + indicator_spacing, center.y),
Vec2::new(indicator_size, indicator_size),
),
UiPanelKind::DockedRight,
),
];
let mut target_dock = None;
for (btn_rect, dock_kind) in &dock_buttons {
if btn_rect.contains(mouse_position) {
target_dock = Some(*dock_kind);
break;
}
}
if let Some(new_kind) = target_dock {
let panel_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = panel_rect {
undocked_rect = Some(rect);
}
panel_kind = new_kind;
default_dock_size = match new_kind {
UiPanelKind::DockedLeft | UiPanelKind::DockedRight => {
undocked_rect.map(|r| r.width()).unwrap_or(300.0) / dpi_scale
}
UiPanelKind::DockedTop | UiPanelKind::DockedBottom => {
undocked_rect.map(|r| r.height()).unwrap_or(300.0) / dpi_scale
}
UiPanelKind::Floating => 300.0,
};
if let Some(node) = world.get_ui_layout_node_mut(entity) {
node.layer = Some(crate::render::wgpu::passes::geometry::UiLayer::DockedPanels);
}
}
world.resources.retained_ui.dock_indicator_active = false;
world.resources.retained_ui.dock_indicator_panel = None;
}
if !header_interaction.pressed && !header_interaction.dragging {
drag_offset = None;
if world.resources.retained_ui.dock_indicator_panel == Some(entity) {
world.resources.retained_ui.dock_indicator_active = false;
world.resources.retained_ui.dock_indicator_panel = None;
}
}
if !ctx.mouse_occluded && resizable {
let panel_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = panel_rect {
let edge_margin = 6.0 * dpi_scale;
let near_left = (mouse_position.x - rect.min.x).abs() < edge_margin;
let near_right = (mouse_position.x - rect.max.x).abs() < edge_margin;
let near_top = (mouse_position.y - rect.min.y).abs() < edge_margin;
let near_bottom = (mouse_position.y - rect.max.y).abs() < edge_margin;
let inside_x = mouse_position.x >= rect.min.x - edge_margin
&& mouse_position.x <= rect.max.x + edge_margin;
let inside_y = mouse_position.y >= rect.min.y - edge_margin
&& mouse_position.y <= rect.max.y + edge_margin;
let detected_edge = match panel_kind {
UiPanelKind::DockedLeft => {
if near_right && inside_y {
Some(ResizeEdge::Right)
} else {
None
}
}
UiPanelKind::DockedRight => {
if near_left && inside_y {
Some(ResizeEdge::Left)
} else {
None
}
}
UiPanelKind::DockedTop => {
if near_bottom && inside_x {
Some(ResizeEdge::Bottom)
} else {
None
}
}
UiPanelKind::DockedBottom => {
if near_top && inside_x {
Some(ResizeEdge::Top)
} else {
None
}
}
UiPanelKind::Floating => {
if near_top && near_left && inside_x && inside_y {
Some(ResizeEdge::TopLeft)
} else if near_top && near_right && inside_x && inside_y {
Some(ResizeEdge::TopRight)
} else if near_bottom && near_left && inside_x && inside_y {
Some(ResizeEdge::BottomLeft)
} else if near_bottom && near_right && inside_x && inside_y {
Some(ResizeEdge::BottomRight)
} else if near_left && inside_y {
Some(ResizeEdge::Left)
} else if near_right && inside_y {
Some(ResizeEdge::Right)
} else if near_top && inside_x {
Some(ResizeEdge::Top)
} else if near_bottom && inside_x {
Some(ResizeEdge::Bottom)
} else {
None
}
}
};
if let Some(edge) = detected_edge {
world.resources.retained_ui.requested_cursor = Some(match edge {
ResizeEdge::Left | ResizeEdge::Right => winit::window::CursorIcon::EwResize,
ResizeEdge::Top | ResizeEdge::Bottom => winit::window::CursorIcon::NsResize,
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
winit::window::CursorIcon::NwseResize
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
winit::window::CursorIcon::NeswResize
}
});
}
if !header_interaction.dragging && mouse_just_pressed && detected_edge.is_some() {
resize_edge = detected_edge;
resize_start_rect = Some(rect);
resize_start_mouse = Some(mouse_position);
}
}
}
if let Some(edge) = resize_edge {
world.resources.retained_ui.requested_cursor = Some(match edge {
ResizeEdge::Left | ResizeEdge::Right => winit::window::CursorIcon::EwResize,
ResizeEdge::Top | ResizeEdge::Bottom => winit::window::CursorIcon::NsResize,
ResizeEdge::TopLeft | ResizeEdge::BottomRight => winit::window::CursorIcon::NwseResize,
ResizeEdge::TopRight | ResizeEdge::BottomLeft => winit::window::CursorIcon::NeswResize,
});
if mouse_down {
if let (Some(start_rect), Some(start_mouse)) = (resize_start_rect, resize_start_mouse) {
let delta = mouse_position - start_mouse;
let min_size = data.min_size;
let mut new_pos = start_rect.min;
let mut new_size = start_rect.size();
match edge {
ResizeEdge::Right => {
new_size.x = (start_rect.width() + delta.x).max(min_size.x);
}
ResizeEdge::Bottom => {
new_size.y = (start_rect.height() + delta.y).max(min_size.y);
}
ResizeEdge::Left => {
let new_width = (start_rect.width() - delta.x).max(min_size.x);
new_pos.x = start_rect.max.x - new_width;
new_size.x = new_width;
}
ResizeEdge::Top => {
let new_height = (start_rect.height() - delta.y).max(min_size.y);
new_pos.y = start_rect.max.y - new_height;
new_size.y = new_height;
}
ResizeEdge::TopLeft => {
let new_width = (start_rect.width() - delta.x).max(min_size.x);
let new_height = (start_rect.height() - delta.y).max(min_size.y);
new_pos.x = start_rect.max.x - new_width;
new_pos.y = start_rect.max.y - new_height;
new_size.x = new_width;
new_size.y = new_height;
}
ResizeEdge::TopRight => {
let new_height = (start_rect.height() - delta.y).max(min_size.y);
new_pos.y = start_rect.max.y - new_height;
new_size.x = (start_rect.width() + delta.x).max(min_size.x);
new_size.y = new_height;
}
ResizeEdge::BottomLeft => {
let new_width = (start_rect.width() - delta.x).max(min_size.x);
new_pos.x = start_rect.max.x - new_width;
new_size.x = new_width;
new_size.y = (start_rect.height() + delta.y).max(min_size.y);
}
ResizeEdge::BottomRight => {
new_size.x = (start_rect.width() + delta.x).max(min_size.x);
new_size.y = (start_rect.height() + delta.y).max(min_size.y);
}
}
if panel_kind != UiPanelKind::Floating {
match panel_kind {
UiPanelKind::DockedLeft | UiPanelKind::DockedRight => {
default_dock_size = new_size.x / dpi_scale;
}
UiPanelKind::DockedTop | UiPanelKind::DockedBottom => {
default_dock_size = new_size.y / dpi_scale;
}
UiPanelKind::Floating => {}
}
} else if let Some(node) = world.get_ui_layout_node_mut(entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
let current_ab = window.position.absolute.unwrap_or(Vec2::new(0.0, 0.0));
let parent_min = node.computed_rect.min - current_ab * dpi_scale;
let logical_pos = (new_pos - parent_min) / dpi_scale;
let logical_size = new_size / dpi_scale;
window.position =
Ab(Vec2::new(logical_pos.x.round(), logical_pos.y.round())).into();
window.size =
Ab(Vec2::new(logical_size.x.round(), logical_size.y.round())).into();
}
}
} else {
resize_edge = None;
resize_start_rect = None;
resize_start_mouse = None;
}
}
if let Some(UiWidgetState::Panel(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.drag_offset = drag_offset;
widget_data.resize_edge = resize_edge;
widget_data.resize_start_rect = resize_start_rect;
widget_data.resize_start_mouse = resize_start_mouse;
widget_data.panel_kind = panel_kind;
widget_data.undocked_rect = undocked_rect;
widget_data.default_dock_size = default_dock_size;
}
}
fn handle_color_picker(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiColorPickerData,
) {
let mode = data.mode;
let mut any_changed = false;
let mut slider_values = [0.0_f32; 4];
for (index, slider_entity) in data.slider_entities.iter().enumerate() {
if let Some(UiWidgetState::Slider(slider_data)) = world.get_ui_widget_state(*slider_entity)
{
if slider_data.changed {
any_changed = true;
}
slider_values[index] = slider_data.value;
}
}
let color = match mode {
crate::ecs::ui::components::ColorPickerMode::Rgb => Vec4::new(
slider_values[0],
slider_values[1],
slider_values[2],
slider_values[3],
),
crate::ecs::ui::components::ColorPickerMode::Hsv => {
let hsv = crate::ecs::ui::color::Hsva {
hue: slider_values[0],
saturation: slider_values[1],
value: slider_values[2],
alpha: slider_values[3],
};
hsv.to_rgba()
}
};
if any_changed && let Some(swatch_color) = world.get_ui_node_color_mut(data.swatch_entity) {
swatch_color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(color);
}
if let Some(UiWidgetState::ColorPicker(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.color = color;
widget_data.changed = any_changed;
}
if any_changed {
world
.resources
.retained_ui
.frame_events
.push(crate::ecs::ui::resources::UiEvent::ColorPickerChanged { entity, color });
}
}
fn handle_selectable_label(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiSelectableLabelData,
) {
if let Some(UiWidgetState::SelectableLabel(widget_data)) = world.get_ui_widget_state_mut(entity)
{
widget_data.changed = false;
}
if interaction.clicked {
let new_selected = !data.selected;
if let Some(group_id) = data.group_id
&& new_selected
{
let siblings = world
.resources
.retained_ui
.selectable_label_groups
.get(&group_id)
.cloned()
.unwrap_or_default();
for other in siblings {
if other == entity {
continue;
}
let other_data = world.get_ui_widget_state(other).cloned();
if let Some(UiWidgetState::SelectableLabel(other_sl)) = other_data
&& other_sl.selected
{
world.ui_set_selected(other, false);
if let Some(UiWidgetState::SelectableLabel(wd)) =
world.get_ui_widget_state_mut(other)
{
wd.changed = true;
}
}
}
}
world.ui_set_selected(entity, new_selected);
if let Some(UiWidgetState::SelectableLabel(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
widget_data.selected = new_selected;
widget_data.changed = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::SelectableLabelClicked {
entity,
selected: new_selected,
},
);
}
}
#[allow(clippy::too_many_arguments)]
fn handle_drag_value(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiDragValueData,
focused_entity: Option<freecs::Entity>,
frame_chars: &[char],
frame_keys: &[(KeyCode, bool)],
ctrl_held: bool,
shift_held: bool,
mouse_position: Vec2,
current_time: f64,
dpi_scale: f32,
) {
let is_focused = focused_entity == Some(entity);
let mut value = data.value;
let mut changed = false;
let mut editing = data.editing;
let mut edit_text = data.edit_text.clone();
let mut cursor_position = data.cursor_position;
let mut selection_start = data.selection_start;
let mut cursor_blink_timer = data.cursor_blink_timer;
let mut scroll_offset = data.scroll_offset;
let mut drag_start_value = data.drag_start_value;
let mut undo_stack = data.undo_stack.clone();
let mut clear_focus = false;
if editing && is_focused {
let mut needs_snapshot = false;
for character in frame_chars {
if *character >= ' '
&& (character.is_ascii_digit() || *character == '.' || *character == '-')
{
if !needs_snapshot {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
});
needs_snapshot = true;
}
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars: Vec<char> = edit_text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.push(*character);
new_text.extend(chars[max..].iter());
edit_text = new_text;
cursor_position = min + 1;
selection_start = None;
} else {
let chars: Vec<char> = edit_text.chars().collect();
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.push(*character);
new_text.extend(chars[cursor_position..].iter());
edit_text = new_text;
cursor_position += 1;
}
cursor_blink_timer = current_time;
}
}
if needs_snapshot {
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
for (key, is_pressed) in frame_keys {
if !is_pressed {
continue;
}
let chars: Vec<char> = edit_text.chars().collect();
let len = chars.len();
match key {
KeyCode::Backspace => {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
});
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
edit_text = new_text;
cursor_position = min;
selection_start = None;
} else if cursor_position > 0 {
if ctrl_held {
let new_pos = prev_word_boundary(&edit_text, cursor_position);
let mut new_text: String = chars[..new_pos].iter().collect();
new_text.extend(chars[cursor_position..].iter());
edit_text = new_text;
cursor_position = new_pos;
} else {
let mut new_text: String =
chars[..cursor_position - 1].iter().collect();
new_text.extend(chars[cursor_position..].iter());
edit_text = new_text;
cursor_position -= 1;
}
}
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
},
current_time,
);
cursor_blink_timer = current_time;
}
KeyCode::Delete => {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
});
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
edit_text = new_text;
cursor_position = min;
selection_start = None;
} else if cursor_position < len {
if ctrl_held {
let end_pos = next_word_boundary(&edit_text, cursor_position);
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[end_pos..].iter());
edit_text = new_text;
} else {
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[cursor_position + 1..].iter());
edit_text = new_text;
}
}
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
},
current_time,
);
cursor_blink_timer = current_time;
}
KeyCode::ArrowLeft => {
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let min = selection_start.unwrap().min(cursor_position);
cursor_position = min;
selection_start = None;
cursor_blink_timer = current_time;
continue;
}
if ctrl_held {
cursor_position = prev_word_boundary(&edit_text, cursor_position);
} else {
cursor_position = cursor_position.saturating_sub(1);
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::ArrowRight => {
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let max = selection_start.unwrap().max(cursor_position);
cursor_position = max;
selection_start = None;
cursor_blink_timer = current_time;
continue;
}
if ctrl_held {
cursor_position = next_word_boundary(&edit_text, cursor_position);
} else if cursor_position < len {
cursor_position += 1;
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::Home => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
cursor_position = 0;
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::End => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
cursor_position = len;
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::KeyA if ctrl_held => {
selection_start = Some(0);
cursor_position = len;
cursor_blink_timer = current_time;
}
KeyCode::KeyZ if ctrl_held => {
if let Some(snapshot) = undo_stack.undo() {
edit_text = snapshot.text.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
cursor_blink_timer = current_time;
}
}
KeyCode::KeyY if ctrl_held => {
if let Some(snapshot) = undo_stack.redo() {
edit_text = snapshot.text.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
cursor_blink_timer = current_time;
}
}
KeyCode::KeyC if ctrl_held => {
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let selected: String =
edit_text.chars().skip(min).take(max - min).collect();
world.resources.retained_ui.clipboard_text = selected;
}
}
KeyCode::KeyX if ctrl_held => {
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let selected: String =
edit_text.chars().skip(min).take(max - min).collect();
world.resources.retained_ui.clipboard_text = selected;
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
});
let chars: Vec<char> = edit_text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
edit_text = new_text;
cursor_position = min;
selection_start = None;
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
}
KeyCode::KeyV if ctrl_held => {
let paste_text = world.resources.retained_ui.clipboard_text.clone();
if !paste_text.is_empty() {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
});
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars: Vec<char> = edit_text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.push_str(&paste_text);
new_text.extend(chars[max..].iter());
edit_text = new_text;
cursor_position = min + paste_text.chars().count();
selection_start = None;
} else {
let chars: Vec<char> = edit_text.chars().collect();
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.push_str(&paste_text);
new_text.extend(chars[cursor_position..].iter());
edit_text = new_text;
cursor_position += paste_text.chars().count();
}
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: edit_text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
}
KeyCode::Escape => {
editing = false;
selection_start = None;
clear_focus = true;
}
KeyCode::Enter => {
if let Ok(parsed) = edit_text.parse::<f32>() {
value = parsed.clamp(data.min, data.max);
changed = true;
}
editing = false;
selection_start = None;
clear_focus = true;
}
_ => {}
}
}
if interaction.clicked {
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let local_x = (mouse_position.x - rect.min.x) / dpi_scale - 8.0 + scroll_offset;
let new_pos = byte_index_at_x(&atlas, &edit_text, font_size, local_x);
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else {
selection_start = None;
}
cursor_position = new_pos;
cursor_blink_timer = current_time;
}
}
}
if interaction.double_clicked {
let len = edit_text.chars().count();
let pos = cursor_position.min(len);
let word_start = prev_word_boundary(&edit_text, pos);
let word_end = next_word_boundary(&edit_text, pos).min(len);
selection_start = Some(word_start);
cursor_position = word_end;
}
world
.resources
.text_cache
.set_text(data.text_slot, &edit_text);
} else if editing && !is_focused {
if let Ok(parsed) = edit_text.parse::<f32>() {
value = parsed.clamp(data.min, data.max);
changed = true;
}
editing = false;
selection_start = None;
} else {
if interaction.dragging
&& let Some(drag_start) = interaction.drag_start
{
let delta_x = mouse_position.x - drag_start.x;
let pixel_threshold = 3.0;
if delta_x.abs() > pixel_threshold {
let new_value = (drag_start_value + (delta_x / dpi_scale) * data.speed)
.clamp(data.min, data.max);
if (new_value - value).abs() > f32::EPSILON {
value = new_value;
changed = true;
}
}
}
if interaction.clicked && !interaction.dragging {
editing = true;
edit_text = format!("{:.prec$}", data.value, prec = data.precision);
cursor_position = edit_text.chars().count();
selection_start = Some(0);
cursor_blink_timer = current_time;
world.resources.retained_ui.focused_entity = Some(entity);
}
if interaction.pressed && !interaction.dragging && interaction.drag_start.is_some() {
drag_start_value = value;
}
}
if clear_focus {
world.resources.retained_ui.focused_entity = None;
}
if !editing {
let display = format!(
"{}{:.prec$}{}",
data.prefix,
value,
data.suffix,
prec = data.precision
);
world
.resources
.text_cache
.set_text(data.text_slot, &display);
scroll_offset = 0.0;
}
let cursor_visible = editing && is_focused && ((current_time - cursor_blink_timer) % 1.0) < 0.5;
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity) {
cursor_node.visible = cursor_visible;
}
let has_selection = editing
&& is_focused
&& selection_start.is_some()
&& selection_start != Some(cursor_position);
if let Some(sel_node) = world.get_ui_layout_node_mut(data.selection_entity) {
sel_node.visible = has_selection;
}
if editing {
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let text_before_cursor: String = edit_text.chars().take(cursor_position).collect();
let cursor_x = measure_text_width(&atlas, &text_before_cursor, font_size);
let visible_width = rect.width() / dpi_scale - 16.0;
if cursor_x - scroll_offset > visible_width {
scroll_offset = cursor_x - visible_width;
} else if cursor_x - scroll_offset < 0.0 {
scroll_offset = cursor_x;
}
let cursor_screen_x = cursor_x - scroll_offset;
let mut sel_start_x = 0.0f32;
let mut sel_end_x = 0.0f32;
if has_selection {
let sel_start = selection_start.unwrap();
let sel_min = sel_start.min(cursor_position);
let sel_max = sel_start.max(cursor_position);
let text_before_sel: String = edit_text.chars().take(sel_min).collect();
let text_to_sel_end: String = edit_text.chars().take(sel_max).collect();
sel_start_x =
measure_text_width(&atlas, &text_before_sel, font_size) - scroll_offset;
sel_end_x =
measure_text_width(&atlas, &text_to_sel_end, font_size) - scroll_offset;
}
drop(atlas);
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
cursor_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + cursor_screen_x, 4.0)).into();
}
if has_selection
&& let Some(sel_node) = world.get_ui_layout_node_mut(data.selection_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
sel_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + sel_start_x, 4.0)).into();
window.size = crate::ecs::ui::units::Ab(Vec2::new(
sel_end_x - sel_start_x,
rect.height() / dpi_scale - 8.0,
))
.into();
}
}
}
}
if let Some(up) = data.up_entity {
let up_clicked = world
.get_ui_node_interaction(up)
.map(|i| i.clicked)
.unwrap_or(false);
if up_clicked {
value = (value + data.step).min(data.max);
changed = true;
}
}
if let Some(down) = data.down_entity {
let down_clicked = world
.get_ui_node_interaction(down)
.map(|i| i.clicked)
.unwrap_or(false);
if down_clicked {
value = (value - data.step).max(data.min);
changed = true;
}
}
if let Some(interaction_comp) = world.get_ui_node_interaction_mut(entity) {
if editing {
interaction_comp.cursor_icon = Some(winit::window::CursorIcon::Text);
} else {
interaction_comp.cursor_icon = Some(winit::window::CursorIcon::EwResize);
}
}
if let Some(UiWidgetState::DragValue(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.value = value;
widget_data.changed = changed;
widget_data.editing = editing;
widget_data.edit_text = edit_text;
widget_data.cursor_position = cursor_position;
widget_data.selection_start = selection_start;
widget_data.cursor_blink_timer = cursor_blink_timer;
widget_data.scroll_offset = scroll_offset;
widget_data.drag_start_value = drag_start_value;
widget_data.undo_stack = undo_stack;
}
if changed {
world
.resources
.retained_ui
.frame_events
.push(crate::ecs::ui::resources::UiEvent::DragValueChanged { entity, value });
}
}
fn handle_context_menu(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiContextMenuData,
) {
if !data.open {
if let Some(UiWidgetState::ContextMenu(widget_data)) = world.get_ui_widget_state_mut(entity)
{
widget_data.clicked_item = None;
}
return;
}
let use_defs = !data.item_defs.is_empty();
let mut clicked_command = None;
let mut should_close = false;
if use_defs {
clicked_command = check_defs_for_click(world, &data.item_defs);
handle_submenu_hover(world, entity, &data.item_defs);
} else {
for (index, item_entity) in data.item_entities.iter().enumerate() {
if *item_entity == freecs::Entity::default() {
continue;
}
let item_clicked = world
.get_ui_node_interaction(*item_entity)
.map(|i| i.clicked)
.unwrap_or(false);
if item_clicked {
clicked_command = Some(index);
should_close = true;
break;
}
}
}
if clicked_command.is_some() {
should_close = true;
}
let frame_keys = &world.resources.retained_ui.frame_keys;
for (key, is_pressed) in frame_keys {
if *is_pressed && *key == KeyCode::Escape {
should_close = true;
}
}
let hovered = world.resources.retained_ui.hovered_entity;
let mouse_state = world.resources.input.mouse.state;
let left_just_pressed =
mouse_state.contains(crate::ecs::input::resources::MouseState::LEFT_JUST_PRESSED);
if left_just_pressed && let Some(hovered_ent) = hovered {
let mut is_inside =
data.item_entities.contains(&hovered_ent) || hovered_ent == data.popup_entity;
if !is_inside && use_defs {
is_inside = is_entity_in_submenu_popups(&data.item_defs, hovered_ent, world);
}
if !is_inside {
should_close = true;
}
}
if should_close {
if let Some(node) = world.get_ui_layout_node_mut(data.popup_entity) {
node.visible = false;
}
close_submenu_popups(world, &data.item_defs);
if let Some(UiWidgetState::ContextMenu(widget_data)) = world.get_ui_widget_state_mut(entity)
{
widget_data.open = false;
widget_data.clicked_item = clicked_command;
close_submenu_defs_state(&mut widget_data.item_defs);
}
world.resources.retained_ui.active_context_menu = None;
if let Some(cmd_id) = clicked_command {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::ContextMenuItemClicked {
entity,
item_index: cmd_id,
},
);
}
}
}
fn check_defs_for_click(
world: &World,
defs: &[crate::ecs::ui::components::ContextMenuItemDef],
) -> Option<usize> {
for def in defs {
if let crate::ecs::ui::components::ContextMenuItemKind::Action = &def.kind
&& let Some(cmd_id) = def.command_id
{
let clicked = world
.get_ui_node_interaction(def.row_entity)
.map(|i| i.clicked)
.unwrap_or(false);
if clicked {
return Some(cmd_id);
}
}
if let crate::ecs::ui::components::ContextMenuItemKind::Submenu { children, .. } = &def.kind
&& let Some(cmd_id) = check_defs_for_click(world, children)
{
return Some(cmd_id);
}
}
None
}
fn handle_submenu_hover(
world: &mut World,
menu_entity: freecs::Entity,
defs: &[crate::ecs::ui::components::ContextMenuItemDef],
) {
let hovered = world.resources.retained_ui.hovered_entity;
let mut submenu_indices = Vec::new();
for (index, def) in defs.iter().enumerate() {
if let crate::ecs::ui::components::ContextMenuItemKind::Submenu { .. } = &def.kind {
submenu_indices.push(index);
}
}
for &sub_index in &submenu_indices {
let def = &defs[sub_index];
let row_hovered = hovered == Some(def.row_entity);
if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
popup_entity,
open,
children,
..
} = &def.kind
{
let child_popup = *popup_entity;
let was_open = *open;
if row_hovered && !was_open {
let row_rect = world
.get_ui_layout_node(def.row_entity)
.map(|n| n.computed_rect);
if let Some(rect) = row_rect {
let dpi_scale = world.resources.window.cached_scale_factor;
let pos = Vec2::new(rect.max.x, rect.min.y) / dpi_scale;
if let Some(node) = world.get_ui_layout_node_mut(child_popup)
&& 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(pos).into();
node.visible = true;
}
}
if let Some(UiWidgetState::ContextMenu(menu_data)) =
world.get_ui_widget_state_mut(menu_entity)
&& let Some(item_def) = menu_data.item_defs.get_mut(sub_index)
&& let crate::ecs::ui::components::ContextMenuItemKind::Submenu { open, .. } =
&mut item_def.kind
{
*open = true;
}
} else if !row_hovered && was_open {
let popup_hovered = hovered == Some(child_popup);
let child_item_hovered =
is_entity_in_submenu_popups(children, hovered.unwrap_or_default(), world)
|| popup_hovered;
if !child_item_hovered {
if let Some(node) = world.get_ui_layout_node_mut(child_popup) {
node.visible = false;
}
close_submenu_popups(world, children);
if let Some(UiWidgetState::ContextMenu(menu_data)) =
world.get_ui_widget_state_mut(menu_entity)
&& let Some(item_def) = menu_data.item_defs.get_mut(sub_index)
&& let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
open,
children,
..
} = &mut item_def.kind
{
*open = false;
close_submenu_defs_state(children);
}
}
}
if was_open {
let current_defs = if let Some(UiWidgetState::ContextMenu(menu_data)) =
world.get_ui_widget_state(menu_entity)
{
if let Some(item_def) = menu_data.item_defs.get(sub_index) {
if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
children,
..
} = &item_def.kind
{
Some(children.clone())
} else {
None
}
} else {
None
}
} else {
None
};
if let Some(child_defs) = current_defs {
handle_submenu_hover(world, menu_entity, &child_defs);
}
}
}
}
}
fn is_entity_in_submenu_popups(
defs: &[crate::ecs::ui::components::ContextMenuItemDef],
target: freecs::Entity,
world: &World,
) -> bool {
for def in defs {
if def.row_entity == target {
return true;
}
if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
popup_entity,
children,
..
} = &def.kind
{
if *popup_entity == target {
return true;
}
let popup_rect = world
.get_ui_layout_node(*popup_entity)
.map(|n| n.computed_rect);
if let Some(rect) = popup_rect {
let mouse_pos = world.resources.input.mouse.position;
if rect.contains(mouse_pos) {
return true;
}
}
if is_entity_in_submenu_popups(children, target, world) {
return true;
}
}
}
false
}
fn close_submenu_popups(
world: &mut World,
defs: &[crate::ecs::ui::components::ContextMenuItemDef],
) {
for def in defs {
if let crate::ecs::ui::components::ContextMenuItemKind::Submenu {
popup_entity,
children,
..
} = &def.kind
{
if let Some(node) = world.get_ui_layout_node_mut(*popup_entity) {
node.visible = false;
}
close_submenu_popups(world, children);
}
}
}
fn close_submenu_defs_state(defs: &mut [crate::ecs::ui::components::ContextMenuItemDef]) {
for def in defs.iter_mut() {
if let crate::ecs::ui::components::ContextMenuItemKind::Submenu { open, children, .. } =
&mut def.kind
{
*open = false;
close_submenu_defs_state(children);
}
}
}
fn handle_tree_view(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiTreeViewData,
ctrl_held: bool,
_shift_held: bool,
frame_keys: &[(KeyCode, bool)],
focused_entity: Option<freecs::Entity>,
) {
if let Some(UiWidgetState::TreeView(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.changed = false;
widget_data.context_menu_node = None;
}
let node_entities = data.node_entities.clone();
let multi_select = data.multi_select;
let tree_entity = entity;
for node_entity in &node_entities {
let node_data = world.get_ui_widget_state(*node_entity).cloned();
let Some(UiWidgetState::TreeNode(tree_node)) = node_data else {
continue;
};
let row_interaction = snapshot_interaction(world, tree_node.row_entity);
let arrow_clicked = row_interaction.clicked
&& world
.get_ui_layout_node(tree_node.arrow_entity)
.map(|node| {
node.computed_rect
.contains(world.resources.input.mouse.position)
})
.unwrap_or(false);
if row_interaction.clicked && !arrow_clicked {
if !multi_select || !ctrl_held {
let previously_selected = data.selected_nodes.clone();
for prev in &previously_selected {
if *prev != *node_entity {
let prev_data = world.get_ui_widget_state(*prev).cloned();
if let Some(UiWidgetState::TreeNode(prev_node)) = prev_data {
if let Some(color) = world.get_ui_node_color_mut(prev_node.row_entity) {
color.colors[crate::ecs::ui::state::UiSelected::INDEX] = None;
}
if let Some(weights) =
world.get_ui_state_weights_mut(prev_node.row_entity)
{
weights.weights[crate::ecs::ui::state::UiSelected::INDEX] = 0.0;
}
if let Some(UiWidgetState::TreeNode(wd)) =
world.get_ui_widget_state_mut(*prev)
{
wd.selected = false;
}
}
}
}
}
let new_selected = if ctrl_held && multi_select {
!tree_node.selected
} else {
true
};
let accent = world
.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(color) = world.get_ui_node_color_mut(tree_node.row_entity) {
color.colors[crate::ecs::ui::state::UiSelected::INDEX] = if new_selected {
Some(selected_bg)
} else {
None
};
}
if let Some(weights) = world.get_ui_state_weights_mut(tree_node.row_entity) {
weights.weights[crate::ecs::ui::state::UiSelected::INDEX] =
if new_selected { 1.0 } else { 0.0 };
}
if let Some(UiWidgetState::TreeNode(wd)) = world.get_ui_widget_state_mut(*node_entity) {
wd.selected = new_selected;
}
let mut new_selection: Vec<freecs::Entity> = Vec::new();
for ne in &node_entities {
if let Some(UiWidgetState::TreeNode(nd)) = world.get_ui_widget_state(*ne)
&& nd.selected
{
new_selection.push(*ne);
}
}
if let Some(UiWidgetState::TreeView(wd)) = world.get_ui_widget_state_mut(tree_entity) {
wd.selected_nodes = new_selection;
wd.changed = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeSelected {
tree: tree_entity,
node: *node_entity,
selected: new_selected,
},
);
}
if arrow_clicked || row_interaction.double_clicked {
let new_expanded = !tree_node.expanded;
if new_expanded && tree_node.lazy && !tree_node.lazy_loaded {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeExpandRequested {
tree: tree_entity,
node: *node_entity,
user_data: tree_node.user_data,
},
);
} else {
world.resources.text_cache.set_text(
tree_node.arrow_text_slot,
if new_expanded { "\u{25BC}" } else { "\u{25B6}" },
);
if let Some(node) = world.get_ui_layout_node_mut(tree_node.children_container) {
node.visible = new_expanded;
}
if let Some(UiWidgetState::TreeNode(wd)) =
world.get_ui_widget_state_mut(*node_entity)
{
wd.expanded = new_expanded;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeToggled {
tree: tree_entity,
node: *node_entity,
expanded: new_expanded,
},
);
}
}
if row_interaction.right_clicked {
let mouse_position = world.resources.input.mouse.position;
if let Some(UiWidgetState::TreeView(wd)) = world.get_ui_widget_state_mut(tree_entity) {
wd.context_menu_node = Some(*node_entity);
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeContextMenu {
tree: tree_entity,
node: *node_entity,
position: mouse_position,
},
);
}
}
if focused_entity == Some(entity) || focused_entity == Some(data.content_entity) {
let visible_nodes: Vec<freecs::Entity> = node_entities
.iter()
.filter(|ne| {
let node_data = world.get_ui_widget_state(**ne).cloned();
if let Some(UiWidgetState::TreeNode(tn)) = &node_data {
let mut current_parent = tn.parent_node;
while let Some(parent_entity) = current_parent {
if let Some(UiWidgetState::TreeNode(pn)) =
world.get_ui_widget_state(parent_entity)
{
if !pn.expanded {
return false;
}
current_parent = pn.parent_node;
} else {
break;
}
}
true
} else {
false
}
})
.copied()
.collect();
let current_focused = data
.selected_nodes
.first()
.copied()
.or_else(|| visible_nodes.first().copied());
let current_index =
current_focused.and_then(|cf| visible_nodes.iter().position(|n| *n == cf));
for &(key, pressed) in frame_keys {
if !pressed {
continue;
}
let new_focus = match key {
KeyCode::ArrowDown => current_index
.map(|idx| (idx + 1).min(visible_nodes.len().saturating_sub(1)))
.or(Some(0)),
KeyCode::ArrowUp => current_index.map(|idx| idx.saturating_sub(1)).or(Some(0)),
KeyCode::ArrowRight => {
if let Some(focus) = current_focused
&& let Some(UiWidgetState::TreeNode(tn)) =
world.get_ui_widget_state(focus).cloned()
&& !tn.expanded
{
world
.resources
.text_cache
.set_text(tn.arrow_text_slot, "\u{25BC}");
if let Some(node) = world.get_ui_layout_node_mut(tn.children_container) {
node.visible = true;
}
if let Some(UiWidgetState::TreeNode(wd)) =
world.get_ui_widget_state_mut(focus)
{
wd.expanded = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeToggled {
tree: tree_entity,
node: focus,
expanded: true,
},
);
}
None
}
KeyCode::ArrowLeft => {
if let Some(focus) = current_focused
&& let Some(UiWidgetState::TreeNode(tn)) =
world.get_ui_widget_state(focus).cloned()
{
if tn.expanded {
world
.resources
.text_cache
.set_text(tn.arrow_text_slot, "\u{25B6}");
if let Some(node) = world.get_ui_layout_node_mut(tn.children_container)
{
node.visible = false;
}
if let Some(UiWidgetState::TreeNode(wd)) =
world.get_ui_widget_state_mut(focus)
{
wd.expanded = false;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeToggled {
tree: tree_entity,
node: focus,
expanded: false,
},
);
None
} else if let Some(parent) = tn.parent_node {
visible_nodes.iter().position(|n| *n == parent)
} else {
None
}
} else {
None
}
}
KeyCode::Enter | KeyCode::Space => {
if let Some(focus) = current_focused
&& let Some(UiWidgetState::TreeNode(tn)) =
world.get_ui_widget_state(focus).cloned()
{
let new_expanded = !tn.expanded;
world.resources.text_cache.set_text(
tn.arrow_text_slot,
if new_expanded { "\u{25BC}" } else { "\u{25B6}" },
);
if let Some(node) = world.get_ui_layout_node_mut(tn.children_container) {
node.visible = new_expanded;
}
if let Some(UiWidgetState::TreeNode(wd)) =
world.get_ui_widget_state_mut(focus)
{
wd.expanded = new_expanded;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeToggled {
tree: tree_entity,
node: focus,
expanded: new_expanded,
},
);
}
None
}
_ => None,
};
if let Some(new_idx) = new_focus
&& new_idx < visible_nodes.len()
{
let new_node = visible_nodes[new_idx];
if let Some(old) = current_focused
&& let Some(UiWidgetState::TreeNode(old_tn)) =
world.get_ui_widget_state(old).cloned()
{
if let Some(color) = world.get_ui_node_color_mut(old_tn.row_entity) {
color.colors[crate::ecs::ui::state::UiSelected::INDEX] = None;
}
if let Some(weights) = world.get_ui_state_weights_mut(old_tn.row_entity) {
weights.weights[crate::ecs::ui::state::UiSelected::INDEX] = 0.0;
}
if let Some(UiWidgetState::TreeNode(wd)) = world.get_ui_widget_state_mut(old) {
wd.selected = false;
}
}
let accent = world
.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::TreeNode(new_tn)) =
world.get_ui_widget_state(new_node).cloned()
{
if let Some(color) = world.get_ui_node_color_mut(new_tn.row_entity) {
color.colors[crate::ecs::ui::state::UiSelected::INDEX] = Some(selected_bg);
}
if let Some(weights) = world.get_ui_state_weights_mut(new_tn.row_entity) {
weights.weights[crate::ecs::ui::state::UiSelected::INDEX] = 1.0;
}
}
if let Some(UiWidgetState::TreeNode(wd)) = world.get_ui_widget_state_mut(new_node) {
wd.selected = true;
}
if let Some(UiWidgetState::TreeView(wd)) =
world.get_ui_widget_state_mut(tree_entity)
{
wd.selected_nodes = vec![new_node];
wd.changed = true;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TreeNodeSelected {
tree: tree_entity,
node: new_node,
selected: true,
},
);
}
}
}
}
fn handle_modal_dialog(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiModalDialogData,
frame_keys: &[(KeyCode, bool)],
) {
if !world.get_ui_layout_node(entity).is_some_and(|n| n.visible) {
if let Some(UiWidgetState::ModalDialog(widget_data)) = world.get_ui_widget_state_mut(entity)
{
widget_data.result = None;
}
return;
}
let mut result = None;
if let Some(ok) = data.ok_button
&& world.get_ui_node_interaction(ok).is_some_and(|i| i.clicked)
{
result = Some(true);
}
if let Some(cancel) = data.cancel_button
&& world
.get_ui_node_interaction(cancel)
.is_some_and(|i| i.clicked)
{
result = Some(false);
}
for (key, is_pressed) in frame_keys {
if *is_pressed && *key == KeyCode::Escape {
result = Some(false);
}
}
if let Some(confirmed) = result {
if let Some(node) = world.get_ui_layout_node_mut(data.backdrop_entity) {
node.visible = false;
}
if let Some(node) = world.get_ui_layout_node_mut(entity) {
node.visible = false;
}
if let Some(UiWidgetState::ModalDialog(widget_data)) = world.get_ui_widget_state_mut(entity)
{
widget_data.result = Some(confirmed);
}
world.resources.retained_ui.active_modal = None;
world
.resources
.retained_ui
.frame_events
.push(crate::ecs::ui::resources::UiEvent::ModalClosed { entity, confirmed });
}
}
pub fn ui_event_bubble_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
world.resources.retained_ui.bubbled_events.clear();
let events = world.resources.retained_ui.frame_events.clone();
if !world.resources.children_cache_valid {
world.validate_and_rebuild_children_cache();
}
for event in &events {
if !event.bubbles() {
continue;
}
let target = event.target_entity();
let mut current = target;
loop {
let parent_opt = world.get_parent(current).and_then(|p| p.0);
let Some(parent) = parent_opt else {
break;
};
world.resources.retained_ui.bubbled_events.push(
crate::ecs::ui::resources::BubbledUiEvent {
event: event.clone(),
target,
ancestor: parent,
stopped: false,
},
);
current = parent;
}
}
}
struct DataGridContext<'a> {
ctrl_held: bool,
shift_held: bool,
frame_keys: &'a [(KeyCode, bool)],
mouse_position: Vec2,
mouse_just_pressed: bool,
mouse_down: bool,
}
fn handle_data_grid(
world: &mut crate::ecs::world::World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiDataGridData,
ctx: &DataGridContext<'_>,
) {
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
grid.sort_changed = false;
grid.selection_changed = false;
}
let mut clicked_row = false;
for (pool_index, pool_row) in data.pool_rows.iter().enumerate() {
let row_interaction = snapshot_interaction(world, pool_row.row_entity);
if !row_interaction.clicked {
continue;
}
clicked_row = true;
let visible_row = data.visible_start + pool_index;
let effective_row_count = data
.filtered_indices
.as_ref()
.map_or(data.total_rows, |indices| indices.len());
if visible_row >= effective_row_count {
continue;
}
let data_row = visible_row;
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
grid.focused = true;
if ctx.ctrl_held {
if grid.selected_rows.contains(&data_row) {
grid.selected_rows.remove(&data_row);
} else {
grid.selected_rows.insert(data_row);
}
grid.selection_anchor = Some(data_row);
} else if ctx.shift_held
&& let Some(anchor) = grid.selection_anchor
{
let range_start = anchor.min(data_row);
let range_end = anchor.max(data_row);
grid.selected_rows.clear();
for row in range_start..=range_end {
grid.selected_rows.insert(row);
}
} else {
grid.selected_rows.clear();
grid.selected_rows.insert(data_row);
grid.selection_anchor = Some(data_row);
}
grid.selection_changed = true;
}
break;
}
if ctx.mouse_just_pressed && !clicked_row {
let grid_rect = world
.get_ui_layout_node(entity)
.map(|node| node.computed_rect);
if let Some(rect) = grid_rect {
let inside = ctx.mouse_position.x >= rect.min.x
&& ctx.mouse_position.x <= rect.max.x
&& ctx.mouse_position.y >= rect.min.y
&& ctx.mouse_position.y <= rect.max.y;
if !inside
&& let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity)
{
grid.focused = false;
}
}
}
for (column_index, header_entity) in data.header_entities.iter().enumerate() {
if column_index < data.columns.len()
&& data.columns[column_index].sortable
&& snapshot_interaction(world, *header_entity).clicked
{
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
if grid.sort_column == Some(column_index) {
if grid.sort_ascending {
grid.sort_ascending = false;
} else {
grid.sort_column = None;
}
} else {
grid.sort_column = Some(column_index);
grid.sort_ascending = true;
}
grid.sort_changed = true;
}
data_grid_update_sort_indicators(world, entity, data);
break;
}
}
for (pool_index, pool_row) in data.pool_rows.iter().enumerate() {
let row_interaction = snapshot_interaction(world, pool_row.row_entity);
if !row_interaction.double_clicked {
continue;
}
let visible_row = data.visible_start + pool_index;
let effective_row_count = data
.filtered_indices
.as_ref()
.map_or(data.total_rows, |indices| indices.len());
if visible_row >= effective_row_count {
continue;
}
let mouse_x = ctx.mouse_position.x;
let mut col_found = None;
for (col_idx, cell_entity) in pool_row.cell_entities.iter().enumerate() {
if col_idx < data.columns.len()
&& data.columns[col_idx].editable
&& let Some(cell_node) = world.get_ui_layout_node(*cell_entity)
{
let cell_rect = cell_node.computed_rect;
if mouse_x >= cell_rect.min.x && mouse_x <= cell_rect.max.x {
col_found = Some(col_idx);
break;
}
}
}
if let Some(column) = col_found {
world.ui_data_grid_start_edit(entity, visible_row, column);
}
break;
}
if let Some(editing_input) = data.editing_input_entity
&& data.editing_cell.is_some()
{
let focused = world.resources.retained_ui.focused_entity == Some(editing_input);
let mut commit = false;
let mut cancel = false;
for &(key, pressed) in ctx.frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::Enter => commit = true,
KeyCode::Escape => cancel = true,
_ => {}
}
}
if !focused && !commit && !cancel {
commit = true;
}
if commit {
world.ui_data_grid_stop_edit(entity, true);
} else if cancel {
world.ui_data_grid_stop_edit(entity, false);
}
}
handle_data_grid_column_resize(world, entity, data, ctx);
let is_focused = if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state(entity)
{
grid.focused
} else {
false
};
if is_focused && data.total_rows > 0 {
handle_data_grid_keyboard(world, entity, data, ctx);
}
update_data_grid_visibility(world, entity, data);
}
fn data_grid_update_sort_indicators(
world: &mut crate::ecs::world::World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiDataGridData,
) {
let (sort_column, sort_ascending) =
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state(entity) {
(grid.sort_column, grid.sort_ascending)
} else {
return;
};
for (index, text_slot) in data.header_text_slots.iter().enumerate() {
let base_label = &data.columns[index].label;
let display = match sort_column {
Some(col) if col == index => {
let indicator = if sort_ascending {
" \u{25B2}"
} else {
" \u{25BC}"
};
format!("{base_label}{indicator}")
}
_ => base_label.clone(),
};
world.resources.text_cache.set_text(*text_slot, &display);
}
}
fn handle_command_palette(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiCommandPaletteData,
frame_keys: &[(winit::keyboard::KeyCode, bool)],
) {
if !data.open {
if let Some(UiWidgetState::CommandPalette(palette)) = world.get_ui_widget_state_mut(entity)
{
palette.executed_command = None;
}
return;
}
let text_changed = if let Some(UiWidgetState::TextInput(ti)) =
world.get_ui_widget_state(data.text_input_entity)
{
ti.changed
} else {
false
};
if text_changed {
let new_text = if let Some(UiWidgetState::TextInput(ti)) =
world.get_ui_widget_state(data.text_input_entity)
{
ti.text.clone()
} else {
String::new()
};
let filter_lower = new_text.to_lowercase();
let new_indices: Vec<usize> = data
.commands
.iter()
.enumerate()
.filter(|(_, cmd)| {
if filter_lower.is_empty() {
return true;
}
cmd.label.to_lowercase().contains(&filter_lower)
|| cmd.category.to_lowercase().contains(&filter_lower)
})
.map(|(index, _)| index)
.collect();
if let Some(UiWidgetState::CommandPalette(palette)) = world.get_ui_widget_state_mut(entity)
{
palette.filter_text = new_text;
palette.filtered_indices = new_indices;
palette.selected_index = 0;
}
world.ui_command_palette_rebuild_results(entity);
}
let filtered_count = data.filtered_indices.len();
let mut execute_index = None;
let mut should_close = false;
for &(key, pressed) in frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::ArrowUp => {
if let Some(UiWidgetState::CommandPalette(palette)) =
world.get_ui_widget_state_mut(entity)
{
palette.selected_index = palette.selected_index.saturating_sub(1);
}
}
KeyCode::ArrowDown => {
if filtered_count > 0
&& let Some(UiWidgetState::CommandPalette(palette)) =
world.get_ui_widget_state_mut(entity)
{
palette.selected_index = (palette.selected_index + 1).min(filtered_count - 1);
}
}
KeyCode::Enter => {
if filtered_count > 0 {
let sel = if let Some(UiWidgetState::CommandPalette(palette)) =
world.get_ui_widget_state(entity)
{
palette.selected_index
} else {
0
};
if sel < filtered_count {
execute_index = Some(data.filtered_indices[sel]);
}
}
}
KeyCode::Escape => {
should_close = true;
}
_ => {}
}
}
for (pool_index, &row_entity) in data.result_entities.iter().enumerate() {
if pool_index < filtered_count {
let clicked = world
.get_ui_node_interaction(row_entity)
.map(|i| i.clicked)
.unwrap_or(false);
if clicked {
execute_index = Some(data.filtered_indices[pool_index]);
}
}
}
let selected_index =
if let Some(UiWidgetState::CommandPalette(palette)) = world.get_ui_widget_state(entity) {
palette.selected_index
} else {
0
};
let accent_color = world
.resources
.retained_ui
.theme_state
.active_theme()
.accent_color;
for (pool_index, &row_entity) in data.result_entities.iter().enumerate() {
if pool_index < filtered_count {
let is_selected = pool_index == selected_index;
let bg = if is_selected {
Vec4::new(accent_color.x, accent_color.y, accent_color.z, 0.3)
} else {
Vec4::new(0.0, 0.0, 0.0, 0.0)
};
if let Some(color) = world.get_ui_node_color_mut(row_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(bg);
}
}
}
let backdrop_clicked = world
.get_ui_node_interaction(data.backdrop_entity)
.map(|i| i.clicked)
.unwrap_or(false);
if backdrop_clicked {
should_close = true;
}
if execute_index.is_some() {
should_close = true;
}
if should_close {
if let Some(node) = world.get_ui_layout_node_mut(data.backdrop_entity) {
node.visible = false;
}
if let Some(node) = world.get_ui_layout_node_mut(entity) {
node.visible = false;
}
if let Some(UiWidgetState::CommandPalette(palette)) = world.get_ui_widget_state_mut(entity)
{
palette.open = false;
palette.executed_command = execute_index;
}
if world.resources.retained_ui.focused_entity == Some(data.text_input_entity) {
world.resources.retained_ui.focused_entity = None;
}
if let Some(cmd_index) = execute_index {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::CommandPaletteExecuted {
entity,
command_index: cmd_index,
},
);
}
}
}
fn handle_property_grid(
world: &mut World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiPropertyGridData,
mouse_position: Vec2,
mouse_just_pressed: bool,
mouse_down: bool,
) {
let currently_resizing =
if let Some(UiWidgetState::PropertyGrid(grid)) = world.get_ui_widget_state(entity) {
grid.resize_active
} else {
return;
};
if currently_resizing {
world.resources.retained_ui.requested_cursor = Some(winit::window::CursorIcon::ColResize);
if mouse_down {
let (start_x, start_width) = if let Some(UiWidgetState::PropertyGrid(grid)) =
world.get_ui_widget_state(entity)
{
(grid.resize_start_x, grid.resize_start_width)
} else {
return;
};
let grid_rect = world
.get_ui_layout_node(entity)
.map(|node| node.computed_rect);
let grid_width = grid_rect.map(|r| r.width()).unwrap_or(300.0);
let delta = mouse_position.x - start_x;
let new_width = (start_width + delta).clamp(30.0, grid_width - 50.0);
if let Some(UiWidgetState::PropertyGrid(grid)) = world.get_ui_widget_state_mut(entity) {
grid.label_width = new_width;
}
let (viewport_width, viewport_height) = world
.resources
.window
.cached_viewport_size
.map(|(w, h)| (w as f32, h as f32))
.unwrap_or((800.0, 600.0));
let grid_height = grid_rect.map(|r| r.height()).unwrap_or(0.0);
for label_entity in &data.label_entities {
if let Some(node) = world.get_ui_layout_node_mut(*label_entity)
&& let Some(ref mut child_size) = node.flow_child_size
{
let mut evaluated =
child_size.evaluate(&crate::ecs::ui::units::UiEvalContext {
parent_width: grid_width,
parent_height: grid_height,
viewport_width,
viewport_height,
font_size: 16.0,
absolute_scale: 1.0,
});
evaluated.x = new_width;
*child_size = crate::ecs::ui::units::Ab(evaluated).into();
}
}
} else if let Some(UiWidgetState::PropertyGrid(grid)) =
world.get_ui_widget_state_mut(entity)
{
grid.resize_active = false;
}
return;
}
let edge_threshold = 5.0;
let mut near_edge = false;
for label_entity in &data.label_entities {
let label_rect = world
.get_ui_layout_node(*label_entity)
.map(|node| node.computed_rect);
if let Some(rect) = label_rect {
let near_right_edge = (mouse_position.x - rect.max.x).abs() < edge_threshold
&& mouse_position.y >= rect.min.y
&& mouse_position.y <= rect.max.y;
if near_right_edge {
near_edge = true;
if mouse_just_pressed {
if let Some(UiWidgetState::PropertyGrid(grid)) =
world.get_ui_widget_state_mut(entity)
{
grid.resize_active = true;
grid.resize_start_x = mouse_position.x;
grid.resize_start_width = grid.label_width;
}
return;
}
break;
}
}
}
if near_edge {
world.resources.retained_ui.requested_cursor = Some(winit::window::CursorIcon::ColResize);
}
}
fn handle_data_grid_column_resize(
world: &mut crate::ecs::world::World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiDataGridData,
ctx: &DataGridContext<'_>,
) {
let currently_resizing =
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state(entity) {
grid.resize_column
} else {
None
};
if let Some(resize_col) = currently_resizing {
world.resources.retained_ui.requested_cursor = Some(winit::window::CursorIcon::ColResize);
if ctx.mouse_down {
let (start_x, start_width) =
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state(entity) {
(grid.resize_start_x, grid.resize_start_width)
} else {
return;
};
let delta = ctx.mouse_position.x - start_x;
let new_width = (start_width + delta).max(30.0);
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
grid.columns[resize_col].width = new_width;
}
let vp = world
.resources
.window
.cached_viewport_size
.map(|(w, h)| (w as f32, h as f32))
.unwrap_or((800.0, 600.0));
if let Some(header_entity) = data.header_entities.get(resize_col)
&& let Some(node) = world.get_ui_layout_node_mut(*header_entity)
&& let Some(ref mut child_size) = node.flow_child_size
{
let mut evaluated = child_size.evaluate(&crate::ecs::ui::units::UiEvalContext {
parent_width: vp.0,
parent_height: vp.1,
viewport_width: vp.0,
viewport_height: vp.1,
font_size: 16.0,
absolute_scale: 1.0,
});
evaluated.x = new_width;
*child_size = crate::ecs::ui::units::Ab(evaluated).into();
}
for pool_row in &data.pool_rows {
if let Some(&cell_entity) = pool_row.cell_entities.get(resize_col)
&& let Some(node) = world.get_ui_layout_node_mut(cell_entity)
&& let Some(ref mut child_size) = node.flow_child_size
{
let mut evaluated =
child_size.evaluate(&crate::ecs::ui::units::UiEvalContext {
parent_width: vp.0,
parent_height: vp.1,
viewport_width: vp.0,
viewport_height: vp.1,
font_size: 16.0,
absolute_scale: 1.0,
});
evaluated.x = new_width;
*child_size = crate::ecs::ui::units::Ab(evaluated).into();
}
}
} else if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
grid.resize_column = None;
}
return;
}
let edge_threshold = 5.0;
let mut near_any_edge = false;
for (column_index, header_entity) in data.header_entities.iter().enumerate() {
let header_rect = world
.get_ui_layout_node(*header_entity)
.map(|node| node.computed_rect);
if let Some(rect) = header_rect {
let near_right_edge = (ctx.mouse_position.x - rect.max.x).abs() < edge_threshold
&& ctx.mouse_position.y >= rect.min.y
&& ctx.mouse_position.y <= rect.max.y;
if near_right_edge {
near_any_edge = true;
if ctx.mouse_just_pressed {
if let Some(UiWidgetState::DataGrid(grid)) =
world.get_ui_widget_state_mut(entity)
{
grid.resize_column = Some(column_index);
grid.resize_start_x = ctx.mouse_position.x;
grid.resize_start_width = data.columns[column_index].width;
}
return;
}
break;
}
}
}
if near_any_edge {
world.resources.retained_ui.requested_cursor = Some(winit::window::CursorIcon::ColResize);
}
}
fn handle_data_grid_keyboard(
world: &mut crate::ecs::world::World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiDataGridData,
ctx: &DataGridContext<'_>,
) {
let total_rows = data
.filtered_indices
.as_ref()
.map_or(data.total_rows, |indices| indices.len());
if total_rows == 0 {
return;
}
let current_anchor =
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state(entity) {
grid.selection_anchor
} else {
return;
};
let anchor = current_anchor.unwrap_or(0);
let mut new_anchor = None;
for &(key, pressed) in ctx.frame_keys {
if !pressed {
continue;
}
match key {
KeyCode::ArrowUp => {
new_anchor = Some(anchor.saturating_sub(1));
}
KeyCode::ArrowDown => {
new_anchor = Some((anchor + 1).min(total_rows - 1));
}
KeyCode::PageUp => {
new_anchor = Some(anchor.saturating_sub(data.pool_size));
}
KeyCode::PageDown => {
new_anchor = Some((anchor + data.pool_size).min(total_rows - 1));
}
KeyCode::Home => {
new_anchor = Some(0);
}
KeyCode::End => {
new_anchor = Some(total_rows - 1);
}
KeyCode::KeyA if ctx.ctrl_held => {
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
grid.selected_rows.clear();
for row in 0..total_rows {
grid.selected_rows.insert(row);
}
grid.selection_changed = true;
}
return;
}
_ => {}
}
}
if let Some(target) = new_anchor {
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
if ctx.shift_held {
let range_start = anchor.min(target);
let range_end = anchor.max(target);
grid.selected_rows.clear();
for row in range_start..=range_end {
grid.selected_rows.insert(row);
}
} else {
grid.selected_rows.clear();
grid.selected_rows.insert(target);
grid.selection_anchor = Some(target);
}
grid.selection_changed = true;
}
let row_height = data.row_height;
let target_offset = target as f32 * row_height;
if let Some(UiWidgetState::ScrollArea(scroll_data)) =
world.get_ui_widget_state(data.scroll_entity)
{
let scroll_offset = scroll_data.scroll_offset;
let visible_height = scroll_data.visible_height;
if target_offset < scroll_offset {
if let Some(UiWidgetState::ScrollArea(scroll_data)) =
world.get_ui_widget_state_mut(data.scroll_entity)
{
scroll_data.scroll_offset = target_offset;
}
} else if target_offset + row_height > scroll_offset + visible_height
&& let Some(UiWidgetState::ScrollArea(scroll_data)) =
world.get_ui_widget_state_mut(data.scroll_entity)
{
scroll_data.scroll_offset = target_offset + row_height - visible_height;
}
}
}
}
fn update_data_grid_visibility(
world: &mut crate::ecs::world::World,
entity: freecs::Entity,
data: &crate::ecs::ui::components::UiDataGridData,
) {
let scroll_offset = if let Some(UiWidgetState::ScrollArea(scroll_data)) =
world.get_ui_widget_state(data.scroll_entity)
{
scroll_data.scroll_offset
} else {
0.0
};
let effective_row_count = data
.filtered_indices
.as_ref()
.map_or(data.total_rows, |indices| indices.len());
let row_height = data.row_height;
let visible_start = if row_height > 0.0 {
(scroll_offset / row_height).floor() as usize
} else {
0
};
let visible_start = visible_start.min(effective_row_count.saturating_sub(data.pool_size));
let top_height = visible_start as f32 * row_height;
if let Some(node) = world.get_ui_layout_node_mut(data.top_spacer) {
node.flow_child_size =
Some(crate::ecs::ui::units::Ab(nalgebra_glm::Vec2::new(0.0, top_height)).into());
}
let visible_end = (visible_start + data.pool_size).min(effective_row_count);
let bottom_rows = effective_row_count.saturating_sub(visible_end);
let bottom_height = bottom_rows as f32 * row_height;
if let Some(node) = world.get_ui_layout_node_mut(data.bottom_spacer) {
node.flow_child_size =
Some(crate::ecs::ui::units::Ab(nalgebra_glm::Vec2::new(0.0, bottom_height)).into());
}
let selected_rows =
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state(entity) {
grid.selected_rows.clone()
} else {
std::collections::HashSet::new()
};
for (pool_index, pool_row) in data.pool_rows.iter().enumerate() {
let visible_row = visible_start + pool_index;
let visible = visible_row < effective_row_count;
if let Some(node) = world.get_ui_layout_node_mut(pool_row.row_entity) {
node.visible = visible;
}
if visible {
let is_selected = selected_rows.contains(&visible_row);
if let Some(weights) = world.get_ui_state_weights_mut(pool_row.row_entity) {
weights.weights[crate::ecs::ui::state::UiSelected::INDEX] =
if is_selected { 1.0 } else { 0.0 };
}
}
}
if let Some(UiWidgetState::DataGrid(grid)) = world.get_ui_widget_state_mut(entity) {
grid.visible_start = visible_start;
}
}
#[allow(clippy::too_many_arguments)]
fn handle_text_area(
world: &mut World,
entity: freecs::Entity,
interaction: &InteractionSnapshot,
data: &crate::ecs::ui::components::UiTextAreaData,
focused_entity: Option<freecs::Entity>,
frame_chars: &[char],
frame_keys: &[(KeyCode, bool)],
ctrl_held: bool,
shift_held: bool,
mouse_position: Vec2,
current_time: f64,
dpi_scale: f32,
) {
let is_focused = focused_entity == Some(entity);
let mut text = data.text.clone();
let mut cursor_position = data.cursor_position;
let mut selection_start = data.selection_start;
let mut changed = false;
let mut cursor_blink_timer = data.cursor_blink_timer;
let mut scroll_offset_y = data.scroll_offset_y;
let mut clear_focus = false;
let line_height = data.line_height;
let mut undo_stack = data.undo_stack.clone();
let mut needs_snapshot = false;
let max_length = data.max_length;
if is_focused {
for character in frame_chars {
if *character >= ' ' {
if let Some(max) = max_length {
let result_len = if let Some(sel) = selection_start {
let selected = sel.max(cursor_position) - sel.min(cursor_position);
text.chars().count() - selected + 1
} else {
text.chars().count() + 1
};
if result_len > max {
continue;
}
}
if !needs_snapshot {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
needs_snapshot = true;
}
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.push(*character);
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min + 1;
selection_start = None;
} else {
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.push(*character);
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position += 1;
}
changed = true;
cursor_blink_timer = current_time;
}
}
if needs_snapshot {
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
for (key, is_pressed) in frame_keys {
if !is_pressed {
continue;
}
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
match key {
KeyCode::Backspace => {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
let mut did_change = false;
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min;
selection_start = None;
did_change = true;
} else if cursor_position > 0 {
if ctrl_held {
let new_pos = prev_word_boundary(&text, cursor_position);
let mut new_text: String = chars[..new_pos].iter().collect();
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position = new_pos;
} else {
let mut new_text: String =
chars[..cursor_position - 1].iter().collect();
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position -= 1;
}
did_change = true;
}
if did_change {
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
changed = true;
}
cursor_blink_timer = current_time;
}
KeyCode::Delete => {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
let mut did_change = false;
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min;
selection_start = None;
did_change = true;
} else if cursor_position < len {
if ctrl_held {
let end_pos = next_word_boundary(&text, cursor_position);
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[end_pos..].iter());
text = new_text;
} else {
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[cursor_position + 1..].iter());
text = new_text;
}
did_change = true;
}
if did_change {
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
changed = true;
}
cursor_blink_timer = current_time;
}
KeyCode::Enter => {
if ctrl_held {
clear_focus = true;
} else {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars_vec: Vec<char> = text.chars().collect();
let mut new_text: String = chars_vec[..min].iter().collect();
new_text.push('\n');
new_text.extend(chars_vec[max..].iter());
text = new_text;
cursor_position = min + 1;
selection_start = None;
} else {
let chars_vec: Vec<char> = text.chars().collect();
let mut new_text: String =
chars_vec[..cursor_position].iter().collect();
new_text.push('\n');
new_text.extend(chars_vec[cursor_position..].iter());
text = new_text;
cursor_position += 1;
}
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
changed = true;
cursor_blink_timer = current_time;
}
}
KeyCode::ArrowLeft => {
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let min = selection_start.unwrap().min(cursor_position);
cursor_position = min;
selection_start = None;
cursor_blink_timer = current_time;
continue;
}
if ctrl_held {
cursor_position = prev_word_boundary(&text, cursor_position);
} else {
cursor_position = cursor_position.saturating_sub(1);
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::ArrowRight => {
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let max = selection_start.unwrap().max(cursor_position);
cursor_position = max;
selection_start = None;
cursor_blink_timer = current_time;
continue;
}
if ctrl_held {
cursor_position = next_word_boundary(&text, cursor_position);
} else if cursor_position < len {
cursor_position += 1;
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::ArrowUp => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, cur_col) = line_col_from_char_position(&text, cursor_position);
if cur_line > 0 {
cursor_position = char_position_from_line_col(&text, cur_line - 1, cur_col);
} else {
cursor_position = 0;
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::ArrowDown => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, cur_col) = line_col_from_char_position(&text, cursor_position);
let total_lines = line_count(&text);
if cur_line + 1 < total_lines {
cursor_position = char_position_from_line_col(&text, cur_line + 1, cur_col);
} else {
cursor_position = len;
}
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::Home => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, _) = line_col_from_char_position(&text, cursor_position);
cursor_position = line_start_char_index(&text, cur_line);
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::End => {
if shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, _) = line_col_from_char_position(&text, cursor_position);
let lt = line_text(&text, cur_line);
cursor_position = line_start_char_index(&text, cur_line) + lt.chars().count();
if !shift_held {
selection_start = None;
}
cursor_blink_timer = current_time;
}
KeyCode::KeyA if ctrl_held => {
selection_start = Some(0);
cursor_position = len;
cursor_blink_timer = current_time;
}
KeyCode::KeyZ if ctrl_held => {
if let Some(snapshot) = undo_stack.undo() {
text = snapshot.text.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
changed = true;
cursor_blink_timer = current_time;
}
}
KeyCode::KeyY if ctrl_held => {
if let Some(snapshot) = undo_stack.redo() {
text = snapshot.text.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
changed = true;
cursor_blink_timer = current_time;
}
}
KeyCode::KeyC if ctrl_held => {
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let selected: String = text.chars().skip(min).take(max - min).collect();
world.resources.retained_ui.clipboard_text = selected;
}
}
KeyCode::KeyX if ctrl_held => {
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let selected: String = text.chars().skip(min).take(max - min).collect();
world.resources.retained_ui.clipboard_text = selected;
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min;
selection_start = None;
changed = true;
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
}
KeyCode::KeyV if ctrl_held => {
let mut paste_text = world.resources.retained_ui.clipboard_text.clone();
if !paste_text.is_empty() {
if let Some(max) = max_length {
let existing = text.chars().count();
let removed = selection_start
.map(|s| s.max(cursor_position) - s.min(cursor_position))
.unwrap_or(0);
let available = max.saturating_sub(existing - removed);
paste_text = paste_text.chars().take(available).collect();
}
if !paste_text.is_empty() {
undo_stack.push_initial(crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
});
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.push_str(&paste_text);
new_text.extend(chars[max..].iter());
text = new_text;
cursor_position = min + paste_text.chars().count();
selection_start = None;
} else {
let chars: Vec<char> = text.chars().collect();
let mut new_text: String =
chars[..cursor_position].iter().collect();
new_text.push_str(&paste_text);
new_text.extend(chars[cursor_position..].iter());
text = new_text;
cursor_position += paste_text.chars().count();
}
changed = true;
undo_stack.push(
crate::ecs::ui::components::TextSnapshot {
text: text.clone(),
cursor_position,
selection_start,
},
current_time,
);
}
}
}
KeyCode::Escape => {
selection_start = None;
clear_focus = true;
}
_ => {}
}
}
if interaction.clicked {
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let local_y =
(mouse_position.y - rect.min.y) / dpi_scale - 8.0 + scroll_offset_y;
let clicked_line = (local_y / line_height).max(0.0) as usize;
let total_lines = line_count(&text);
let target_line = clicked_line.min(total_lines.saturating_sub(1));
let lt = line_text(&text, target_line);
let local_x = (mouse_position.x - rect.min.x) / dpi_scale - 8.0;
let col = byte_index_at_x(&atlas, lt, font_size, local_x);
let line_start = line_start_char_index(&text, target_line);
if shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else {
selection_start = None;
}
cursor_position = line_start + col;
cursor_blink_timer = current_time;
}
}
}
if interaction.double_clicked {
let len = text.chars().count();
let pos = cursor_position.min(len);
let word_start = prev_word_boundary(&text, pos);
let word_end = next_word_boundary(&text, pos).min(len);
selection_start = Some(word_start);
cursor_position = word_end;
}
} else {
selection_start = None;
}
if clear_focus {
world.resources.retained_ui.focused_entity = None;
}
if changed {
world.resources.text_cache.set_text(data.text_slot, &text);
#[cfg(feature = "syntax_highlighting")]
if let Some(language) = &data.syntax_language {
crate::ecs::ui::syntax::highlight_text_area(world, &text, language, data.text_slot);
}
}
let cursor_visible = is_focused && ((current_time - cursor_blink_timer) % 1.0) < 0.5;
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity) {
cursor_node.visible = cursor_visible;
}
let (cur_line, cur_col) = line_col_from_char_position(&text, cursor_position);
let visible_rows = data.visible_rows;
let cursor_y = cur_line as f32 * line_height;
let visible_height = visible_rows as f32 * line_height;
if cursor_y - scroll_offset_y >= visible_height {
scroll_offset_y = cursor_y - visible_height + line_height;
} else if cursor_y < scroll_offset_y {
scroll_offset_y = cursor_y;
}
let total_lines = line_count(&text);
let max_scroll = ((total_lines as f32 * line_height) - visible_height).max(0.0);
scroll_offset_y = scroll_offset_y.clamp(0.0, max_scroll);
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let cur_line_text = line_text(&text, cur_line);
let text_before_cursor: String = cur_line_text.chars().take(cur_col).collect();
let cursor_x = measure_text_width(&atlas, &text_before_cursor, font_size);
let cursor_screen_y = cur_line as f32 * line_height - scroll_offset_y;
let has_selection =
is_focused && selection_start.is_some() && selection_start != Some(cursor_position);
let sel_positions: Vec<(f32, f32, f32)> = if has_selection {
let sel_start = selection_start.unwrap();
let sel_min = sel_start.min(cursor_position);
let sel_max = sel_start.max(cursor_position);
let (min_line, min_col) = line_col_from_char_position(&text, sel_min);
let (max_line, max_col) = line_col_from_char_position(&text, sel_max);
let mut positions = Vec::new();
for line_idx in min_line..=max_line {
let lt = line_text(&text, line_idx);
let start_col = if line_idx == min_line { min_col } else { 0 };
let end_col = if line_idx == max_line {
max_col
} else {
lt.chars().count()
};
let start_text: String = lt.chars().take(start_col).collect();
let end_text: String = lt.chars().take(end_col).collect();
let sx = measure_text_width(&atlas, &start_text, font_size);
let ex = measure_text_width(&atlas, &end_text, font_size);
let sy = line_idx as f32 * line_height - scroll_offset_y;
positions.push((sx, ex - sx, sy));
}
positions
} else {
Vec::new()
};
drop(atlas);
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
cursor_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + cursor_x, 8.0 + cursor_screen_y))
.into();
}
for (pool_index, sel_entity) in data.selection_pool.iter().enumerate() {
if pool_index < sel_positions.len() {
let (sx, width, sy) = sel_positions[pool_index];
if let Some(sel_node) = world.get_ui_layout_node_mut(*sel_entity) {
sel_node.visible = true;
if let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
sel_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + sx, 8.0 + sy)).into();
window.size =
crate::ecs::ui::units::Ab(Vec2::new(width.max(2.0), line_height))
.into();
}
}
} else if let Some(sel_node) = world.get_ui_layout_node_mut(*sel_entity) {
sel_node.visible = false;
}
}
let _ = rect;
}
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TextAreaChanged {
entity,
text: text.clone(),
},
);
}
if clear_focus {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TextInputSubmitted {
entity,
text: text.clone(),
},
);
}
let text_is_empty = text.is_empty();
let placeholder_entity =
if let Some(UiWidgetState::TextArea(widget_data)) = world.get_ui_widget_state_mut(entity) {
widget_data.text = text;
widget_data.cursor_position = cursor_position;
widget_data.selection_start = selection_start;
widget_data.changed = changed;
widget_data.cursor_blink_timer = cursor_blink_timer;
widget_data.scroll_offset_y = scroll_offset_y;
widget_data.undo_stack = undo_stack;
widget_data.placeholder_entity
} else {
None
};
if let Some(ph_entity) = placeholder_entity
&& let Some(node) = world.get_ui_layout_node_mut(ph_entity)
{
node.visible = text_is_empty;
}
}
struct RichTextEditorContext<'a> {
entity: freecs::Entity,
interaction: &'a InteractionSnapshot,
data: &'a crate::ecs::ui::components::UiRichTextEditorData,
focused_entity: Option<freecs::Entity>,
frame_chars: &'a [char],
frame_keys: &'a [(KeyCode, bool)],
ctrl_held: bool,
shift_held: bool,
mouse_position: Vec2,
current_time: f64,
dpi_scale: f32,
}
fn handle_rich_text_editor(world: &mut World, context: RichTextEditorContext<'_>) {
let entity = context.entity;
let is_focused = context.focused_entity == Some(entity);
let data = context.data;
let mut text = data.text.clone();
let mut char_styles = data.char_styles.clone();
let mut current_style = data.current_style.clone();
let mut cursor_position = data.cursor_position;
let mut selection_start = data.selection_start;
let mut changed = false;
let mut cursor_blink_timer = data.cursor_blink_timer;
let mut scroll_offset_y = data.scroll_offset_y;
let mut clear_focus = false;
let line_height = data.line_height;
let mut undo_stack = data.undo_stack.clone();
let mut needs_snapshot = false;
if is_focused {
for character in context.frame_chars {
if *character >= ' ' {
if !needs_snapshot {
undo_stack.push_initial(crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
});
needs_snapshot = true;
}
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..min].iter().collect();
new_text.push(*character);
new_text.extend(chars[max..].iter());
text = new_text;
char_styles.drain(min..max);
char_styles.insert(min, current_style.clone());
cursor_position = min + 1;
selection_start = None;
} else {
let chars: Vec<char> = text.chars().collect();
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.push(*character);
new_text.extend(chars[cursor_position..].iter());
text = new_text;
char_styles.insert(cursor_position, current_style.clone());
cursor_position += 1;
}
changed = true;
cursor_blink_timer = context.current_time;
}
}
if needs_snapshot {
undo_stack.push(
crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
},
context.current_time,
);
}
for (key, is_pressed) in context.frame_keys {
if !is_pressed {
continue;
}
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
match key {
KeyCode::Backspace => {
undo_stack.push_initial(crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
});
let mut did_change = false;
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
char_styles.drain(min..max);
cursor_position = min;
selection_start = None;
did_change = true;
} else if cursor_position > 0 {
if context.ctrl_held {
let new_pos = prev_word_boundary(&text, cursor_position);
let mut new_text: String = chars[..new_pos].iter().collect();
new_text.extend(chars[cursor_position..].iter());
text = new_text;
char_styles.drain(new_pos..cursor_position);
cursor_position = new_pos;
} else {
let mut new_text: String =
chars[..cursor_position - 1].iter().collect();
new_text.extend(chars[cursor_position..].iter());
text = new_text;
char_styles.remove(cursor_position - 1);
cursor_position -= 1;
}
did_change = true;
}
if did_change {
undo_stack.push(
crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
},
context.current_time,
);
changed = true;
}
cursor_blink_timer = context.current_time;
}
KeyCode::Delete => {
undo_stack.push_initial(crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
});
let mut did_change = false;
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let mut new_text: String = chars[..min].iter().collect();
new_text.extend(chars[max..].iter());
text = new_text;
char_styles.drain(min..max);
cursor_position = min;
selection_start = None;
did_change = true;
} else if cursor_position < len {
if context.ctrl_held {
let end_pos = next_word_boundary(&text, cursor_position);
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[end_pos..].iter());
text = new_text;
char_styles.drain(cursor_position..end_pos);
} else {
let mut new_text: String = chars[..cursor_position].iter().collect();
new_text.extend(chars[cursor_position + 1..].iter());
text = new_text;
char_styles.remove(cursor_position);
}
did_change = true;
}
if did_change {
undo_stack.push(
crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
},
context.current_time,
);
changed = true;
}
cursor_blink_timer = context.current_time;
}
KeyCode::Enter => {
if context.ctrl_held {
clear_focus = true;
} else {
undo_stack.push_initial(crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
});
if let Some(sel_start) = selection_start {
let min = sel_start.min(cursor_position);
let max = sel_start.max(cursor_position);
let chars_vec: Vec<char> = text.chars().collect();
let mut new_text: String = chars_vec[..min].iter().collect();
new_text.push('\n');
new_text.extend(chars_vec[max..].iter());
text = new_text;
char_styles.drain(min..max);
char_styles.insert(min, current_style.clone());
cursor_position = min + 1;
selection_start = None;
} else {
let chars_vec: Vec<char> = text.chars().collect();
let mut new_text: String =
chars_vec[..cursor_position].iter().collect();
new_text.push('\n');
new_text.extend(chars_vec[cursor_position..].iter());
text = new_text;
char_styles.insert(cursor_position, current_style.clone());
cursor_position += 1;
}
undo_stack.push(
crate::ecs::ui::components::RichTextSnapshot {
text: text.clone(),
char_styles: char_styles.clone(),
cursor_position,
selection_start,
},
context.current_time,
);
changed = true;
cursor_blink_timer = context.current_time;
}
}
KeyCode::ArrowLeft => {
if context.shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let min = selection_start.unwrap().min(cursor_position);
cursor_position = min;
selection_start = None;
cursor_blink_timer = context.current_time;
continue;
}
if context.ctrl_held {
cursor_position = prev_word_boundary(&text, cursor_position);
} else {
cursor_position = cursor_position.saturating_sub(1);
}
if !context.shift_held {
selection_start = None;
}
cursor_blink_timer = context.current_time;
}
KeyCode::ArrowRight => {
if context.shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else if selection_start.is_some() {
let max = selection_start.unwrap().max(cursor_position);
cursor_position = max;
selection_start = None;
cursor_blink_timer = context.current_time;
continue;
}
if context.ctrl_held {
cursor_position = next_word_boundary(&text, cursor_position);
} else if cursor_position < len {
cursor_position += 1;
}
if !context.shift_held {
selection_start = None;
}
cursor_blink_timer = context.current_time;
}
KeyCode::ArrowUp => {
if context.shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, cur_col) = line_col_from_char_position(&text, cursor_position);
if cur_line > 0 {
cursor_position = char_position_from_line_col(&text, cur_line - 1, cur_col);
} else {
cursor_position = 0;
}
if !context.shift_held {
selection_start = None;
}
cursor_blink_timer = context.current_time;
}
KeyCode::ArrowDown => {
if context.shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, cur_col) = line_col_from_char_position(&text, cursor_position);
let total_lines = line_count(&text);
if cur_line + 1 < total_lines {
cursor_position = char_position_from_line_col(&text, cur_line + 1, cur_col);
} else {
cursor_position = len;
}
if !context.shift_held {
selection_start = None;
}
cursor_blink_timer = context.current_time;
}
KeyCode::Home => {
if context.shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, _) = line_col_from_char_position(&text, cursor_position);
cursor_position = line_start_char_index(&text, cur_line);
if !context.shift_held {
selection_start = None;
}
cursor_blink_timer = context.current_time;
}
KeyCode::End => {
if context.shift_held && selection_start.is_none() {
selection_start = Some(cursor_position);
}
let (cur_line, _) = line_col_from_char_position(&text, cursor_position);
let lt = line_text(&text, cur_line);
cursor_position = line_start_char_index(&text, cur_line) + lt.chars().count();
if !context.shift_held {
selection_start = None;
}
cursor_blink_timer = context.current_time;
}
KeyCode::KeyA if context.ctrl_held => {
selection_start = Some(0);
cursor_position = len;
cursor_blink_timer = context.current_time;
}
KeyCode::KeyB if context.ctrl_held => {
toggle_selection_style(
&mut char_styles,
&mut current_style,
selection_start,
cursor_position,
|s| &mut s.bold,
);
changed = true;
}
KeyCode::KeyI if context.ctrl_held => {
toggle_selection_style(
&mut char_styles,
&mut current_style,
selection_start,
cursor_position,
|s| &mut s.italic,
);
changed = true;
}
KeyCode::KeyU if context.ctrl_held => {
toggle_selection_style(
&mut char_styles,
&mut current_style,
selection_start,
cursor_position,
|s| &mut s.underline,
);
changed = true;
}
KeyCode::KeyZ if context.ctrl_held => {
if let Some(snapshot) = undo_stack.undo() {
text = snapshot.text.clone();
char_styles = snapshot.char_styles.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
changed = true;
cursor_blink_timer = context.current_time;
}
}
KeyCode::KeyY if context.ctrl_held => {
if let Some(snapshot) = undo_stack.redo() {
text = snapshot.text.clone();
char_styles = snapshot.char_styles.clone();
cursor_position = snapshot.cursor_position;
selection_start = snapshot.selection_start;
changed = true;
cursor_blink_timer = context.current_time;
}
}
KeyCode::Escape => {
selection_start = None;
clear_focus = true;
}
_ => {}
}
}
if context.interaction.clicked {
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let local_y = (context.mouse_position.y - rect.min.y) / context.dpi_scale - 8.0
+ scroll_offset_y;
let clicked_line = (local_y / line_height).max(0.0) as usize;
let total_lines = line_count(&text);
let target_line = clicked_line.min(total_lines.saturating_sub(1));
let lt = line_text(&text, target_line);
let local_x = (context.mouse_position.x - rect.min.x) / context.dpi_scale - 8.0;
let col = byte_index_at_x(&atlas, lt, font_size, local_x);
let line_start = line_start_char_index(&text, target_line);
if context.shift_held {
if selection_start.is_none() {
selection_start = Some(cursor_position);
}
} else {
selection_start = None;
}
cursor_position = line_start + col;
cursor_blink_timer = context.current_time;
}
}
}
if context.interaction.double_clicked {
let len = text.chars().count();
let pos = cursor_position.min(len);
let word_start = prev_word_boundary(&text, pos);
let word_end = next_word_boundary(&text, pos).min(len);
selection_start = Some(word_start);
cursor_position = word_end;
}
} else {
selection_start = None;
}
if clear_focus {
world.resources.retained_ui.focused_entity = None;
}
if changed {
world.resources.text_cache.set_text(data.text_slot, &text);
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();
world
.resources
.retained_ui
.text_slot_character_colors
.insert(data.text_slot, colors);
} else {
world
.resources
.retained_ui
.text_slot_character_colors
.remove(&data.text_slot);
}
}
let cursor_visible = is_focused && ((context.current_time - cursor_blink_timer) % 1.0) < 0.5;
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity) {
cursor_node.visible = cursor_visible;
}
let (cur_line, cur_col) = line_col_from_char_position(&text, cursor_position);
let visible_rows = data.visible_rows;
let cursor_y = cur_line as f32 * line_height;
let visible_height = visible_rows as f32 * line_height;
if cursor_y - scroll_offset_y >= visible_height {
scroll_offset_y = cursor_y - visible_height + line_height;
} else if cursor_y < scroll_offset_y {
scroll_offset_y = cursor_y;
}
let total_lines = line_count(&text);
let max_scroll = ((total_lines as f32 * line_height) - visible_height).max(0.0);
scroll_offset_y = scroll_offset_y.clamp(0.0, max_scroll);
let input_rect = world.get_ui_layout_node(entity).map(|n| n.computed_rect);
if let Some(rect) = input_rect {
let font_size = world
.resources
.retained_ui
.theme_state
.active_theme()
.font_size;
let best_idx = world
.resources
.text_cache
.font_manager
.best_bitmap_font_for_size(font_size);
let font_arc = world
.resources
.text_cache
.font_manager
.get_bitmap_font_arc(best_idx);
if let Some(atlas) = font_arc {
let cur_line_text = line_text(&text, cur_line);
let text_before_cursor: String = cur_line_text.chars().take(cur_col).collect();
let cursor_x = measure_text_width(&atlas, &text_before_cursor, font_size);
let cursor_screen_y = cur_line as f32 * line_height - scroll_offset_y;
let has_selection =
is_focused && selection_start.is_some() && selection_start != Some(cursor_position);
let sel_positions: Vec<(f32, f32, f32)> = if has_selection {
let sel_start = selection_start.unwrap();
let sel_min = sel_start.min(cursor_position);
let sel_max = sel_start.max(cursor_position);
let (min_line, min_col) = line_col_from_char_position(&text, sel_min);
let (max_line, max_col) = line_col_from_char_position(&text, sel_max);
let mut positions = Vec::new();
for line_idx in min_line..=max_line {
let lt = line_text(&text, line_idx);
let start_col = if line_idx == min_line { min_col } else { 0 };
let end_col = if line_idx == max_line {
max_col
} else {
lt.chars().count()
};
let start_text: String = lt.chars().take(start_col).collect();
let end_text: String = lt.chars().take(end_col).collect();
let sx = measure_text_width(&atlas, &start_text, font_size);
let ex = measure_text_width(&atlas, &end_text, font_size);
let sy = line_idx as f32 * line_height - scroll_offset_y;
positions.push((sx, ex - sx, sy));
}
positions
} else {
Vec::new()
};
drop(atlas);
if let Some(cursor_node) = world.get_ui_layout_node_mut(data.cursor_entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
cursor_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + cursor_x, 8.0 + cursor_screen_y))
.into();
}
for (pool_index, sel_entity) in data.selection_pool.iter().enumerate() {
if pool_index < sel_positions.len() {
let (sx, width, sy) = sel_positions[pool_index];
if let Some(sel_node) = world.get_ui_layout_node_mut(*sel_entity) {
sel_node.visible = true;
if let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
sel_node.layouts[crate::ecs::ui::state::UiBase::INDEX].as_mut()
{
window.position =
crate::ecs::ui::units::Ab(Vec2::new(8.0 + sx, 8.0 + sy)).into();
window.size =
crate::ecs::ui::units::Ab(Vec2::new(width.max(2.0), line_height))
.into();
}
}
} else if let Some(sel_node) = world.get_ui_layout_node_mut(*sel_entity) {
sel_node.visible = false;
}
}
let _ = rect;
}
}
if changed {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::RichTextEditorChanged {
entity,
text: text.clone(),
},
);
}
if clear_focus {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TextInputSubmitted {
entity,
text: text.clone(),
},
);
}
let placeholder_update = if let Some(UiWidgetState::RichTextEditor(widget_data)) =
world.get_ui_widget_state_mut(entity)
{
let text_is_empty = text.is_empty();
widget_data.text = text;
widget_data.char_styles = char_styles;
widget_data.current_style = current_style;
widget_data.cursor_position = cursor_position;
widget_data.selection_start = selection_start;
widget_data.changed = changed;
widget_data.cursor_blink_timer = cursor_blink_timer;
widget_data.scroll_offset_y = scroll_offset_y;
widget_data.undo_stack = undo_stack;
widget_data
.placeholder_entity
.filter(|_| changed)
.map(|ph| (ph, text_is_empty))
} else {
None
};
if let Some((ph_entity, text_is_empty)) = placeholder_update
&& let Some(node) = world.get_ui_layout_node_mut(ph_entity)
{
node.visible = text_is_empty;
}
}
fn toggle_selection_style(
char_styles: &mut [crate::ecs::ui::components::CharStyle],
current_style: &mut crate::ecs::ui::components::CharStyle,
selection_start: Option<usize>,
cursor_position: usize,
accessor: fn(&mut crate::ecs::ui::components::CharStyle) -> &mut bool,
) {
if let Some(sel_start) = selection_start {
let start = sel_start.min(cursor_position);
let end = sel_start.max(cursor_position);
let all_set = (start..end).all(|index| {
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 < char_styles.len() {
*accessor(&mut char_styles[index]) = new_value;
}
}
*accessor(current_style) = new_value;
} else {
let field = accessor(current_style);
*field = !*field;
}
}
fn compute_tile_rects(
data: &mut UiTileContainerData,
tile_id: TileId,
available: crate::ecs::ui::types::Rect,
) {
if tile_id.0 >= data.rects.len() {
data.rects
.resize(tile_id.0 + 1, crate::ecs::ui::types::Rect::default());
}
data.rects[tile_id.0] = available;
let Some(node) = data.tiles[tile_id.0].clone() else {
return;
};
match node {
TileNode::Pane { .. } => {}
TileNode::Split {
direction,
ratio,
children,
} => {
let splitter = data.splitter_width;
match direction {
SplitDirection::Horizontal => {
let total_width = available.width() - splitter;
let left_width = total_width * ratio;
let left_rect = crate::ecs::ui::types::Rect {
min: available.min,
max: Vec2::new(available.min.x + left_width, available.max.y),
};
let right_rect = crate::ecs::ui::types::Rect {
min: Vec2::new(available.min.x + left_width + splitter, available.min.y),
max: available.max,
};
compute_tile_rects(data, children[0], left_rect);
compute_tile_rects(data, children[1], right_rect);
}
SplitDirection::Vertical => {
let total_height = available.height() - splitter;
let top_height = total_height * ratio;
let top_rect = crate::ecs::ui::types::Rect {
min: available.min,
max: Vec2::new(available.max.x, available.min.y + top_height),
};
let bottom_rect = crate::ecs::ui::types::Rect {
min: Vec2::new(available.min.x, available.min.y + top_height + splitter),
max: available.max,
};
compute_tile_rects(data, children[0], top_rect);
compute_tile_rects(data, children[1], bottom_rect);
}
}
}
TileNode::Tabs { panes, active } => {
let tab_bar_h = data.tab_bar_height;
let content_rect = crate::ecs::ui::types::Rect {
min: Vec2::new(available.min.x, available.min.y + tab_bar_h),
max: available.max,
};
for (index, &pane_id) in panes.iter().enumerate() {
if pane_id.0 >= data.rects.len() {
data.rects
.resize(pane_id.0 + 1, crate::ecs::ui::types::Rect::default());
}
if index == active {
compute_tile_rects(data, pane_id, content_rect);
} else {
data.rects[pane_id.0] = content_rect;
}
}
}
}
}
fn tile_hit_test_splitter(data: &UiTileContainerData, mouse: Vec2) -> Option<TileId> {
for (index, tile) in data.tiles.iter().enumerate() {
if let Some(TileNode::Split {
direction,
children,
..
}) = tile
{
let Some(child0_rect) = data.rects.get(children[0].0) else {
continue;
};
let splitter_half = data.splitter_width * 0.5 + 2.0;
let parent_rect = &data.rects[index];
let hit = match direction {
SplitDirection::Horizontal => {
let boundary = child0_rect.max.x + data.splitter_width * 0.5;
(mouse.x - boundary).abs() < splitter_half
&& mouse.y >= parent_rect.min.y
&& mouse.y <= parent_rect.max.y
}
SplitDirection::Vertical => {
let boundary = child0_rect.max.y + data.splitter_width * 0.5;
(mouse.y - boundary).abs() < splitter_half
&& mouse.x >= parent_rect.min.x
&& mouse.x <= parent_rect.max.x
}
};
if hit {
return Some(TileId(index));
}
}
}
None
}
fn tile_hit_test_tab_bar(data: &UiTileContainerData, mouse: Vec2) -> Option<(TileId, usize)> {
for (index, tile) in data.tiles.iter().enumerate() {
if let Some(TileNode::Tabs { panes, .. }) = tile
&& !panes.is_empty()
{
let rect = &data.rects[index];
let tab_bar_rect = crate::ecs::ui::types::Rect {
min: rect.min,
max: Vec2::new(rect.max.x, rect.min.y + data.tab_bar_height),
};
if tab_bar_rect.contains(mouse) {
let tab_width = tab_bar_rect.width() / panes.len() as f32;
let relative_x = mouse.x - tab_bar_rect.min.x;
let tab_index = (relative_x / tab_width) as usize;
let tab_index = tab_index.min(panes.len() - 1);
return Some((TileId(index), tab_index));
}
}
}
None
}
fn tile_hit_test_tab_close(
data: &UiTileContainerData,
tabs_id: TileId,
tab_index: usize,
mouse: Vec2,
) -> bool {
let rect = &data.rects[tabs_id.0];
if let Some(TileNode::Tabs { panes, .. }) = data.get(tabs_id)
&& !panes.is_empty()
{
let close_w = data.tab_close_width();
let tab_width = rect.width() / panes.len() as f32;
let tab_x = rect.min.x + tab_index as f32 * tab_width;
let close_region_left = tab_x + tab_width - close_w;
let close_region_right = tab_x + tab_width - 1.0;
let close_region_top = rect.min.y + 1.0;
let close_region_bottom = rect.min.y + data.tab_bar_height - 1.0;
mouse.x >= close_region_left
&& mouse.x <= close_region_right
&& mouse.y >= close_region_top
&& mouse.y <= close_region_bottom
} else {
false
}
}
fn tile_hit_test_drop_zone(data: &UiTileContainerData, mouse: Vec2) -> Option<DropPreview> {
for (index, tile) in data.tiles.iter().enumerate() {
if !matches!(tile, Some(TileNode::Tabs { .. })) {
continue;
}
let rect = &data.rects[index];
if !rect.contains(mouse) {
continue;
}
let width = rect.width();
let height = rect.height();
let relative = mouse - rect.min;
let zone_fraction = 0.25;
let zone = if relative.x < width * zone_fraction {
DropZone::Left
} else if relative.x > width * (1.0 - zone_fraction) {
DropZone::Right
} else if relative.y < height * zone_fraction {
DropZone::Top
} else if relative.y > height * (1.0 - zone_fraction) {
DropZone::Bottom
} else {
DropZone::Center
};
return Some(DropPreview {
target_tile: TileId(index),
zone,
});
}
None
}
struct TileContainerContext {
mouse_position: Vec2,
mouse_just_pressed: bool,
mouse_just_released: bool,
mouse_down: bool,
}
fn handle_tile_container(
world: &mut World,
entity: freecs::Entity,
data: &UiTileContainerData,
ctx: &TileContainerContext,
) {
if let Some(node) = world.get_ui_layout_node(entity)
&& (!node.visible
|| (node.computed_rect.width() <= 0.0 && node.computed_rect.height() <= 0.0))
{
return;
}
let mouse_position = ctx.mouse_position;
let mouse_just_pressed = ctx.mouse_just_pressed;
let mouse_just_released = ctx.mouse_just_released;
let mouse_down = ctx.mouse_down;
let reserved = &world.resources.retained_ui.reserved_areas;
let viewport_size = world
.resources
.window
.cached_viewport_size
.map(|(width, height)| Vec2::new(width as f32, height as f32))
.unwrap_or(Vec2::new(800.0, 600.0));
let container_rect = crate::ecs::ui::types::Rect {
min: Vec2::new(reserved.left, reserved.top),
max: Vec2::new(
viewport_size.x - reserved.right,
viewport_size.y - reserved.bottom,
),
};
if let Some(node) = world.get_ui_layout_node_mut(entity)
&& let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
&mut node.layouts[crate::ecs::ui::state::UiBase::INDEX]
{
window.position = crate::ecs::ui::units::Ab(container_rect.min).into();
window.size = crate::ecs::ui::units::Ab(container_rect.size()).into();
}
let mut data = data.clone();
let root = data.root;
compute_tile_rects(&mut data, root, container_rect);
let total_panes = data
.tiles
.iter()
.filter(|t| matches!(t, Some(TileNode::Pane { .. })))
.count();
if let Some((split_id, _start_ratio)) = data.dragging_splitter {
if mouse_down {
let parent_rect = data.rects[split_id.0];
let splitter_w = data.splitter_width;
if let Some(TileNode::Split {
direction, ratio, ..
}) = data.get_mut(split_id)
{
let new_ratio = match direction {
SplitDirection::Horizontal => {
let total = parent_rect.width() - splitter_w;
if total > 0.0 {
((mouse_position.x - parent_rect.min.x) / total).clamp(0.1, 0.9)
} else {
*ratio
}
}
SplitDirection::Vertical => {
let total = parent_rect.height() - splitter_w;
if total > 0.0 {
((mouse_position.y - parent_rect.min.y) / total).clamp(0.1, 0.9)
} else {
*ratio
}
}
};
*ratio = new_ratio;
}
let root = data.root;
compute_tile_rects(&mut data, root, container_rect);
}
if mouse_just_released || !mouse_down {
if mouse_just_released && let Some(TileNode::Split { ratio, .. }) = data.get(split_id) {
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TileSplitterMoved {
container: entity,
split_id,
ratio: *ratio,
},
);
}
data.dragging_splitter = None;
}
} else if let Some((source_tabs_id, tab_index, start_pos)) = data.pending_tab_drag {
if mouse_down {
let distance = nalgebra_glm::length(&(mouse_position - start_pos));
if distance > 8.0 {
data.pending_tab_drag = None;
data.dragging_tab = Some((source_tabs_id, tab_index, mouse_position));
data.drop_preview = tile_hit_test_drop_zone(&data, mouse_position);
}
} else {
data.pending_tab_drag = None;
}
} else if let Some((source_tabs_id, tab_index, _drag_offset)) = data.dragging_tab {
if mouse_down {
data.dragging_tab = Some((source_tabs_id, tab_index, mouse_position));
data.drop_preview = tile_hit_test_drop_zone(&data, mouse_position);
}
if mouse_just_released || !mouse_down {
let preview = data.drop_preview.take();
data.dragging_tab = None;
if mouse_just_released
&& let Some(preview) = preview
&& let Some(TileNode::Tabs { panes, .. }) = data.get(source_tabs_id).cloned()
&& tab_index < panes.len()
{
let pane_id = panes[tab_index];
let target = preview.target_tile;
match preview.zone {
DropZone::Center => {
if target == source_tabs_id {
if let Some((_, drop_index)) =
tile_hit_test_tab_bar(&data, mouse_position)
.filter(|(tid, _)| *tid == source_tabs_id)
&& let Some(TileNode::Tabs { panes, active }) =
data.get_mut(source_tabs_id)
&& tab_index < panes.len()
&& drop_index < panes.len()
&& drop_index != tab_index
{
let moved = panes.remove(tab_index);
panes.insert(drop_index, moved);
*active = drop_index;
}
} else {
if let Some(TileNode::Tabs {
panes: source_panes,
active: source_active,
}) = data.get_mut(source_tabs_id)
{
source_panes.retain(|id| *id != pane_id);
if *source_active >= source_panes.len() && !source_panes.is_empty()
{
*source_active = source_panes.len() - 1;
}
}
if let Some(TileNode::Tabs {
panes: target_panes,
active: target_active,
}) = data.get_mut(target)
{
target_panes.push(pane_id);
*target_active = target_panes.len() - 1;
}
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TileTabActivated {
container: entity,
pane_id,
},
);
collapse_empty_tiles(&mut data, source_tabs_id);
}
}
zone => {
if target == source_tabs_id && panes.len() == 1 {
} else {
let (direction, swap) = match zone {
DropZone::Left => (SplitDirection::Horizontal, true),
DropZone::Right => (SplitDirection::Horizontal, false),
DropZone::Top => (SplitDirection::Vertical, true),
DropZone::Bottom => (SplitDirection::Vertical, false),
DropZone::Center => unreachable!(),
};
if let Some(TileNode::Tabs {
panes: source_panes,
active: source_active,
}) = data.get_mut(source_tabs_id)
{
source_panes.retain(|id| *id != pane_id);
if *source_active >= source_panes.len() && !source_panes.is_empty()
{
*source_active = source_panes.len() - 1;
}
}
let new_tabs = TileNode::Tabs {
panes: vec![pane_id],
active: 0,
};
let new_tabs_id = data.alloc(new_tabs);
if let Some(old_node) = data.tiles[target.0].take() {
let old_id = data.alloc(old_node);
let children = if swap {
[new_tabs_id, old_id]
} else {
[old_id, new_tabs_id]
};
data.tiles[target.0] = Some(TileNode::Split {
direction,
ratio: 0.5,
children,
});
}
collapse_empty_tiles(&mut data, source_tabs_id);
}
}
}
}
}
} else {
if mouse_just_pressed && container_rect.contains(mouse_position) {
if let Some(split_id) = tile_hit_test_splitter(&data, mouse_position) {
let current_ratio = if let Some(TileNode::Split { ratio, .. }) = data.get(split_id)
{
*ratio
} else {
0.5
};
data.dragging_splitter = Some((split_id, current_ratio));
} else if let Some((tabs_id, tab_index)) = tile_hit_test_tab_bar(&data, mouse_position)
{
let close_hit = total_panes >= 2
&& tile_hit_test_tab_close(&data, tabs_id, tab_index, mouse_position);
if close_hit {
if let Some(TileNode::Tabs { panes, .. }) = data.get(tabs_id)
&& tab_index < panes.len()
{
let pane_id = panes[tab_index];
let title = if let Some(TileNode::Pane { title, .. }) = data.get(pane_id) {
title.clone()
} else {
String::new()
};
if let Some(TileNode::Pane { content_entity, .. }) = data.get(pane_id)
&& let Some(node) = world.get_ui_layout_node_mut(*content_entity)
{
node.visible = false;
}
if let Some(TileNode::Tabs { panes, active }) = data.get_mut(tabs_id) {
panes.retain(|id| *id != pane_id);
if *active >= panes.len() && !panes.is_empty() {
*active = panes.len() - 1;
}
}
data.free(pane_id);
collapse_empty_tiles(&mut data, tabs_id);
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TileTabClosed {
container: entity,
pane_id,
title,
},
);
}
} else if let Some(TileNode::Tabs { panes, active }) = data.get_mut(tabs_id)
&& tab_index < panes.len()
{
let pane_id = panes[tab_index];
*active = tab_index;
data.pending_tab_drag = Some((tabs_id, tab_index, mouse_position));
world.resources.retained_ui.frame_events.push(
crate::ecs::ui::resources::UiEvent::TileTabActivated {
container: entity,
pane_id,
},
);
}
}
}
data.drop_preview = None;
}
let root = data.root;
compute_tile_rects(&mut data, root, container_rect);
for (index, tile) in data.tiles.iter().enumerate() {
if let Some(TileNode::Pane { content_entity, .. }) = tile {
let rect = data.rects.get(index).copied().unwrap_or_default();
let mut is_active = true;
for tabs_tile in &data.tiles {
if let Some(TileNode::Tabs { panes, active }) = tabs_tile
&& panes.contains(&TileId(index))
&& panes.get(*active) != Some(&TileId(index))
{
is_active = false;
break;
}
}
if let Some(node) = world.get_ui_layout_node_mut(*content_entity) {
node.visible = is_active;
if let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
&mut node.layouts[crate::ecs::ui::state::UiBase::INDEX]
{
window.position = crate::ecs::ui::units::Ab(Vec2::new(
rect.min.x - container_rect.min.x,
rect.min.y - container_rect.min.y,
))
.into();
window.size =
crate::ecs::ui::units::Ab(Vec2::new(rect.width(), rect.height())).into();
}
}
}
}
if let Some((split_id, _)) = data.dragging_splitter {
if let Some(TileNode::Split { direction, .. }) = data.get(split_id) {
let cursor = match direction {
SplitDirection::Horizontal => winit::window::CursorIcon::ColResize,
SplitDirection::Vertical => winit::window::CursorIcon::RowResize,
};
world.resources.retained_ui.requested_cursor = Some(cursor);
}
} else if container_rect.contains(mouse_position)
&& let Some(split_id) = tile_hit_test_splitter(&data, mouse_position)
&& let Some(TileNode::Split { direction, .. }) = data.get(split_id)
{
let cursor = match direction {
SplitDirection::Horizontal => winit::window::CursorIcon::ColResize,
SplitDirection::Vertical => winit::window::CursorIcon::RowResize,
};
world.resources.retained_ui.requested_cursor = Some(cursor);
}
data.hovered_close = if total_panes >= 2
&& data.dragging_tab.is_none()
&& data.dragging_splitter.is_none()
&& container_rect.contains(mouse_position)
{
if let Some((tabs_id, tab_index)) = tile_hit_test_tab_bar(&data, mouse_position)
&& tile_hit_test_tab_close(&data, tabs_id, tab_index, mouse_position)
{
Some((tabs_id, tab_index))
} else {
None
}
} else {
None
};
world.resources.retained_ui.layout_dirty = true;
world.resources.retained_ui.render_dirty = true;
if let Some(UiWidgetState::TileContainer(widget_data)) = world.get_ui_widget_state_mut(entity) {
*widget_data = data;
}
}
fn collapse_empty_tiles(data: &mut UiTileContainerData, tabs_id: TileId) {
let remaining = if let Some(TileNode::Tabs { panes, .. }) = data.get(tabs_id) {
panes.len()
} else {
return;
};
if remaining > 0 {
return;
}
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);
}
}
pub fn ui_event_dispatch_system(world: &mut World) {
if !world.resources.retained_ui.enabled {
return;
}
let events = world.resources.retained_ui.frame_events.clone();
if !events.is_empty() {
let mut handlers = std::mem::take(&mut world.resources.retained_ui.event_handlers);
for event in &events {
let target = event.target_entity();
if let Some(entity_handlers) = handlers.get_mut(&target) {
for handler in entity_handlers.iter_mut() {
handler(world, event);
}
}
}
let newly_registered = std::mem::take(&mut world.resources.retained_ui.event_handlers);
for (entity, mut new_handlers) in newly_registered {
handlers
.entry(entity)
.or_default()
.append(&mut new_handlers);
}
world.resources.retained_ui.event_handlers = handlers;
}
let mut click_entities: Vec<freecs::Entity> = events
.iter()
.filter_map(|event| match event {
crate::ecs::ui::resources::UiEvent::ButtonClicked(entity)
| crate::ecs::ui::resources::UiEvent::SelectableLabelClicked { entity, .. } => {
Some(*entity)
}
_ => None,
})
.collect();
for &entity in world.resources.retained_ui.click_reactions.keys() {
if !click_entities.contains(&entity)
&& world
.get_ui_node_interaction(entity)
.is_some_and(|interaction| interaction.clicked)
{
click_entities.push(entity);
}
}
if !click_entities.is_empty() {
let mut click_reactions = std::mem::take(&mut world.resources.retained_ui.click_reactions);
for entity in &click_entities {
if let Some(reaction_list) = click_reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(world);
}
}
}
let newly_registered_clicks =
std::mem::take(&mut world.resources.retained_ui.click_reactions);
for (entity, mut new_reactions) in newly_registered_clicks {
click_reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.click_reactions = click_reactions;
}
let submit_events: Vec<(freecs::Entity, String)> = events
.iter()
.filter_map(|event| {
if let crate::ecs::ui::resources::UiEvent::TextInputSubmitted { entity, text } = event {
Some((*entity, text.clone()))
} else {
None
}
})
.collect();
if !submit_events.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.submit_reactions);
for (entity, text) in &submit_events {
if let Some(reaction_list) = reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(text.clone(), world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.submit_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.submit_reactions = reactions;
}
let confirm_events: Vec<(freecs::Entity, bool)> = events
.iter()
.filter_map(|event| {
if let crate::ecs::ui::resources::UiEvent::ModalClosed { entity, confirmed } = event {
Some((*entity, *confirmed))
} else {
None
}
})
.collect();
if !confirm_events.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.confirm_reactions);
for (entity, confirmed) in &confirm_events {
if let Some(reaction_list) = reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(*confirmed, world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.confirm_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.confirm_reactions = reactions;
}
let menu_events: Vec<(freecs::Entity, usize)> = events
.iter()
.filter_map(|event| match event {
crate::ecs::ui::resources::UiEvent::ContextMenuItemClicked { entity, item_index }
| crate::ecs::ui::resources::UiEvent::MenuItemClicked { entity, item_index } => {
Some((*entity, *item_index))
}
crate::ecs::ui::resources::UiEvent::BreadcrumbClicked {
entity,
segment_index,
} => Some((*entity, *segment_index)),
_ => None,
})
.collect();
if !menu_events.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.menu_select_reactions);
for (entity, index) in &menu_events {
if let Some(reaction_list) = reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(*index, world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.menu_select_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.menu_select_reactions = reactions;
}
let command_events: Vec<(freecs::Entity, usize)> = events
.iter()
.filter_map(|event| {
if let crate::ecs::ui::resources::UiEvent::CommandPaletteExecuted {
entity,
command_index,
} = event
{
Some((*entity, *command_index))
} else {
None
}
})
.collect();
if !command_events.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.command_reactions);
for (entity, index) in &command_events {
if let Some(reaction_list) = reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(*index, world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.command_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.command_reactions = reactions;
}
let tree_events: Vec<(freecs::Entity, freecs::Entity)> = events
.iter()
.filter_map(|event| {
if let crate::ecs::ui::resources::UiEvent::TreeNodeSelected { tree, node, .. } = event {
Some((*tree, *node))
} else {
None
}
})
.collect();
if !tree_events.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.tree_select_reactions);
for (tree_entity, node) in &tree_events {
if let Some(reaction_list) = reactions.get_mut(tree_entity) {
for reaction in reaction_list.iter_mut() {
reaction(*node, world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.tree_select_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.tree_select_reactions = reactions;
}
let tree_context_menu_events: Vec<(freecs::Entity, freecs::Entity, nalgebra_glm::Vec2)> =
events
.iter()
.filter_map(|event| {
if let crate::ecs::ui::resources::UiEvent::TreeNodeContextMenu {
tree,
node,
position,
} = event
{
Some((*tree, *node, *position))
} else {
None
}
})
.collect();
if !tree_context_menu_events.is_empty() {
let mut reactions =
std::mem::take(&mut world.resources.retained_ui.tree_context_menu_reactions);
for (tree_entity, node, position) in &tree_context_menu_events {
if let Some(reaction_list) = reactions.get_mut(tree_entity) {
for reaction in reaction_list.iter_mut() {
reaction(*node, *position, world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.tree_context_menu_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.tree_context_menu_reactions = reactions;
}
let multi_select_events: Vec<(freecs::Entity, Vec<usize>)> = events
.iter()
.filter_map(|event| {
if let crate::ecs::ui::resources::UiEvent::MultiSelectChanged {
entity,
selected_indices,
} = event
{
Some((*entity, selected_indices.clone()))
} else {
None
}
})
.collect();
if !multi_select_events.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.multi_select_reactions);
for (entity, indices) in &multi_select_events {
if let Some(reaction_list) = reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(indices.clone(), world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.multi_select_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.multi_select_reactions = reactions;
}
let date_events: Vec<(freecs::Entity, i32, u32, u32)> = events
.iter()
.filter_map(|event| {
if let crate::ecs::ui::resources::UiEvent::DatePickerChanged {
entity,
year,
month,
day,
} = event
{
Some((*entity, *year, *month, *day))
} else {
None
}
})
.collect();
if !date_events.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.date_changed_reactions);
for (entity, year, month, day) in &date_events {
if let Some(reaction_list) = reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(*year, *month, *day, world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.date_changed_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.date_changed_reactions = reactions;
}
let changed_entity_keys: Vec<freecs::Entity> = world
.resources
.retained_ui
.changed_reactions
.keys()
.copied()
.collect();
if !changed_entity_keys.is_empty() {
let changed_entities: Vec<freecs::Entity> = changed_entity_keys
.into_iter()
.filter(|entity| world.ui_changed(*entity))
.collect();
if !changed_entities.is_empty() {
let mut reactions = std::mem::take(&mut world.resources.retained_ui.changed_reactions);
for entity in &changed_entities {
if let Some(reaction_list) = reactions.get_mut(entity) {
for reaction in reaction_list.iter_mut() {
reaction(world);
}
}
}
let newly = std::mem::take(&mut world.resources.retained_ui.changed_reactions);
for (entity, mut new_reactions) in newly {
reactions
.entry(entity)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.changed_reactions = reactions;
}
}
}
pub fn ui_property_sync_system(world: &mut World) {
let property_ids: Vec<crate::ecs::ui::resources::PropertyId> = world
.resources
.retained_ui
.bound_properties
.keys()
.copied()
.collect();
for id in property_ids {
let (entity, dirty_from_code, value_clone) = {
let Some(prop) = world.resources.retained_ui.bound_properties.get(&id) else {
continue;
};
(
prop.entity,
prop.dirty_from_code,
match &prop.value {
crate::ecs::ui::resources::PropertyValue::F32(v) => {
crate::ecs::ui::resources::PropertyValue::F32(*v)
}
crate::ecs::ui::resources::PropertyValue::Bool(v) => {
crate::ecs::ui::resources::PropertyValue::Bool(*v)
}
crate::ecs::ui::resources::PropertyValue::Usize(v) => {
crate::ecs::ui::resources::PropertyValue::Usize(*v)
}
crate::ecs::ui::resources::PropertyValue::String(v) => {
crate::ecs::ui::resources::PropertyValue::String(v.clone())
}
crate::ecs::ui::resources::PropertyValue::Vec4(v) => {
crate::ecs::ui::resources::PropertyValue::Vec4(*v)
}
},
)
};
let widget_changed = if let Some(state) = world.get_ui_widget_state(entity) {
match state {
UiWidgetState::Slider(data) => data.changed,
UiWidgetState::DragValue(data) => data.changed,
UiWidgetState::Toggle(data) => data.changed,
UiWidgetState::Checkbox(data) => data.changed,
UiWidgetState::TextInput(data) => data.changed,
UiWidgetState::Dropdown(data) => data.changed,
UiWidgetState::TextArea(data) => data.changed,
UiWidgetState::RichTextEditor(data) => data.changed,
UiWidgetState::ColorPicker(data) => data.changed,
UiWidgetState::TabBar(data) => data.changed,
UiWidgetState::SelectableLabel(data) => data.changed,
UiWidgetState::CollapsingHeader(data) => data.changed,
UiWidgetState::Splitter(data) => data.changed,
UiWidgetState::RangeSlider(data) => data.changed,
UiWidgetState::Radio(data) => {
let group_id = data.group_id;
let members = world
.resources
.retained_ui
.radio_groups
.get(&group_id)
.cloned()
.unwrap_or_default();
members.iter().any(|&member| {
matches!(
world.get_ui_widget_state(member),
Some(UiWidgetState::Radio(r)) if r.changed
)
})
}
_ => false,
}
} else {
false
};
if widget_changed {
if let Some(state) = world.get_ui_widget_state(entity) {
let new_value = match state {
UiWidgetState::Slider(data) => {
Some(crate::ecs::ui::resources::PropertyValue::F32(data.value))
}
UiWidgetState::DragValue(data) => {
Some(crate::ecs::ui::resources::PropertyValue::F32(data.value))
}
UiWidgetState::Toggle(data) => {
Some(crate::ecs::ui::resources::PropertyValue::Bool(data.value))
}
UiWidgetState::Checkbox(data) => {
Some(crate::ecs::ui::resources::PropertyValue::Bool(data.value))
}
UiWidgetState::TextInput(data) => Some(
crate::ecs::ui::resources::PropertyValue::String(data.text.clone()),
),
UiWidgetState::Dropdown(data) => Some(
crate::ecs::ui::resources::PropertyValue::Usize(data.selected_index),
),
UiWidgetState::TextArea(data) => Some(
crate::ecs::ui::resources::PropertyValue::String(data.text.clone()),
),
UiWidgetState::RichTextEditor(data) => Some(
crate::ecs::ui::resources::PropertyValue::String(data.text.clone()),
),
UiWidgetState::ColorPicker(data) => {
Some(crate::ecs::ui::resources::PropertyValue::Vec4(data.color))
}
UiWidgetState::TabBar(data) => Some(
crate::ecs::ui::resources::PropertyValue::Usize(data.selected_tab),
),
UiWidgetState::SelectableLabel(data) => Some(
crate::ecs::ui::resources::PropertyValue::Bool(data.selected),
),
UiWidgetState::CollapsingHeader(data) => {
Some(crate::ecs::ui::resources::PropertyValue::Bool(data.open))
}
UiWidgetState::Radio(data) => {
let group_id = data.group_id;
world
.ui_radio_group_value(group_id)
.map(crate::ecs::ui::resources::PropertyValue::Usize)
}
UiWidgetState::Splitter(data) => {
Some(crate::ecs::ui::resources::PropertyValue::F32(data.ratio))
}
UiWidgetState::RangeSlider(data) => {
Some(crate::ecs::ui::resources::PropertyValue::Vec4(
nalgebra_glm::Vec4::new(data.low_value, data.high_value, 0.0, 0.0),
))
}
_ => None,
};
if let Some(val) = new_value
&& let Some(prop) = world.resources.retained_ui.bound_properties.get_mut(&id)
{
prop.value = val;
prop.dirty_from_widget = true;
}
}
} else if dirty_from_code && let Some(state) = world.get_ui_widget_state(entity) {
match (state, &value_clone) {
(UiWidgetState::Slider(_), crate::ecs::ui::resources::PropertyValue::F32(v)) => {
let v = *v;
world.ui_slider_set_value(entity, v);
}
(UiWidgetState::DragValue(_), crate::ecs::ui::resources::PropertyValue::F32(v)) => {
let v = *v;
world.ui_drag_value_set_value(entity, v);
}
(UiWidgetState::Toggle(_), crate::ecs::ui::resources::PropertyValue::Bool(v)) => {
let v = *v;
world.ui_toggle_set_value(entity, v);
}
(UiWidgetState::Checkbox(_), crate::ecs::ui::resources::PropertyValue::Bool(v)) => {
if let Some(UiWidgetState::Checkbox(data)) =
world.get_ui_widget_state_mut(entity)
{
data.value = *v;
}
}
(
UiWidgetState::SelectableLabel(_),
crate::ecs::ui::resources::PropertyValue::Bool(v),
) => {
let v = *v;
world.ui_set_selected(entity, v);
}
(
UiWidgetState::CollapsingHeader(header_data),
crate::ecs::ui::resources::PropertyValue::Bool(v),
) => {
let new_open = *v;
let content_entity = header_data.content_entity;
let arrow_text_slot = header_data.arrow_text_slot;
if let Some(UiWidgetState::CollapsingHeader(data)) =
world.get_ui_widget_state_mut(entity)
{
data.open = new_open;
}
if let Some(node) = world.get_ui_layout_node_mut(content_entity) {
node.visible = new_open;
}
world.resources.text_cache.set_text(
arrow_text_slot,
if new_open { "\u{25BC}" } else { "\u{25B6}" },
);
}
(
UiWidgetState::Radio(radio_data),
crate::ecs::ui::resources::PropertyValue::Usize(v),
) => {
let target_index = *v;
let group_id = radio_data.group_id;
let members = world
.resources
.retained_ui
.radio_groups
.get(&group_id)
.cloned()
.unwrap_or_default();
for member in members {
if let Some(UiWidgetState::Radio(member_data)) =
world.get_ui_widget_state(member).cloned().as_ref()
{
let should_select = member_data.option_index == target_index;
if let Some(node) =
world.get_ui_layout_node_mut(member_data.inner_entity)
{
node.visible = should_select;
}
if let Some(UiWidgetState::Radio(wd)) =
world.get_ui_widget_state_mut(member)
{
wd.selected = should_select;
}
}
}
}
(
UiWidgetState::TextInput(_),
crate::ecs::ui::resources::PropertyValue::String(v),
) => {
world.ui_text_input_set_value(entity, v);
}
(
UiWidgetState::TextArea(_),
crate::ecs::ui::resources::PropertyValue::String(v),
) => {
world.ui_text_area_set_value(entity, v);
}
(
UiWidgetState::RichTextEditor(_),
crate::ecs::ui::resources::PropertyValue::String(v),
) => {
world.ui_rich_text_editor_set_value(entity, v);
}
(
UiWidgetState::Dropdown(_),
crate::ecs::ui::resources::PropertyValue::Usize(v),
) => {
let v = *v;
world.ui_dropdown_set_value(entity, v);
}
(UiWidgetState::TabBar(_), crate::ecs::ui::resources::PropertyValue::Usize(v)) => {
let v = *v;
world.ui_tab_bar_set_value(entity, v);
}
(
UiWidgetState::ColorPicker(_),
crate::ecs::ui::resources::PropertyValue::Vec4(v),
) => {
let v = *v;
world.ui_color_picker_set_value(entity, v);
}
(UiWidgetState::Splitter(_), crate::ecs::ui::resources::PropertyValue::F32(v)) => {
if let Some(UiWidgetState::Splitter(data)) =
world.get_ui_widget_state_mut(entity)
{
data.ratio = *v;
}
}
(
UiWidgetState::RangeSlider(_),
crate::ecs::ui::resources::PropertyValue::Vec4(v),
) => {
world.ui_range_slider_set_values(entity, v.x, v.y);
}
_ => {}
}
}
}
for _iteration in 0..8 {
let mut dirty_props: Vec<(String, crate::ecs::ui::resources::PropertyValue)> = Vec::new();
for (name, property_id) in &world.resources.retained_ui.named_properties {
if let Some(bp) = world
.resources
.retained_ui
.bound_properties
.get(property_id)
&& (bp.dirty_from_widget || bp.dirty_from_code)
{
dirty_props.push((name.clone(), bp.value.clone()));
}
}
if dirty_props.is_empty() {
break;
}
for (name, _) in &dirty_props {
if let Some(property_id) = world
.resources
.retained_ui
.named_properties
.get(name)
.copied()
&& let Some(bp) = world
.resources
.retained_ui
.bound_properties
.get_mut(&property_id)
{
bp.dirty_from_widget = false;
bp.dirty_from_code = false;
}
}
let mut reactions = std::mem::take(&mut world.resources.retained_ui.property_reactions);
for (name, value) in &dirty_props {
if let Some(reaction_list) = reactions.get_mut(name) {
for reaction in reaction_list.iter_mut() {
reaction(value, world);
}
}
}
let newly_registered = std::mem::take(&mut world.resources.retained_ui.property_reactions);
for (name, mut new_reactions) in newly_registered {
reactions
.entry(name)
.or_default()
.append(&mut new_reactions);
}
world.resources.retained_ui.property_reactions = reactions;
}
}