This commit is contained in:
Aitor Moreno 2026-03-16 12:24:05 +01:00
parent d9073b1828
commit 1975e3b0e3
5 changed files with 568 additions and 160 deletions

View File

@ -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)]

View File

@ -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<TextAlign>, // Multiple
pub text_direction: Multiple<TextDirection>, // Multiple
pub text_decoration: Multiple<TextDecoration>,
pub text_transform: Multiple<TextTransform>,
pub font_family: Multiple<FontFamily>,
pub font_size: Multiple<f32>,
pub font_weight: Multiple<i32>,
pub font_variant_id: Multiple<Uuid>,
pub line_height: Multiple<f32>,
pub letter_spacing: Multiple<f32>,
pub fills: Vec<Fill>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(u8)]
pub enum MultipleState {
Undefined = 0,
Single = 1,
Multiple = 2,
}
#[derive(Debug)]
pub struct Multiple<T> {
state: MultipleState,
value: Option<T>
}
impl<T> Multiple<T> {
pub fn empty() -> Self {
Self {
state: MultipleState::Undefined,
value: None
}
}
pub fn new(state: MultipleState, value: Option<T>) -> Self {
Self {
state,
value
}
}
pub fn state(&self) -> &MultipleState {
&self.state
}
pub fn value(&self) -> &Option<T> {
&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<T>) {
if self.state == MultipleState::Undefined
|| self.state == MultipleState::Multiple {
self.state = MultipleState::Single;
}
self.value = value;
}
pub fn set_single(&mut self, value: Option<T>) {
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<T>) -> 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<Uuid>,
pub cursor_visible: bool,
pub last_blink_time: f64,
pub current_styles: TextEditorStyles,
pending_events: Vec<TextEditorEvent>,
}
@ -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 = &paragraphs[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(
&paragraphs[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 {

View File

@ -36,12 +36,42 @@ impl From<RawFillData> for shapes::Fill {
}
}
impl TryFrom<&shapes::Fill> for RawFillData {
type Error = String;
fn try_from(fill: &shapes::Fill) -> Result<Self, Self::Error> {
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<RawFillData> 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<Self, Self::Error> {
@ -54,7 +84,7 @@ impl TryFrom<&[u8]> for RawFillData {
}
// FIXME: return Result
pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec<shapes::Fill> {
pub fn read_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec<shapes::Fill> {
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<shapes::Fi
.collect()
}
/// Serializes raw fills to bytes using the same fixed-size chunk layout consumed by
/// `read_fills_from_bytes`.
pub fn write_fills_to_bytes(fills: Vec<shapes::Fill>) -> Vec<u8> {
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<RawFillData> = 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 }),
]
);
}
}

View File

@ -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<char> {
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 = &paragraphs[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;
};

View File

@ -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(
&paragraphs[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(&current, paragraphs, word_boundary)
}
CursorDirection::Forward => {
text_helpers::move_cursor_forward(&current, paragraphs, word_boundary)
}
CursorDirection::LineBefore => {
text_helpers::move_cursor_up(&current, paragraphs, text_content)
}
CursorDirection::LineAfter => {
text_helpers::move_cursor_down(&current, paragraphs, text_content)
}
CursorDirection::LineStart => {
text_helpers::move_cursor_line_start(&current, paragraphs)
}
CursorDirection::LineEnd => text_helpers::move_cursor_line_end(&current, 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();
}