Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"parley_bench",
"parley_core",
"parley_dev",
"parley_flow",
"examples/tiny_skia_render",
"examples/swash_render",
"examples/vello_editor",
Expand Down
25 changes: 25 additions & 0 deletions parley_flow/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "parley_flow"
description = "High-level text surfaces, multi-selection, and document aggregation on top of parley"
keywords = ["text", "layout", "flow"]
categories = ["gui", "graphics"]
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
exclude = ["/tests"]
readme = "README.md"

[package.metadata.docs.rs]
all-features = true

[lints]
workspace = true

[features]
default = ["std"]
std = ["parley/std"]
libm = ["parley/libm"]

[dependencies]
parley = { workspace = true }
13 changes: 13 additions & 0 deletions parley_flow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# parley_flow (TextBlock + TextFlow)

A small, flow-first layer on top of Parley that introduces:

- TextBlock: a minimal trait for blocks of laid-out text (paragraphs, labels)
- LayoutBlock: adapter for a `parley::layout::Layout` + `&str`
- TextFlow: explicit ordered containers (rect + join policy) for deterministic hit-testing,
cross-block navigation, and text concatenation
- Flow-based helpers: `hit_test`, `selection_geometry`, `copy_text`

Status: experimental. Names and APIs may change as this evolves.

See `src/design.rs` for comparisons to TextKit/DirectWrite/Android and long-term goals.
74 changes: 74 additions & 0 deletions parley_flow/examples/basic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2025 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Minimal example showing surfaces, multi-selection geometry, and copy.

use parley::{Alignment, AlignmentOptions, FontContext, Layout, LayoutContext, StyleProperty};
use parley_flow::{
BoundaryPolicy, LayoutBlock, SelectionSegment, SelectionSet, copy_text, flow::TextFlow,
hit_test, selection_geometry,
};

fn build_layout(
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<()>,
text: &str,
) -> Layout<()> {
let mut builder = layout_cx.ranged_builder(font_cx, text, 1.0, true);
builder.push_default(StyleProperty::FontSize(16.0));
let mut layout: Layout<()> = builder.build(text);
let width = Some(200.0);
layout.break_all_lines(width);
layout.align(width, Alignment::Start, AlignmentOptions::default());
layout
}

fn main() {
// Build two simple paragraph layouts with Parley
let mut font_cx = FontContext::new();
let mut layout_cx = LayoutContext::new();

let text1 = "Hello world";
let text2 = "Second line";
let layout1 = build_layout(&mut font_cx, &mut layout_cx, text1);
let layout2 = build_layout(&mut font_cx, &mut layout_cx, text2);

// Wrap them as surfaces with y-offsets stacked vertically
let surfaces = vec![
LayoutBlock {
id: 0_u32,
layout: &layout1,
text: text1,
},
LayoutBlock {
id: 1_u32,
layout: &layout2,
text: text2,
},
];
let flow = TextFlow::<u32>::from_vertical_stack::<(), _>(&surfaces, BoundaryPolicy::Newline);

// Hit-test near the start of the first paragraph to get a caret
let caret = hit_test::<(), _>(&flow, &surfaces, 2.0, 2.0).expect("caret");
println!("Caret: {:?}", caret);

// Build a multi-selection spanning both surfaces
let mut set = SelectionSet::collapsed(caret);
set.add_segment(SelectionSegment::new(0, 0..5)); // "Hello"
set.add_segment(SelectionSegment::new(1, 0..6)); // "Second"

// Compute geometry (global coordinates)
let mut rects = Vec::new();
selection_geometry::<(), _, _>(&flow, &surfaces, &set, |bb, _| rects.push((bb, 0)));
println!("Selection rects (count={}):", rects.len());
for (bb, surface_ix) in &rects {
println!(
" surface={} rect=({},{})->({},{}))",
surface_ix, bb.x0, bb.y0, bb.x1, bb.y1
);
}

// Extract text across surfaces
let copied = copy_text::<(), _>(&flow, &surfaces, &set);
println!("Copied text:\n{}", copied);
}
108 changes: 108 additions & 0 deletions parley_flow/src/block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Copyright 2025 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Text blocks and a simple adapter for Parley [`parley::layout::Layout`].

use alloc::string::String;
use core::ops::Range;

use parley::layout::Layout;
use parley::style::Brush;

/// A uniform facade for anything that behaves like a text layout block.
///
/// Implementers must provide:
/// - A stable identifier (`id`).
/// - A layout reference for geometry and hit-testing.
/// - Text access for extraction via [`TextBlock::text_slice`] and/or
/// [`TextBlock::read_text`].
///
/// Geometry and ordering are defined by `FlowItem` rectangles in a `TextFlow`; this
/// trait does not carry positional information.
pub trait TextBlock<B: Brush> {
/// Identifier type used by this surface set.
type Id: Copy + Ord + Eq + core::fmt::Debug;

/// Stable identifier for the surface.
fn id(&self) -> Self::Id;

/// The underlying Parley layout for this surface.
///
/// This is typically a [`parley::layout::Layout`] built by your code.
fn layout(&self) -> &Layout<B>;

/// Return a borrowed text slice for a local byte `range`, if valid and contiguous.
///
/// This is the fast path used when the underlying storage is contiguous. Implementers that
/// do not have contiguous storage (e.g., ropes) can return `None` and instead implement
/// [`TextBlock::read_text`]. The `range` must be on UTF‑8 character boundaries.
fn text_slice(&self, _range: Range<usize>) -> Option<&str> {
None
}

/// Append the text in the local byte `range` into `out` and return `true` if successful.
///
/// This is the fallback used by helpers like [`crate::copy_text`] to support non‑contiguous
/// storage. The default implementation tries [`TextBlock::text_slice`] and, if present,
/// pushes it into `out`.
fn read_text(&self, range: Range<usize>, out: &mut String) -> bool {
if let Some(s) = self.text_slice(range) {
out.push_str(s);
true
} else {
false
}
}
}

/// A simple adapter turning a Parley [`parley::layout::Layout`] and source text into a
/// [`TextBlock`].
///
/// Construct this when you already have a `Layout` and the `&str` it was built from, then
/// pass it to helpers like [`crate::hit_test`], [`crate::selection_geometry`], and
/// [`crate::copy_text`].
#[derive(Copy, Clone)]
pub struct LayoutBlock<'a, B: Brush, Id> {
/// Stable identifier for the surface.
pub id: Id,
/// The layout to expose.
pub layout: &'a Layout<B>,
/// The source text used to build `layout`.
pub text: &'a str,
}

impl<'a, B: Brush, Id: Copy + Ord + Eq + core::fmt::Debug> TextBlock<B> for LayoutBlock<'a, B, Id> {
type Id = Id;

fn id(&self) -> Self::Id {
self.id
}

fn layout(&self) -> &Layout<B> {
self.layout
}

fn text_slice(&self, range: Range<usize>) -> Option<&str> {
self.text.get(range)
}

fn read_text(&self, range: Range<usize>, out: &mut String) -> bool {
if let Some(slice) = self.text.get(range) {
out.push_str(slice);
true
} else {
false
}
}
}

impl<'a, B: Brush, Id: Copy + Ord + Eq + core::fmt::Debug> core::fmt::Debug
for LayoutBlock<'a, B, Id>
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LayoutBlock")
.field("id", &self.id)
.field("text_len", &self.text.len())
.finish_non_exhaustive()
}
}
Loading
Loading