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::InteractionSnapshot;
use super::text_cursor::{
byte_index_at_x, char_position_from_line_col, line_col_from_char_position, line_count,
line_start_char_index, line_text, measure_text_width, next_word_boundary, prev_word_boundary,
};
pub(super) struct RichTextEditorContext<'a> {
pub(super) entity: freecs::Entity,
pub(super) interaction: &'a InteractionSnapshot,
pub(super) data: &'a crate::ecs::ui::components::UiRichTextEditorData,
pub(super) focused_entity: Option<freecs::Entity>,
pub(super) frame_chars: &'a [char],
pub(super) frame_keys: &'a [(KeyCode, bool)],
pub(super) ctrl_held: bool,
pub(super) shift_held: bool,
pub(super) mouse_position: Vec2,
pub(super) current_time: f64,
pub(super) dpi_scale: f32,
}
pub(super) 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.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 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.ui.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.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 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.ui.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.ui.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.ui.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.ui.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.ui.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;
}
}