Skip to content

feat: add Text component with per-span styling#12

Merged
AsPulse merged 2 commits into
mainfrom
feat/text-component
May 26, 2026
Merged

feat: add Text component with per-span styling#12
AsPulse merged 2 commits into
mainfrom
feat/text-component

Conversation

@AsPulse
Copy link
Copy Markdown
Member

@AsPulse AsPulse commented May 26, 2026

Adds a Text vector component that shapes a sequence of styled spans through rustybuzz and emits one filled Path per glyph through the existing VectorGraphic pipeline. Per-span styling is expressed as overrides on each TextSpan (fill / font / size / weight), and the shaped spans can be pulled out as independently placeable graphics for per-span effects.

  • tellur-core::text (new): Text { font, size, weight, fill, spans: Vec<TextSpan> }, with TextSpan holding text plus Option<Paint> / Option<Arc<Font>> / Option<f32> / Option<Weight> overrides. Spans inherit any unset field from the enclosing Text. Shaping runs through rustybuzz::shape; glyph outlines come from ttf_parser::Face::outline_glyph and are converted to PathCommands. Line metrics (ascender/descender/line_gap) come from the base font.
  • Font: owns the font bytes via Arc<Vec<u8>> plus a face_index for .ttc collections. Constructors:
    • Font::from_bytes(bytes) / Font::from_bytes_indexed(bytes, index)
    • Font::load_file(path)
    • Font::find_by_name(name) / Font::find_by_name_with_weight(name, weight) via fontdb
    • Font::sans_serif() / serif() / monospace() / cursive() / fantasy() via fontconfig (the same machinery fc-match uses, so per-language / per-script overrides on the host are honoured rather than relying on a hardcoded fallback list).
  • Static SANS_SERIF / SERIF / MONOSPACE / CURSIVE / FANTASY as LazyLock<Arc<Font>>: the OS resolution runs once on first access; subsequent uses are just Arc::clone.
  • Weight newtype (CSS-style 100..900 with THIN / ... / BLACK constants) drives the OpenType wght axis via rustybuzz::Face::set_variations. Has visible effect on fonts whose wght axis renders correctly through ttf-parser; on others (or non-VF fonts), use find_by_name_with_weight to load the per-weight file from the family instead.
  • Text::into_spans() -> Vec<Placed<TextSpanGraphic>> decomposes a shaped line into one independently placed component per input TextSpan. Each TextSpanGraphic is a VectorComponent whose layout box matches the span's own width × the line height, so callers can pattern-match let [a, b, c] = text.into_spans().try_into().unwrap(); and apply per-span effects (rasterize → outline / drop shadow / transform) before recomposing into a layer.
  • tellur-core::layer: VectorLayer.size and Layer.size change from Vec2 to Option<Vec2>. None auto-fits to the children's combined paint bounds — view_box.origin (vector) / paint_rect.origin (raster) tracks the bounding rect's origin so children at negative positions are kept inside the box rather than clipped. ::fit() constructors mirror ::new(size). Auto-fit children are laid out under Constraints::UNBOUNDED; Fill-mode children there collapse to zero on each axis, which is the expected interaction.
  • tellur-core::placement: impl From<Placed<C>> for Placed<dyn VectorComponent> (and the raster counterpart) for C: VectorComponent + 'static, so concrete-typed Placed<TextSpanGraphic> etc. flow into Vec<Placed<dyn _>> via .into() without a hand-written cast. This is what lets Text::into_spans()'s output drop straight into a VectorLayer.
  • Dependencies: rustybuzz (shaping, re-exports ttf-parser), fontdb (system font database for find_by_name), fontconfig (FFI binding for find_generic — the dev shell's flake.nix gets pkgs.fontconfig so the linker resolves libfontconfig).
  • Example: tellur-renderer/examples/text_to_png.rs (new) renders "Hello world!" with the middle span in red, centered on a 1920×1080 white canvas, font resolved via the SANS_SERIF static.

@AsPulse AsPulse merged commit f3fb995 into main May 26, 2026
14 checks passed
@AsPulse AsPulse deleted the feat/text-component branch May 26, 2026 17:16
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.

1 participant