From 1975e3b0e307648d24e82c704745f5acbcd773a1 Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Mon, 16 Mar 2026 12:24:05 +0100 Subject: [PATCH] WIP --- render-wasm/src/shapes.rs | 7 +- render-wasm/src/state/text_editor.rs | 421 ++++++++++++++++++++++++++- render-wasm/src/wasm/fills.rs | 82 +++++- render-wasm/src/wasm/text/helpers.rs | 14 +- render-wasm/src/wasm/text_editor.rs | 204 ++++--------- 5 files changed, 568 insertions(+), 160 deletions(-) diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index ef12164896..ad63696de4 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -152,10 +152,11 @@ pub enum ConstraintH { } #[derive(Debug, Clone, PartialEq, Copy)] +#[repr(u8)] pub enum VerticalAlign { - Top, - Center, - Bottom, + Top = 0, + Center = 1, + Bottom = 2, } #[derive(Debug, Clone, PartialEq, Copy)] diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 464a0a39f5..d8b59b80c6 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -1,8 +1,14 @@ #![allow(dead_code)] -use crate::shapes::{TextContent, TextPositionWithAffinity}; +use std::io::Write; + +use crate::shapes::{ + Fill, FontFamily, TextAlign, TextContent, TextDecoration, TextDirection, + TextPositionWithAffinity, TextTransform, VerticalAlign, +}; use crate::uuid::Uuid; -use crate::wasm::text::helpers as text_helpers; +use crate::wasm::text::helpers::{self as text_helpers, find_text_span_at_offset}; +use crate::wasm::text_editor::CursorDirection; use skia_safe::{ textlayout::{Affinity, PositionWithAffinity}, Color, @@ -97,6 +103,158 @@ const CURSOR_WIDTH: f32 = 1.5; const CURSOR_COLOR: Color = Color::BLACK; const CURSOR_BLINK_INTERVAL_MS: f64 = 530.0; +#[derive(Debug)] +pub struct TextEditorStyles { + pub vertical_align: VerticalAlign, + pub text_align: Multiple, // Multiple + pub text_direction: Multiple, // Multiple + pub text_decoration: Multiple, + pub text_transform: Multiple, + pub font_family: Multiple, + pub font_size: Multiple, + pub font_weight: Multiple, + pub font_variant_id: Multiple, + pub line_height: Multiple, + pub letter_spacing: Multiple, + pub fills: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum MultipleState { + Undefined = 0, + Single = 1, + Multiple = 2, +} + +#[derive(Debug)] +pub struct Multiple { + state: MultipleState, + value: Option +} + +impl Multiple { + pub fn empty() -> Self { + Self { + state: MultipleState::Undefined, + value: None + } + } + + pub fn new(state: MultipleState, value: Option) -> Self { + Self { + state, + value + } + } + + pub fn state(&self) -> &MultipleState { + &self.state + } + + pub fn value(&self) -> &Option { + &self.value + } + + pub fn is_undefined(&self) -> bool { + self.state == MultipleState::Undefined + } + + pub fn is_multiple(&self) -> bool { + self.state == MultipleState::Multiple + } + + pub fn is_single(&self) -> bool { + self.state == MultipleState::Single + } + + pub fn is_single_some(&self) -> bool { + !self.is_undefined() && self.value.is_some() + } + + pub fn is_single_none(&self) -> bool { + !self.is_undefined() && self.value.is_none() + } + + pub fn reset(&mut self) { + self.state = MultipleState::Undefined; + self.value = None; + } + + pub fn set(&mut self, value: Option) { + if self.state == MultipleState::Undefined + || self.state == MultipleState::Multiple { + self.state = MultipleState::Single; + } + self.value = value; + } + + pub fn set_single(&mut self, value: Option) { + self.state = MultipleState::Single; + self.value = value; + } + + pub fn set_multiple(&mut self) { + self.state = MultipleState::Multiple; + self.value = None; + } + + pub fn merge(&mut self, value: Option) -> bool + where + T: PartialEq, + { + if self.state == MultipleState::Multiple { + return false; + } + + if self.state == MultipleState::Undefined { + self.set_single(value); + return true; + } + + if self.value.as_ref() != value.as_ref() { + self.set_multiple(); + return false; + } + + self.value = value; + true + } +} + +impl TextEditorStyles { + pub fn new() -> Self { + Self { + vertical_align: VerticalAlign::Top, + text_align: Multiple::empty(), + text_direction: Multiple::empty(), + text_decoration: Multiple::empty(), + text_transform: Multiple::empty(), + font_family: Multiple::empty(), + font_size: Multiple::empty(), + font_weight: Multiple::empty(), + font_variant_id: Multiple::empty(), + line_height: Multiple::empty(), + letter_spacing: Multiple::empty(), + fills: Vec::new(), + } + } + + pub fn reset(&mut self) { + self.text_align.reset(); + self.text_direction.reset(); + self.text_decoration.reset(); + self.text_transform.reset(); + self.font_family.reset(); + self.font_size.reset(); + self.font_weight.reset(); + self.font_variant_id.reset(); + self.line_height.reset(); + self.letter_spacing.reset(); + self.fills.clear(); + } +} + pub struct TextEditorTheme { pub selection_color: Color, pub cursor_width: f32, @@ -113,6 +271,7 @@ pub struct TextEditorState { pub active_shape_id: Option, pub cursor_visible: bool, pub last_blink_time: f64, + pub current_styles: TextEditorStyles, pending_events: Vec, } @@ -131,6 +290,7 @@ impl TextEditorState { cursor_visible: true, last_blink_time: 0.0, pending_events: Vec::new(), + current_styles: TextEditorStyles::new(), } } @@ -205,7 +365,7 @@ impl TextEditorState { offset, )); self.reset_blink(); - self.push_event(crate::state::TextEditorEvent::SelectionChanged); + self.push_event(TextEditorEvent::SelectionChanged); true } @@ -293,6 +453,122 @@ impl TextEditorState { self.push_event(TextEditorEvent::SelectionChanged); } + pub fn update_styles(&mut self, text_content: &TextContent) -> bool { + if self.selection.is_selection() { + let paragraphs = text_content.paragraphs(); + if paragraphs.is_empty() { + return false; + } + + let start = self.selection.start(); + if start.paragraph >= paragraphs.len() { + return false; + } + + let end = self.selection.end(); + let end_paragraph = end.paragraph.min(paragraphs.len() - 1); + + self.current_styles.reset(); + + let mut has_selected_content = false; + let mut has_fills = false; + let mut fills_are_multiple = false; + + for para_idx in start.paragraph..=end_paragraph { + let paragraph = ¶graphs[para_idx]; + + let paragraph_char_count: usize = paragraph + .children() + .iter() + .map(|span| span.text.chars().count()) + .sum(); + + let range_start = if para_idx == start.paragraph { + start.offset.min(paragraph_char_count) + } else { + 0 + }; + + let range_end = if para_idx == end.paragraph { + end.offset.min(paragraph_char_count) + } else { + paragraph_char_count + }; + + if range_start >= range_end { + continue; + } + + has_selected_content = true; + self.current_styles.text_align.merge(Some(paragraph.text_align())); + + let mut char_pos = 0; + for span in paragraph.children() { + let span_len = span.text.chars().count(); + let span_start = char_pos; + let span_end = char_pos + span_len; + char_pos += span_len; + + let selected_start = range_start.max(span_start); + let selected_end = range_end.min(span_end); + if selected_start >= selected_end { + continue; + } + + self.current_styles.text_direction.merge(Some(span.text_direction)); + self.current_styles.text_decoration.merge(span.text_decoration); + self.current_styles.text_transform.merge(span.text_transform); + self.current_styles.font_family.merge(Some(span.font_family.clone())); + self.current_styles.font_size.merge(Some(span.font_size)); + self.current_styles.font_weight.merge(Some(span.font_weight)); + self.current_styles.font_variant_id.merge(Some(span.font_variant_id)); + self.current_styles.line_height.merge(Some(span.line_height)); + self.current_styles.letter_spacing.merge(Some(span.letter_spacing)); + + if !fills_are_multiple { + if !has_fills { + self.current_styles.fills = span.fills.clone(); + has_fills = true; + } else if self.current_styles.fills != span.fills { + fills_are_multiple = true; + self.current_styles.fills.clear(); + } + } + } + } + + return has_selected_content; + } + // It is a caret. + let focus = self.selection.focus; + let paragraphs = text_content.paragraphs(); + let Some(current_paragraph) = paragraphs.get(focus.paragraph) else { + return false; + }; + let current_offset = focus.offset; + let current_text_span = find_text_span_at_offset(current_paragraph, current_offset); + + self.current_styles.text_align.set_single(Some(current_paragraph.text_align())); + if let Some((text_span_index, _)) = current_text_span { + if let Some(text_span) = current_paragraph.children().get(text_span_index) { + self.current_styles.text_direction.set_single(Some(text_span.text_direction)); + self.current_styles.text_decoration.set_single(text_span.text_decoration); + self.current_styles.text_transform.set_single(text_span.text_transform); + self.current_styles.font_family.set_single(Some(text_span.font_family.clone())); + self.current_styles.font_size.set_single(Some(text_span.font_size)); + self.current_styles.font_weight.set_single(Some(text_span.font_weight)); + self.current_styles.font_variant_id.set_single(Some(text_span.font_variant_id)); + self.current_styles.line_height.set_single(Some(text_span.line_height)); + self.current_styles.letter_spacing.set_single(Some(text_span.letter_spacing)); + self.current_styles.fills = text_span.fills.clone(); + } + } else { + self.current_styles.line_height.set_single(Some(current_paragraph.line_height())); + self.current_styles.letter_spacing.set_single(Some(current_paragraph.letter_spacing())); + } + true + } + pub fn update_blink(&mut self, timestamp_ms: f64) { if !self.has_focus { return; @@ -329,6 +605,145 @@ impl TextEditorState { pub fn has_pending_events(&self) -> bool { !self.pending_events.is_empty() } + + pub fn delete_backward(&mut self, text_content: &mut TextContent, word_boundary: bool) { + if self.selection.is_selection() { + text_helpers::delete_selection_range(text_content, &self.selection); + let start = self.selection.start(); + let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs()); + self.selection.set_caret(clamped); + } else if word_boundary { + let cursor = self.selection.focus; + if let Some(new_cursor) = text_helpers::delete_word_before(text_content, &cursor) { + self.selection.set_caret(new_cursor); + } + } else { + let cursor = self.selection.focus; + if let Some(new_cursor) = text_helpers::delete_char_before(text_content, &cursor) { + self.selection.set_caret(new_cursor); + } + } + + text_content.layout.paragraphs.clear(); + text_content.layout.paragraph_builders.clear(); + + self.reset_blink(); + self.push_event(TextEditorEvent::ContentChanged); + self.push_event(TextEditorEvent::NeedsLayout); + } + + pub fn delete_forward(&mut self, text_content: &mut TextContent, word_boundary: bool) { + if self.selection.is_selection() { + text_helpers::delete_selection_range(text_content, &self.selection); + let start = self.selection.start(); + let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs()); + self.selection.set_caret(clamped); + } else if word_boundary { + let cursor = self.selection.focus; + text_helpers::delete_word_after(text_content, &cursor); + let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs()); + self.selection.set_caret(clamped); + } else { + let cursor = self.selection.focus; + text_helpers::delete_char_after(text_content, &cursor); + let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs()); + self.selection.set_caret(clamped); + } + + text_content.layout.paragraphs.clear(); + text_content.layout.paragraph_builders.clear(); + + self.reset_blink(); + self.push_event(TextEditorEvent::ContentChanged); + self.push_event(TextEditorEvent::NeedsLayout); + } + + pub fn insert_paragraph(&mut self, text_content: &mut TextContent) { + if self.selection.is_selection() { + text_helpers::delete_selection_range(text_content, &self.selection); + let start = self.selection.start(); + self.selection.set_caret(start); + } + + let cursor = self.selection.focus; + if text_helpers::split_paragraph_at_cursor(text_content, &cursor) { + let new_cursor = + TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0); + self.selection.set_caret(new_cursor); + } + + text_content.layout.paragraphs.clear(); + text_content.layout.paragraph_builders.clear(); + + self.reset_blink(); + self.push_event(TextEditorEvent::ContentChanged); + self.push_event(TextEditorEvent::NeedsLayout); + } + + pub fn move_cursor( + &mut self, + text_content: &TextContent, + direction: CursorDirection, + word_boundary: bool, + extend_selection: bool, + ) -> bool { + let paragraphs = text_content.paragraphs(); + if paragraphs.is_empty() { + return false; + } + + let focus = self.selection.focus; + + // Get the text direction of the span at the current cursor position + let text_span_text_direction = if focus.paragraph < paragraphs.len() { + text_helpers::get_text_span_text_direction_at_offset( + ¶graphs[focus.paragraph], + focus.offset, + ) + } else { + TextDirection::LTR + }; + + // For horizontal navigation, swap Backward/Forward when in RTL text + let adjusted_direction = if text_span_text_direction == TextDirection::RTL { + match direction { + CursorDirection::Backward => CursorDirection::Forward, + CursorDirection::Forward => CursorDirection::Backward, + other => other, + } + } else { + direction + }; + + let new_cursor = match adjusted_direction { + CursorDirection::Backward => { + text_helpers::move_cursor_backward(&focus, paragraphs, word_boundary) + } + CursorDirection::Forward => { + text_helpers::move_cursor_forward(&focus, paragraphs, word_boundary) + } + CursorDirection::LineBefore => { + text_helpers::move_cursor_up(&focus, paragraphs, text_content) + } + CursorDirection::LineAfter => { + text_helpers::move_cursor_down(&focus, paragraphs, text_content) + } + CursorDirection::LineStart => text_helpers::move_cursor_line_start(&focus, paragraphs), + CursorDirection::LineEnd => text_helpers::move_cursor_line_end(&focus, paragraphs), + }; + + if extend_selection { + self.selection.extend_to(new_cursor); + } else { + self.selection.set_caret(new_cursor); + } + + self.update_styles(text_content); + + self.reset_blink(); + self.push_event(TextEditorEvent::SelectionChanged); + true + } } fn is_word_char(c: char) -> bool { diff --git a/render-wasm/src/wasm/fills.rs b/render-wasm/src/wasm/fills.rs index 8513856455..5813a921d7 100644 --- a/render-wasm/src/wasm/fills.rs +++ b/render-wasm/src/wasm/fills.rs @@ -36,12 +36,42 @@ impl From for shapes::Fill { } } +impl TryFrom<&shapes::Fill> for RawFillData { + type Error = String; + + fn try_from(fill: &shapes::Fill) -> Result { + match fill { + shapes::Fill::Solid(shapes::SolidColor(color)) => { + Ok(RawFillData::Solid(solid::RawSolidData { + color: ((color.a() as u32) << 24) + | ((color.r() as u32) << 16) + | ((color.g() as u32) << 8) + | (color.b() as u32), + })) + } + shapes::Fill::LinearGradient(_) => { + Err("LinearGradient serialization is not implemented".to_string()) + } + shapes::Fill::RadialGradient(_) => { + Err("RadialGradient serialization is not implemented".to_string()) + } + shapes::Fill::Image(_) => Err("Image fill serialization is not implemented".to_string()), + } + } +} + impl From<[u8; RAW_FILL_DATA_SIZE]> for RawFillData { fn from(bytes: [u8; RAW_FILL_DATA_SIZE]) -> Self { unsafe { std::mem::transmute(bytes) } } } +impl From for [u8; RAW_FILL_DATA_SIZE] { + fn from(fill_data: RawFillData) -> Self { + unsafe { std::mem::transmute(fill_data) } + } +} + impl TryFrom<&[u8]> for RawFillData { type Error = String; fn try_from(bytes: &[u8]) -> Result { @@ -54,7 +84,7 @@ impl TryFrom<&[u8]> for RawFillData { } // FIXME: return Result -pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec { +pub fn read_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec { buffer .chunks_exact(RAW_FILL_DATA_SIZE) .take(num_fills) @@ -66,6 +96,16 @@ pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec) -> Vec { + fills + .iter() + .map(|fill| RawFillData::try_from(fill).expect("Unsupported fill type for serialization")) + .flat_map(|raw_fill| <[u8; RAW_FILL_DATA_SIZE]>::from(raw_fill)) + .collect() +} + #[no_mangle] #[wasm_error] pub extern "C" fn set_shape_fills() -> Result<()> { @@ -74,7 +114,7 @@ pub extern "C" fn set_shape_fills() -> Result<()> { // The first byte contains the actual number of fills let num_fills = bytes.first().copied().unwrap_or(0) as usize; // Skip the first 4 bytes (header with fill count) and parse only the actual fills - let fills = parse_fills_from_bytes(&bytes[4..], num_fills); + let fills = read_fills_from_bytes(&bytes[4..], num_fills); shape.set_fills(fills); mem::free_bytes()?; }); @@ -124,4 +164,42 @@ mod tests { RawFillData::Solid(solid::RawSolidData { color: 0xfffabada }) ); } + + #[test] + fn test_write_fills_to_bytes_single_solid() { + let fills = vec![shapes::Fill::Solid(shapes::SolidColor(shapes::Color::new( + 0xfffabada, + )))]; + + let bytes = write_fills_to_bytes(fills); + + assert_eq!(bytes.len(), RAW_FILL_DATA_SIZE); + assert_eq!(bytes[0], 0x00); + assert_eq!( + RawFillData::try_from(&bytes[..]).unwrap(), + RawFillData::Solid(solid::RawSolidData { color: 0xfffabada }) + ); + } + + #[test] + fn test_write_fills_to_bytes_roundtrip_multiple() { + let fills = vec![ + shapes::Fill::Solid(shapes::SolidColor(shapes::Color::new(0xfffabada))), + shapes::Fill::Solid(shapes::SolidColor(shapes::Color::new(0xff112233))), + ]; + + let bytes = write_fills_to_bytes(fills); + let decoded: Vec = bytes + .chunks_exact(RAW_FILL_DATA_SIZE) + .map(|chunk| RawFillData::try_from(chunk).unwrap()) + .collect(); + + assert_eq!( + decoded, + vec![ + RawFillData::Solid(solid::RawSolidData { color: 0xfffabada }), + RawFillData::Solid(solid::RawSolidData { color: 0xff112233 }), + ] + ); + } } diff --git a/render-wasm/src/wasm/text/helpers.rs b/render-wasm/src/wasm/text/helpers.rs index 88ec07754c..b0c57c5288 100644 --- a/render-wasm/src/wasm/text/helpers.rs +++ b/render-wasm/src/wasm/text/helpers.rs @@ -1,4 +1,4 @@ -use crate::shapes::{Paragraph, TextContent, TextPositionWithAffinity}; +use crate::shapes::{Paragraph, TextContent, TextDirection, TextPositionWithAffinity}; use crate::state::TextSelection; /// Get total character count in a paragraph. @@ -10,11 +10,11 @@ pub fn paragraph_char_count(para: &Paragraph) -> usize { } /// Get the text direction of the span at a given offset in a paragraph. -pub fn get_span_text_direction_at_offset( +pub fn get_text_span_text_direction_at_offset( para: &Paragraph, char_offset: usize, -) -> skia_safe::textlayout::TextDirection { - if let Some((span_idx, _)) = find_span_at_offset(para, char_offset) { +) -> TextDirection { + if let Some((span_idx, _)) = find_text_span_at_offset(para, char_offset) { if let Some(span) = para.children().get(span_idx) { return span.text_direction; } @@ -258,7 +258,7 @@ pub fn paragraph_text_char_at(para: &Paragraph, offset: usize) -> Option { None } -pub fn find_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, usize)> { +pub fn find_text_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, usize)> { let children = para.children(); let mut accumulated = 0; for (span_idx, span) in children.iter().enumerate() { @@ -338,7 +338,7 @@ pub fn insert_text_at_cursor( return Some(text.chars().count()); } - let (span_idx, offset_in_span) = find_span_at_offset(para, cursor.offset)?; + let (span_idx, offset_in_span) = find_text_span_at_offset(para, cursor.offset)?; let children = para.children_mut(); let span = &mut children[span_idx]; @@ -690,7 +690,7 @@ pub fn split_paragraph_at_cursor( let para = ¶graphs[cursor.paragraph]; - let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.offset) else { + let Some((span_idx, offset_in_span)) = find_text_span_at_offset(para, cursor.offset) else { return false; }; diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 3e09bfaabe..ba5969f42f 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -3,12 +3,12 @@ use macros::{wasm_error, ToJs}; use crate::math::{Matrix, Point, Rect}; use crate::mem; use crate::shapes::{Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign}; -use crate::state::TextSelection; +use crate::state::{TextEditorEvent, TextSelection}; use crate::utils::uuid_from_u32_quartet; use crate::utils::uuid_to_u32_quartet; use crate::wasm::text::helpers as text_helpers; use crate::{with_state, with_state_mut, STATE}; -use skia_safe::{textlayout::TextDirection, Color}; +use skia_safe::Color; #[derive(PartialEq, ToJs)] #[repr(u8)] @@ -355,10 +355,10 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> { state.text_editor_state.reset_blink(); state .text_editor_state - .push_event(crate::state::TextEditorEvent::ContentChanged); + .push_event(TextEditorEvent::ContentChanged); state .text_editor_state - .push_event(crate::state::TextEditorEvent::NeedsLayout); + .push_event(TextEditorEvent::NeedsLayout); state.render_state.mark_touched(shape_id); }); @@ -386,36 +386,9 @@ pub extern "C" fn text_editor_delete_backward(word_boundary: bool) { return; }; - let selection = state.text_editor_state.selection; - - if selection.is_selection() { - text_helpers::delete_selection_range(text_content, &selection); - let start = selection.start(); - let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs()); - state.text_editor_state.selection.set_caret(clamped); - } else if word_boundary { - let cursor = selection.focus; - if let Some(new_cursor) = text_helpers::delete_word_before(text_content, &cursor) { - state.text_editor_state.selection.set_caret(new_cursor); - } - } else { - let cursor = selection.focus; - if let Some(new_cursor) = text_helpers::delete_char_before(text_content, &cursor) { - state.text_editor_state.selection.set_caret(new_cursor); - } - } - - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); - - state.text_editor_state.reset_blink(); state .text_editor_state - .push_event(crate::state::TextEditorEvent::ContentChanged); - state - .text_editor_state - .push_event(crate::state::TextEditorEvent::NeedsLayout); - + .delete_backward(text_content, word_boundary); state.render_state.mark_touched(shape_id); }); } @@ -439,36 +412,9 @@ pub extern "C" fn text_editor_delete_forward(word_boundary: bool) { return; }; - let selection = state.text_editor_state.selection; - - if selection.is_selection() { - text_helpers::delete_selection_range(text_content, &selection); - let start = selection.start(); - let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs()); - state.text_editor_state.selection.set_caret(clamped); - } else if word_boundary { - let cursor = selection.focus; - text_helpers::delete_word_after(text_content, &cursor); - let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs()); - state.text_editor_state.selection.set_caret(clamped); - } else { - let cursor = selection.focus; - text_helpers::delete_char_after(text_content, &cursor); - let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs()); - state.text_editor_state.selection.set_caret(clamped); - } - - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); - - state.text_editor_state.reset_blink(); state .text_editor_state - .push_event(crate::state::TextEditorEvent::ContentChanged); - state - .text_editor_state - .push_event(crate::state::TextEditorEvent::NeedsLayout); - + .delete_forward(text_content, word_boundary); state.render_state.mark_touched(shape_id); }); } @@ -492,33 +438,7 @@ pub extern "C" fn text_editor_insert_paragraph() { return; }; - let selection = state.text_editor_state.selection; - - if selection.is_selection() { - text_helpers::delete_selection_range(text_content, &selection); - let start = selection.start(); - state.text_editor_state.selection.set_caret(start); - } - - let cursor = state.text_editor_state.selection.focus; - - if text_helpers::split_paragraph_at_cursor(text_content, &cursor) { - let new_cursor = - TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0); - state.text_editor_state.selection.set_caret(new_cursor); - } - - text_content.layout.paragraphs.clear(); - text_content.layout.paragraph_builders.clear(); - - state.text_editor_state.reset_blink(); - state - .text_editor_state - .push_event(crate::state::TextEditorEvent::ContentChanged); - state - .text_editor_state - .push_event(crate::state::TextEditorEvent::NeedsLayout); - + state.text_editor_state.insert_paragraph(text_content); state.render_state.mark_touched(shape_id); }); } @@ -550,63 +470,12 @@ pub extern "C" fn text_editor_move_cursor( return; }; - let paragraphs = text_content.paragraphs(); - if paragraphs.is_empty() { - return; - } - - let current = state.text_editor_state.selection.focus; - - // Get the text direction of the span at the current cursor position - let span_text_direction = if current.paragraph < paragraphs.len() { - text_helpers::get_span_text_direction_at_offset( - ¶graphs[current.paragraph], - current.offset, - ) - } else { - TextDirection::LTR - }; - - // For horizontal navigation, swap Backward/Forward when in RTL text - let adjusted_direction = if span_text_direction == TextDirection::RTL { - match direction { - CursorDirection::Backward => CursorDirection::Forward, - CursorDirection::Forward => CursorDirection::Backward, - other => other, - } - } else { - direction - }; - - let new_cursor = match adjusted_direction { - CursorDirection::Backward => { - text_helpers::move_cursor_backward(¤t, paragraphs, word_boundary) - } - CursorDirection::Forward => { - text_helpers::move_cursor_forward(¤t, paragraphs, word_boundary) - } - CursorDirection::LineBefore => { - text_helpers::move_cursor_up(¤t, paragraphs, text_content) - } - CursorDirection::LineAfter => { - text_helpers::move_cursor_down(¤t, paragraphs, text_content) - } - CursorDirection::LineStart => { - text_helpers::move_cursor_line_start(¤t, paragraphs) - } - CursorDirection::LineEnd => text_helpers::move_cursor_line_end(¤t, paragraphs), - }; - - if extend_selection { - state.text_editor_state.selection.extend_to(new_cursor); - } else { - state.text_editor_state.selection.set_caret(new_cursor); - } - - state.text_editor_state.reset_blink(); - state - .text_editor_state - .push_event(crate::state::TextEditorEvent::SelectionChanged); + state.text_editor_state.move_cursor( + text_content, + direction, + word_boundary, + extend_selection, + ); }); } @@ -648,6 +517,52 @@ pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 { }) } +#[no_mangle] +pub extern "C" fn text_editor_get_current_styles() -> *mut u8 { + with_state_mut!(state, { + if !state.text_editor_state.has_focus { + return std::ptr::null_mut(); + } + + if state.text_editor_state.selection.is_collapsed() { + return std::ptr::null_mut(); + } + + let Some(shape_id) = state.text_editor_state.active_shape_id else { + return std::ptr::null_mut(); + }; + + let Some(shape) = state.shapes.get(&shape_id) else { + return std::ptr::null_mut(); + }; + + let Type::Text(text_content) = &shape.shape_type else { + return std::ptr::null_mut(); + }; + + let mut bytes = vec![0u8; 1024]; + bytes[0..4].copy_from_slice(&u32::to_le_bytes(state.text_editor_state.current_styles.vertical_align as u32)); + bytes[4..8].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_align.state() as u32)); + bytes[8..12].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_direction.state() as u32)); + bytes[12..16].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_decoration.state() as u32)); + bytes[16..20].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.text_transform.state() as u32)); + bytes[20..24].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_family.state() as u32)); + bytes[24..28].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_size.state() as u32)); + bytes[28..32].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_weight.state() as u32)); + bytes[32..36].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.font_variant_id.state() as u32)); + bytes[36..40].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.line_height.state() as u32)); + bytes[40..44].copy_from_slice(&u32::to_le_bytes(*state.text_editor_state.current_styles.letter_spacing.state() as u32)); + bytes[44..48].copy_from_slice(&usize::to_le_bytes(state.text_editor_state.current_styles.fills.len())); + + let offset: usize = 48; + for fill in state.text_editor_state.current_styles.fills.iter() { + + } + + std::ptr::null_mut() + }) +} + #[no_mangle] pub extern "C" fn text_editor_get_selection_rects() -> *mut u8 { with_state_mut!(state, { @@ -673,7 +588,6 @@ pub extern "C" fn text_editor_get_selection_rects() -> *mut u8 { let selection = &state.text_editor_state.selection; let rects = get_selection_rects(text_content, selection, shape); - if rects.is_empty() { return std::ptr::null_mut(); }