penpot/render-wasm/src/wasm/text_editor.rs
2026-03-12 12:50:26 +01:00

1008 lines
32 KiB
Rust

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::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};
#[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
// ============================================================================
#[no_mangle]
pub extern "C" fn text_editor_apply_theme(
selection_color: u32,
cursor_width: f32,
cursor_color: u32,
) {
with_state_mut!(state, {
// NOTE: In the future could be interesting to fill al this data from
// a structure pointer.
state.text_editor_state.theme.selection_color = Color::new(selection_color);
state.text_editor_state.theme.cursor_width = cursor_width;
state.text_editor_state.theme.cursor_color = Color::new(cursor_color);
})
}
#[no_mangle]
pub extern "C" fn text_editor_start(a: u32, b: u32, c: u32, d: u32) -> bool {
with_state_mut!(state, {
let shape_id = uuid_from_u32_quartet(a, b, c, d);
let Some(shape) = state.shapes.get(&shape_id) else {
return false;
};
if !matches!(shape.shape_type, Type::Text(_)) {
return false;
}
state.text_editor_state.start(shape_id);
true
})
}
#[no_mangle]
pub extern "C" fn text_editor_stop() -> bool {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return false;
}
state.text_editor_state.stop();
true
})
}
#[no_mangle]
pub extern "C" fn text_editor_is_active() -> bool {
with_state!(state, { state.text_editor_state.is_active })
}
#[no_mangle]
pub extern "C" fn text_editor_is_active_with_id(a: u32, b: u32, c: u32, d: u32) -> bool {
with_state!(state, {
let shape_id = uuid_from_u32_quartet(a, b, c, d);
let Some(active_shape_id) = state.text_editor_state.active_shape_id else {
return false;
};
state.text_editor_state.is_active && active_shape_id == shape_id
})
}
#[no_mangle]
pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
with_state!(state, {
if let Some(shape_id) = state.text_editor_state.active_shape_id {
let (a, b, c, d) = uuid_to_u32_quartet(&shape_id);
unsafe {
*buffer_ptr = a;
*buffer_ptr.add(1) = b;
*buffer_ptr.add(2) = c;
*buffer_ptr.add(3) = d;
}
}
})
}
#[no_mangle]
pub extern "C" fn text_editor_select_all() -> bool {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return false;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return false;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return false;
};
let Type::Text(text_content) = &shape.shape_type else {
return false;
};
state.text_editor_state.select_all(text_content)
})
}
#[no_mangle]
pub extern "C" fn text_editor_select_word_boundary(x: f32, y: f32) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
let Type::Text(text_content) = &shape.shape_type else {
return;
};
let point = Point::new(x, y);
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
state
.text_editor_state
.select_word_boundary(text_content, &position);
}
})
}
#[no_mangle]
pub extern "C" fn text_editor_poll_event() -> u8 {
with_state_mut!(state, { state.text_editor_state.poll_event() as u8 })
}
// ============================================================================
// SELECTION MANAGEMENT
// ============================================================================
#[no_mangle]
pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
let Type::Text(text_content) = &shape.shape_type else {
return;
};
let point = Point::new(x, y);
state.text_editor_state.start_pointer_selection();
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
state.text_editor_state.set_caret_from_position(&position);
}
});
}
#[no_mangle]
pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let point = Point::new(x, y);
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
if !state.text_editor_state.is_pointer_selection_active {
return;
}
let Type::Text(text_content) = &shape.shape_type else {
return;
};
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
state
.text_editor_state
.extend_selection_from_position(&position);
}
});
}
#[no_mangle]
pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let point = Point::new(x, y);
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
if !state.text_editor_state.is_pointer_selection_active {
return;
}
let Type::Text(text_content) = &shape.shape_type else {
return;
};
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
state
.text_editor_state
.extend_selection_from_position(&position);
}
state.text_editor_state.stop_pointer_selection();
});
}
#[no_mangle]
pub extern "C" fn text_editor_set_cursor_from_offset(x: f32, y: f32) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let point = Point::new(x, y);
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
let Type::Text(text_content) = &shape.shape_type else {
return;
};
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
state.text_editor_state.set_caret_from_position(&position);
}
});
}
#[no_mangle]
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
let point = Point::new(x, y);
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
let shape_matrix = shape.get_matrix();
let Type::Text(text_content) = &shape.shape_type else {
return;
};
if let Some(position) =
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
{
state.text_editor_state.set_caret_from_position(&position);
}
});
}
// ============================================================================
// TEXT OPERATIONS
// ============================================================================
// FIXME: Review if all the return Ok(()) should be Err instead.
#[no_mangle]
#[wasm_error]
pub extern "C" fn text_editor_insert_text() -> Result<()> {
let bytes = crate::mem::bytes();
let text = match String::from_utf8(bytes) {
Ok(text) => text,
Err(_) => return Ok(()),
};
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return Ok(());
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return Ok(());
};
let Some(shape) = state.shapes.get_mut(&shape_id) else {
return Ok(());
};
let Type::Text(text_content) = &mut shape.shape_type else {
return Ok(());
};
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 let Some(new_cursor) =
text_helpers::insert_text_with_newlines(text_content, &cursor, &text)
{
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.render_state.mark_touched(shape_id);
});
crate::mem::free_bytes()?;
Ok(())
}
#[no_mangle]
pub extern "C" fn text_editor_delete_backward(word_boundary: bool) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get_mut(&shape_id) else {
return;
};
let Type::Text(text_content) = &mut shape.shape_type else {
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);
state.render_state.mark_touched(shape_id);
});
}
#[no_mangle]
pub extern "C" fn text_editor_delete_forward(word_boundary: bool) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get_mut(&shape_id) else {
return;
};
let Type::Text(text_content) = &mut shape.shape_type else {
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);
state.render_state.mark_touched(shape_id);
});
}
#[no_mangle]
pub extern "C" fn text_editor_insert_paragraph() {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get_mut(&shape_id) else {
return;
};
let Type::Text(text_content) = &mut shape.shape_type else {
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.render_state.mark_touched(shape_id);
});
}
// ============================================================================
// NAVIGATION
// ============================================================================
#[no_mangle]
pub extern "C" fn text_editor_move_cursor(
direction: CursorDirection,
word_boundary: bool,
extend_selection: bool,
) {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
let Type::Text(text_content) = &shape.shape_type else {
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);
});
}
// ============================================================================
// RENDERING & EXPORT
// ============================================================================
#[no_mangle]
pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 {
with_state_mut!(state, {
if !state.text_editor_state.is_active || !state.text_editor_state.cursor_visible {
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 cursor = &state.text_editor_state.selection.focus;
if let Some(rect) = get_cursor_rect(text_content, cursor, shape) {
let mut bytes = vec![0u8; 16];
bytes[0..4].copy_from_slice(&rect.left().to_le_bytes());
bytes[4..8].copy_from_slice(&rect.top().to_le_bytes());
bytes[8..12].copy_from_slice(&rect.width().to_le_bytes());
bytes[12..16].copy_from_slice(&rect.height().to_le_bytes());
return mem::write_bytes(bytes);
}
std::ptr::null_mut()
})
}
#[no_mangle]
pub extern "C" fn text_editor_get_selection_rects() -> *mut u8 {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
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 selection = &state.text_editor_state.selection;
let rects = get_selection_rects(text_content, selection, shape);
if rects.is_empty() {
return std::ptr::null_mut();
}
let mut bytes = Vec::with_capacity(4 + rects.len() * 16);
bytes.extend_from_slice(&(rects.len() as u32).to_le_bytes());
for rect in rects {
bytes.extend_from_slice(&rect.left().to_le_bytes());
bytes.extend_from_slice(&rect.top().to_le_bytes());
bytes.extend_from_slice(&rect.width().to_le_bytes());
bytes.extend_from_slice(&rect.height().to_le_bytes());
}
mem::write_bytes(bytes)
})
}
#[no_mangle]
pub extern "C" fn text_editor_update_blink(timestamp_ms: f64) {
with_state_mut!(state, {
state.text_editor_state.update_blink(timestamp_ms);
});
}
#[no_mangle]
pub extern "C" fn text_editor_render_overlay() {
with_state_mut!(state, {
if !state.text_editor_state.is_active {
return;
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return;
};
if let Some(shape) = state.shapes.get(&shape_id) {
if let Type::Text(text_content) = &shape.shape_type {
if text_content.needs_update_layout() {
let selrect = shape.selrect();
if let Some(shape) = state.shapes.get_mut(&shape_id) {
if let Type::Text(text_content) = &mut shape.shape_type {
text_content.update_layout(selrect);
}
}
}
}
}
let Some(shape) = state.shapes.get(&shape_id) else {
return;
};
let transform = shape.get_concatenated_matrix(&state.shapes);
use crate::render::text_editor as te_render;
use crate::render::SurfaceId;
let canvas = state.render_state.surfaces.canvas(SurfaceId::Target);
canvas.save();
let viewbox = state.render_state.viewbox;
let zoom = viewbox.zoom * state.render_state.options.dpr();
canvas.scale((zoom, zoom));
canvas.translate((-viewbox.area.left, -viewbox.area.top));
te_render::render_overlay(canvas, &state.text_editor_state, shape, &transform);
canvas.restore();
state.render_state.flush_and_submit();
});
}
#[no_mangle]
pub extern "C" fn text_editor_export_content() -> *mut u8 {
with_state!(state, {
if !state.text_editor_state.is_active {
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 json_parts: Vec<String> = Vec::new();
for para in text_content.paragraphs() {
let mut span_parts: Vec<String> = Vec::new();
for span in para.children() {
let escaped_text = span
.text
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t");
span_parts.push(format!("\"{}\"", escaped_text));
}
json_parts.push(format!("[{}]", span_parts.join(",")));
}
let json = format!("[{}]", json_parts.join(","));
let mut bytes = json.into_bytes();
bytes.push(0);
crate::mem::write_bytes(bytes)
})
}
#[no_mangle]
pub extern "C" fn text_editor_export_selection() -> *mut u8 {
use std::ptr;
with_state!(state, {
if !state.text_editor_state.is_active {
return ptr::null_mut();
}
let Some(shape_id) = state.text_editor_state.active_shape_id else {
return ptr::null_mut();
};
let Some(shape) = state.shapes.get(&shape_id) else {
return ptr::null_mut();
};
let Type::Text(text_content) = &shape.shape_type else {
return ptr::null_mut();
};
let selection = &state.text_editor_state.selection;
let start = selection.start();
let end = selection.end();
let paragraphs = text_content.paragraphs();
let mut result = String::new();
let end_paragraph = end.paragraph.min(paragraphs.len().saturating_sub(1)) + 1;
for (para_idx, _) in paragraphs
.iter()
.enumerate()
.take(end_paragraph)
.skip(start.paragraph)
{
let para = &paragraphs[para_idx];
let mut para_text = String::new();
let para_char_count: usize = para
.children()
.iter()
.map(|span| span.text.chars().count())
.sum();
let range_start = if para_idx == start.paragraph {
start.offset
} else {
0
};
let range_end = if para_idx == end.paragraph {
end.offset
} else {
para_char_count
};
if range_start < range_end {
let mut char_pos = 0;
for span in para.children() {
let span_len = span.text.chars().count();
let span_start = char_pos;
let span_end = char_pos + span_len;
let sel_start = range_start.max(span_start);
let sel_end = range_end.min(span_end);
if sel_start < sel_end {
let rel_start = sel_start - span_start;
let rel_end = sel_end - span_start;
let text: String = span
.text
.chars()
.skip(rel_start)
.take(rel_end - rel_start)
.collect();
para_text.push_str(&text);
}
char_pos += span_len;
}
}
if para_idx > start.paragraph {
result.push('\n');
}
result.push_str(&para_text);
}
let mut bytes = result.into_bytes();
bytes.push(0);
crate::mem::write_bytes(bytes)
})
}
#[no_mangle]
pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 {
with_state!(state, {
if !state.text_editor_state.is_active {
return 0;
}
let sel = &state.text_editor_state.selection;
unsafe {
*buffer_ptr = sel.anchor.paragraph as u32;
*buffer_ptr.add(1) = sel.anchor.offset as u32;
*buffer_ptr.add(2) = sel.focus.paragraph as u32;
*buffer_ptr.add(3) = sel.focus.offset as u32;
}
1
})
}
// ============================================================================
// HELPERS: Cursor & Selection
// ============================================================================
fn get_cursor_rect(
text_content: &TextContent,
cursor: &TextPositionWithAffinity,
shape: &Shape,
) -> Option<Rect> {
let paragraphs = text_content.paragraphs();
if cursor.paragraph >= paragraphs.len() {
return None;
}
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
let valign_offset = match shape.vertical_align() {
VerticalAlign::Center => (shape.selrect().height() - total_height) / 2.0,
VerticalAlign::Bottom => shape.selrect().height() - total_height,
_ => 0.0,
};
let mut y_offset = valign_offset;
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
if idx == cursor.paragraph {
let char_pos = cursor.offset;
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
let rects = laid_out_para.get_rects_for_range(
char_pos..char_pos,
RectHeightStyle::Tight,
RectWidthStyle::Tight,
);
let (x, height) = if !rects.is_empty() {
(rects[0].rect.left(), rects[0].rect.height())
} else {
let pos = laid_out_para.get_glyph_position_at_coordinate((0.0, 0.0));
let height = laid_out_para.height();
(pos.position as f32, height)
};
let cursor_width = 2.0;
let selrect = shape.selrect();
let base_x = selrect.x();
let base_y = selrect.y() + y_offset;
return Some(Rect::from_xywh(base_x + x, base_y, cursor_width, height));
}
y_offset += laid_out_para.height();
}
None
}
/// Get selection rectangles for a given selection.
fn get_selection_rects(
text_content: &TextContent,
selection: &TextSelection,
shape: &Shape,
) -> Vec<Rect> {
let mut rects = Vec::new();
let start = selection.start();
let end = selection.end();
let paragraphs = text_content.paragraphs();
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
let selrect = shape.selrect();
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
let valign_offset = match shape.vertical_align() {
VerticalAlign::Center => (selrect.height() - total_height) / 2.0,
VerticalAlign::Bottom => selrect.height() - total_height,
_ => 0.0,
};
let mut y_offset = valign_offset;
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
let para_height = laid_out_para.height();
if para_idx < start.paragraph || para_idx > end.paragraph {
y_offset += para_height;
continue;
}
if para_idx >= paragraphs.len() {
y_offset += para_height;
continue;
}
let para = &paragraphs[para_idx];
let para_char_count: usize = para
.children()
.iter()
.map(|span| span.text.chars().count())
.sum();
let range_start = if para_idx == start.paragraph {
start.offset
} else {
0
};
let range_end = if para_idx == end.paragraph {
end.offset
} else {
para_char_count
};
if range_start < range_end {
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
let text_boxes = laid_out_para.get_rects_for_range(
range_start..range_end,
RectHeightStyle::Tight,
RectWidthStyle::Tight,
);
for text_box in text_boxes {
let r = text_box.rect;
rects.push(Rect::from_xywh(
selrect.x() + r.left(),
selrect.y() + y_offset + r.top(),
r.width(),
r.height(),
));
}
}
y_offset += para_height;
}
rects
}