elkowar/eww · error

Failed to obtain toplevel window

Error message

Failed to obtain toplevel window

What it means

For systray items, widget.toplevel() returns None until the widget is realized and mapped into a window; eww expects it and additionally downcasts to gtk::Window. Panicking here means the tray icon widget had no toplevel GTK window at the moment `maintain` ran.

Solutions

  1. Defer the work until the widget is mapped: only call toplevel() inside a connect_map/realize handler, or skip the item if toplevel() is None and retry on the next maintain pass.
  2. Restart/reload the bar so the tray widget is realized before items are processed.
  3. Update eww — hardening this race (items arriving before the window maps) is a known class of systray bug worth reporting upstream.
  4. Replace both expects with logging and early-return so a single bad item cannot kill the daemon.

Example fix

// before
let window = widget
    .toplevel().expect("Failed to obtain toplevel window")
    .downcast::<Window>().expect("Failed to downcast window");
// after
let window = match widget.toplevel().and_then(|t| t.downcast::<Window>().ok()) {
    Some(w) => w,
    None => {
        log::warn!("Systray item has no toplevel window yet; skipping");
        return;
    }
};
Defensive patterns

Strategy: type-guard

Validate before calling

// only proceed once the widget is realized and mapped
if !widget.is_realized() || widget.toplevel().is_none() { return; }

Type guard

fn toplevel_window(widget: &gtk::Widget) -> Option<gtk::Window> {
    widget.toplevel().and_then(|t| t.downcast::<gtk::Window>().ok())
}

Try / catch

// replace expect with early return
let window = match toplevel_window(&widget) {
    Some(w) => w,
    None => { log::warn!("no toplevel window for tray item yet"); return; }
};

Prevention

When it happens

Trigger: maintain() processing a StatusNotifier item while the containing widget is not yet realized/mapped — e.g. the tray is created before the bar window exists, the bar was re-created during a reload, or the item arrived during window teardown — so toplevel() yields None; a non-Window toplevel also fails the downcast.

Common situations: Eww bars reloaded or re-created while a systray item connects, tray widgets in popup/unmapped containers, and race conditions on startup where items register faster than the bar window maps.

Related errors


AI-assisted analysis of elkowar/eww@48f5aa8b37 (2026-09-08). Data as JSON: /api/errors/373316b0f6c0f2cc. Report an issue: GitHub.

Appendix: source

Thrown at crates/eww/src/widgets/systray.rs:199

        // TODO this is a lot of code duplication unfortunately, i'm not really sure how to
        // refactor without making the borrow checker angry

        // set status
        match item.status().await? {
            notifier_host::Status::Passive => widget.hide(),
            notifier_host::Status::Active | notifier_host::Status::NeedsAttention => widget.show(),
        }

        // set title
        widget.set_tooltip_text(Some(&item.sni.title().await?));

        // set icon
        let scale = icon.scale_factor();
        load_icon_for_item(&icon, &item, *icon_size.borrow_and_update(), scale).await;

        let item = Rc::new(item);
        let window =
            widget.toplevel().expect("Failed to obtain toplevel window").downcast::<Window>().expect("Failed to downcast window");
        widget.add_events(gdk::EventMask::BUTTON_PRESS_MASK);
        widget.connect_button_press_event(glib::clone!(@strong item => move |_, evt| {
            let (x, y) = (evt.root().0 as i32 + window.x(), evt.root().1 as i32 + window.y());
            let item_is_menu = run_async_task(async { item.sni.item_is_menu().await });
            let have_item_is_menu = item_is_menu.is_ok();
            let item_is_menu = item_is_menu.unwrap_or(false);
            log::debug!(
                "mouse click button={}, x={}, y={}, have_item_is_menu={}, item_is_menu={}",
                evt.button(),
                x,
                y,
                have_item_is_menu,
                item_is_menu
            );

            let result = match (evt.button(), item_is_menu) {
                (gdk::BUTTON_PRIMARY, false) => {
                    let result = run_async_task(async { item.sni.activate(x, y).await });

View on GitHub (pinned to 48f5aa8b37)