nightshade 0.14.1

A cross-platform data-oriented game engine.
Documentation
use crate::ecs::world::World;
use std::collections::HashMap;

use nalgebra_glm::{Vec2, Vec4};

use crate::ecs::ui::components::*;
use crate::ecs::ui::units::Ab;

/// (or when data changes) before setting cell contents.
use crate::prelude::*;
pub fn ui_data_grid_set_row_count(world: &mut World, entity: freecs::Entity, count: usize) {
    if let Some(data) = world.ui.get_ui_data_grid_mut(entity) {
        data.total_rows = count;
        data.selected_rows.retain(|&row| row < count);
    }
}

/// Sets the text of a cell in the data grid. `data_row` is the absolute
/// row index in the data set (not the pool index).
pub fn ui_data_grid_set_cell(
    world: &mut World,
    entity: freecs::Entity,
    data_row: usize,
    column: usize,
    text: &str,
) {
    if let Some(data) = world.ui.get_ui_data_grid(entity) {
        if data_row < data.visible_start {
            return;
        }
        let pool_index = data_row - data.visible_start;
        if let Some(pool_row) = data.pool_rows.get(pool_index)
            && let Some(&text_slot) = pool_row.cell_text_slots.get(column)
        {
            world.resources.text.cache.set_text(text_slot, text);
        }
    }
}

/// Returns whether the sort state changed this frame.
pub fn ui_data_grid_sort_changed(world: &World, entity: freecs::Entity) -> bool {
    if let Some(data) = world.ui.get_ui_data_grid(entity) {
        data.sort_changed
    } else {
        false
    }
}

/// Returns whether the selection changed this frame.
pub fn ui_data_grid_selection_changed(world: &World, entity: freecs::Entity) -> bool {
    if let Some(data) = world.ui.get_ui_data_grid(entity) {
        data.selection_changed
    } else {
        false
    }
}

pub fn ui_data_grid_set_filter(world: &mut World, entity: freecs::Entity, indices: &[usize]) {
    if let Some(grid) = world.ui.get_ui_data_grid_mut(entity) {
        grid.filtered_indices = Some(indices.to_vec());
        grid.selected_rows.clear();
        grid.selection_anchor = None;
        grid.selection_changed = true;
    }
}

pub fn ui_data_grid_clear_filter(world: &mut World, entity: freecs::Entity) {
    if let Some(grid) = world.ui.get_ui_data_grid_mut(entity) {
        grid.filtered_indices = None;
        grid.selected_rows.clear();
        grid.selection_anchor = None;
        grid.selection_changed = true;
    }
}

pub fn ui_data_grid_populate(
    world: &mut World,
    entity: freecs::Entity,
    source: &dyn crate::ecs::ui::components::DataGridDataSource,
) {
    let row_count = source.row_count();
    ui_data_grid_set_row_count(world, entity, row_count);
    let Some(data) = widget::<UiDataGridData>(world, entity) else {
        return;
    };
    let end = (data.visible_start + data.pool_size).min(data.total_rows);
    let range = data.visible_start..end;
    let col_count = data.columns.len();
    let filtered = data.filtered_indices.clone();
    for visible_row in range {
        let data_row = filtered
            .as_ref()
            .and_then(|indices| indices.get(visible_row).copied())
            .unwrap_or(visible_row);
        for column in 0..col_count {
            let text = source.cell_text(data_row, column);
            ui_data_grid_set_cell(world, entity, visible_row, column, &text);
        }
    }
}

pub fn ui_data_grid_populate_fn(
    world: &mut World,
    entity: freecs::Entity,
    row_count: usize,
    cell_fn: impl Fn(usize, usize) -> String,
) {
    ui_data_grid_set_row_count(world, entity, row_count);
    let Some(data) = widget::<UiDataGridData>(world, entity) else {
        return;
    };
    let end = (data.visible_start + data.pool_size).min(data.total_rows);
    let range = data.visible_start..end;
    let col_count = data.columns.len();
    let filtered = data.filtered_indices.clone();
    for visible_row in range {
        let data_row = filtered
            .as_ref()
            .and_then(|indices| indices.get(visible_row).copied())
            .unwrap_or(visible_row);
        for column in 0..col_count {
            let text = cell_fn(data_row, column);
            ui_data_grid_set_cell(world, entity, visible_row, column, &text);
        }
    }
}

pub fn ui_data_grid_start_edit(
    world: &mut World,
    entity: freecs::Entity,
    row: usize,
    column: usize,
) {
    let info = if let Some(data) = world.ui.get_ui_data_grid(entity) {
        if column < data.columns.len()
            && data.columns[column].editable
            && let Some(input_entity) = data.editing_input_entity
        {
            let pool_row_idx = if row >= data.visible_start {
                row - data.visible_start
            } else {
                return;
            };
            if pool_row_idx >= data.pool_rows.len() {
                return;
            }
            let cell_entity = data.pool_rows[pool_row_idx].cell_entities[column];
            let cell_text_slot = data.pool_rows[pool_row_idx].cell_text_slots[column];
            let cell_text = world
                .resources
                .text
                .cache
                .get_text(cell_text_slot)
                .unwrap_or_default()
                .to_string();
            Some((input_entity, cell_entity, cell_text))
        } else {
            None
        }
    } else {
        None
    };

    if let Some((input_entity, cell_entity, cell_text)) = info {
        ui_text_input_set_value(world, input_entity, &cell_text);
        if let Some(input_data) = world.ui.get_ui_text_input_mut(input_entity) {
            input_data.selection_start = Some(0);
        }

        let root_rect = world
            .ui
            .get_ui_layout_node(entity)
            .map(|n| n.computed_rect)
            .unwrap_or_default();
        let cell_rect = world
            .ui
            .get_ui_layout_node(cell_entity)
            .map(|n| n.computed_rect)
            .unwrap_or_default();

        let rel_x = cell_rect.min.x - root_rect.min.x;
        let rel_y = cell_rect.min.y - root_rect.min.y;
        let width = cell_rect.width();
        let height = cell_rect.height();

        if let Some(node) = world.ui.get_ui_layout_node_mut(input_entity) {
            node.visible = true;
            if let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
                node.base_layout.as_mut()
            {
                window.position = Ab(Vec2::new(rel_x, rel_y)).into();
                window.size = Ab(Vec2::new(width, height)).into();
            }
        }
        world
            .resources
            .retained_ui
            .interaction_for_active_mut()
            .focused_entity = Some(input_entity);
        if let Some(data) = world.ui.get_ui_data_grid_mut(entity) {
            data.editing_cell = Some((row, column));
        }
    }
}

pub fn ui_data_grid_stop_edit(world: &mut World, entity: freecs::Entity, commit: bool) {
    let info = if let Some(data) = world.ui.get_ui_data_grid(entity) {
        if let Some((row, column)) = data.editing_cell
            && let Some(input_entity) = data.editing_input_entity
        {
            Some((input_entity, row, column))
        } else {
            None
        }
    } else {
        None
    };

    if let Some((input_entity, row, column)) = info {
        if commit {
            let text = widget::<UiTextInputData>(world, input_entity)
                .map(|d| d.text.clone())
                .unwrap_or_default();
            world.resources.retained_ui.frame.events.push(
                crate::ecs::ui::resources::UiEvent::DataGridCellEdited {
                    entity,
                    row,
                    column,
                    text,
                },
            );
        }
        if let Some(node) = world.ui.get_ui_layout_node_mut(input_entity) {
            node.visible = false;
        }
        if world
            .resources
            .retained_ui
            .interaction_for_active_mut()
            .focused_entity
            == Some(input_entity)
        {
            world
                .resources
                .retained_ui
                .interaction_for_active_mut()
                .focused_entity = None;
        }
        if let Some(data) = world.ui.get_ui_data_grid_mut(entity) {
            data.editing_cell = None;
        }
    }
}

pub fn ui_text_area_set_value(world: &mut World, entity: freecs::Entity, text: &str) {
    let extract = world
        .ui
        .get_ui_text_area(entity)
        .map(|data| (data.text_slot, data.placeholder_entity));

    if let Some((slot, placeholder_entity)) = extract {
        world.resources.text.cache.set_text(slot, text);
        if let Some(data) = world.ui.get_ui_text_area_mut(entity) {
            data.text = text.to_string();
            data.cursor_position = text.chars().count();
            data.selection_start = None;
        }
        if let Some(ph_entity) = placeholder_entity
            && let Some(node) = world.ui.get_ui_layout_node_mut(ph_entity)
        {
            node.visible = text.is_empty();
        }
    }
}

pub fn ui_rich_text_editor_set_value(world: &mut World, entity: freecs::Entity, text: &str) {
    let slot = world
        .ui
        .get_ui_rich_text_editor(entity)
        .map(|data| data.text_slot);

    if let Some(slot) = slot {
        world.resources.text.cache.set_text(slot, text);
        let char_count = text.chars().count();
        if let Some(data) = world.ui.get_ui_rich_text_editor_mut(entity) {
            data.text = text.to_string();
            data.char_styles = vec![CharStyle::default(); char_count];
            data.cursor_position = char_count;
            data.selection_start = None;
        }
    }
}

pub fn ui_rich_text_editor_toggle_bold(world: &mut World, entity: freecs::Entity) {
    ui_rich_text_editor_toggle_style(world, entity, |style| &mut style.bold);
}

pub fn ui_rich_text_editor_toggle_italic(world: &mut World, entity: freecs::Entity) {
    ui_rich_text_editor_toggle_style(world, entity, |style| &mut style.italic);
}

pub fn ui_rich_text_editor_toggle_underline(world: &mut World, entity: freecs::Entity) {
    ui_rich_text_editor_toggle_style(world, entity, |style| &mut style.underline);
}

pub fn ui_rich_text_editor_set_color(
    world: &mut World,
    entity: freecs::Entity,
    color: Option<Vec4>,
) {
    let update_info = if let Some(data) = world.ui.get_ui_rich_text_editor_mut(entity) {
        if let Some(sel_start) = data.selection_start {
            let start = sel_start.min(data.cursor_position);
            let end = sel_start.max(data.cursor_position);
            for index in start..end {
                if index < data.char_styles.len() {
                    data.char_styles[index].color = color;
                }
            }
        }
        data.current_style.color = color;
        data.changed = true;
        Some((data.char_styles.clone(), data.text_slot))
    } else {
        None
    };

    if let Some((char_styles, text_slot)) = update_info {
        update_rich_text_char_colors(
            &char_styles,
            text_slot,
            &mut world.resources.retained_ui.text_cache.character_colors,
        );
    }
}

pub fn ui_rich_text_editor_toggle_style(
    world: &mut World,
    entity: freecs::Entity,
    accessor: fn(&mut CharStyle) -> &mut bool,
) {
    let update_info = if let Some(data) = world.ui.get_ui_rich_text_editor_mut(entity) {
        if let Some(sel_start) = data.selection_start {
            let start = sel_start.min(data.cursor_position);
            let end = sel_start.max(data.cursor_position);
            let all_set = (start..end).all(|index| {
                data.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 < data.char_styles.len() {
                    *accessor(&mut data.char_styles[index]) = new_value;
                }
            }
            *accessor(&mut data.current_style) = new_value;
        } else {
            let field = accessor(&mut data.current_style);
            *field = !*field;
        }
        data.changed = true;
        Some((data.char_styles.clone(), data.text_slot))
    } else {
        None
    };

    if let Some((char_styles, text_slot)) = update_info {
        update_rich_text_char_colors(
            &char_styles,
            text_slot,
            &mut world.resources.retained_ui.text_cache.character_colors,
        );
    }
}

fn update_rich_text_char_colors(
    char_styles: &[CharStyle],
    text_slot: usize,
    slot_colors: &mut HashMap<usize, Vec<Option<Vec4>>>,
) {
    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();
        slot_colors.insert(text_slot, colors);
    } else {
        slot_colors.remove(&text_slot);
    }
}