Premise: Expose parley's rich text layout and shaping API as MCP tools for both runtime computation and code generation. Approach: Completionist harvesting using runtime-only tools (context state), dual-mode tools (style/layout), and fragment tools (text layout code gen).
Shares characteristics with taffy:
- Natural serialization: Style properties are enums/primitives
- Synchronous operations: Text shaping is deterministic
- Stateful by nature: FontContext/LayoutContext manage state
- Clear API flow: Create context → Build layout → Break lines → Position glyphs
Similar to other "straightforward" crates:
- No async/lifetimes in core API
- All inputs/outputs serializable
- CSS-like style properties
- Visual domain (text layout)
Domain-specific characteristics:
- Text shaping complexity: HarfBuzz integration, OpenType features
- Bidirectional text: RTL/LTR handling, Unicode normalization
- Typography details: Kerning, ligatures, variation axes
- Line breaking: Unicode line break algorithm
Perfect fit for:
- Rich text editors (AI-assisted text layout)
- Document generation (PDF, HTML rendering)
- Typography tools (font preview, style exploration)
- Accessibility analysis (text metrics, contrast checking)
- Internationalization (bidi, locales, multi-script)
Parley's core API uses shared resources and builders with lifetimes:
pub struct FontContext { /* font database */ }
pub struct LayoutContext { /* scratch space */ }
pub struct RangedBuilder<'a, B> { /* lifetime-bound */ }
pub struct TreeBuilder<'a, B> { /* lifetime-bound */ }
pub struct Layout<B> { /* output */ }Crossing the JSON boundary:
| Category | Examples | MCP Strategy |
|---|---|---|
| Contexts | FontContext, LayoutContext |
✅ Runtime-only (UUID → context handle) |
| Style properties | StyleProperty (23 variants) |
✅ Dual-mode (serialize as JSON) |
| Text content | String, char ranges | ✅ Dual-mode (plain text in JSON) |
| Layout output | Layout<B> with lines/runs |
✅ Dual-mode (serialize positioned glyphs) |
| Builders (lifetimes) | RangedBuilder<'a, B> |
|
| Brush trait | B: Brush (generic color) |
✅ Dual-mode (RGBA color in JSON) |
| Font queries | FontStack, FontWeight, etc. |
✅ Dual-mode (serialize as strings/numbers) |
| Glyph data | Positioned glyphs, metrics | ✅ Dual-mode (serialize coordinates/IDs) |
Key insight: Like taffy, parley is stateful (contexts, builders) but all inputs and outputs are serializable:
- Input: Text + StyleProperty spans
- Output: Lines with positioned glyphs (coordinates, metrics, indices)
- State: FontContext/LayoutContext via UUID handles
UUID-keyed handles for persistent contexts and layouts:
context_create_font— Create FontContext with default fontscontext_create_font_from_paths— Load specific font filescontext_create_layout— Create LayoutContextcontext_delete_font— Remove FontContextcontext_delete_layout— Remove LayoutContextcontext_register_font— Add font to databasecontext_query_fonts— List available fonts- And 13 more for context lifecycle
builder_create_ranged— Create RangedBuilderbuilder_create_tree— Create TreeBuilderbuilder_add_text— Set text contentbuilder_push_default_style— Set global fallback stylebuilder_push_ranged_style— Apply style to character rangebuilder_push_tree_style_open— Start style span (tree builder)builder_push_tree_style_close— End style span (tree builder)builder_push_inline_box— Insert non-text elementbuilder_build— Finalize and create Layoutbuilder_delete— Clean up builder- And 20 more for builder operations
layout_create— Create empty Layoutlayout_delete— Remove Layoutlayout_break_lines— Compute line breaks with max widthlayout_align— Apply alignment (start, center, end, justify)layout_width— Get layout widthlayout_height— Get layout heightlayout_full_width— Width including trailing whitespacelayout_line_count— Number of lineslayout_get_line— Access line by indexlayout_is_rtl— Check dominant text directionlayout_scale— Get scale factor- And 29 more for layout queries
line_metrics— Get baseline, ascent, descentline_text_range— Get character range for lineline_break_reason— Why line brokeline_run_count— Number of runs in lineline_item_count— Number of positioned itemsline_runs— Iterate over runsline_items— Iterate over positioned glyphs/boxes- And 18 more for line inspection
run_metrics— Get run metricsrun_font_size— Get font size for runrun_text_range— Get character rangerun_glyph_count— Number of glyphsrun_glyphs— Access positioned glyphsitem_position— Get X/Y coordinatesitem_glyph_id— Get glyph ID from fontitem_advance— Get horizontal advance- And 17 more for glyph data
font_query_by_family— Find fonts by family namefont_query_by_weight— Find fonts by weightfont_metrics— Get font metrics (ascent, descent, cap height)font_has_glyph— Check if font contains codepointfont_list_features— List OpenType featuresfont_list_variations— List variation axes- And 14 more for font inspection
Tools that both create style/layout JSON AND generate code:
style_font_stack— Set font family stackstyle_font_size— Set font size (f32)style_font_weight— Set weight (100-900)style_font_style— Set style (normal, italic, oblique)style_font_width— Set width/stretchstyle_font_variations— Set OpenType variation axesstyle_font_features— Set OpenType featuresstyle_brush— Set text color/fillstyle_underline— Enable underlinestyle_underline_offset— Underline positionstyle_underline_size— Underline thicknessstyle_underline_brush— Underline colorstyle_strikethrough— Enable strikethroughstyle_strikethrough_offset— Strikethrough positionstyle_strikethrough_size— Strikethrough thicknessstyle_strikethrough_brush— Strikethrough colorstyle_line_height— Line height multiplierstyle_letter_spacing— Character spacingstyle_word_spacing— Word spacingstyle_word_break— Line break rulesstyle_overflow_wrap— Emergency line breakingstyle_text_wrap_mode— Wrap behaviorstyle_locale— Language/locale- And 27 more for style combinations
brush_solid— Create solid color brushbrush_rgba— RGBA colorbrush_rgb— RGB colorbrush_hex— Parse hex color (#RRGGBB)brush_named— Named CSS colors- And 15 more for color types
font_weight_thin— Weight 100font_weight_normal— Weight 400font_weight_bold— Weight 700font_style_normal— Normal stylefont_style_italic— Italicfont_style_oblique— Oblique with anglefont_stretch_condensed— Condensed widthfont_stretch_expanded— Expanded widthfont_feature_enable— Enable OpenType featurefont_variation_set— Set variation axis value- And 20 more for font property values
layout_to_json— Serialize entire Layoutlayout_lines_to_json— Lines with metricslayout_glyphs_to_json— All positioned glyphslayout_text_with_positions— Text with XY coordsline_to_json— Single line datarun_to_json— Single run dataglyph_to_json— Single glyph position/metricsmetrics_to_json— Line/font metrics- And 32 more for serialization patterns
text_range_all— Entire texttext_range_from_to— Start to end indicestext_range_from— Start to text endtext_range_to— Text start to endtext_range_word— Word boundariestext_range_line— Line boundaries- And 14 more for range patterns
align_start— Left/right based on directionalign_center— Center alignmentalign_end— Right/left based on directionalign_justify— Justify (stretch)- And 6 more for alignment options
line_break_auto— Default Unicode rulesline_break_loose— More permissiveline_break_normal— Standard rulesline_break_strict— Strict rulesline_break_anywhere— Emergency breaking- And 5 more for break rules
Code generation for text layout computations:
emit_font_context— Generate FontContext::new()emit_layout_context— Generate LayoutContext::new()emit_context_setup— Complete context initialization- And 7 more for context setup patterns
emit_ranged_builder— Generate RangedBuilder codeemit_tree_builder— Generate TreeBuilder codeemit_push_default_style— Generate default style codeemit_push_ranged_style— Generate ranged style codeemit_build_layout— Generate build() call- And 10 more for builder patterns
emit_break_lines— Generate line breaking codeemit_align— Generate alignment codeemit_layout_query— Generate layout access code- And 7 more for layout computation patterns
assemble_parley_binary— Generate complete executableemit_text_renderer— Generate rendering codeemit_layout_test— Generate layout test- And 2 more for assembly patterns
{
"type": "FontStack",
"families": ["Helvetica Neue", "Helvetica", "Arial", "sans-serif"]
}
{
"type": "FontSize",
"size": 16.0
}
{
"type": "FontWeight",
"weight": 700
}
{
"type": "Brush",
"color": { "r": 0, "g": 0, "b": 0, "a": 255 }
}
{
"type": "Underline",
"enabled": true,
"offset": 2.0,
"size": 1.0,
"brush": { "r": 0, "g": 0, "b": 255, "a": 255 }
}{
"text": "Hello, world! This is bold and italic.",
"styles": [
{
"property": { "type": "FontSize", "size": 16.0 },
"range": { "start": 0, "end": 40 }
},
{
"property": { "type": "FontWeight", "weight": 700 },
"range": { "start": 22, "end": 26 }
},
{
"property": { "type": "FontStyle", "style": "Italic" },
"range": { "start": 31, "end": 37 }
}
]
}{
"width": 400.0,
"height": 120.0,
"scale": 1.0,
"is_rtl": false,
"lines": [
{
"index": 0,
"metrics": {
"baseline": 14.0,
"ascent": 12.0,
"descent": 4.0,
"leading": 2.0
},
"text_range": { "start": 0, "end": 25 },
"break_reason": "MaxAdvance",
"runs": [
{
"font_size": 16.0,
"text_range": { "start": 0, "end": 25 },
"glyphs": [
{
"id": 43,
"x": 0.0,
"y": 14.0,
"advance": 8.5,
"cluster": 0
},
{
"id": 72,
"x": 8.5,
"y": 14.0,
"advance": 9.2,
"cluster": 1
}
// ... more glyphs
]
}
]
}
// ... more lines
]
}{
"fonts": [
{
"family": "Helvetica Neue",
"weight": 400,
"style": "Normal",
"stretch": "Normal",
"features": ["kern", "liga", "calt"],
"variations": [
{ "tag": "wght", "min": 100.0, "max": 900.0, "default": 400.0 }
]
}
]
}Goal: Establish UUID-keyed registry pattern for FontContext and LayoutContext.
crates/elicit_parley/
├── Cargo.toml
└── src/
├── lib.rs
├── context_registry.rs # UUID → context mapping
├── builder_registry.rs # UUID → builder (lifetime management)
├── layout_registry.rs # UUID → Layout mapping
├── style_tools.rs # Dual-mode style creation
├── layout_tools.rs # Dual-mode layout serialization
├── fragments.rs # Code generation tools
└── serde_types.rs # JSON wrappers
[package]
name = "elicit_parley"
version = "0.1.0"
edition = "2021"
[dependencies]
elicitation = { workspace = true }
elicitation_derive.workspace = true
parley = "0.7"
fontique = "0.4" # Font querying (used by parley)
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
uuid.workspace = true
rmcp.workspace = true
tracing.workspace = true
[features]
emit = ["dep:quote", "elicitation/emit"]use elicitation_derive::elicit_tool;
use parley::{FontContext, LayoutContext};
pub struct ParleyPlugin {
font_contexts: Arc<Mutex<HashMap<Uuid, FontContext>>>,
layout_contexts: Arc<Mutex<HashMap<Uuid, LayoutContext>>>,
// Builders have lifetimes, so we store them differently
// Each builder gets a UUID, but we can't store them long-term
// Instead, we use a session-based approach (create → use → build → delete)
layouts: Arc<Mutex<HashMap<Uuid, Layout<SimpleBrush>>>>,
}
#[derive(Debug, Clone, Copy)]
pub struct SimpleBrush {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
#[elicit_tool(
plugin = "parley_context",
name = "context_create_font",
description = "Create FontContext with system fonts"
)]
async fn context_create_font(_: FontContextCreateParams) -> Result<CallToolResult, ErrorData> {
let font_context = FontContext::new();
let context_id = Uuid::new_v4();
let plugin = get_plugin();
plugin.font_contexts.lock().unwrap().insert(context_id, font_context);
Ok(CallToolResult::success(json!({ "font_context_id": context_id })))
}
#[elicit_tool(
plugin = "parley_context",
name = "context_create_layout",
description = "Create LayoutContext for building layouts"
)]
async fn context_create_layout(_: LayoutContextCreateParams) -> Result<CallToolResult, ErrorData> {
let layout_context = LayoutContext::new();
let context_id = Uuid::new_v4();
let plugin = get_plugin();
plugin.layout_contexts.lock().unwrap().insert(context_id, layout_context);
Ok(CallToolResult::success(json!({ "layout_context_id": context_id })))
}#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuilderCreateParams {
pub font_context_id: Uuid,
pub layout_context_id: Uuid,
pub scale: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuilderBuildParams {
pub font_context_id: Uuid,
pub layout_context_id: Uuid,
pub text: String,
pub default_styles: Vec<StylePropertyJson>,
pub ranged_styles: Vec<RangedStyleJson>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RangedStyleJson {
pub property: StylePropertyJson,
pub range: RangeJson,
}
#[elicit_tool(
plugin = "parley_builder",
name = "builder_build_ranged",
description = "Build layout with ranged styles (one-shot operation)"
)]
async fn builder_build_ranged(p: BuilderBuildParams) -> Result<CallToolResult, ErrorData> {
let plugin = get_plugin();
let mut font_contexts = plugin.font_contexts.lock().unwrap();
let mut layout_contexts = plugin.layout_contexts.lock().unwrap();
let font_ctx = font_contexts.get_mut(&p.font_context_id)
.ok_or_else(|| ErrorData::new("Font context not found"))?;
let layout_ctx = layout_contexts.get_mut(&p.layout_context_id)
.ok_or_else(|| ErrorData::new("Layout context not found"))?;
// Create builder (short-lived)
let mut builder = layout_ctx.ranged_builder(font_ctx, p.scale);
// Apply default styles
for style in &p.default_styles {
let property = style.to_style_property()?;
builder.push_default(&property);
}
// Apply ranged styles
for ranged_style in &p.ranged_styles {
let property = ranged_style.property.to_style_property()?;
let range = ranged_style.range.to_range()?;
builder.push(&property, range);
}
// Build layout
let layout = builder.build(&p.text);
let layout_id = Uuid::new_v4();
drop(font_contexts);
drop(layout_contexts);
plugin.layouts.lock().unwrap().insert(layout_id, layout);
Ok(CallToolResult::success(json!({ "layout_id": layout_id })))
}Goal: Tools that both create StyleProperty JSON and emit code.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum StylePropertyJson {
FontStack { families: Vec<String> },
FontSize { size: f32 },
FontWeight { weight: u16 },
FontStyle { style: String }, // "Normal", "Italic", "Oblique"
FontWidth { width: String }, // "Normal", "Condensed", "Expanded", etc.
Brush { color: ColorJson },
Underline { enabled: bool },
UnderlineOffset { offset: f32 },
UnderlineSize { size: f32 },
UnderlineBrush { color: ColorJson },
Strikethrough { enabled: bool },
StrikethroughOffset { offset: f32 },
StrikethroughSize { size: f32 },
StrikethroughBrush { color: ColorJson },
LineHeight { height: f32 },
LetterSpacing { spacing: f32 },
WordSpacing { spacing: f32 },
Locale { locale: String },
// ... and 6 more for FontVariations, FontFeatures, etc.
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorJson {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl StylePropertyJson {
pub fn to_style_property(&self) -> Result<StyleProperty<SimpleBrush>, String> {
match self {
StylePropertyJson::FontSize { size } => {
Ok(StyleProperty::FontSize(*size))
}
StylePropertyJson::FontWeight { weight } => {
Ok(StyleProperty::FontWeight(FontWeight::new(*weight as f32)))
}
StylePropertyJson::Brush { color } => {
Ok(StyleProperty::Brush(SimpleBrush {
r: color.r,
g: color.g,
b: color.b,
a: color.a,
}))
}
// ... convert all variants
_ => todo!(),
}
}
}#[elicit_tool(
plugin = "parley_style",
name = "style_font_size",
description = "Create font size style property",
emit = Auto
)]
async fn style_font_size(p: StyleFontSizeParams) -> Result<CallToolResult, ErrorData> {
let style_json = StylePropertyJson::FontSize { size: p.size };
Ok(CallToolResult::success(json!({ "style": style_json })))
}
impl CustomEmit<StyleFontSizeParams> for StyleFontSizeEmit {
fn emit_code(params: &StyleFontSizeParams) -> TokenStream {
let size = params.size;
quote! { StyleProperty::FontSize(#size) }
}
}#[elicit_tool(
plugin = "parley_style",
name = "style_font_stack",
description = "Create font family stack",
emit = Auto
)]
async fn style_font_stack(p: StyleFontStackParams) -> Result<CallToolResult, ErrorData> {
let style_json = StylePropertyJson::FontStack {
families: p.families.clone(),
};
Ok(CallToolResult::success(json!({ "style": style_json })))
}
impl CustomEmit<StyleFontStackParams> for StyleFontStackEmit {
fn emit_code(params: &StyleFontStackParams) -> TokenStream {
let families: Vec<TokenStream> = params.families.iter()
.map(|f| {
let family = f.as_str();
quote! { #family }
})
.collect();
quote! {
StyleProperty::FontStack(FontStack::List(&[
#(FamilyName::Named(#families)),*
]))
}
}
}Goal: Serialize layout results with positioned glyphs and metrics.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutJson {
pub width: f32,
pub height: f32,
pub scale: f32,
pub is_rtl: bool,
pub lines: Vec<LineJson>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineJson {
pub index: usize,
pub metrics: LineMetricsJson,
pub text_range: RangeJson,
pub break_reason: String,
pub runs: Vec<RunJson>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LineMetricsJson {
pub baseline: f32,
pub ascent: f32,
pub descent: f32,
pub leading: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunJson {
pub font_size: f32,
pub text_range: RangeJson,
pub glyphs: Vec<GlyphJson>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GlyphJson {
pub id: u16,
pub x: f32,
pub y: f32,
pub advance: f32,
pub cluster: u32,
}#[elicit_tool(
plugin = "parley_layout",
name = "layout_to_json",
description = "Serialize entire layout with positioned glyphs",
emit = Auto
)]
async fn layout_to_json(p: LayoutToJsonParams) -> Result<CallToolResult, ErrorData> {
let plugin = get_plugin();
let layouts = plugin.layouts.lock().unwrap();
let layout = layouts.get(&p.layout_id)
.ok_or_else(|| ErrorData::new("Layout not found"))?;
let layout_json = serialize_layout(layout);
Ok(CallToolResult::success(json!({ "layout": layout_json })))
}
fn serialize_layout(layout: &Layout<SimpleBrush>) -> LayoutJson {
let lines: Vec<LineJson> = layout.lines()
.enumerate()
.map(|(i, line)| {
let metrics = line.metrics();
let runs: Vec<RunJson> = line.runs()
.map(|run| {
// Note: Actual glyph iteration requires more complex API
// This is simplified for the plan
RunJson {
font_size: run.font_size(),
text_range: RangeJson::from_range(run.text_range()),
glyphs: vec![], // TODO: extract glyph positions
}
})
.collect();
LineJson {
index: i,
metrics: LineMetricsJson {
baseline: metrics.baseline,
ascent: metrics.ascent,
descent: metrics.descent,
leading: metrics.leading,
},
text_range: RangeJson::from_range(line.text_range()),
break_reason: format!("{:?}", line.break_reason()),
runs,
}
})
.collect();
LayoutJson {
width: layout.width(),
height: layout.height(),
scale: layout.scale(),
is_rtl: layout.is_rtl(),
lines,
}
}Goal: Line breaking and alignment operations.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutBreakLinesParams {
pub layout_id: Uuid,
pub max_advance: f32,
}
#[elicit_tool(
plugin = "parley_layout",
name = "layout_break_lines",
description = "Compute line breaks with maximum advance (width)"
)]
async fn layout_break_lines(p: LayoutBreakLinesParams) -> Result<CallToolResult, ErrorData> {
let plugin = get_plugin();
let mut layouts = plugin.layouts.lock().unwrap();
let layout = layouts.get_mut(&p.layout_id)
.ok_or_else(|| ErrorData::new("Layout not found"))?;
layout.break_all_lines(p.max_advance);
Ok(CallToolResult::success(json!({
"layout_id": p.layout_id,
"line_count": layout.len()
})))
}#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LayoutAlignParams {
pub layout_id: Uuid,
pub container_width: Option<f32>,
pub alignment: String, // "Start", "Center", "End", "Justify"
}
#[elicit_tool(
plugin = "parley_layout",
name = "layout_align",
description = "Apply alignment to layout"
)]
async fn layout_align(p: LayoutAlignParams) -> Result<CallToolResult, ErrorData> {
let plugin = get_plugin();
let mut layouts = plugin.layouts.lock().unwrap();
let layout = layouts.get_mut(&p.layout_id)
.ok_or_else(|| ErrorData::new("Layout not found"))?;
let alignment = match p.alignment.as_str() {
"Start" => Alignment::Start,
"Center" => Alignment::Center,
"End" => Alignment::End,
"Justify" => Alignment::Justify,
_ => return Err(ErrorData::new("Unknown alignment")),
};
layout.align(p.container_width, alignment);
Ok(CallToolResult::success(json!({ "layout_id": p.layout_id })))
}Goal: Generate complete text layout code.
#[elicit_tool(
plugin = "parley_fragments",
name = "emit_ranged_builder",
description = "Generate RangedBuilder code with styles",
emit = Auto
)]
async fn emit_ranged_builder(p: EmitRangedBuilderParams) -> Result<CallToolResult, ErrorData> {
let default_styles = p.default_styles.iter()
.map(emit_style_property)
.collect::<Vec<_>>();
let ranged_styles = p.ranged_styles.iter()
.map(|rs| {
let prop = emit_style_property(&rs.property);
let range = emit_range(&rs.range);
format!("builder.push(&{}, {});", prop, range)
})
.collect::<Vec<_>>()
.join("\n ");
let code = format!(
r#"let mut builder = layout_ctx.ranged_builder(font_ctx, {});
// Default styles
{}
// Ranged styles
{}
// Build layout
let layout = builder.build("{}");
"#,
p.scale,
default_styles.iter()
.map(|s| format!("builder.push_default(&{});", s))
.collect::<Vec<_>>()
.join("\n"),
ranged_styles,
p.text.replace("\"", "\\\"")
);
Ok(CallToolResult::success(Content::text(code)))
}#[elicit_tool(
plugin = "parley_fragments",
name = "assemble_parley_binary",
description = "Generate complete executable with text layout",
emit = Auto
)]
async fn assemble_parley_binary(p: AssembleParams) -> Result<CallToolResult, ErrorData> {
let cargo_toml = generate_cargo_toml(&p);
let main_rs = generate_main_with_layout(&p);
Ok(CallToolResult::success(json!({
"Cargo.toml": cargo_toml,
"src/main.rs": main_rs,
"description": "Complete parley text layout binary"
})))
}
fn generate_main_with_layout(p: &AssembleParams) -> String {
format!(
r#"use parley::{{FontContext, LayoutContext, StyleProperty}};
fn main() {{
// Create contexts
let mut font_ctx = FontContext::new();
let mut layout_ctx = LayoutContext::new();
// Build layout
{}
// Break lines
layout.break_all_lines({});
// Print results
println!("Width: {{}}", layout.width());
println!("Height: {{}}", layout.height());
println!("Lines: {{}}", layout.len());
}}
"#,
p.builder_code,
p.max_advance
)
}- Phase 1a — Crate scaffold:
Cargo.toml,lib.rs,serde_types.rs - Phase 1b — Context registry:
context_registry.rs(20 runtime tools) - Phase 1c — Builder workflow: one-shot build operations (30 runtime tools)
- Phase 1d —
just check elicit_parley; fix compilation - Phase 2a — Style dual-mode tools: font properties (20 dual-mode tools)
- Phase 2b — Style dual-mode tools: text properties (20 dual-mode tools)
- Phase 2c — Style dual-mode tools: color/brush (20 dual-mode tools)
- Phase 2d —
just check elicit_parley - Phase 3a — Layout operations: break/align (40 runtime tools)
- Phase 3b — Layout serialization: JSON output (40 dual-mode tools)
- Phase 3c —
just check elicit_parley - Phase 4a — Line/run queries: metrics and iteration (50 runtime tools)
- Phase 4b — Font queries: database inspection (20 runtime tools)
- Phase 4c —
just check elicit_parley - Phase 5a — Fragment tools: builder code gen (15 fragment tools)
- Phase 5b — Fragment tools: context setup (10 fragment tools)
- Phase 5c — Fragment tools: layout queries (10 fragment tools)
- Phase 5d — Fragment tools: assembly (5 fragment tools)
- Phase 5e —
just check elicit_parley - Phase 6 — Wire into
elicit_serveremit chain
| Category | Count | Implementation Strategy |
|---|---|---|
| Runtime Context Management | 20 | UUID → FontContext/LayoutContext |
| Runtime Builder Workflow | 30 | One-shot build operations |
| Runtime Layout Operations | 40 | Break lines, align, query |
| Runtime Line/Run Queries | 25 | Metrics, text ranges, glyphs |
| Runtime Glyph Queries | 25 | Positions, advances, IDs |
| Runtime Font Queries | 20 | Font database inspection |
| Dual-Mode Style Properties | 50 | emit = Auto + CustomEmit |
| Dual-Mode Brush/Color | 20 | emit = Auto + CustomEmit |
| Dual-Mode Font Types | 30 | emit = Auto + CustomEmit |
| Dual-Mode Layout Serialization | 40 | emit = Auto + CustomEmit |
| Dual-Mode Text Ranges | 20 | emit = Auto + CustomEmit |
| Dual-Mode Alignment Types | 10 | emit = Auto + CustomEmit |
| Dual-Mode Line Break Types | 10 | emit = Auto + CustomEmit |
| Fragment Context Setup | 10 | Code generation only |
| Fragment Builder Code | 15 | Code generation only |
| Fragment Layout Computation | 10 | Code generation only |
| Fragment Assembly | 5 | Code generation only |
| Total | 380 |
- Rich Text: Full typography support (kerning, ligatures, OpenType)
- Bidirectional: RTL/LTR text handling (Arabic, Hebrew)
- International: Locale-aware line breaking, multi-script
- High Quality: HarfBuzz shaping engine integration
- Font Control: OpenType features, variation axes
- Positioned Output: Exact glyph coordinates for rendering
- CSS-like: Familiar styling properties (font-weight, line-height)
- Linebender Ecosystem: Used by xilem, masonry, vello
| Aspect | taffy | parley |
|---|---|---|
| Domain | Box layout (flexbox/grid) | Text layout (shaping/breaking) |
| Statefulness | Tree of boxes | Contexts + layouts |
| Complexity | CSS layout algorithms | Typography (HarfBuzz, OpenType) |
| Output | Box positions | Glyph positions |
| Runtime tools | 180 (53%) | 160 (42%) |
| Dual-mode tools | 120 (35%) | 180 (47%) |
| Total tools | 340 | 380 |
| Use case | UI layout | Text rendering |
Both are stateful and "straightforward":
- Natural JSON serialization for inputs/outputs
- Synchronous operations
- CSS-like properties
- Visual domains (UI vs text)
Parley adds typography complexity:
- Font shaping (HarfBuzz)
- OpenType features/variations
- Bidirectional text
- Unicode line breaking
xilem uses parley for text. elicit_parley enables:
- AI-generated rich text layouts
- Typography exploration tools
- Font feature testing
vello renders parley layouts. elicit_parley enables:
- GPU-rendered text generation
- Vector text for design tools
Any renderer can use parley's positioned glyphs:
- PDF generation
- Canvas rendering
- Game engine text
- Terminal UI