iced-cube

Overlays

Toast

Short notifications that stack in a corner and close by themselves.

use iced_cube::overlay::toast::{self, Position, Variant, toast, toasts};
API reference
Variants: Default, success and destructive toasts.Variants: Default, success and destructive toasts.
Static preview
Variants. Default, success and destructive toasts.

Usage

Toasts often come from work running off the UI thread, such as a download or a sync. They reach the app through a bounded channel rather than shared state:

  • toast::subscription() creates the channel and owns the receiver. Its first event is Ready with the sender.
  • The app keeps the sender and gives a clone to each producer.
  • The subscription delivers whatever is waiting in the channel in batches of up to 16, so a burst causes a handful of redraws rather than one per toast.
  • toast::timer only runs while a toast is counting down.

A whole app that raises a toast from a worker thread:

use iced::{Element, Subscription};
use iced_cube::button;
use iced_cube::overlay::toast::{self, toast, toasts};

#[derive(Debug, Clone)]
enum Message {
    Toast(toast::Event),
    Export,
}

#[derive(Default)]
struct App {
    toasts: toast::State,
    sender: Option<toast::Sender>,
}

impl App {
    fn update(&mut self, message: Message) {
        match message {
            Message::Toast(event) => {
                if let Some(toast::Output::Ready(sender)) = self.toasts.update(event) {
                    self.sender = Some(sender);
                }
            }
            Message::Export => {
                let Some(mut sender) = self.sender.clone() else {
                    return;
                };
                std::thread::spawn(move || {
                    // Do the work, then report back. try_send drops the
                    // toast if the channel is full.
                    let _ = sender.try_send(toast("Export finished"));
                });
            }
        }
    }

    fn view(&self) -> Element<'_, Message> {
        toasts(&self.toasts, button("Export").on_press(Message::Export))
            .on_event(Message::Toast)
            .into()
    }

    fn subscription(&self) -> Subscription<Message> {
        Subscription::batch([toast::subscription(), toast::timer(&self.toasts)])
            .map(Message::Toast)
    }
}

fn main() -> iced::Result {
    iced::application(App::default, App::update, App::view)
        .subscription(App::subscription)
        .run()
}

Subscriptions covers sending from async code, waiting for room instead of dropping, and how the timer stays idle.

An Undo button needs to know what to undo. Rather than keeping toast ids and matching them, queue the toast with state.push_with(toast, message) on a toast::State<Message>. When the button is pressed, update returns Output::Payload(message), and you handle it like any other message, as the action example does. If the toast is dismissed or times out, the message is dropped with it. Payloads only go through push_with on the UI thread; toasts from the channel have none.

Undo is easy to miss, so a toast with an action gets more room than the rest. It stays at least ten seconds, it goes ahead of any toasts waiting their turn (taking the place of the newest plain toast if all three slots are full), and Alt+Z presses its button through the keymap. Every countdown pauses while the pointer is over the toasts, so nothing closes as you reach for it.

Time only enters the queue through Tick, so the queue is easy to test: push toasts, call state.tick(now) with any instant, and check what is left. A toast starts counting down when it becomes visible, not when it arrives.

The background job example uses an async stream that waits with futures-timer instead of a thread, so it also runs in the browser.

Examples

Background job

A producer sends toasts through the channel. It waits with futures-timer rather than a thread, so it also runs in the browser.

Background job: A producer sends toasts through the channel. It waits with futures-timer rather than a thread, so it also runs in the browser.Background job: A producer sends toasts through the channel. It waits with futures-timer rather than a thread, so it also runs in the browser.
Static preview

With action

An undo action in the top right corner that hands back the message which reverses the archive. It stays at least ten seconds, pauses under the pointer, and Alt+Z presses it.

With action: An undo action in the top right corner that hands back the message which reverses the archive. It stays at least ten seconds, pauses under the pointer, and Alt+Z presses it.With action: An undo action in the top right corner that hands back the message which reverses the archive. It stays at least ten seconds, pauses under the pointer, and Alt+Z presses it.
Static preview

Keyboard shortcuts

Escape closes the newest toast and Shift+Escape closes them all, through the default keymap.

Keyboard shortcuts: Escape closes the newest toast and Shift+Escape closes them all, through the default keymap.Keyboard shortcuts: Escape closes the newest toast and Shift+Escape closes them all, through the default keymap.
Static preview

API

ItemDescription
toast(title)Creates a toast that closes five seconds after it becomes visible.
.description(text) / .variant(Variant)Adds a second line. Variants are Default, Success and Destructive.
.action(label)Adds a button. Pressing it closes the toast and returns Output::Action. A toast with an action stays at least ACTION_DURATION (10 seconds) and shows ahead of toasts waiting their turn.
.duration(duration) / .persistent()Changes how long the toast stays, or keeps it until it is closed.
toast::StateThe queue. At most three toasts show at once by default; change it with .with_limit(n). State::new() makes a queue without payloads; State::<Message>::default() makes one whose action buttons can hand back your messages.
state.update(Event)Handles Ready, Received, Dismiss, Action, Tick, Pause and Resume. Returns Output::Ready(sender), Output::Payload(value) for a toast queued with push_with, or Output::Action(id, toast) for any other action.
state.push(toast)Queues a toast from the UI thread without the channel, and returns its Id.
state.push_with(toast, payload)Queues a toast whose action button hands back payload, such as the message that undoes what it reports. Dismissing or expiring drops the payload.
state.dismiss(id)Removes a toast, visible or waiting, and returns it.
state.tick(now)Closes toasts whose time is up at now and starts the countdown of newly visible ones. While paused it adds the time since the last tick back to every countdown. Returns the ids that closed.
state.is_paused()Whether the pointer is over the toasts, which stops every countdown until it leaves.
toast.lifetime()How long the toast stays once shown, after the action minimum is applied, or None for a persistent toast.
state.visible() / state.waiting() / state.len()The visible toasts with their ids, how many are queued behind them, and the total.
toast::subscription()Owns the channel. Emits Ready(sender) once, then batches of up to BATCH_SIZE (16) toasts.
toast::SenderThe sending half of the channel, which holds CHANNEL_CAPACITY (32) toasts. Clone it for each producer.
toast::timer(&state)Ticks every TICK (250 ms) while a visible toast is counting down, and is idle otherwise.
toasts(&state, content)Renders the toasts over your content. Toast buttons render disabled until .on_event(f) is set.
.position(Position)TopLeft, TopCenter, TopRight, BottomLeft, BottomCenter or BottomRight. Defaults to BottomRight.
toast::close_button_id(id)The widget id of a toast's close button, for focusing it or clicking it in tests.
toast::default_keymap()The default shortcuts as a Keymap<Action>. Bind, unbind or clear chords to change them.
action.events(&state)The events for a resolved Action: Dismiss for DismissLatest or DismissAll, and Action for ActivateLatest, which presses the button of the newest visible toast that has one. Pass each one to state.update.
state.ids()Every queued toast id, visible first.

Keyboard

Default shortcuts. Every one can be changed or removed with a keymap; see Keyboard shortcuts.

KeysAction
EscapeDismissLatest Closes the newest visible toast.
Shift+EscapeDismissAll Closes every toast, including those waiting their turn.
Alt+ZActivateLatest Presses the action button, such as Undo, of the newest visible toast that has one.
Esc