Last active
October 27, 2025 15:39
-
-
Save airstrike/984642d803a5ce5dc1f07a0f3bb0e3e1 to your computer and use it in GitHub Desktop.
Multi-window iced example with custom theme palette
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
| // iced.version = "0.13" # iirc | |
| use std::collections::BTreeMap; | |
| use std::time::Duration; | |
| use iced::theme::Palette; | |
| use iced::widget::{button, center, container, horizontal_space, row, text}; | |
| use iced::{time, window, Center, Element, Fill, Subscription, Task, Theme}; | |
| struct App { | |
| windows: BTreeMap<window::Id, Window>, | |
| settings: settings::Settings, | |
| } | |
| /// Simple enum that declares the type of window content to be created. This | |
| /// is helpful because it is `Copy` unlike the `WindowContent` enum. | |
| #[derive(Debug, Clone, Copy, PartialEq)] | |
| pub enum WindowType { | |
| Media, | |
| Settings, | |
| } | |
| impl WindowType { | |
| /// The [`window::Settings`] for each type of window. | |
| pub fn window_settings(&self) -> window::Settings { | |
| match self { | |
| WindowType::Media => window::Settings::default(), | |
| WindowType::Settings => window::Settings { | |
| size: (400.0, 300.0).into(), | |
| resizable: false, | |
| ..window::Settings::default() | |
| }, | |
| } | |
| } | |
| /// Create the appropriate [`Window`] for this type. | |
| fn create(&self) -> Window { | |
| match self { | |
| WindowType::Media => Window::Media(media::Manager::default()), | |
| WindowType::Settings => Window::Settings, | |
| } | |
| } | |
| } | |
| /// A [`Window`] can contain different types of content. It is only created by | |
| /// the App when the `WindowOpened` message is received, which will contain | |
| /// the `window::Id` of the new window and the `WindowType` that was requested. | |
| #[derive(Debug, Clone, PartialEq)] | |
| pub enum Window { | |
| Media(media::Manager), | |
| Settings, | |
| } | |
| #[derive(Debug, Clone)] | |
| pub enum Message { | |
| // Messages handled at the top level | |
| WindowOpened(window::Id, WindowType), | |
| WindowClosed(window::Id), | |
| Tick, | |
| // Messages for the different types of windows | |
| Media(window::Id, media::Message), | |
| Settings(settings::Message), | |
| } | |
| impl App { | |
| fn new() -> (Self, Task<Message>) { | |
| let window_type = WindowType::Media; | |
| let (_, open) = window::open(window_type.window_settings()); | |
| ( | |
| Self { | |
| windows: BTreeMap::new(), | |
| settings: Default::default(), | |
| }, | |
| open.map(move |id| Message::WindowOpened(id, window_type)), | |
| ) | |
| } | |
| fn title(&self, id: window::Id) -> String { | |
| match self.windows.get(&id) { | |
| Some(Window::Media(_)) => "iced • Media Manager".to_string(), | |
| Some(Window::Settings) => "iced • Media Manager: Settings".to_string(), | |
| None => "Unknown Window".to_string(), | |
| } | |
| } | |
| fn update(&mut self, message: Message) -> Task<Message> { | |
| match message { | |
| Message::WindowOpened(id, window_type) => { | |
| let window = window_type.create(); | |
| self.windows.insert(id, window); | |
| } | |
| Message::WindowClosed(id) => { | |
| self.windows.remove(&id); | |
| if self.windows.is_empty() { | |
| return iced::exit(); | |
| } | |
| } | |
| Message::Media(id, message) => { | |
| if let Some(Window::Media(manager)) = self.windows.get_mut(&id) { | |
| if let Some(action) = manager.update(message) { | |
| match action { | |
| media::Action::OpenSettings => { | |
| // Try to find an existing Settings window to focus first, otherwise open a new one | |
| if let Some((id, Window::Settings)) = self | |
| .windows | |
| .iter() | |
| .find(|(_, window)| matches!(window, Window::Settings)) | |
| { | |
| return window::gain_focus(*id); | |
| } else { | |
| let window_type = WindowType::Settings; | |
| let (_, open) = window::open(window_type.window_settings()); | |
| return open | |
| .map(move |id| Message::WindowOpened(id, window_type)); | |
| } | |
| } | |
| media::Action::Run(task) => { | |
| return task.map(move |message| Message::Media(id, message)); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| Message::Settings(message) => { | |
| settings::update(&mut self.settings, message); | |
| } | |
| Message::Tick => { | |
| if let Some((_, Window::Media(manager))) = self.windows.iter_mut().next() { | |
| manager.tick() | |
| } | |
| } | |
| } | |
| Task::none() | |
| } | |
| fn view(&self, id: window::Id) -> Element<Message> { | |
| if let Some(window) = self.windows.get(&id) { | |
| match &window { | |
| Window::Media(manager) => manager | |
| .view() | |
| .map(move |message| Message::Media(id, message)), | |
| Window::Settings => { | |
| settings::view(&self.settings).map(move |message| Message::Settings(message)) | |
| } | |
| } | |
| } else { | |
| horizontal_space().into() | |
| } | |
| } | |
| fn theme(&self, _: window::Id) -> Theme { | |
| rust_theme() | |
| } | |
| fn subscription(&self) -> Subscription<Message> { | |
| Subscription::batch(vec![ | |
| window::close_events().map(Message::WindowClosed), | |
| time::every(Duration::from_millis(200)).map(|_| Message::Tick), | |
| ]) | |
| } | |
| } | |
| mod media { | |
| use super::*; | |
| use iced::widget::{column, slider}; | |
| #[derive(Debug, Clone)] | |
| pub enum Message { | |
| OpenSettings, | |
| TogglePlay(bool), | |
| Seek(f32), | |
| } | |
| pub enum Action { | |
| OpenSettings, | |
| Run(Task<Message>), // placeholder for future tasks you may want to include that are not window related | |
| // and therefore which would generate a media::Message instead of an app::Message | |
| } | |
| #[derive(Debug, Clone, PartialEq)] | |
| pub struct Manager { | |
| playing: bool, | |
| ticks: f32, | |
| max_length: f32, | |
| } | |
| impl Default for Manager { | |
| fn default() -> Self { | |
| Self { | |
| playing: false, | |
| ticks: 0.0, | |
| max_length: 100.0, | |
| } | |
| } | |
| } | |
| impl Manager { | |
| pub fn view(&self) -> Element<'_, Message> { | |
| let title = text("Media Window"); | |
| let open_settings = | |
| button(text("⚙️").shaping(text::Shaping::Advanced)).on_press(Message::OpenSettings); | |
| let media_content = Element::from( | |
| container( | |
| // Just some placeholder info about the current media file | |
| text(format!( | |
| "Length: {:06.2}\n\ | |
| Current Time: {:06.2}", | |
| self.max_length, self.ticks | |
| )) | |
| .center() | |
| .font(iced::Font::MONOSPACE), | |
| ) | |
| .align_x(Center) | |
| .align_y(Center) | |
| .width(800) | |
| .height(500) | |
| .style(container::bordered_box), | |
| ); | |
| let seeker = slider(0.0..=self.max_length, self.ticks, Message::Seek); | |
| let play_pause = button( | |
| text(if self.playing { "⏸" } else { "▶" }) | |
| .size(18) | |
| .line_height(1.0) | |
| .align_x(Center) | |
| .align_y(Center) | |
| .width(15) | |
| .height(20) | |
| .font(iced::Font::MONOSPACE) | |
| .shaping(text::Shaping::Advanced), | |
| ) | |
| .on_press(Message::TogglePlay(!self.playing)); | |
| center( | |
| column![ | |
| row![container(title).center_x(Fill), open_settings].width(Fill), | |
| media_content, | |
| row![seeker, play_pause].spacing(20), | |
| ] | |
| .align_x(Center) | |
| .padding(20) | |
| .spacing(20), | |
| ) | |
| .into() | |
| } | |
| pub fn tick(&mut self) { | |
| if self.playing { | |
| self.ticks += 1.0; | |
| if self.ticks == self.max_length { | |
| self.playing = false; | |
| } | |
| } | |
| } | |
| pub fn update(&mut self, message: Message) -> Option<Action> { | |
| match message { | |
| Message::OpenSettings => return Some(Action::OpenSettings), | |
| Message::TogglePlay(playing) => { | |
| self.playing = playing; | |
| } | |
| Message::Seek(ticks) => { | |
| self.ticks = ticks; | |
| } | |
| } | |
| None | |
| } | |
| } | |
| } | |
| mod settings { | |
| use super::*; | |
| use iced::widget::{column, toggler}; | |
| #[derive(Debug, Clone)] | |
| pub enum Message { | |
| ToggleFrobnicator(bool), | |
| } | |
| pub struct Settings { | |
| // placeholder for some app-level settings that get modified by a deeply | |
| // nested widget (right now just the toggler in the settings window) | |
| frobnicate: bool, | |
| } | |
| impl Default for Settings { | |
| fn default() -> Self { | |
| Self { frobnicate: false } | |
| } | |
| } | |
| pub fn view(settings: &Settings) -> Element<'_, Message> { | |
| let title = text("Settings Window"); | |
| let settings_content = container(text("Placeholder settings content")) | |
| .center(Fill) | |
| .style(container::bordered_box); | |
| center( | |
| column![ | |
| row![ | |
| container(title).center_x(Fill), | |
| toggler(settings.frobnicate).on_toggle(Message::ToggleFrobnicator) | |
| ] | |
| .width(Fill), | |
| settings_content | |
| ] | |
| .align_x(Center) | |
| .padding(20) | |
| .spacing(20), | |
| ) | |
| .into() | |
| } | |
| pub fn update(settings: &mut Settings, message: Message) { | |
| match message { | |
| Message::ToggleFrobnicator(frobnicate) => { | |
| settings.frobnicate = frobnicate; | |
| } | |
| } | |
| } | |
| } | |
| pub fn main() -> iced::Result { | |
| iced::daemon(App::title, App::update, App::view) | |
| .subscription(App::subscription) | |
| .theme(App::theme) | |
| .run_with(App::new) | |
| } | |
| fn rust_theme() -> Theme { | |
| let palette = Palette { | |
| background: iced::color!(0x2A2A3B), | |
| text: iced::color!(0xFFB562), | |
| primary: iced::color!(0xDE5935), | |
| success: iced::color!(0x8CC26F), | |
| danger: iced::color!(0xC14C3D), | |
| }; | |
| Theme::custom(String::from("RUST_THEME"), palette) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment