Skip to main content

iced_cube/application/
status_bar.rs

1//! A thin bar along the bottom edge of a window, with start, centre and
2//! end sections of small items: text, an icon with text, a clickable
3//! item, a spinner, a progress bar and a badge.
4//!
5//! Status usually comes from outside the UI thread, such as a sync job, a
6//! build or the editor's cursor, so items arrive through a channel.
7//! [`subscription`] creates a bounded channel, emits [`Event::Ready`] with
8//! its [`Sender`] once, then delivers whatever producers send in batches as
9//! [`Event::Received`]. Producers send [`Update`]s: set an item by its id,
10//! replacing any item with the same id, or remove one. [`animation`] ticks
11//! only while a spinner is showing.
12//!
13//! ```no_run
14//! use iced::{Element, Subscription};
15//! use iced_cube::status_bar::{self, Section, Update, status_bar};
16//!
17//! #[derive(Debug, Clone)]
18//! enum Message {
19//!     Status(status_bar::Event<&'static str>),
20//! }
21//!
22//! #[derive(Default)]
23//! struct App {
24//!     status: status_bar::State<&'static str>,
25//!     sender: Option<status_bar::Sender<&'static str>>,
26//! }
27//!
28//! impl App {
29//!     fn update(&mut self, message: Message) {
30//!         let Message::Status(event) = message;
31//!         if let Some(status_bar::Output::Ready(mut sender)) = self.status.update(event) {
32//!             // Hand clones of the sender to worker threads or tasks.
33//!             let _ = sender.try_send(Update::Set(
34//!                 status_bar::text("cursor", "Ln 1, Col 1").section(Section::End),
35//!             ));
36//!             self.sender = Some(sender);
37//!         }
38//!     }
39//!
40//!     fn view(&self) -> Element<'_, Message> {
41//!         status_bar(&self.status).on_event(Message::Status).into()
42//!     }
43//!
44//!     fn subscription(&self) -> Subscription<Message> {
45//!         Subscription::batch([
46//!             status_bar::subscription(),
47//!             status_bar::animation(&self.status),
48//!         ])
49//!         .map(Message::Status)
50//!     }
51//! }
52//! ```
53
54use std::rc::Rc;
55
56use iced::futures::channel::mpsc;
57use iced::futures::{Stream, StreamExt, stream};
58use iced::time::Instant;
59use iced::widget::text::LineHeight;
60use iced::widget::{self, column, container, responsive, row, space};
61use iced::{Alignment, Background, Element, Length, Padding, Subscription, Theme, window};
62
63use crate::feedback::badge::{self};
64use crate::feedback::progress::{self, progress as progress_bar};
65use crate::feedback::spinner::{self, advance, spinner as spinning};
66use crate::icon::{Glyph, opacity, themed};
67use crate::overlay::tooltip::{Position, tooltip};
68use crate::primitives::button::{self, Variant};
69use crate::theme::{Tokens, space as gap, text_size};
70
71/// How many updates a producer can send before it has to wait.
72pub const CHANNEL_CAPACITY: usize = 32;
73/// The most updates delivered in one [`Event::Received`].
74pub const BATCH_SIZE: usize = 16;
75/// The id [`subscription`] runs under, so iced keeps one stream alive.
76pub const SUBSCRIPTION_ID: &str = "iced-cube-status-bar";
77/// Below this width the sections wrap onto several lines.
78pub const COMPACT_WIDTH: f32 = 480.0;
79/// Height of every item's line.
80pub const LINE: f32 = 16.0;
81/// Width of a progress item's bar.
82pub const PROGRESS_WIDTH: f32 = 80.0;
83
84const ICON_SIZE: f32 = 14.0;
85
86/// Where an item sits along the bar.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
88pub enum Section {
89    #[default]
90    Start,
91    Centre,
92    End,
93}
94
95impl Section {
96    pub const ALL: [Section; 3] = [Section::Start, Section::Centre, Section::End];
97}
98
99/// What an item shows besides its label and icon.
100#[derive(Debug, Clone, Copy, PartialEq)]
101pub enum Content {
102    /// Muted text.
103    Text,
104    /// A small ghost button that sends [`Event::Press`].
105    Action,
106    /// A spinner before the label, animated by [`animation`].
107    Spinner,
108    /// A short progress bar after the label, from 0.0 to 1.0.
109    Progress(f32),
110    /// A badge holding the label.
111    Badge(badge::Variant),
112}
113
114/// One item of the bar.
115#[derive(Debug, Clone, PartialEq)]
116pub struct Item<Id> {
117    pub id: Id,
118    pub section: Section,
119    pub label: String,
120    pub icon: Option<Glyph>,
121    pub tooltip: Option<String>,
122    pub content: Content,
123}
124
125fn new_item<Id>(id: Id, label: impl Into<String>, content: Content) -> Item<Id> {
126    Item {
127        id,
128        section: Section::default(),
129        label: label.into(),
130        icon: None,
131        tooltip: None,
132        content,
133    }
134}
135
136/// Muted text, such as a cursor position. Add `.icon(...)` for an icon.
137pub fn text<Id>(id: Id, label: impl Into<String>) -> Item<Id> {
138    new_item(id, label, Content::Text)
139}
140
141/// A clickable item, drawn as a small ghost button.
142pub fn action<Id>(id: Id, label: impl Into<String>) -> Item<Id> {
143    new_item(id, label, Content::Action)
144}
145
146/// A spinner with a label, for work in progress.
147pub fn spinner<Id>(id: Id, label: impl Into<String>) -> Item<Id> {
148    new_item(id, label, Content::Spinner)
149}
150
151/// A short progress bar, clamped to 0.0..=1.0. Add `.label(...)` to
152/// show text before it.
153pub fn progress<Id>(id: Id, value: f32) -> Item<Id> {
154    new_item(id, "", Content::Progress(progress::clamp(value)))
155}
156
157/// A badge, such as an error count.
158pub fn badge<Id>(id: Id, label: impl Into<String>, variant: badge::Variant) -> Item<Id> {
159    new_item(id, label, Content::Badge(variant))
160}
161
162impl<Id> Item<Id> {
163    pub fn section(mut self, section: Section) -> Self {
164        self.section = section;
165        self
166    }
167
168    pub fn label(mut self, label: impl Into<String>) -> Self {
169        self.label = label.into();
170        self
171    }
172
173    /// Adds a Lucide icon before the label.
174    pub fn icon(mut self, glyph: Glyph) -> Self {
175        self.icon = Some(glyph);
176        self
177    }
178
179    /// Shown above the item while the pointer rests on it.
180    pub fn tooltip(mut self, tooltip: impl Into<String>) -> Self {
181        self.tooltip = Some(tooltip.into());
182        self
183    }
184}
185
186/// What a producer sends through the channel.
187#[derive(Debug, Clone, PartialEq)]
188pub enum Update<Id> {
189    /// Adds an item, or replaces the item with the same id where it stands.
190    Set(Item<Id>),
191    Remove(Id),
192    Clear,
193}
194
195/// The sending half of the status channel. Clone it for each producer.
196pub type Sender<Id> = mpsc::Sender<Update<Id>>;
197
198/// Changes to a status bar.
199#[derive(Debug, Clone)]
200pub enum Event<Id> {
201    /// The channel is open. Keep the sender and hand clones to producers.
202    Ready(Sender<Id>),
203    /// Updates from the channel, oldest first.
204    Received(Vec<Update<Id>>),
205    /// A window frame, which moves the spinners on.
206    Frame(Instant),
207    /// A clickable item was pressed.
208    Press(Id),
209}
210
211/// What the app may need to act on after an [`Event`].
212#[derive(Debug, Clone)]
213pub enum Output<Id> {
214    Ready(Sender<Id>),
215    /// A clickable item was pressed.
216    Pressed(Id),
217}
218
219/// The items and the spinners' phase.
220#[derive(Debug, Clone, PartialEq)]
221pub struct State<Id> {
222    items: Vec<Item<Id>>,
223    phase: f32,
224    last_frame: Option<Instant>,
225}
226
227impl<Id> Default for State<Id> {
228    fn default() -> Self {
229        Self {
230            items: Vec::new(),
231            phase: 0.0,
232            last_frame: None,
233        }
234    }
235}
236
237impl<Id: PartialEq> State<Id> {
238    /// An empty bar.
239    pub fn new() -> Self {
240        Self::default()
241    }
242
243    /// A bar starting with `items`, as if each had been set in turn.
244    pub fn with_items(mut self, items: impl IntoIterator<Item = Item<Id>>) -> Self {
245        for item in items {
246            self.apply(Update::Set(item));
247        }
248        self
249    }
250
251    /// Every item, in the order it was first set.
252    pub fn items(&self) -> &[Item<Id>] {
253        &self.items
254    }
255
256    pub fn item(&self, id: &Id) -> Option<&Item<Id>> {
257        self.items.iter().find(|item| item.id == *id)
258    }
259
260    /// The items in one section, in order.
261    pub fn section(&self, section: Section) -> impl Iterator<Item = &Item<Id>> {
262        self.items
263            .iter()
264            .filter(move |item| item.section == section)
265    }
266
267    pub fn is_empty(&self) -> bool {
268        self.items.is_empty()
269    }
270
271    /// The spinners' phase, in turns.
272    pub fn phase(&self) -> f32 {
273        self.phase
274    }
275
276    /// Whether any spinner is showing, so frames are needed.
277    pub fn needs_frames(&self) -> bool {
278        self.items
279            .iter()
280            .any(|item| item.content == Content::Spinner)
281    }
282
283    /// Applies one update, as the channel does.
284    pub fn apply(&mut self, update: Update<Id>) {
285        match update {
286            Update::Set(item) => match self.items.iter_mut().find(|old| old.id == item.id) {
287                Some(old) => *old = item,
288                None => self.items.push(item),
289            },
290            Update::Remove(id) => self.items.retain(|item| item.id != id),
291            Update::Clear => self.items.clear(),
292        }
293        if !self.needs_frames() {
294            self.last_frame = None;
295        }
296    }
297
298    /// Applies an event and returns what the app may need to act on.
299    pub fn update(&mut self, event: Event<Id>) -> Option<Output<Id>> {
300        match event {
301            Event::Ready(sender) => return Some(Output::Ready(sender)),
302            Event::Received(batch) => {
303                for update in batch {
304                    self.apply(update);
305                }
306            }
307            Event::Frame(now) => {
308                if let Some(last) = self.last_frame {
309                    self.phase = advance(self.phase, now.saturating_duration_since(last));
310                }
311                self.last_frame = Some(now);
312            }
313            Event::Press(id) => {
314                let pressable = self
315                    .item(&id)
316                    .is_some_and(|item| item.content == Content::Action);
317                return pressable.then_some(Output::Pressed(id));
318            }
319        }
320        None
321    }
322}
323
324/// Owns the status channel. Emits [`Event::Ready`] first, then batches.
325/// It runs under [`SUBSCRIPTION_ID`], so iced keeps the same stream
326/// however often the app asks for it.
327pub fn subscription<Id: Send + 'static>() -> Subscription<Event<Id>> {
328    Subscription::run_with(SUBSCRIPTION_ID, |_| stream::<Id>())
329}
330
331/// The stream behind [`subscription`], for driving it without a runtime.
332pub fn stream<Id: Send + 'static>() -> impl Stream<Item = Event<Id>> {
333    let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
334    stream::once(async move { Event::Ready(sender) }).chain(batches(receiver).map(Event::Received))
335}
336
337/// Groups whatever is already waiting in the channel into one batch.
338pub fn batches<T>(receiver: impl Stream<Item = T>) -> impl Stream<Item = Vec<T>> {
339    receiver.ready_chunks(BATCH_SIZE)
340}
341
342/// Window frames while a spinner is showing, and nothing otherwise.
343pub fn animation<Id: PartialEq + 'static>(state: &State<Id>) -> Subscription<Event<Id>> {
344    if !state.needs_frames() {
345        return Subscription::none();
346    }
347    window::frames().map(Event::Frame)
348}
349
350/// The bar: the page background with muted text, below a line along its
351/// top edge. Progress tracks and badges read against it in both themes.
352pub fn bar_style(tokens: &Tokens) -> container::Style {
353    container::Style {
354        background: Some(Background::Color(tokens.background)),
355        text_color: Some(tokens.muted_foreground),
356        ..container::Style::default()
357    }
358}
359
360/// The colour of text and icons in the bar.
361pub fn text_colour(tokens: &Tokens) -> iced::Color {
362    tokens.muted_foreground
363}
364
365type OnEvent<'a, Id, Message> = Rc<dyn Fn(Event<Id>) -> Message + 'a>;
366
367/// A status bar builder. Convert it into an [`Element`] to render.
368pub struct StatusBar<'a, Id, Message> {
369    state: &'a State<Id>,
370    compact_below: f32,
371    on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
372}
373
374impl<Id: std::fmt::Debug, Message> std::fmt::Debug for StatusBar<'_, Id, Message> {
375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376        f.debug_struct("StatusBar")
377            .field("state", self.state)
378            .field("compact_below", &self.compact_below)
379            .finish_non_exhaustive()
380    }
381}
382
383/// Renders the items of `state`. Without [`on_event`](StatusBar::on_event)
384/// clickable items render disabled.
385pub fn status_bar<'a, Id, Message>(state: &'a State<Id>) -> StatusBar<'a, Id, Message> {
386    StatusBar {
387        state,
388        compact_below: COMPACT_WIDTH,
389        on_event: None,
390    }
391}
392
393impl<'a, Id, Message> StatusBar<'a, Id, Message> {
394    /// Below this width, the items flow onto as many lines as they need
395    /// instead of splitting into three sections. Defaults to
396    /// [`COMPACT_WIDTH`].
397    pub fn compact_below(mut self, width: f32) -> Self {
398        self.compact_below = width;
399        self
400    }
401
402    pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
403        self.on_event = Some(Box::new(on_event));
404        self
405    }
406}
407
408impl<'a, Id, Message> From<StatusBar<'a, Id, Message>> for Element<'a, Message>
409where
410    Id: Clone + PartialEq + 'a,
411    Message: Clone + 'a,
412{
413    fn from(bar: StatusBar<'a, Id, Message>) -> Self {
414        let StatusBar {
415            state,
416            compact_below,
417            on_event,
418        } = bar;
419        let on_event: Option<OnEvent<'a, Id, Message>> = on_event.map(Rc::from);
420
421        let content = responsive(move |size| {
422            let items = |section: Section| -> Vec<Element<'a, Message>> {
423                state
424                    .section(section)
425                    .map(|item| view_item(item, state.phase, on_event.as_ref()))
426                    .collect()
427            };
428            if size.width < compact_below {
429                let all = Section::ALL.into_iter().flat_map(items);
430                return row(all)
431                    .spacing(gap::MD)
432                    .align_y(Alignment::Center)
433                    .wrap()
434                    .vertical_spacing(gap::XS)
435                    .into();
436            }
437            let section = |section| {
438                row(items(section))
439                    .spacing(gap::MD)
440                    .align_y(Alignment::Center)
441            };
442            row![
443                section(Section::Start),
444                space::horizontal(),
445                section(Section::Centre),
446                space::horizontal(),
447                section(Section::End),
448            ]
449            .spacing(gap::MD)
450            .align_y(Alignment::Center)
451            .into()
452        })
453        .height(Length::Shrink);
454
455        column![
456            crate::application::edge(),
457            container(content)
458                .padding(Padding::from([gap::XS, gap::SM]))
459                .width(Length::Fill)
460                .style(|theme| bar_style(&Tokens::of(theme))),
461        ]
462        .width(Length::Fill)
463        .into()
464    }
465}
466
467fn muted_text<'a>(label: String) -> widget::Text<'a, Theme> {
468    widget::text(label)
469        .size(text_size::XS)
470        .line_height(LineHeight::Absolute(LINE.into()))
471        .wrapping(widget::text::Wrapping::None)
472}
473
474fn glyph<'a, Message: 'a>(glyph: Glyph, enabled: bool) -> Element<'a, Message> {
475    themed(glyph, ICON_SIZE, opacity(enabled), |theme| {
476        text_colour(&Tokens::of(theme))
477    })
478    .into()
479}
480
481fn view_item<'a, Id, Message>(
482    item: &Item<Id>,
483    phase: f32,
484    on_event: Option<&OnEvent<'a, Id, Message>>,
485) -> Element<'a, Message>
486where
487    Id: Clone + 'a,
488    Message: Clone + 'a,
489{
490    let label = || -> Option<Element<'a, Message>> {
491        (!item.label.is_empty()).then(|| {
492            muted_text(item.label.clone())
493                .style(|theme| widget::text::Style {
494                    color: Some(text_colour(&Tokens::of(theme))),
495                })
496                .into()
497        })
498    };
499    let icon = item.icon.map(|icon| glyph(icon, true));
500
501    let element: Element<'a, Message> = match item.content {
502        Content::Text => labelled(icon.into_iter().chain(label())),
503        Content::Spinner => {
504            labelled(std::iter::once(spinning(phase).size(spinner::Size::Sm).into()).chain(label()))
505        }
506        Content::Progress(value) => labelled(
507            icon.into_iter().chain(label()).chain(std::iter::once(
508                progress_bar(value)
509                    .size(progress::Size::Sm)
510                    .width(PROGRESS_WIDTH)
511                    .into(),
512            )),
513        ),
514        Content::Badge(variant) => {
515            let badge = badge::badge(item.label.clone()).variant(variant);
516            match item.icon {
517                Some(icon) => badge.icon(icon).into(),
518                None => badge.into(),
519            }
520        }
521        Content::Action => action_button(item, on_event),
522    };
523    match &item.tooltip {
524        Some(label) => tooltip(element, label.clone())
525            .position(Position::Top)
526            .into(),
527        None => element,
528    }
529}
530
531fn labelled<'a, Message: 'a>(
532    parts: impl Iterator<Item = Element<'a, Message>>,
533) -> Element<'a, Message> {
534    row(parts)
535        .spacing(gap::XS + 2.0)
536        .align_y(Alignment::Center)
537        .into()
538}
539
540fn action_button<'a, Id, Message>(
541    item: &Item<Id>,
542    on_event: Option<&OnEvent<'a, Id, Message>>,
543) -> Element<'a, Message>
544where
545    Id: Clone + 'a,
546    Message: Clone + 'a,
547{
548    let enabled = on_event.is_some();
549    let mut content = row![].spacing(gap::XS + 2.0).align_y(Alignment::Center);
550    if let Some(icon) = item.icon {
551        content = content.push(themed(icon, ICON_SIZE, opacity(enabled), |theme| {
552            Tokens::of(theme).foreground
553        }));
554    }
555    content = content.push(muted_text(item.label.clone()));
556    widget::button(content)
557        .padding(Padding::from([2.0, 6.0]))
558        .style(|theme, status| button::style(&Tokens::of(theme), Variant::Ghost, status))
559        .on_press_maybe(on_event.map(|on_event| on_event(Event::Press(item.id.clone()))))
560        .into()
561}
562
563#[cfg(test)]
564mod tests {
565    use super::*;
566    use crate::theme::{dark, light};
567    use iced::futures::SinkExt;
568    use iced::futures::executor::block_on;
569
570    fn sample() -> State<&'static str> {
571        State::new().with_items([
572            text("branch", "main").icon(crate::lucide!(GitBranch)),
573            spinner("sync", "Syncing"),
574            progress("build", 0.4)
575                .label("Building")
576                .section(Section::Centre),
577            action("problems", "2 problems").section(Section::End),
578            badge("mode", "Insert", badge::Variant::Secondary).section(Section::End),
579        ])
580    }
581
582    #[test]
583    fn builders_set_every_field() {
584        let item = text("cursor", "Ln 1, Col 1")
585            .icon(crate::lucide!(MousePointer))
586            .section(Section::End)
587            .tooltip("Go to line");
588        assert_eq!(item.id, "cursor");
589        assert_eq!(item.label, "Ln 1, Col 1");
590        assert_eq!(item.icon, Some(crate::lucide!(MousePointer)));
591        assert_eq!(item.section, Section::End);
592        assert_eq!(item.tooltip.as_deref(), Some("Go to line"));
593        assert_eq!(item.content, Content::Text);
594
595        assert_eq!(action(1, "Run").content, Content::Action);
596        assert_eq!(spinner(1, "Busy").content, Content::Spinner);
597        assert_eq!(progress(1, 1.5).content, Content::Progress(1.0));
598        assert_eq!(progress(1, f32::NAN).content, Content::Progress(0.0));
599        assert!(progress(1, 0.5).label.is_empty());
600        assert_eq!(
601            badge(1, "3", badge::Variant::Destructive).content,
602            Content::Badge(badge::Variant::Destructive)
603        );
604        assert_eq!(Section::default(), Section::Start);
605    }
606
607    #[test]
608    fn items_keep_their_order_within_sections() {
609        let state = sample();
610        let start: Vec<_> = state.section(Section::Start).map(|item| item.id).collect();
611        let end: Vec<_> = state.section(Section::End).map(|item| item.id).collect();
612        assert_eq!(start, ["branch", "sync"]);
613        assert_eq!(end, ["problems", "mode"]);
614        assert_eq!(state.items().len(), 5);
615        assert!(!state.is_empty());
616    }
617
618    #[test]
619    fn set_replaces_in_place_and_remove_and_clear_delete() {
620        let mut state = sample();
621        state.apply(Update::Set(text("sync", "Synced")));
622        assert_eq!(state.items()[1].label, "Synced");
623        assert_eq!(state.items()[1].content, Content::Text);
624        assert_eq!(state.items().len(), 5);
625
626        state.apply(Update::Remove("build"));
627        assert!(state.item(&"build").is_none());
628        state.apply(Update::Remove("missing"));
629        assert_eq!(state.items().len(), 4);
630
631        state.apply(Update::Clear);
632        assert!(state.is_empty());
633    }
634
635    #[test]
636    fn a_batch_applies_in_order() {
637        let mut state: State<&str> = State::new();
638        let output = state.update(Event::Received(vec![
639            Update::Set(text("a", "one")),
640            Update::Set(text("b", "two")),
641            Update::Set(text("a", "three")),
642            Update::Remove("b"),
643        ]));
644        assert!(output.is_none());
645        assert_eq!(state.items().len(), 1);
646        assert_eq!(state.items()[0].label, "three");
647        assert!(state.update(Event::Received(Vec::new())).is_none());
648    }
649
650    #[test]
651    fn frames_move_the_spinners_only_while_one_shows() {
652        let mut state = sample();
653        assert!(state.needs_frames());
654        let start = Instant::now();
655        let _ = state.update(Event::Frame(start));
656        assert_eq!(state.phase(), 0.0, "the first frame only starts the clock");
657        let _ = state.update(Event::Frame(start + spinner::PERIOD / 4));
658        assert!((state.phase() - 0.25).abs() < 1e-3);
659
660        state.apply(Update::Remove("sync"));
661        assert!(!state.needs_frames());
662        assert!(
663            state.last_frame.is_none(),
664            "the clock stops with the spinner"
665        );
666    }
667
668    #[test]
669    fn only_clickable_items_report_presses() {
670        let mut state = sample();
671        assert!(matches!(
672            state.update(Event::Press("problems")),
673            Some(Output::Pressed("problems"))
674        ));
675        assert!(state.update(Event::Press("branch")).is_none());
676        assert!(state.update(Event::Press("missing")).is_none());
677    }
678
679    #[test]
680    fn ready_hands_the_sender_back() {
681        let (sender, _receiver) = mpsc::channel(1);
682        let output = State::<u8>::new().update(Event::Ready(sender));
683        assert!(matches!(output, Some(Output::Ready(_))));
684    }
685
686    #[test]
687    fn stream_emits_ready_first_then_batches() {
688        let mut events = Box::pin(stream::<u8>());
689        let Some(Event::Ready(mut sender)) = block_on(events.next()) else {
690            panic!("first event must be Ready");
691        };
692        block_on(async {
693            for n in 0..3 {
694                sender.send(Update::Set(text(n, format!("{n}")))).await.ok();
695            }
696        });
697        let Some(Event::Received(batch)) = block_on(events.next()) else {
698            panic!("expected a batch");
699        };
700        assert_eq!(batch.len(), 3);
701    }
702
703    #[test]
704    fn batches_split_bursts_at_batch_size() {
705        let items = stream::iter(0..BATCH_SIZE + 3);
706        let sizes: Vec<_> = block_on(batches(items).map(|batch| batch.len()).collect());
707        assert_eq!(sizes, [BATCH_SIZE, 3]);
708    }
709
710    #[test]
711    fn builder_defaults_and_overrides() {
712        let state = sample();
713        let bar: StatusBar<'_, &str, ()> = status_bar(&state);
714        assert_eq!(bar.compact_below, COMPACT_WIDTH);
715        assert!(bar.on_event.is_none());
716        let bar: StatusBar<'_, &str, ()> = status_bar(&state).compact_below(0.0).on_event(|_| ());
717        assert_eq!(bar.compact_below, 0.0);
718        assert!(bar.on_event.is_some());
719    }
720
721    #[test]
722    fn the_bar_has_muted_text_in_both_themes() {
723        for theme in [light(), dark()] {
724            let tokens = Tokens::of(&theme);
725            let style = bar_style(&tokens);
726            assert_eq!(style.background, Some(Background::Color(tokens.background)));
727            assert_eq!(style.text_color, Some(tokens.muted_foreground));
728            assert_eq!(text_colour(&tokens), tokens.muted_foreground);
729        }
730    }
731}