diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index d25c713e81..46a32ef16e 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -249,7 +249,7 @@ (let [fonts (f/get-content-fonts content) fallback-fonts (fonts-from-text-content content true) all-fonts (concat fonts fallback-fonts) - result (f/store-fonts shape-id all-fonts)] + result (f/store-fonts all-fonts)] (f/load-fallback-fonts-for-editor! fallback-fonts) (h/call wasm/internal-module "_update_shape_text_layout") result)) diff --git a/frontend/src/app/render_wasm/api/shared.js b/frontend/src/app/render_wasm/api/shared.js index 172cb2a97d..e5456d91d2 100644 --- a/frontend/src/app/render_wasm/api/shared.js +++ b/frontend/src/app/render_wasm/api/shared.js @@ -240,3 +240,12 @@ export const RawGrowType = { "auto-height": 2, }; +export const CursorDirection = { + "backward": 0, + "forward": 1, + "line-before": 2, + "line-after": 3, + "line-start": 4, + "line-end": 5, +}; + diff --git a/render-wasm/src/render/text_editor.rs b/render-wasm/src/render/text_editor.rs index 4c9bff3953..be37ce627d 100644 --- a/render-wasm/src/render/text_editor.rs +++ b/render-wasm/src/render/text_editor.rs @@ -1,12 +1,7 @@ use crate::shapes::{Shape, TextContent, Type, VerticalAlign}; -use crate::state::{TextCursor, TextEditorState, TextSelection}; +use crate::state::{TextEditorState, TextSelection}; use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle}; -use skia_safe::{Canvas, Color, Matrix, Paint, 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; +use skia_safe::{BlendMode, Canvas, Matrix, Paint, Rect}; pub fn render_overlay( canvas: &Canvas, @@ -26,23 +21,28 @@ 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 +50,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 +62,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 +83,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 +122,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 +133,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 +144,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 +159,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 +218,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..d7474cc92f 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,41 @@ 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 +151,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 +158,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 +165,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 +198,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..37758f5bb1 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -1,3 +1,5 @@ +use macros::ToJs; + use crate::math::{Matrix, Point, Rect}; use crate::mem; use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign}; @@ -6,6 +8,18 @@ use crate::utils::uuid_from_u32_quartet; use crate::utils::uuid_to_u32_quartet; use crate::{with_state, with_state_mut, STATE}; +#[derive(PartialEq, ToJs)] +#[repr(u8)] +#[allow(dead_code)] +pub enum CursorDirection { + Backward = 0, + Forward = 1, + LineBefore = 2, + LineAfter = 3, + LineStart = 4, + LineEnd = 5, +} + // ============================================================================ // STATE MANAGEMENT // ============================================================================ @@ -462,7 +476,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,13 +502,16 @@ 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), - _ => current, + 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), }; if extend_selection { @@ -504,11 +521,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 +956,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 +969,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);