Skip to content

Instantly share code, notes, and snippets.

@airstrike
Created February 7, 2025 01:23
Show Gist options
  • Save airstrike/937c4cc61532389cdb8aee354c99d04b to your computer and use it in GitHub Desktop.
Save airstrike/937c4cc61532389cdb8aee354c99d04b to your computer and use it in GitHub Desktop.
iced::daemon with only two windows
use std::collections::BTreeMap;
use iced::advanced::graphics::image::image_rs::ImageFormat;
use iced::{window, Size, Subscription, Task, Element};
use crate::theme::constants::WINDOW_ICON;
use crate::theme::Theme;
use crate::{app, assistant};
#[derive(Debug)]
pub enum Message {
OpenApp(window::Id),
OpenAssistant(window::Id),
WindowClosed(window::Id),
App(app::Message),
Assistant(assistant::Message),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WindowType {
App,
Assistant,
}
#[derive(Default)]
pub struct Windows {
windows: BTreeMap<window::Id, WindowType>,
app: app::App,
theme: Theme,
}
impl Windows {
pub fn new() -> (Self, Task<Message>) {
let window_type = WindowType::App;
let (_, open) = window::open(window_type.window_settings());
(Self::default(), open.map(Message::OpenApp))
}
pub fn theme(&self, _: window::Id) -> Theme {
self.theme.clone()
}
pub fn title(&self, id: window::Id) -> String {
match self.windows.get(&id) {
Some(WindowType::App) => self.app.title(),
Some(WindowType::Assistant) => self.app.assistant.title(),
None => "".to_string(), // before any windows are opened
}
}
pub fn view(&self, id: window::Id) -> Element<Message> {
match self.windows.get(&id) {
Some(WindowType::App) => self.app.view().map(Message::App),
Some(WindowType::Assistant) => {
self.app.assistant.view(&self.theme).map(Message::Assistant)
}
_ => "".into(), // before any windows are opened
}
}
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::OpenApp(id) => {
if !self.windows.values().any(|&w| w == WindowType::App) {
self.windows.insert(id, WindowType::App);
}
Task::none()
}
Message::OpenAssistant(id) => {
if !self.windows.values().any(|&w| w == WindowType::Assistant) {
self.windows.insert(id, WindowType::Assistant);
}
Task::none()
}
Message::WindowClosed(id) => {
let window_type = self.windows.remove(&id);
if self.windows.is_empty() || Some(WindowType::App) == window_type {
iced::exit()
} else {
Task::none()
}
}
Message::App(message) => {
let action = self.app.update(message);
if let Some(instruction) = action.instruction {
let instruction_task = self.perform(instruction);
Task::batch(vec![action.task.map(Message::App), instruction_task])
} else {
action.task.map(Message::App)
}
}
Message::Assistant(message) => {
let action = self.app.assistant.update(message);
if let Some(instruction) = action.instruction {
let instruction_task = self.app.perform(instruction).map(Message::App);
Task::batch(vec![action.task.map(Message::Assistant), instruction_task])
} else {
action.task.map(Message::Assistant)
}
}
}
}
// note this is "app::Instruction" but we could change it to some "window::Instruction"
// and .map() the `self.perform(instruction)` in the `Message::App` match arm i.e.
// `self.perform(instruction).map(Message::App)` but that's not necessary right now
// and I don't expect the assistant to want to perform any window-specific instructions
// (famous last words)
pub fn perform(&mut self, instruction: app::Instruction) -> Task<Message> {
match instruction {
app::Instruction::FocusAssistant => {
if let Some((id, _)) = self
.windows
.iter()
.find(|(_, &w)| w == WindowType::Assistant)
{
window::gain_focus(*id)
} else {
let window_type = WindowType::Assistant;
let (_, open) = window::open(window_type.window_settings());
open.map(Message::OpenAssistant)
}
}
}
}
pub fn subscription(&self) -> Subscription<Message> {
Subscription::batch(vec![
self.app.subscription().map(Message::App),
window::close_events().map(Message::WindowClosed),
])
}
}
impl WindowType {
/// The [`iced::window::Settings`] for each type of window.
pub fn window_settings(&self) -> window::Settings {
match self {
WindowType::App => {
let icon =
window::icon::from_file_data(WINDOW_ICON, Some(ImageFormat::Png)).unwrap();
window::Settings {
min_size: Some(Size::new(800.0, 600.0)),
size: Size::new(1400.0, 800.0),
icon: Some(icon),
position: window::Position::Centered,
..Default::default()
}
}
WindowType::Assistant => window::Settings {
size: (400.0, 300.0).into(),
resizable: true,
..window::Settings::default()
},
}
}
}
@decipher3114
Copy link

The Action type is good until you reach level 3 (3rd level of app state abstraction).
Creating Message and Instruction for each child and update() and perform() for the parent doesn't seem very intuitive to me.

@airstrike
Copy link
Author

airstrike commented Feb 7, 2025

The Action type is good until you reach level 3 (3rd level of app state abstraction). Creating Message and Instruction for each child and update() and perform() for the parent doesn't seem very intuitive to me.

The parent already has an update anyway, and perform is just a convenient way to extract that bit of logic from the body of update so that the code is more manageable. You could write it all inside the update message pattern matching if you strongly prefer it.

The complexity you're talking about will exist regardless of the type of the children's return function. The alternatives are an Action type like:

pub enum Action {
    Instruction1,
    Instruction2,
    Run(Task<Message>),
}

which is basically the same as pub struct Action except you shove the tasks together with the remaining instructions, meaning you can't ever have something that does both a task and an instruction unless you used Vec<Action> and that would be much worse to manage, as you'd have to match on each item of the vec to know whether it's a Task or not in order to return it to the runtime.

The other alternative is to just return Task<Message> every time and produce messages like Task::done(Message::Instruction1). This has the downside of making your update pattern matching even longer and mixing up two concerns: (1) messages generated as a result of a specific user interaction like a button press and (2) instructions from children views to their ancestor layers.

I've actually written the iced_receipts code in all three of those variants and it was pretty clear to me that this proposed Action<Instruction, Message> type is the most ergonomic. It's all about trade offs, and this requires a bit more familiarity with the usual plumbing from iced apps, but I think it's the winning design pattern. Or at least I haven't been shown anything that outperforms it in terms of simplicity and effectiveness.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment