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"] }
1 Like

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

1 Like

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.

1 Like

I’ve been toying with a tray_icon + iced combination for a while, and have something that works well at this stage: GitHub - FSund/ha-iracing-monitor: iRacing monitor for Home Assistant

I’m having the same issues as @ponchofiesta though, with very high memory usage (200+ Mb on both Windows and Linux), even when no windows are showing. This seems excessive for an application that most of the time only has a tray icon (the frontend is only used for configuration).

I’ve tried the same approach with starting the daemon or app when the tray icon is clicked, which works fine the first time, but I get a winit RecreationAttempt error the second time I try to open the GUI.

I’ve been looking at the integration example to figure out an easy way to manage the event loop myself, but the example goes way over my head.

Has anyone else figured out a good way to only run the iced frontend when required, but save the RAM otherwise?