iced-rs / iced

A cross-platform GUI library for Rust, inspired by Elm
https://iced.rs
MIT License
23.06k stars 1.06k forks source link

System themes #1022

Open mmstick opened 2 years ago

mmstick commented 2 years ago

What's the stance on supporting system themes? It's often important to have a consistent UX across the desktop, with all applications sharing a common system theme by default. Will iced support allowing users and distributors to design system themes that can be applied universally to all iced applications?

yusdacra commented 2 years ago

I'm not sure how feasible it would be, but perhaps writing iced backends for native toolkits of a platform (qt, gtk, etc.) would work? I know druid supports native platform toolkits so it should be possible.

mmstick commented 2 years ago

Let's say that I have an OS, and I want to build that OS around a toolkit that isn't GTK or Qt. I'm already using GTK for building applications in this OS, but I want to see if there are better solutions that I could use should I want to transition away from GTK. So what I want isn't to use a toolkit that has a reliance on GTK or Qt, but a toolkit that I can use as an alternative to them. But that would require that such theoretical toolkit supports defining system themes.

yusdacra commented 2 years ago

Ah so you mean as in, say, something like a "theming backend", which could perhaps support using GTK themes, or Qt themes etc.?

mmstick commented 2 years ago

Having something like /etc/iced/settings.{ron|toml} that supports defining a theme by name, and having themes placed in /usr/share/themes/{NameOfTheme}/iced/, using whatever format works best for iced. This way, if I develop an application for this OS, it would use the system theme by default, but users could change the system theme to one they created. Applications could extend the theme or opt into their own custom theme at the application level.

mmstick commented 2 years ago

I guess to be clear, support for GTK and Qt themes is not something I'd expect. Instead, a theme format specific to Iced. I don't generally think it wise to try to guess color schemes and layouts from a GTK theme.

hecrj commented 2 years ago

I think it sounds like a good idea! I am not happy with our current default theme. This would be part of the efforts to improve it.

We could make iced search for user settings to obtain the default theme, instead of using the currently hardcoded one. For this, we would need to change the widgets slightly and pass the default theme around when rendering, since it will be chosen at runtime during initialization now.

13r0ck commented 2 years ago

Where are the hard coded values? I am willing to try and make a draft for this functionality.

emann commented 2 years ago

As mentioned in https://github.com/iced-rs/iced/discussions/1257, I'd love to take part in implementing this by tackling an important precursor, which is passing around a default styling/theme at runtime. The right place seems to be adding a widget_stylesheet: dyn WidgetStyleSheet to renderer::Style for each widget, but I'm not sure. Doing some experimenting today but would love some input/ideas

Update: thats actually kinda a bad place for it to go, especially if we want the user to be able to change the theme at runtime. Current implementation adds fn styling(&self) -> renderer::Style to Application et. al. Will have a demo in a lil bit

emann commented 2 years ago

Demo Time!

Explanation

Took the styling example project and created an application_theme example. Unlike the styling example, the widgets don't require individual calls to .style(SomeStyleSheet) other than the one button that does so in order to show that custom styling still works. All of the structs that implement the various widgets' respective StyleSheet traits are still present but they are used to create a renderer::Style that is returned by styling() and passed through to all Widget::draw() calls. The .styling() impl for the root struct that implements Sandbox is simply:

fn styling(&self) -> Style { self.theme.into() }

as the Theme enum has an impl From<Theme> for Style which returns the renderer::Style for the theme selected.


ToDo

If/when this gets merged in all that will be left is to create platform-specific impl StyleSheet for Default for the various widgets.


Be warned - the ugly theme really is ugly

application_theme_demo

oknozor commented 2 years ago

Hey what's the current status for this @emann ? I'd could really use this. If I may suggest it would be really nice to have just a tiny subset of css (using pest or nom) to configure the themes. I am currently using toml/ron to let user configure iced styling for an app. Not having color syntax highlight is a pain.

13r0ck commented 2 years ago

@oknozor https://github.com/iced-rs/rfcs/pull/6

Frostie314159 commented 1 year ago

Just to add to this. There is a crate called dark-light which can detect the system theme across all platforms(not sure about web though). Using it might be feasible.

lucatrv commented 1 year ago

The following sample code, using the dark-light crate, works but detects system theme only once when the app is launched.

use iced::{Element, Sandbox, Settings, Theme};

pub fn main() -> iced::Result {
    Hello::run(Settings::default())
}

struct Hello;

impl Sandbox for Hello {
    type Message = ();

    fn new() -> Self {
        Self
    }

    fn title(&self) -> String {
        String::from("A cool application")
    }

    fn update(&mut self, _message: Self::Message) {}

    fn view(&self) -> Element<Self::Message> {
        "Hello, world!".into()
    }

    fn theme(&self) -> Theme {
        match dark_light::detect() {
            dark_light::Mode::Light | dark_light::Mode::Default => Theme::Light,
            dark_light::Mode::Dark => Theme::Dark,
        }
    }
}

In the following sample code, the app theme is aligned to the system theme only when the counter is updated.

use iced::widget::{button, column, text};
use iced::{Alignment, Element, Sandbox, Settings, Theme};

pub fn main() -> iced::Result {
    Counter::run(Settings::default())
}

struct Counter {
    value: i32,
}

#[derive(Debug, Clone, Copy)]
enum Message {
    IncrementPressed,
    DecrementPressed,
}

impl Sandbox for Counter {
    type Message = Message;

    fn new() -> Self {
        Self { value: 0 }
    }

    fn title(&self) -> String {
        String::from("Counter - Iced")
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::IncrementPressed => {
                self.value += 1;
            }
            Message::DecrementPressed => {
                self.value -= 1;
            }
        }
    }

    fn view(&self) -> Element<Message> {
        column![
            button("Increment").on_press(Message::IncrementPressed),
            text(self.value).size(50),
            button("Decrement").on_press(Message::DecrementPressed)
        ]
        .padding(20)
        .align_items(Alignment::Center)
        .into()
    }

    fn theme(&self) -> Theme {
        match dark_light::detect() {
            dark_light::Mode::Light | dark_light::Mode::Default => Theme::Light,
            dark_light::Mode::Dark => Theme::Dark,
        }
    }
}

It would be nice to be able to detect system theme changes automatically and keep the app theme aligned to it.

ModProg commented 1 year ago

It would be nice to be able to detect system theme changes automatically and keep the app theme aligned to it.

I implemented Application::subscription:

fn subscription(&self) -> iced::Subscription<Self::Message> {
    iced_native::subscription::events().map(Message::EventOccurred)
}

And that meant it would auto update. Though I'm not sure what the implications of that are.

hecrj commented 1 year ago

It should be fairly easy to create a subscription::channel that runs dark_light::detect in a loop every minute or so and publishes Mode changes to the application.

lucatrv commented 1 year ago

Could system theme changes be detected by listening to an external event?

lucatrv commented 1 year ago

It should be fairly easy to create a subscription::channel that runs dark_light::detect in a loop every minute or so and publishes Mode changes to the application.

I'm trying to implement what suggested. I looked at the iced::subscription::channel documentation and at the websocket example, but sorry I'm having a hard time figuring out how to proceed...

Here is a sample code, with an event subscription already implemented, and I would like to add a channel subscription for dark_light theme detection, can someone kindly help me out?

use std::path::PathBuf;
use iced::widget::text;
use iced::{event, executor, subscription, window};
use iced::{Application, Command, Element, Event, Settings, Subscription, Theme};

pub fn main() -> iced::Result {
    FileDrop::run(Settings::default())
}

struct FileDrop {
    text: String,
    theme: Theme,
}

#[derive(Debug)]
enum Message {
    FileDropped(PathBuf),
}

impl Application for FileDrop {
    type Executor = executor::Default;
    type Message = Message;
    type Theme = Theme;
    type Flags = ();

    fn new(_flags: Self::Flags) -> (FileDrop, Command<Self::Message>) {
        (
            FileDrop {
                text: String::from("Drop a file here!"),
                theme: system_theme_mode(),
            },
            Command::none(),
        )
    }

    fn title(&self) -> String {
        String::from("File Drop")
    }

    fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
        match message {
            Message::FileDropped(file) => {
                self.text = format!("Dropped file:\n{}", file.to_string_lossy());
                Command::none()
            }
        }
    }

    fn view(&self) -> Element<Self::Message> {
        text(&self.text).into()
    }

    fn theme(&self) -> Theme {
        self.theme.clone()
    }

    fn subscription(&self) -> Subscription<Self::Message> {
        subscription::events_with(|event, status| match (event, status) {
            (Event::Window(window::Event::FileDropped(file)), event::Status::Ignored) => {
                Some(Message::FileDropped(file))
            }
            _ => None,
        })
    }
}

fn system_theme_mode() -> Theme {
    match dark_light::detect() {
        dark_light::Mode::Light | dark_light::Mode::Default => Theme::Light,
        dark_light::Mode::Dark => Theme::Dark,
    }
}
lucatrv commented 1 year ago

I think I finally found my solution, I report it here in case it was useful for others:

use iced::widget::text;
use iced::{event, executor, subscription, time, window};
use iced::{Application, Command, Element, Event, Settings, Subscription, Theme};
use std::path::PathBuf;

fn main() -> iced::Result {
    FileDrop::run(Settings::default())
}

struct FileDrop {
    text: String,
    theme: Theme,
}

#[derive(Debug)]
enum Message {
    FileDropped(PathBuf),
    SystemThemeMode(Theme),
}

impl Application for FileDrop {
    type Executor = executor::Default;
    type Message = Message;
    type Theme = Theme;
    type Flags = ();

    fn new(_flags: Self::Flags) -> (FileDrop, Command<Self::Message>) {
        (
            FileDrop {
                text: String::from("Drop a file here!"),
                theme: system_theme_mode(),
            },
            Command::none(),
        )
    }

    fn title(&self) -> String {
        String::from("File Drop")
    }

    fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
        match message {
            Message::FileDropped(file) => {
                self.text = format!("Dropped file:\n{}", file.to_string_lossy());
                Command::none()
            }
            Message::SystemThemeMode(theme) => {
                self.theme = theme;
                Command::none()
            }
        }
    }

    fn view(&self) -> Element<Self::Message> {
        Element::new(text(&self.text))
    }

    fn theme(&self) -> Self::Theme {
        self.theme.clone()
    }

    fn subscription(&self) -> Subscription<Self::Message> {
        Subscription::batch([
            subscription::events_with(|event, status| match (event, status) {
                (Event::Window(window::Event::FileDropped(file)), event::Status::Ignored) => {
                    Some(Message::FileDropped(file))
                }
                _ => None,
            }),
            time::every(time::Duration::from_secs(60))
                .map(|_| Message::SystemThemeMode(system_theme_mode())),
        ])
    }
}

fn system_theme_mode() -> Theme {
    match dark_light::detect() {
        dark_light::Mode::Light | dark_light::Mode::Default => Theme::Light,
        dark_light::Mode::Dark => Theme::Dark,
    }
}

This requires either one of the following iced features to be enabled: tokio, async-std, or smol.