Eventloop stops after closing iced window

Hi,
I want to create a tray icon app with iced settings window.

I’m using tao, tray-icon and iced.
At first there is an eventloop by tao. There the tray menu events are handled. This works very well.

Then I want to open an iced window. The window opens and I can close it. But after then the eventloop stops working.

I assume iced somehow stops the loop.

Is there a way to continue the loop after the window was closed?

Example:

use iced::{widget::text, Element};
use tao::event_loop::{ControlFlow, EventLoop};
use tokio::runtime::Runtime;
use tray_icon::{
    menu::{Menu, MenuEvent, MenuItem},
    TrayIconBuilder, TrayIconEvent,
};

fn main() {
    let runtime = Runtime::new().expect("Tokio Runtime konnte nicht erstellt werden");
    let event_loop = EventLoop::new();

    let item_update = MenuItem::new("update", true, None);
    let item_config = MenuItem::new("settings", true, None);
    let item_exit = MenuItem::new("quit", true, None);
    let menu = Menu::new();
    let _ = menu.append_items(&[&item_update, &item_config, &item_exit]);
    let mut tray_icon = None;

    let mut iced_window = None;

    let menu_channel = MenuEvent::receiver();
    let tray_channel = TrayIconEvent::receiver();

    event_loop.run(move |event, _, control_flow| {
        println!("event start");
        *control_flow = ControlFlow::WaitUntil(
            std::time::Instant::now() + std::time::Duration::from_millis(16),
        );

        if let tao::event::Event::NewEvents(tao::event::StartCause::Init) = event {
            println!("Building tray icon");
            tray_icon = Some(
                TrayIconBuilder::new()
                    .with_menu(Box::new(menu.clone()))
                    .with_tooltip("tao - awesome windowing lib")
                    .build()
                    .unwrap(),
            );
        }

        if let Ok(event) = menu_channel.try_recv() {
            println!("menu event: {event:?}");
            if event.id == item_exit.id() {
                println!("Exiting...");
                tray_icon.take();
                *control_flow = ControlFlow::Exit;
            } else if event.id == item_config.id() {
                if iced_window.is_none() {
                    println!("Starting window");
                    iced_window = Some(
                        iced::application(
                            SettingsWindow::title,
                            SettingsWindow::update,
                            SettingsWindow::view,
                        )
                        .run()
                        .expect("Error running window"),
                    );
                    println!("Window closed");
                }
            } else if event.id == item_update.id() {
                println!("Updating...");
                runtime.spawn(async {
                    let _ = perform_web_request().await;
                    println!("Web-Request done");
                });
            }
        }

        if let Ok(event) = tray_channel.try_recv() {
            println!("tray event: {event:?}");
        }
        //println!("event loop exit");
    });
}

async fn perform_web_request() -> Result<String, ()> {
    Ok("test".into())
}

#[derive(Debug, Clone, Copy)]
enum Message {}

#[derive(Default)]
struct SettingsWindow;

impl SettingsWindow {
    fn title(&self) -> String {
        "Einstellungen".to_string()
    }

    fn update(&mut self, message: Message) {}

    fn view(&self) -> Element<Message> {
        text("Hier können die Einstellungen vorgenommen werden.").into()
    }
}

cargo.toml:

[package]
name = "iced-tray-icon"
version = "0.1.0"
edition = "2021"

[dependencies]
tao = "0.30.8"
iced = "0.13.1"
tray-icon = "0.19.0"
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }

I suggest trying to run iced in daemon mode with iced::daemon rather than iced::application

That snippet looks super interesting. It would be great to have an example available for the community as I think this setup is heavily sought after

Sounds promising. But I’m afraid that will cause high memory usage. At the moment my app uses about 2MB memory. When a window is opened it uses much more. I’ll try that tomorrow.

At the moment I did not figure out how to integrate the tray icon into daemon mechanism. The tray events somehow must trigger the iced update() function.

But I can tell an empty app using this way without any window opened uses 33 MB of memory which is really bad.

I haven’t explored tray icon apps so I’m afraid I can’t help, but for the record I’d say “really bad” is subjective. 33MB of memory seems absolutely tiny to me in 2024 unless I was writing embedded software… which is really not iced’s focus as a cross-platform GUI library.

How much RAM do yo expect a graphical application to use?

Let’s do some quick math:

  • Each pixel p in your app has 4 color components (RGBA) → c = 4 * p
  • Each component c is a single-precision float (f32) of 4 bytes → b = 4 * c

Given a resolution of p pixels, the total amount of memory (in bytes) needed to store a surface representing it is b = 4 * 4 * p.

For instance, if we have a 1080p resolution (p = 1920 * 1080 pixels), then:

b = 4 * 4 * 1920 * 1080 = 33177600 bytes = 33.1776 MB

A daemon opens a “boot window” to initialize renderers, so the memory is allocated nevertheless.