Skip to content

Optional flex/grid layout: options and a proposal #257

Open

Description

@silouanwright

On #223 you mentioned adding optional flex/grid layout later and kept layout free for it. I'd like to help build it and wanted to agree the shape first.

Options I looked at:

  1. No layout system
  2. Small helpers
  3. Layout attributes in svgr!
  4. Taffy behind a layout feature (my current recommendation)

1. No layout system

Positions stay absolute. The single-scene template already adapts to formats with UNIT and proportional positions, and a fit loop picks font sizes. What it can't express is flow: blocks that push each other down as text wraps, boxes that hug their text, and a row that becomes a column in portrait. SpeakerScene does this by hand: the abstract starts at TITLE_Y + occupied_height(), the corner bracket hangs off it with - 110, and the name pill is sized with estimate_text_width + 32 and centred at 1534 while the avatar sits at 1540. Every new block or format means redoing those numbers.

2. Small helpers

Something like stack and a bounding box on WrappedTextStructure. Small and dependency-free, and enough for SpeakerScene, but no wrapping, grow/shrink or grid. #80 tried stacks before text measurement existed and got stuck on child sizes, which text_width (and #256) now provide.

3. Layout attributes in svgr!

The nicest to write, but the macro can't see fonts or {} children, so it would be a large change. It could be added later on top of option 4.

4. Taffy behind a layout feature

Taffy does flex, grid and block in pure Rust, is MIT licensed, has no required dependencies, and is used by Bevy, Servo and Zed. Text is measured with fframes' own text measurement.

I built a working example on my fork using Taffy directly. One layout tree gives a lower third in 16:9 and a centred card in 9:16, and a long name wraps and grows the card:

The same title card laid out with Taffy in 16:9 and 9:16, with a short and a long name

A small wrapper in fframes could look like this:

// in render_frame: built and solved on first use, cached on the scene
let solved = self.layout.get_or_solve(&mut frame, ctx, (1920.0, 1080.0), |t| {
    let title = t.text(self.talk_title, title_font);
    let name = t.text(self.speaker_name, name_font);
    t.column(&[title, name], Style { gap: 18.0, ..Default::default() })
});
solved.rect(name)        // (x, y, w, h), already Animatable
solved.text_svgr(title)  // wrapped text at its solved position
  • Layout is solved once, not per frame, and cached like Captions' layout.
  • Motion doesn't change: layout gives resting positions, and you animate a wrapper <g transform> with frame.animate.
  • It works with today's text_width; fix(text): measure shaped text width instead of summing rounded advances #256 makes the widths exact.
  • The PR would add the example to e2e in both aspect ratios, with no unit tests.

Questions

  1. Does a runtime Taffy API match what you had in mind, or would you rather start from svgr! attributes?
  2. How should a scene know its design size: an explicit size argument, a generic V: Video, or a field on ctx?

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions