mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
Merge pull request #7361 from penpot/azazeln28-feat-dom-textarea-position
🎉 Text Editor DOM textarea position
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, ¶graph, 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
103
render-wasm/src/state/text_editor.rs
Normal file
103
render-wasm/src/state/text_editor.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user