Skip to content

Commit caabcf1

Browse files
authored
Implement inline box layout (#67)
### Objective - Resolve #25 ### Changes made The concept of a "layout item" has been introduced. This is either a run of text or an inline box.
1 parent 5b60803 commit caabcf1

File tree

14 files changed

+1170
-507
lines changed

14 files changed

+1170
-507
lines changed

examples/swash_render/src/main.rs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
77
use image::codecs::png::PngEncoder;
88
use image::{self, Pixel, Rgba, RgbaImage};
9-
use parley::layout::{Alignment, Glyph, GlyphRun, Layout};
9+
use parley::layout::{Alignment, Glyph, GlyphRun, Layout, PositionedLayoutItem};
1010
use parley::style::{FontStack, FontWeight, StyleProperty};
11-
use parley::{FontContext, LayoutContext};
11+
use parley::{FontContext, InlineBox, LayoutContext};
1212
use peniko::Color;
1313
use std::fs::File;
1414
use swash::scale::image::Content;
@@ -63,11 +63,25 @@ fn main() {
6363
let bold_style = StyleProperty::FontWeight(bold);
6464
builder.push(&bold_style, 0..4);
6565

66+
builder.push_inline_box(InlineBox {
67+
id: 0,
68+
index: 40,
69+
width: 50.0,
70+
height: 50.0,
71+
});
72+
builder.push_inline_box(InlineBox {
73+
id: 1,
74+
index: 50,
75+
width: 50.0,
76+
height: 30.0,
77+
});
78+
6679
// Build the builder into a Layout
6780
let mut layout: Layout<Color> = builder.build();
6881

6982
// Perform layout (including bidi resolution and shaping) with start alignment
70-
layout.break_all_lines(max_advance, Alignment::Start);
83+
layout.break_all_lines(max_advance);
84+
layout.align(max_advance, Alignment::Start);
7185

7286
// Create image to render into
7387
let width = layout.width().ceil() as u32 + (padding * 2);
@@ -77,8 +91,21 @@ fn main() {
7791
// Iterate over laid out lines
7892
for line in layout.lines() {
7993
// Iterate over GlyphRun's within each line
80-
for glyph_run in line.glyph_runs() {
81-
render_glyph_run(&mut scale_cx, &glyph_run, &mut img, padding);
94+
for item in line.items() {
95+
match item {
96+
PositionedLayoutItem::GlyphRun(glyph_run) => {
97+
render_glyph_run(&mut scale_cx, &glyph_run, &mut img, padding);
98+
}
99+
PositionedLayoutItem::InlineBox(inline_box) => {
100+
for x_off in 0..(inline_box.width.floor() as u32) {
101+
for y_off in 0..(inline_box.height.floor() as u32) {
102+
let x = inline_box.x as u32 + x_off + padding;
103+
let y = inline_box.y as u32 + y_off + padding;
104+
img.put_pixel(x, y, Rgba([0, 0, 0, 255]));
105+
}
106+
}
107+
}
108+
};
82109
}
83110
}
84111

examples/tiny_skia_render/src/main.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@
77
//! Note: Emoji rendering is not currently implemented in this example. See the swash example
88
//! if you need emoji rendering.
99
10-
use parley::layout::{Alignment, GlyphRun, Layout};
10+
use parley::layout::{Alignment, GlyphRun, Layout, PositionedLayoutItem};
1111
use parley::style::{FontStack, FontWeight, StyleProperty};
12-
use parley::{FontContext, LayoutContext};
12+
use parley::{FontContext, InlineBox, LayoutContext};
1313
use peniko::Color as PenikoColor;
1414
use skrifa::instance::{LocationRef, NormalizedCoord, Size};
1515
use skrifa::outline::{DrawSettings, OutlinePen};
1616
use skrifa::raw::FontRef as ReadFontsRef;
1717
use skrifa::{GlyphId, MetadataProvider, OutlineGlyph};
1818
use tiny_skia::{
19-
Color as TinySkiaColor, FillRule, Paint, PathBuilder, Pixmap, PixmapMut, Transform,
19+
Color as TinySkiaColor, FillRule, Paint, PathBuilder, Pixmap, PixmapMut, Rect, Transform,
2020
};
2121

2222
fn main() {
@@ -64,11 +64,19 @@ fn main() {
6464
let bold_style = StyleProperty::FontWeight(bold);
6565
builder.push(&bold_style, 0..4);
6666

67+
builder.push_inline_box(InlineBox {
68+
id: 0,
69+
index: 40,
70+
width: 50.0,
71+
height: 50.0,
72+
});
73+
6774
// Build the builder into a Layout
6875
let mut layout: Layout<PenikoColor> = builder.build();
6976

7077
// Perform layout (including bidi resolution and shaping) with start alignment
71-
layout.break_all_lines(max_advance, Alignment::Start);
78+
layout.break_all_lines(max_advance);
79+
layout.align(max_advance, Alignment::Start);
7280
let width = layout.width().ceil() as u32;
7381
let height = layout.height().ceil() as u32;
7482
let padded_width = width + padding * 2;
@@ -85,8 +93,17 @@ fn main() {
8593

8694
// Render each glyph run
8795
for line in layout.lines() {
88-
for glyph_run in line.glyph_runs() {
89-
render_glyph_run(&glyph_run, &mut pen, padding);
96+
for item in line.items() {
97+
match item {
98+
PositionedLayoutItem::GlyphRun(glyph_run) => {
99+
render_glyph_run(&glyph_run, &mut pen, padding);
100+
}
101+
PositionedLayoutItem::InlineBox(inline_box) => {
102+
pen.set_origin(inline_box.x + padding as f32, inline_box.y + padding as f32);
103+
pen.set_color(to_tiny_skia(foreground_color));
104+
pen.fill_rect(inline_box.width, inline_box.height);
105+
}
106+
};
90107
}
91108
}
92109

@@ -177,6 +194,12 @@ impl TinySkiaPen<'_> {
177194
self.paint.set_color(color);
178195
}
179196

197+
fn fill_rect(&mut self, width: f32, height: f32) {
198+
let rect = Rect::from_xywh(self.x, self.y, width, height).unwrap();
199+
self.pixmap
200+
.fill_rect(rect, &self.paint, Transform::identity(), None);
201+
}
202+
180203
fn draw_glyph(
181204
&mut self,
182205
glyph: &OutlineGlyph<'_>,

parley/src/context.rs

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,14 @@ use swash::text::cluster::CharInfo;
2020

2121
use core::ops::RangeBounds;
2222

23+
use crate::inline_box::InlineBox;
24+
2325
/// Context for building a text layout.
2426
pub struct LayoutContext<B: Brush = [u8; 4]> {
2527
bidi: bidi::BidiResolver,
2628
rcx: ResolveContext,
2729
styles: Vec<RangedStyle<B>>,
30+
inline_boxes: Vec<InlineBox>,
2831
rsb: RangedStyleBuilder<B>,
2932
info: Vec<(CharInfo, u16)>,
3033
scx: ShapeContext,
@@ -36,6 +39,7 @@ impl<B: Brush> LayoutContext<B> {
3639
bidi: bidi::BidiResolver::new(),
3740
rcx: ResolveContext::default(),
3841
styles: vec![],
42+
inline_boxes: vec![],
3943
rsb: RangedStyleBuilder::default(),
4044
info: vec![],
4145
scx: ShapeContext::default(),
@@ -112,6 +116,10 @@ impl<'a, B: Brush, T: TextSource> RangedBuilder<'a, B, T> {
112116
self.lcx.rsb.push(resolved, range);
113117
}
114118

119+
pub fn push_inline_box(&mut self, inline_box: InlineBox) {
120+
self.lcx.inline_boxes.push(inline_box);
121+
}
122+
115123
#[cfg(feature = "std")]
116124
pub fn build_into(&mut self, layout: &mut Layout<B>) {
117125
layout.data.clear();
@@ -121,6 +129,7 @@ impl<'a, B: Brush, T: TextSource> RangedBuilder<'a, B, T> {
121129
let is_empty = text.is_empty();
122130
if is_empty {
123131
// Force a layout to have at least one line.
132+
// TODO: support layouts with no text
124133
text = " ";
125134
}
126135
layout.data.has_bidi = !lcx.bidi.levels().is_empty();
@@ -135,44 +144,42 @@ impl<'a, B: Brush, T: TextSource> RangedBuilder<'a, B, T> {
135144
char_index += 1;
136145
}
137146
}
138-
use super::layout::{Decoration, Style};
139-
fn conv_deco<B: Brush>(
140-
deco: &ResolvedDecoration<B>,
141-
default_brush: &B,
142-
) -> Option<Decoration<B>> {
143-
if deco.enabled {
144-
Some(Decoration {
145-
brush: deco.brush.clone().unwrap_or_else(|| default_brush.clone()),
146-
offset: deco.offset,
147-
size: deco.size,
148-
})
149-
} else {
150-
None
151-
}
152-
}
153-
layout.data.styles.extend(lcx.styles.iter().map(|s| {
154-
let s = &s.style;
155-
Style {
156-
brush: s.brush.clone(),
157-
underline: conv_deco(&s.underline, &s.brush),
158-
strikethrough: conv_deco(&s.strikethrough, &s.brush),
159-
line_height: s.line_height,
160-
}
161-
}));
147+
148+
// Copy the visual styles into the layout
149+
layout
150+
.data
151+
.styles
152+
.extend(lcx.styles.iter().map(|s| s.style.as_layout_style()));
153+
154+
// Sort the inline boxes as subsequent code assumes that they are in text index order.
155+
// Note: It's important that this is a stable sort to allow users to control the order of contiguous inline boxes
156+
lcx.inline_boxes.sort_by_key(|b| b.index);
157+
158+
// dbg!(&lcx.inline_boxes);
159+
162160
{
163161
let query = fcx.collection.query(&mut fcx.source_cache);
164162
super::shape::shape_text(
165163
&lcx.rcx,
166164
query,
167165
&lcx.styles,
166+
&lcx.inline_boxes,
168167
&lcx.info,
169168
lcx.bidi.levels(),
170169
&mut lcx.scx,
171170
text,
172171
layout,
173172
);
174173
}
174+
175+
// Move inline boxes into the layout
176+
layout.data.inline_boxes.clear();
177+
core::mem::swap(&mut layout.data.inline_boxes, &mut lcx.inline_boxes);
178+
175179
layout.data.finish();
180+
181+
// Extra processing if the text is empty
182+
// TODO: update this logic to work with inline boxes
176183
if is_empty {
177184
layout.data.text_len = 0;
178185
let run = &mut layout.data.runs[0];

parley/src/inline_box.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2024 the Parley Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
/// A box to be laid out inline with text
5+
#[derive(Debug, Clone)]
6+
pub struct InlineBox {
7+
/// User-specified identifier for the box, which can be used by the user to determine which box in
8+
/// parley's output corresponds to which box in it's input.
9+
pub id: u64,
10+
/// The byte offset into the underlying text string at which the box should be placed.
11+
/// This must not be within a unicode code point.
12+
pub index: usize,
13+
/// The width of the box in pixels
14+
pub width: f32,
15+
/// The height of the box in pixels
16+
pub height: f32,
17+
}

parley/src/layout/alignment.rs

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2024 the Parley Authors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
4+
use super::{Alignment, BreakReason, LayoutData};
5+
use crate::style::Brush;
6+
7+
pub(crate) fn align<B: Brush>(
8+
layout: &mut LayoutData<B>,
9+
alignment_width: Option<f32>,
10+
alignment: Alignment,
11+
) {
12+
let alignment_width = alignment_width.unwrap_or_else(|| {
13+
let max_line_length = layout
14+
.lines
15+
.iter()
16+
.map(|line| line.metrics.advance)
17+
.max_by(f32::total_cmp)
18+
.unwrap_or(0.0);
19+
max_line_length.min(max_line_length)
20+
});
21+
22+
// Apply alignment to line items
23+
for line in &mut layout.lines {
24+
// TODO: remove this field
25+
line.alignment = alignment;
26+
27+
// Compute free space.
28+
let free_space = alignment_width - line.metrics.advance + line.metrics.trailing_whitespace;
29+
30+
// Alignment only applies if free_space > 0
31+
if free_space <= 0. {
32+
continue;
33+
}
34+
35+
match alignment {
36+
Alignment::Start => {
37+
// Do nothing
38+
}
39+
Alignment::End => {
40+
line.metrics.offset = free_space;
41+
}
42+
Alignment::Middle => {
43+
line.metrics.offset = free_space * 0.5;
44+
}
45+
Alignment::Justified => {
46+
if line.break_reason == BreakReason::None || line.num_spaces == 0 {
47+
continue;
48+
}
49+
50+
let adjustment = free_space / line.num_spaces as f32;
51+
let mut applied = 0;
52+
for line_item in layout.line_items[line.item_range.clone()]
53+
.iter()
54+
.filter(|item| item.is_text_run())
55+
{
56+
// Iterate over clusters in the run
57+
// - Iterate forwards for even bidi levels (which represent RTL runs)
58+
// - Iterate backwards for odd bidi levels (which represent RTL runs)
59+
let clusters = &mut layout.clusters[line_item.cluster_range.clone()];
60+
let bidi_level_is_odd = line_item.bidi_level & 1 != 0;
61+
if bidi_level_is_odd {
62+
for cluster in clusters.iter_mut().rev() {
63+
if applied == line.num_spaces {
64+
break;
65+
}
66+
if cluster.info.whitespace().is_space_or_nbsp() {
67+
cluster.advance += adjustment;
68+
applied += 1;
69+
}
70+
}
71+
} else {
72+
for cluster in clusters.iter_mut() {
73+
if applied == line.num_spaces {
74+
break;
75+
}
76+
if cluster.info.whitespace().is_space_or_nbsp() {
77+
cluster.advance += adjustment;
78+
applied += 1;
79+
}
80+
}
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
/// Removes previous justification applied to clusters.
89+
/// This is part of resetting state in preparation for re-linebreaking the same layout.
90+
pub(crate) fn unjustify<B: Brush>(layout: &mut LayoutData<B>) {
91+
for line in &layout.lines {
92+
if line.alignment == Alignment::Justified
93+
&& line.max_advance.is_finite()
94+
&& line.max_advance < f32::MAX
95+
{
96+
let extra = line.max_advance - line.metrics.advance + line.metrics.trailing_whitespace;
97+
if line.break_reason != BreakReason::None && line.num_spaces != 0 {
98+
let adjustment = extra / line.num_spaces as f32;
99+
let mut applied = 0;
100+
for line_run in layout.line_items[line.item_range.clone()]
101+
.iter()
102+
.filter(|item| item.is_text_run())
103+
{
104+
if line_run.bidi_level & 1 != 0 {
105+
for cluster in layout.clusters[line_run.cluster_range.clone()]
106+
.iter_mut()
107+
.rev()
108+
{
109+
if applied == line.num_spaces {
110+
break;
111+
}
112+
if cluster.info.whitespace().is_space_or_nbsp() {
113+
cluster.advance -= adjustment;
114+
applied += 1;
115+
}
116+
}
117+
} else {
118+
for cluster in layout.clusters[line_run.cluster_range.clone()].iter_mut() {
119+
if applied == line.num_spaces {
120+
break;
121+
}
122+
if cluster.info.whitespace().is_space_or_nbsp() {
123+
cluster.advance -= adjustment;
124+
applied += 1;
125+
}
126+
}
127+
}
128+
}
129+
}
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)