Skip to content

Commit

Permalink
Tree style builder (#76)
Browse files Browse the repository at this point in the history
- Builds on top of #84 which
should be merged first to preserve history

## Changes made

- ### Entire unresolved style struct
     - Adds an "unresolved style struct" (`TextStyle`) to `style/mod.rs`
- The new `TreeBuilder` allows you to directly pass an entire style
struct (rather than just "changed styles" relative to a previous node in
the tree) as this is easier for integrating with Stylo which already
resolves inherited styles. So I have added an "unresolved" version of
the style struct to allow this API to still plug into the other parts of
Parley's style resolution functionality.
- Adds a corresponding`resolve_entire_style_set` method to
`LayoutContext` to convert `TextStyle` into `ResolvedStyle`.
- ### Moved code
- Moves the `RangedBuilder` from `context.rs` to a new `builder.rs`
module (as there are now two builders, justifying a separate module).
- Extract most of `RangedBuilder::build_into` into a standalone
`build_into_layout` function (`builder.rs`) that can be shared between
the ranged and tree builders.
- Moves the `RangedStyle` and `RangedProperty` types from
`resolve/range.rs` to `resolve/mod.rs`. These types are shared between
the `RangedBuilder` and the `TreeBuilder`.
- ### Tree builder
- Adds a `TreeBuilder` (also to `builder.rs`). This mostly delegates to
`TreeStyleBuilder`
- Add a `TreeStyleBuilder` (`resolve/tree.rs`). This is the vast
majority of the new code
- The `TreeStyleBuilder` implements HTML-style whitespace collapsing
(opt-in). This probably ought to become a style rather than being a flag
on the style builder.
- Updated swash example to optionally use the tree builder (depending on
command line opt)
  • Loading branch information
nicoburns authored Sep 19, 2024
1 parent 7a4cc48 commit 5cd5ea5
Show file tree
Hide file tree
Showing 11 changed files with 653 additions and 183 deletions.
115 changes: 84 additions & 31 deletions examples/swash_render/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use image::codecs::png::PngEncoder;
use image::{self, Pixel, Rgba, RgbaImage};
use parley::layout::{Alignment, Glyph, GlyphRun, Layout, PositionedLayoutItem};
use parley::style::{FontStack, FontWeight, StyleProperty};
use parley::style::{FontStack, FontWeight, StyleProperty, TextStyle};
use parley::{FontContext, InlineBox, LayoutContext};
use peniko::Color;
use std::fs::File;
Expand Down Expand Up @@ -44,40 +44,93 @@ fn main() {
let mut layout_cx = LayoutContext::new();
let mut scale_cx = ScaleContext::new();

// Create a RangedBuilder
let mut builder = layout_cx.ranged_builder(&mut font_cx, &text, display_scale);

// Set default text colour styles (set foreground text color)
// Setup some Parley text styles
let brush_style = StyleProperty::Brush(text_color);
builder.push_default(&brush_style);

// Set default font family
let font_stack = FontStack::Source("system-ui");
let font_stack_style = StyleProperty::FontStack(font_stack);
builder.push_default(&font_stack_style);
builder.push_default(&StyleProperty::LineHeight(1.3));
builder.push_default(&StyleProperty::FontSize(16.0));

// Set the first 4 characters to bold
let font_stack_style: StyleProperty<Color> = StyleProperty::FontStack(font_stack);
let bold = FontWeight::new(600.0);
let bold_style = StyleProperty::FontWeight(bold);
builder.push(&bold_style, 0..4);

builder.push_inline_box(InlineBox {
id: 0,
index: 40,
width: 50.0,
height: 50.0,
});
builder.push_inline_box(InlineBox {
id: 1,
index: 50,
width: 50.0,
height: 30.0,
});

// Build the builder into a Layout
let mut layout: Layout<Color> = builder.build();

let mut layout = if std::env::args().any(|arg| arg == "--tree") {
// TREE BUILDER
// ============

// TODO: cleanup API

let root_style = TextStyle {
brush: text_color,
font_stack,
line_height: 1.3,
font_size: 16.0,
..Default::default()
};

let mut builder = layout_cx.tree_builder(&mut font_cx, display_scale, &root_style);

builder.push_style_modification_span(&[bold_style]);
builder.push_text(&text[0..5]);
builder.pop_style_span();

builder.push_text(&text[5..40]);

builder.push_inline_box(InlineBox {
id: 0,
index: 0,
width: 50.0,
height: 50.0,
});

builder.push_text(&text[40..50]);

builder.push_inline_box(InlineBox {
id: 1,
index: 50,
width: 50.0,
height: 30.0,
});

builder.push_text(&text[50..]);

// Build the builder into a Layout
// let mut layout: Layout<Color> = builder.build(&text);
let (layout, _text): (Layout<Color>, String) = builder.build();
layout
} else {
// RANGE BUILDER
// ============

// Creatse a RangedBuilder
let mut builder = layout_cx.ranged_builder(&mut font_cx, &text, display_scale);

// Set default text colour styles (set foreground text color)
builder.push_default(&brush_style);

// Set default font family
builder.push_default(&font_stack_style);
builder.push_default(&StyleProperty::LineHeight(1.3));
builder.push_default(&StyleProperty::FontSize(16.0));

// Set the first 4 characters to bold
builder.push(&bold_style, 0..4);

builder.push_inline_box(InlineBox {
id: 0,
index: 40,
width: 50.0,
height: 50.0,
});
builder.push_inline_box(InlineBox {
id: 1,
index: 50,
width: 50.0,
height: 30.0,
});

// Build the builder into a Layout
// let mut layout: Layout<Color> = builder.build(&text);
let layout: Layout<Color> = builder.build(&text);
layout
};

// Perform layout (including bidi resolution and shaping) with start alignment
layout.break_all_lines(max_advance);
Expand Down
2 changes: 1 addition & 1 deletion examples/tiny_skia_render/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn main() {
});

// Build the builder into a Layout
let mut layout: Layout<PenikoColor> = builder.build();
let mut layout: Layout<PenikoColor> = builder.build(&text);

// Perform layout (including bidi resolution and shaping) with start alignment
layout.break_all_lines(max_advance);
Expand Down
2 changes: 1 addition & 1 deletion examples/vello_editor/src/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ impl Editor {
builder.push_default(&parley::style::StyleProperty::FontStack(
parley::style::FontStack::Source("system-ui"),
));
builder.build_into(&mut self.layout);
builder.build_into(&mut self.layout, &self.buffer);
self.layout.break_all_lines(Some(width - INSET * 2.0));
self.layout
.align(Some(width - INSET * 2.0), parley::layout::Alignment::Start);
Expand Down
186 changes: 186 additions & 0 deletions parley/src/builder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Copyright 2021 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Context for layout.

use super::context::*;
use super::style::*;
use super::FontContext;

#[cfg(feature = "std")]
use super::layout::Layout;

use core::ops::RangeBounds;

use crate::inline_box::InlineBox;

/// Builder for constructing a text layout with ranged attributes.
pub struct RangedBuilder<'a, B: Brush> {
pub(crate) scale: f32,
pub(crate) lcx: &'a mut LayoutContext<B>,
pub(crate) fcx: &'a mut FontContext,
}

impl<'a, B: Brush> RangedBuilder<'a, B> {
pub fn push_default(&mut self, property: &StyleProperty<B>) {
let resolved = self
.lcx
.rcx
.resolve_property(self.fcx, property, self.scale);
self.lcx.ranged_style_builder.push_default(resolved);
}

pub fn push(&mut self, property: &StyleProperty<B>, range: impl RangeBounds<usize>) {
let resolved = self
.lcx
.rcx
.resolve_property(self.fcx, property, self.scale);
self.lcx.ranged_style_builder.push(resolved, range);
}

pub fn push_inline_box(&mut self, inline_box: InlineBox) {
self.lcx.inline_boxes.push(inline_box);
}

#[cfg(feature = "std")]
pub fn build_into(&mut self, layout: &mut Layout<B>, text: impl AsRef<str>) {
// Apply RangedStyleBuilder styles to LayoutContext
self.lcx.ranged_style_builder.finish(&mut self.lcx.styles);

// Call generic layout builder method
build_into_layout(layout, self.scale, text.as_ref(), self.lcx, self.fcx);
}

#[cfg(feature = "std")]
pub fn build(&mut self, text: impl AsRef<str>) -> Layout<B> {
let mut layout = Layout::default();
self.build_into(&mut layout, text);
layout
}
}

/// Builder for constructing a text layout with a tree of attributes.
pub struct TreeBuilder<'a, B: Brush> {
pub(crate) scale: f32,
pub(crate) lcx: &'a mut LayoutContext<B>,
pub(crate) fcx: &'a mut FontContext,
}

impl<'a, B: Brush> TreeBuilder<'a, B> {
pub fn push_style_span(&mut self, style: TextStyle<B>) {
let resolved = self
.lcx
.rcx
.resolve_entire_style_set(self.fcx, &style, self.scale);
self.lcx.tree_style_builder.push_style_span(resolved);
}

pub fn push_style_modification_span<'s, 'iter>(
&mut self,
properties: impl IntoIterator<Item = &'iter StyleProperty<'s, B>>,
) where
's: 'iter,
B: 'iter,
{
self.lcx.tree_style_builder.push_style_modification_span(
properties
.into_iter()
.map(|p| self.lcx.rcx.resolve_property(self.fcx, p, self.scale)),
);
}

pub fn pop_style_span(&mut self) {
self.lcx.tree_style_builder.pop_style_span();
}

pub fn push_text(&mut self, text: &str) {
self.lcx.tree_style_builder.push_text(text);
}

pub fn push_inline_box(&mut self, mut inline_box: InlineBox) {
self.lcx.tree_style_builder.push_uncommitted_text(false);
// TODO: arrange type better here to factor out the index
inline_box.index = self.lcx.tree_style_builder.current_text_len();
self.lcx.inline_boxes.push(inline_box);
}

pub fn set_white_space_mode(&mut self, white_space_collapse: WhiteSpaceCollapse) {
self.lcx
.tree_style_builder
.set_white_space_mode(white_space_collapse);
}

#[cfg(feature = "std")]
pub fn build_into(&mut self, layout: &mut Layout<B>) -> String {
// Apply TreeStyleBuilder styles to LayoutContext
let text = self.lcx.tree_style_builder.finish(&mut self.lcx.styles);

self.lcx.analyze_text(&text);

// Call generic layout builder method
build_into_layout(layout, self.scale, &text, self.lcx, self.fcx);

text
}

#[cfg(feature = "std")]
pub fn build(&mut self) -> (Layout<B>, String) {
let mut layout = Layout::default();
let text = self.build_into(&mut layout);
(layout, text)
}
}

#[cfg(feature = "std")]
fn build_into_layout<B: Brush>(
layout: &mut Layout<B>,
scale: f32,
text: &str,
lcx: &mut LayoutContext<B>,
fcx: &mut FontContext,
) {
layout.data.clear();
layout.data.scale = scale;
layout.data.has_bidi = !lcx.bidi.levels().is_empty();
layout.data.base_level = lcx.bidi.base_level();
layout.data.text_len = text.len();

let mut char_index = 0;
for (i, style) in lcx.styles.iter().enumerate() {
for _ in text[style.range.clone()].chars() {
lcx.info[char_index].1 = i as u16;
char_index += 1;
}
}

// Copy the visual styles into the layout
layout
.data
.styles
.extend(lcx.styles.iter().map(|s| s.style.as_layout_style()));

// Sort the inline boxes as subsequent code assumes that they are in text index order.
// Note: It's important that this is a stable sort to allow users to control the order of contiguous inline boxes
lcx.inline_boxes.sort_by_key(|b| b.index);

{
let query = fcx.collection.query(&mut fcx.source_cache);
super::shape::shape_text(
&lcx.rcx,
query,
&lcx.styles,
&lcx.inline_boxes,
&lcx.info,
lcx.bidi.levels(),
&mut lcx.scx,
text,
layout,
);
}

// Move inline boxes into the layout
layout.data.inline_boxes.clear();
core::mem::swap(&mut layout.data.inline_boxes, &mut lcx.inline_boxes);

layout.data.finish();
}
Loading

0 comments on commit 5cd5ea5

Please sign in to comment.