use crate::ecs::ui::state::UiStateTrait as _;
use nalgebra_glm::{Vec2, Vec4};
use winit::keyboard::KeyCode;
use crate::ecs::ui::components::UiWidgetState;
use crate::ecs::world::World;
use super::snapshot_interaction;
pub(super) struct DataGridContext<'a> {
pub(super) ctrl_held: bool,
pub(super) shift_held: bool,
pub(super) frame_keys: &'a [(KeyCode, bool)],
pub(super) mouse_position: Vec2,
pub(super) mouse_just_pressed: bool,
pub(super) mouse_down: bool,
}
pub(super) 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.ui.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.ui.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
.ui
.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.ui.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.ui.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.ui.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.ui.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.ui.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);
}
}
pub(super) 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.ui.get_ui_widget_state_mut(entity)
{
palette.executed_command = None;
}
return;
}
let text_changed = if let Some(UiWidgetState::TextInput(ti)) =
world.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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
.ui
.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.ui.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.ui.get_ui_node_color_mut(row_entity) {
color.colors[crate::ecs::ui::state::UiBase::INDEX] = Some(bg);
}
}
}
let backdrop_clicked = world
.ui
.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.ui.get_ui_layout_node_mut(data.backdrop_entity) {
node.visible = false;
}
if let Some(node) = world.ui.get_ui_layout_node_mut(entity) {
node.visible = false;
}
if let Some(UiWidgetState::CommandPalette(palette)) =
world.ui.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,
},
);
}
}
}
pub(super) 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.ui.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.ui.get_ui_widget_state(entity)
{
(grid.resize_start_x, grid.resize_start_width)
} else {
return;
};
let grid_rect = world
.ui
.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.ui.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.ui.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.ui.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
.ui
.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.ui.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);
}
}
pub(super) 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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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
.ui
.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.ui.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);
}
}
pub(super) 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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.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.ui.get_ui_widget_state_mut(entity) {
grid.visible_start = visible_start;
}
}