Created
February 7, 2025 01:23
-
-
Save airstrike/937c4cc61532389cdb8aee354c99d04b to your computer and use it in GitHub Desktop.
iced::daemon with only two windows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
}, | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The parent already has an
update
anyway, andperform
is just a convenient way to extract that bit of logic from the body ofupdate
so that the code is more manageable. You could write it all inside theupdate
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: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 usedVec<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 aTask
or not in order to return it to the runtime.The other alternative is to just return
Task<Message>
every time and produce messages likeTask::done(Message::Instruction1)
. This has the downside of making yourupdate
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 proposedAction<Instruction, Message>
type is the most ergonomic. It's all about trade offs, and this requires a bit more familiarity with the usual plumbing fromiced
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.