SIGN IN SIGN UP

story: Join the toolbar buttons and add a million-row table example (#2697)

## Description

Two related bits of story polish.

**Toolbar buttons now read as one control.** `dropdown_child` gave every
dropdown
its own `ButtonGroup` holding a single button, and `ButtonGroup` skips
all border
merging at one child — so a typical toolbar (Size + Options) rendered as
two
fully-rounded buttons touching each other, with a doubled border down
the middle.
`StoryToolbar` now keeps its items unbuilt until render, where it knows
the count
and each index, and squares off the inner corners.

This also let the `StoryToolbarMenuTrigger` wrapper go (struct plus four
trait
impls): `Popover::trigger` only requires `Selectable + IntoElement`,
which
`Button` already satisfies.

No component was changed — this is entirely inside `crates/story`.

**DataTable story: a million-row example.** Replaces the 1,000-row
option; 5,000
stays, since that is what the story opens with. The rarely-used Clear
button
moves into the Options menu, and the ID column no longer centers its
text.

## Performance

Two things had to change before a million rows was worth showing.

| | before | after |
| :--- | ---: | ---: |
| `random_stocks(1_000_000)`, debug | 24.0 s | 0.71 s |
| `random_stocks(10_000)`, debug | 231 ms | 27.7 ms |

Every row randomized all 40-odd fields independently. Rows are now
cloned from a
512-row random pool, each still getting its own id and
`Counter::random()`, so no
two rows read alike on screen. Retained size is unchanged at 408 B/row
(389 MB at
a million rows).

The remaining ~0.7 s still landed on the main thread between two paints,
which
was visible as a hitch on switching. Generation moved to the background
executor
behind the story's existing `full_loading` state, so no frame is
dropped.

Separately, the "Refresh Data" tick walked the entire table every 33 ms
and
randomized ~1/3 of the rows. At a million rows that is millions of field
draws
per frame to update the few dozen rows on screen, which would have made
the new
example unusable with that option on. It is now bounded to the first
2,000 rows.

## How to Test

```sh
cargo run
```

Open the DataTable story:

- the toolbar buttons form one segmented control, no doubled borders;
- Rows offers 100 / 500 / 5,000 / 10,000 / 1,000,000, with 5,000 checked
on open;
- switching to 1,000,000 shows the loading state and does not hitch;
- Options ends with a separator and Clear Selection;
- Refresh Data stays smooth at a million rows.

`cargo check`, `cargo clippy` and `rustfmt --check` are clean for the
changed
files.

## AI Assistance

AI assisted with the implementation and with measuring the generation
timings
quoted above.

## Checklist

- [x] I have read the [CONTRIBUTING](../CONTRIBUTING.md) document and
followed the guidelines.
- [x] Reviewed the changes in this PR and confirmed AI generated code
(if any) is accurate.
- [x] `cargo run` story verification — see How to Test.
- [x] Cross-platform performance testing is not applicable; no
platform-specific path changed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
J
Jason Lee committed
dc2943fac0ee9b907e21cc415867bd45615c220c
Parent: bbe0675
Committed by GitHub <noreply@github.com> on 8/13/2026, 3:39:07 PM