-
-
Save airstrike/937c4cc61532389cdb8aee354c99d04b to your computer and use it in GitHub Desktop.
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() | |
}, | |
} | |
} | |
} |
The
Action
type is good until you reach level 3 (3rd level of app state abstraction). CreatingMessage
andInstruction
for each child andupdate()
andperform()
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.
The
Action
type is good until you reach level 3 (3rd level of app state abstraction).Creating
Message
andInstruction
for each child andupdate()
andperform()
for the parent doesn't seem very intuitive to me.