Skip to content

Add breadcrumb primitive - #114

Merged
ShannonHeylmun merged 4 commits into
mainfrom
105-consider-a-breadcrumb-component-wordmarksegments-separator-current
Jul 6, 2026
Merged

Add breadcrumb primitive#114
ShannonHeylmun merged 4 commits into
mainfrom
105-consider-a-breadcrumb-component-wordmarksegments-separator-current

Conversation

@ShannonHeylmun

@ShannonHeylmun ShannonHeylmun commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

An ordered trail of segments joined by a themed chevron separator, for app-chrome navigation (CITADEL › Trade dashboard). Pure composition, no custom widget: flex_row of button/label/icon.

A segment is interactive (renders as a quiet inline button) if built with .on_select(...), or a plain current-location label otherwise — so "last segment styled as current by default" falls out naturally from simply not attaching a callback to it, with no separate "is current" flag to keep in sync.

Closes #105.

Summary by CodeRabbit

  • New Features
    • Added a new breadcrumb navigation component to the gallery, including an interactive demo and a static comparison.
    • Breadcrumbs can now be clicked to update the current location and jump between levels.
    • Breadcrumbs are available as a reusable UI element in the component set.
  • Accessibility / UX
    • Breadcrumbs now provide accessible landmarks and current-page semantics.
    • Icons can be marked as decorative so assistive technologies treat them as hidden.
  • Style
    • Updated component listings to include the new breadcrumb entry.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ShannonHeylmun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51e20c7c-0970-4f1b-96bf-90c167e136fd

📥 Commits

Reviewing files that changed from the base of the PR and between b03a31f and 7138cc1.

📒 Files selected for processing (8)
  • examples/gallery.rs
  • src/components/access_wrap.rs
  • src/components/breadcrumb/demo.rs
  • src/components/breadcrumb/mod.rs
  • src/components/breadcrumb/view.rs
  • src/components/icon/view.rs
  • src/components/mod.rs
  • src/lib.rs
📝 Walkthrough

Walkthrough

Adds a breadcrumb component with rendering primitives, accessibility annotations, a demo panel, and gallery/module wiring. Icon rendering also gains a decorative mode that hides glyphs from assistive technologies.

Changes

Breadcrumb component and demo

Layer / File(s) Summary
Breadcrumb view
src/components/breadcrumb/view.rs
Defines breadcrumb segments and builders, then renders ordered segments with chevron separators, interactive callbacks, current-page labeling, and trail-level navigation annotation.
Breadcrumb demo panel
src/components/breadcrumb/demo.rs
Adds demo state, live breadcrumb interaction, jump controls, panel layout, view lifecycle handling, and the public panel constructor.
Module and gallery wiring
src/components/breadcrumb/mod.rs, src/components/mod.rs, examples/gallery.rs, src/lib.rs
Registers the breadcrumb module, re-exports its API, adds ComponentKind::Breadcrumb with label/order support, wires the gallery panel switch, and rewraps an unrelated export list in lib.rs.

Accessibility support

Layer / File(s) Summary
Access annotations and decorative icons
src/components/access_wrap.rs, src/components/icon/view.rs
Introduces transparent accesskit annotation wrappers, role/state mapping, tests, and icon decoration that hides decorative glyphs from assistive technologies.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant BreadcrumbDemoPanel
  participant BreadcrumbDemo
  participant BreadcrumbView

  User->>BreadcrumbView: click selectable segment or jump control
  BreadcrumbView->>BreadcrumbDemo: update location
  BreadcrumbDemoPanel->>BreadcrumbDemo: read updated location during rebuild
  BreadcrumbDemoPanel->>BreadcrumbView: regenerate trail view
  BreadcrumbDemoPanel-->>User: render updated breadcrumb trail
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: zheylmun

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly states the main change: adding a breadcrumb primitive.
Linked Issues check ✅ Passed The new breadcrumb builder matches #105 with ordered segments, interactive callbacks, and a current trailing segment by default.
Out of Scope Changes check ✅ Passed The demo and accessibility helpers appear to support the breadcrumb feature rather than introduce unrelated functionality.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 105-consider-a-breadcrumb-component-wordmarksegments-separator-current

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/breadcrumb/view.rs`:
- Around line 87-119: Breadcrumb::render is missing an explicit use<Capture> for
the returned opaque widget view, which can cause the elided &Theme lifetime to
be captured and prevent boxing into Box<AnyWidgetView<State, Action>>. Update
Breadcrumb::render to use the suggested use<State, Action> capture on the impl
WidgetView return type so the rendered view stays compatible with the existing
`'static` children collection and button/label/icon rendering path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b2957685-a3f3-4bcd-a725-874a20de21bb

📥 Commits

Reviewing files that changed from the base of the PR and between cda8993 and e464b8a.

📒 Files selected for processing (6)
  • examples/gallery.rs
  • src/components/breadcrumb/demo.rs
  • src/components/breadcrumb/mod.rs
  • src/components/breadcrumb/view.rs
  • src/components/mod.rs
  • src/lib.rs

Comment on lines +87 to +119
pub fn render(self, theme: &Theme) -> impl WidgetView<State, Action>
where
State: 'static,
Action: 'static,
{
let mut children: Vec<Box<AnyWidgetView<State, Action>>> =
Vec::with_capacity(self.segments.len() * 2);
for (i, seg) in self.segments.into_iter().enumerate() {
if i > 0 {
children.push(Box::new(
icon(IconName::ChevronRight)
.color(theme.palette.text_faint)
.size(theme.typography.size_caption)
.render(theme),
));
}
let view: Box<AnyWidgetView<State, Action>> = match seg.on_select {
Some(on_select) => Box::new(
button(move |s: &mut State| on_select(s))
.label(seg.label)
.variant(ButtonVariant::Text)
.tint(theme.palette.text_muted)
.render(theme),
),
None => Box::new(label(seg.label).color(theme.palette.text).render(theme)),
};
children.push(view);
}
flex_row(children)
.cross_axis_alignment(CrossAxisAlignment::Center)
.gap(Length::px(f64::from(theme.density.pad) / 3.0))
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the crate's declared Rust edition and compare `render`/view-builder
# signatures across components for `use<...>` bound consistency.

fd Cargo.toml -x cat {}

echo "--- functions/methods taking &Theme and returning impl WidgetView, with their use<> bounds ---"
rg -n -B2 -A2 'fn render.*theme: &Theme.*-> impl WidgetView' src/components --type=rust
rg -n 'use<' src/components --type=rust

Repository: VoidstarSolutions/void_ui

Length of output: 18002


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- breadcrumb demo around trail() ---"
sed -n '1,140p' src/components/breadcrumb/demo.rs

echo "--- AnyWidgetView / WidgetView definitions ---"
rg -n -A4 -B4 'type AnyWidgetView|trait WidgetView|impl WidgetView' src -t rust

Repository: VoidstarSolutions/void_ui

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- breadcrumb demo state / panel construction ---"
sed -n '1,260p' src/components/breadcrumb/demo.rs

echo "--- breadcrumb view around render() ---"
sed -n '1,180p' src/components/breadcrumb/view.rs

Repository: VoidstarSolutions/void_ui

Length of output: 11731


🌐 Web query:

Rust 2024 RPIT precise capturing use<...> anonymous lifetime &Theme trait object AnyWidgetView 'static

💡 Result:

In the Rust 2024 edition, return-position impl Trait (RPIT) opaque types automatically capture all in-scope type and lifetime parameters [1][2]. Precise capturing, enabled via the use<...> syntax, allows developers to explicitly control or "opt-out" of this default behavior by specifying exactly which parameters are captured by the opaque type [3][4]. The use<...> syntax functions as a syntactic bound [5]. For example, impl Trait + use<'a> specifies that only the lifetime 'a is captured [6]. Using use<> (empty brackets) is a common way to specify that the opaque type should capture nothing, which can be useful for ensuring an opaque type is 'static when it would otherwise implicitly capture a non-'static lifetime [7]. Regarding AnyWidgetView in the Xilem framework, it is a type alias representing a trait object: dyn AnyView<State, Action, ViewCtx, Pod<DynWidget>> + Send + Sync [8][9]. Because it is a dynamic trait object, it does not involve RPIT opaque types directly in its definition. However, when working with WidgetView trait methods such as boxed()—which returns a Box<AnyWidgetView<...>>—you may encounter use<...> bounds in function signatures to manage capture requirements for the underlying types being returned [10][11]. For instance, a function returning an impl WidgetView<State> may be annotated with + use<State> to explicitly capture the State type parameter, satisfying the requirements for the returned opaque type [11]. In summary: - Rust 2024 automatically captures all in-scope generics in RPIT [2]. - use<...> provides granular control to limit or precisely define these captures [3][6]. - AnyWidgetView is a dyn-compatible type alias used in Xilem for type-erased views [8][12]. - The use<...> syntax is often used in conjunction with trait-based return types to ensure the correct generic parameters (like State) are captured for the opaque return type [11].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- AnyWidgetView alias and boxing helpers ---"
rg -n -A6 -B6 'type AnyWidgetView|fn boxed|boxed\(' src /root/.cargo/registry -g '*.rs' 2>/dev/null | head -n 200

Repository: VoidstarSolutions/void_ui

Length of output: 8204


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- boxing patterns for AnyWidgetView ---"
rg -n -A3 -B3 'Box<AnyWidgetView|boxed\(\)|AnyWidgetView<' src -g '*.rs'

Repository: VoidstarSolutions/void_ui

Length of output: 42377


Add + use<State, Action> to Breadcrumb::render. In edition 2024, the elided &Theme lifetime is captured by the opaque return type, which makes this view harder to box into the 'static Box<AnyWidgetView<...>> used by the breadcrumb demo/state path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/breadcrumb/view.rs` around lines 87 - 119, Breadcrumb::render
is missing an explicit use<Capture> for the returned opaque widget view, which
can cause the elided &Theme lifetime to be captured and prevent boxing into
Box<AnyWidgetView<State, Action>>. Update Breadcrumb::render to use the
suggested use<State, Action> capture on the impl WidgetView return type so the
rendered view stays compatible with the existing `'static` children collection
and button/label/icon rendering path.

@ShannonHeylmun
ShannonHeylmun force-pushed the 105-consider-a-breadcrumb-component-wordmarksegments-separator-current branch from e464b8a to 9e749a5 Compare July 5, 2026 18:15
@ShannonHeylmun
ShannonHeylmun requested a review from zheylmun July 5, 2026 18:23
@ShannonHeylmun
ShannonHeylmun force-pushed the 105-consider-a-breadcrumb-component-wordmarksegments-separator-current branch 2 times, most recently from 17b70e8 to ec3e6b1 Compare July 5, 2026 19:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/components/access_wrap.rs (2)

195-225: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Optional: tests only cover accessibility_role, not the node mutation. The accessibility() branches (set_aria_current(Page) / set_hidden()) — the actual behavior callers depend on — are untested. Worth adding if a Node can be constructed in a unit test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/access_wrap.rs` around lines 195 - 225, The current tests in
AccessAnnotateWidget only verify accessibility_role(), but they do not cover the
behavior in accessibility() where the Node is mutated for
AccessAnnotation::CurrentPage and AccessAnnotation::Hidden. Add a unit test
around AccessAnnotateWidget::accessibility() that constructs or mocks a Node and
asserts the branches call set_aria_current(Page) and set_hidden() respectively,
alongside the existing role checks, so the actual accessibility mutations are
covered.

170-188: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an accessible name to the navigation landmark. The breadcrumb wrapper is exposed as Role::Navigation with no label; threading an optional label through annotate (for example, node.set_label("Breadcrumb")) would make it easier to distinguish from other navigation landmarks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/access_wrap.rs` around lines 170 - 188, The breadcrumb
wrapper’s Navigation landmark is unlabeled, so update access_wrap.rs to thread
an optional label through annotate/accessibility for
AccessAnnotation::Navigation and set an accessible name on the node (for example
via node.set_label) while keeping Role::Navigation unchanged; use the
accessibility_role and accessibility methods in AccessWrap to locate the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/components/access_wrap.rs`:
- Around line 195-225: The current tests in AccessAnnotateWidget only verify
accessibility_role(), but they do not cover the behavior in accessibility()
where the Node is mutated for AccessAnnotation::CurrentPage and
AccessAnnotation::Hidden. Add a unit test around
AccessAnnotateWidget::accessibility() that constructs or mocks a Node and
asserts the branches call set_aria_current(Page) and set_hidden() respectively,
alongside the existing role checks, so the actual accessibility mutations are
covered.
- Around line 170-188: The breadcrumb wrapper’s Navigation landmark is
unlabeled, so update access_wrap.rs to thread an optional label through
annotate/accessibility for AccessAnnotation::Navigation and set an accessible
name on the node (for example via node.set_label) while keeping Role::Navigation
unchanged; use the accessibility_role and accessibility methods in AccessWrap to
locate the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc61dd7b-fbd0-4251-a898-8d38f627b6d9

📥 Commits

Reviewing files that changed from the base of the PR and between e464b8a and b03a31f.

📒 Files selected for processing (8)
  • examples/gallery.rs
  • src/components/access_wrap.rs
  • src/components/breadcrumb/demo.rs
  • src/components/breadcrumb/mod.rs
  • src/components/breadcrumb/view.rs
  • src/components/icon/view.rs
  • src/components/mod.rs
  • src/lib.rs
✅ Files skipped from review due to trivial changes (2)
  • src/lib.rs
  • examples/gallery.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/components/breadcrumb/mod.rs
  • src/components/mod.rs
  • src/components/breadcrumb/view.rs
  • src/components/breadcrumb/demo.rs

@ShannonHeylmun
ShannonHeylmun force-pushed the 105-consider-a-breadcrumb-component-wordmarksegments-separator-current branch from b03a31f to 605b10c Compare July 6, 2026 22:48
An ordered trail of segments joined by a themed chevron separator, for
app-chrome navigation (CITADEL › Trade dashboard). Pure composition,
no custom widget: flex_row of button/label/icon.

A segment is interactive (renders as a quiet inline button) if built
with .on_select(...), or a plain current-location label otherwise —
so "last segment styled as current by default" falls out naturally
from simply not attaching a callback to it, with no separate
"is current" flag to keep in sync.

Closes #105.
New src/components/breadcrumb/widget.rs: BreadcrumbNav, a transparent single-child pass-through widget (same shape as masonry's own Passthrough/SizedBox) whose only job is accessibility_role() -> Role::Navigation. No layout/paint of its own — it delegates measure/layout/children straight through to its child.
view.rs: added BreadcrumbNavView<V>, a thin hand-written View wrapper (mirroring xilem_masonry's own sized_box implementation) that builds the flex_row trail, then wraps its resulting widget in BreadcrumbNav. Breadcrumb::render now returns this wrapped view instead of the bare flex_row.
The trick that made this tractable: rather than fighting AnyWidgetView's type-erasure (which is hardcoded to Pod<Passthrough> inside xilem itself, so you can't substitute a custom role there), the wrapper stores its child as a plain masonry WidgetPod<dyn Widget> — exactly how masonry's own SizedBox/Passthrough do it — and uses .downcast() in rebuild/teardown/message to get back to the concrete inner view's element type.
BreadcrumbCurrent (new widget in breadcrumb/widget.rs) — same transparent single-child pass-through shape as BreadcrumbNav, but its accessibility() calls node.set_aria_current(AriaCurrent::Page) — accesskit's native equivalent of the web's aria-current="page". Reports Role::GenericContainer itself (the text stays exposed via the wrapped label).
BreadcrumbCurrentView<V> (new in view.rs) — same hand-written single-child View pattern as BreadcrumbNavView, wrapping just the trailing/current segment's label.
Breadcrumb::render's "no on_select" branch now wraps its label(...) in this instead of rendering it bare.
New shared primitive: src/components/access_wrap.rs — since this was the third time I needed "a transparent single-child widget that exists only to attach one piece of accesskit state" (after BreadcrumbNav for #1 and BreadcrumbCurrent for #2), I consolidated all three into one AccessAnnotateWidget + annotate() view function, parameterized by an AccessAnnotation enum (Navigation / CurrentPage / Hidden). This replaced breadcrumb's two bespoke widgets (deleted breadcrumb/widget.rs entirely) and is now the crate-wide fix point for this whole class of problem.
icon().decorative() — new builder method. When set, wraps the rendered glyph in annotate(_, AccessAnnotation::Hidden), which calls accesskit's node.set_hidden(). This is the actual crate-wide fix: any icon anywhere in void_ui can now opt out of being read by a screen reader, not just breadcrumb's.
Breadcrumb's chevron separator now calls .decorative(), closing the specific gap you flagged.
@ShannonHeylmun
ShannonHeylmun force-pushed the 105-consider-a-breadcrumb-component-wordmarksegments-separator-current branch from 605b10c to 7138cc1 Compare July 6, 2026 22:57
@ShannonHeylmun
ShannonHeylmun merged commit ae7fa18 into main Jul 6, 2026
4 checks passed
@ShannonHeylmun
ShannonHeylmun deleted the 105-consider-a-breadcrumb-component-wordmarksegments-separator-current branch July 7, 2026 04:45
@coderabbitai coderabbitai Bot mentioned this pull request Jul 9, 2026
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.

Consider a breadcrumb component (wordmark/segments · separator · current)

1 participant