Skip to content

styled_text: add compact style spans - #631

Open
waywardmonkeys wants to merge 1 commit into
linebender:mainfrom
waywardmonkeys:styled-text-first-slice
Open

styled_text: add compact style spans#631
waywardmonkeys wants to merge 1 commit into
linebender:mainfrom
waywardmonkeys:styled-text-first-slice

Conversation

@waywardmonkeys

@waywardmonkeys waywardmonkeys commented May 26, 2026

Copy link
Copy Markdown
Contributor

Add styled_text as a first-slice crate for compact styled text storage.

The core crate stores resolved complete styles as compact StyleId spans over attributed_text, with layout and paint payloads interned separately so paint-only changes can share layout identity. It includes a builder that resolves overlapping patch ranges and reusable segment workspaces for downstream layout or paint passes.

This intentionally leaves lowering to Parley, cascading, editing policy, and application-specific style vocabularies outside the core styled_text crate.

@waywardmonkeys

Copy link
Copy Markdown
Contributor Author

This was done with the assistance of Codex (GPT 5.3, 5.4, 5.5, medium, high, xhigh).

@waywardmonkeys

Copy link
Copy Markdown
Contributor Author

Some things I have in mind for follow up PRs:

  1. Public builder polish
  • Make the builder API feel like the obvious way to construct styled text.
  • Reduce “find a range, apply patch” ergonomics where possible.
  • Maybe add a push-oriented builder shape for appending text with current style state.
  1. Base style / patch convenience
  • Provide a small default patch type or helper for common style updates.
  • Keep the trait-based StylePatch escape hatch, but avoid making every user define boilerplate for basic cases.
  1. Better Parley adapter ergonomics
  • Make styled_text_parley easier to call from real code.
  • Keep the reusable workspace story front and center.
  • Avoid multiplying APIs; one good path is better than a pile of helpers.
  1. Benchmarks / memory measurements
  • Add focused measurements for style interning, paint-only changes, lowering into Parley, and workspace reuse.
  • Maybe add malloc_size_of or equivalent only if we decide the dependency is acceptable.

@PoignardAzur PoignardAzur left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick note: I think this code would be much easier to review if you split it into roughly one file per type. Right now it's kind of hard to find where types are defined without doing a lot of Ctrl+F.

@waywardmonkeys
waywardmonkeys removed the request for review from taj-p June 1, 2026 05:59
@waywardmonkeys
waywardmonkeys force-pushed the styled-text-first-slice branch 2 times, most recently from 30aec40 to bb51b0a Compare June 1, 2026 08:00
@waywardmonkeys
waywardmonkeys force-pushed the styled-text-first-slice branch from bb51b0a to 45f79cc Compare June 26, 2026 00:52
@waywardmonkeys waywardmonkeys changed the title styled_text: add compact style spans and Parley lowering styled_text: add compact style spans Jun 26, 2026
@waywardmonkeys

Copy link
Copy Markdown
Contributor Author

I have removed styled_text_parley from this PR leaving this one with just the base styled_text work ... I will be doing the styled_text_parley as a separate PR on top of this one.

Add `styled_text` as a first-slice crate for compact styled text storage.

The core crate stores resolved complete styles as compact `StyleId` spans over `attributed_text`, with layout and paint payloads interned separately so paint-only changes can share layout identity. It includes a builder that resolves overlapping patch ranges and reusable segment workspaces for downstream layout or paint passes.

This intentionally leaves lowering to Parley, cascading, editing policy, and application-specific style vocabularies outside the core `styled_text` crate.
@waywardmonkeys
waywardmonkeys force-pushed the styled-text-first-slice branch from 45f79cc to 4722b21 Compare June 26, 2026 01:37

@DJMcNab DJMcNab left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems reasonable to me.

Especially as we're not going to depend on this in Parley, it's fairly low-risk for experimentation. And it does unlock some doors for things we will need (HTML richtext copy/paste, etc.).

Comment thread styled_text/src/lib.rs
//! That keeps the core crate independent of any toolkit's style vocabulary
//! while giving downstream code a simple stream of text ranges and full styles.
//!
//! ## Concepts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit:

Suggested change
//! ## Concepts
//! # Concepts

(Plus elsewhere in this file)

This isn't blocking, but it means that if I make the README changes before this lands then we get consistency.

Comment thread .github/workflows/ci.yml
run: cargo rdme --check --heading-base-level=0 --workspace-project=parlance

- name: Run cargo rdme (styled_text)
run: cargo rdme --check --heading-base-level=0 --workspace-project=styled_text

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know I need to go back to this repo and apply this change more widely, but we may as well start with the new best practise.

This previous mess was my fault, and I now know that the standard approach is to use h1 subheadings in Rust code.

Suggested change
run: cargo rdme --check --heading-base-level=0 --workspace-project=styled_text
run: cargo rdme --check --workspace-project=styled_text

///
/// This record is exposed for diagnostics, invalidation decisions, and adapters
/// that need to know whether two full styles share the same layout or paint
/// payload. Text spans should store [`StyleId`], not this component record.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a conceptual question, are the memory savings from interning a 2-id structure really worth much? Are we expecting to retain the same StyleId an average of more than twice for the majority of cases - or is this a play for handling huge documents (order of book length?)?

Comment on lines +208 to +209
layout: self.get_layout(record.layout_id())?,
paint: self.get_paint(record.paint_id())?,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these unwrap, or debug assert or something? It seems to me that this can only fail if something has gone wrong in our own bookkeeping.

Comment on lines +231 to +234
id: StyleId,
record: StyleRecord,
layout: &'a L,
paint: &'a P,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a conceptual reason these aren't public fields (potentially #[non_exhaustive])

/// Builder for [`StyleSet`] values.
///
/// Interning is currently a linear search over retained vectors. That keeps the
/// first slice dependency-free and compact; callers can reuse a builder during

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the "first slice" here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, a note to myself that should be removed. First slice of the implementation.

self.intern_style_record(StyleRecord { layout, paint })
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to check my understanding, it's not possible to directly intern a style record which is a combination of two existing layouts and paints from ids?

Comment thread styled_text/src/text.rs
Comment on lines +23 to +24
/// [`apply_style`](Self::apply_style). To introduce new styles, rebuild through
/// [`StyledTextBuilder`](crate::StyledTextBuilder) or a fresh [`StyleSet`].

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First-pass comment: I wonder if we can expose Arc::make_mut in a sensible way here.

Comment on lines +168 to +172
/// Finishes the builder and returns compact resolved styled text.
///
/// Overlapping patches are applied in the order they were applied to the
/// builder over a clone of the base layout and paint styles for each
/// resolved non-base segment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is (up to) $$\mathcal{O}(n^2)$$ in the number of patches, right?

} else {
let mut resolved_spans = Vec::new();
let mut workspace = AttributeSegmentsWorkspace::new();
let mut pending: Option<(TextRange, StyleId)> = None;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this needs a comment here. I'm having a hard time grasping what this does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants