1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
// Ported from material_3_expressive v1.0.8 (MIT, © 2026 Paa Developments),
// `lib/components/app_bars/` — `m3e_app_bars.dart`, `enums/m3e_app_bar_enums.dart`,
// `styles/m3e_app_bar_theme.dart`, `components/m3e_app_bar_semantics.dart`
// (retrieved 2026-08-20). Upstream:
// https://github.com/paadevelopments/material_3_expressive
// Porting decisions (each restated with its reference site in the module docs
// below): the collapsing bar's scroll linkage is an app-fed controlled prop
// rather than a sliver-protocol participant; the small/collapsed band keeps M3's
// published 64dp rather than upstream's own 72dp content-padding model; the
// bar's slot edge insets and slot gap stay this catalog's 4dp rather than
// upstream's 0/8dp pair, while a top bar's title keeps M3's 16dp edge inset.
//! The M3 Expressive **app bar** family: four constructors over one set of
//! metrics, shape families, and densities.
//!
//! | Constructor | Upstream | Shape |
//! |---|---|---|
//! | [`app_bar`] | `M3EAppBar.top` | a fixed, single-line top bar: leading slot, title, actions |
//! | [`search_app_bar`] | `M3EAppBar.search` | the same bar with a [`crate::search_bar`] filling the title slot |
//! | [`bottom_app_bar`] | `M3EAppBar.bottom` | a bottom-docked action row with an optional prominent/FAB slot |
//! | [`sliver_app_bar`] | `M3EAppBar.sliver` | a collapsing bar whose headline interpolates between an expanded and a collapsed band |
//!
//! Every bar in the family shares the slot contract [`crate::toolbar`] and
//! [`crate::navbar`] also follow: a `leading` slot and an ordered `Vec` of
//! `actions` (plus the bottom bar's `fab` slot) are opaque caller-supplied
//! [`AnyView`] children routed through [`ChildPod`]s exactly like
//! [`frust::Row`]'s children — this family lays them out and routes events to
//! them, but never paints or tints their content itself (the leading slot's
//! `onSurface` / actions' `onSurfaceVariant` tinting guidance is the *caller's*
//! job to apply to whatever `AnyView` it supplies, e.g. an icon button; there is
//! no icon primitive here to tint, and upstream's own `IconTheme.merge`
//! ambient-icon-size push has no analogue on this framework's public surface).
//!
//! Every top and sliver bar also shifts its leading slot right by
//! `corner_insets.top_left.width` and its trailing edge left by
//! `corner_insets.top_right.width` whenever that corner's height is above zero
//! (the iPadOS 26+ window control). The band height is unchanged, the other
//! corners are ignored, the bottom bar is unaffected, and the value is never
//! consumed by `safe_area`, so a bar under a top-consuming safe area still sees it.
//! The shift assumes the bar spans the window's top edge; a bar placed elsewhere
//! (detail pane, sheet, dialog) opts out with `corner_shift(false)`. There is no
//! automatic detection.
//!
//! The **title** is the one child a top/collapsing bar fully owns when given as
//! a string: composed as a child [`frust::TextView`] (no hand-shaped text),
//! themed `onSurface` (the `Text` default role) so it survives a live theme swap
//! through `Text`'s own layout-time color resolution, ellipsized to one line
//! (upstream's `overflow: TextOverflow.ellipsis` on both `titleText` paths).
//! A top bar on [`AppBarContainer::PrimaryContainer`] themes it
//! `onPrimaryContainer` instead, through the same deferred color-role seam.
//! [`AppBarView::title_view`] replaces it with any caller view — upstream's
//! `title` widget slot, and what [`search_app_bar`] itself uses.
//!
//! # Top-bar container, elevation, and self-inset
//!
//! A top bar's fill resolves at paint time, so it follows a brightness flip
//! or a live theme swap. Precedence: [`AppBarView::background`], then an
//! explicit [`AppBarView::container`] role, then — when
//! [`AppBarView::elevation`] is above 0 — that elevation level's
//! `surface_role` (M3 tonal elevation: level 2 is `surfaceContainer`), then
//! `colors.surface`. An elevation above 0 also paints a drop shadow first, the
//! way [`mod@crate::card`]'s elevated variant does
//! ([`frust::authoring::PaintScene::draw_shadow`] from the level's
//! `ShadowSpec`, `colors.shadow` at its alpha), then the fill, then the slots.
//! A `Scaffold` paints its body before its app bar, so the shadow lands on the
//! body. `container(AppBarContainer::InversePrimary)` is the fill Flutter's
//! counter template gives its `AppBar`; `.elevation(2)` adds a level-2 shadow
//! beneath it.
//!
//! A top bar ([`app_bar`] and [`search_app_bar`]) consumes the **top, left
//! and right** window insets itself by default — Flutter's `AppBar`, whose
//! toolbar sits in a `SafeArea(bottom: false)` — the way
//! [`crate::navigation_bar`] consumes the bottom one: its `layout` reads
//! `ctx.window_insets().padding()`, grows the bar by the top inset, paints
//! the fill (and shadow) edge to edge through that band and across the full
//! width so a colored bar extends under the status bar and a landscape
//! cutout, and lays the slots out below the top inset and inside the side
//! ones. Every horizontal slot rule (leading at `PAD_X`, a bare title edge at
//! `TITLE_EDGE_INSET`, actions ending `PAD_X` in, the centered-title
//! arithmetic) measures from the inset-adjusted edge. A parent
//! `frust::safe_area` that already consumed an edge leaves 0 on it here, so
//! wrapping the bar in `safe_area(..).bottom(false)` never double-insets — it
//! only leaves the consumed bands outside the bar unpainted.
//! [`AppBarView::safe_area`]`(false)` opts out of all three edges for a bar
//! that is not docked to the window's top edge.
//!
//! The corner shift above stacks on the consumed side padding: a
//! `CornerInsets` width is measured from the safe-area edge, not the window
//! edge, so a slot's origin is `padding.left + corner_insets.top_left.width`
//! (mirrored on the right). That is exactly what the same bar gets under a
//! parent safe area that consumed the side padding first (the corner is never
//! consumed), so both compositions place the slots identically.
//!
//! # Metrics ([`AppBarMetrics`]), densities, and the one deliberate divergence
//!
//! | Band | Regular | Compact | Source |
//! |---|---|---|---|
//! | small / collapsed | 64 | 56 | m3.material.io/components/top-app-bars/specs |
//! | medium expanded | 112 | 104 | `M3EAppBarTheme.mediumExpanded` (= the M3 spec) |
//! | large expanded | 152 | 144 | `M3EAppBarTheme.largeExpanded` (= the M3 spec) |
//! | bottom | 80 | 80 | `M3EAppBarTheme.bottomHeight` (= the M3 spec) |
//!
//! [`AppBarDensity::Compact`] subtracts upstream's own
//! `compactHeightReduction` (8dp) from every band **but the bottom one** —
//! `M3EAppBar.bottom` pins `density: regular` and reads `bottomHeight` straight
//! off the theme, so there is no compact bottom bar to port.
//!
//! **The divergence**: upstream's `smallHeight`/`collapsedHeight` are both 72dp
//! (its own "8 + 56 + 8" content-padding model, `M3EAppBarTheme`'s own comment);
//! this port keeps the published M3 small-top-app-bar height of 64dp, which is
//! also what this catalog already shipped and what
//! [`crate::selection_app_bar`]'s contextual band mirrors so the two read as one
//! continuous surface across a selection-mode swap. Upstream's 72dp is
//! reachable as an explicit [`AppBarView::toolbar_height`] (its own
//! `toolbarHeight` override). The same reasoning keeps this family's 4dp slot
//! edge inset and 4dp slot gap rather than upstream's `contentPadding` (0
//! horizontal) + `titleGap` (8dp) pair: a leading or action icon button
//! carries a 48dp touch target, which puts its 24dp glyph at the M3 16dp mark.
//! The **title** is the exception: a top bar with no leading slot starts its
//! title `TITLE_EDGE_INSET` (16dp) from the leading edge, and one with no
//! actions ends the title's available width 16dp from the trailing edge —
//! the published M3 small-top-app-bar geometry (and Flutter's
//! `NavigationToolbar` middle spacing), since a bare title has no touch
//! target to carry it off the edge.
//!
//! # Shape families
//!
//! [`AppBarShapeFamily::Square`] resolves `theme.shape.none` and
//! [`AppBarShapeFamily::Round`] resolves `theme.shape.small` (8dp) —
//! `M3EAppBarTheme.shape`'s own `M3EShapes.radiusNone`/`radiusSmall` pair. A
//! square bar fills a plain rect, a round one a rounded rect. Upstream's own
//! per-constructor defaults are ported as-is: square for the top/search bars,
//! round for the collapsing one, square (unconfigurable) for the bottom one.
//!
//! # Scroll-linked collapse: the app owns the wiring
//!
//! A widget here has no window or scroll handle — [`frust::ScrollView`]'s two
//! seams ([`on_scroll`](frust::ScrollView::on_scroll),
//! [`on_refresh_release`](frust::ScrollView::on_refresh_release)) are installed
//! on the scroll view at construction, and only the app holds both it and the
//! bar. So the collapse is a **controlled prop**, the same app-side-rule shape
//! [`crate::SearchViewMode::for_width`] takes for the search view's
//! presentation split: the app feeds the [`ScrollInfo`](frust::ScrollInfo) it
//! observes into [`SliverAppBarView::scroll_offset`] (or a fraction it computes
//! itself into [`SliverAppBarView::collapse`]), and this widget is a pure
//! function of it.
//!
//! ```
//! use frust::{ScrollInfo, scroll_view, text};
//! use frust_material::{AppBarVariant, sliver_app_bar};
//!
//! struct App {
//! /// The last offset the body reported — the bar's collapse input.
//! scrolled: f64,
//! }
//!
//! fn header(state: &App) -> frust_material::SliverAppBarView<App> {
//! sliver_app_bar("Inbox")
//! .variant(AppBarVariant::Large)
//! .scroll_offset(state.scrolled)
//! }
//!
//! fn body(_state: &App) -> impl frust::View<App> + use<> {
//! scroll_view(text("content"))
//! .on_scroll(|state: &mut App, info: ScrollInfo| state.scrolled = info.offset)
//! }
//! ```
//!
//! [`AppBarCollapse`] is that mapping as a standalone pure function pair, so an
//! app can pin the same geometry its layout math needs (how tall the bar is at
//! a given offset, how far the scroll must travel to collapse it) without
//! building a view.
//!
//! There is no motion controller anywhere in this family: a collapse is a value
//! change, applied on the rebuild that carries it. That is already the
//! `reduce_motion` behavior every animated widget in this catalog falls back to,
//! so there is nothing here for that flag to collapse.
//!
//! # Semantics
//!
//! A top/collapsing bar is one accesskit container node of [`Role::TitleBar`]
//! (the closest accesskit vocabulary to "a window/app title bar region", chosen
//! over a generic `Role::GenericContainer` since accesskit publishes a
//! purpose-built role for exactly this region), labelled with
//! [`AppBarView::semantic_label`] or the title text, whose children are the
//! leading slot, the title, and each action in that order via
//! [`frust::authoring::SemanticsCtx::push_container`]. A bottom bar is a
//! [`Role::Toolbar`] container instead — the same role [`crate::toolbar`] uses,
//! since a bottom action row is not a title bar. This replaces upstream's
//! `M3ESliverSemantic` render-object wrapper (`m3e_app_bar_semantics.dart`),
//! which exists only because a Flutter sliver cannot carry a `Semantics` widget
//! directly.
//!
//! # Not ported
//!
//! * **`safeArea` / `MediaQuery.viewPadding` on the collapsing and bottom
//! bars.** Only the fixed top bar (and [`crate::selection_app_bar`]'s
//! contextual branch, which mirrors it) self-insets (see *Top-bar container,
//! elevation, and self-inset* above); a caller wraps a collapsing or bottom
//! app bar in `frust::safe_area(...)`.
//! * **`automaticallyImplyLeading`.** Upstream reads `Navigator.maybeOf(context)`
//! to synthesize a back button; a widget here has no navigator handle, and
//! this catalog's convention is an explicit `leading` slot (supply
//! [`fn@crate::icon_button`] with [`crate::icons::ARROW_BACK`]).
//! * **`floating`/`snap`.** Both need the scroll *direction* and a per-frame
//! sliver-protocol callback this framework has no seam for; an app that wants
//! reveal-on-scroll-up computes its own collapse fraction and feeds it in.
//! [`SliverAppBarView::pinned`] *is* ported — it selects whether the bar
//! bottoms out at the collapsed band or scrolls away entirely.
//! * **`elevation` / `Material` shadow on the collapsing and bottom bars.**
//! Upstream's own default is `0`, so a ported default paints nothing; only
//! the fixed top bar takes [`AppBarView::elevation`].
//! * **`clipBehavior`.** The collapsing bar clips its own content band once the
//! bar is shorter than it (see [`mod@sliver`]); nothing else in the family
//! overflows its box, so there is no clip knob to expose.
//! * **`foregroundColor` / `IconTheme.merge`.** Slot content is opaque here —
//! the caller styles its own icon buttons, per the slot contract above.
//! * **`M3EAppBarTheme` as a theme extension.** MaterialTokens-only: every
//! color/shape below resolves from [`frust::Theme`]/`ColorScheme` directly,
//! with an explicit builder override winning over both, the same precedence
//! [`mod@crate::list_item`]/[`mod@crate::card_list`] use.
//!
//! # Attribution
//!
//! See `plugins/material/NOTICE`'s "MIT License — Additional Copyright Holders
//! (Vendored Components)" section and its Module Attribution Header Convention.
use ;
use ;
use ;
use Color;
pub use ;
pub use ;
pub use ;
/// Container height of the small/center-aligned Top App Bar at
/// [`AppBarDensity::Regular`], in logical px.
///
/// Source: m3.material.io/components/top-app-bars/specs (also
/// material-components-android `docs/components/TopAppBar.md`). Deliberately
/// *not* upstream's own 72dp `smallHeight`/`collapsedHeight` — see the [module
/// docs](self)' metrics table for why, and
/// [`AppBarView::toolbar_height`](top::AppBarView::toolbar_height) for the
/// per-bar override that reaches it.
const HEIGHT: f64 = 64.0;
/// Horizontal inset from a bar's leading/trailing edges to the leading/action
/// slots, in logical px (upstream's `contentPadding`, 0 horizontal).
const PAD_X: f64 = 4.0;
/// Horizontal inset from a top bar's leading/trailing edge to its title when
/// no leading slot / no action sits on that side, in logical px — the M3
/// small-top-app-bar title margin (m3.material.io/components/top-app-bars/specs).
/// Slots keep [`PAD_X`]; see the [module docs](self)' divergence paragraph.
const TITLE_EDGE_INSET: f64 = 16.0;
/// Highest elevation level [`AppBarView::elevation`](top::AppBarView::elevation)
/// accepts (`theme.elevation.level5`); larger values clamp to it.
const MAX_ELEVATION: u8 = 5;
/// Gap between adjacent slots (leading↔title, title↔actions, action↔action), in
/// logical px (upstream's `titleGap`, 8dp).
const GAP: f64 = 4.0;
/// Height reduction [`AppBarDensity::Compact`] applies to every band but the
/// bottom bar's — upstream's `M3EAppBarTheme.compactHeightReduction`.
const COMPACT_REDUCTION: f64 = 8.0;
/// Expanded band of the medium collapsing bar, in logical px —
/// `M3EAppBarTheme.mediumExpanded`, which is also the published M3
/// medium-top-app-bar height.
const MEDIUM_EXPANDED: f64 = 112.0;
/// Expanded band of the large collapsing bar, in logical px —
/// `M3EAppBarTheme.largeExpanded`, which is also the published M3
/// large-top-app-bar height.
const LARGE_EXPANDED: f64 = 152.0;
/// Bottom app bar band, in logical px — `M3EAppBarTheme.bottomHeight`, which is
/// also the published M3 bottom-app-bar height. Density-independent (see the
/// [module docs](self)).
const BOTTOM_HEIGHT: f64 = 80.0;
/// Start and bottom inset of a collapsing bar's *expanded* headline, in logical
/// px — upstream's `FlexibleSpaceBar(titlePadding: start 16, bottom 16, end 16)`.
const HEADLINE_INSET: f64 = 16.0;
/// The collapsed title's M3 `titleLargeEmphasized` type-scale token, hardcoded
/// here rather than read from a live `Theme::type_scale`: unlike a themed
/// *color* or font *family*, `Text` has no layout-time-deferred *size/weight*
/// resolution seam (see `docs/CODE_STANDARDS.md`'s Theming conventions — only
/// a color role and an opted-in family role are resolved after `View::build`;
/// the title opts into the family, see `title_text_view`). Matches
/// `frust-theme::typography`'s `TITLE_LARGE_EMPHASIZED`: the baseline
/// `TITLE_LARGE` size/line-height (m3.material.io) with the weight stepped
/// Regular → Medium.
const TITLE_SIZE: f32 = 22.0;
/// Line height of [`TITLE_SIZE`]'s type token, in logical px.
const TITLE_LINE_HEIGHT: f32 = 28.0;
/// Weight shared by both title tokens — the M3E *emphasized* type-scale
/// consumption (Regular → Medium).
const TITLE_WEIGHT: FontWeight = MEDIUM;
/// The expanded headline's M3 `headlineSmallEmphasized` type-scale token, the
/// style upstream's `M3EAppBarTheme.titleStyle(collapsed: false)` resolves.
/// Hardcoded for the reason [`TITLE_SIZE`] is.
const HEADLINE_SIZE: f32 = 24.0;
/// Line height of [`HEADLINE_SIZE`]'s type token, in logical px.
const HEADLINE_LINE_HEIGHT: f32 = 32.0;
/// Unthemed fallback container fill for a top/collapsing bar (a theme resolves
/// this from `colors.surface`).
const CONTAINER: Color = from_rgb8;
/// Unthemed fallback container fill for a bottom bar (a theme resolves this from
/// `colors.surface_container`) — matches [`crate::toolbar`]'s and
/// [`crate::navbar`]'s own `CONTAINER` constant exactly, the same M3 "surface
/// container" role.
const BOTTOM_CONTAINER: Color = from_rgb8;
/// Unthemed fallback shadow color: opaque black at the elevation table's `0.3`
/// alpha, the same constant [`mod@crate::card`]'s elevated variant falls back to.
const FALLBACK_SHADOW_COLOR: Color = new;
/// Unthemed fallback corner radius for [`AppBarShapeFamily::Round`], in logical
/// px — `crate::tokens::shape_scale()`'s own `small` value.
const ROUND_RADIUS: f64 = 8.0;
/// The theme color role a top bar's container fills with —
/// [`AppBarView::container`](top::AppBarView::container). Resolved at paint
/// time from the live theme's scheme; an explicit
/// [`AppBarView::background`](top::AppBarView::background) still wins.
///
/// | Role | Fill | String-title color |
/// |---|---|---|
/// | [`Surface`](Self::Surface) | `surface` | `onSurface` |
/// | [`SurfaceContainer`](Self::SurfaceContainer) | `surfaceContainer` | `onSurface` |
/// | [`InversePrimary`](Self::InversePrimary) | `inversePrimary` | `onSurface` |
/// | [`PrimaryContainer`](Self::PrimaryContainer) | `primaryContainer` | `onPrimaryContainer` |
///
/// `InversePrimary` is the fill Flutter's own counter template gives its
/// `AppBar`; Flutter keeps the foreground `onSurface` there, and so does this.
/// Which collapsing-bar layout [`sliver_app_bar`] renders —
/// `M3EAppBarVariant`. The variant influences only the collapsing bar; the
/// fixed [`app_bar`] always renders the collapsed, single-line layout.
/// The corner shape family of an app bar container — `M3EAppBarShapeFamily`.
/// The vertical density of an app bar — `M3EAppBarDensity`.
/// The resolved band heights of one density — upstream's `M3EAppBarMetrics`,
/// as produced by `M3EAppBarTheme.metrics(density)`.
///
/// ```
/// use frust_material::{AppBarDensity, AppBarMetrics, AppBarVariant};
///
/// let regular = AppBarMetrics::for_density(AppBarDensity::Regular);
/// assert_eq!(regular.small_height, 64.0);
/// assert_eq!(regular.expanded(AppBarVariant::Large), 152.0);
///
/// // Compact takes 8dp off every band but the bottom bar's.
/// let compact = AppBarMetrics::for_density(AppBarDensity::Compact);
/// assert_eq!(compact.small_height, 56.0);
/// assert_eq!(compact.bottom_height, regular.bottom_height);
/// ```
/// The collapse geometry of one [`sliver_app_bar`] configuration: the pure
/// mapping between an app's scroll offset, the bar's collapse fraction, and the
/// band it occupies.
///
/// Every method is clamp-disciplined and NaN-safe — a non-finite input resolves
/// to the resting (expanded) end rather than propagating.
///
/// ```
/// use frust_material::{AppBarCollapse, AppBarDensity, AppBarVariant};
///
/// let geometry = AppBarCollapse::new(AppBarVariant::Large, AppBarDensity::Regular, true);
/// assert_eq!(geometry.expanded_height(), 152.0);
/// assert_eq!(geometry.collapsed_height(), 64.0);
///
/// // A pinned bar bottoms out at its collapsed band, 88px of scroll later.
/// assert_eq!(geometry.travel(), 88.0);
/// assert_eq!(geometry.height(0.0), 152.0);
/// assert_eq!(geometry.height(1.0), 64.0);
/// assert_eq!(geometry.fraction_for_offset(44.0), 0.5);
///
/// // Past the end, and off the map, both clamp.
/// assert_eq!(geometry.fraction_for_offset(1_000.0), 1.0);
/// assert_eq!(geometry.fraction_for_offset(f64::NAN), 0.0);
/// ```
/// `value` clamped into `[0, 1]`, mapping NaN to the resting `0.0` rather than
/// propagating it into a layout.
/// Linear interpolation from `a` to `b` at an already-clamped `t`.
/// `constraint` if finite, `0.0` otherwise — the degenerate unbounded-width
/// case every full-width bar in this family resolves the same way
/// ([`crate::toolbar`]'s docked variant included).
/// A bar's title: either a string this family owns and styles, or a caller view
/// it merely places (upstream's `titleText` / `title` pair).
pub
/// Build the title's type-erased child view at `size`/`line_height`: emphasized
/// title/headline text defaulting to the `Text` widget's own `OnSurface` themed
/// role (an app bar title reads as ordinary on-surface content), ellipsized to
/// one line like upstream's own `titleText`.
///
/// Its font family follows the live theme's `titleLargeEmphasized` role — the
/// collapsed, resting role — and by design that one family governs a sliver
/// bar's whole collapse. The sliver interpolates the title's size AND line
/// height between `headlineSmallEmphasized` (expanded) and
/// `titleLargeEmphasized` (collapsed) at build time, but a family cannot
/// interpolate, and switching faces mid-collapse would make the title visibly
/// jump. Under a type scale whose headline and title families differ — Glyph's
/// (Space Mono headline, IBM Plex Mono title) or Cupertino's (SF Pro Display
/// at 20pt and up, SF Pro Text below) — the expanded headline therefore
/// renders in the title face. The Material scale gives both roles Roboto Flex.
///
/// `role` is the themed color role: `OnSurface` everywhere but a top bar whose
/// container calls for a different foreground (see
/// [`AppBarContainer::title_role`]).
/// Materialize `slot` into a view reference, using `storage` to hold the text
/// case's freshly-composed view for the caller's borrow.
///
/// The two title cases have to reach [`frust::authoring::rebuild_children`] as
/// one uniform `&AnyView` list, and only the string case has a view to compose;
/// this is the one-slot storage that lets both spell it the same way.
/// [`title_ref`] with an explicit themed color `role` for the string case.
/// The resolved container fill of a top/collapsing bar. Precedence is the
/// builder's own `background` (applied by the caller), then
/// `colors.surface`, then the [`CONTAINER`] fallback.
/// The resolved container fill of a fixed top bar with an optional explicit
/// `container` role at `elevation` (already clamped). Precedence (the
/// builder's own `background` is applied by the caller first): `container`,
/// then the elevation level's `surface_role` when `elevation > 0`, then
/// `colors.surface`. Unthemed, `Surface` keeps the [`CONTAINER`] fallback and
/// every other role reads the Material 3 baseline light scheme.
/// `pick` applied to the theme's scheme, or to the Material 3 baseline light
/// scheme unthemed.
/// `role`'s color in `scheme`.
/// Elevation level `level` of the theme's table, or of this catalog's own
/// [`crate::tokens::elevation`] table unthemed. Callers pass the value
/// [`AppBarView::elevation`](top::AppBarView::elevation) already clamped to
/// [`MAX_ELEVATION`]; the match's last arm reads level 5 for anything above
/// it regardless.
/// The drop shadow a top bar at `elevation` paints beneath its fill, as
/// `(y_offset, blur_std_dev, color)` — `None` at level 0. Themed: the level's
/// `ShadowSpec` for the theme's brightness, `colors.shadow` at its alpha
/// (mirroring [`mod@crate::card`]'s `resolve_shadow_color`). Unthemed: the same
/// level of [`crate::tokens::elevation`] with [`FALLBACK_SHADOW_COLOR`].
/// The resolved container fill of a bottom bar — `colors.surface_container`
/// (upstream's `bottomBackgroundColor`), falling back to [`BOTTOM_CONTAINER`].
/// The resolved corner radius for `family` against a bar of `width`×`height`.
/// Themed: `shape.small` (round) / `shape.none` (square). Unthemed: the
/// equivalent literals, resolved the same way.
/// Fill a bar's container: a plain rect at a zero radius (so a square bar stays
/// one `fill_rect`, the shape every recorder scene observes), a rounded rect
/// otherwise.
/// Push the family's semantics container for `role`, labelled with `label` when
/// there is one, forwarding every slot pod in visual order.