Skip to content

Commit 3759c15

Browse files
committed
Fix text editing for layouts with inline boxes
1 parent d967497 commit 3759c15

File tree

5 files changed

+140
-89
lines changed

5 files changed

+140
-89
lines changed

parley/src/layout/cluster.rs

+54-38
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright 2021 the Parley Authors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4-
use super::{BreakReason, Brush, Cluster, ClusterInfo, Glyph, Layout, Line, Range, Run, Style};
4+
use super::{
5+
BreakReason, Brush, Cluster, ClusterInfo, Glyph, Layout, Line, LineItem, Range, Run, Style,
6+
};
57
use swash::text::cluster::Whitespace;
68

79
/// Defines the visual side of the cluster for hit testing.
@@ -21,8 +23,8 @@ impl<'a, B: Brush> Cluster<'a, B> {
2123
let mut path = ClusterPath::default();
2224
if let Some((line_index, line)) = layout.line_for_byte_index(byte_index) {
2325
path.line_index = line_index as u32;
24-
for (run_index, run) in line.runs().enumerate() {
25-
path.run_index = run_index as u32;
26+
for run in line.runs() {
27+
path.run_index = run.index;
2628
if !run.text_range().contains(&byte_index) {
2729
continue;
2830
}
@@ -44,32 +46,39 @@ impl<'a, B: Brush> Cluster<'a, B> {
4446
path.line_index = line_index as u32;
4547
let mut offset = line.metrics().offset;
4648
let last_run_index = line.len().saturating_sub(1);
47-
for (run_index, run) in line.runs().enumerate() {
48-
let is_last_run = run_index == last_run_index;
49-
let run_advance = run.advance();
50-
path.run_index = run_index as u32;
51-
path.logical_index = 0;
52-
if x > offset + run_advance && !is_last_run {
53-
offset += run_advance;
54-
continue;
55-
}
56-
let last_cluster_index = run.cluster_range().len().saturating_sub(1);
57-
for (visual_index, cluster) in run.visual_clusters().enumerate() {
58-
let is_last_cluster = is_last_run && visual_index == last_cluster_index;
59-
path.logical_index =
60-
run.visual_to_logical(visual_index).unwrap_or_default() as u32;
61-
let cluster_advance = cluster.advance();
62-
let edge = offset;
63-
offset += cluster_advance;
64-
if x > offset && !is_last_cluster {
65-
continue;
49+
for item in line.items_nonpositioned() {
50+
match item {
51+
LineItem::Run(run) => {
52+
let is_last_run = run.index as usize == last_run_index;
53+
let run_advance = run.advance();
54+
path.run_index = run.index;
55+
path.logical_index = 0;
56+
if x > offset + run_advance && !is_last_run {
57+
offset += run_advance;
58+
continue;
59+
}
60+
let last_cluster_index = run.cluster_range().len().saturating_sub(1);
61+
for (visual_index, cluster) in run.visual_clusters().enumerate() {
62+
let is_last_cluster = is_last_run && visual_index == last_cluster_index;
63+
path.logical_index =
64+
run.visual_to_logical(visual_index).unwrap_or_default() as u32;
65+
let cluster_advance = cluster.advance();
66+
let edge = offset;
67+
offset += cluster_advance;
68+
if x > offset && !is_last_cluster {
69+
continue;
70+
}
71+
let side = if x <= edge + cluster_advance * 0.5 {
72+
ClusterSide::Left
73+
} else {
74+
ClusterSide::Right
75+
};
76+
return Some((path.cluster(layout)?, side));
77+
}
78+
}
79+
LineItem::InlineBox(inline_box) => {
80+
offset += inline_box.width;
6681
}
67-
let side = if x <= edge + cluster_advance * 0.5 {
68-
ClusterSide::Left
69-
} else {
70-
ClusterSide::Right
71-
};
72-
return Some((path.cluster(layout)?, side));
7382
}
7483
}
7584
}
@@ -246,7 +255,7 @@ impl<'a, B: Brush> Cluster<'a, B> {
246255
for line_index in self.path.line_index()..layout.len() {
247256
let line = layout.get(line_index)?;
248257
for run_index in run_index..line.len() {
249-
if let Some(run) = line.run(run_index) {
258+
if let Some(run) = line.item(run_index).and_then(|item| item.run()) {
250259
if !run.cluster_range().is_empty() {
251260
return ClusterPath {
252261
line_index: line_index as u32,
@@ -287,7 +296,7 @@ impl<'a, B: Brush> Cluster<'a, B> {
287296
let line = layout.get(line_index)?;
288297
let first_run = run_index.unwrap_or(line.len());
289298
for run_index in (0..first_run).rev() {
290-
if let Some(run) = line.run(run_index) {
299+
if let Some(run) = line.item(run_index).and_then(|item| item.run()) {
291300
let range = run.cluster_range();
292301
if !range.is_empty() {
293302
return ClusterPath {
@@ -361,13 +370,20 @@ impl<'a, B: Brush> Cluster<'a, B> {
361370
let line = self.path.line(self.run.layout)?;
362371
let mut offset = line.metrics().offset;
363372
for run_index in 0..=self.path.run_index() {
364-
let run = line.run(run_index)?;
365-
if run_index != self.path.run_index() {
366-
offset += run.advance();
367-
} else {
368-
let visual_index = run.logical_to_visual(self.path.logical_index())?;
369-
for cluster in run.visual_clusters().take(visual_index) {
370-
offset += cluster.advance();
373+
let item = line.item(run_index)?;
374+
match item {
375+
LineItem::Run(run) => {
376+
if run_index != self.path.run_index() {
377+
offset += run.advance();
378+
} else {
379+
let visual_index = run.logical_to_visual(self.path.logical_index())?;
380+
for cluster in run.visual_clusters().take(visual_index) {
381+
offset += cluster.advance();
382+
}
383+
}
384+
}
385+
LineItem::InlineBox(inline_box) => {
386+
offset += inline_box.width;
371387
}
372388
}
373389
}
@@ -441,7 +457,7 @@ impl ClusterPath {
441457

442458
/// Returns the run for this path and the specified layout.
443459
pub fn run<'a, B: Brush>(&self, layout: &'a Layout<B>) -> Option<Run<'a, B>> {
444-
self.line(layout)?.run(self.run_index())
460+
self.line(layout)?.item(self.run_index())?.run()
445461
}
446462

447463
/// Returns the cluster for this path and the specified layout.

parley/src/layout/cursor.rs

+32-13
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
66
#[cfg(feature = "accesskit")]
77
use super::LayoutAccessibility;
8-
use super::{Affinity, BreakReason, Brush, Cluster, ClusterSide, Layout, Line};
8+
use super::{Affinity, BreakReason, Brush, Cluster, ClusterSide, Layout, Line, LineItem};
99
#[cfg(feature = "accesskit")]
1010
use accesskit::TextPosition;
1111
use alloc::vec::Vec;
@@ -74,7 +74,7 @@ impl Cursor {
7474
) -> Option<Self> {
7575
let (line_index, run_index) = *layout_access.run_paths_by_access_id.get(&pos.node)?;
7676
let line = layout.get(line_index)?;
77-
let run = line.run(run_index)?;
77+
let run = line.item(run_index)?.run()?;
7878
let index = run
7979
.get(pos.character_index)
8080
.map(|cluster| cluster.text_range().start)
@@ -838,18 +838,37 @@ impl Selection {
838838
let mut start_x = metrics.offset as f64;
839839
let mut cur_x = start_x;
840840
let mut cluster_count = 0;
841-
for run in line.runs() {
842-
for cluster in run.visual_clusters() {
843-
let advance = cluster.advance() as f64;
844-
if text_range.contains(&cluster.text_range().start) {
845-
cluster_count += 1;
846-
cur_x += advance;
847-
} else {
848-
if cur_x != start_x {
849-
f(Rect::new(start_x, line_min, cur_x, line_max));
841+
let mut box_advance = 0.0;
842+
let mut have_seen_any_runs = false;
843+
for item in line.items_nonpositioned() {
844+
match item {
845+
LineItem::Run(run) => {
846+
have_seen_any_runs = true;
847+
for cluster in run.visual_clusters() {
848+
let advance = cluster.advance() as f64 + box_advance;
849+
box_advance = 0.0;
850+
if text_range.contains(&cluster.text_range().start) {
851+
cluster_count += 1;
852+
cur_x += advance;
853+
} else {
854+
if cur_x != start_x {
855+
f(Rect::new(start_x, line_min, cur_x, line_max));
856+
}
857+
cur_x += advance;
858+
start_x = cur_x;
859+
}
860+
}
861+
}
862+
LineItem::InlineBox(inline_box) => {
863+
box_advance += inline_box.width as f64;
864+
// HACK: Don't display selections for inline boxes
865+
// if they're the first thing in the line. This
866+
// makes the selection match the cursor position.
867+
if !have_seen_any_runs {
868+
cur_x += box_advance;
869+
box_advance = 0.0;
870+
start_x = cur_x;
850871
}
851-
cur_x += advance;
852-
start_x = cur_x;
853872
}
854873
}
855874
}

parley/src/layout/line/mod.rs

+48-35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Copyright 2021 the Parley Authors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33

4-
use super::{BreakReason, Brush, Glyph, LayoutItemKind, Line, LineItemData, Range, Run, Style};
4+
use crate::InlineBox;
5+
6+
use super::{BreakReason, Brush, Glyph, LayoutItemKind, Line, Range, Run, Style};
57

68
pub(crate) mod greedy;
79

@@ -30,53 +32,52 @@ impl<'a, B: Brush> Line<'a, B> {
3032
self.data.item_range.is_empty()
3133
}
3234

33-
/// Returns the run at the specified index.
34-
pub(crate) fn item(&self, index: usize) -> Option<&LineItemData> {
35-
let index = self.data.item_range.start + index;
36-
if index >= self.data.item_range.end {
37-
return None;
38-
}
39-
let item = self.layout.data.line_items.get(index)?;
40-
Some(item)
41-
}
42-
43-
/// Returns the run at the specified index.
44-
pub fn run(&self, index: usize) -> Option<Run<'a, B>> {
35+
/// Returns the line item at the specified index.
36+
pub(crate) fn item(&self, index: usize) -> Option<LineItem<'a, B>> {
4537
let original_index = index;
4638
let index = self.data.item_range.start + index;
4739
if index >= self.data.item_range.end {
4840
return None;
4941
}
5042
let item = self.layout.data.line_items.get(index)?;
5143

52-
if item.kind == LayoutItemKind::TextRun {
53-
Some(Run {
44+
Some(match item.kind {
45+
LayoutItemKind::TextRun => LineItem::Run(Run {
5446
layout: self.layout,
5547
line_index: self.index,
5648
index: original_index as u32,
5749
data: self.layout.data.runs.get(item.index)?,
5850
line_data: Some(item),
59-
})
60-
} else {
61-
None
62-
}
51+
}),
52+
LayoutItemKind::InlineBox => {
53+
LineItem::InlineBox(self.layout.data.inline_boxes.get(item.index)?)
54+
}
55+
})
6356
}
6457

6558
/// Returns an iterator over the runs for the line.
66-
// TODO: provide iterator over inline_boxes and items
6759
pub fn runs(&self) -> impl Iterator<Item = Run<'a, B>> + Clone {
60+
self.items_nonpositioned().filter_map(|item| item.run())
61+
}
62+
63+
/// Returns an iterator over the non-glyph runs and inline boxes for the line.
64+
pub(crate) fn items_nonpositioned(&self) -> impl Iterator<Item = LineItem<'a, B>> + Clone {
6865
let copy = self.clone();
6966
let line_items = &copy.layout.data.line_items[self.data.item_range.clone()];
7067
line_items
7168
.iter()
7269
.enumerate()
73-
.filter(|(_, item)| item.kind == LayoutItemKind::TextRun)
74-
.map(move |(index, line_data)| Run {
75-
layout: copy.layout,
76-
line_index: copy.index,
77-
index: index as u32,
78-
data: &copy.layout.data.runs[line_data.index],
79-
line_data: Some(line_data),
70+
.map(move |(item_index, line_data)| match line_data.kind {
71+
LayoutItemKind::TextRun => LineItem::Run(Run {
72+
layout: self.layout,
73+
line_index: self.index,
74+
index: item_index as u32,
75+
data: &copy.layout.data.runs[line_data.index],
76+
line_data: Some(line_data),
77+
}),
78+
LayoutItemKind::InlineBox => {
79+
LineItem::InlineBox(&self.layout.data.inline_boxes[line_data.index])
80+
}
8081
})
8182
}
8283

@@ -130,6 +131,22 @@ impl LineMetrics {
130131
}
131132
}
132133

134+
/// A line item and its corresponding data (a run or inline box). Unlike a
135+
/// [`PositionedLayoutItem`], runs are not guaranteed to be split by style.
136+
pub(crate) enum LineItem<'a, B: Brush> {
137+
Run(Run<'a, B>),
138+
InlineBox(&'a InlineBox),
139+
}
140+
141+
impl<'a, B: Brush> LineItem<'a, B> {
142+
pub(crate) fn run(self) -> Option<Run<'a, B>> {
143+
match self {
144+
LineItem::Run(run) => Some(run),
145+
_ => None,
146+
}
147+
}
148+
}
149+
133150
/// The computed result of an item (glyph run or inline box) within a layout
134151
#[derive(Clone)]
135152
pub enum PositionedLayoutItem<'a, B: Brush> {
@@ -221,15 +238,13 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
221238
fn next(&mut self) -> Option<Self::Item> {
222239
loop {
223240
let item = self.line.item(self.item_index)?;
224-
match item.kind {
225-
LayoutItemKind::InlineBox => {
226-
let inline_box = &self.line.layout.data.inline_boxes[item.index];
227-
241+
match item {
242+
LineItem::InlineBox(inline_box) => {
228243
let x = self.offset + self.line.data.metrics.offset;
229244

230245
self.item_index += 1;
231246
self.glyph_start = 0;
232-
self.offset += item.advance;
247+
self.offset += inline_box.width;
233248
return Some(PositionedLayoutItem::InlineBox(PositionedInlineBox {
234249
x,
235250
y: self.line.data.metrics.baseline - inline_box.height,
@@ -238,9 +253,7 @@ impl<'a, B: Brush> Iterator for GlyphRunIter<'a, B> {
238253
id: inline_box.id,
239254
}));
240255
}
241-
242-
LayoutItemKind::TextRun => {
243-
let run = self.line.run(self.item_index)?;
256+
LineItem::Run(run) => {
244257
let mut iter = run
245258
.visual_clusters()
246259
.flat_map(|c| c.glyphs())

parley/src/layout/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use swash::{GlyphId, NormalizedCoord, Synthesis};
3535
pub use alignment::AlignmentOptions;
3636
pub use cluster::{Affinity, ClusterPath, ClusterSide};
3737
pub use cursor::{Cursor, Selection};
38+
pub(crate) use line::LineItem;
3839
pub use line::greedy::BreakLines;
3940
pub use line::{GlyphRun, LineMetrics, PositionedInlineBox, PositionedLayoutItem};
4041
pub use run::RunMetrics;

parley/src/shape.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,9 @@ pub(crate) fn shape_text<'a, B: Brush>(
136136
}
137137

138138
// Iterate over characters in the text
139-
for ((char_index, ch), (info, style_index)) in text.chars().enumerate().zip(infos) {
139+
for ((char_index, (byte_index, ch)), (info, style_index)) in
140+
text.char_indices().enumerate().zip(infos)
141+
{
140142
let mut break_run = false;
141143
let mut script = info.script();
142144
if !real_script(script) {
@@ -167,9 +169,9 @@ pub(crate) fn shape_text<'a, B: Brush>(
167169
// - We do this *before* processing the text run because we need to know whether we should
168170
// break the run due to the presence of an inline box.
169171
while let Some((box_idx, inline_box)) = current_box {
170-
// println!("{} {}", char_index, inline_box.index);
172+
// println!("{} {}", byte_index, inline_box.index);
171173

172-
if inline_box.index == char_index {
174+
if inline_box.index == byte_index {
173175
break_run = true;
174176
deferred_boxes.push(box_idx);
175177
// Update the current box to the next box

0 commit comments

Comments
 (0)