Merge pull request #3051 from iced-rs/replace-dark-light

System Theme Reactions
This commit is contained in:
Héctor 2025-09-08 14:50:49 +02:00 • committed by GitHub
commit a9091f9edd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
45 changed files with 1069 additions and 594 deletions

734
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -22,7 +22,7 @@ all-features = true
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[features] [features]
default = ["wgpu", "tiny-skia", "crisp", "web-colors", "auto-detect-theme", "thread-pool"] default = ["wgpu", "tiny-skia", "crisp", "web-colors", "thread-pool", "linux-theme-detection"]
# Enables the `wgpu` GPU-accelerated renderer backend # Enables the `wgpu` GPU-accelerated renderer backend
wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"] wgpu = ["iced_renderer/wgpu", "iced_widget/wgpu"]
# Enables the `tiny-skia` software renderer backend # Enables the `tiny-skia` software renderer backend
@ -54,21 +54,19 @@ tokio = ["iced_futures/tokio"]
# Enables `smol` as the `executor::Default` on native platforms # Enables `smol` as the `executor::Default` on native platforms
smol = ["iced_futures/smol"] smol = ["iced_futures/smol"]
# Enables querying system information # Enables querying system information
system = ["iced_winit/system"] sysinfo = ["iced_winit/sysinfo"]
# Enables broken "sRGB linear" blending to reproduce color management of the Web # Enables broken "sRGB linear" blending to reproduce color management of the Web
web-colors = ["iced_renderer/web-colors"] web-colors = ["iced_renderer/web-colors"]
# Enables pixel snapping for crisp edges by default (can cause jitter!) # Enables pixel snapping for crisp edges by default (can cause jitter!)
crisp = ["iced_core/crisp", "iced_widget/crisp"] crisp = ["iced_core/crisp", "iced_widget/crisp"]
# Enables the WebGL backend # Enables the WebGL backend
webgl = ["iced_renderer/webgl"] webgl = ["iced_renderer/webgl"]
# Enables syntax highligthing # Enables syntax highlighting
highlighter = ["iced_highlighter", "iced_widget/highlighter"] highlighter = ["iced_highlighter", "iced_widget/highlighter"]
# Enables the advanced module # Enables the advanced module
advanced = ["iced_core/advanced", "iced_widget/advanced"] advanced = ["iced_core/advanced", "iced_widget/advanced"]
# Embeds Fira Sans into the final application; useful for testing and Wasm builds # Embeds Fira Sans into the final application; useful for testing and Wasm builds
fira-sans = ["iced_renderer/fira-sans"] fira-sans = ["iced_renderer/fira-sans"]
# Auto-detects light/dark mode for the built-in theme
auto-detect-theme = ["iced_core/auto-detect-theme"]
# Enables basic text shaping by default # Enables basic text shaping by default
basic-shaping = ["iced_core/basic-shaping"] basic-shaping = ["iced_core/basic-shaping"]
# Enables advanced text shaping by default # Enables advanced text shaping by default
@ -79,6 +77,8 @@ strict-assertions = ["iced_renderer/strict-assertions"]
unconditional-rendering = ["iced_winit/unconditional-rendering"] unconditional-rendering = ["iced_winit/unconditional-rendering"]
# Enables support for the `sipper` library # Enables support for the `sipper` library
sipper = ["iced_runtime/sipper"] sipper = ["iced_runtime/sipper"]
# Enables Linux system theme detection
linux-theme-detection = ["iced_winit/linux-theme-detection"]
[dependencies] [dependencies]
iced_debug.workspace = true iced_debug.workspace = true
@ -175,10 +175,9 @@ bytemuck = { version = "1.0", features = ["derive"] }
bytes = "1.6" bytes = "1.6"
cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "b8dc518b8640928178a501257e353b73bc06cf47" } cargo-hot = { package = "cargo-hot-protocol", git = "https://github.com/hecrj/cargo-hot.git", rev = "b8dc518b8640928178a501257e353b73bc06cf47" }
cosmic-text = "0.14" cosmic-text = "0.14"
dark-light = "2.0" cryoglyph = { git = "https://github.com/iced-rs/cryoglyph.git", rev = "453cedec0d2ec563bd7fa87e84a2319bcebb1ba3" }
futures = { version = "0.3", default-features = false } futures = { version = "0.3", default-features = false }
glam = "0.25" glam = "0.25"
cryoglyph = { git = "https://github.com/iced-rs/cryoglyph.git", rev = "453cedec0d2ec563bd7fa87e84a2319bcebb1ba3" }
guillotiere = "0.6" guillotiere = "0.6"
half = "2.2" half = "2.2"
image = { version = "0.25", default-features = false } image = { version = "0.25", default-features = false }
@ -188,16 +187,17 @@ lilt = "0.8"
log = "0.4" log = "0.4"
lyon = "1.0" lyon = "1.0"
lyon_path = "1.0" lyon_path = "1.0"
mundy = { version = "0.2", default-features = false }
num-traits = "0.2" num-traits = "0.2"
ouroboros = "0.18" ouroboros = "0.18"
png = "0.17" png = "0.18"
pulldown-cmark = "0.12" pulldown-cmark = "0.12"
qrcode = { version = "0.13", default-features = false } qrcode = { version = "0.13", default-features = false }
raw-window-handle = "0.6" raw-window-handle = "0.6"
resvg = "0.42" resvg = "0.42"
rustc-hash = "2.0" rustc-hash = "2.0"
serde = "1.0"
semver = "1.0" semver = "1.0"
serde = "1.0"
sha2 = "0.10" sha2 = "0.10"
sipper = "0.1" sipper = "0.1"
smol = "2" smol = "2"

View file

@ -14,7 +14,6 @@ keywords.workspace = true
workspace = true workspace = true
[features] [features]
auto-detect-theme = ["dep:dark-light"]
advanced = [] advanced = []
crisp = [] crisp = []
basic-shaping = [] basic-shaping = []
@ -32,9 +31,6 @@ smol_str.workspace = true
thiserror.workspace = true thiserror.workspace = true
web-time.workspace = true web-time.workspace = true
dark-light.workspace = true
dark-light.optional = true
serde.workspace = true serde.workspace = true
serde.optional = true serde.optional = true
serde.features = ["derive"] serde.features = ["derive"]

View file

@ -166,31 +166,6 @@ impl Theme {
} }
} }
impl Default for Theme {
fn default() -> Self {
#[cfg(feature = "auto-detect-theme")]
{
use std::sync::LazyLock;
static DEFAULT: LazyLock<Theme> = LazyLock::new(|| {
match dark_light::detect()
.unwrap_or(dark_light::Mode::Unspecified)
{
dark_light::Mode::Dark => Theme::Dark,
dark_light::Mode::Light | dark_light::Mode::Unspecified => {
Theme::Light
}
}
});
DEFAULT.clone()
}
#[cfg(not(feature = "auto-detect-theme"))]
Theme::Light
}
}
impl fmt::Display for Theme { impl fmt::Display for Theme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -256,6 +231,18 @@ impl fmt::Display for Custom {
} }
} }
/// A theme mode, denoting the tone or brightness of a theme.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
/// No specific tone.
#[default]
None,
/// A mode referring to themes with light tones.
Light,
/// A mode referring to themes with dark tones.
Dark,
}
/// The base style of a theme. /// The base style of a theme.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style { pub struct Style {
@ -268,7 +255,13 @@ pub struct Style {
/// The default blank style of a theme. /// The default blank style of a theme.
pub trait Base { pub trait Base {
/// Returns the default base [`Style`] of a theme. /// Returns the default theme for the preferred [`Mode`].
fn default(preference: Mode) -> Self;
/// Returns the [`Mode`] of the theme.
fn mode(&self) -> Mode;
/// Returns the default base [`Style`] of the theme.
fn base(&self) -> Style; fn base(&self) -> Style;
/// Returns the color [`Palette`] of the theme. /// Returns the color [`Palette`] of the theme.
@ -280,6 +273,39 @@ pub trait Base {
} }
impl Base for Theme { impl Base for Theme {
fn default(preference: Mode) -> Self {
use std::env;
use std::sync::OnceLock;
static SYSTEM: OnceLock<Option<Theme>> = OnceLock::new();
let system = SYSTEM.get_or_init(|| {
let name = env::var("ICED_THEME").ok()?;
Theme::ALL
.iter()
.find(|theme| theme.to_string() == name)
.cloned()
});
if let Some(system) = system {
return system.clone();
}
match preference {
Mode::None | Mode::Light => Self::Light,
Mode::Dark => Self::Dark,
}
}
fn mode(&self) -> Mode {
if self.extended_palette().is_dark {
Mode::Dark
} else {
Mode::Light
}
}
fn base(&self) -> Style { fn base(&self) -> Style {
default(self) default(self)
} }

View file

@ -12,7 +12,7 @@ mod time_machine;
use crate::core::border; use crate::core::border;
use crate::core::keyboard; use crate::core::keyboard;
use crate::core::theme::{self, Base, Theme}; use crate::core::theme::{self, Theme};
use crate::core::time::seconds; use crate::core::time::seconds;
use crate::core::window; use crate::core::window;
use crate::core::{Alignment::Center, Color, Element, Length::Fill}; use crate::core::{Alignment::Center, Color, Element, Length::Fill};
@ -90,7 +90,11 @@ where
state.subscription(&self.program) state.subscription(&self.program)
} }
fn theme(&self, state: &Self::State, window: window::Id) -> Self::Theme { fn theme(
&self,
state: &Self::State,
window: window::Id,
) -> Option<Self::Theme> {
state.theme(&self.program, window) state.theme(&self.program, window)
} }
@ -307,14 +311,12 @@ where
} }
}; };
let theme = program.theme(state, window); fn derive_theme<T: theme::Base>(theme: &T) -> Theme {
let derive_theme = move || {
theme theme
.palette() .palette()
.map(|palette| Theme::custom("iced devtools", palette)) .map(|palette| Theme::custom("iced devtools", palette))
.unwrap_or_default() .unwrap_or(Theme::Dark)
}; }
let mode = match &self.mode { let mode = match &self.mode {
Mode::None => None, Mode::None => None,
@ -340,7 +342,7 @@ where
} }
} }
.map(|mode| { .map(|mode| {
themer(derive_theme(), Element::from(mode).map(Event::Message)) themer(derive_theme, Element::from(mode).map(Event::Message))
}); });
let notification = self let notification = self
@ -359,7 +361,7 @@ where
.push_maybe(mode.map(opaque)) .push_maybe(mode.map(opaque))
.push_maybe(notification.map(|notification| { .push_maybe(notification.map(|notification| {
themer( themer(
derive_theme(), derive_theme,
bottom_right(opaque( bottom_right(opaque(
container(notification) container(notification)
.padding(10) .padding(10)
@ -389,7 +391,7 @@ where
Subscription::batch([subscription, hotkeys, commands]) Subscription::batch([subscription, hotkeys, commands])
} }
fn theme(&self, program: &P, window: window::Id) -> P::Theme { fn theme(&self, program: &P, window: window::Id) -> Option<P::Theme> {
program.theme(self.state(), window) program.theme(self.state(), window)
} }

View file

@ -10,7 +10,7 @@ use iced::{Element, Fill, Point, Rectangle, Renderer, Subscription, Theme};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Arc::new, Arc::update, Arc::view) iced::application(Arc::new, Arc::update, Arc::view)
.subscription(Arc::subscription) .subscription(Arc::subscription)
.theme(|_| Theme::Dark) .theme(Theme::Dark)
.run() .run()
} }

View file

@ -4,7 +4,7 @@ use iced::{Element, Theme};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Example::default, Example::update, Example::view) iced::application(Example::default, Example::update, Example::view)
.theme(|_| Theme::CatppuccinMocha) .theme(Theme::CatppuccinMocha)
.run() .run()
} }

View file

@ -37,7 +37,7 @@ impl Events {
} }
Message::EventOccurred(event) => { Message::EventOccurred(event) => {
if let Event::Window(window::Event::CloseRequested) = event { if let Event::Window(window::Event::CloseRequested) = event {
window::get_latest().and_then(window::close) window::latest().and_then(window::close)
} else { } else {
Task::none() Task::none()
} }
@ -47,7 +47,7 @@ impl Events {
Task::none() Task::none()
} }
Message::Exit => window::get_latest().and_then(window::close), Message::Exit => window::latest().and_then(window::close),
} }
} }

View file

@ -20,7 +20,7 @@ enum Message {
impl Exit { impl Exit {
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::Confirm => window::get_latest().and_then(window::close), Message::Confirm => window::latest().and_then(window::close),
Message::Exit => { Message::Exit => {
self.show_confirm = true; self.show_confirm = true;

View file

@ -11,7 +11,7 @@ use iced::{
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Image::default, Image::update, Image::view) iced::application(Image::default, Image::update, Image::view)
.subscription(Image::subscription) .subscription(Image::subscription)
.theme(|_| Theme::TokyoNight) .theme(Theme::TokyoNight)
.run() .run()
} }

View file

@ -16,7 +16,7 @@ pub fn main() -> iced::Result {
iced::application(GameOfLife::default, GameOfLife::update, GameOfLife::view) iced::application(GameOfLife::default, GameOfLife::update, GameOfLife::view)
.subscription(GameOfLife::subscription) .subscription(GameOfLife::subscription)
.theme(|_| Theme::Dark) .theme(Theme::Dark)
.centered() .centered()
.run() .run()
} }

View file

@ -19,11 +19,11 @@ pub fn main() -> iced::Result {
.run() .run()
} }
#[derive(Default, Debug)] #[derive(Debug, Default)]
struct Layout { struct Layout {
example: Example, example: Example,
explain: bool, explain: bool,
theme: Theme, theme: Option<Theme>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -51,7 +51,7 @@ impl Layout {
self.explain = explain; self.explain = explain;
} }
Message::ThemeSelected(theme) => { Message::ThemeSelected(theme) => {
self.theme = theme; self.theme = Some(theme);
} }
} }
} }
@ -74,7 +74,8 @@ impl Layout {
horizontal_space(), horizontal_space(),
checkbox("Explain", self.explain) checkbox("Explain", self.explain)
.on_toggle(Message::ExplainToggled), .on_toggle(Message::ExplainToggled),
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeSelected), pick_list(Theme::ALL, self.theme.as_ref(), Message::ThemeSelected)
.placeholder("Theme"),
] ]
.spacing(20) .spacing(20)
.align_y(Center); .align_y(Center);
@ -116,7 +117,7 @@ impl Layout {
.into() .into()
} }
fn theme(&self) -> Theme { fn theme(&self) -> Option<Theme> {
self.theme.clone() self.theme.clone()
} }
} }

View file

@ -66,7 +66,7 @@ impl Example {
return Task::none(); return Task::none();
}; };
window::get_position(*last_window) window::position(*last_window)
.then(|last_position| { .then(|last_position| {
let position = last_position.map_or( let position = last_position.map_or(
window::Position::Default, window::Position::Default,
@ -138,12 +138,8 @@ impl Example {
} }
} }
fn theme(&self, window: window::Id) -> Theme { fn theme(&self, window: window::Id) -> Option<Theme> {
if let Some(window) = self.windows.get(&window) { Some(self.windows.get(&window)?.theme.clone())
window.theme.clone()
} else {
Theme::default()
}
} }
fn scale_factor(&self, window: window::Id) -> f32 { fn scale_factor(&self, window: window::Id) -> f32 {

View file

@ -20,7 +20,7 @@ struct QRGenerator {
data: String, data: String,
qr_code: Option<qr_code::Data>, qr_code: Option<qr_code::Data>,
total_size: Option<f32>, total_size: Option<f32>,
theme: Theme, theme: Option<Theme>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -58,7 +58,7 @@ impl QRGenerator {
self.total_size = Some(total_size); self.total_size = Some(total_size);
} }
Message::ThemeChanged(theme) => { Message::ThemeChanged(theme) => {
self.theme = theme; self.theme = Some(theme);
} }
} }
} }
@ -78,7 +78,8 @@ impl QRGenerator {
let choose_theme = row![ let choose_theme = row![
text("Theme:"), text("Theme:"),
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged,) pick_list(Theme::ALL, self.theme.as_ref(), Message::ThemeChanged)
.placeholder("Theme")
] ]
.spacing(10) .spacing(10)
.align_y(Center); .align_y(Center);
@ -107,7 +108,7 @@ impl QRGenerator {
center(content).padding(20).into() center(content).padding(20).into()
} }
fn theme(&self) -> Theme { fn theme(&self) -> Option<Theme> {
self.theme.clone() self.theme.clone()
} }
} }

View file

@ -49,7 +49,7 @@ impl Example {
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match message { match message {
Message::Screenshot => { Message::Screenshot => {
return window::get_latest() return window::latest()
.and_then(window::screenshot) .and_then(window::screenshot)
.map(Message::Screenshotted); .map(Message::Screenshotted);
} }

View file

@ -15,7 +15,7 @@ pub fn main() -> iced::Result {
#[derive(Default)] #[derive(Default)]
struct Styling { struct Styling {
theme: Theme, theme: Option<Theme>,
input_value: String, input_value: String,
slider_value: f32, slider_value: f32,
checkbox_value: bool, checkbox_value: bool,
@ -32,13 +32,14 @@ enum Message {
TogglerToggled(bool), TogglerToggled(bool),
PreviousTheme, PreviousTheme,
NextTheme, NextTheme,
ClearTheme,
} }
impl Styling { impl Styling {
fn update(&mut self, message: Message) { fn update(&mut self, message: Message) {
match message { match message {
Message::ThemeChanged(theme) => { Message::ThemeChanged(theme) => {
self.theme = theme; self.theme = Some(theme);
} }
Message::InputChanged(value) => self.input_value = value, Message::InputChanged(value) => self.input_value = value,
Message::ButtonPressed => {} Message::ButtonPressed => {}
@ -46,21 +47,29 @@ impl Styling {
Message::CheckboxToggled(value) => self.checkbox_value = value, Message::CheckboxToggled(value) => self.checkbox_value = value,
Message::TogglerToggled(value) => self.toggler_value = value, Message::TogglerToggled(value) => self.toggler_value = value,
Message::PreviousTheme | Message::NextTheme => { Message::PreviousTheme | Message::NextTheme => {
if let Some(current) = Theme::ALL let current = Theme::ALL.iter().position(|candidate| {
.iter() self.theme.as_ref() == Some(candidate)
.position(|candidate| &self.theme == candidate) });
{
self.theme = if matches!(message, Message::NextTheme) { self.theme = Some(if matches!(message, Message::NextTheme) {
Theme::ALL[(current + 1) % Theme::ALL.len()].clone() Theme::ALL[current.map(|current| current + 1).unwrap_or(0)
} else if current == 0 { % Theme::ALL.len()]
.clone()
} else {
let current = current.unwrap_or(0);
if current == 0 {
Theme::ALL Theme::ALL
.last() .last()
.expect("Theme::ALL must not be empty") .expect("Theme::ALL must not be empty")
.clone() .clone()
} else { } else {
Theme::ALL[current - 1].clone() Theme::ALL[current - 1].clone()
}; }
} });
}
Message::ClearTheme => {
self.theme = None;
} }
} }
} }
@ -68,8 +77,9 @@ impl Styling {
fn view(&self) -> Element<'_, Message> { fn view(&self) -> Element<'_, Message> {
let choose_theme = column![ let choose_theme = column![
text("Theme:"), text("Theme:"),
pick_list(Theme::ALL, Some(&self.theme), Message::ThemeChanged) pick_list(Theme::ALL, self.theme.as_ref(), Message::ThemeChanged)
.width(Fill), .width(Fill)
.placeholder("System"),
] ]
.spacing(10); .spacing(10);
@ -186,11 +196,14 @@ impl Styling {
keyboard::key::Named::ArrowDown keyboard::key::Named::ArrowDown
| keyboard::key::Named::ArrowRight, | keyboard::key::Named::ArrowRight,
) => Some(Message::NextTheme), ) => Some(Message::NextTheme),
keyboard::Key::Named(keyboard::key::Named::Space) => {
Some(Message::ClearTheme)
}
_ => None, _ => None,
}) })
} }
fn theme(&self) -> Theme { fn theme(&self) -> Option<Theme> {
self.theme.clone() self.theme.clone()
} }
} }
@ -210,9 +223,7 @@ mod tests {
.cloned() .cloned()
.map(|theme| { .map(|theme| {
let mut styling = Styling::default(); let mut styling = Styling::default();
styling.update(Message::ThemeChanged(theme)); styling.update(Message::ThemeChanged(theme.clone()));
let theme = styling.theme();
let mut ui = simulator(styling.view()); let mut ui = simulator(styling.view());
let snapshot = ui.snapshot(&theme)?; let snapshot = ui.snapshot(&theme)?;

View file

@ -7,6 +7,6 @@ publish = false
[dependencies] [dependencies]
iced.workspace = true iced.workspace = true
iced.features = ["system"] iced.features = ["sysinfo"]
bytesize = "1.1" bytesize = "1.1"

View file

@ -1,5 +1,6 @@
use iced::system;
use iced::widget::{button, center, column, text}; use iced::widget::{button, center, column, text};
use iced::{Element, Task, system}; use iced::{Element, Task};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Example::new, Example::update, Example::view).run() iced::application(Example::new, Example::update, Example::view).run()
@ -26,7 +27,7 @@ impl Example {
fn new() -> (Self, Task<Message>) { fn new() -> (Self, Task<Message>) {
( (
Self::Loading, Self::Loading,
system::fetch_information().map(Message::InformationReceived), system::information().map(Message::InformationReceived),
) )
} }

View file

@ -8,7 +8,7 @@ use iced::{Center, Element, Fill, Font, Right, Theme};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Table::new, Table::update, Table::view) iced::application(Table::new, Table::update, Table::view)
.theme(|_| Theme::CatppuccinMocha) .theme(Theme::CatppuccinMocha)
.run() .run()
} }

View file

@ -1 +1 @@
99f418007af163f172e163565f166da31015521e1bf7de95fa55cda2fb5a7db5 0acb67235c6a11014a2d2b825e0a70069bca0c67bee0cdb38a0144fc72b25220

View file

@ -5,7 +5,7 @@ use iced::widget::{
}; };
use iced::window; use iced::window;
use iced::{ use iced::{
Center, Element, Fill, Font, Function, Subscription, Task as Command, Center, Element, Fill, Font, Function, Subscription, Task as Command, Theme,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -151,7 +151,7 @@ impl Todos {
widget::focus_next() widget::focus_next()
} }
} }
Message::ToggleFullscreen(mode) => window::get_latest() Message::ToggleFullscreen(mode) => window::latest()
.and_then(move |window| window::set_mode(window, mode)), .and_then(move |window| window::set_mode(window, mode)),
Message::Loaded(_) => Command::none(), Message::Loaded(_) => Command::none(),
}; };
@ -194,7 +194,7 @@ impl Todos {
let title = text("todos") let title = text("todos")
.width(Fill) .width(Fill)
.size(100) .size(100)
.color([0.5, 0.5, 0.5]) .style(subtle)
.align_x(Center); .align_x(Center);
let input = text_input("What needs to be done?", input_value) let input = text_input("What needs to be done?", input_value)
@ -447,7 +447,7 @@ fn empty_message(message: &str) -> Element<'_, Message> {
.width(Fill) .width(Fill)
.size(25) .size(25)
.align_x(Center) .align_x(Center)
.color([0.7, 0.7, 0.7]), .style(subtle),
) )
.height(200) .height(200)
.into() .into()
@ -471,6 +471,12 @@ fn delete_icon() -> Text<'static> {
icon('\u{F1F8}') icon('\u{F1F8}')
} }
fn subtle(theme: &Theme) -> text::Style {
text::Style {
color: Some(theme.extended_palette().background.strongest.color),
}
}
// Persistence // Persistence
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
struct SavedState { struct SavedState {

View file

@ -1,11 +1,10 @@
use iced::border;
use iced::widget::{Button, Column, Container, Slider}; use iced::widget::{Button, Column, Container, Slider};
use iced::widget::{ use iced::widget::{
button, center_x, center_y, checkbox, column, horizontal_space, image, button, center_x, center_y, checkbox, column, horizontal_space, image,
radio, rich_text, row, scrollable, slider, span, text, text_input, toggler, radio, rich_text, row, scrollable, slider, span, text, text_input, toggler,
vertical_space, vertical_space,
}; };
use iced::{Center, Color, Element, Fill, Font, Pixels, Theme}; use iced::{Center, Color, Element, Fill, Font, Pixels, color};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
@ -201,7 +200,7 @@ impl Tour {
Self::container("Welcome!") Self::container("Welcome!")
.push( .push(
"This is a simple tour meant to showcase a bunch of \ "This is a simple tour meant to showcase a bunch of \
widgets that can be easily implemented on top of Iced.", widgets that come bundled in Iced.",
) )
.push( .push(
"Iced is a cross-platform GUI library for Rust focused on \ "Iced is a cross-platform GUI library for Rust focused on \
@ -216,28 +215,19 @@ impl Tour {
built on top of wgpu, a graphics library supporting Vulkan, \ built on top of wgpu, a graphics library supporting Vulkan, \
Metal, DX11, and DX12.", Metal, DX11, and DX12.",
) )
.push({ .push(
let theme = Theme::default();
let palette = theme.extended_palette();
rich_text![ rich_text![
"Additionally, this tour can also run on WebAssembly ", "Additionally, this tour can also run on WebAssembly ",
"by leveraging ", "by leveraging ",
span("trunk") span("trunk")
.color(palette.primary.base.color) .color(color!(0x7777FF))
.background(palette.background.weakest.color) .underline(true)
.border(
border::rounded(2)
.width(1)
.color(palette.background.weak.color)
)
.padding([0, 2])
.font(Font::MONOSPACE) .font(Font::MONOSPACE)
.link(Message::OpenTrunk), .link(Message::OpenTrunk),
"." "."
] ]
.on_link_click(std::convert::identity) .on_link_click(std::convert::identity),
}) )
.push( .push(
"You will need to interact with the UI in order to reach \ "You will need to interact with the UI in order to reach \
the end!", the end!",

View file

@ -11,7 +11,7 @@ pub fn main() -> iced::Result {
VectorialText::update, VectorialText::update,
VectorialText::view, VectorialText::view,
) )
.theme(|_| Theme::Dark) .theme(Theme::Dark)
.run() .run()
} }

View file

@ -12,7 +12,7 @@ use iced::{
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
iced::application(Example::default, Example::update, Example::view) iced::application(Example::default, Example::update, Example::view)
.subscription(Example::subscription) .subscription(Example::subscription)
.theme(|_| Theme::Dark) .theme(Theme::Dark)
.run() .run()
} }

View file

@ -37,6 +37,7 @@ where
event: Event::Window(window::Event::RedrawRequested(_)), event: Event::Window(window::Event::RedrawRequested(_)),
.. ..
} }
| subscription::Event::SystemThemeChanged(_)
| subscription::Event::PlatformSpecific(_) => None, | subscription::Event::PlatformSpecific(_) => None,
subscription::Event::Interaction { subscription::Event::Interaction {
window, window,
@ -66,7 +67,8 @@ where
event, event,
status, status,
} => f(event, status, window), } => f(event, status, window),
subscription::Event::PlatformSpecific(_) => None, subscription::Event::SystemThemeChanged(_)
| subscription::Event::PlatformSpecific(_) => None,
}) })
} }

View file

@ -4,6 +4,7 @@ mod tracker;
pub use tracker::Tracker; pub use tracker::Tracker;
use crate::core::event; use crate::core::event;
use crate::core::theme;
use crate::core::window; use crate::core::window;
use crate::futures::Stream; use crate::futures::Stream;
use crate::{BoxStream, MaybeSend}; use crate::{BoxStream, MaybeSend};
@ -27,6 +28,9 @@ pub enum Event {
status: event::Status, status: event::Status,
}, },
/// The system theme has changed.
SystemThemeChanged(theme::Mode),
/// A platform specific event. /// A platform specific event.
PlatformSpecific(PlatformSpecific), PlatformSpecific(PlatformSpecific),
} }
@ -422,7 +426,8 @@ where
} }
} }
pub(crate) fn filter_map<I, F, T>(id: I, f: F) -> Subscription<T> /// Creatges a [`Subscription`] from a hashable id and a filter function.
pub fn filter_map<I, F, T>(id: I, f: F) -> Subscription<T>
where where
I: Hash + 'static, I: Hash + 'static,
F: Fn(Event) -> Option<T> + MaybeSend + 'static, F: Fn(Event) -> Option<T> + MaybeSend + 'static,

View file

@ -59,7 +59,7 @@ pub trait Compositor: Sized {
); );
/// Returns [`Information`] used by this [`Compositor`]. /// Returns [`Information`] used by this [`Compositor`].
fn fetch_information(&self) -> Information; fn information(&self) -> Information;
/// Loads a font from its bytes. /// Loads a font from its bytes.
fn load_font(&mut self, font: Cow<'static, [u8]>) { fn load_font(&mut self, font: Cow<'static, [u8]>) {
@ -178,7 +178,7 @@ impl Compositor for () {
fn load_font(&mut self, _font: Cow<'static, [u8]>) {} fn load_font(&mut self, _font: Cow<'static, [u8]>) {}
fn fetch_information(&self) -> Information { fn information(&self) -> Information {
Information { Information {
adapter: String::from("Null Renderer"), adapter: String::from("Null Renderer"),
backend: String::from("Null"), backend: String::from("Null"),

View file

@ -25,7 +25,7 @@ pub trait Program: Sized {
type Message: Message + 'static; type Message: Message + 'static;
/// The theme of the program. /// The theme of the program.
type Theme: Default + theme::Base; type Theme: theme::Base;
/// The renderer of the program. /// The renderer of the program.
type Renderer: Renderer; type Renderer: Renderer;
@ -86,8 +86,12 @@ pub trait Program: Sized {
Subscription::none() Subscription::none()
} }
fn theme(&self, _state: &Self::State, _window: window::Id) -> Self::Theme { fn theme(
<Self::Theme as Default>::default() &self,
_state: &Self::State,
_window: window::Id,
) -> Option<Self::Theme> {
None
} }
fn style(&self, _state: &Self::State, theme: &Self::Theme) -> theme::Style { fn style(&self, _state: &Self::State, theme: &Self::Theme) -> theme::Style {
@ -152,7 +156,7 @@ pub fn with_title<P: Program>(
&self, &self,
state: &Self::State, state: &Self::State,
window: window::Id, window: window::Id,
) -> Self::Theme { ) -> Option<Self::Theme> {
self.program.theme(state, window) self.program.theme(state, window)
} }
@ -238,7 +242,7 @@ pub fn with_subscription<P: Program>(
&self, &self,
state: &Self::State, state: &Self::State,
window: window::Id, window: window::Id,
) -> Self::Theme { ) -> Option<Self::Theme> {
self.program.theme(state, window) self.program.theme(state, window)
} }
@ -264,7 +268,7 @@ pub fn with_subscription<P: Program>(
/// Decorates a [`Program`] with the given theme function. /// Decorates a [`Program`] with the given theme function.
pub fn with_theme<P: Program>( pub fn with_theme<P: Program>(
program: P, program: P,
f: impl Fn(&P::State, window::Id) -> P::Theme, f: impl Fn(&P::State, window::Id) -> Option<P::Theme>,
) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> { ) -> impl Program<State = P::State, Message = P::Message, Theme = P::Theme> {
struct WithTheme<P, F> { struct WithTheme<P, F> {
program: P, program: P,
@ -273,7 +277,7 @@ pub fn with_theme<P: Program>(
impl<P: Program, F> Program for WithTheme<P, F> impl<P: Program, F> Program for WithTheme<P, F>
where where
F: Fn(&P::State, window::Id) -> P::Theme, F: Fn(&P::State, window::Id) -> Option<P::Theme>,
{ {
type State = P::State; type State = P::State;
type Message = P::Message; type Message = P::Message;
@ -285,7 +289,7 @@ pub fn with_theme<P: Program>(
&self, &self,
state: &Self::State, state: &Self::State,
window: window::Id, window: window::Id,
) -> Self::Theme { ) -> Option<Self::Theme> {
(self.theme)(state, window) (self.theme)(state, window)
} }
@ -407,7 +411,7 @@ pub fn with_style<P: Program>(
&self, &self,
state: &Self::State, state: &Self::State,
window: window::Id, window: window::Id,
) -> Self::Theme { ) -> Option<Self::Theme> {
self.program.theme(state, window) self.program.theme(state, window)
} }
@ -478,7 +482,7 @@ pub fn with_scale_factor<P: Program>(
&self, &self,
state: &Self::State, state: &Self::State,
window: window::Id, window: window::Id,
) -> Self::Theme { ) -> Option<Self::Theme> {
self.program.theme(state, window) self.program.theme(state, window)
} }
@ -561,7 +565,7 @@ pub fn with_executor<P: Program, E: Executor>(
&self, &self,
state: &Self::State, state: &Self::State,
window: window::Id, window: window::Id,
) -> Self::Theme { ) -> Option<Self::Theme> {
self.program.theme(state, window) self.program.theme(state, window)
} }
@ -628,7 +632,7 @@ impl<P: Program> Instance<P> {
} }
/// Returns the current theme of the [`Instance`]. /// Returns the current theme of the [`Instance`].
pub fn theme(&self, window: window::Id) -> P::Theme { pub fn theme(&self, window: window::Id) -> Option<P::Theme> {
self.program.theme(&self.state, window) self.program.theme(&self.state, window)
} }

View file

@ -313,8 +313,8 @@ where
delegate!(self, compositor, compositor.load_font(font)); delegate!(self, compositor, compositor.load_font(font));
} }
fn fetch_information(&self) -> compositor::Information { fn information(&self) -> compositor::Information {
delegate!(self, compositor, compositor.fetch_information()) delegate!(self, compositor, compositor.information())
} }
fn present( fn present(

View file

@ -1,11 +1,20 @@
//! Access the native system. //! Access the native system.
use crate::core::theme;
use crate::futures::futures::channel::oneshot; use crate::futures::futures::channel::oneshot;
use crate::futures::subscription::{self, Subscription};
use crate::task::{self, Task};
/// An operation to be performed on the system. /// An operation to be performed on the system.
#[derive(Debug)] #[derive(Debug)]
pub enum Action { pub enum Action {
/// Query system information and produce `T` with the result. /// Send available system information.
QueryInformation(oneshot::Sender<Information>), GetInformation(oneshot::Sender<Information>),
/// Send the current system theme mode.
GetTheme(oneshot::Sender<theme::Mode>),
/// Notify to the runtime that the system theme has changed.
NotifyTheme(theme::Mode),
} }
/// Contains information about the system (e.g. system name, processor, memory, graphics adapter). /// Contains information about the system (e.g. system name, processor, memory, graphics adapter).
@ -37,3 +46,29 @@ pub struct Information {
/// Model information for the active graphics adapter /// Model information for the active graphics adapter
pub graphics_adapter: String, pub graphics_adapter: String,
} }
/// Returns available system information.
pub fn information() -> Task<Information> {
task::oneshot(|channel| {
crate::Action::System(Action::GetInformation(channel))
})
}
/// Returns the current system theme.
pub fn theme() -> Task<theme::Mode> {
task::oneshot(|sender| crate::Action::System(Action::GetTheme(sender)))
}
/// Subscribes to system theme changes.
pub fn theme_changes() -> Subscription<theme::Mode> {
#[derive(Hash)]
struct ThemeChanges;
subscription::filter_map(ThemeChanges, |event| {
let subscription::Event::SystemThemeChanged(mode) = event else {
return None;
};
Some(mode)
})
}

View file

@ -266,12 +266,12 @@ pub fn close<T>(id: Id) -> Task<T> {
} }
/// Gets the window [`Id`] of the oldest window. /// Gets the window [`Id`] of the oldest window.
pub fn get_oldest() -> Task<Option<Id>> { pub fn oldest() -> Task<Option<Id>> {
task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel))) task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel)))
} }
/// Gets the window [`Id`] of the latest window. /// Gets the window [`Id`] of the latest window.
pub fn get_latest() -> Task<Option<Id>> { pub fn latest() -> Task<Option<Id>> {
task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel))) task::oneshot(|channel| crate::Action::Window(Action::GetLatest(channel)))
} }
@ -315,14 +315,14 @@ pub fn set_resize_increments<T>(id: Id, increments: Option<Size>) -> Task<T> {
} }
/// Get the window's size in logical dimensions. /// Get the window's size in logical dimensions.
pub fn get_size(id: Id) -> Task<Size> { pub fn size(id: Id) -> Task<Size> {
task::oneshot(move |channel| { task::oneshot(move |channel| {
crate::Action::Window(Action::GetSize(id, channel)) crate::Action::Window(Action::GetSize(id, channel))
}) })
} }
/// Gets the maximized state of the window with the given [`Id`]. /// Gets the maximized state of the window with the given [`Id`].
pub fn get_maximized(id: Id) -> Task<bool> { pub fn is_maximized(id: Id) -> Task<bool> {
task::oneshot(move |channel| { task::oneshot(move |channel| {
crate::Action::Window(Action::GetMaximized(id, channel)) crate::Action::Window(Action::GetMaximized(id, channel))
}) })
@ -334,7 +334,7 @@ pub fn maximize<T>(id: Id, maximized: bool) -> Task<T> {
} }
/// Gets the minimized state of the window with the given [`Id`]. /// Gets the minimized state of the window with the given [`Id`].
pub fn get_minimized(id: Id) -> Task<Option<bool>> { pub fn is_minimized(id: Id) -> Task<Option<bool>> {
task::oneshot(move |channel| { task::oneshot(move |channel| {
crate::Action::Window(Action::GetMinimized(id, channel)) crate::Action::Window(Action::GetMinimized(id, channel))
}) })
@ -346,14 +346,14 @@ pub fn minimize<T>(id: Id, minimized: bool) -> Task<T> {
} }
/// Gets the position in logical coordinates of the window with the given [`Id`]. /// Gets the position in logical coordinates of the window with the given [`Id`].
pub fn get_position(id: Id) -> Task<Option<Point>> { pub fn position(id: Id) -> Task<Option<Point>> {
task::oneshot(move |channel| { task::oneshot(move |channel| {
crate::Action::Window(Action::GetPosition(id, channel)) crate::Action::Window(Action::GetPosition(id, channel))
}) })
} }
/// Gets the scale factor of the window with the given [`Id`]. /// Gets the scale factor of the window with the given [`Id`].
pub fn get_scale_factor(id: Id) -> Task<f32> { pub fn scale_factor(id: Id) -> Task<f32> {
task::oneshot(move |channel| { task::oneshot(move |channel| {
crate::Action::Window(Action::GetScaleFactor(id, channel)) crate::Action::Window(Action::GetScaleFactor(id, channel))
}) })
@ -365,7 +365,7 @@ pub fn move_to<T>(id: Id, position: Point) -> Task<T> {
} }
/// Gets the current [`Mode`] of the window. /// Gets the current [`Mode`] of the window.
pub fn get_mode(id: Id) -> Task<Mode> { pub fn mode(id: Id) -> Task<Mode> {
task::oneshot(move |channel| { task::oneshot(move |channel| {
crate::Action::Window(Action::GetMode(id, channel)) crate::Action::Window(Action::GetMode(id, channel))
}) })
@ -426,7 +426,7 @@ pub fn show_system_menu<T>(id: Id) -> Task<T> {
/// Gets an identifier unique to the window, provided by the underlying windowing system. This is /// Gets an identifier unique to the window, provided by the underlying windowing system. This is
/// not to be confused with [`Id`]. /// not to be confused with [`Id`].
pub fn get_raw_id<Message>(id: Id) -> Task<u64> { pub fn raw_id<Message>(id: Id) -> Task<u64> {
task::oneshot(|channel| { task::oneshot(|channel| {
crate::Action::Window(Action::GetRawId(id, channel)) crate::Action::Window(Action::GetRawId(id, channel))
}) })

View file

@ -7,7 +7,7 @@
//! //!
//! pub fn main() -> iced::Result { //! pub fn main() -> iced::Result {
//! iced::application(u64::default, update, view) //! iced::application(u64::default, update, view)
//! .theme(|_| Theme::Dark) //! .theme(Theme::Dark)
//! .centered() //! .centered()
//! .run() //! .run()
//! } //! }
@ -35,7 +35,7 @@ use crate::shell;
use crate::theme; use crate::theme;
use crate::window; use crate::window;
use crate::{ use crate::{
Element, Executor, Font, Result, Settings, Size, Subscription, Task, Element, Executor, Font, Result, Settings, Size, Subscription, Task, Theme,
}; };
use iced_debug as debug; use iced_debug as debug;
@ -75,14 +75,14 @@ pub use timed::timed;
/// } /// }
/// ``` /// ```
pub fn application<State, Message, Theme, Renderer>( pub fn application<State, Message, Theme, Renderer>(
boot: impl Boot<State, Message>, boot: impl BootFn<State, Message>,
update: impl Update<State, Message>, update: impl UpdateFn<State, Message>,
view: impl for<'a> View<'a, State, Message, Theme, Renderer>, view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
) -> Application<impl Program<State = State, Message = Message, Theme = Theme>> ) -> Application<impl Program<State = State, Message = Message, Theme = Theme>>
where where
State: 'static, State: 'static,
Message: program::Message + 'static, Message: program::Message + 'static,
Theme: Default + theme::Base, Theme: theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
{ {
use std::marker::PhantomData; use std::marker::PhantomData;
@ -101,11 +101,11 @@ where
for Instance<State, Message, Theme, Renderer, Boot, Update, View> for Instance<State, Message, Theme, Renderer, Boot, Update, View>
where where
Message: program::Message + 'static, Message: program::Message + 'static,
Theme: Default + theme::Base, Theme: theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
Boot: self::Boot<State, Message>, Boot: self::BootFn<State, Message>,
Update: self::Update<State, Message>, Update: self::UpdateFn<State, Message>,
View: for<'a> self::View<'a, State, Message, Theme, Renderer>, View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
{ {
type State = State; type State = State;
type Message = Message; type Message = Message;
@ -320,10 +320,10 @@ impl<P: Program> Application<P> {
} }
} }
/// Sets the [`Title`] of the [`Application`]. /// Sets the title of the [`Application`].
pub fn title( pub fn title(
self, self,
title: impl Title<P::State>, title: impl TitleFn<P::State>,
) -> Application< ) -> Application<
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
> { > {
@ -355,13 +355,13 @@ impl<P: Program> Application<P> {
/// Sets the theme logic of the [`Application`]. /// Sets the theme logic of the [`Application`].
pub fn theme( pub fn theme(
self, self,
f: impl Fn(&P::State) -> P::Theme, f: impl ThemeFn<P::State, P::Theme>,
) -> Application< ) -> Application<
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
> { > {
Application { Application {
raw: program::with_theme(self.raw, move |state, _window| { raw: program::with_theme(self.raw, move |state, _window| {
debug::hot(|| f(state)) debug::hot(|| f.theme(state))
}), }),
settings: self.settings, settings: self.settings,
window: self.window, window: self.window,
@ -425,12 +425,12 @@ impl<P: Program> Application<P> {
/// In practice, this means that [`application`] can both take /// In practice, this means that [`application`] can both take
/// simple functions like `State::default` and more advanced ones /// simple functions like `State::default` and more advanced ones
/// that return a [`Task`]. /// that return a [`Task`].
pub trait Boot<State, Message> { pub trait BootFn<State, Message> {
/// Initializes the [`Application`] state. /// Initializes the [`Application`] state.
fn boot(&self) -> (State, Task<Message>); fn boot(&self) -> (State, Task<Message>);
} }
impl<T, C, State, Message> Boot<State, Message> for T impl<T, C, State, Message> BootFn<State, Message> for T
where where
T: Fn() -> C, T: Fn() -> C,
C: IntoBoot<State, Message>, C: IntoBoot<State, Message>,
@ -464,18 +464,18 @@ impl<State, Message> IntoBoot<State, Message> for (State, Task<Message>) {
/// any closure `Fn(&State) -> String`. /// any closure `Fn(&State) -> String`.
/// ///
/// This trait allows the [`application`] builder to take any of them. /// This trait allows the [`application`] builder to take any of them.
pub trait Title<State> { pub trait TitleFn<State> {
/// Produces the title of the [`Application`]. /// Produces the title of the [`Application`].
fn title(&self, state: &State) -> String; fn title(&self, state: &State) -> String;
} }
impl<State> Title<State> for &'static str { impl<State> TitleFn<State> for &'static str {
fn title(&self, _state: &State) -> String { fn title(&self, _state: &State) -> String {
self.to_string() self.to_string()
} }
} }
impl<T, State> Title<State> for T impl<T, State> TitleFn<State> for T
where where
T: Fn(&State) -> String, T: Fn(&State) -> String,
{ {
@ -488,18 +488,18 @@ where
/// ///
/// This trait allows the [`application`] builder to take any closure that /// This trait allows the [`application`] builder to take any closure that
/// returns any `Into<Task<Message>>`. /// returns any `Into<Task<Message>>`.
pub trait Update<State, Message> { pub trait UpdateFn<State, Message> {
/// Processes the message and updates the state of the [`Application`]. /// Processes the message and updates the state of the [`Application`].
fn update(&self, state: &mut State, message: Message) -> Task<Message>; fn update(&self, state: &mut State, message: Message) -> Task<Message>;
} }
impl<State, Message> Update<State, Message> for () { impl<State, Message> UpdateFn<State, Message> for () {
fn update(&self, _state: &mut State, _message: Message) -> Task<Message> { fn update(&self, _state: &mut State, _message: Message) -> Task<Message> {
Task::none() Task::none()
} }
} }
impl<T, State, Message, C> Update<State, Message> for T impl<T, State, Message, C> UpdateFn<State, Message> for T
where where
T: Fn(&mut State, Message) -> C, T: Fn(&mut State, Message) -> C,
C: Into<Task<Message>>, C: Into<Task<Message>>,
@ -513,13 +513,13 @@ where
/// ///
/// This trait allows the [`application`] builder to take any closure that /// This trait allows the [`application`] builder to take any closure that
/// returns any `Into<Element<'_, Message>>`. /// returns any `Into<Element<'_, Message>>`.
pub trait View<'a, State, Message, Theme, Renderer> { pub trait ViewFn<'a, State, Message, Theme, Renderer> {
/// Produces the widget of the [`Application`]. /// Produces the widget of the [`Application`].
fn view(&self, state: &'a State) -> Element<'a, Message, Theme, Renderer>; fn view(&self, state: &'a State) -> Element<'a, Message, Theme, Renderer>;
} }
impl<'a, T, State, Message, Theme, Renderer, Widget> impl<'a, T, State, Message, Theme, Renderer, Widget>
View<'a, State, Message, Theme, Renderer> for T ViewFn<'a, State, Message, Theme, Renderer> for T
where where
T: Fn(&'a State) -> Widget, T: Fn(&'a State) -> Widget,
State: 'static, State: 'static,
@ -529,3 +529,35 @@ where
self(state).into() self(state).into()
} }
} }
/// The theme logic of some [`Application`].
///
/// Any implementors of this trait can be provided as an argument to
/// [`Application::theme`].
///
/// `iced` provides two implementors:
/// - the built-in [`Theme`] itself
/// - and any `Fn(&State) -> impl Into<Option<Theme>>`.
pub trait ThemeFn<State, Theme> {
/// Returns the theme of the [`Application`] for the current state.
///
/// If `None` is returned, `iced` will try to use a theme that
/// matches the system color scheme.
fn theme(&self, state: &State) -> Option<Theme>;
}
impl<State> ThemeFn<State, Theme> for Theme {
fn theme(&self, _state: &State) -> Option<Theme> {
Some(self.clone())
}
}
impl<F, T, State, Theme> ThemeFn<State, Theme> for F
where
F: Fn(&State) -> T,
T: Into<Option<Theme>>,
{
fn theme(&self, state: &State) -> Option<Theme> {
(self)(state).into()
}
}

View file

@ -1,5 +1,5 @@
//! An [`Application`] that receives an [`Instant`] in update logic. //! An [`Application`] that receives an [`Instant`] in update logic.
use crate::application::{Application, Boot, View}; use crate::application::{Application, BootFn, ViewFn};
use crate::program; use crate::program;
use crate::theme; use crate::theme;
use crate::time::Instant; use crate::time::Instant;
@ -20,17 +20,17 @@ use iced_debug as debug;
/// ///
/// [`comet`]: https://github.com/iced-rs/comet /// [`comet`]: https://github.com/iced-rs/comet
pub fn timed<State, Message, Theme, Renderer>( pub fn timed<State, Message, Theme, Renderer>(
boot: impl Boot<State, Message>, boot: impl BootFn<State, Message>,
update: impl Update<State, Message>, update: impl UpdateFn<State, Message>,
subscription: impl Fn(&State) -> Subscription<Message>, subscription: impl Fn(&State) -> Subscription<Message>,
view: impl for<'a> View<'a, State, Message, Theme, Renderer>, view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
) -> Application< ) -> Application<
impl Program<State = State, Message = (Message, Instant), Theme = Theme>, impl Program<State = State, Message = (Message, Instant), Theme = Theme>,
> >
where where
State: 'static, State: 'static,
Message: program::Message + 'static, Message: program::Message + 'static,
Theme: Default + theme::Base + 'static, Theme: theme::Base + 'static,
Renderer: program::Renderer + 'static, Renderer: program::Renderer + 'static,
{ {
use std::marker::PhantomData; use std::marker::PhantomData;
@ -69,12 +69,12 @@ where
> >
where where
Message: program::Message + 'static, Message: program::Message + 'static,
Theme: Default + theme::Base + 'static, Theme: theme::Base + 'static,
Renderer: program::Renderer + 'static, Renderer: program::Renderer + 'static,
Boot: self::Boot<State, Message>, Boot: self::BootFn<State, Message>,
Update: self::Update<State, Message>, Update: self::UpdateFn<State, Message>,
Subscription: Fn(&State) -> self::Subscription<Message>, Subscription: Fn(&State) -> self::Subscription<Message>,
View: for<'a> self::View<'a, State, Message, Theme, Renderer>, View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
{ {
type State = State; type State = State;
type Message = (Message, Instant); type Message = (Message, Instant);
@ -148,9 +148,9 @@ where
/// The update logic of some timed [`Application`]. /// The update logic of some timed [`Application`].
/// ///
/// This is like [`application::Update`](super::Update), /// This is like [`application::UpdateFn`](super::UpdateFn),
/// but it also takes an [`Instant`]. /// but it also takes an [`Instant`].
pub trait Update<State, Message> { pub trait UpdateFn<State, Message> {
/// Processes the message and updates the state of the [`Application`]. /// Processes the message and updates the state of the [`Application`].
fn update( fn update(
&self, &self,
@ -160,7 +160,7 @@ pub trait Update<State, Message> {
) -> impl Into<Task<Message>>; ) -> impl Into<Task<Message>>;
} }
impl<State, Message> Update<State, Message> for () { impl<State, Message> UpdateFn<State, Message> for () {
fn update( fn update(
&self, &self,
_state: &mut State, _state: &mut State,
@ -170,7 +170,7 @@ impl<State, Message> Update<State, Message> for () {
} }
} }
impl<T, State, Message, C> Update<State, Message> for T impl<T, State, Message, C> UpdateFn<State, Message> for T
where where
T: Fn(&mut State, Message, Instant) -> C, T: Fn(&mut State, Message, Instant) -> C,
C: Into<Task<Message>>, C: Into<Task<Message>>,

View file

@ -4,7 +4,9 @@ use crate::program::{self, Program};
use crate::shell; use crate::shell;
use crate::theme; use crate::theme;
use crate::window; use crate::window;
use crate::{Element, Executor, Font, Result, Settings, Subscription, Task}; use crate::{
Element, Executor, Font, Result, Settings, Subscription, Task, Theme,
};
use iced_debug as debug; use iced_debug as debug;
@ -21,14 +23,14 @@ use std::borrow::Cow;
/// ///
/// [`exit`]: crate::exit /// [`exit`]: crate::exit
pub fn daemon<State, Message, Theme, Renderer>( pub fn daemon<State, Message, Theme, Renderer>(
boot: impl application::Boot<State, Message>, boot: impl application::BootFn<State, Message>,
update: impl application::Update<State, Message>, update: impl application::UpdateFn<State, Message>,
view: impl for<'a> View<'a, State, Message, Theme, Renderer>, view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>> ) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>>
where where
State: 'static, State: 'static,
Message: program::Message + 'static, Message: program::Message + 'static,
Theme: Default + theme::Base, Theme: theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
{ {
use std::marker::PhantomData; use std::marker::PhantomData;
@ -47,11 +49,11 @@ where
for Instance<State, Message, Theme, Renderer, Boot, Update, View> for Instance<State, Message, Theme, Renderer, Boot, Update, View>
where where
Message: program::Message + 'static, Message: program::Message + 'static,
Theme: Default + theme::Base, Theme: theme::Base,
Renderer: program::Renderer, Renderer: program::Renderer,
Boot: application::Boot<State, Message>, Boot: application::BootFn<State, Message>,
Update: application::Update<State, Message>, Update: application::UpdateFn<State, Message>,
View: for<'a> self::View<'a, State, Message, Theme, Renderer>, View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
{ {
type State = State; type State = State;
type Message = Message; type Message = Message;
@ -169,10 +171,10 @@ impl<P: Program> Daemon<P> {
self self
} }
/// Sets the [`Title`] of the [`Daemon`]. /// Sets the title of the [`Daemon`].
pub fn title( pub fn title(
self, self,
title: impl Title<P::State>, title: impl TitleFn<P::State>,
) -> Daemon< ) -> Daemon<
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
> { > {
@ -202,13 +204,13 @@ impl<P: Program> Daemon<P> {
/// Sets the theme logic of the [`Daemon`]. /// Sets the theme logic of the [`Daemon`].
pub fn theme( pub fn theme(
self, self,
f: impl Fn(&P::State, window::Id) -> P::Theme, f: impl ThemeFn<P::State, P::Theme>,
) -> Daemon< ) -> Daemon<
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>, impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
> { > {
Daemon { Daemon {
raw: program::with_theme(self.raw, move |state, window| { raw: program::with_theme(self.raw, move |state, window| {
debug::hot(|| f(state, window)) debug::hot(|| f.theme(state, window))
}), }),
settings: self.settings, settings: self.settings,
} }
@ -266,18 +268,18 @@ impl<P: Program> Daemon<P> {
/// any closure `Fn(&State, window::Id) -> String`. /// any closure `Fn(&State, window::Id) -> String`.
/// ///
/// This trait allows the [`daemon`] builder to take any of them. /// This trait allows the [`daemon`] builder to take any of them.
pub trait Title<State> { pub trait TitleFn<State> {
/// Produces the title of the [`Daemon`]. /// Produces the title of the [`Daemon`].
fn title(&self, state: &State, window: window::Id) -> String; fn title(&self, state: &State, window: window::Id) -> String;
} }
impl<State> Title<State> for &'static str { impl<State> TitleFn<State> for &'static str {
fn title(&self, _state: &State, _window: window::Id) -> String { fn title(&self, _state: &State, _window: window::Id) -> String {
self.to_string() self.to_string()
} }
} }
impl<T, State> Title<State> for T impl<T, State> TitleFn<State> for T
where where
T: Fn(&State, window::Id) -> String, T: Fn(&State, window::Id) -> String,
{ {
@ -290,7 +292,7 @@ where
/// ///
/// This trait allows the [`daemon`] builder to take any closure that /// This trait allows the [`daemon`] builder to take any closure that
/// returns any `Into<Element<'_, Message>>`. /// returns any `Into<Element<'_, Message>>`.
pub trait View<'a, State, Message, Theme, Renderer> { pub trait ViewFn<'a, State, Message, Theme, Renderer> {
/// Produces the widget of the [`Daemon`]. /// Produces the widget of the [`Daemon`].
fn view( fn view(
&self, &self,
@ -300,7 +302,7 @@ pub trait View<'a, State, Message, Theme, Renderer> {
} }
impl<'a, T, State, Message, Theme, Renderer, Widget> impl<'a, T, State, Message, Theme, Renderer, Widget>
View<'a, State, Message, Theme, Renderer> for T ViewFn<'a, State, Message, Theme, Renderer> for T
where where
T: Fn(&'a State, window::Id) -> Widget, T: Fn(&'a State, window::Id) -> Widget,
State: 'static, State: 'static,
@ -314,3 +316,35 @@ where
self(state, window).into() self(state, window).into()
} }
} }
/// The theme logic of some [`Daemon`].
///
/// Any implementors of this trait can be provided as an argument to
/// [`Daemon::theme`].
///
/// `iced` provides two implementors:
/// - the built-in [`Theme`] itself
/// - and any `Fn(&State, window::Id) -> impl Into<Option<Theme>>`.
pub trait ThemeFn<State, Theme> {
/// Returns the theme of the [`Daemon`] for the current state and window.
///
/// If `None` is returned, `iced` will try to use a theme that
/// matches the system color scheme.
fn theme(&self, state: &State, window: window::Id) -> Option<Theme>;
}
impl<State> ThemeFn<State, Theme> for Theme {
fn theme(&self, _state: &State, _window: window::Id) -> Option<Theme> {
Some(self.clone())
}
}
impl<F, T, State, Theme> ThemeFn<State, Theme> for F
where
F: Fn(&State, window::Id) -> T,
T: Into<Option<Theme>>,
{
fn theme(&self, state: &State, window: window::Id) -> Option<Theme> {
(self)(state, window).into()
}
}

View file

@ -587,11 +587,12 @@ pub mod mouse {
}; };
} }
#[cfg(feature = "system")]
pub mod system { pub mod system {
//! Retrieve system information. //! Retrieve system information.
pub use crate::runtime::system::Information; pub use crate::runtime::system::{theme, theme_changes};
pub use crate::shell::system::*;
#[cfg(feature = "sysinfo")]
pub use crate::runtime::system::{Information, information};
} }
pub mod overlay { pub mod overlay {
@ -691,14 +692,14 @@ pub type Result = std::result::Result<(), Error>;
/// } /// }
/// ``` /// ```
pub fn run<State, Message, Theme, Renderer>( pub fn run<State, Message, Theme, Renderer>(
update: impl application::Update<State, Message> + 'static, update: impl application::UpdateFn<State, Message> + 'static,
view: impl for<'a> application::View<'a, State, Message, Theme, Renderer> view: impl for<'a> application::ViewFn<'a, State, Message, Theme, Renderer>
+ 'static, + 'static,
) -> Result ) -> Result
where where
State: Default + 'static, State: Default + 'static,
Message: program::Message + 'static, Message: program::Message + 'static,
Theme: Default + theme::Base + 'static, Theme: theme::Base + 'static,
Renderer: program::Renderer + 'static, Renderer: program::Renderer + 'static,
{ {
application(State::default, update, view).run() application(State::default, update, view).run()

View file

@ -494,10 +494,13 @@ impl Snapshot {
if path.exists() { if path.exists() {
let file = fs::File::open(&path)?; let file = fs::File::open(&path)?;
let decoder = png::Decoder::new(file); let decoder = png::Decoder::new(io::BufReader::new(file));
let mut reader = decoder.read_info()?; let mut reader = decoder.read_info()?;
let mut bytes = vec![0; reader.output_buffer_size()]; let n = reader
.output_buffer_size()
.expect("snapshot should fit in memory");
let mut bytes = vec![0; n];
let info = reader.next_frame(&mut bytes)?; let info = reader.next_frame(&mut bytes)?;
Ok(self.screenshot.bytes == bytes[..info.buffer_size()]) Ok(self.screenshot.bytes == bytes[..info.buffer_size()])

View file

@ -100,7 +100,7 @@ impl crate::graphics::Compositor for Compositor {
surface.layer_stack.clear(); surface.layer_stack.clear();
} }
fn fetch_information(&self) -> Information { fn information(&self) -> Information {
Information { Information {
adapter: String::from("CPU"), adapter: String::from("CPU"),
backend: String::from("tiny-skia"), backend: String::from("tiny-skia"),

View file

@ -332,7 +332,7 @@ impl graphics::Compositor for Compositor {
); );
} }
fn fetch_information(&self) -> compositor::Information { fn information(&self) -> compositor::Information {
let information = self.adapter.get_info(); let information = self.adapter.get_info();
compositor::Information { compositor::Information {

View file

@ -2063,7 +2063,7 @@ where
/// A widget that applies any `Theme` to its contents. /// A widget that applies any `Theme` to its contents.
pub fn themer<'a, Message, OldTheme, NewTheme, Renderer>( pub fn themer<'a, Message, OldTheme, NewTheme, Renderer>(
new_theme: NewTheme, to_theme: impl Fn(&OldTheme) -> NewTheme,
content: impl Into<Element<'a, Message, NewTheme, Renderer>>, content: impl Into<Element<'a, Message, NewTheme, Renderer>>,
) -> Themer< ) -> Themer<
'a, 'a,
@ -2077,7 +2077,7 @@ where
Renderer: core::Renderer, Renderer: core::Renderer,
NewTheme: Clone, NewTheme: Clone,
{ {
Themer::new(move |_| new_theme.clone(), content) Themer::new(to_theme, content)
} }
/// Creates a [`PaneGrid`] with the given [`pane_grid::State`] and view function. /// Creates a [`PaneGrid`] with the given [`pane_grid::State`] and view function.

View file

@ -16,13 +16,14 @@ workspace = true
[features] [features]
default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
debug = ["iced_debug/enable"] debug = ["iced_debug/enable"]
system = ["sysinfo"] sysinfo = ["dep:sysinfo"]
program = [] program = []
x11 = ["winit/x11"] x11 = ["winit/x11"]
wayland = ["winit/wayland"] wayland = ["winit/wayland"]
wayland-dlopen = ["winit/wayland-dlopen"] wayland-dlopen = ["winit/wayland-dlopen"]
wayland-csd-adwaita = ["winit/wayland-csd-adwaita"] wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
unconditional-rendering = [] unconditional-rendering = []
linux-theme-detection = ["dep:mundy", "mundy/async-io", "mundy/color-scheme"]
[dependencies] [dependencies]
iced_debug.workspace = true iced_debug.workspace = true
@ -42,3 +43,7 @@ sysinfo.optional = true
web-sys.workspace = true web-sys.workspace = true
web-sys.features = ["Document", "Window", "HtmlCanvasElement"] web-sys.features = ["Document", "Window", "HtmlCanvasElement"]
wasm-bindgen-futures.workspace = true wasm-bindgen-futures.workspace = true
[target.'cfg(target_os = "linux")'.dependencies]
mundy.workspace = true
mundy.optional = true

View file

@ -5,6 +5,7 @@
use crate::core::input_method; use crate::core::input_method;
use crate::core::keyboard; use crate::core::keyboard;
use crate::core::mouse; use crate::core::mouse;
use crate::core::theme;
use crate::core::touch; use crate::core::touch;
use crate::core::window; use crate::core::window;
use crate::core::{Event, Point, Size}; use crate::core::{Event, Point, Size};
@ -322,7 +323,7 @@ pub fn window_event(
} }
} }
/// Converts a [`window::Level`] to a [`winit`] window level. /// Converts a [`window::Level`] into a [`winit`] window level.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`winit`]: https://github.com/rust-windowing/winit
pub fn window_level(level: window::Level) -> winit::window::WindowLevel { pub fn window_level(level: window::Level) -> winit::window::WindowLevel {
@ -335,7 +336,7 @@ pub fn window_level(level: window::Level) -> winit::window::WindowLevel {
} }
} }
/// Converts a [`window::Position`] to a [`winit`] logical position for a given monitor. /// Converts a [`window::Position`] into a [`winit`] logical position for a given monitor.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`winit`]: https://github.com/rust-windowing/winit
pub fn position( pub fn position(
@ -407,7 +408,7 @@ pub fn position(
} }
} }
/// Converts a [`window::Mode`] to a [`winit`] fullscreen mode. /// Converts a [`window::Mode`] into a [`winit`] fullscreen mode.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`winit`]: https://github.com/rust-windowing/winit
pub fn fullscreen( pub fn fullscreen(
@ -422,7 +423,7 @@ pub fn fullscreen(
} }
} }
/// Converts a [`window::Mode`] to a visibility flag. /// Converts a [`window::Mode`] into a visibility flag.
pub fn visible(mode: window::Mode) -> bool { pub fn visible(mode: window::Mode) -> bool {
match mode { match mode {
window::Mode::Windowed | window::Mode::Fullscreen => true, window::Mode::Windowed | window::Mode::Fullscreen => true,
@ -430,7 +431,7 @@ pub fn visible(mode: window::Mode) -> bool {
} }
} }
/// Converts a [`winit`] fullscreen mode to a [`window::Mode`]. /// Converts a [`winit`] fullscreen mode into a [`window::Mode`].
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`winit`]: https://github.com/rust-windowing/winit
pub fn mode(mode: Option<winit::window::Fullscreen>) -> window::Mode { pub fn mode(mode: Option<winit::window::Fullscreen>) -> window::Mode {
@ -440,7 +441,28 @@ pub fn mode(mode: Option<winit::window::Fullscreen>) -> window::Mode {
} }
} }
/// Converts a [`mouse::Interaction`] to a [`winit`] cursor icon. /// Converts a [`winit`] window theme into a [`theme::Mode`].
///
/// [`winit`]: https://github.com/rust-windowing/winit
pub fn theme_mode(theme: winit::window::Theme) -> theme::Mode {
match theme {
winit::window::Theme::Light => theme::Mode::Light,
winit::window::Theme::Dark => theme::Mode::Dark,
}
}
/// Converts a [`theme::Mode`] into a window theme.
///
/// [`winit`]: https://github.com/rust-windowing/winit
pub fn window_theme(mode: theme::Mode) -> Option<winit::window::Theme> {
match mode {
theme::Mode::None => None,
theme::Mode::Light => Some(winit::window::Theme::Light),
theme::Mode::Dark => Some(winit::window::Theme::Dark),
}
}
/// Converts a [`mouse::Interaction`] into a [`winit`] cursor icon.
/// ///
/// [`winit`]: https://github.com/rust-windowing/winit /// [`winit`]: https://github.com/rust-windowing/winit
pub fn mouse_interaction( pub fn mouse_interaction(
@ -511,7 +533,7 @@ pub fn modifiers(
result result
} }
/// Converts a physical cursor position to a logical `Point`. /// Converts a physical cursor position into a logical `Point`.
pub fn cursor_position( pub fn cursor_position(
position: winit::dpi::PhysicalPosition<f64>, position: winit::dpi::PhysicalPosition<f64>,
scale_factor: f32, scale_factor: f32,
@ -1182,7 +1204,7 @@ pub fn icon(icon: window::Icon) -> Option<winit::window::Icon> {
winit::window::Icon::from_rgba(pixels, size.width, size.height).ok() winit::window::Icon::from_rgba(pixels, size.width, size.height).ok()
} }
/// Convertions some [`input_method::Purpose`] to its `winit` counterpart. /// Converts some [`input_method::Purpose`] into its `winit` counterpart.
pub fn ime_purpose( pub fn ime_purpose(
purpose: input_method::Purpose, purpose: input_method::Purpose,
) -> winit::window::ImePurpose { ) -> winit::window::ImePurpose {

View file

@ -29,9 +29,6 @@ pub use winit;
pub mod clipboard; pub mod clipboard;
pub mod conversion; pub mod conversion;
#[cfg(feature = "system")]
pub mod system;
mod error; mod error;
mod proxy; mod proxy;
mod window; mod window;
@ -53,6 +50,7 @@ use crate::futures::futures::{Future, StreamExt};
use crate::futures::subscription; use crate::futures::subscription;
use crate::futures::{Executor, Runtime}; use crate::futures::{Executor, Runtime};
use crate::graphics::{Compositor, compositor}; use crate::graphics::{Compositor, compositor};
use crate::runtime::system;
use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::{Action, Task}; use crate::runtime::{Action, Task};
@ -111,7 +109,7 @@ where
let (_id, open) = runtime::window::open(window_settings); let (_id, open) = runtime::window::open(window_settings);
open.then(move |_| task.take().unwrap_or(Task::none())) open.then(move |_| task.take().unwrap_or_else(Task::none))
} else { } else {
task task
}; };
@ -126,6 +124,7 @@ where
let (event_sender, event_receiver) = mpsc::unbounded(); let (event_sender, event_receiver) = mpsc::unbounded();
let (control_sender, control_receiver) = mpsc::unbounded(); let (control_sender, control_receiver) = mpsc::unbounded();
let (system_theme_sender, system_theme_receiver) = oneshot::channel();
let instance = Box::pin(run_instance::<P>( let instance = Box::pin(run_instance::<P>(
program, program,
@ -136,6 +135,7 @@ where
is_daemon, is_daemon,
graphics_settings, graphics_settings,
settings.fonts, settings.fonts,
system_theme_receiver,
)); ));
let context = task::Context::from_waker(task::noop_waker_ref()); let context = task::Context::from_waker(task::noop_waker_ref());
@ -147,6 +147,7 @@ where
sender: mpsc::UnboundedSender<Event<Action<Message>>>, sender: mpsc::UnboundedSender<Event<Action<Message>>>,
receiver: mpsc::UnboundedReceiver<Control>, receiver: mpsc::UnboundedReceiver<Control>,
error: Option<Error>, error: Option<Error>,
system_theme: Option<oneshot::Sender<theme::Mode>>,
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
canvas: Option<web_sys::HtmlCanvasElement>, canvas: Option<web_sys::HtmlCanvasElement>,
@ -159,6 +160,7 @@ where
sender: event_sender, sender: event_sender,
receiver: control_receiver, receiver: control_receiver,
error: None, error: None,
system_theme: Some(system_theme_sender),
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
canvas: None, canvas: None,
@ -172,10 +174,15 @@ where
Message: std::fmt::Debug, Message: std::fmt::Debug,
F: Future<Output = ()>, F: Future<Output = ()>,
{ {
fn resumed( fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
&mut self, if let Some(sender) = self.system_theme.take() {
_event_loop: &winit::event_loop::ActiveEventLoop, let _ = sender.send(
) { event_loop
.system_theme()
.map(conversion::theme_mode)
.unwrap_or_default(),
);
}
} }
fn new_events( fn new_events(
@ -498,6 +505,7 @@ async fn run_instance<P>(
is_daemon: bool, is_daemon: bool,
graphics_settings: graphics::Settings, graphics_settings: graphics::Settings,
default_fonts: Vec<Cow<'static, [u8]>>, default_fonts: Vec<Cow<'static, [u8]>>,
mut _system_theme: oneshot::Receiver<theme::Mode>,
) where ) where
P: Program + 'static, P: Program + 'static,
P::Theme: theme::Base, P::Theme: theme::Base,
@ -517,6 +525,38 @@ async fn run_instance<P>(
let mut user_interfaces = ManuallyDrop::new(FxHashMap::default()); let mut user_interfaces = ManuallyDrop::new(FxHashMap::default());
let mut clipboard = Clipboard::unconnected(); let mut clipboard = Clipboard::unconnected();
#[cfg(all(feature = "linux-theme-detection", target_os = "linux"))]
let mut system_theme = {
let to_mode = |color_scheme| match color_scheme {
mundy::ColorScheme::NoPreference => theme::Mode::None,
mundy::ColorScheme::Light => theme::Mode::Light,
mundy::ColorScheme::Dark => theme::Mode::Dark,
};
runtime.run(
mundy::Preferences::stream(mundy::Interest::ColorScheme)
.map(move |preferences| {
Action::System(system::Action::NotifyTheme(to_mode(
preferences.color_scheme,
)))
})
.boxed(),
);
mundy::Preferences::once_blocking(
mundy::Interest::ColorScheme,
core::time::Duration::from_millis(200),
)
.map(|preferences| to_mode(preferences.color_scheme))
.unwrap_or_default()
};
#[cfg(not(all(feature = "linux-theme-detection", target_os = "linux")))]
let mut system_theme =
_system_theme.try_recv().ok().flatten().unwrap_or_default();
log::info!("System theme: {system_theme:?}");
loop { loop {
// Empty the queue if possible // Empty the queue if possible
let event = if let Ok(event) = event_receiver.try_next() { let event = if let Ok(event) = event_receiver.try_next() {
@ -595,14 +635,7 @@ async fn run_instance<P>(
} }
} }
debug::theme_changed(|| { let is_first = window_manager.is_empty();
if window_manager.is_empty() {
theme::Base::palette(&program.theme(id))
} else {
None
}
});
let window = window_manager.insert( let window = window_manager.insert(
id, id,
window, window,
@ -611,8 +644,21 @@ async fn run_instance<P>(
.as_mut() .as_mut()
.expect("Compositor must be initialized"), .expect("Compositor must be initialized"),
exit_on_close_request, exit_on_close_request,
system_theme,
); );
window.raw.set_theme(conversion::window_theme(
window.state.theme_mode(),
));
debug::theme_changed(|| {
if is_first {
theme::Base::palette(window.state.theme())
} else {
None
}
});
let logical_size = window.state.logical_size(); let logical_size = window.state.logical_size();
let _ = user_interfaces.insert( let _ = user_interfaces.insert(
@ -695,6 +741,7 @@ async fn run_instance<P>(
run_action( run_action(
action, action,
&program, &program,
&mut runtime,
&mut compositor, &mut compositor,
&mut events, &mut events,
&mut messages, &mut messages,
@ -704,6 +751,7 @@ async fn run_instance<P>(
&mut window_manager, &mut window_manager,
&mut ui_caches, &mut ui_caches,
&mut is_window_opening, &mut is_window_opening,
&mut system_theme,
); );
actions += 1; actions += 1;
} }
@ -862,11 +910,24 @@ async fn run_instance<P>(
continue; continue;
}; };
if matches!( match window_event {
window_event, winit::event::WindowEvent::Resized(_) => {
winit::event::WindowEvent::Resized(_) window.raw.request_redraw();
) { }
window.raw.request_redraw(); winit::event::WindowEvent::ThemeChanged(theme) => {
let mode = conversion::theme_mode(theme);
if mode != system_theme {
system_theme = mode;
runtime.broadcast(
subscription::Event::SystemThemeChanged(
mode,
),
);
}
}
_ => {}
} }
if matches!( if matches!(
@ -879,6 +940,7 @@ async fn run_instance<P>(
id, id,
)), )),
&program, &program,
&mut runtime,
&mut compositor, &mut compositor,
&mut events, &mut events,
&mut messages, &mut messages,
@ -888,9 +950,14 @@ async fn run_instance<P>(
&mut window_manager, &mut window_manager,
&mut ui_caches, &mut ui_caches,
&mut is_window_opening, &mut is_window_opening,
&mut system_theme,
); );
} else { } else {
window.state.update(&window.raw, &window_event); window.state.update(
&program,
&window.raw,
&window_event,
);
if let Some(event) = conversion::window_event( if let Some(event) = conversion::window_event(
window_event, window_event,
@ -1095,6 +1162,7 @@ fn update<P: Program, E: Executor>(
fn run_action<'a, P, C>( fn run_action<'a, P, C>(
action: Action<P::Message>, action: Action<P::Message>,
program: &'a program::Instance<P>, program: &'a program::Instance<P>,
runtime: &mut Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>,
compositor: &mut Option<C>, compositor: &mut Option<C>,
events: &mut Vec<(window::Id, core::Event)>, events: &mut Vec<(window::Id, core::Event)>,
messages: &mut Vec<P::Message>, messages: &mut Vec<P::Message>,
@ -1107,13 +1175,13 @@ fn run_action<'a, P, C>(
window_manager: &mut WindowManager<P, C>, window_manager: &mut WindowManager<P, C>,
ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>, ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>,
is_window_opening: &mut bool, is_window_opening: &mut bool,
system_theme: &mut theme::Mode,
) where ) where
P: Program, P: Program,
C: Compositor<Renderer = P::Renderer> + 'static, C: Compositor<Renderer = P::Renderer> + 'static,
P::Theme: theme::Base, P::Theme: theme::Base,
{ {
use crate::runtime::clipboard; use crate::runtime::clipboard;
use crate::runtime::system;
use crate::runtime::window; use crate::runtime::window;
match action { match action {
@ -1421,21 +1489,44 @@ fn run_action<'a, P, C>(
} }
}, },
Action::System(action) => match action { Action::System(action) => match action {
system::Action::QueryInformation(_channel) => { system::Action::GetInformation(_channel) => {
#[cfg(feature = "system")] #[cfg(feature = "sysinfo")]
{ {
if let Some(compositor) = compositor { if let Some(compositor) = compositor {
let graphics_info = compositor.fetch_information(); let graphics_info = compositor.information();
let _ = std::thread::spawn(move || { let _ = std::thread::spawn(move || {
let information = let information = system_information(graphics_info);
crate::system::information(graphics_info);
let _ = _channel.send(information); let _ = _channel.send(information);
}); });
} }
} }
} }
system::Action::GetTheme(channel) => {
let _ = channel.send(*system_theme);
}
system::Action::NotifyTheme(mode) => {
if mode != *system_theme {
*system_theme = mode;
runtime.broadcast(subscription::Event::SystemThemeChanged(
mode,
));
}
let Some(theme) = conversion::window_theme(mode) else {
return;
};
for (_id, window) in window_manager.iter_mut() {
window.state.update(
program,
&window.raw,
&winit::event::WindowEvent::ThemeChanged(theme),
);
}
}
}, },
Action::Widget(operation) => { Action::Widget(operation) => {
let mut current_operation = Some(operation); let mut current_operation = Some(operation);
@ -1544,3 +1635,37 @@ pub fn user_force_quit(
_ => false, _ => false,
} }
} }
#[cfg(feature = "sysinfo")]
fn system_information(
graphics: compositor::Information,
) -> system::Information {
use sysinfo::{Process, System};
let mut system = System::new_all();
system.refresh_all();
let cpu_brand = system
.cpus()
.first()
.map(|cpu| cpu.brand().to_string())
.unwrap_or_default();
let memory_used = sysinfo::get_current_pid()
.and_then(|pid| system.process(pid).ok_or("Process not found"))
.map(Process::memory)
.ok();
system::Information {
system_name: System::name(),
system_kernel: System::kernel_version(),
system_version: System::long_os_version(),
system_short_version: System::os_version(),
cpu_brand,
cpu_cores: system.physical_core_count(),
memory_total: system.total_memory(),
memory_used,
graphics_adapter: graphics.adapter,
graphics_backend: graphics.backend,
}
}

View file

@ -1,43 +0,0 @@
//! Access the native system.
use crate::graphics::compositor;
use crate::runtime::system::{Action, Information};
use crate::runtime::{self, Task};
/// Query for available system information.
pub fn fetch_information() -> Task<Information> {
runtime::task::oneshot(|channel| {
runtime::Action::System(Action::QueryInformation(channel))
})
}
pub(crate) fn information(
graphics_info: compositor::Information,
) -> Information {
use sysinfo::{Process, System};
let mut system = System::new_all();
system.refresh_all();
let cpu_brand = system
.cpus()
.first()
.map(|cpu| cpu.brand().to_string())
.unwrap_or_default();
let memory_used = sysinfo::get_current_pid()
.and_then(|pid| system.process(pid).ok_or("Process not found"))
.map(Process::memory)
.ok();
Information {
system_name: System::name(),
system_kernel: System::kernel_version(),
system_version: System::long_os_version(),
system_short_version: System::os_version(),
cpu_brand,
cpu_cores: system.physical_core_count(),
memory_total: system.total_memory(),
memory_used,
graphics_adapter: graphics_info.adapter,
graphics_backend: graphics_info.backend,
}
}

View file

@ -55,8 +55,9 @@ where
program: &program::Instance<P>, program: &program::Instance<P>,
compositor: &mut C, compositor: &mut C,
exit_on_close_request: bool, exit_on_close_request: bool,
system_theme: theme::Mode,
) -> &mut Window<P, C> { ) -> &mut Window<P, C> {
let state = State::new(program, id, &window); let state = State::new(program, id, &window, system_theme);
let viewport_version = state.viewport_version(); let viewport_version = state.viewport_version();
let physical_size = state.physical_size(); let physical_size = state.physical_size();
let surface = compositor.create_surface( let surface = compositor.create_surface(

View file

@ -9,7 +9,7 @@ use winit::window::Window;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
/// The state of a multi-windowed [`Program`]. /// The state of the window of a [`Program`].
pub struct State<P: Program> pub struct State<P: Program>
where where
P::Theme: theme::Base, P::Theme: theme::Base,
@ -20,7 +20,9 @@ where
viewport_version: u64, viewport_version: u64,
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
modifiers: winit::keyboard::ModifiersState, modifiers: winit::keyboard::ModifiersState,
theme: P::Theme, theme: Option<P::Theme>,
theme_mode: theme::Mode,
default_theme: P::Theme,
style: theme::Style, style: theme::Style,
} }
@ -29,7 +31,7 @@ where
P::Theme: theme::Base, P::Theme: theme::Base,
{ {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("multi_window::State") f.debug_struct("window::State")
.field("title", &self.title) .field("title", &self.title)
.field("scale_factor", &self.scale_factor) .field("scale_factor", &self.scale_factor)
.field("viewport", &self.viewport) .field("viewport", &self.viewport)
@ -49,11 +51,15 @@ where
program: &program::Instance<P>, program: &program::Instance<P>,
window_id: window::Id, window_id: window::Id,
window: &Window, window: &Window,
system_theme: theme::Mode,
) -> Self { ) -> Self {
let title = program.title(window_id); let title = program.title(window_id);
let scale_factor = program.scale_factor(window_id); let scale_factor = program.scale_factor(window_id);
let theme = program.theme(window_id); let theme = program.theme(window_id);
let style = program.style(&theme); let theme_mode =
theme.as_ref().map(theme::Base::mode).unwrap_or_default();
let default_theme = <P::Theme as theme::Base>::default(system_theme);
let style = program.style(theme.as_ref().unwrap_or(&default_theme));
let viewport = { let viewport = {
let physical_size = window.inner_size(); let physical_size = window.inner_size();
@ -72,6 +78,8 @@ where
cursor_position: None, cursor_position: None,
modifiers: winit::keyboard::ModifiersState::default(), modifiers: winit::keyboard::ModifiersState::default(),
theme, theme,
theme_mode,
default_theme,
style, style,
} }
} }
@ -123,7 +131,12 @@ where
/// Returns the current theme of the [`State`]. /// Returns the current theme of the [`State`].
pub fn theme(&self) -> &P::Theme { pub fn theme(&self) -> &P::Theme {
&self.theme self.theme.as_ref().unwrap_or(&self.default_theme)
}
/// Returns the current [`theme::Mode`] of the [`State`].
pub fn theme_mode(&self) -> theme::Mode {
self.theme_mode
} }
/// Returns the current background [`Color`] of the [`State`]. /// Returns the current background [`Color`] of the [`State`].
@ -137,7 +150,12 @@ where
} }
/// Processes the provided window event and updates the [`State`] accordingly. /// Processes the provided window event and updates the [`State`] accordingly.
pub fn update(&mut self, window: &Window, event: &WindowEvent) { pub fn update(
&mut self,
program: &program::Instance<P>,
window: &Window,
event: &WindowEvent,
) {
match event { match event {
WindowEvent::Resized(new_size) => { WindowEvent::Resized(new_size) => {
let size = Size::new(new_size.width, new_size.height); let size = Size::new(new_size.width, new_size.height);
@ -174,6 +192,16 @@ where
WindowEvent::ModifiersChanged(new_modifiers) => { WindowEvent::ModifiersChanged(new_modifiers) => {
self.modifiers = new_modifiers.state(); self.modifiers = new_modifiers.state();
} }
WindowEvent::ThemeChanged(theme) => {
self.default_theme = <P::Theme as theme::Base>::default(
conversion::theme_mode(*theme),
);
if self.theme.is_none() {
self.style = program.style(&self.default_theme);
window.request_redraw();
}
}
_ => {} _ => {}
} }
} }
@ -182,7 +210,7 @@ where
/// window. /// window.
/// ///
/// Normally, a [`Program`] should be synchronized with its [`State`] /// Normally, a [`Program`] should be synchronized with its [`State`]
/// and window after calling [`State::update`]. /// and window after calling [`Program::update`].
pub fn synchronize( pub fn synchronize(
&mut self, &mut self,
program: &program::Instance<P>, program: &program::Instance<P>,
@ -217,6 +245,45 @@ where
// Update theme and appearance // Update theme and appearance
self.theme = program.theme(window_id); self.theme = program.theme(window_id);
self.style = program.style(&self.theme); self.style = program.style(self.theme());
let new_mode = self
.theme
.as_ref()
.map(theme::Base::mode)
.unwrap_or_default();
if self.theme_mode != new_mode {
#[cfg(not(target_os = "linux"))]
{
window.set_theme(conversion::window_theme(new_mode));
// Assume the old mode matches the system one
// We will be notified otherwise
if new_mode == theme::Mode::None {
self.default_theme =
<P::Theme as theme::Base>::default(self.theme_mode);
if self.theme.is_none() {
self.style = program.style(&self.default_theme);
}
}
}
#[cfg(target_os = "linux")]
{
// mundy always notifies system theme changes, so we
// just restore the default theme mode.
let new_mode = if new_mode == theme::Mode::None {
theme::Base::mode(&self.default_theme)
} else {
new_mode
};
window.set_theme(conversion::window_theme(new_mode));
}
self.theme_mode = new_mode;
}
} }
} }