Skip to content

InlineBox and InlineImage - #25710

Merged
alice-i-cecile merged 38 commits into
bevyengine:mainfrom
ickshonpe:inline-box
Sep 10, 2026
Merged

InlineBox and InlineImage#25710
alice-i-cecile merged 38 commits into
bevyengine:mainfrom
ickshonpe:inline-box

Conversation

@ickshonpe

@ickshonpe ickshonpe commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Objective

Allow space to be reserved for non-text items in text layouts.

Solution

New InlineBox component with size and kind fields, kind controls whether the box should take up space in the layout or be zero sized (so kind of equivalent to absolute positioning). InlineBox isn't intended to be a user facing API. Instead, primary widget components should require InlineBox and update it as needed (for instance if its image asset changes size).

The traversal traits in text_access now support InlineBox. InlineBox entities are always leaf nodes, if they have TextSpan children, those children are ignored. This is so that InlineBox can have children to support more complex widgets composed from multiple entities.

TextPipeline::update_buffer adds the inline boxes yielded by the text reader to the Parley layout context's RangedBuilder. And update_text_layout_info adds a list of the laid out boxes to each text entity's TextLayoutInfo.

Also includes a very basic InlineImage component and accompanying systems that allows images to be interspersed with UI text. Support for resizing, flipping and atlas options can be added in a follow up PR.

  • Parley's custom out of flow mode isn't supported, for now.
  • Text picking is unchanged. It should be trivial to make InlineBoxes pickable, left out to keep this PR a manageable size.
  • The example is very limited, it's just there to show that InlineImage actually works. It should be extended once the complete InlineImage api is added.

Testing

Includes a basic example:

cargo run --example inline_image

A UI testbed scene, Scene::InlineImage:

cargo run --example testbed_ui -- InlineImage

And a traversal test, inline_boxes_are_accessible_through_text_access in bevy_sprite::text2d.

Showcase

image

…rator fields now expect a `TextItem` enum that can either be text or an inline box. Both are added to the layout using Parley's ranged builder.
These are updated in `update_text_layout_info` from the parley generated layout.
…tem to `bevy_ui`. Options for sizing and flipping etc can be added later, this is just a very basic implemenation that allows us to test that inline image widgets work.
…extWriter` traversal traits.

Added traversal test for text2d. Updated UI accessibility.
@ickshonpe ickshonpe added A-Rendering Drawing game state to the screen A-UI Graphical user interfaces, styles, layouts, and widgets D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 7, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in Rendering Sep 7, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in UI Sep 7, 2026
@ickshonpe ickshonpe added C-Feature A new feature, making something new possible and removed A-Rendering Drawing game state to the screen labels Sep 7, 2026
@ickshonpe
ickshonpe requested a review from viridia September 7, 2026 11:59
@ickshonpe ickshonpe changed the title InlineBox InlineBox and InlineImage Sep 7, 2026
Improved doc comments.

Fixed failing `inline_boxes_are_accessible_through_text_access` test, was just misusing the new API.
@ickshonpe ickshonpe added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Sep 8, 2026
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Sep 8, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 8, 2026
viridia added a commit to viridia/bevy_action_map that referenced this pull request Sep 9, 2026
The phase headings had stopped carrying information. Phase VII had prose and no
chunks, its prose described a tier of docs/issues.md that is now empty and
retired, and everything else was either "Phase IX" or "Unscheduled by phase".
Six semantic categories replace them, ordered roughly by priority, with the
gated table still last.

Every chunk body is byte-identical to its previous version; the only content
changes are the headings, their intro prose, and the Glyph ids row.

That row loses two of its three gates. The generic tier's blank, unlabeled
buttons become a content task rather than a design question, since the stamp can
be authored onto the icons by hand. And the presentation shape stops being
forced: an icon prompt had to be block-level because PromptSpan is TextSpan-based
and Bevy had no inline image in a text run, which made falling back from an icon
to a word a change of layout kind rather than of content. bevyengine/bevy#25710
removes that once it merges.

"Never built" said identifiers are defined. They are not — R18.4 specifies one
and no code declares it, the only occurrence of "glyph" in src/ being a doc
comment.

LLM Usage Disclosure: implementation, tests and documentation written by
Claude Opus 5; design decisions, review and acceptance by the author.
viridia added a commit to viridia/bevy_action_map that referenced this pull request Sep 9, 2026
Its gate is met. The row deferred on an identifier scheme that Kenney's real
file names were the way to falsify, and the import did that: all 164 entries in
assets/input_prompts/ resolve as
format!("input_prompts/{tier}/{}.png", control.name()), with no lookup table.

Scoped so the three things that could have made it large are stated omissions.
Icons go only where PromptSpan already renders, not into the settings table's
capture cells, which have no width to spare and want the authoritative name
rather than a glyph. The generic tier's stamped art is a content task and stays
out. And bevyengine/bevy#25710 gates the inline form only — the identifier, the
resolution and the manifest do not wait on it, so the chunk can land with a
block-level prompt if the PR has not merged.

Resolution reads the manifest rather than probing for a file, so a gap steps to
the next tier and a missing path is never a load error.

LLM Usage Disclosure: implementation, tests and documentation written by
Claude Opus 5; design decisions, review and acceptance by the author.
@alice-i-cecile
alice-i-cecile added this pull request to the merge queue Sep 10, 2026
Merged via the queue into bevyengine:main with commit ac70f41 Sep 10, 2026
42 checks passed
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in Rendering Sep 10, 2026
@github-project-automation github-project-automation Bot moved this from Needs SME Triage to Done in UI Sep 10, 2026
viridia added a commit to viridia/bevy_action_map that referenced this pull request Sep 13, 2026
R18.4's identifier lands as `Glyph`/`GlyphTier` in present.rs — a brand's
own tier tried before its generic fallback, `#[non_exhaustive]` for R18.9's
foreign shapes rather than guessing at one before a backend exists to
shape it against. `IconPromptSpan` resolves and renders it, falling back
to `PromptSpan`'s own text, bracketed, wherever nothing has art.

`InlineImage` (bevyengine/bevy#25710) turned out to size itself from the
loaded image's own pixels with no resize hook, so the 64px source pack
towered over a button's own text. `assets/input_prompts_inline/` is the
fix: a pre-scaled second copy of every entry, generated by
import_input_prompts.py rather than sized at runtime. Getting the resize
itself right took two more rounds: a naive `-resize` bled the source's
white-under-transparency into the scaled edge as a halo, and a palette
small enough to compress well flattened the antialiasing into visible
octagonal bands. Compositing over black and back, in full RGBA, is what
actually reads clean at 25px.

Verified on Disasteroids' Cancel/Confirm buttons, which also dropped
their hardcoded parentheses — a caller no longer wraps either outcome in
its own punctuation, since an icon reads as a control on its own and
bracketing is `IconPromptSpan`'s fallback to add, not the caller's.

Split Friction's `Join` moves off `ControlClass::AnyButton` onto
`GamepadButton::South` and `KeyCode::Enter` directly, both because a
class binding has no Steam expression (docs/steam.md) and because it
gives the join screen's caption something concrete to name instead of a
hardcoded "press any button." That caption now reads live off `Join`'s
own bindings. The one thing this traded away: `Fired`'s value is
device-agnostic by design, so knowing which device actually pressed
`Join` now means reading `Gamepad`/`ButtonInput` directly in
`pair_on_join`, which does not survive a Steam authority — flagged in
Roadmap's deferred table rather than left to be rediscovered.

Not doing: the block `IconPrompt` component. `IconPromptSpan` covers the
inline case this chunk verified; a block one waits for a screen that
needs it.

LLM Usage Disclosure: implementation, tests and documentation written by
Claude Sonnet 5; design decisions, review and acceptance by the author.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-UI Graphical user interfaces, styles, layouts, and widgets C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Uncontroversial This work is generally agreed upon

Projects

Status: Done
Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants