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

View file

@ -14,7 +14,6 @@ keywords.workspace = true
workspace = true
[features]
auto-detect-theme = ["dep:dark-light"]
advanced = []
crisp = []
basic-shaping = []
@ -32,9 +31,6 @@ smol_str.workspace = true
thiserror.workspace = true
web-time.workspace = true
dark-light.workspace = true
dark-light.optional = true
serde.workspace = true
serde.optional = true
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 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
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.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Style {
@ -268,7 +255,13 @@ pub struct Style {
/// The default blank style of a theme.
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;
/// Returns the color [`Palette`] of the theme.
@ -280,6 +273,39 @@ pub trait Base {
}
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 {
default(self)
}

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ impl Events {
}
Message::EventOccurred(event) => {
if let Event::Window(window::Event::CloseRequested) = event {
window::get_latest().and_then(window::close)
window::latest().and_then(window::close)
} else {
Task::none()
}
@ -47,7 +47,7 @@ impl Events {
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 {
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Confirm => window::get_latest().and_then(window::close),
Message::Confirm => window::latest().and_then(window::close),
Message::Exit => {
self.show_confirm = true;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
use iced::system;
use iced::widget::{button, center, column, text};
use iced::{Element, Task, system};
use iced::{Element, Task};
pub fn main() -> iced::Result {
iced::application(Example::new, Example::update, Example::view).run()
@ -26,7 +27,7 @@ impl Example {
fn new() -> (Self, Task<Message>) {
(
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 {
iced::application(Table::new, Table::update, Table::view)
.theme(|_| Theme::CatppuccinMocha)
.theme(Theme::CatppuccinMocha)
.run()
}

View file

@ -1 +1 @@
99f418007af163f172e163565f166da31015521e1bf7de95fa55cda2fb5a7db5
0acb67235c6a11014a2d2b825e0a70069bca0c67bee0cdb38a0144fc72b25220

View file

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

View file

@ -1,11 +1,10 @@
use iced::border;
use iced::widget::{Button, Column, Container, Slider};
use iced::widget::{
button, center_x, center_y, checkbox, column, horizontal_space, image,
radio, rich_text, row, scrollable, slider, span, text, text_input, toggler,
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 {
#[cfg(target_arch = "wasm32")]
@ -201,7 +200,7 @@ impl Tour {
Self::container("Welcome!")
.push(
"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(
"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, \
Metal, DX11, and DX12.",
)
.push({
let theme = Theme::default();
let palette = theme.extended_palette();
.push(
rich_text![
"Additionally, this tour can also run on WebAssembly ",
"by leveraging ",
span("trunk")
.color(palette.primary.base.color)
.background(palette.background.weakest.color)
.border(
border::rounded(2)
.width(1)
.color(palette.background.weak.color)
)
.padding([0, 2])
.color(color!(0x7777FF))
.underline(true)
.font(Font::MONOSPACE)
.link(Message::OpenTrunk),
"."
]
.on_link_click(std::convert::identity)
})
.on_link_click(std::convert::identity),
)
.push(
"You will need to interact with the UI in order to reach \
the end!",

View file

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

View file

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

View file

@ -37,6 +37,7 @@ where
event: Event::Window(window::Event::RedrawRequested(_)),
..
}
| subscription::Event::SystemThemeChanged(_)
| subscription::Event::PlatformSpecific(_) => None,
subscription::Event::Interaction {
window,
@ -66,7 +67,8 @@ where
event,
status,
} => 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;
use crate::core::event;
use crate::core::theme;
use crate::core::window;
use crate::futures::Stream;
use crate::{BoxStream, MaybeSend};
@ -27,6 +28,9 @@ pub enum Event {
status: event::Status,
},
/// The system theme has changed.
SystemThemeChanged(theme::Mode),
/// A platform specific event.
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
I: Hash + 'static,
F: Fn(Event) -> Option<T> + MaybeSend + 'static,

View file

@ -59,7 +59,7 @@ pub trait Compositor: Sized {
);
/// Returns [`Information`] used by this [`Compositor`].
fn fetch_information(&self) -> Information;
fn information(&self) -> Information;
/// Loads a font from its bytes.
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 fetch_information(&self) -> Information {
fn information(&self) -> Information {
Information {
adapter: String::from("Null Renderer"),
backend: String::from("Null"),

View file

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

View file

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

View file

@ -1,11 +1,20 @@
//! Access the native system.
use crate::core::theme;
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.
#[derive(Debug)]
pub enum Action {
/// Query system information and produce `T` with the result.
QueryInformation(oneshot::Sender<Information>),
/// Send available system 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).
@ -37,3 +46,29 @@ pub struct Information {
/// Model information for the active graphics adapter
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.
pub fn get_oldest() -> Task<Option<Id>> {
pub fn oldest() -> Task<Option<Id>> {
task::oneshot(|channel| crate::Action::Window(Action::GetOldest(channel)))
}
/// 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)))
}
@ -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.
pub fn get_size(id: Id) -> Task<Size> {
pub fn size(id: Id) -> Task<Size> {
task::oneshot(move |channel| {
crate::Action::Window(Action::GetSize(id, channel))
})
}
/// 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| {
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`].
pub fn get_minimized(id: Id) -> Task<Option<bool>> {
pub fn is_minimized(id: Id) -> Task<Option<bool>> {
task::oneshot(move |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`].
pub fn get_position(id: Id) -> Task<Option<Point>> {
pub fn position(id: Id) -> Task<Option<Point>> {
task::oneshot(move |channel| {
crate::Action::Window(Action::GetPosition(id, channel))
})
}
/// 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| {
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.
pub fn get_mode(id: Id) -> Task<Mode> {
pub fn mode(id: Id) -> Task<Mode> {
task::oneshot(move |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
/// 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| {
crate::Action::Window(Action::GetRawId(id, channel))
})

View file

@ -7,7 +7,7 @@
//!
//! pub fn main() -> iced::Result {
//! iced::application(u64::default, update, view)
//! .theme(|_| Theme::Dark)
//! .theme(Theme::Dark)
//! .centered()
//! .run()
//! }
@ -35,7 +35,7 @@ use crate::shell;
use crate::theme;
use crate::window;
use crate::{
Element, Executor, Font, Result, Settings, Size, Subscription, Task,
Element, Executor, Font, Result, Settings, Size, Subscription, Task, Theme,
};
use iced_debug as debug;
@ -75,14 +75,14 @@ pub use timed::timed;
/// }
/// ```
pub fn application<State, Message, Theme, Renderer>(
boot: impl Boot<State, Message>,
update: impl Update<State, Message>,
view: impl for<'a> View<'a, State, Message, Theme, Renderer>,
boot: impl BootFn<State, Message>,
update: impl UpdateFn<State, Message>,
view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
) -> Application<impl Program<State = State, Message = Message, Theme = Theme>>
where
State: 'static,
Message: program::Message + 'static,
Theme: Default + theme::Base,
Theme: theme::Base,
Renderer: program::Renderer,
{
use std::marker::PhantomData;
@ -101,11 +101,11 @@ where
for Instance<State, Message, Theme, Renderer, Boot, Update, View>
where
Message: program::Message + 'static,
Theme: Default + theme::Base,
Theme: theme::Base,
Renderer: program::Renderer,
Boot: self::Boot<State, Message>,
Update: self::Update<State, Message>,
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
Boot: self::BootFn<State, Message>,
Update: self::UpdateFn<State, Message>,
View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
{
type State = State;
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(
self,
title: impl Title<P::State>,
title: impl TitleFn<P::State>,
) -> Application<
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`].
pub fn theme(
self,
f: impl Fn(&P::State) -> P::Theme,
f: impl ThemeFn<P::State, P::Theme>,
) -> Application<
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
> {
Application {
raw: program::with_theme(self.raw, move |state, _window| {
debug::hot(|| f(state))
debug::hot(|| f.theme(state))
}),
settings: self.settings,
window: self.window,
@ -425,12 +425,12 @@ impl<P: Program> Application<P> {
/// In practice, this means that [`application`] can both take
/// simple functions like `State::default` and more advanced ones
/// that return a [`Task`].
pub trait Boot<State, Message> {
pub trait BootFn<State, Message> {
/// Initializes the [`Application`] state.
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
T: Fn() -> C,
C: IntoBoot<State, Message>,
@ -464,18 +464,18 @@ impl<State, Message> IntoBoot<State, Message> for (State, Task<Message>) {
/// any closure `Fn(&State) -> String`.
///
/// 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`].
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 {
self.to_string()
}
}
impl<T, State> Title<State> for T
impl<T, State> TitleFn<State> for T
where
T: Fn(&State) -> String,
{
@ -488,18 +488,18 @@ where
///
/// This trait allows the [`application`] builder to take any closure that
/// 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`].
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> {
Task::none()
}
}
impl<T, State, Message, C> Update<State, Message> for T
impl<T, State, Message, C> UpdateFn<State, Message> for T
where
T: Fn(&mut State, Message) -> C,
C: Into<Task<Message>>,
@ -513,13 +513,13 @@ where
///
/// This trait allows the [`application`] builder to take any closure that
/// 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`].
fn view(&self, state: &'a State) -> Element<'a, Message, Theme, Renderer>;
}
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
T: Fn(&'a State) -> Widget,
State: 'static,
@ -529,3 +529,35 @@ where
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.
use crate::application::{Application, Boot, View};
use crate::application::{Application, BootFn, ViewFn};
use crate::program;
use crate::theme;
use crate::time::Instant;
@ -20,17 +20,17 @@ use iced_debug as debug;
///
/// [`comet`]: https://github.com/iced-rs/comet
pub fn timed<State, Message, Theme, Renderer>(
boot: impl Boot<State, Message>,
update: impl Update<State, Message>,
boot: impl BootFn<State, Message>,
update: impl UpdateFn<State, 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<
impl Program<State = State, Message = (Message, Instant), Theme = Theme>,
>
where
State: 'static,
Message: program::Message + 'static,
Theme: Default + theme::Base + 'static,
Theme: theme::Base + 'static,
Renderer: program::Renderer + 'static,
{
use std::marker::PhantomData;
@ -69,12 +69,12 @@ where
>
where
Message: program::Message + 'static,
Theme: Default + theme::Base + 'static,
Theme: theme::Base + 'static,
Renderer: program::Renderer + 'static,
Boot: self::Boot<State, Message>,
Update: self::Update<State, Message>,
Boot: self::BootFn<State, Message>,
Update: self::UpdateFn<State, 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 Message = (Message, Instant);
@ -148,9 +148,9 @@ where
/// 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`].
pub trait Update<State, Message> {
pub trait UpdateFn<State, Message> {
/// Processes the message and updates the state of the [`Application`].
fn update(
&self,
@ -160,7 +160,7 @@ pub trait Update<State, Message> {
) -> impl Into<Task<Message>>;
}
impl<State, Message> Update<State, Message> for () {
impl<State, Message> UpdateFn<State, Message> for () {
fn update(
&self,
_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
T: Fn(&mut State, Message, Instant) -> C,
C: Into<Task<Message>>,

View file

@ -4,7 +4,9 @@ use crate::program::{self, Program};
use crate::shell;
use crate::theme;
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;
@ -21,14 +23,14 @@ use std::borrow::Cow;
///
/// [`exit`]: crate::exit
pub fn daemon<State, Message, Theme, Renderer>(
boot: impl application::Boot<State, Message>,
update: impl application::Update<State, Message>,
view: impl for<'a> View<'a, State, Message, Theme, Renderer>,
boot: impl application::BootFn<State, Message>,
update: impl application::UpdateFn<State, Message>,
view: impl for<'a> ViewFn<'a, State, Message, Theme, Renderer>,
) -> Daemon<impl Program<State = State, Message = Message, Theme = Theme>>
where
State: 'static,
Message: program::Message + 'static,
Theme: Default + theme::Base,
Theme: theme::Base,
Renderer: program::Renderer,
{
use std::marker::PhantomData;
@ -47,11 +49,11 @@ where
for Instance<State, Message, Theme, Renderer, Boot, Update, View>
where
Message: program::Message + 'static,
Theme: Default + theme::Base,
Theme: theme::Base,
Renderer: program::Renderer,
Boot: application::Boot<State, Message>,
Update: application::Update<State, Message>,
View: for<'a> self::View<'a, State, Message, Theme, Renderer>,
Boot: application::BootFn<State, Message>,
Update: application::UpdateFn<State, Message>,
View: for<'a> self::ViewFn<'a, State, Message, Theme, Renderer>,
{
type State = State;
type Message = Message;
@ -169,10 +171,10 @@ impl<P: Program> Daemon<P> {
self
}
/// Sets the [`Title`] of the [`Daemon`].
/// Sets the title of the [`Daemon`].
pub fn title(
self,
title: impl Title<P::State>,
title: impl TitleFn<P::State>,
) -> Daemon<
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`].
pub fn theme(
self,
f: impl Fn(&P::State, window::Id) -> P::Theme,
f: impl ThemeFn<P::State, P::Theme>,
) -> Daemon<
impl Program<State = P::State, Message = P::Message, Theme = P::Theme>,
> {
Daemon {
raw: program::with_theme(self.raw, move |state, window| {
debug::hot(|| f(state, window))
debug::hot(|| f.theme(state, window))
}),
settings: self.settings,
}
@ -266,18 +268,18 @@ impl<P: Program> Daemon<P> {
/// any closure `Fn(&State, window::Id) -> String`.
///
/// 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`].
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 {
self.to_string()
}
}
impl<T, State> Title<State> for T
impl<T, State> TitleFn<State> for T
where
T: Fn(&State, window::Id) -> String,
{
@ -290,7 +292,7 @@ where
///
/// This trait allows the [`daemon`] builder to take any closure that
/// 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`].
fn view(
&self,
@ -300,7 +302,7 @@ pub trait View<'a, State, Message, Theme, Renderer> {
}
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
T: Fn(&'a State, window::Id) -> Widget,
State: 'static,
@ -314,3 +316,35 @@ where
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 {
//! Retrieve system information.
pub use crate::runtime::system::Information;
pub use crate::shell::system::*;
pub use crate::runtime::system::{theme, theme_changes};
#[cfg(feature = "sysinfo")]
pub use crate::runtime::system::{Information, information};
}
pub mod overlay {
@ -691,14 +692,14 @@ pub type Result = std::result::Result<(), Error>;
/// }
/// ```
pub fn run<State, Message, Theme, Renderer>(
update: impl application::Update<State, Message> + 'static,
view: impl for<'a> application::View<'a, State, Message, Theme, Renderer>
update: impl application::UpdateFn<State, Message> + 'static,
view: impl for<'a> application::ViewFn<'a, State, Message, Theme, Renderer>
+ 'static,
) -> Result
where
State: Default + 'static,
Message: program::Message + 'static,
Theme: Default + theme::Base + 'static,
Theme: theme::Base + 'static,
Renderer: program::Renderer + 'static,
{
application(State::default, update, view).run()

View file

@ -494,10 +494,13 @@ impl Snapshot {
if path.exists() {
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 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)?;
Ok(self.screenshot.bytes == bytes[..info.buffer_size()])

View file

@ -100,7 +100,7 @@ impl crate::graphics::Compositor for Compositor {
surface.layer_stack.clear();
}
fn fetch_information(&self) -> Information {
fn information(&self) -> Information {
Information {
adapter: String::from("CPU"),
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();
compositor::Information {

View file

@ -2063,7 +2063,7 @@ where
/// A widget that applies any `Theme` to its contents.
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>>,
) -> Themer<
'a,
@ -2077,7 +2077,7 @@ where
Renderer: core::Renderer,
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.

View file

@ -16,13 +16,14 @@ workspace = true
[features]
default = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"]
debug = ["iced_debug/enable"]
system = ["sysinfo"]
sysinfo = ["dep:sysinfo"]
program = []
x11 = ["winit/x11"]
wayland = ["winit/wayland"]
wayland-dlopen = ["winit/wayland-dlopen"]
wayland-csd-adwaita = ["winit/wayland-csd-adwaita"]
unconditional-rendering = []
linux-theme-detection = ["dep:mundy", "mundy/async-io", "mundy/color-scheme"]
[dependencies]
iced_debug.workspace = true
@ -42,3 +43,7 @@ sysinfo.optional = true
web-sys.workspace = true
web-sys.features = ["Document", "Window", "HtmlCanvasElement"]
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::keyboard;
use crate::core::mouse;
use crate::core::theme;
use crate::core::touch;
use crate::core::window;
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
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
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
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 {
match mode {
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
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
pub fn mouse_interaction(
@ -511,7 +533,7 @@ pub fn modifiers(
result
}
/// Converts a physical cursor position to a logical `Point`.
/// Converts a physical cursor position into a logical `Point`.
pub fn cursor_position(
position: winit::dpi::PhysicalPosition<f64>,
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()
}
/// Convertions some [`input_method::Purpose`] to its `winit` counterpart.
/// Converts some [`input_method::Purpose`] into its `winit` counterpart.
pub fn ime_purpose(
purpose: input_method::Purpose,
) -> winit::window::ImePurpose {

View file

@ -29,9 +29,6 @@ pub use winit;
pub mod clipboard;
pub mod conversion;
#[cfg(feature = "system")]
pub mod system;
mod error;
mod proxy;
mod window;
@ -53,6 +50,7 @@ use crate::futures::futures::{Future, StreamExt};
use crate::futures::subscription;
use crate::futures::{Executor, Runtime};
use crate::graphics::{Compositor, compositor};
use crate::runtime::system;
use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::{Action, Task};
@ -111,7 +109,7 @@ where
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 {
task
};
@ -126,6 +124,7 @@ where
let (event_sender, event_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>(
program,
@ -136,6 +135,7 @@ where
is_daemon,
graphics_settings,
settings.fonts,
system_theme_receiver,
));
let context = task::Context::from_waker(task::noop_waker_ref());
@ -147,6 +147,7 @@ where
sender: mpsc::UnboundedSender<Event<Action<Message>>>,
receiver: mpsc::UnboundedReceiver<Control>,
error: Option<Error>,
system_theme: Option<oneshot::Sender<theme::Mode>>,
#[cfg(target_arch = "wasm32")]
canvas: Option<web_sys::HtmlCanvasElement>,
@ -159,6 +160,7 @@ where
sender: event_sender,
receiver: control_receiver,
error: None,
system_theme: Some(system_theme_sender),
#[cfg(target_arch = "wasm32")]
canvas: None,
@ -172,10 +174,15 @@ where
Message: std::fmt::Debug,
F: Future<Output = ()>,
{
fn resumed(
&mut self,
_event_loop: &winit::event_loop::ActiveEventLoop,
) {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
if let Some(sender) = self.system_theme.take() {
let _ = sender.send(
event_loop
.system_theme()
.map(conversion::theme_mode)
.unwrap_or_default(),
);
}
}
fn new_events(
@ -498,6 +505,7 @@ async fn run_instance<P>(
is_daemon: bool,
graphics_settings: graphics::Settings,
default_fonts: Vec<Cow<'static, [u8]>>,
mut _system_theme: oneshot::Receiver<theme::Mode>,
) where
P: Program + 'static,
P::Theme: theme::Base,
@ -517,6 +525,38 @@ async fn run_instance<P>(
let mut user_interfaces = ManuallyDrop::new(FxHashMap::default());
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 {
// Empty the queue if possible
let event = if let Ok(event) = event_receiver.try_next() {
@ -595,14 +635,7 @@ async fn run_instance<P>(
}
}
debug::theme_changed(|| {
if window_manager.is_empty() {
theme::Base::palette(&program.theme(id))
} else {
None
}
});
let is_first = window_manager.is_empty();
let window = window_manager.insert(
id,
window,
@ -611,8 +644,21 @@ async fn run_instance<P>(
.as_mut()
.expect("Compositor must be initialized"),
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 _ = user_interfaces.insert(
@ -695,6 +741,7 @@ async fn run_instance<P>(
run_action(
action,
&program,
&mut runtime,
&mut compositor,
&mut events,
&mut messages,
@ -704,6 +751,7 @@ async fn run_instance<P>(
&mut window_manager,
&mut ui_caches,
&mut is_window_opening,
&mut system_theme,
);
actions += 1;
}
@ -862,11 +910,24 @@ async fn run_instance<P>(
continue;
};
if matches!(
window_event,
winit::event::WindowEvent::Resized(_)
) {
window.raw.request_redraw();
match window_event {
winit::event::WindowEvent::Resized(_) => {
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!(
@ -879,6 +940,7 @@ async fn run_instance<P>(
id,
)),
&program,
&mut runtime,
&mut compositor,
&mut events,
&mut messages,
@ -888,9 +950,14 @@ async fn run_instance<P>(
&mut window_manager,
&mut ui_caches,
&mut is_window_opening,
&mut system_theme,
);
} else {
window.state.update(&window.raw, &window_event);
window.state.update(
&program,
&window.raw,
&window_event,
);
if let Some(event) = conversion::window_event(
window_event,
@ -1095,6 +1162,7 @@ fn update<P: Program, E: Executor>(
fn run_action<'a, P, C>(
action: Action<P::Message>,
program: &'a program::Instance<P>,
runtime: &mut Runtime<P::Executor, Proxy<P::Message>, Action<P::Message>>,
compositor: &mut Option<C>,
events: &mut Vec<(window::Id, core::Event)>,
messages: &mut Vec<P::Message>,
@ -1107,13 +1175,13 @@ fn run_action<'a, P, C>(
window_manager: &mut WindowManager<P, C>,
ui_caches: &mut FxHashMap<window::Id, user_interface::Cache>,
is_window_opening: &mut bool,
system_theme: &mut theme::Mode,
) where
P: Program,
C: Compositor<Renderer = P::Renderer> + 'static,
P::Theme: theme::Base,
{
use crate::runtime::clipboard;
use crate::runtime::system;
use crate::runtime::window;
match action {
@ -1421,21 +1489,44 @@ fn run_action<'a, P, C>(
}
},
Action::System(action) => match action {
system::Action::QueryInformation(_channel) => {
#[cfg(feature = "system")]
system::Action::GetInformation(_channel) => {
#[cfg(feature = "sysinfo")]
{
if let Some(compositor) = compositor {
let graphics_info = compositor.fetch_information();
let graphics_info = compositor.information();
let _ = std::thread::spawn(move || {
let information =
crate::system::information(graphics_info);
let information = system_information(graphics_info);
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) => {
let mut current_operation = Some(operation);
@ -1544,3 +1635,37 @@ pub fn user_force_quit(
_ => 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>,
compositor: &mut C,
exit_on_close_request: bool,
system_theme: theme::Mode,
) -> &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 physical_size = state.physical_size();
let surface = compositor.create_surface(

View file

@ -9,7 +9,7 @@ use winit::window::Window;
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>
where
P::Theme: theme::Base,
@ -20,7 +20,9 @@ where
viewport_version: u64,
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
modifiers: winit::keyboard::ModifiersState,
theme: P::Theme,
theme: Option<P::Theme>,
theme_mode: theme::Mode,
default_theme: P::Theme,
style: theme::Style,
}
@ -29,7 +31,7 @@ where
P::Theme: theme::Base,
{
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("scale_factor", &self.scale_factor)
.field("viewport", &self.viewport)
@ -49,11 +51,15 @@ where
program: &program::Instance<P>,
window_id: window::Id,
window: &Window,
system_theme: theme::Mode,
) -> Self {
let title = program.title(window_id);
let scale_factor = program.scale_factor(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 physical_size = window.inner_size();
@ -72,6 +78,8 @@ where
cursor_position: None,
modifiers: winit::keyboard::ModifiersState::default(),
theme,
theme_mode,
default_theme,
style,
}
}
@ -123,7 +131,12 @@ where
/// Returns the current theme of the [`State`].
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`].
@ -137,7 +150,12 @@ where
}
/// 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 {
WindowEvent::Resized(new_size) => {
let size = Size::new(new_size.width, new_size.height);
@ -174,6 +192,16 @@ where
WindowEvent::ModifiersChanged(new_modifiers) => {
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.
///
/// Normally, a [`Program`] should be synchronized with its [`State`]
/// and window after calling [`State::update`].
/// and window after calling [`Program::update`].
pub fn synchronize(
&mut self,
program: &program::Instance<P>,
@ -217,6 +245,45 @@ where
// Update theme and appearance
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;
}
}
}