nightshade 0.17.0

A cross-platform data-oriented game engine.
Documentation
use nalgebra_glm::Vec2;
use winit::keyboard::KeyCode;

use crate::ecs::world::World;

use super::InteractionSnapshot;
use super::text_cursor::{byte_index_at_x, measure_text_width};
use super::text_edit::EditBuffer;
use crate::ecs::ui::components::{CharStyle, TextSnapshot};

use crate::prelude::*;

pub(super) fn handle_drag_value(
    world: &mut World,
    entity: freecs::Entity,
    interaction: &InteractionSnapshot,
    data: &crate::ecs::ui::components::UiDragValueData,
    ctx: &super::TextEditContext<'_>,
) {
    let focused_entity = ctx.focused_entity;
    let frame_chars = ctx.frame_chars;
    let frame_keys = ctx.frame_keys;
    let ctrl_held = ctx.ctrl_held;
    let shift_held = ctx.shift_held;
    let mouse_position = ctx.mouse_position;
    let current_time = ctx.current_time;
    let dpi_scale = ctx.dpi_scale;
    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 buffer =
            EditBuffer::new(edit_text.clone(), cursor_position, selection_start, false);
        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(snapshot(&buffer));
                    needs_snapshot = true;
                }
                buffer.insert_char(*character, CharStyle::default());
                cursor_blink_timer = current_time;
            }
        }
        if needs_snapshot {
            undo_stack.push(snapshot(&buffer), current_time);
        }

        for (key, is_pressed) in frame_keys {
            if !is_pressed {
                continue;
            }
            match key {
                KeyCode::Backspace => {
                    undo_stack.push_initial(snapshot(&buffer));
                    buffer.backspace(ctrl_held);
                    undo_stack.push(snapshot(&buffer), current_time);
                    cursor_blink_timer = current_time;
                }
                KeyCode::Delete => {
                    undo_stack.push_initial(snapshot(&buffer));
                    buffer.delete_forward(ctrl_held);
                    undo_stack.push(snapshot(&buffer), current_time);
                    cursor_blink_timer = current_time;
                }
                KeyCode::ArrowLeft => {
                    buffer.move_left(ctrl_held, shift_held);
                    cursor_blink_timer = current_time;
                }
                KeyCode::ArrowRight => {
                    buffer.move_right(ctrl_held, shift_held);
                    cursor_blink_timer = current_time;
                }
                KeyCode::Home => {
                    buffer.move_home(shift_held);
                    cursor_blink_timer = current_time;
                }
                KeyCode::End => {
                    buffer.move_end(shift_held);
                    cursor_blink_timer = current_time;
                }
                KeyCode::KeyA if ctrl_held => {
                    buffer.select_all();
                    cursor_blink_timer = current_time;
                }
                KeyCode::KeyZ if ctrl_held => {
                    if let Some(restored) = undo_stack.undo() {
                        buffer.text = restored.text.clone();
                        buffer.cursor = restored.cursor_position;
                        buffer.selection = restored.selection_start;
                        cursor_blink_timer = current_time;
                    }
                }
                KeyCode::KeyY if ctrl_held => {
                    if let Some(restored) = undo_stack.redo() {
                        buffer.text = restored.text.clone();
                        buffer.cursor = restored.cursor_position;
                        buffer.selection = restored.selection_start;
                        cursor_blink_timer = current_time;
                    }
                }
                KeyCode::KeyC if ctrl_held => {
                    if let Some(selected) = buffer.selected_text() {
                        ui_set_clipboard_text(world, selected);
                    }
                }
                KeyCode::KeyX if ctrl_held => {
                    if let Some(selected) = buffer.selected_text() {
                        ui_set_clipboard_text(world, selected);
                        undo_stack.push_initial(snapshot(&buffer));
                        buffer.delete_selection();
                        undo_stack.push(snapshot(&buffer), current_time);
                    }
                }
                KeyCode::KeyV if ctrl_held => {
                    let paste_text = ui_read_system_clipboard(world);
                    if !paste_text.is_empty() {
                        undo_stack.push_initial(snapshot(&buffer));
                        buffer.insert_str(&paste_text, CharStyle::default());
                        undo_stack.push(snapshot(&buffer), current_time);
                    }
                }
                KeyCode::Escape => {
                    editing = false;
                    buffer.selection = None;
                    clear_focus = true;
                }
                KeyCode::Enter => {
                    if let Ok(parsed) = buffer.text.parse::<f32>() {
                        value = parsed.clamp(data.min, data.max);
                        changed = true;
                    }
                    editing = false;
                    buffer.selection = None;
                    clear_focus = true;
                }
                _ => {}
            }
        }

        if interaction.clicked
            && let Some(rect) = world.ui.get_ui_layout_node(entity).map(|n| n.computed_rect)
        {
            let font_size = world
                .resources
                .retained_ui
                .theme_state
                .active_theme()
                .font_size;
            let local_x = (mouse_position.x - rect.min.x) / dpi_scale - 8.0 + scroll_offset;
            let new_pos = byte_index_at_x(
                &mut world.resources.text.font_engine,
                &buffer.text,
                font_size,
                local_x,
            );
            if shift_held {
                if buffer.selection.is_none() {
                    buffer.selection = Some(buffer.cursor);
                }
            } else {
                buffer.selection = None;
            }
            buffer.cursor = new_pos;
            cursor_blink_timer = current_time;
        }

        if interaction.double_clicked {
            buffer.select_word_at_cursor();
        }

        edit_text = buffer.text;
        cursor_position = buffer.cursor;
        selection_start = buffer.selection;

        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
                .interaction_for_active_mut()
                .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
            .interaction_for_active_mut()
            .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.ui.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.ui.get_ui_layout_node_mut(data.selection_entity) {
        sel_node.visible = has_selection;
    }

    if editing {
        let input_rect = world.ui.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 text_before_cursor: String = edit_text.chars().take(cursor_position).collect();
                let cursor_x = measure_text_width(
                    &mut world.resources.text.font_engine,
                    &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(
                        &mut world.resources.text.font_engine,
                        &text_before_sel,
                        font_size,
                    ) - scroll_offset;
                    sel_end_x = measure_text_width(
                        &mut world.resources.text.font_engine,
                        &text_to_sel_end,
                        font_size,
                    ) - scroll_offset;
                }

                if let Some(cursor_node) = world.ui.get_ui_layout_node_mut(data.cursor_entity)
                    && let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
                        cursor_node.base_layout.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.ui.get_ui_layout_node_mut(data.selection_entity)
                    && let Some(crate::ecs::ui::layout_types::UiLayoutType::Window(window)) =
                        sel_node.base_layout.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
            .ui
            .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
            .ui
            .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.ui.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(widget_data) = world.ui.get_ui_drag_value_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 snapshot(buffer: &EditBuffer) -> TextSnapshot {
    TextSnapshot {
        text: buffer.text.clone(),
        cursor_position: buffer.cursor,
        selection_start: buffer.selection,
    }
}