From 1a9ffaaccc57cf4c00909c49768fcb47b2b3b0cc Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Mon, 2 Feb 2026 09:34:37 +0100 Subject: [PATCH] :recycle: Refactor minor things --- render-wasm/src/render/text_editor.rs | 36 +++++++++++++-------------- render-wasm/src/state/text_editor.rs | 29 +++++++++++++-------- render-wasm/src/wasm/text_editor.rs | 36 ++++++++++++++++----------- 3 files changed, 58 insertions(+), 43 deletions(-) diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index 4c9bff3953..1a3839bccf 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -1,12 +1,8 @@ use crate::shapes::{Shape, TextContent, Type, VerticalAlign}; use crate::state::{TextCursor, TextEditorState, TextSelection}; use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle}; -use skia_safe::{Canvas, Color, Matrix, Paint, Rect}; +use skia_safe::{Canvas, Color, Matrix, Paint, BlendMode, Rect}; -const CURSOR_WIDTH: f32 = 1.5; -/// FIXME: Use theme color, take into account background color for contrast -const SELECTION_COLOR: Color = Color::from_argb(80, 66, 133, 244); -const CURSOR_COLOR: Color = Color::BLACK; pub fn render_overlay( canvas: &Canvas, @@ -26,23 +22,23 @@ pub fn render_overlay( canvas.concat(transform); if editor_state.selection.is_selection() { - render_selection(canvas, &editor_state.selection, text_content, shape); + render_selection(canvas, &editor_state, text_content, shape); } if editor_state.cursor_visible { - render_cursor(canvas, &editor_state.selection.focus, text_content, shape); + render_cursor(canvas, &editor_state, text_content, shape); } canvas.restore(); } -fn render_cursor(canvas: &Canvas, cursor: &TextCursor, text_content: &TextContent, shape: &Shape) { - let Some(rect) = calculate_cursor_rect(cursor, text_content, shape) else { +fn render_cursor(canvas: &Canvas, editor_state: &TextEditorState, text_content: &TextContent, shape: &Shape) { + let Some(rect) = calculate_cursor_rect(editor_state, text_content, shape) else { return; }; let mut paint = Paint::default(); - paint.set_color(CURSOR_COLOR); + paint.set_color(editor_state.theme.cursor_color); paint.set_anti_alias(true); canvas.draw_rect(rect, &paint); @@ -50,10 +46,11 @@ fn render_cursor(canvas: &Canvas, cursor: &TextCursor, text_content: &TextConten fn render_selection( canvas: &Canvas, - selection: &TextSelection, + editor_state: &TextEditorState, text_content: &TextContent, shape: &Shape, ) { + let selection = &editor_state.selection; let rects = calculate_selection_rects(selection, text_content, shape); if rects.is_empty() { @@ -61,9 +58,9 @@ fn render_selection( } let mut paint = Paint::default(); - paint.set_color(SELECTION_COLOR); + paint.set_blend_mode(BlendMode::Multiply); + paint.set_color(editor_state.theme.selection_color); paint.set_anti_alias(true); - for rect in rects { canvas.draw_rect(rect, &paint); } @@ -82,10 +79,11 @@ fn vertical_align_offset( } fn calculate_cursor_rect( - cursor: &TextCursor, + editor_state: &TextEditorState, text_content: &TextContent, shape: &Shape, ) -> Option { + let cursor = editor_state.selection.focus; let paragraphs = text_content.paragraphs(); if cursor.paragraph >= paragraphs.len() { return None; @@ -120,7 +118,7 @@ fn calculate_cursor_rect( } else if char_pos == 0 { let rects = laid_out_para.get_rects_for_range( 0..1, - RectHeightStyle::Tight, + RectHeightStyle::Max, RectWidthStyle::Tight, ); if !rects.is_empty() { @@ -131,7 +129,7 @@ fn calculate_cursor_rect( } else if char_pos >= para_char_count { let rects = laid_out_para.get_rects_for_range( para_char_count.saturating_sub(1)..para_char_count, - RectHeightStyle::Tight, + RectHeightStyle::Max, RectWidthStyle::Tight, ); if !rects.is_empty() { @@ -142,7 +140,7 @@ fn calculate_cursor_rect( } else { let rects = laid_out_para.get_rects_for_range( char_pos..char_pos + 1, - RectHeightStyle::Tight, + RectHeightStyle::Max, RectWidthStyle::Tight, ); if !rects.is_empty() { @@ -157,7 +155,7 @@ fn calculate_cursor_rect( return Some(Rect::from_xywh( selrect.x() + cursor_x, selrect.y() + y_offset, - CURSOR_WIDTH, + editor_state.theme.cursor_width, cursor_height, )); } @@ -216,7 +214,7 @@ fn calculate_selection_rects( use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle}; let text_boxes = laid_out_para.get_rects_for_range( range_start..range_end, - RectHeightStyle::Tight, + RectHeightStyle::Max, RectWidthStyle::Tight, ); diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index e8766d49ae..b1a632e3c8 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -2,6 +2,7 @@ use crate::shapes::TextPositionWithAffinity; use crate::uuid::Uuid; +use skia_safe::{Color}; /// Cursor position within text content. /// Uses character offsets for precise positioning. @@ -105,27 +106,42 @@ pub enum EditorEvent { NeedsLayout = 3, } +/// FIXME: It should be better to get these constants from the frontend through the API. +const SELECTION_COLOR: Color = Color::from_argb(255, 0, 209, 184); +const CURSOR_WIDTH: f32 = 1.5; +const CURSOR_COLOR: Color = Color::BLACK; +const CURSOR_BLINK_INTERVAL_MS: f64 = 530.0; + +pub struct TextEditorTheme { + pub selection_color: Color, + pub cursor_width: f32, + pub cursor_color: Color, +} + pub struct TextEditorState { + pub theme: TextEditorTheme, pub selection: TextSelection, pub is_active: bool, pub active_shape_id: Option, pub cursor_visible: bool, pub last_blink_time: f64, - pub x_affinity: Option, pending_events: Vec, } -const CURSOR_BLINK_INTERVAL_MS: f64 = 530.0; impl TextEditorState { pub fn new() -> Self { Self { + theme: TextEditorTheme { + selection_color: SELECTION_COLOR, + cursor_width: CURSOR_WIDTH, + cursor_color: CURSOR_COLOR, + }, selection: TextSelection::new(), is_active: false, active_shape_id: None, cursor_visible: true, last_blink_time: 0.0, - x_affinity: None, pending_events: Vec::new(), } } @@ -136,7 +152,6 @@ impl TextEditorState { self.cursor_visible = true; self.last_blink_time = 0.0; self.selection = TextSelection::new(); - self.x_affinity = None; self.pending_events.clear(); } @@ -144,7 +159,6 @@ impl TextEditorState { self.is_active = false; self.active_shape_id = None; self.cursor_visible = false; - self.x_affinity = None; self.pending_events.clear(); } @@ -152,7 +166,6 @@ impl TextEditorState { let cursor = TextCursor::new(position.paragraph as usize, position.offset as usize); self.selection.set_caret(cursor); self.reset_blink(); - self.clear_x_affinity(); self.push_event(EditorEvent::SelectionChanged); } @@ -186,10 +199,6 @@ impl TextEditorState { self.last_blink_time = 0.0; } - pub fn clear_x_affinity(&mut self) { - self.x_affinity = None; - } - pub fn push_event(&mut self, event: EditorEvent) { if self.pending_events.last() != Some(&event) { self.pending_events.push(event); diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 66f11d9261..b437ec7c63 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -1,3 +1,5 @@ +use std::io::Cursor; + use crate::math::{Matrix, Point, Rect}; use crate::mem; use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign}; @@ -6,6 +8,17 @@ use crate::utils::uuid_from_u32_quartet; use crate::utils::uuid_to_u32_quartet; use crate::{with_state, with_state_mut, STATE}; +#[derive(Debug, Copy, Clone, PartialEq)] +#[repr(u8)] +pub enum CursorDirection { + Backward, + Forward, + LineBefore, + LineAfter, + LineStart, + LineEnd, +} + // ============================================================================ // STATE MANAGEMENT // ============================================================================ @@ -462,7 +475,7 @@ pub extern "C" fn text_editor_insert_paragraph() { // ============================================================================ #[no_mangle] -pub extern "C" fn text_editor_move_cursor(direction: u8, extend_selection: bool) { +pub extern "C" fn text_editor_move_cursor(direction: CursorDirection, extend_selection: bool) { with_state_mut!(state, { if !state.text_editor_state.is_active { return; @@ -488,12 +501,12 @@ pub extern "C" fn text_editor_move_cursor(direction: u8, extend_selection: bool) let current = state.text_editor_state.selection.focus; let new_cursor = match direction { - 0 => move_cursor_left(¤t, paragraphs), - 1 => move_cursor_right(¤t, paragraphs), - 2 => move_cursor_up(¤t, paragraphs, text_content, shape), - 3 => move_cursor_down(¤t, paragraphs, text_content, shape), - 4 => move_cursor_line_start(¤t, paragraphs), - 5 => move_cursor_line_end(¤t, paragraphs), + CursorDirection::Backward => move_cursor_backward(¤t, paragraphs), + CursorDirection::Forward => move_cursor_forward(¤t, paragraphs), + CursorDirection::LineBefore => move_cursor_up(¤t, paragraphs, text_content, shape), + CursorDirection::LineAfter => move_cursor_down(¤t, paragraphs, text_content, shape), + CursorDirection::LineStart => move_cursor_line_start(¤t, paragraphs), + CursorDirection::LineEnd => move_cursor_line_end(¤t, paragraphs), _ => current, }; @@ -504,11 +517,6 @@ pub extern "C" fn text_editor_move_cursor(direction: u8, extend_selection: bool) } state.text_editor_state.reset_blink(); - - if direction == 0 || direction == 1 || direction == 4 || direction == 5 { - state.text_editor_state.clear_x_affinity(); - } - state .text_editor_state .push_event(crate::state::EditorEvent::SelectionChanged); @@ -944,7 +952,7 @@ fn clamp_cursor(cursor: TextCursor, paragraphs: &[Paragraph]) -> TextCursor { } /// Move cursor left by one character. -fn move_cursor_left(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor { +fn move_cursor_backward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor { if cursor.char_offset > 0 { TextCursor::new(cursor.paragraph, cursor.char_offset - 1) } else if cursor.paragraph > 0 { @@ -957,7 +965,7 @@ fn move_cursor_left(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor } /// Move cursor right by one character. -fn move_cursor_right(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor { +fn move_cursor_forward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor { let para = ¶graphs[cursor.paragraph]; let char_count = paragraph_char_count(para);