Merge pull request #7361 from penpot/azazeln28-feat-dom-textarea-position

🎉 Text Editor DOM textarea position
This commit is contained in:
Elena Torró
2025-10-16 14:30:41 +02:00
committed by GitHub
31 changed files with 2262 additions and 1326 deletions

View File

@@ -77,6 +77,20 @@ macro_rules! with_current_shape {
};
}
#[macro_export]
macro_rules! with_state_mut_current_shape {
($state:ident, |$shape:ident: &Shape| $block:block) => {
let $state = unsafe {
#[allow(static_mut_refs)]
STATE.as_mut()
}
.expect("Got an invalid state pointer");
if let Some($shape) = $state.current_shape() {
$block
}
};
}
/// This is called from JS after the WebGL context has been created.
#[no_mangle]
pub extern "C" fn init(width: i32, height: i32) {

View File

@@ -945,7 +945,17 @@ impl RenderState {
) -> Result<(), String> {
performance::begin_measure!("process_animation_frame");
if self.render_in_progress {
self.render_shape_tree_partial(tree, modifiers, structure, scale_content, timestamp)?;
if tree.len() != 0 {
self.render_shape_tree_partial(
tree,
modifiers,
structure,
scale_content,
timestamp,
)?;
} else {
println!("Empty tree");
}
self.flush_and_submit();
if self.render_in_progress {

View File

@@ -223,10 +223,15 @@ fn draw_text(
let mut group_offset_y = global_offset_y;
let group_len = paragraph_builder_group.len();
for paragraph_builder in paragraph_builder_group.iter_mut() {
for (paragraph_index, paragraph_builder) in paragraph_builder_group.iter_mut().enumerate() {
let mut paragraph = paragraph_builder.build();
paragraph.layout(paragraph_width);
let paragraph_height = paragraph.height();
let _paragraph_height = paragraph.height();
// FIXME: I've kept the _paragraph_height variable to have
// a reminder in the future to keep digging why the ideographic_baseline
// works so well and not the paragraph_height. I think we should test
// this more.
let ideographic_baseline = paragraph.ideographic_baseline();
let xy = (shape.selrect().x(), shape.selrect().y() + group_offset_y);
paragraph.paint(canvas, xy);
@@ -234,18 +239,17 @@ fn draw_text(
render_text_decoration(canvas, &paragraph, paragraph_builder, line_metrics, xy);
}
#[allow(clippy::collapsible_else_if)]
if group_len == 1 {
group_offset_y += paragraph_height;
group_offset_y += ideographic_baseline;
} else {
if paragraph_index == 0 {
group_offset_y += ideographic_baseline;
}
}
}
if group_len > 1 {
let mut first_paragraph = paragraph_builder_group[0].build();
first_paragraph.layout(paragraph_width);
global_offset_y += first_paragraph.height();
} else {
global_offset_y = group_offset_y;
}
global_offset_y = group_offset_y;
}
}

View File

@@ -1130,8 +1130,7 @@ impl Shape {
if let Some(path) = shape_type.path_mut() {
path.transform(transform);
}
}
if let Type::Text(text) = &mut self.shape_type {
} else if let Type::Text(text) = &mut self.shape_type {
text.transform(transform);
}
}

View File

@@ -85,6 +85,30 @@ impl TextContentSize {
}
}
#[derive(Debug, Clone, Copy)]
pub struct TextPositionWithAffinity {
pub position_with_affinity: PositionWithAffinity,
pub paragraph: i32,
pub leaf: i32,
pub offset: i32,
}
impl TextPositionWithAffinity {
pub fn new(
position_with_affinity: PositionWithAffinity,
paragraph: i32,
leaf: i32,
offset: i32,
) -> Self {
Self {
position_with_affinity,
paragraph,
leaf,
offset,
}
}
}
#[derive(Debug)]
pub struct TextContentLayoutResult(
Vec<ParagraphBuilderGroup>,
@@ -95,7 +119,7 @@ pub struct TextContentLayoutResult(
#[derive(Debug)]
pub struct TextContentLayout {
pub paragraph_builders: Vec<ParagraphBuilderGroup>,
pub paragraphs: Vec<Vec<skia_safe::textlayout::Paragraph>>,
pub paragraphs: Vec<Vec<skia::textlayout::Paragraph>>,
}
impl Clone for TextContentLayout {
@@ -245,18 +269,49 @@ impl TextContent {
self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y);
}
pub fn get_caret_position_at(&self, point: &Point) -> Option<PositionWithAffinity> {
pub fn get_caret_position_at(&self, point: &Point) -> Option<TextPositionWithAffinity> {
let mut offset_y = 0.0;
let paragraphs = self.layout.paragraphs.iter().flatten();
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
for paragraph in paragraphs {
let mut paragraph_index: i32 = -1;
let mut leaf_index: i32 = -1;
for layout_paragraph in layout_paragraphs {
paragraph_index += 1;
let start_y = offset_y;
let end_y = offset_y + paragraph.height();
let end_y = offset_y + layout_paragraph.height();
// We only test against paragraphs that can contain the current y
// coordinate.
if point.y > start_y && point.y < end_y {
let position_with_affinity = paragraph.get_glyph_position_at_coordinate(*point);
return Some(position_with_affinity);
let position_with_affinity =
layout_paragraph.get_glyph_position_at_coordinate(*point);
if let Some(paragraph) = self.paragraphs().get(paragraph_index as usize) {
// Computed position keeps the current position in terms
// of number of characters of text. This is used to know
// in which leaf we are.
let mut computed_position = 0;
let mut leaf_offset = 0;
for leaf in paragraph.children() {
leaf_index += 1;
let length = leaf.text.len();
let start_position = computed_position;
let end_position = computed_position + length;
let current_position = position_with_affinity.position as usize;
if start_position <= current_position && end_position >= current_position {
leaf_offset = position_with_affinity.position - start_position as i32;
break;
}
computed_position += length;
}
return Some(TextPositionWithAffinity::new(
position_with_affinity,
paragraph_index,
leaf_index,
leaf_offset,
));
}
}
offset_y += paragraph.height();
offset_y += layout_paragraph.height();
}
None
}

View File

@@ -2,7 +2,9 @@ use skia_safe::{self as skia, textlayout::FontCollection, Path, Point};
use std::collections::HashMap;
mod shapes_pool;
mod text_editor;
pub use shapes_pool::*;
pub use text_editor::*;
use crate::render::RenderState;
use crate::shapes::Shape;
@@ -19,6 +21,7 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data;
/// must not be shared between different Web Workers.
pub(crate) struct State {
pub render_state: RenderState,
pub text_editor_state: TextEditorState,
pub current_id: Option<Uuid>,
pub shapes: ShapesPool,
pub modifiers: HashMap<Uuid, skia::Matrix>,
@@ -30,6 +33,7 @@ impl State {
pub fn new(width: i32, height: i32) -> Self {
State {
render_state: RenderState::new(width, height),
text_editor_state: TextEditorState::new(),
current_id: None,
shapes: ShapesPool::new(),
modifiers: HashMap::new(),
@@ -50,6 +54,16 @@ impl State {
&self.render_state
}
#[allow(dead_code)]
pub fn text_editor_state_mut(&mut self) -> &mut TextEditorState {
&mut self.text_editor_state
}
#[allow(dead_code)]
pub fn text_editor_state(&self) -> &TextEditorState {
&self.text_editor_state
}
pub fn render_from_cache(&mut self) {
self.render_state
.render_from_cache(&self.shapes, &self.modifiers, &self.structure);

View File

@@ -0,0 +1,103 @@
#![allow(dead_code)]
use crate::shapes::TextPositionWithAffinity;
/// TODO: Now this is just a tuple with 2 i32 working
/// as indices (paragraph and leaf).
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct TextNodePosition {
pub paragraph: i32,
pub leaf: i32,
}
impl TextNodePosition {
pub fn new(paragraph: i32, leaf: i32) -> Self {
Self { paragraph, leaf }
}
#[allow(dead_code)]
pub fn is_invalid(&self) -> bool {
self.paragraph < 0 || self.leaf < 0
}
}
pub struct TextPosition {
node: Option<TextNodePosition>,
offset: i32,
}
impl TextPosition {
pub fn new() -> Self {
Self {
node: None,
offset: -1,
}
}
pub fn set(&mut self, node: Option<TextNodePosition>, offset: i32) {
self.node = node;
self.offset = offset;
}
}
pub struct TextSelection {
focus: TextPosition,
anchor: TextPosition,
}
impl TextSelection {
pub fn new() -> Self {
Self {
focus: TextPosition::new(),
anchor: TextPosition::new(),
}
}
#[allow(dead_code)]
pub fn is_caret(&self) -> bool {
self.focus.node == self.anchor.node && self.focus.offset == self.anchor.offset
}
#[allow(dead_code)]
pub fn is_selection(&self) -> bool {
!self.is_caret()
}
pub fn set_focus(&mut self, node: Option<TextNodePosition>, offset: i32) {
self.focus.set(node, offset);
}
pub fn set_anchor(&mut self, node: Option<TextNodePosition>, offset: i32) {
self.anchor.set(node, offset);
}
pub fn set(&mut self, node: Option<TextNodePosition>, offset: i32) {
self.set_focus(node, offset);
self.set_anchor(node, offset);
}
}
pub struct TextEditorState {
selection: TextSelection,
}
impl TextEditorState {
pub fn new() -> Self {
Self {
selection: TextSelection::new(),
}
}
pub fn set_caret_position_from(
&mut self,
text_position_with_affinity: TextPositionWithAffinity,
) {
self.selection.set(
Some(TextNodePosition::new(
text_position_with_affinity.paragraph,
text_position_with_affinity.leaf,
)),
text_position_with_affinity.offset,
);
}
}

View File

@@ -7,7 +7,7 @@ use crate::shapes::{
self, GrowType, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
};
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
use crate::{with_current_shape, with_current_shape_mut, with_state_mut, STATE};
use crate::{with_current_shape_mut, with_state_mut, with_state_mut_current_shape, STATE};
const RAW_LEAF_DATA_SIZE: usize = std::mem::size_of::<RawTextLeaf>();
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
@@ -370,7 +370,7 @@ pub extern "C" fn update_shape_text_layout_for_all() {
#[no_mangle]
pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 {
with_current_shape!(state, |shape: &Shape| {
with_state_mut_current_shape!(state, |shape: &Shape| {
if let Type::Text(text_content) = &shape.shape_type {
let mut matrix = Matrix::new_identity();
let shape_matrix = shape.get_concatenated_matrix(&state.shapes);
@@ -384,11 +384,11 @@ pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 {
if let Some(position_with_affinity) =
text_content.get_caret_position_at(&mapped_point)
{
return position_with_affinity.position;
return position_with_affinity.position_with_affinity.position;
}
}
} else {
panic!("Trying to update grow type in a shape that it's not a text shape");
panic!("Trying to get caret position of a shape that it's not a text shape");
}
});
-1