mirror of
https://github.com/penpot/penpot.git
synced 2026-05-03 07:08:44 +00:00
WIP
This commit is contained in:
parent
d9073b1828
commit
1975e3b0e3
@ -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)]
|
||||
|
||||
@ -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 = ¶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 {
|
||||
|
||||
@ -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 }),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 = ¶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;
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user