-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Theming Reloaded #2312
Theming Reloaded #2312
Conversation
30b7ab9
to
7161cb4
Compare
This entire PR is looking great to me! I've already tried to upgrade some of my apps using this and it feels really good. I also really like the themer(
color,
button("I am dynamic!").style(|color, _status| button::Appearance::default().with_background(color)),
) There is only one problem I can see with it right now. This works because you've implemented the button's let gradient_box = themer(
gradient,
container(horizontal_space())
.width(Length::Fill)
.height(Length::Fill),
); The problem is that the themer propagates the let gradient_content = column![
text("Input:"),
text_input(
"placeholder",
if self.transparent { "transparent" } else { "not transparent" }
),
horizontal_space(),
];
let gradient_box = themer(
gradient,
container(gradient_content)
.width(Length::Fill)
.height(Length::Fill),
); It would fail because It also means that if the user wants to change something else besides the ones you've implemented in a dynamic way, it is not possible. For example, let's say I have an application where I want the border width of some container to change according to some value on my App, how would I do that? We can't do this: container(gradient_content)
.style(|t, s| {
let width = if self.transparent {
2.0
} else {
1.0
};
container::Appearance {
border: iced::Border {
color: Color::BLACK,
width,
radius: 0.0.into(),
},
..container::bordered_box(t, s)
}
}); Because of let border = iced::Border {
color: Color::BLACK,
width: if self.transparent { 2.0 } else { 1.0 },
radius: 0.0.into(),
};
let c = themer(
border,
container(gradient_content)
); I'm not sure what the solution should be other than implementing every widget's |
You can nest
You can use anything as the new theme: themer((theme, self.transparent), ...) We just need to add a |
Until then, Themer::new(
move |theme| {
let width = if self.transparent {
2.0
} else {
1.0
};
container::Appearance {
border: Border {
color: Color::BLACK,
width,
radius: 0.0.into(),
},
..container::bordered_box(theme, container::Status::Idle)
}
},
my_container,
) |
This doesn't work with the error that the container doesn't implement So I've tried using the nested The full code: (the error is on a comment on the line it happens)use iced::application;
use iced::widget::{
checkbox, column, container, horizontal_space, row, slider, text, text_input, themer, Themer
};
use iced::{gradient, window};
use iced::{
Alignment, Color, Element, Length, Radians, Sandbox, Settings, Theme,
};
pub fn main() -> iced::Result {
tracing_subscriber::fmt::init();
Gradient::run(Settings {
window: window::Settings {
transparent: true,
..Default::default()
},
..Default::default()
})
}
#[derive(Debug, Clone, Copy)]
struct Gradient {
start: Color,
end: Color,
angle: Radians,
transparent: bool,
}
#[derive(Debug, Clone, Copy)]
enum Message {
StartChanged(Color),
EndChanged(Color),
AngleChanged(Radians),
TransparentToggled(bool),
}
impl Sandbox for Gradient {
type Message = Message;
fn new() -> Self {
Self {
start: Color::WHITE,
end: Color::new(0.0, 0.0, 1.0, 1.0),
angle: Radians(0.0),
transparent: false,
}
}
fn title(&self) -> String {
String::from("Gradient")
}
fn update(&mut self, message: Message) {
match message {
Message::StartChanged(color) => self.start = color,
Message::EndChanged(color) => self.end = color,
Message::AngleChanged(angle) => self.angle = angle,
Message::TransparentToggled(transparent) => {
self.transparent = transparent;
}
}
}
fn view(&self) -> Element<Message> {
let Self {
start,
end,
angle,
transparent,
} = *self;
let gradient = gradient::Linear::new(angle)
.add_stop(0.0, start)
.add_stop(1.0, end);
let inner_content = themer(
iced::Sandbox::theme(self),
column![
text("Input:"),
text_input(
"placeholder",
if self.transparent { "transparent" } else { "not transparent" }
),
horizontal_space(),
]
);
let content_themer = Themer::new(
move |theme| {
let width = if self.transparent {
2.0
} else {
1.0
};
container::Appearance {
border: iced::Border {
color: Color::BLACK,
width,
radius: 0.0.into(),
},
..container::bordered_box(theme, container::Status::Idle)
}
},
container(inner_content)
.width(500)
.height(200)
);
let gradient_box = themer(
gradient,
// HERE IS WHERE IT COMPLAINS:
// Diagnostics:
// 1. the trait bound `iced::advanced::iced_graphics::iced_core::Element<'_, _, iced::gradient::Linear, _>: std::convert::From<iced::widget::Container<'_, Message>>` is not satisfied
// the following other types implement trait `std::convert::From<T>`:
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::Column<'a, Message, Theme, Renderer>>>
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::MouseArea<'a, Message, Theme, Renderer>>>
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::Row<'a, Message, Theme, Renderer>>>
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::Themer<'a, Message, Theme, NewTheme, F, Renderer>>>
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::Button<'a, Message, Theme, Renderer>>>
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::Checkbox<'a, Message, Theme, Renderer>>>
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::ComboBox<'a, T, Message, Theme, Renderer>>>
// <iced::advanced::iced_graphics::iced_core::Element<'a, Message, Theme, Renderer> as std::convert::From<iced::widget::Container<'a, Message, Theme, Renderer>>>
// and 25 others
// required for `iced::widget::Container<'_, Message>` to implement `std::convert::Into<iced::advanced::iced_graphics::iced_core::Element<'_, _, iced::gradient::Linear, _>>` [E0277]
container(content_themer)
.width(Length::Fill)
.height(Length::Fill),
);
let angle_picker = row![
text("Angle").width(64),
slider(Radians::RANGE, self.angle, Message::AngleChanged)
.step(0.01)
]
.spacing(8)
.padding(8)
.align_items(Alignment::Center);
let transparency_toggle = iced::widget::Container::new(
checkbox("Transparent window", transparent)
.on_toggle(Message::TransparentToggled),
)
.padding(8);
column![
color_picker("Start", self.start).map(Message::StartChanged),
color_picker("End", self.end).map(Message::EndChanged),
angle_picker,
transparency_toggle,
gradient_box,
]
.into()
}
fn style(&self, theme: &Theme) -> application::Appearance {
if self.transparent {
application::Appearance {
background_color: Color::TRANSPARENT,
text_color: theme.palette().text,
}
} else {
application::default(theme)
}
}
}
fn color_picker(label: &str, color: Color) -> Element<'_, Color> {
row![
text(label).width(64),
slider(0.0..=1.0, color.r, move |r| { Color { r, ..color } })
.step(0.01),
slider(0.0..=1.0, color.g, move |g| { Color { g, ..color } })
.step(0.01),
slider(0.0..=1.0, color.b, move |b| { Color { b, ..color } })
.step(0.01),
slider(0.0..=1.0, color.a, move |a| { Color { a, ..color } })
.step(0.01),
]
.spacing(8)
.padding(8)
.align_items(Alignment::Center)
.into()
} I'm probably doing something wrong. But still haven't find out what... From what I understand there is no problem if the content of the container to which we are giving the gradient with themer has no content that requires an appearance, like Text, Space... But if the content needs to have an appearance from a Theme it complains, because we will have elements of type How would you approach something like this? |
Oh I see it now. The problem is here: let content_themer = Themer::new(
move |theme| {
let width = if self.transparent {
2.0
} else {
1.0
};
container::Appearance {
border: iced::Border {
color: Color::BLACK,
width,
radius: 0.0.into(),
},
..container::bordered_box(theme, container::Status::Idle)
}
},
container(inner_content)
.width(500)
.height(200)
); When using the above it is expecting the incoming theme ( let inner_content = themer(
iced::Sandbox::theme(self),
column![
text("Input:"),
text_input(
"placeholder",
if self.transparent { "transparent" } else { "not transparent" }
),
horizontal_space(),
]
);
let content_themer = Themer::new(
move |_theme| {
let width = if self.transparent {
2.0
} else {
1.0
};
container::Appearance {
border: iced::Border {
color: Color::BLACK,
width,
radius: 0.0.into(),
},
..container::bordered_box(&iced::Sandbox::theme(self), container::Status::Idle)
}
},
container(inner_content)
.width(500)
.height(200)
); This way it works! Thank you for the explication! |
@alex-ds13 If it's just a simple toggle, why not something like this? container(column![...])
.style(if self.transparent { thick_bordered_box } else { container::bordered_box }) Where fn thick_bordered_box(theme: &Theme, status: &container::Status) -> container::Appearance {
container::Appearance {
border: iced::Border {
color: Color::BLACK,
width: 2.0,
radius: 0.0.into(),
},
..container::bordered_box(theme, status)
}
} |
This was just an experiment to see if I could still change some appearance aspects according to the App's state. But yes that would make sense for this case. Even the case I'm using right now could be done like that. I'm using a modal to show the errors of the app and I want that modal to always have the same theme and look, regardless of the app's theme. I'm using a |
@alex-ds13 Remember that you can always implement your own sub-theme for just the modal and implement // modal.rs
pub enum Theme {
Information,
Warning,
Error(error::Severity),
// ...
}
impl container::DefaultStyle for Theme {
// ...
}
impl text_input::DefaultStyle for Theme {
// ...
}
pub enum Message {
// ...
}
pub fn view<'a>() -> Element<'a, Message, Theme> {
// ...
} Then: themer(modal::Theme::Error(self.error.severity()), modal::view()) |
@alex-ds13 #2326 should let you use closures now! |
The current theming approach is quite painful and confusing. It was presented a couple of years ago in this RFC and—while at the time it was a clear improvement to the box-all-the-styles approach we had—it introduced a whole new set of moving parts. Way too many moving parts.
A simple use case
Let's say we are a new user and we want to change the style of a
button
. Quite a simple use case, certainly. We search for "button style" in the API reference (yes, the API is actually somewhat documented!) and we find theButton::style
method. Great! But wait, what is... this?!How do we even use this? What is
style
supposed to be exactly? We have a genericTheme
which implements aStyleSheet
trait which has an associatedStyle
type. Three moving parts! So we need to figure out first the specificTheme
type we are dealing with, find itsStyleSheet
implementation, look at theStyle
associated type and then learn how to use that. But... I just want to change the color of a button! How hard can that be?!The current system is terribly complicated; and I have wanted to revisit and redesign it for a while. It seems that time has finally come.
Functions all the way down
What would the ideal
style
method look like? It should let the user easily customize the appearance of a widget based on its current status without much friction.The only reason to have
StyleSheet
traits is to allow users to define different appearances for each particular widget status (like active, hovered, disabled, etc.). However, if we encode the widget status as data we can then leverage a single entrypoint for the whole appearance of a widget! A simple function!Thus, widget style is defined using a function pointer that takes the current
Theme
and some widgetStatus
(if the widget is stateful) and produces the finalAppearance
of the widget.There is only one moving part here: the generic
Theme
type. However, if you are not using a custom theme, this will always be the built-inTheme
. And if you are using a custom theme, then you are probably already familiar with theTheme
generic type.We can easily change our button color now:
We can even make a style helper for our red buttons:
And using it is straightforward:
Each widget module exposes some built-in style helpers that are readily available. For instance,
button
exposesprimary
,secondary
,success
,danger
, andtext
. Using them is analogous to the previous example:Since styles are just functions, this means you can easily compose and extend existing styles:
I think this design is glaringly simple; and it was the obvious approach all along. I guess it's not so easy to escape my past trauma as a web developer...
Capturing state
But wait! If styles are just function pointers and not closures... Does that mean that we cannot style widgets based on application state? Let's say we have a
Color
in our state. Can we use it to style a widget? Yes, thanks to the newthemer
widget!The
themer
widget introduced in #2209 can be used to change theTheme
type for a subtree of the widget tree. This means we can set ourColor
as theTheme
generic type and use that in our style function. For instance:In fact, since
Color
can be directly used as the default style of a button, we can just write:By making this kind of dynamism opt-in, we avoid either boxing closures or introducing yet another generic type to all the widgets.
Custom themes
Finally, custom themes do not need to worry about a myriad of
StyleSheet
traits andStyle
enums anymore. Instead, they only need to implement theDefaultStyle
trait of each widget. For instance, let's say we have a custom theme namedMyCustomTheme
and we want all of its buttons to be red by default (oh god, why):And that's all! I think this considerably simplifies the most painful part of
iced
🥳