Skip to content

Instantly share code, notes, and snippets.

@airstrike
Last active October 27, 2025 15:39
Show Gist options
  • Select an option

  • Save airstrike/984642d803a5ce5dc1f07a0f3bb0e3e1 to your computer and use it in GitHub Desktop.

Select an option

Save airstrike/984642d803a5ce5dc1f07a0f3bb0e3e1 to your computer and use it in GitHub Desktop.
Multi-window iced example with custom theme palette
// 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