From 02fa3bdabe4edd011d93664e6cb98b0082da4d73 Mon Sep 17 00:00:00 2001 From: valadaptive Date: Sat, 15 Mar 2025 02:43:16 -0400 Subject: [PATCH] Fix text editing for layouts with inline boxes --- parley/src/layout/cluster.rs | 92 ++++++++++++++++++++--------------- parley/src/layout/cursor.rs | 45 ++++++++++++----- parley/src/layout/line/mod.rs | 83 ++++++++++++++++++------------- parley/src/layout/mod.rs | 1 + parley/src/shape.rs | 8 +-- 5 files changed, 140 insertions(+), 89 deletions(-) diff --git a/parley/src/layout/cluster.rs b/parley/src/layout/cluster.rs index 5ca86f2f..80ef6b8d 100644 --- a/parley/src/layout/cluster.rs +++ b/parley/src/layout/cluster.rs @@ -1,7 +1,9 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::{BreakReason, Brush, Cluster, ClusterInfo, Glyph, Layout, Line, Range, Run, Style}; +use super::{ + BreakReason, Brush, Cluster, ClusterInfo, Glyph, Layout, Line, LineItem, Range, Run, Style, +}; use swash::text::cluster::Whitespace; /// Defines the visual side of the cluster for hit testing. @@ -21,8 +23,8 @@ impl<'a, B: Brush> Cluster<'a, B> { let mut path = ClusterPath::default(); if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) { path.line_index = line_index as u32; - for (run_index, run) in line.runs().enumerate() { - path.run_index = run_index as u32; + for run in line.runs() { + path.run_index = run.index; if !run.text_range().contains(&byte_index) { continue; } @@ -44,32 +46,39 @@ impl<'a, B: Brush> Cluster<'a, B> { path.line_index = line_index as u32; let mut offset = line.metrics().offset; let last_run_index = line.len().saturating_sub(1); - for (run_index, run) in line.runs().enumerate() { - let is_last_run = run_index == last_run_index; - let run_advance = run.advance(); - path.run_index = run_index as u32; - path.logical_index = 0; - if x > offset + run_advance && !is_last_run { - offset += run_advance; - continue; - } - let last_cluster_index = run.cluster_range().len().saturating_sub(1); - for (visual_index, cluster) in run.visual_clusters().enumerate() { - let is_last_cluster = is_last_run && visual_index == last_cluster_index; - path.logical_index = - run.visual_to_logical(visual_index).unwrap_or_default() as u32; - let cluster_advance = cluster.advance(); - let edge = offset; - offset += cluster_advance; - if x > offset && !is_last_cluster { - continue; + for item in line.items_nonpositioned() { + match item { + LineItem::Run(run) => { + let is_last_run = run.index as usize == last_run_index; + let run_advance = run.advance(); + path.run_index = run.index; + path.logical_index = 0; + if x > offset + run_advance && !is_last_run { + offset += run_advance; + continue; + } + let last_cluster_index = run.cluster_range().len().saturating_sub(1); + for (visual_index, cluster) in run.visual_clusters().enumerate() { + let is_last_cluster = is_last_run && visual_index == last_cluster_index; + path.logical_index = + run.visual_to_logical(visual_index).unwrap_or_default() as u32; + let cluster_advance = cluster.advance(); + let edge = offset; + offset += cluster_advance; + if x > offset && !is_last_cluster { + continue; + } + let side = if x <= edge + cluster_advance * 0.5 { + ClusterSide::Left + } else { + ClusterSide::Right + }; + return Some((path.cluster(layout)?, side)); + } + } + LineItem::InlineBox(inline_box) => { + offset += inline_box.width; } - let side = if x <= edge + cluster_advance * 0.5 { - ClusterSide::Left - } else { - ClusterSide::Right - }; - return Some((path.cluster(layout)?, side)); } } } @@ -246,7 +255,7 @@ impl<'a, B: Brush> Cluster<'a, B> { for line_index in self.path.line_index()..layout.len() { let line = layout.get(line_index)?; for run_index in run_index..line.len() { - if let Some(run) = line.run(run_index) { + if let Some(run) = line.item(run_index).and_then(|item| item.run()) { if !run.cluster_range().is_empty() { return ClusterPath { line_index: line_index as u32, @@ -287,7 +296,7 @@ impl<'a, B: Brush> Cluster<'a, B> { let line = layout.get(line_index)?; let first_run = run_index.unwrap_or(line.len()); for run_index in (0..first_run).rev() { - if let Some(run) = line.run(run_index) { + if let Some(run) = line.item(run_index).and_then(|item| item.run()) { let range = run.cluster_range(); if !range.is_empty() { return ClusterPath { @@ -361,13 +370,20 @@ impl<'a, B: Brush> Cluster<'a, B> { let line = self.path.line(self.run.layout)?; let mut offset = line.metrics().offset; for run_index in 0..=self.path.run_index() { - let run = line.run(run_index)?; - if run_index != self.path.run_index() { - offset += run.advance(); - } else { - let visual_index = run.logical_to_visual(self.path.logical_index())?; - for cluster in run.visual_clusters().take(visual_index) { - offset += cluster.advance(); + let item = line.item(run_index)?; + match item { + LineItem::Run(run) => { + if run_index != self.path.run_index() { + offset += run.advance(); + } else { + let visual_index = run.logical_to_visual(self.path.logical_index())?; + for cluster in run.visual_clusters().take(visual_index) { + offset += cluster.advance(); + } + } + } + LineItem::InlineBox(inline_box) => { + offset += inline_box.width; } } } @@ -441,7 +457,7 @@ impl ClusterPath { /// Returns the run for this path and the specified layout. pub fn run<'a, B: Brush>(&self, layout: &'a Layout) -> Option> { - self.line(layout)?.run(self.run_index()) + self.line(layout)?.item(self.run_index())?.run() } /// Returns the cluster for this path and the specified layout. diff --git a/parley/src/layout/cursor.rs b/parley/src/layout/cursor.rs index b5e24227..d3c8446e 100644 --- a/parley/src/layout/cursor.rs +++ b/parley/src/layout/cursor.rs @@ -5,7 +5,7 @@ #[cfg(feature = "accesskit")] use super::LayoutAccessibility; -use super::{Affinity, BreakReason, Brush, Cluster, ClusterSide, Layout, Line}; +use super::{Affinity, BreakReason, Brush, Cluster, ClusterSide, Layout, Line, LineItem}; #[cfg(feature = "accesskit")] use accesskit::TextPosition; use alloc::vec::Vec; @@ -74,7 +74,7 @@ impl Cursor { ) -> Option { let (line_index, run_index) = *layout_access.run_paths_by_access_id.get(&pos.node)?; let line = layout.get(line_index)?; - let run = line.run(run_index)?; + let run = line.item(run_index)?.run()?; let index = run .get(pos.character_index) .map(|cluster| cluster.text_range().start) @@ -838,18 +838,37 @@ impl Selection { let mut start_x = metrics.offset as f64; let mut cur_x = start_x; let mut cluster_count = 0; - for run in line.runs() { - for cluster in run.visual_clusters() { - let advance = cluster.advance() as f64; - if text_range.contains(&cluster.text_range().start) { - cluster_count += 1; - cur_x += advance; - } else { - if cur_x != start_x { - f(Rect::new(start_x, line_min, cur_x, line_max)); + let mut box_advance = 0.0; + let mut have_seen_any_runs = false; + for item in line.items_nonpositioned() { + match item { + LineItem::Run(run) => { + have_seen_any_runs = true; + for cluster in run.visual_clusters() { + let advance = cluster.advance() as f64 + box_advance; + box_advance = 0.0; + if text_range.contains(&cluster.text_range().start) { + cluster_count += 1; + cur_x += advance; + } else { + if cur_x != start_x { + f(Rect::new(start_x, line_min, cur_x, line_max)); + } + cur_x += advance; + start_x = cur_x; + } + } + } + LineItem::InlineBox(inline_box) => { + box_advance += inline_box.width as f64; + // HACK: Don't display selections for inline boxes + // if they're the first thing in the line. This + // makes the selection match the cursor position. + if !have_seen_any_runs { + cur_x += box_advance; + box_advance = 0.0; + start_x = cur_x; } - cur_x += advance; - start_x = cur_x; } } } diff --git a/parley/src/layout/line/mod.rs b/parley/src/layout/line/mod.rs index 5c7ea96f..e01e2aa7 100644 --- a/parley/src/layout/line/mod.rs +++ b/parley/src/layout/line/mod.rs @@ -1,7 +1,9 @@ // Copyright 2021 the Parley Authors // SPDX-License-Identifier: Apache-2.0 OR MIT -use super::{BreakReason, Brush, Glyph, LayoutItemKind, Line, LineItemData, Range, Run, Style}; +use crate::InlineBox; + +use super::{BreakReason, Brush, Glyph, LayoutItemKind, Line, Range, Run, Style}; pub(crate) mod greedy; @@ -30,18 +32,8 @@ impl<'a, B: Brush> Line<'a, B> { self.data.item_range.is_empty() } - /// Returns the run at the specified index. - pub(crate) fn item(&self, index: usize) -> Option<&LineItemData> { - let index = self.data.item_range.start + index; - if index >= self.data.item_range.end { - return None; - } - let item = self.layout.data.line_items.get(index)?; - Some(item) - } - - /// Returns the run at the specified index. - pub fn run(&self, index: usize) -> Option> { + /// Returns the line item at the specified index. + pub(crate) fn item(&self, index: usize) -> Option> { let original_index = index; let index = self.data.item_range.start + index; if index >= self.data.item_range.end { @@ -49,34 +41,43 @@ impl<'a, B: Brush> Line<'a, B> { } let item = self.layout.data.line_items.get(index)?; - if item.kind == LayoutItemKind::TextRun { - Some(Run { + Some(match item.kind { + LayoutItemKind::TextRun => LineItem::Run(Run { layout: self.layout, line_index: self.index, index: original_index as u32, data: self.layout.data.runs.get(item.index)?, line_data: Some(item), - }) - } else { - None - } + }), + LayoutItemKind::InlineBox => { + LineItem::InlineBox(self.layout.data.inline_boxes.get(item.index)?) + } + }) } /// Returns an iterator over the runs for the line. - // TODO: provide iterator over inline_boxes and items pub fn runs(&self) -> impl Iterator> + 'a + Clone { + self.items_nonpositioned().filter_map(|item| item.run()) + } + + /// Returns an iterator over the non-glyph runs and inline boxes for the line. + pub(crate) fn items_nonpositioned(&self) -> impl Iterator> + Clone { let copy = self.clone(); let line_items = ©.layout.data.line_items[self.data.item_range.clone()]; line_items .iter() .enumerate() - .filter(|(_, item)| item.kind == LayoutItemKind::TextRun) - .map(move |(index, line_data)| Run { - layout: copy.layout, - line_index: copy.index, - index: index as u32, - data: ©.layout.data.runs[line_data.index], - line_data: Some(line_data), + .map(move |(item_index, line_data)| match line_data.kind { + LayoutItemKind::TextRun => LineItem::Run(Run { + layout: copy.layout, + line_index: copy.index, + index: item_index as u32, + data: ©.layout.data.runs[line_data.index], + line_data: Some(line_data), + }), + LayoutItemKind::InlineBox => { + LineItem::InlineBox(©.layout.data.inline_boxes[line_data.index]) + } }) } @@ -130,6 +131,22 @@ impl LineMetrics { } } +/// A line item and its corresponding data (a run or inline box). Unlike a +/// [`PositionedLayoutItem`], runs are not guaranteed to be split by style. +pub(crate) enum LineItem<'a, B: Brush> { + Run(Run<'a, B>), + InlineBox(&'a InlineBox), +} + +impl<'a, B: Brush> LineItem<'a, B> { + pub(crate) fn run(self) -> Option> { + match self { + LineItem::Run(run) => Some(run), + _ => None, + } + } +} + /// The computed result of an item (glyph run or inline box) within a layout #[derive(Clone)] pub enum PositionedLayoutItem<'a, B: Brush> { @@ -221,15 +238,13 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { fn next(&mut self) -> Option { loop { let item = self.line.item(self.item_index)?; - match item.kind { - LayoutItemKind::InlineBox => { - let inline_box = &self.line.layout.data.inline_boxes[item.index]; - + match item { + LineItem::InlineBox(inline_box) => { let x = self.offset + self.line.data.metrics.offset; self.item_index += 1; self.glyph_start = 0; - self.offset += item.advance; + self.offset += inline_box.width; return Some(PositionedLayoutItem::InlineBox(PositionedInlineBox { x, y: self.line.data.metrics.baseline - inline_box.height, @@ -238,9 +253,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> { id: inline_box.id, })); } - - LayoutItemKind::TextRun => { - let run = self.line.run(self.item_index)?; + LineItem::Run(run) => { let mut iter = run .visual_clusters() .flat_map(|c| c.glyphs()) diff --git a/parley/src/layout/mod.rs b/parley/src/layout/mod.rs index 8d4d12e8..28f77a5c 100644 --- a/parley/src/layout/mod.rs +++ b/parley/src/layout/mod.rs @@ -33,6 +33,7 @@ pub use alignment::AlignmentOptions; pub use cluster::{Affinity, ClusterPath, ClusterSide}; pub use cursor::{Cursor, Selection}; pub use data::BreakReason; +pub(crate) use line::LineItem; pub use line::greedy::BreakLines; pub use line::{GlyphRun, LineMetrics, PositionedInlineBox, PositionedLayoutItem}; pub use run::RunMetrics; diff --git a/parley/src/shape.rs b/parley/src/shape.rs index 6c99e3cd..8376f78c 100644 --- a/parley/src/shape.rs +++ b/parley/src/shape.rs @@ -136,7 +136,9 @@ pub(crate) fn shape_text<'a, B: Brush>( } // Iterate over characters in the text - for ((char_index, ch), (info, style_index)) in text.chars().enumerate().zip(infos) { + for ((char_index, (byte_index, ch)), (info, style_index)) in + text.char_indices().enumerate().zip(infos) + { let mut break_run = false; let mut script = info.script(); if !real_script(script) { @@ -167,9 +169,9 @@ pub(crate) fn shape_text<'a, B: Brush>( // - We do this *before* processing the text run because we need to know whether we should // break the run due to the presence of an inline box. while let Some((box_idx, inline_box)) = current_box { - // println!("{} {}", char_index, inline_box.index); + // println!("{} {}", byte_index, inline_box.index); - if inline_box.index == char_index { + if inline_box.index == byte_index { break_run = true; deferred_boxes.push(box_idx); // Update the current box to the next box