day-pieces 0.6.1

Declarative app development API using native UI toolkits
Documentation
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
// Copyright © The Daybrite Project
// SPDX-License-Identifier: MPL-2.0

//! Toolbar items: `toolbar_button`, `toolbar_toggle`, `toolbar_menu`, `toolbar_label`,
//! `toolbar_segmented`, `toolbar_separator`, and [`Decorate::toolbar`], the one way to declare
//! them (docs/toolbars.md).
//!
//! Where an item appears is decided by the piece that declares it, so an app never says it twice:
//! items declared on a destination page ride the detail chrome and leave when the page does,
//! items on a `nav` ride its sidebar column, and items on a window's root piece ride every
//! page of that window. [`day_spec::ToolbarPlacement`] then says where on that chrome the item
//! sits.
//!
//! Search is not here. It is declared on the navigation surface it filters
//! (`Nav::searchable`, docs/search.md), which is what lets the platform move it (into the
//! navigation list on a narrow window) without the app re-declaring anything.
//!
//! A toolbar is chrome, not a piece: it is not laid out by day and does not live in the tree. It
//! lowers to day_spec's toolkit-neutral [`day_spec::ToolbarItem`] model, which each backend
//! realizes with its platform's bar: `NSToolbar`, `AdwHeaderBar`, `QToolBar`, `CommandBar`,
//! a `UINavigationItem`, a `MaterialToolbar` menu (docs/toolbars.md).
//!
//! ```ignore
//! reader_page(article).toolbar([
//!     toolbar_button(day_core::id!(refresh), tr("refresh")).icon(Symbol::Refresh).action(refresh_all),
//!     toolbar_toggle(day_core::id!(star), tr("star"), starred).icon(Symbol::Star),
//! ])
//! ```

use std::cell::RefCell;
use std::rc::Rc;

use day_reactive::{AnyBinding, Binding, Scope, bind, bind_seeded};
use day_spec::{Icon, Symbol, ToolbarItem, ToolbarItemKind, ToolbarPatch, ToolbarValue};

use crate::{IntoReactive, IntoText, MenuEntry, Reactive, TextSource};

/// A toolbar item under construction. Build a command with [`toolbar_button`], a two-state
/// button with [`toolbar_toggle`], a pull-down with [`toolbar_menu`], one control over a set of
/// choices with [`toolbar_segmented`], static text with [`toolbar_label`], and a divider with
/// [`toolbar_separator`]. Search is declared on the navigation surface instead
/// (`Nav::searchable`, docs/search.md).
#[derive(Clone)]
pub struct ToolbarEntry {
    id: String,
    kind: Kind,
    label: Option<TextSource>,
    tooltip: Option<TextSource>,
    icon: Option<Icon>,
    enabled: Reactive<bool>,
    action: Option<Rc<dyn Fn()>>,
    placement: day_spec::ToolbarPlacement,
    label_style: day_spec::LabelStyle,
    prominent: bool,
}

/// The app-side kinds, carrying the live signals the spec model cannot.
#[derive(Clone)]
enum Kind {
    Button,
    Segmented(Vec<Segment>, AnyBinding<usize>),
    Toggle(AnyBinding<bool>),
    // A command owns its checked state; native toggle intent invokes its handler, then reads
    // the resulting value back instead of writing into a second, mirrored signal.
    CommandToggle(Rc<dyn Fn() -> bool>),
    Menu(Vec<MenuEntry>),
    Label,
    Separator,
}

fn entry(id: day_core::ElementId, kind: Kind) -> ToolbarEntry {
    ToolbarEntry {
        id: id.as_str().to_owned(),
        kind,
        label: None,
        tooltip: None,
        icon: None,
        enabled: Reactive::Const(true),
        action: None,
        placement: day_spec::ToolbarPlacement::Automatic,
        label_style: day_spec::LabelStyle::Automatic,
        prominent: false,
    }
}

/// A push button: `toolbar_button(day_core::id!(refresh), tr("refresh")).icon(Symbol::Refresh).action(…)`.
pub fn toolbar_button<M>(id: day_core::ElementId, label: impl IntoText<M>) -> ToolbarEntry {
    ToolbarEntry {
        label: Some(label.into_text()),
        ..entry(id, Kind::Button)
    }
}

/// A row of mutually exclusive choices as one native control (docs/toolbars.md): the platform's
/// segmented control, bound to `selected`.
///
/// Reach for this instead of N toggles whenever exactly one choice is on at a time: a theme
/// picker, a view mode. The platform then draws it as the single control it is, announces it as
/// one, and keeps the exclusivity itself; three toggles leave all of that to the app.
///
/// ```ignore
/// toolbar_segmented(day_core::id!(theme), vec![
///     segment(tr("light")).icon(Symbol::Light),
///     segment(tr("system")).icon(Symbol::Auto),
///     segment(tr("dark")).icon(Symbol::Dark),
/// ], mode)
/// ```
pub fn toolbar_segmented(
    id: day_core::ElementId,
    segments: Vec<Segment>,
    selected: impl Binding<usize>,
) -> ToolbarEntry {
    ToolbarEntry {
        label: None,
        ..entry(id, Kind::Segmented(segments, AnyBinding::new(selected)))
    }
}

/// One choice in a [`toolbar_segmented`] control.
#[derive(Clone)]
pub struct Segment {
    label: TextSource,
    icon: Option<day_spec::Icon>,
}

/// A segment showing `label`; add `.icon(…)` for the platforms that draw one.
pub fn segment<M>(label: impl IntoText<M>) -> Segment {
    Segment {
        label: label.into_text(),
        icon: None,
    }
}

impl Segment {
    /// A standard [`Symbol`](day_spec::Symbol), drawn with the platform's own glyph.
    pub fn icon(mut self, s: day_spec::Symbol) -> Segment {
        self.icon = Some(day_spec::Icon::Symbol(s));
        self
    }
    /// A bundled image from `resource/images`, for a segment the standard set has no glyph for.
    pub fn image(mut self, name: impl Into<String>) -> Segment {
        self.icon = Some(day_spec::Icon::Image(name.into()));
        self
    }
}

/// A two-state button bound to `on` (a `Signal<bool>` or any other binding): the user flipping
/// it writes the binding, and writing the binding restyles the button.
pub fn toolbar_toggle<M>(
    id: day_core::ElementId,
    label: impl IntoText<M>,
    on: impl Binding<bool>,
) -> ToolbarEntry {
    ToolbarEntry {
        label: Some(label.into_text()),
        ..entry(id, Kind::Toggle(AnyBinding::new(on)))
    }
}

/// A button that drops a menu, built from the same entries [`crate::app_menu`] takes.
pub fn toolbar_menu<M>(
    id: day_core::ElementId,
    label: impl IntoText<M>,
    items: Vec<MenuEntry>,
) -> ToolbarEntry {
    ToolbarEntry {
        label: Some(label.into_text()),
        ..entry(id, Kind::Menu(items))
    }
}

/// Static text in the bar: a status or a caption.
pub fn toolbar_label<M>(id: day_core::ElementId, text: impl IntoText<M>) -> ToolbarEntry {
    ToolbarEntry {
        label: Some(text.into_text()),
        ..entry(id, Kind::Label)
    }
}

/// A divider, where the platform draws one (macOS toolbars have none, so AppKit renders it as a
/// fixed gap; docs/toolbars.md). It takes an id like every other item: an id is an item's
/// identity on the bar, which is what lets Day add and remove one item at a time.
pub fn toolbar_separator(id: day_core::ElementId) -> ToolbarEntry {
    entry(id, Kind::Separator)
}

impl ToolbarEntry {
    pub(crate) fn command_checked(mut self, checked: Rc<dyn Fn() -> bool>) -> Self {
        self.kind = Kind::CommandToggle(checked);
        self
    }

    /// Override a presentation id (for example, preserve an existing dayscript toolbar id).
    pub fn id(mut self, id: day_core::ElementId) -> Self {
        self.id = id.as_str().to_owned();
        self
    }
    /// Run `f` when the item is chosen. On a toggle or a search field the value binding carries
    /// the change; an action here runs in addition to it.
    pub fn action(mut self, f: impl Fn() + 'static) -> ToolbarEntry {
        self.action = Some(Rc::new(f));
        self
    }

    /// Draw a standard [`Symbol`], using the platform's icon set: an SF Symbol on macOS, a
    /// freedesktop icon name on GTK and Qt, a Fluent glyph on Windows.
    pub fn icon(mut self, symbol: Symbol) -> ToolbarEntry {
        self.icon = Some(Icon::Symbol(symbol));
        self
    }

    /// Draw a bundled image from `resource/images`, for an icon only this app has. Prefer
    /// [`ToolbarEntry::icon`] for anything standard: one PNG cannot look native on four desktops.
    pub fn image(mut self, name: impl Into<day_spec::ImageName>) -> ToolbarEntry {
        self.icon = Some(Icon::Image(name.into().as_str().to_string()));
        self
    }

    /// Hover help. Defaults to the item's label.
    pub fn tooltip<M>(mut self, t: impl IntoText<M>) -> ToolbarEntry {
        self.tooltip = Some(t.into_text());
        self
    }

    /// Whether the item can be used (default `true`): a constant, a `Signal<bool>`, or a
    /// closure re-evaluated whenever its reactive reads change. A reactive value patches the
    /// one item rather than rebuilding the bar, so a command greying out never disturbs a
    /// search field mid-word.
    pub fn enabled<M>(mut self, on: impl IntoReactive<bool, M>) -> ToolbarEntry {
        self.enabled = on.into_reactive();
        self
    }

    /// This item's role on the chrome carrying it (docs/toolbars.md): leading, centered,
    /// trailing, or first to fold away. It never names a surface: which chrome an item rides
    /// follows from the piece that declared it.
    pub fn placement(mut self, placement: day_spec::ToolbarPlacement) -> ToolbarEntry {
        self.placement = placement;
        self
    }

    /// Draw the title, the icon, or both, where the platform can do more than one. An item
    /// folded into an overflow menu shows its title whatever this asks for.
    pub fn label_style(mut self, style: day_spec::LabelStyle) -> ToolbarEntry {
        self.label_style = style;
        self
    }

    /// Draw this item in the platform's emphasized style, for the one action a chrome is
    /// really offering.
    pub fn prominent(mut self) -> ToolbarEntry {
        self.prominent = true;
        self
    }
}

/// Where a contribution's items come from: a fixed list, or a closure re-run on every change.
#[derive(Clone)]
pub enum ToolbarSource {
    Fixed(Vec<ToolbarEntry>),
    Derived(Rc<dyn Fn() -> Vec<ToolbarEntry>>),
}

/// Anything [`crate::Decorate::toolbar`] accepts: one entry, a list of them, or a closure that
/// derives the list and re-runs whenever its reactive reads change.
///
/// The marker parameter `M` is what lets one method name take all three. A blanket impl over
/// `Fn() -> Vec<ToolbarEntry>` and a concrete impl for `Vec<ToolbarEntry>` overlap as far as
/// coherence can tell, since it will not assume `Vec` never gains an `Fn` impl. This is the same
/// E0119 dodge as [`IntoText`](crate::IntoText), and resolved the same way.
pub trait ToolbarContent<M> {
    fn into_source(self) -> ToolbarSource;
}

/// Marker for a single [`ToolbarEntry`].
pub struct OneMark;
/// Marker for a list of entries: a `Vec` or an array.
pub struct ManyMark;
/// Marker for a closure that derives the list.
pub struct DerivedMark;

impl ToolbarContent<OneMark> for ToolbarEntry {
    fn into_source(self) -> ToolbarSource {
        ToolbarSource::Fixed(vec![self])
    }
}

impl ToolbarContent<ManyMark> for Vec<ToolbarEntry> {
    fn into_source(self) -> ToolbarSource {
        ToolbarSource::Fixed(self)
    }
}

impl<const N: usize> ToolbarContent<ManyMark> for [ToolbarEntry; N] {
    fn into_source(self) -> ToolbarSource {
        ToolbarSource::Fixed(Vec::from(self))
    }
}

impl<F: Fn() -> Vec<ToolbarEntry> + 'static> ToolbarContent<DerivedMark> for F {
    fn into_source(self) -> ToolbarSource {
        ToolbarSource::Derived(Rc::new(self))
    }
}

/// The item a sidebar host draws for itself: leading, the platform's glyph, and no app
/// action, since the click drives that window's split directly.
pub fn sidebar_toggle_item(host: day_core::RNode) -> ToolbarEntry {
    toolbar_button(
        day_core::id!(day::sidebar_toggle),
        day_localization::t(day_localization::key::day_toggle_sidebar),
    )
    .icon(Symbol::Sidebar)
    .placement(day_spec::ToolbarPlacement::Navigation)
    // The host rides in the closure: a second window's button toggles that window's own
    // sidebar, never the first host the toolkit happens to find.
    .action(move || {
        day_core::toggle_sidebar(host);
    })
}

/// Register `content` against `chrome` for as long as the current reactive scope lives.
///
/// The one path every declaration takes: [`crate::Decorate::toolbar`] resolves the chrome from
/// where the piece sits, and a `nav` names its own sidebar page explicitly. A fixed list
/// lowers once; a derived one lowers inside an `Effect`, and each pass owns its bindings through
/// a child scope the next pass disposes, so a re-derived bar leaves no binding writing patches at
/// items that no longer exist.
pub fn contribute(chrome: day_core::Chrome, content: ToolbarSource) {
    // Whether the page carrying these is on screen. The toolkits that draw one bar per window
    // are handed the showing pages' items already merged, and this is how day-core knows which
    // those are: the same gate stack `register_nav` uses, so "showing" means one thing across
    // the navigation layer (docs/toolbars.md).
    // Whether the page carrying these is on screen, from the page itself (docs/toolbars.md).
    let active = day_core::current_page_gate();
    let gate = active.clone();
    // Captured here, at the declaration site: a derived list re-runs long after this build, when
    // no page is being built and the column would answer `Window` (docs/toolbars.md).
    let column = day_core::current_page_column();
    let token = match content {
        ToolbarSource::Fixed(entries) => {
            let texts: Vec<(Option<TextSource>, Option<TextSource>)> = entries
                .iter()
                .map(|e| (e.label.clone(), e.tooltip.clone()))
                .collect();
            let token = day_core::register_contribution_gated(
                chrome,
                lower(entries, chrome, column),
                active,
            );
            // A fixed list lowers once, so a localized label or tooltip is followed here: the
            // items keep their actions and values and only take the new words.
            let live = |t: &Option<TextSource>| matches!(t, Some(TextSource::Dyn(_)));
            if texts.iter().any(|(l, t)| live(l) || live(t)) {
                let resolve = move || -> Vec<(String, Option<String>)> {
                    texts
                        .iter()
                        .map(|(l, t)| {
                            (
                                l.as_ref().map(TextSource::resolve).unwrap_or_default(),
                                t.as_ref().map(TextSource::resolve),
                            )
                        })
                        .collect()
                };
                let seed = day_reactive::untrack(&resolve);
                bind_seeded(seed, resolve, move |now: &Vec<(String, Option<String>)>| {
                    day_core::relabel_contribution(token, now)
                });
            }
            token
        }
        ToolbarSource::Derived(builder) => {
            let token = day_core::register_contribution_gated(chrome, Vec::new(), active);
            let pass: Rc<RefCell<Option<Scope>>> = Rc::new(RefCell::new(None));
            let outer = Scope::child();
            outer.enter(|| {
                day_reactive::Effect::new(move || {
                    // Track the locale even when the builder has no localized reads of its own.
                    let _ = day_localization::locale().get();
                    // Reactions rerun in the caller's ambient scope. Capture the owner
                    // explicitly, and keep both entry construction (AnyBinding slots) and
                    // lowering (value callbacks/bindings) in the same owned pass. A root
                    // scope would survive removal of this view and read its disposed slots.
                    let next = outer.create_child();
                    let items = next.enter(|| lower(builder(), chrome, column));
                    if let Some(old) = pass.borrow_mut().replace(next) {
                        old.dispose();
                    }
                    day_core::update_contribution(token, items);
                });
            });
            token
        }
    };
    // Recompose when what is on screen changes. Reading the gate inside an effect subscribes to
    // whatever it reads (the selection, the pushed path), so a tab switch or a push re-merges
    // the window's bar without the navigation layer having to announce it.
    if let Some(gate) = gate {
        day_reactive::Effect::new(move || {
            let _ = gate();
            day_core::chrome_changed();
        });
    }
    Scope::current().on_cleanup(move || day_core::unregister_contribution(token));
}

/// Lower app-side entries to the spec model, registering each item's closures with day-core and
/// wiring the live bindings (toggle state, search text, a reactive `enabled`).
fn lower(
    entries: Vec<ToolbarEntry>,
    chrome: day_core::Chrome,
    column: day_spec::ToolbarColumn,
) -> Vec<ToolbarItem> {
    entries
        .into_iter()
        .map(|e| {
            let ToolbarEntry {
                id,
                kind,
                label,
                tooltip,
                icon,
                enabled,
                action,
                placement,
                label_style,
                prominent,
            } = e;
            let label = label.map(|t| t.initial()).unwrap_or_default();
            let extra = action;

            // Buttons and menus dispatch through the menu registry; toggles and search fields
            // register a value callback instead (day-core keeps both in one id space).
            let (kind, action_id) = match kind {
                Kind::Button => (
                    ToolbarItemKind::Button,
                    extra
                        .clone()
                        .map(day_core::register_menu_action)
                        .unwrap_or(0),
                ),
                Kind::Menu(items) => (
                    ToolbarItemKind::Menu {
                        items: crate::lower_menu(items),
                    },
                    extra
                        .clone()
                        .map(day_core::register_menu_action)
                        .unwrap_or(0),
                ),
                Kind::Toggle(on) => {
                    let seed = on.get_untracked();
                    let item = id.clone();
                    let extra = extra.clone();
                    let act = day_core::register_toolbar_value(Rc::new(move |v: &ToolbarValue| {
                        if let ToolbarValue::On(next) = v {
                            on.set(*next);
                            if let Some(f) = &extra {
                                f();
                            }
                        }
                    }));
                    // The app's own writes patch the one item back.
                    bind_seeded(
                        seed,
                        move || on.get(),
                        move |v: &bool| {
                            day_core::patch_chrome(
                                chrome,
                                ToolbarPatch::On {
                                    item: item.clone(),
                                    on: *v,
                                },
                            );
                        },
                    );
                    (ToolbarItemKind::Toggle { on: seed }, act)
                }
                Kind::CommandToggle(read) => {
                    let seed = day_reactive::untrack(|| read());
                    let item = id.clone();
                    let after = read.clone();
                    let act = day_core::register_toolbar_value(Rc::new(move |v: &ToolbarValue| {
                        if let ToolbarValue::On(wanted) = v {
                            // Native toggles and dayscript carry desired state. Do not undo an
                            // update another presentation already made, or an idempotent set.
                            if *wanted != day_reactive::untrack(|| after())
                                && let Some(f) = &extra
                            {
                                f();
                            }
                            // Restore even when the handler declines the change: native toggles
                            // may have optimistically flipped before sending their event.
                            day_core::patch_chrome(
                                chrome,
                                ToolbarPatch::On {
                                    item: item.clone(),
                                    on: day_reactive::untrack(|| after()),
                                },
                            );
                        }
                    }));
                    let item = id.clone();
                    bind_seeded(
                        seed,
                        move || read(),
                        move |on: &bool| {
                            day_core::patch_chrome(
                                chrome,
                                ToolbarPatch::On {
                                    item: item.clone(),
                                    on: *on,
                                },
                            );
                        },
                    );
                    (ToolbarItemKind::Toggle { on: seed }, act)
                }
                Kind::Segmented(segments, sel) => {
                    let seed = sel.get_untracked().min(segments.len().saturating_sub(1));
                    let item = id.clone();
                    let extra = extra.clone();
                    let act = day_core::register_toolbar_value(Rc::new(move |v: &ToolbarValue| {
                        if let ToolbarValue::Selected(next) = v {
                            sel.set(*next);
                            if let Some(f) = &extra {
                                f();
                            }
                        }
                    }));
                    // The app's own writes patch the one item back, exactly as a toggle's do.
                    bind_seeded(
                        seed,
                        move || sel.get(),
                        move |v: &usize| {
                            day_core::patch_chrome(
                                chrome,
                                ToolbarPatch::Selected {
                                    item: item.clone(),
                                    index: *v,
                                },
                            );
                        },
                    );
                    (
                        ToolbarItemKind::Segmented {
                            segments: segments
                                .into_iter()
                                .map(|s| day_spec::ToolbarSegment {
                                    title: s.label.initial(),
                                    icon: s.icon,
                                })
                                .collect(),
                            selected: seed,
                        },
                        act,
                    )
                }
                Kind::Label => (ToolbarItemKind::Label, 0),
                Kind::Separator => (ToolbarItemKind::Separator, 0),
            };

            // Seed the item from the source, rather than leaving the declared default and
            // letting the binding correct it: the binding's first run happens here, inside
            // `lower`, and the model it patches is only stored by the window-bar edit this list
            // is on its way to, so the correction landed on the previous bar (or nothing)
            // and the new one installed enabled. A command that starts out unavailable then
            // lowered live and answered dayscript's `toolbar:` step.
            let enabled_now = enabled.get_untracked();
            if let Reactive::Dyn(_) = &enabled {
                let item = id.clone();
                bind(
                    move || enabled.get(),
                    move |on: &bool| {
                        day_core::patch_chrome(
                            chrome,
                            ToolbarPatch::Enabled {
                                item: item.clone(),
                                on: *on,
                            },
                        );
                    },
                );
            }
            let enabled = enabled_now;

            ToolbarItem {
                id,
                kind,
                label,
                tooltip: tooltip.map(|t| t.initial()),
                icon,
                enabled,
                action: action_id,
                placement,
                label_style,
                prominent,
                column,
            }
        })
        .collect()
}

#[cfg(feature = "conformance")]
pub(crate) mod conformance {
    use day_core::conformance::{Case, Drive};
    use day_reactive::Signal;
    use day_spec::Cap;

    use crate::*;

    /// A toolbar button declared by a page runs its action.
    #[day_macros::test(day_core)]
    fn toolbar_button_runs_action() -> Case {
        Case::new()
            .proves_duty("edit_toolbar")
            .proves_cap(Cap::Toolbar)
            .proves_modifier("toolbar")
            .requires(Cap::Toolbar)
            .page(|| {
                let count = Signal::new(0i64);
                label(move || format!("count {}", count.get()))
                    .id(day_core::id!(count))
                    .toolbar([toolbar_button(day_core::id!(bump), "Bump")
                        .action(move || count.update(|n| *n += 1))])
            })
            .drive(|d: Drive| async move {
                d.toolbar_press("bump").await?;
                d.toolbar_press("bump").await?;
                d.assert_text("count", "count 2").await
            })
    }

    /// A toolbar toggle writes its signal.
    #[day_macros::test(day_core)]
    fn toolbar_toggle_writes_signal() -> Case {
        Case::new()
            .proves_duty("update_toolbar")
            .proves_modifier("toolbar")
            .requires(Cap::Toolbar)
            .page(|| {
                let on = Signal::new(false);
                label(move || format!("star {}", on.get()))
                    .id(day_core::id!(star))
                    .toolbar([toolbar_toggle(day_core::id!(star), "Star", on)])
            })
            .drive(|d: Drive| async move {
                d.toolbar_toggle("star", true).await?;
                d.assert_text("star", "star true").await?;
                d.toolbar_toggle("star", false).await?;
                d.assert_text("star", "star false").await
            })
    }
}