Merge pull request #2312 from iced-rs/theming-reloaded

Theming reloaded
This commit is contained in:
Héctor Ramón 2024-03-08 14:00:28 +01:00 committed by GitHub
commit edf7d7ca75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
97 changed files with 5738 additions and 6660 deletions

View file

@ -16,7 +16,6 @@ jobs:
cargo doc --no-deps --all-features \ cargo doc --no-deps --all-features \
-p iced_core \ -p iced_core \
-p iced_highlighter \ -p iced_highlighter \
-p iced_style \
-p iced_futures \ -p iced_futures \
-p iced_runtime \ -p iced_runtime \
-p iced_graphics \ -p iced_graphics \

View file

@ -39,8 +39,6 @@ tokio = ["iced_futures/tokio"]
async-std = ["iced_futures/async-std"] async-std = ["iced_futures/async-std"]
# 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 advanced color conversion via `palette`
palette = ["iced_core/palette"]
# Enables querying system information # Enables querying system information
system = ["iced_winit/system"] system = ["iced_winit/system"]
# Enables broken "sRGB linear" blending to reproduce color management of the Web # Enables broken "sRGB linear" blending to reproduce color management of the Web
@ -57,7 +55,6 @@ advanced = []
fira-sans = ["iced_renderer/fira-sans"] fira-sans = ["iced_renderer/fira-sans"]
[dependencies] [dependencies]
iced_core.workspace = true
iced_futures.workspace = true iced_futures.workspace = true
iced_renderer.workspace = true iced_renderer.workspace = true
iced_widget.workspace = true iced_widget.workspace = true
@ -90,7 +87,6 @@ members = [
"highlighter", "highlighter",
"renderer", "renderer",
"runtime", "runtime",
"style",
"tiny_skia", "tiny_skia",
"wgpu", "wgpu",
"widget", "widget",
@ -116,7 +112,6 @@ iced_graphics = { version = "0.13.0-dev", path = "graphics" }
iced_highlighter = { version = "0.13.0-dev", path = "highlighter" } iced_highlighter = { version = "0.13.0-dev", path = "highlighter" }
iced_renderer = { version = "0.13.0-dev", path = "renderer" } iced_renderer = { version = "0.13.0-dev", path = "renderer" }
iced_runtime = { version = "0.13.0-dev", path = "runtime" } iced_runtime = { version = "0.13.0-dev", path = "runtime" }
iced_style = { version = "0.13.0-dev", path = "style" }
iced_tiny_skia = { version = "0.13.0-dev", path = "tiny_skia" } iced_tiny_skia = { version = "0.13.0-dev", path = "tiny_skia" }
iced_wgpu = { version = "0.13.0-dev", path = "wgpu" } iced_wgpu = { version = "0.13.0-dev", path = "wgpu" }
iced_widget = { version = "0.13.0-dev", path = "widget" } iced_widget = { version = "0.13.0-dev", path = "widget" }

View file

@ -15,14 +15,13 @@ bitflags.workspace = true
glam.workspace = true glam.workspace = true
log.workspace = true log.workspace = true
num-traits.workspace = true num-traits.workspace = true
once_cell.workspace = true
palette.workspace = true
smol_str.workspace = true smol_str.workspace = true
thiserror.workspace = true thiserror.workspace = true
web-time.workspace = true web-time.workspace = true
xxhash-rust.workspace = true xxhash-rust.workspace = true
palette.workspace = true
palette.optional = true
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
raw-window-handle.workspace = true raw-window-handle.workspace = true

View file

@ -11,6 +11,19 @@ pub enum Background {
// TODO: Add image variant // TODO: Add image variant
} }
impl Background {
/// Scales the the alpha channel of the [`Background`] by the given
/// factor.
pub fn scale_alpha(self, factor: f32) -> Self {
match self {
Self::Color(color) => Self::Color(color.scale_alpha(factor)),
Self::Gradient(gradient) => {
Self::Gradient(gradient.scale_alpha(factor))
}
}
}
}
impl From<Color> for Background { impl From<Color> for Background {
fn from(color: Color) -> Self { fn from(color: Color) -> Self {
Background::Color(color) Background::Color(color)

View file

@ -1,5 +1,5 @@
//! Draw lines around containers. //! Draw lines around containers.
use crate::Color; use crate::{Color, Pixels};
/// A border. /// A border.
#[derive(Debug, Clone, Copy, PartialEq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Default)]
@ -15,11 +15,38 @@ pub struct Border {
} }
impl Border { impl Border {
/// Creates a new default [`Border`] with the given [`Radius`]. /// Creates a new default rounded [`Border`] with the given [`Radius`].
pub fn with_radius(radius: impl Into<Radius>) -> Self { ///
/// ```
/// # use iced_core::Border;
/// #
/// assert_eq!(Border::rounded(10), Border::default().with_radius(10));
/// ```
pub fn rounded(radius: impl Into<Radius>) -> Self {
Self::default().with_radius(radius)
}
/// Updates the [`Color`] of the [`Border`].
pub fn with_color(self, color: impl Into<Color>) -> Self {
Self {
color: color.into(),
..self
}
}
/// Updates the [`Radius`] of the [`Border`].
pub fn with_radius(self, radius: impl Into<Radius>) -> Self {
Self { Self {
radius: radius.into(), radius: radius.into(),
..Self::default() ..self
}
}
/// Updates the width of the [`Border`].
pub fn with_width(self, width: impl Into<Pixels>) -> Self {
Self {
width: width.into().0,
..self
} }
} }
} }

View file

@ -1,4 +1,3 @@
#[cfg(feature = "palette")]
use palette::rgb::{Srgb, Srgba}; use palette::rgb::{Srgb, Srgba};
/// A color in the `sRGB` color space. /// A color in the `sRGB` color space.
@ -151,6 +150,14 @@ impl Color {
pub fn inverse(self) -> Color { pub fn inverse(self) -> Color {
Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a) Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a)
} }
/// Scales the alpha channel of the [`Color`] by the given factor.
pub fn scale_alpha(self, factor: f32) -> Color {
Self {
a: self.a * factor,
..self
}
}
} }
impl From<[f32; 3]> for Color { impl From<[f32; 3]> for Color {
@ -202,7 +209,6 @@ macro_rules! color {
}}; }};
} }
#[cfg(feature = "palette")]
/// Converts from palette's `Rgba` type to a [`Color`]. /// Converts from palette's `Rgba` type to a [`Color`].
impl From<Srgba> for Color { impl From<Srgba> for Color {
fn from(rgba: Srgba) -> Self { fn from(rgba: Srgba) -> Self {
@ -210,7 +216,6 @@ impl From<Srgba> for Color {
} }
} }
#[cfg(feature = "palette")]
/// Converts from [`Color`] to palette's `Rgba` type. /// Converts from [`Color`] to palette's `Rgba` type.
impl From<Color> for Srgba { impl From<Color> for Srgba {
fn from(c: Color) -> Self { fn from(c: Color) -> Self {
@ -218,7 +223,6 @@ impl From<Color> for Srgba {
} }
} }
#[cfg(feature = "palette")]
/// Converts from palette's `Rgb` type to a [`Color`]. /// Converts from palette's `Rgb` type to a [`Color`].
impl From<Srgb> for Color { impl From<Srgb> for Color {
fn from(rgb: Srgb) -> Self { fn from(rgb: Srgb) -> Self {
@ -226,7 +230,6 @@ impl From<Srgb> for Color {
} }
} }
#[cfg(feature = "palette")]
/// Converts from [`Color`] to palette's `Rgb` type. /// Converts from [`Color`] to palette's `Rgb` type.
impl From<Color> for Srgb { impl From<Color> for Srgb {
fn from(c: Color) -> Self { fn from(c: Color) -> Self {
@ -234,7 +237,6 @@ impl From<Color> for Srgb {
} }
} }
#[cfg(feature = "palette")]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -12,17 +12,13 @@ pub enum Gradient {
} }
impl Gradient { impl Gradient {
/// Adjust the opacity of the gradient by a multiplier applied to each color stop. /// Scales the alpha channel of the [`Gradient`] by the given factor.
pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self { pub fn scale_alpha(self, factor: f32) -> Self {
match &mut self { match self {
Gradient::Linear(linear) => { Gradient::Linear(linear) => {
for stop in linear.stops.iter_mut().flatten() { Gradient::Linear(linear.scale_alpha(factor))
stop.color.a *= alpha_multiplier;
}
} }
} }
self
} }
} }
@ -100,4 +96,14 @@ impl Linear {
self self
} }
/// Scales the alpha channel of the [`Linear`] gradient by the given
/// factor.
pub fn scale_alpha(mut self, factor: f32) -> Self {
for stop in self.stops.iter_mut().flatten() {
stop.color.a *= factor;
}
self
}
} }

View file

@ -30,6 +30,7 @@ pub mod overlay;
pub mod renderer; pub mod renderer;
pub mod svg; pub mod svg;
pub mod text; pub mod text;
pub mod theme;
pub mod time; pub mod time;
pub mod touch; pub mod touch;
pub mod widget; pub mod widget;
@ -76,6 +77,7 @@ pub use shadow::Shadow;
pub use shell::Shell; pub use shell::Shell;
pub use size::Size; pub use size::Size;
pub use text::Text; pub use text::Text;
pub use theme::Theme;
pub use transformation::Transformation; pub use transformation::Transformation;
pub use vector::Vector; pub use vector::Vector;
pub use widget::Widget; pub use widget::Widget;

221
core/src/theme.rs Normal file
View file

@ -0,0 +1,221 @@
//! Use the built-in theme and styles.
pub mod palette;
pub use palette::Palette;
use std::fmt;
use std::sync::Arc;
/// A built-in theme.
#[derive(Debug, Clone, PartialEq, Default)]
pub enum Theme {
/// The built-in light variant.
#[default]
Light,
/// The built-in dark variant.
Dark,
/// The built-in Dracula variant.
Dracula,
/// The built-in Nord variant.
Nord,
/// The built-in Solarized Light variant.
SolarizedLight,
/// The built-in Solarized Dark variant.
SolarizedDark,
/// The built-in Gruvbox Light variant.
GruvboxLight,
/// The built-in Gruvbox Dark variant.
GruvboxDark,
/// The built-in Catppuccin Latte variant.
CatppuccinLatte,
/// The built-in Catppuccin Frappé variant.
CatppuccinFrappe,
/// The built-in Catppuccin Macchiato variant.
CatppuccinMacchiato,
/// The built-in Catppuccin Mocha variant.
CatppuccinMocha,
/// The built-in Tokyo Night variant.
TokyoNight,
/// The built-in Tokyo Night Storm variant.
TokyoNightStorm,
/// The built-in Tokyo Night Light variant.
TokyoNightLight,
/// The built-in Kanagawa Wave variant.
KanagawaWave,
/// The built-in Kanagawa Dragon variant.
KanagawaDragon,
/// The built-in Kanagawa Lotus variant.
KanagawaLotus,
/// The built-in Moonfly variant.
Moonfly,
/// The built-in Nightfly variant.
Nightfly,
/// The built-in Oxocarbon variant.
Oxocarbon,
/// A [`Theme`] that uses a [`Custom`] palette.
Custom(Arc<Custom>),
}
impl Theme {
/// A list with all the defined themes.
pub const ALL: &'static [Self] = &[
Self::Light,
Self::Dark,
Self::Dracula,
Self::Nord,
Self::SolarizedLight,
Self::SolarizedDark,
Self::GruvboxLight,
Self::GruvboxDark,
Self::CatppuccinLatte,
Self::CatppuccinFrappe,
Self::CatppuccinMacchiato,
Self::CatppuccinMocha,
Self::TokyoNight,
Self::TokyoNightStorm,
Self::TokyoNightLight,
Self::KanagawaWave,
Self::KanagawaDragon,
Self::KanagawaLotus,
Self::Moonfly,
Self::Nightfly,
Self::Oxocarbon,
];
/// Creates a new custom [`Theme`] from the given [`Palette`].
pub fn custom(name: String, palette: Palette) -> Self {
Self::custom_with_fn(name, palette, palette::Extended::generate)
}
/// Creates a new custom [`Theme`] from the given [`Palette`], with
/// a custom generator of a [`palette::Extended`].
pub fn custom_with_fn(
name: String,
palette: Palette,
generate: impl FnOnce(Palette) -> palette::Extended,
) -> Self {
Self::Custom(Arc::new(Custom::with_fn(name, palette, generate)))
}
/// Returns the [`Palette`] of the [`Theme`].
pub fn palette(&self) -> Palette {
match self {
Self::Light => Palette::LIGHT,
Self::Dark => Palette::DARK,
Self::Dracula => Palette::DRACULA,
Self::Nord => Palette::NORD,
Self::SolarizedLight => Palette::SOLARIZED_LIGHT,
Self::SolarizedDark => Palette::SOLARIZED_DARK,
Self::GruvboxLight => Palette::GRUVBOX_LIGHT,
Self::GruvboxDark => Palette::GRUVBOX_DARK,
Self::CatppuccinLatte => Palette::CATPPUCCIN_LATTE,
Self::CatppuccinFrappe => Palette::CATPPUCCIN_FRAPPE,
Self::CatppuccinMacchiato => Palette::CATPPUCCIN_MACCHIATO,
Self::CatppuccinMocha => Palette::CATPPUCCIN_MOCHA,
Self::TokyoNight => Palette::TOKYO_NIGHT,
Self::TokyoNightStorm => Palette::TOKYO_NIGHT_STORM,
Self::TokyoNightLight => Palette::TOKYO_NIGHT_LIGHT,
Self::KanagawaWave => Palette::KANAGAWA_WAVE,
Self::KanagawaDragon => Palette::KANAGAWA_DRAGON,
Self::KanagawaLotus => Palette::KANAGAWA_LOTUS,
Self::Moonfly => Palette::MOONFLY,
Self::Nightfly => Palette::NIGHTFLY,
Self::Oxocarbon => Palette::OXOCARBON,
Self::Custom(custom) => custom.palette,
}
}
/// Returns the [`palette::Extended`] of the [`Theme`].
pub fn extended_palette(&self) -> &palette::Extended {
match self {
Self::Light => &palette::EXTENDED_LIGHT,
Self::Dark => &palette::EXTENDED_DARK,
Self::Dracula => &palette::EXTENDED_DRACULA,
Self::Nord => &palette::EXTENDED_NORD,
Self::SolarizedLight => &palette::EXTENDED_SOLARIZED_LIGHT,
Self::SolarizedDark => &palette::EXTENDED_SOLARIZED_DARK,
Self::GruvboxLight => &palette::EXTENDED_GRUVBOX_LIGHT,
Self::GruvboxDark => &palette::EXTENDED_GRUVBOX_DARK,
Self::CatppuccinLatte => &palette::EXTENDED_CATPPUCCIN_LATTE,
Self::CatppuccinFrappe => &palette::EXTENDED_CATPPUCCIN_FRAPPE,
Self::CatppuccinMacchiato => {
&palette::EXTENDED_CATPPUCCIN_MACCHIATO
}
Self::CatppuccinMocha => &palette::EXTENDED_CATPPUCCIN_MOCHA,
Self::TokyoNight => &palette::EXTENDED_TOKYO_NIGHT,
Self::TokyoNightStorm => &palette::EXTENDED_TOKYO_NIGHT_STORM,
Self::TokyoNightLight => &palette::EXTENDED_TOKYO_NIGHT_LIGHT,
Self::KanagawaWave => &palette::EXTENDED_KANAGAWA_WAVE,
Self::KanagawaDragon => &palette::EXTENDED_KANAGAWA_DRAGON,
Self::KanagawaLotus => &palette::EXTENDED_KANAGAWA_LOTUS,
Self::Moonfly => &palette::EXTENDED_MOONFLY,
Self::Nightfly => &palette::EXTENDED_NIGHTFLY,
Self::Oxocarbon => &palette::EXTENDED_OXOCARBON,
Self::Custom(custom) => &custom.extended,
}
}
}
impl fmt::Display for Theme {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Light => write!(f, "Light"),
Self::Dark => write!(f, "Dark"),
Self::Dracula => write!(f, "Dracula"),
Self::Nord => write!(f, "Nord"),
Self::SolarizedLight => write!(f, "Solarized Light"),
Self::SolarizedDark => write!(f, "Solarized Dark"),
Self::GruvboxLight => write!(f, "Gruvbox Light"),
Self::GruvboxDark => write!(f, "Gruvbox Dark"),
Self::CatppuccinLatte => write!(f, "Catppuccin Latte"),
Self::CatppuccinFrappe => write!(f, "Catppuccin Frappé"),
Self::CatppuccinMacchiato => write!(f, "Catppuccin Macchiato"),
Self::CatppuccinMocha => write!(f, "Catppuccin Mocha"),
Self::TokyoNight => write!(f, "Tokyo Night"),
Self::TokyoNightStorm => write!(f, "Tokyo Night Storm"),
Self::TokyoNightLight => write!(f, "Tokyo Night Light"),
Self::KanagawaWave => write!(f, "Kanagawa Wave"),
Self::KanagawaDragon => write!(f, "Kanagawa Dragon"),
Self::KanagawaLotus => write!(f, "Kanagawa Lotus"),
Self::Moonfly => write!(f, "Moonfly"),
Self::Nightfly => write!(f, "Nightfly"),
Self::Oxocarbon => write!(f, "Oxocarbon"),
Self::Custom(custom) => custom.fmt(f),
}
}
}
/// A [`Theme`] with a customized [`Palette`].
#[derive(Debug, Clone, PartialEq)]
pub struct Custom {
name: String,
palette: Palette,
extended: palette::Extended,
}
impl Custom {
/// Creates a [`Custom`] theme from the given [`Palette`].
pub fn new(name: String, palette: Palette) -> Self {
Self::with_fn(name, palette, palette::Extended::generate)
}
/// Creates a [`Custom`] theme from the given [`Palette`] with
/// a custom generator of a [`palette::Extended`].
pub fn with_fn(
name: String,
palette: Palette,
generate: impl FnOnce(Palette) -> palette::Extended,
) -> Self {
Self {
name,
palette,
extended: generate(palette),
}
}
}
impl fmt::Display for Custom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name)
}
}

View file

@ -1,5 +1,5 @@
//! Define the colors of a theme. //! Define the colors of a theme.
use crate::core::{color, Color}; use crate::{color, Color};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use palette::color_difference::Wcag21RelativeContrast; use palette::color_difference::Wcag21RelativeContrast;

View file

@ -17,7 +17,6 @@ pub use text::{LineHeight, Shaping};
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Text<'a, Theme, Renderer> pub struct Text<'a, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
content: Cow<'a, str>, content: Cow<'a, str>,
@ -29,12 +28,11 @@ where
vertical_alignment: alignment::Vertical, vertical_alignment: alignment::Vertical,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
shaping: Shaping, shaping: Shaping,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, Theme, Renderer> Text<'a, Theme, Renderer> impl<'a, Theme, Renderer> Text<'a, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// Create a new fragment of [`Text`] with the given contents. /// Create a new fragment of [`Text`] with the given contents.
@ -49,7 +47,7 @@ where
horizontal_alignment: alignment::Horizontal::Left, horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top, vertical_alignment: alignment::Vertical::Top,
shaping: Shaping::Basic, shaping: Shaping::Basic,
style: Default::default(), style: Style::default(),
} }
} }
@ -74,8 +72,20 @@ where
} }
/// Sets the style of the [`Text`]. /// Sets the style of the [`Text`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self {
self.style = style.into(); self.style = Style::Themed(style);
self
}
/// Sets the [`Color`] of the [`Text`].
pub fn color(mut self, color: impl Into<Color>) -> Self {
self.style = Style::Colored(Some(color.into()));
self
}
/// Sets the [`Color`] of the [`Text`], if `Some`.
pub fn color_maybe(mut self, color: Option<impl Into<Color>>) -> Self {
self.style = Style::Colored(color.map(Into::into));
self self
} }
@ -123,7 +133,6 @@ pub struct State<P: Paragraph>(P);
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Text<'a, Theme, Renderer> for Text<'a, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -175,14 +184,12 @@ where
) { ) {
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
draw( let appearance = match self.style {
renderer, Style::Themed(f) => f(theme),
style, Style::Colored(color) => Appearance { color },
layout, };
state,
theme.appearance(self.style.clone()), draw(renderer, style, layout, state, appearance, viewport);
viewport,
);
} }
} }
@ -273,7 +280,7 @@ pub fn draw<Renderer>(
impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>> impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
fn from( fn from(
@ -285,7 +292,6 @@ where
impl<'a, Theme, Renderer> Clone for Text<'a, Theme, Renderer> impl<'a, Theme, Renderer> Clone for Text<'a, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn clone(&self) -> Self { fn clone(&self) -> Self {
@ -298,7 +304,7 @@ where
horizontal_alignment: self.horizontal_alignment, horizontal_alignment: self.horizontal_alignment,
vertical_alignment: self.vertical_alignment, vertical_alignment: self.vertical_alignment,
font: self.font, font: self.font,
style: self.style.clone(), style: self.style,
shaping: self.shaping, shaping: self.shaping,
} }
} }
@ -306,7 +312,6 @@ where
impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer> impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn from(content: &'a str) -> Self { fn from(content: &'a str) -> Self {
@ -317,7 +322,7 @@ where
impl<'a, Message, Theme, Renderer> From<&'a str> impl<'a, Message, Theme, Renderer> From<&'a str>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
fn from(content: &'a str) -> Self { fn from(content: &'a str) -> Self {
@ -325,15 +330,6 @@ where
} }
} }
/// The style sheet of some text.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default + Clone;
/// Produces the [`Appearance`] of some text.
fn appearance(&self, style: Self::Style) -> Appearance;
}
/// The apperance of some text. /// The apperance of some text.
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct Appearance { pub struct Appearance {
@ -342,3 +338,29 @@ pub struct Appearance {
/// The default, `None`, means using the inherited color. /// The default, `None`, means using the inherited color.
pub color: Option<Color>, pub color: Option<Color>,
} }
#[derive(Debug)]
enum Style<Theme> {
Themed(fn(&Theme) -> Appearance),
Colored(Option<Color>),
}
impl<Theme> Clone for Style<Theme> {
fn clone(&self) -> Self {
*self
}
}
impl<Theme> Copy for Style<Theme> {}
impl<Theme> Default for Style<Theme> {
fn default() -> Self {
Style::Colored(None)
}
}
impl<Theme> From<fn(&Theme) -> Appearance> for Style<Theme> {
fn from(f: fn(&Theme) -> Appearance) -> Self {
Style::Themed(f)
}
}

View file

@ -49,7 +49,9 @@ impl Sandbox for Example {
column![ column![
text("Bezier tool example").width(Length::Shrink).size(50), text("Bezier tool example").width(Length::Shrink).size(50),
self.bezier.view(&self.curves).map(Message::AddCurve), self.bezier.view(&self.curves).map(Message::AddCurve),
button("Clear").padding(8).on_press(Message::Clear), button("Clear")
.style(button::danger)
.on_press(Message::Clear),
] ]
.padding(20) .padding(20)
.spacing(20) .spacing(20)

View file

@ -1,6 +1,5 @@
use iced::executor; use iced::executor;
use iced::font::{self, Font}; use iced::font::{self, Font};
use iced::theme;
use iced::widget::{checkbox, column, container, row, text}; use iced::widget::{checkbox, column, container, row, text};
use iced::{Application, Command, Element, Length, Settings, Theme}; use iced::{Application, Command, Element, Length, Settings, Theme};
@ -71,10 +70,10 @@ impl Application for Example {
}; };
let checkboxes = row![ let checkboxes = row![
styled_checkbox("Primary", theme::Checkbox::Primary), styled_checkbox("Primary", checkbox::primary),
styled_checkbox("Secondary", theme::Checkbox::Secondary), styled_checkbox("Secondary", checkbox::secondary),
styled_checkbox("Success", theme::Checkbox::Success), styled_checkbox("Success", checkbox::success),
styled_checkbox("Danger", theme::Checkbox::Danger), styled_checkbox("Danger", checkbox::danger),
] ]
.spacing(20); .spacing(20);

View file

@ -3,7 +3,7 @@ use iced::mouse;
use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke};
use iced::widget::{canvas, container}; use iced::widget::{canvas, container};
use iced::{ use iced::{
Application, Color, Command, Element, Length, Point, Rectangle, Renderer, Application, Command, Element, Length, Point, Rectangle, Renderer,
Settings, Subscription, Theme, Vector, Settings, Subscription, Theme, Vector,
}; };
@ -80,6 +80,10 @@ impl Application for Clock {
) )
}) })
} }
fn theme(&self) -> Theme {
Theme::TokyoNight
}
} }
impl<Message> canvas::Program<Message> for Clock { impl<Message> canvas::Program<Message> for Clock {
@ -89,16 +93,18 @@ impl<Message> canvas::Program<Message> for Clock {
&self, &self,
_state: &Self::State, _state: &Self::State,
renderer: &Renderer, renderer: &Renderer,
_theme: &Theme, theme: &Theme,
bounds: Rectangle, bounds: Rectangle,
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
) -> Vec<Geometry> { ) -> Vec<Geometry> {
let clock = self.clock.draw(renderer, bounds.size(), |frame| { let clock = self.clock.draw(renderer, bounds.size(), |frame| {
let palette = theme.extended_palette();
let center = frame.center(); let center = frame.center();
let radius = frame.width().min(frame.height()) / 2.0; let radius = frame.width().min(frame.height()) / 2.0;
let background = Path::circle(center, radius); let background = Path::circle(center, radius);
frame.fill(&background, Color::from_rgb8(0x12, 0x93, 0xD8)); frame.fill(&background, palette.primary.weak.color);
let short_hand = let short_hand =
Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius)); Path::line(Point::ORIGIN, Point::new(0.0, -0.5 * radius));
@ -111,7 +117,7 @@ impl<Message> canvas::Program<Message> for Clock {
let thin_stroke = || -> Stroke { let thin_stroke = || -> Stroke {
Stroke { Stroke {
width, width,
style: stroke::Style::Solid(Color::WHITE), style: stroke::Style::Solid(palette.primary.weak.text),
line_cap: LineCap::Round, line_cap: LineCap::Round,
..Stroke::default() ..Stroke::default()
} }
@ -120,7 +126,7 @@ impl<Message> canvas::Program<Message> for Clock {
let wide_stroke = || -> Stroke { let wide_stroke = || -> Stroke {
Stroke { Stroke {
width: width * 3.0, width: width * 3.0,
style: stroke::Style::Solid(Color::WHITE), style: stroke::Style::Solid(palette.primary.weak.text),
line_cap: LineCap::Round, line_cap: LineCap::Round,
..Stroke::default() ..Stroke::default()
} }

View file

@ -7,6 +7,6 @@ publish = false
[dependencies] [dependencies]
iced.workspace = true iced.workspace = true
iced.features = ["canvas", "palette"] iced.features = ["canvas"]
palette.workspace = true palette.workspace = true

View file

@ -3,7 +3,7 @@ use iced::mouse;
use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path};
use iced::widget::{column, row, text, Slider}; use iced::widget::{column, row, text, Slider};
use iced::{ use iced::{
Color, Element, Length, Pixels, Point, Rectangle, Renderer, Sandbox, Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Sandbox,
Settings, Size, Vector, Settings, Size, Vector,
}; };
use palette::{ use palette::{
@ -15,6 +15,7 @@ use std::ops::RangeInclusive;
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
ColorPalette::run(Settings { ColorPalette::run(Settings {
antialiasing: true, antialiasing: true,
default_font: Font::MONOSPACE,
..Settings::default() ..Settings::default()
}) })
} }
@ -87,6 +88,19 @@ impl Sandbox for ColorPalette {
.spacing(10) .spacing(10)
.into() .into()
} }
fn theme(&self) -> iced::Theme {
iced::Theme::custom(
String::from("Custom"),
iced::theme::Palette {
background: self.theme.base,
primary: *self.theme.lower.first().unwrap(),
text: *self.theme.higher.last().unwrap(),
success: *self.theme.lower.last().unwrap(),
danger: *self.theme.higher.last().unwrap(),
},
)
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -150,7 +164,7 @@ impl Theme {
.into() .into()
} }
fn draw(&self, frame: &mut Frame) { fn draw(&self, frame: &mut Frame, text_color: Color) {
let pad = 20.0; let pad = 20.0;
let box_size = Size { let box_size = Size {
@ -169,6 +183,7 @@ impl Theme {
horizontal_alignment: alignment::Horizontal::Center, horizontal_alignment: alignment::Horizontal::Center,
vertical_alignment: alignment::Vertical::Top, vertical_alignment: alignment::Vertical::Top,
size: Pixels(15.0), size: Pixels(15.0),
color: text_color,
..canvas::Text::default() ..canvas::Text::default()
}; };
@ -246,12 +261,14 @@ impl<Message> canvas::Program<Message> for Theme {
&self, &self,
_state: &Self::State, _state: &Self::State,
renderer: &Renderer, renderer: &Renderer,
_theme: &iced::Theme, theme: &iced::Theme,
bounds: Rectangle, bounds: Rectangle,
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
) -> Vec<Geometry> { ) -> Vec<Geometry> {
let theme = self.canvas_cache.draw(renderer, bounds.size(), |frame| { let theme = self.canvas_cache.draw(renderer, bounds.size(), |frame| {
self.draw(frame); let palette = theme.extended_palette();
self.draw(frame, palette.background.base.text);
}); });
vec![theme] vec![theme]
@ -308,7 +325,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> {
slider(cr1, c1, move |v| C::new(v, c2, c3)), slider(cr1, c1, move |v| C::new(v, c2, c3)),
slider(cr2, c2, move |v| C::new(c1, v, c3)), slider(cr2, c2, move |v| C::new(c1, v, c3)),
slider(cr3, c3, move |v| C::new(c1, c2, v)), slider(cr3, c3, move |v| C::new(c1, c2, v)),
text(color.to_string()).width(185).size(14), text(color.to_string()).width(185).size(12),
] ]
.spacing(10) .spacing(10)
.align_items(Alignment::Center) .align_items(Alignment::Center)

View file

@ -62,7 +62,7 @@ mod circle {
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: layout.bounds(), bounds: layout.bounds(),
border: Border::with_radius(self.radius), border: Border::rounded(self.radius),
..renderer::Quad::default() ..renderer::Quad::default()
}, },
Color::BLACK, Color::BLACK,

View file

@ -1,14 +1,13 @@
use iced::executor; use iced::executor;
use iced::highlighter::{self, Highlighter}; use iced::highlighter::{self, Highlighter};
use iced::keyboard; use iced::keyboard;
use iced::theme::{self, Theme};
use iced::widget::{ use iced::widget::{
button, column, container, horizontal_space, pick_list, row, text, button, column, container, horizontal_space, pick_list, row, text,
text_editor, tooltip, text_editor, tooltip,
}; };
use iced::{ use iced::{
Alignment, Application, Command, Element, Font, Length, Settings, Alignment, Application, Command, Element, Font, Length, Settings,
Subscription, Subscription, Theme,
}; };
use std::ffi; use std::ffi;
@ -287,10 +286,10 @@ fn action<'a, Message: Clone + 'a>(
label, label,
tooltip::Position::FollowCursor, tooltip::Position::FollowCursor,
) )
.style(theme::Container::Box) .style(container::box_)
.into() .into()
} else { } else {
action.style(theme::Button::Secondary).into() action.style(button::secondary).into()
} }
} }

View file

@ -6,7 +6,6 @@ use grid::Grid;
use preset::Preset; use preset::Preset;
use iced::executor; use iced::executor;
use iced::theme::{self, Theme};
use iced::time; use iced::time;
use iced::widget::{ use iced::widget::{
button, checkbox, column, container, pick_list, row, slider, text, button, checkbox, column, container, pick_list, row, slider, text,
@ -14,6 +13,7 @@ use iced::widget::{
use iced::window; use iced::window;
use iced::{ use iced::{
Alignment, Application, Command, Element, Length, Settings, Subscription, Alignment, Application, Command, Element, Length, Settings, Subscription,
Theme,
}; };
use std::time::Duration; use std::time::Duration;
@ -171,7 +171,7 @@ fn view_controls<'a>(
.on_press(Message::TogglePlayback), .on_press(Message::TogglePlayback),
button("Next") button("Next")
.on_press(Message::Next) .on_press(Message::Next)
.style(theme::Button::Secondary), .style(button::secondary),
] ]
.spacing(10); .spacing(10);
@ -185,17 +185,14 @@ fn view_controls<'a>(
row![ row![
playback_controls, playback_controls,
speed_controls, speed_controls,
checkbox("Grid", is_grid_enabled) checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid),
.on_toggle(Message::ToggleGrid) row![
.size(16) pick_list(preset::ALL, Some(preset), Message::PresetPicked),
.spacing(5) button("Clear")
.text_size(16), .on_press(Message::Clear)
pick_list(preset::ALL, Some(preset), Message::PresetPicked) .style(button::danger)
.padding(8) ]
.text_size(16), .spacing(10)
button("Clear")
.on_press(Message::Clear)
.style(theme::Button::Destructive),
] ]
.padding(10) .padding(10)
.spacing(20) .spacing(20)

View file

@ -1,11 +1,10 @@
use iced::application; use iced::application;
use iced::theme::{self, Theme};
use iced::widget::{ use iced::widget::{
checkbox, column, container, horizontal_space, row, slider, text, checkbox, column, container, horizontal_space, row, slider, text, themer,
}; };
use iced::{gradient, window}; use iced::{gradient, window};
use iced::{ use iced::{
Alignment, Background, Color, Element, Length, Radians, Sandbox, Settings, Alignment, Color, Element, Length, Radians, Sandbox, Settings, Theme,
}; };
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
@ -71,20 +70,16 @@ impl Sandbox for Gradient {
transparent, transparent,
} = *self; } = *self;
let gradient_box = container(horizontal_space()) let gradient = gradient::Linear::new(angle)
.width(Length::Fill) .add_stop(0.0, start)
.height(Length::Fill) .add_stop(1.0, end);
.style(move |_: &_| {
let gradient = gradient::Linear::new(angle)
.add_stop(0.0, start)
.add_stop(1.0, end)
.into();
container::Appearance { let gradient_box = themer(
background: Some(Background::Gradient(gradient)), gradient,
..Default::default() container(horizontal_space())
} .width(Length::Fill)
}); .height(Length::Fill),
);
let angle_picker = row![ let angle_picker = row![
text("Angle").width(64), text("Angle").width(64),
@ -111,16 +106,14 @@ impl Sandbox for Gradient {
.into() .into()
} }
fn style(&self) -> theme::Application { fn style(&self, theme: &Theme) -> application::Appearance {
if self.transparent { if self.transparent {
theme::Application::custom(|theme: &Theme| { application::Appearance {
application::Appearance { background_color: Color::TRANSPARENT,
background_color: Color::TRANSPARENT, text_color: theme.palette().text,
text_color: theme.palette().text, }
}
})
} else { } else {
theme::Application::Default application::default(theme)
} }
} }
} }

View file

@ -1,25 +1,25 @@
use iced_wgpu::Renderer; use iced_wgpu::Renderer;
use iced_widget::{slider, text_input, Column, Row, Text}; use iced_widget::{column, container, row, slider, text, text_input};
use iced_winit::core::{Alignment, Color, Element, Length}; use iced_winit::core::alignment;
use iced_winit::core::{Color, Element, Length, Theme};
use iced_winit::runtime::{Command, Program}; use iced_winit::runtime::{Command, Program};
use iced_winit::style::Theme;
pub struct Controls { pub struct Controls {
background_color: Color, background_color: Color,
text: String, input: String,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Message { pub enum Message {
BackgroundColorChanged(Color), BackgroundColorChanged(Color),
TextChanged(String), InputChanged(String),
} }
impl Controls { impl Controls {
pub fn new() -> Controls { pub fn new() -> Controls {
Controls { Controls {
background_color: Color::BLACK, background_color: Color::BLACK,
text: String::default(), input: String::default(),
} }
} }
@ -38,8 +38,8 @@ impl Program for Controls {
Message::BackgroundColorChanged(color) => { Message::BackgroundColorChanged(color) => {
self.background_color = color; self.background_color = color;
} }
Message::TextChanged(text) => { Message::InputChanged(input) => {
self.text = text; self.input = input;
} }
} }
@ -48,60 +48,48 @@ impl Program for Controls {
fn view(&self) -> Element<Message, Theme, Renderer> { fn view(&self) -> Element<Message, Theme, Renderer> {
let background_color = self.background_color; let background_color = self.background_color;
let text = &self.text;
let sliders = Row::new() let sliders = row![
.width(500) slider(0.0..=1.0, background_color.r, move |r| {
.spacing(20) Message::BackgroundColorChanged(Color {
.push( r,
slider(0.0..=1.0, background_color.r, move |r| { ..background_color
Message::BackgroundColorChanged(Color {
r,
..background_color
})
}) })
.step(0.01), })
) .step(0.01),
.push( slider(0.0..=1.0, background_color.g, move |g| {
slider(0.0..=1.0, background_color.g, move |g| { Message::BackgroundColorChanged(Color {
Message::BackgroundColorChanged(Color { g,
g, ..background_color
..background_color
})
}) })
.step(0.01), })
) .step(0.01),
.push( slider(0.0..=1.0, background_color.b, move |b| {
slider(0.0..=1.0, background_color.b, move |b| { Message::BackgroundColorChanged(Color {
Message::BackgroundColorChanged(Color { b,
b, ..background_color
..background_color
})
}) })
.step(0.01), })
); .step(0.01),
]
.width(500)
.spacing(20);
Row::new() container(
.height(Length::Fill) column![
.align_items(Alignment::End) text("Background color").color(Color::WHITE),
.push( text(format!("{background_color:?}"))
Column::new().align_items(Alignment::End).push( .size(14)
Column::new() .color(Color::WHITE),
.padding(10) text_input("Placeholder", &self.input)
.spacing(10) .on_input(Message::InputChanged),
.push(Text::new("Background color").style(Color::WHITE)) sliders,
.push(sliders) ]
.push( .spacing(10),
Text::new(format!("{background_color:?}")) )
.size(14) .padding(10)
.style(Color::WHITE), .height(Length::Fill)
) .align_y(alignment::Vertical::Bottom)
.push( .into()
text_input("Placeholder", text)
.on_input(Message::TextChanged),
),
),
)
.into()
} }
} }

View file

@ -10,11 +10,10 @@ use iced_winit::conversion;
use iced_winit::core::mouse; use iced_winit::core::mouse;
use iced_winit::core::renderer; use iced_winit::core::renderer;
use iced_winit::core::window; use iced_winit::core::window;
use iced_winit::core::{Color, Font, Pixels, Size}; use iced_winit::core::{Color, Font, Pixels, Size, Theme};
use iced_winit::futures; use iced_winit::futures;
use iced_winit::runtime::program; use iced_winit::runtime::program;
use iced_winit::runtime::Debug; use iced_winit::runtime::Debug;
use iced_winit::style::Theme;
use iced_winit::winit; use iced_winit::winit;
use iced_winit::Clipboard; use iced_winit::Clipboard;

View file

@ -1,7 +1,6 @@
use iced::executor; use iced::executor;
use iced::keyboard; use iced::keyboard;
use iced::mouse; use iced::mouse;
use iced::theme;
use iced::widget::{ use iced::widget::{
button, canvas, checkbox, column, container, horizontal_space, pick_list, button, canvas, checkbox, column, container, horizontal_space, pick_list,
row, scrollable, text, row, scrollable, text,
@ -98,7 +97,7 @@ impl Application for Layout {
} else { } else {
self.example.view() self.example.view()
}) })
.style(|theme: &Theme| { .style(|theme, _status| {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Appearance::default() container::Appearance::default()
@ -262,7 +261,7 @@ fn application<'a>() -> Element<'a, Message> {
.padding(10) .padding(10)
.align_items(Alignment::Center), .align_items(Alignment::Center),
) )
.style(|theme: &Theme| { .style(|theme, _status| {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Appearance::default() container::Appearance::default()
@ -276,7 +275,7 @@ fn application<'a>() -> Element<'a, Message> {
.width(200) .width(200)
.align_items(Alignment::Center), .align_items(Alignment::Center),
) )
.style(theme::Container::Box) .style(container::box_)
.height(Length::Fill) .height(Length::Fill)
.center_y(); .center_y();

View file

@ -1,4 +1,3 @@
use iced::theme;
use iced::widget::{ use iced::widget::{
button, column, horizontal_space, lazy, pick_list, row, scrollable, text, button, column, horizontal_space, lazy, pick_list, row, scrollable, text,
text_input, text_input,
@ -181,11 +180,10 @@ impl Sandbox for App {
column(items.into_iter().map(|item| { column(items.into_iter().map(|item| {
let button = button("Delete") let button = button("Delete")
.on_press(Message::DeleteItem(item.clone())) .on_press(Message::DeleteItem(item.clone()))
.style(theme::Button::Destructive); .style(button::danger);
row![ row![
text(&item.name) text(&item.name).color(item.color),
.style(theme::Text::Color(item.color.into())),
horizontal_space(), horizontal_space(),
pick_list(Color::ALL, Some(item.color), move |color| { pick_list(Color::ALL, Some(item.color), move |color| {
Message::ItemColorChanged(item.clone(), color) Message::ItemColorChanged(item.clone(), color)

View file

@ -2,7 +2,6 @@ use iced::event::{self, Event};
use iced::executor; use iced::executor;
use iced::keyboard; use iced::keyboard;
use iced::keyboard::key; use iced::keyboard::key;
use iced::theme;
use iced::widget::{ use iced::widget::{
self, button, column, container, horizontal_space, pick_list, row, text, self, button, column, container, horizontal_space, pick_list, row, text,
text_input, text_input,
@ -175,7 +174,7 @@ impl Application for App {
) )
.width(300) .width(300)
.padding(10) .padding(10)
.style(theme::Container::Box); .style(container::box_);
Modal::new(content, modal) Modal::new(content, modal)
.on_blur(Message::HideModal) .on_blur(Message::HideModal)

View file

@ -1,13 +1,13 @@
use iced::alignment::{self, Alignment}; use iced::alignment::{self, Alignment};
use iced::executor; use iced::executor;
use iced::keyboard; use iced::keyboard;
use iced::theme::{self, Theme};
use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::pane_grid::{self, PaneGrid};
use iced::widget::{ use iced::widget::{
button, column, container, responsive, row, scrollable, text, button, column, container, responsive, row, scrollable, text,
}; };
use iced::{ use iced::{
Application, Color, Command, Element, Length, Settings, Size, Subscription, Application, Color, Command, Element, Length, Settings, Size, Subscription,
Theme,
}; };
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
@ -162,7 +162,7 @@ impl Application for Example {
let title = row![ let title = row![
pin_button, pin_button,
"Pane", "Pane",
text(pane.id.to_string()).style(if is_focused { text(pane.id.to_string()).color(if is_focused {
PANE_ID_COLOR_FOCUSED PANE_ID_COLOR_FOCUSED
} else { } else {
PANE_ID_COLOR_UNFOCUSED PANE_ID_COLOR_UNFOCUSED
@ -287,10 +287,7 @@ fn view_content<'a>(
) )
] ]
.push_maybe(if total_panes > 1 && !is_pinned { .push_maybe(if total_panes > 1 && !is_pinned {
Some( Some(button("Close", Message::Close(pane)).style(button::danger))
button("Close", Message::Close(pane))
.style(theme::Button::Destructive),
)
} else { } else {
None None
}) })
@ -327,7 +324,7 @@ fn view_controls<'a>(
Some( Some(
button(text(content).size(14)) button(text(content).size(14))
.style(theme::Button::Secondary) .style(button::secondary)
.padding(3) .padding(3)
.on_press(message), .on_press(message),
) )
@ -336,7 +333,7 @@ fn view_controls<'a>(
}); });
let close = button(text("Close").size(14)) let close = button(text("Close").size(14))
.style(theme::Button::Destructive) .style(button::danger)
.padding(3) .padding(3)
.on_press_maybe(if total_panes > 1 && !is_pinned { .on_press_maybe(if total_panes > 1 && !is_pinned {
Some(Message::Close(pane)) Some(Message::Close(pane))
@ -351,7 +348,10 @@ mod style {
use iced::widget::container; use iced::widget::container;
use iced::{Border, Theme}; use iced::{Border, Theme};
pub fn title_bar_active(theme: &Theme) -> container::Appearance { pub fn title_bar_active(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Appearance { container::Appearance {
@ -361,7 +361,10 @@ mod style {
} }
} }
pub fn title_bar_focused(theme: &Theme) -> container::Appearance { pub fn title_bar_focused(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Appearance { container::Appearance {
@ -371,7 +374,10 @@ mod style {
} }
} }
pub fn pane_active(theme: &Theme) -> container::Appearance { pub fn pane_active(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Appearance { container::Appearance {
@ -385,7 +391,10 @@ mod style {
} }
} }
pub fn pane_focused(theme: &Theme) -> container::Appearance { pub fn pane_focused(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette(); let palette = theme.extended_palette();
container::Appearance { container::Appearance {

View file

@ -1,8 +1,6 @@
use iced::futures; use iced::futures;
use iced::widget::{self, column, container, image, row, text}; use iced::widget::{self, column, container, image, row, text};
use iced::{ use iced::{Alignment, Application, Command, Element, Length, Settings, Theme};
Alignment, Application, Color, Command, Element, Length, Settings, Theme,
};
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
Pokedex::run(Settings::default()) Pokedex::run(Settings::default())
@ -116,7 +114,7 @@ impl Pokemon {
text(&self.name).size(30).width(Length::Fill), text(&self.name).size(30).width(Length::Fill),
text(format!("#{}", self.number)) text(format!("#{}", self.number))
.size(20) .size(20)
.style(Color::from([0.5, 0.5, 0.5])), .color([0.5, 0.5, 0.5]),
] ]
.align_items(Alignment::Center) .align_items(Alignment::Center)
.spacing(20), .spacing(20),

View file

@ -1,7 +1,6 @@
use iced::alignment; use iced::alignment;
use iced::executor; use iced::executor;
use iced::keyboard; use iced::keyboard;
use iced::theme;
use iced::widget::{button, column, container, image, row, text, text_input}; use iced::widget::{button, column, container, image, row, text, text_input};
use iced::window; use iced::window;
use iced::window::screenshot::{self, Screenshot}; use iced::window::screenshot::{self, Screenshot};
@ -149,7 +148,7 @@ impl Application for Example {
let image = container(image) let image = container(image)
.padding(10) .padding(10)
.style(theme::Container::Box) .style(container::box_)
.width(Length::FillPortion(2)) .width(Length::FillPortion(2))
.height(Length::Fill) .height(Length::Fill)
.center_x() .center_x()
@ -216,9 +215,9 @@ impl Application for Example {
) )
} else { } else {
button(centered_text("Saving...")) button(centered_text("Saving..."))
.style(theme::Button::Secondary) .style(button::secondary)
} }
.style(theme::Button::Secondary) .style(button::secondary)
.padding([10, 20, 10, 20]) .padding([10, 20, 10, 20])
.width(Length::Fill) .width(Length::Fill)
] ]
@ -227,7 +226,7 @@ impl Application for Example {
crop_controls, crop_controls,
button(centered_text("Crop")) button(centered_text("Crop"))
.on_press(Message::Crop) .on_press(Message::Crop)
.style(theme::Button::Destructive) .style(button::danger)
.padding([10, 20, 10, 20]) .padding([10, 20, 10, 20])
.width(Length::Fill), .width(Length::Fill),
] ]

View file

@ -5,7 +5,8 @@ use iced::widget::{
scrollable, slider, text, vertical_space, Scrollable, scrollable, slider, text, vertical_space, Scrollable,
}; };
use iced::{ use iced::{
Alignment, Application, Color, Command, Element, Length, Settings, Theme, Alignment, Application, Border, Color, Command, Element, Length, Settings,
Theme,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -348,6 +349,6 @@ fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance {
progress_bar::Appearance { progress_bar::Appearance {
background: theme.extended_palette().background.strong.color.into(), background: theme.extended_palette().background.strong.color.into(),
bar: Color::from_rgb8(250, 85, 134).into(), bar: Color::from_rgb8(250, 85, 134).into(),
border_radius: 0.0.into(), border: Border::default(),
} }
} }

View file

@ -9,7 +9,6 @@
use iced::application; use iced::application;
use iced::executor; use iced::executor;
use iced::mouse; use iced::mouse;
use iced::theme::{self, Theme};
use iced::widget::canvas; use iced::widget::canvas;
use iced::widget::canvas::gradient; use iced::widget::canvas::gradient;
use iced::widget::canvas::stroke::{self, Stroke}; use iced::widget::canvas::stroke::{self, Stroke};
@ -17,7 +16,7 @@ use iced::widget::canvas::Path;
use iced::window; use iced::window;
use iced::{ use iced::{
Application, Color, Command, Element, Length, Point, Rectangle, Renderer, Application, Color, Command, Element, Length, Point, Rectangle, Renderer,
Settings, Size, Subscription, Vector, Settings, Size, Subscription, Theme, Vector,
}; };
use std::time::Instant; use std::time::Instant;
@ -80,15 +79,11 @@ impl Application for SolarSystem {
Theme::Dark Theme::Dark
} }
fn style(&self) -> theme::Application { fn style(&self, _theme: &Theme) -> application::Appearance {
fn dark_background(_theme: &Theme) -> application::Appearance { application::Appearance {
application::Appearance { background_color: Color::BLACK,
background_color: Color::BLACK, text_color: Color::WHITE,
text_color: Color::WHITE,
}
} }
theme::Application::custom(dark_background)
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {

View file

@ -1,11 +1,11 @@
use iced::alignment; use iced::alignment;
use iced::executor; use iced::executor;
use iced::keyboard; use iced::keyboard;
use iced::theme::{self, Theme};
use iced::time; use iced::time;
use iced::widget::{button, column, container, row, text}; use iced::widget::{button, column, container, row, text};
use iced::{ use iced::{
Alignment, Application, Command, Element, Length, Settings, Subscription, Alignment, Application, Command, Element, Length, Settings, Subscription,
Theme,
}; };
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -136,7 +136,7 @@ impl Application for Stopwatch {
}; };
let reset_button = button("Reset") let reset_button = button("Reset")
.style(theme::Button::Destructive) .style(button::danger)
.on_press(Message::Reset); .on_press(Message::Reset);
let controls = row![toggle_button, reset_button].spacing(20); let controls = row![toggle_button, reset_button].spacing(20);

View file

@ -1,4 +1,3 @@
use iced::theme;
use iced::widget::{checkbox, column, container, svg}; use iced::widget::{checkbox, column, container, svg};
use iced::{color, Element, Length, Sandbox, Settings}; use iced::{color, Element, Length, Sandbox, Settings};
@ -43,11 +42,11 @@ impl Sandbox for Tiger {
let svg = svg(handle).width(Length::Fill).height(Length::Fill).style( let svg = svg(handle).width(Length::Fill).height(Length::Fill).style(
if self.apply_color_filter { if self.apply_color_filter {
theme::Svg::custom_fn(|_theme| svg::Appearance { |_theme, _status| svg::Appearance {
color: Some(color!(0x0000ff)), color: Some(color!(0x0000ff)),
}) }
} else { } else {
theme::Svg::Default |_theme, _status| svg::Appearance::default()
}, },
); );

View file

@ -209,27 +209,6 @@ mod toast {
&[Self::Primary, Self::Secondary, Self::Success, Self::Danger]; &[Self::Primary, Self::Secondary, Self::Success, Self::Danger];
} }
impl container::StyleSheet for Status {
type Style = Theme;
fn appearance(&self, theme: &Theme) -> container::Appearance {
let palette = theme.extended_palette();
let pair = match self {
Status::Primary => palette.primary.weak,
Status::Secondary => palette.secondary.weak,
Status::Success => palette.success.weak,
Status::Danger => palette.danger.weak,
};
container::Appearance {
background: Some(pair.color.into()),
text_color: pair.text.into(),
..Default::default()
}
}
}
impl fmt::Display for Status { impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
@ -282,14 +261,17 @@ mod toast {
) )
.width(Length::Fill) .width(Length::Fill)
.padding(5) .padding(5)
.style( .style(match toast.status {
theme::Container::Custom(Box::new(toast.status)) Status::Primary => primary,
), Status::Secondary => secondary,
Status::Success => success,
Status::Danger => danger,
}),
horizontal_rule(1), horizontal_rule(1),
container(text(toast.body.as_str())) container(text(toast.body.as_str()))
.width(Length::Fill) .width(Length::Fill)
.padding(5) .padding(5)
.style(theme::Container::Box), .style(container::box_),
]) ])
.max_width(200) .max_width(200)
.into() .into()
@ -676,4 +658,48 @@ mod toast {
Element::new(manager) Element::new(manager)
} }
} }
fn styled(pair: theme::palette::Pair) -> container::Appearance {
container::Appearance {
background: Some(pair.color.into()),
text_color: pair.text.into(),
..Default::default()
}
}
fn primary(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette();
styled(palette.primary.weak)
}
fn secondary(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette();
styled(palette.secondary.weak)
}
fn success(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette();
styled(palette.success.weak)
}
fn danger(
theme: &Theme,
_status: container::Status,
) -> container::Appearance {
let palette = theme.extended_palette();
styled(palette.danger.weak)
}
} }

View file

@ -1,14 +1,14 @@
use iced::alignment::{self, Alignment}; use iced::alignment::{self, Alignment};
use iced::font::{self, Font}; use iced::font::{self, Font};
use iced::keyboard; use iced::keyboard;
use iced::theme::{self, Theme};
use iced::widget::{ use iced::widget::{
self, button, checkbox, column, container, keyed_column, row, scrollable, self, button, checkbox, column, container, keyed_column, row, scrollable,
text, text_input, Text, text, text_input, Text,
}; };
use iced::window; use iced::window;
use iced::{Application, Element}; use iced::{
use iced::{Color, Command, Length, Settings, Size, Subscription}; Application, Command, Element, Length, Settings, Size, Subscription, Theme,
};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -209,7 +209,7 @@ impl Application for Todos {
let title = text("todos") let title = text("todos")
.width(Length::Fill) .width(Length::Fill)
.size(100) .size(100)
.style(Color::from([0.5, 0.5, 0.5])) .color([0.5, 0.5, 0.5])
.horizontal_alignment(alignment::Horizontal::Center); .horizontal_alignment(alignment::Horizontal::Center);
let input = text_input("What needs to be done?", input_value) let input = text_input("What needs to be done?", input_value)
@ -355,6 +355,7 @@ impl Task {
let checkbox = checkbox(&self.description, self.completed) let checkbox = checkbox(&self.description, self.completed)
.on_toggle(TaskMessage::Completed) .on_toggle(TaskMessage::Completed)
.width(Length::Fill) .width(Length::Fill)
.size(17)
.text_shaping(text::Shaping::Advanced); .text_shaping(text::Shaping::Advanced);
row![ row![
@ -362,7 +363,7 @@ impl Task {
button(edit_icon()) button(edit_icon())
.on_press(TaskMessage::Edit) .on_press(TaskMessage::Edit)
.padding(10) .padding(10)
.style(theme::Button::Text), .style(button::text),
] ]
.spacing(20) .spacing(20)
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -385,7 +386,7 @@ impl Task {
) )
.on_press(TaskMessage::Delete) .on_press(TaskMessage::Delete)
.padding(10) .padding(10)
.style(theme::Button::Destructive) .style(button::danger)
] ]
.spacing(20) .spacing(20)
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -402,9 +403,9 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> {
let label = text(label); let label = text(label);
let button = button(label).style(if filter == current_filter { let button = button(label).style(if filter == current_filter {
theme::Button::Primary button::primary
} else { } else {
theme::Button::Text button::text
}); });
button.on_press(Message::FilterChanged(filter)).padding(8) button.on_press(Message::FilterChanged(filter)).padding(8)
@ -467,7 +468,7 @@ fn empty_message(message: &str) -> Element<'_, Message> {
.width(Length::Fill) .width(Length::Fill)
.size(25) .size(25)
.horizontal_alignment(alignment::Horizontal::Center) .horizontal_alignment(alignment::Horizontal::Center)
.style(Color::from([0.7, 0.7, 0.7])), .color([0.7, 0.7, 0.7]),
) )
.height(200) .height(200)
.center_y() .center_y()

View file

@ -1,4 +1,3 @@
use iced::theme;
use iced::widget::tooltip::Position; use iced::widget::tooltip::Position;
use iced::widget::{button, container, tooltip}; use iced::widget::{button, container, tooltip};
use iced::{Element, Length, Sandbox, Settings}; use iced::{Element, Length, Sandbox, Settings};
@ -53,7 +52,7 @@ impl Sandbox for Example {
self.position, self.position,
) )
.gap(10) .gap(10)
.style(theme::Container::Box); .style(container::box_);
container(tooltip) container(tooltip)
.width(Length::Fill) .width(Length::Fill)

View file

@ -1,7 +1,6 @@
use iced::alignment::{self, Alignment}; use iced::alignment::{self, Alignment};
use iced::theme;
use iced::widget::{ use iced::widget::{
checkbox, column, container, horizontal_space, image, radio, row, button, checkbox, column, container, horizontal_space, image, radio, row,
scrollable, slider, text, text_input, toggler, vertical_space, scrollable, slider, text, text_input, toggler, vertical_space,
}; };
use iced::widget::{Button, Column, Container, Slider}; use iced::widget::{Button, Column, Container, Slider};
@ -56,18 +55,17 @@ impl Sandbox for Tour {
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let Tour { steps, .. } = self; let Tour { steps, .. } = self;
let controls = row![] let controls =
.push_maybe(steps.has_previous().then(|| { row![]
button("Back") .push_maybe(steps.has_previous().then(|| {
.on_press(Message::BackPressed) padded_button("Back")
.style(theme::Button::Secondary) .on_press(Message::BackPressed)
})) .style(button::secondary)
.push(horizontal_space()) }))
.push_maybe( .push(horizontal_space())
steps .push_maybe(steps.can_continue().then(|| {
.can_continue() padded_button("Next").on_press(Message::NextPressed)
.then(|| button("Next").on_press(Message::NextPressed)), }));
);
let content: Element<_> = column![ let content: Element<_> = column![
steps.view(self.debug).map(Message::StepMessage), steps.view(self.debug).map(Message::StepMessage),
@ -474,7 +472,7 @@ impl<'a> Step {
let color_section = column![ let color_section = column![
"And its color:", "And its color:",
text(format!("{color:?}")).style(color), text(format!("{color:?}")).color(color),
color_sliders, color_sliders,
] ]
.padding(20) .padding(20)
@ -676,8 +674,8 @@ fn ferris<'a>(
.center_x() .center_x()
} }
fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { fn padded_button<'a, Message: Clone>(label: &str) -> Button<'a, Message> {
iced::widget::button(text(label)).padding([12, 24]) button(text(label)).padding([12, 24])
} }
fn color_slider<'a>( fn color_slider<'a>(

View file

@ -1,14 +1,13 @@
use iced::event::{self, Event}; use iced::event::{self, Event};
use iced::executor; use iced::executor;
use iced::mouse; use iced::mouse;
use iced::theme::{self, Theme};
use iced::widget::{ use iced::widget::{
column, container, horizontal_space, row, scrollable, text, vertical_space, column, container, horizontal_space, row, scrollable, text, vertical_space,
}; };
use iced::window; use iced::window;
use iced::{ use iced::{
Alignment, Application, Color, Command, Element, Font, Length, Point, Alignment, Application, Color, Command, Element, Font, Length, Point,
Rectangle, Settings, Subscription, Rectangle, Settings, Subscription, Theme,
}; };
pub fn main() -> iced::Result { pub fn main() -> iced::Result {
@ -82,7 +81,10 @@ impl Application for Example {
row![ row![
text(label), text(label),
horizontal_space(), horizontal_space(),
text(value).font(Font::MONOSPACE).size(14).style(color), text(value)
.font(Font::MONOSPACE)
.size(14)
.color_maybe(color),
] ]
.height(40) .height(40)
.align_items(Alignment::Center) .align_items(Alignment::Center)
@ -102,13 +104,12 @@ impl Application for Example {
}) })
.unwrap_or_default() .unwrap_or_default()
{ {
Color { Some(Color {
g: 1.0, g: 1.0,
..Color::BLACK ..Color::BLACK
} })
.into()
} else { } else {
theme::Text::Default None
}, },
) )
}; };
@ -120,7 +121,7 @@ impl Application for Example {
Some(Point { x, y }) => format!("({x}, {y})"), Some(Point { x, y }) => format!("({x}, {y})"),
None => "unknown".to_string(), None => "unknown".to_string(),
}, },
theme::Text::Default, None,
), ),
view_bounds("Outer container", self.outer_bounds), view_bounds("Outer container", self.outer_bounds),
view_bounds("Inner container", self.inner_bounds), view_bounds("Inner container", self.inner_bounds),
@ -131,7 +132,7 @@ impl Application for Example {
container(text("I am the outer container!")) container(text("I am the outer container!"))
.id(OUTER_CONTAINER.clone()) .id(OUTER_CONTAINER.clone())
.padding(40) .padding(40)
.style(theme::Container::Box), .style(container::box_),
vertical_space().height(400), vertical_space().height(400),
scrollable( scrollable(
column![ column![
@ -140,7 +141,7 @@ impl Application for Example {
container(text("I am the inner container!")) container(text("I am the inner container!"))
.id(INNER_CONTAINER.clone()) .id(INNER_CONTAINER.clone())
.padding(40) .padding(40)
.style(theme::Container::Box), .style(container::box_),
vertical_space().height(400), vertical_space().height(400),
] ]
.padding(20) .padding(20)

View file

@ -6,7 +6,7 @@ use iced::widget::{
button, column, container, row, scrollable, text, text_input, button, column, container, row, scrollable, text, text_input,
}; };
use iced::{ use iced::{
Application, Color, Command, Element, Length, Settings, Subscription, Theme, color, Application, Command, Element, Length, Settings, Subscription, Theme,
}; };
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -99,7 +99,7 @@ impl Application for WebSocket {
let message_log: Element<_> = if self.messages.is_empty() { let message_log: Element<_> = if self.messages.is_empty() {
container( container(
text("Your messages will appear here...") text("Your messages will appear here...")
.style(Color::from_rgb8(0x88, 0x88, 0x88)), .color(color!(0x888888)),
) )
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)

View file

@ -1,7 +1,9 @@
//! Build interactive cross-platform applications. //! Build interactive cross-platform applications.
use crate::{Command, Element, Executor, Settings, Subscription}; use crate::{Command, Element, Executor, Settings, Subscription};
pub use crate::style::application::{Appearance, StyleSheet}; use crate::shell::application;
pub use application::{default, Appearance, DefaultStyle};
/// An interactive cross-platform application. /// An interactive cross-platform application.
/// ///
@ -91,7 +93,10 @@ pub use crate::style::application::{Appearance, StyleSheet};
/// } /// }
/// } /// }
/// ``` /// ```
pub trait Application: Sized { pub trait Application: Sized
where
Self::Theme: DefaultStyle,
{
/// The [`Executor`] that will run commands and subscriptions. /// The [`Executor`] that will run commands and subscriptions.
/// ///
/// The [default executor] can be a good starting point! /// The [default executor] can be a good starting point!
@ -104,7 +109,7 @@ pub trait Application: Sized {
type Message: std::fmt::Debug + Send; type Message: std::fmt::Debug + Send;
/// The theme of your [`Application`]. /// The theme of your [`Application`].
type Theme: Default + StyleSheet; type Theme: Default;
/// The data needed to initialize your [`Application`]. /// The data needed to initialize your [`Application`].
type Flags; type Flags;
@ -148,11 +153,9 @@ pub trait Application: Sized {
Self::Theme::default() Self::Theme::default()
} }
/// Returns the current `Style` of the [`Theme`]. /// Returns the current [`Appearance`] of the [`Application`].
/// fn style(&self, theme: &Self::Theme) -> Appearance {
/// [`Theme`]: Self::Theme theme.default_style()
fn style(&self) -> <Self::Theme as StyleSheet>::Style {
<Self::Theme as StyleSheet>::Style::default()
} }
/// Returns the event [`Subscription`] for the current state of the /// Returns the event [`Subscription`] for the current state of the
@ -228,11 +231,15 @@ pub trait Application: Sized {
} }
} }
struct Instance<A: Application>(A); struct Instance<A>(A)
where
A: Application,
A::Theme: DefaultStyle;
impl<A> crate::runtime::Program for Instance<A> impl<A> crate::runtime::Program for Instance<A>
where where
A: Application, A: Application,
A::Theme: DefaultStyle,
{ {
type Message = A::Message; type Message = A::Message;
type Theme = A::Theme; type Theme = A::Theme;
@ -247,9 +254,10 @@ where
} }
} }
impl<A> crate::shell::Application for Instance<A> impl<A> application::Application for Instance<A>
where where
A: Application, A: Application,
A::Theme: DefaultStyle,
{ {
type Flags = A::Flags; type Flags = A::Flags;
@ -267,8 +275,8 @@ where
self.0.theme() self.0.theme()
} }
fn style(&self) -> <A::Theme as StyleSheet>::Style { fn style(&self, theme: &A::Theme) -> Appearance {
self.0.style() self.0.style(theme)
} }
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> Subscription<Self::Message> {

View file

@ -162,7 +162,6 @@
#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
use iced_widget::graphics; use iced_widget::graphics;
use iced_widget::renderer; use iced_widget::renderer;
use iced_widget::style;
use iced_winit as shell; use iced_winit as shell;
use iced_winit::core; use iced_winit::core;
use iced_winit::runtime; use iced_winit::runtime;
@ -186,15 +185,14 @@ pub mod advanced;
#[cfg(feature = "multi-window")] #[cfg(feature = "multi-window")]
pub mod multi_window; pub mod multi_window;
pub use style::theme;
pub use crate::core::alignment; pub use crate::core::alignment;
pub use crate::core::border; pub use crate::core::border;
pub use crate::core::color; pub use crate::core::color;
pub use crate::core::gradient; pub use crate::core::gradient;
pub use crate::core::theme;
pub use crate::core::{ pub use crate::core::{
Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, Alignment, Background, Border, Color, ContentFit, Degrees, Gradient,
Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Theme,
Transformation, Vector, Transformation, Vector,
}; };
@ -314,7 +312,6 @@ pub use renderer::Renderer;
pub use sandbox::Sandbox; pub use sandbox::Sandbox;
pub use settings::Settings; pub use settings::Settings;
pub use subscription::Subscription; pub use subscription::Subscription;
pub use theme::Theme;
/// A generic widget. /// A generic widget.
/// ///

View file

@ -1,4 +1,256 @@
//! Leverage multi-window support in your application. //! Leverage multi-window support in your application.
mod application; use crate::window;
use crate::{Command, Element, Executor, Settings, Subscription};
pub use application::Application; pub use crate::application::{Appearance, DefaultStyle};
/// An interactive cross-platform multi-window application.
///
/// This trait is the main entrypoint of Iced. Once implemented, you can run
/// your GUI application by simply calling [`run`](#method.run).
///
/// - On native platforms, it will run in its own windows.
/// - On the web, it will take control of the `<title>` and the `<body>` of the
/// document and display only the contents of the `window::Id::MAIN` window.
///
/// An [`Application`] can execute asynchronous actions by returning a
/// [`Command`] in some of its methods. If you do not intend to perform any
/// background work in your program, the [`Sandbox`] trait offers a simplified
/// interface.
///
/// When using an [`Application`] with the `debug` feature enabled, a debug view
/// can be toggled by pressing `F12`.
///
/// # Examples
/// See the `examples/multi-window` example to see this multi-window `Application` trait in action.
///
/// ## A simple "Hello, world!"
///
/// If you just want to get started, here is a simple [`Application`] that
/// says "Hello, world!":
///
/// ```no_run
/// use iced::{executor, window};
/// use iced::{Command, Element, Settings, Theme};
/// use iced::multi_window::{self, Application};
///
/// pub fn main() -> iced::Result {
/// Hello::run(Settings::default())
/// }
///
/// struct Hello;
///
/// impl multi_window::Application for Hello {
/// type Executor = executor::Default;
/// type Flags = ();
/// type Message = ();
/// type Theme = Theme;
///
/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
/// (Hello, Command::none())
/// }
///
/// fn title(&self, _window: window::Id) -> String {
/// String::from("A cool application")
/// }
///
/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
/// Command::none()
/// }
///
/// fn view(&self, _window: window::Id) -> Element<Self::Message> {
/// "Hello, world!".into()
/// }
/// }
/// ```
///
/// [`Sandbox`]: crate::Sandbox
pub trait Application: Sized
where
Self::Theme: DefaultStyle,
{
/// The [`Executor`] that will run commands and subscriptions.
///
/// The [default executor] can be a good starting point!
///
/// [`Executor`]: Self::Executor
/// [default executor]: crate::executor::Default
type Executor: Executor;
/// The type of __messages__ your [`Application`] will produce.
type Message: std::fmt::Debug + Send;
/// The theme of your [`Application`].
type Theme: Default;
/// The data needed to initialize your [`Application`].
type Flags;
/// Initializes the [`Application`] with the flags provided to
/// [`run`] as part of the [`Settings`].
///
/// Here is where you should return the initial state of your app.
///
/// Additionally, you can return a [`Command`] if you need to perform some
/// async action in the background on startup. This is useful if you want to
/// load state from a file, perform an initial HTTP request, etc.
///
/// [`run`]: Self::run
fn new(flags: Self::Flags) -> (Self, Command<Self::Message>);
/// Returns the current title of the `window` of the [`Application`].
///
/// This title can be dynamic! The runtime will automatically update the
/// title of your window when necessary.
fn title(&self, window: window::Id) -> String;
/// Handles a __message__ and updates the state of the [`Application`].
///
/// This is where you define your __update logic__. All the __messages__,
/// produced by either user interactions or commands, will be handled by
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the background.
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// Returns the widgets to display in the `window` of the [`Application`].
///
/// These widgets can produce __messages__ based on user interaction.
fn view(
&self,
window: window::Id,
) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>;
/// Returns the current [`Theme`] of the `window` of the [`Application`].
///
/// [`Theme`]: Self::Theme
#[allow(unused_variables)]
fn theme(&self, window: window::Id) -> Self::Theme {
Self::Theme::default()
}
/// Returns the current `Style` of the [`Theme`].
///
/// [`Theme`]: Self::Theme
fn style(&self, theme: &Self::Theme) -> Appearance {
Self::Theme::default_style(theme)
}
/// Returns the event [`Subscription`] for the current state of the
/// application.
///
/// A [`Subscription`] will be kept alive as long as you keep returning it,
/// and the __messages__ produced will be handled by
/// [`update`](#tymethod.update).
///
/// By default, this method returns an empty [`Subscription`].
fn subscription(&self) -> Subscription<Self::Message> {
Subscription::none()
}
/// Returns the scale factor of the `window` of the [`Application`].
///
/// It can be used to dynamically control the size of the UI at runtime
/// (i.e. zooming).
///
/// For instance, a scale factor of `2.0` will make widgets twice as big,
/// while a scale factor of `0.5` will shrink them to half their size.
///
/// By default, it returns `1.0`.
#[allow(unused_variables)]
fn scale_factor(&self, window: window::Id) -> f64 {
1.0
}
/// Runs the multi-window [`Application`].
///
/// On native platforms, this method will take control of the current thread
/// until the [`Application`] exits.
///
/// On the web platform, this method __will NOT return__ unless there is an
/// [`Error`] during startup.
///
/// [`Error`]: crate::Error
fn run(settings: Settings<Self::Flags>) -> crate::Result
where
Self: 'static,
{
#[allow(clippy::needless_update)]
let renderer_settings = crate::renderer::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
antialiasing: if settings.antialiasing {
Some(crate::graphics::Antialiasing::MSAAx4)
} else {
None
},
..crate::renderer::Settings::default()
};
Ok(crate::shell::multi_window::run::<
Instance<Self>,
Self::Executor,
crate::renderer::Compositor,
>(settings.into(), renderer_settings)?)
}
}
struct Instance<A>(A)
where
A: Application,
A::Theme: DefaultStyle;
impl<A> crate::runtime::multi_window::Program for Instance<A>
where
A: Application,
A::Theme: DefaultStyle,
{
type Message = A::Message;
type Theme = A::Theme;
type Renderer = crate::Renderer;
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
self.0.update(message)
}
fn view(
&self,
window: window::Id,
) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> {
self.0.view(window)
}
}
impl<A> crate::shell::multi_window::Application for Instance<A>
where
A: Application,
A::Theme: DefaultStyle,
{
type Flags = A::Flags;
fn new(flags: Self::Flags) -> (Self, Command<A::Message>) {
let (app, command) = A::new(flags);
(Instance(app), command)
}
fn title(&self, window: window::Id) -> String {
self.0.title(window)
}
fn theme(&self, window: window::Id) -> A::Theme {
self.0.theme(window)
}
fn style(&self, theme: &Self::Theme) -> Appearance {
self.0.style(theme)
}
fn subscription(&self) -> Subscription<Self::Message> {
self.0.subscription()
}
fn scale_factor(&self, window: window::Id) -> f64 {
self.0.scale_factor(window)
}
}

View file

@ -1,246 +0,0 @@
use crate::style::application::StyleSheet;
use crate::window;
use crate::{Command, Element, Executor, Settings, Subscription};
/// An interactive cross-platform multi-window application.
///
/// This trait is the main entrypoint of Iced. Once implemented, you can run
/// your GUI application by simply calling [`run`](#method.run).
///
/// - On native platforms, it will run in its own windows.
/// - On the web, it will take control of the `<title>` and the `<body>` of the
/// document and display only the contents of the `window::Id::MAIN` window.
///
/// An [`Application`] can execute asynchronous actions by returning a
/// [`Command`] in some of its methods. If you do not intend to perform any
/// background work in your program, the [`Sandbox`] trait offers a simplified
/// interface.
///
/// When using an [`Application`] with the `debug` feature enabled, a debug view
/// can be toggled by pressing `F12`.
///
/// # Examples
/// See the `examples/multi-window` example to see this multi-window `Application` trait in action.
///
/// ## A simple "Hello, world!"
///
/// If you just want to get started, here is a simple [`Application`] that
/// says "Hello, world!":
///
/// ```no_run
/// use iced::{executor, window};
/// use iced::{Command, Element, Settings, Theme};
/// use iced::multi_window::{self, Application};
///
/// pub fn main() -> iced::Result {
/// Hello::run(Settings::default())
/// }
///
/// struct Hello;
///
/// impl multi_window::Application for Hello {
/// type Executor = executor::Default;
/// type Flags = ();
/// type Message = ();
/// type Theme = Theme;
///
/// fn new(_flags: ()) -> (Hello, Command<Self::Message>) {
/// (Hello, Command::none())
/// }
///
/// fn title(&self, _window: window::Id) -> String {
/// String::from("A cool application")
/// }
///
/// fn update(&mut self, _message: Self::Message) -> Command<Self::Message> {
/// Command::none()
/// }
///
/// fn view(&self, _window: window::Id) -> Element<Self::Message> {
/// "Hello, world!".into()
/// }
/// }
/// ```
///
/// [`Sandbox`]: crate::Sandbox
pub trait Application: Sized {
/// The [`Executor`] that will run commands and subscriptions.
///
/// The [default executor] can be a good starting point!
///
/// [`Executor`]: Self::Executor
/// [default executor]: crate::executor::Default
type Executor: Executor;
/// The type of __messages__ your [`Application`] will produce.
type Message: std::fmt::Debug + Send;
/// The theme of your [`Application`].
type Theme: Default + StyleSheet;
/// The data needed to initialize your [`Application`].
type Flags;
/// Initializes the [`Application`] with the flags provided to
/// [`run`] as part of the [`Settings`].
///
/// Here is where you should return the initial state of your app.
///
/// Additionally, you can return a [`Command`] if you need to perform some
/// async action in the background on startup. This is useful if you want to
/// load state from a file, perform an initial HTTP request, etc.
///
/// [`run`]: Self::run
fn new(flags: Self::Flags) -> (Self, Command<Self::Message>);
/// Returns the current title of the `window` of the [`Application`].
///
/// This title can be dynamic! The runtime will automatically update the
/// title of your window when necessary.
fn title(&self, window: window::Id) -> String;
/// Handles a __message__ and updates the state of the [`Application`].
///
/// This is where you define your __update logic__. All the __messages__,
/// produced by either user interactions or commands, will be handled by
/// this method.
///
/// Any [`Command`] returned will be executed immediately in the background.
fn update(&mut self, message: Self::Message) -> Command<Self::Message>;
/// Returns the widgets to display in the `window` of the [`Application`].
///
/// These widgets can produce __messages__ based on user interaction.
fn view(
&self,
window: window::Id,
) -> Element<'_, Self::Message, Self::Theme, crate::Renderer>;
/// Returns the current [`Theme`] of the `window` of the [`Application`].
///
/// [`Theme`]: Self::Theme
#[allow(unused_variables)]
fn theme(&self, window: window::Id) -> Self::Theme {
Self::Theme::default()
}
/// Returns the current `Style` of the [`Theme`].
///
/// [`Theme`]: Self::Theme
fn style(&self) -> <Self::Theme as StyleSheet>::Style {
<Self::Theme as StyleSheet>::Style::default()
}
/// Returns the event [`Subscription`] for the current state of the
/// application.
///
/// A [`Subscription`] will be kept alive as long as you keep returning it,
/// and the __messages__ produced will be handled by
/// [`update`](#tymethod.update).
///
/// By default, this method returns an empty [`Subscription`].
fn subscription(&self) -> Subscription<Self::Message> {
Subscription::none()
}
/// Returns the scale factor of the `window` of the [`Application`].
///
/// It can be used to dynamically control the size of the UI at runtime
/// (i.e. zooming).
///
/// For instance, a scale factor of `2.0` will make widgets twice as big,
/// while a scale factor of `0.5` will shrink them to half their size.
///
/// By default, it returns `1.0`.
#[allow(unused_variables)]
fn scale_factor(&self, window: window::Id) -> f64 {
1.0
}
/// Runs the multi-window [`Application`].
///
/// On native platforms, this method will take control of the current thread
/// until the [`Application`] exits.
///
/// On the web platform, this method __will NOT return__ unless there is an
/// [`Error`] during startup.
///
/// [`Error`]: crate::Error
fn run(settings: Settings<Self::Flags>) -> crate::Result
where
Self: 'static,
{
#[allow(clippy::needless_update)]
let renderer_settings = crate::renderer::Settings {
default_font: settings.default_font,
default_text_size: settings.default_text_size,
antialiasing: if settings.antialiasing {
Some(crate::graphics::Antialiasing::MSAAx4)
} else {
None
},
..crate::renderer::Settings::default()
};
Ok(crate::shell::multi_window::run::<
Instance<Self>,
Self::Executor,
crate::renderer::Compositor,
>(settings.into(), renderer_settings)?)
}
}
struct Instance<A: Application>(A);
impl<A> crate::runtime::multi_window::Program for Instance<A>
where
A: Application,
{
type Message = A::Message;
type Theme = A::Theme;
type Renderer = crate::Renderer;
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
self.0.update(message)
}
fn view(
&self,
window: window::Id,
) -> Element<'_, Self::Message, Self::Theme, Self::Renderer> {
self.0.view(window)
}
}
impl<A> crate::shell::multi_window::Application for Instance<A>
where
A: Application,
{
type Flags = A::Flags;
fn new(flags: Self::Flags) -> (Self, Command<A::Message>) {
let (app, command) = A::new(flags);
(Instance(app), command)
}
fn title(&self, window: window::Id) -> String {
self.0.title(window)
}
fn theme(&self, window: window::Id) -> A::Theme {
self.0.theme(window)
}
fn style(&self) -> <A::Theme as StyleSheet>::Style {
self.0.style()
}
fn subscription(&self) -> Subscription<Self::Message> {
self.0.subscription()
}
fn scale_factor(&self, window: window::Id) -> f64 {
self.0.scale_factor(window)
}
}

View file

@ -1,5 +1,5 @@
use crate::theme::{self, Theme}; use crate::application::{self, Application};
use crate::{Application, Command, Element, Error, Settings, Subscription}; use crate::{Command, Element, Error, Settings, Subscription, Theme};
/// A sandboxed [`Application`]. /// A sandboxed [`Application`].
/// ///
@ -120,11 +120,11 @@ pub trait Sandbox {
Theme::default() Theme::default()
} }
/// Returns the current style variant of [`theme::Application`]. /// Returns the current [`application::Appearance`].
/// fn style(&self, theme: &Theme) -> application::Appearance {
/// By default, it returns [`theme::Application::default`]. use application::DefaultStyle;
fn style(&self) -> theme::Application {
theme::Application::default() theme.default_style()
} }
/// Returns the scale factor of the [`Sandbox`]. /// Returns the scale factor of the [`Sandbox`].
@ -185,8 +185,8 @@ where
T::theme(self) T::theme(self)
} }
fn style(&self) -> theme::Application { fn style(&self, theme: &Theme) -> application::Appearance {
T::style(self) T::style(self, theme)
} }
fn subscription(&self) -> Subscription<T::Message> { fn subscription(&self) -> Subscription<T::Message> {

View file

@ -1,5 +1,5 @@
//! Listen and react to time. //! Listen and react to time.
pub use iced_core::time::{Duration, Instant}; pub use crate::core::time::{Duration, Instant};
#[allow(unused_imports)] #[allow(unused_imports)]
#[cfg_attr( #[cfg_attr(

View file

@ -1,18 +0,0 @@
[package]
name = "iced_style"
description = "The default set of styles of Iced"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[dependencies]
iced_core.workspace = true
iced_core.features = ["palette"]
palette.workspace = true
once_cell.workspace = true

View file

@ -1,23 +0,0 @@
//! Change the appearance of an application.
use iced_core::Color;
/// A set of rules that dictate the style of an application.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Returns the [`Appearance`] of the application for the provided [`Style`].
///
/// [`Style`]: Self::Style
fn appearance(&self, style: &Self::Style) -> Appearance;
}
/// The appearance of an application.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Appearance {
/// The background [`Color`] of the application.
pub background_color: Color,
/// The default text [`Color`] of the application.
pub text_color: Color,
}

View file

@ -1,79 +0,0 @@
//! Change the apperance of a button.
use iced_core::{Background, Border, Color, Shadow, Vector};
/// The appearance of a button.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The amount of offset to apply to the shadow of the button.
pub shadow_offset: Vector,
/// The [`Background`] of the button.
pub background: Option<Background>,
/// The text [`Color`] of the button.
pub text_color: Color,
/// The [`Border`] of the buton.
pub border: Border,
/// The [`Shadow`] of the butoon.
pub shadow: Shadow,
}
impl std::default::Default for Appearance {
fn default() -> Self {
Self {
shadow_offset: Vector::default(),
background: None,
text_color: Color::BLACK,
border: Border::default(),
shadow: Shadow::default(),
}
}
}
/// A set of rules that dictate the style of a button.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the active [`Appearance`] of a button.
fn active(&self, style: &Self::Style) -> Appearance;
/// Produces the hovered [`Appearance`] of a button.
fn hovered(&self, style: &Self::Style) -> Appearance {
let active = self.active(style);
Appearance {
shadow_offset: active.shadow_offset + Vector::new(0.0, 1.0),
..active
}
}
/// Produces the pressed [`Appearance`] of a button.
fn pressed(&self, style: &Self::Style) -> Appearance {
Appearance {
shadow_offset: Vector::default(),
..self.active(style)
}
}
/// Produces the disabled [`Appearance`] of a button.
fn disabled(&self, style: &Self::Style) -> Appearance {
let active = self.active(style);
Appearance {
shadow_offset: Vector::default(),
background: active.background.map(|background| match background {
Background::Color(color) => Background::Color(Color {
a: color.a * 0.5,
..color
}),
Background::Gradient(gradient) => {
Background::Gradient(gradient.mul_alpha(0.5))
}
}),
text_color: Color {
a: active.text_color.a * 0.5,
..active.text_color
},
..active
}
}
}

View file

@ -1,45 +0,0 @@
//! Change the appearance of a checkbox.
use iced_core::{Background, Border, Color};
/// The appearance of a checkbox.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the checkbox.
pub background: Background,
/// The icon [`Color`] of the checkbox.
pub icon_color: Color,
/// The [`Border`] of hte checkbox.
pub border: Border,
/// The text [`Color`] of the checkbox.
pub text_color: Option<Color>,
}
/// A set of rules that dictate the style of a checkbox.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the active [`Appearance`] of a checkbox.
fn active(&self, style: &Self::Style, is_checked: bool) -> Appearance;
/// Produces the hovered [`Appearance`] of a checkbox.
fn hovered(&self, style: &Self::Style, is_checked: bool) -> Appearance;
/// Produces the disabled [`Appearance`] of a checkbox.
fn disabled(&self, style: &Self::Style, is_checked: bool) -> Appearance {
let active = self.active(style, is_checked);
Appearance {
background: match active.background {
Background::Color(color) => Background::Color(Color {
a: color.a * 0.5,
..color
}),
Background::Gradient(gradient) => {
Background::Gradient(gradient.mul_alpha(0.5))
}
},
..active
}
}
}

View file

@ -1,51 +0,0 @@
//! Change the appearance of a container.
use crate::core::{Background, Border, Color, Pixels, Shadow};
/// The appearance of a container.
#[derive(Debug, Clone, Copy, Default)]
pub struct Appearance {
/// The text [`Color`] of the container.
pub text_color: Option<Color>,
/// The [`Background`] of the container.
pub background: Option<Background>,
/// The [`Border`] of the container.
pub border: Border,
/// The [`Shadow`] of the container.
pub shadow: Shadow,
}
impl Appearance {
/// Derives a new [`Appearance`] with a border of the given [`Color`] and
/// `width`.
pub fn with_border(
self,
color: impl Into<Color>,
width: impl Into<Pixels>,
) -> Self {
Self {
border: Border {
color: color.into(),
width: width.into().0,
..Border::default()
},
..self
}
}
/// Derives a new [`Appearance`] with the given [`Background`].
pub fn with_background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
}
}
/// A set of rules that dictate the [`Appearance`] of a container.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the [`Appearance`] of a container.
fn appearance(&self, style: &Self::Style) -> Appearance;
}

View file

@ -1,38 +0,0 @@
//! The styling library of Iced.
//!
//! It contains a set of styles and stylesheets for most of the built-in
//! widgets.
//!
//! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
#![doc(
html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
)]
#![forbid(unsafe_code, rust_2018_idioms)]
#![deny(
unused_results,
missing_docs,
unused_results,
rustdoc::broken_intra_doc_links
)]
pub use iced_core as core;
pub mod application;
pub mod button;
pub mod checkbox;
pub mod container;
pub mod menu;
pub mod pane_grid;
pub mod pick_list;
pub mod progress_bar;
pub mod qr_code;
pub mod radio;
pub mod rule;
pub mod scrollable;
pub mod slider;
pub mod svg;
pub mod text_editor;
pub mod text_input;
pub mod theme;
pub mod toggler;
pub use theme::Theme;

View file

@ -1,26 +0,0 @@
//! Change the appearance of menus.
use iced_core::{Background, Border, Color};
/// The appearance of a menu.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The text [`Color`] of the menu.
pub text_color: Color,
/// The [`Background`] of the menu.
pub background: Background,
/// The [`Border`] of the menu.
pub border: Border,
/// The text [`Color`] of a selected option in the menu.
pub selected_text_color: Color,
/// The background [`Color`] of a selected option in the menu.
pub selected_background: Background,
}
/// The style sheet of a menu.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default + Clone;
/// Produces the [`Appearance`] of a menu.
fn appearance(&self, style: &Self::Style) -> Appearance;
}

View file

@ -1,38 +0,0 @@
//! Change the appearance of a pane grid.
use iced_core::{Background, Border, Color};
/// The appearance of the hovered region of a pane grid.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the pane region.
pub background: Background,
/// The [`Border`] of the pane region.
pub border: Border,
}
/// A line.
///
/// It is normally used to define the highlight of something, like a split.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Line {
/// The [`Color`] of the [`Line`].
pub color: Color,
/// The width of the [`Line`].
pub width: f32,
}
/// A set of rules that dictate the style of a container.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// The [`Appearance`] to draw when a pane is hovered.
fn hovered_region(&self, style: &Self::Style) -> Appearance;
/// The [`Line`] to draw when a split is picked.
fn picked_split(&self, style: &Self::Style) -> Option<Line>;
/// The [`Line`] to draw when a split is hovered.
fn hovered_split(&self, style: &Self::Style) -> Option<Line>;
}

View file

@ -1,29 +0,0 @@
//! Change the appearance of a pick list.
use iced_core::{Background, Border, Color};
/// The appearance of a pick list.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The text [`Color`] of the pick list.
pub text_color: Color,
/// The placeholder [`Color`] of the pick list.
pub placeholder_color: Color,
/// The handle [`Color`] of the pick list.
pub handle_color: Color,
/// The [`Background`] of the pick list.
pub background: Background,
/// The [`Border`] of the pick list.
pub border: Border,
}
/// A set of rules that dictate the style of a container.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default + Clone;
/// Produces the active [`Appearance`] of a pick list.
fn active(&self, style: &<Self as StyleSheet>::Style) -> Appearance;
/// Produces the hovered [`Appearance`] of a pick list.
fn hovered(&self, style: &<Self as StyleSheet>::Style) -> Appearance;
}

View file

@ -1,23 +0,0 @@
//! Change the appearance of a progress bar.
use crate::core::border;
use crate::core::Background;
/// The appearance of a progress bar.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the progress bar.
pub background: Background,
/// The [`Background`] of the bar of the progress bar.
pub bar: Background,
/// The border radius of the progress bar.
pub border_radius: border::Radius,
}
/// A set of rules that dictate the style of a progress bar.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the [`Appearance`] of the progress bar.
fn appearance(&self, style: &Self::Style) -> Appearance;
}

View file

@ -1,20 +0,0 @@
//! Change the appearance of a QR code.
use crate::core::Color;
/// The appearance of a QR code.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Appearance {
/// The color of the QR code data cells
pub cell: Color,
/// The color of the QR code background
pub background: Color,
}
/// A set of rules that dictate the style of a QR code.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the style of a QR code.
fn appearance(&self, style: &Self::Style) -> Appearance;
}

View file

@ -1,29 +0,0 @@
//! Change the appearance of radio buttons.
use iced_core::{Background, Color};
/// The appearance of a radio button.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the radio button.
pub background: Background,
/// The [`Color`] of the dot of the radio button.
pub dot_color: Color,
/// The border width of the radio button.
pub border_width: f32,
/// The border [`Color`] of the radio button.
pub border_color: Color,
/// The text [`Color`] of the radio button.
pub text_color: Option<Color>,
}
/// A set of rules that dictate the style of a radio button.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the active [`Appearance`] of a radio button.
fn active(&self, style: &Self::Style, is_selected: bool) -> Appearance;
/// Produces the hovered [`Appearance`] of a radio button.
fn hovered(&self, style: &Self::Style, is_selected: bool) -> Appearance;
}

View file

@ -1,89 +0,0 @@
//! Change the appearance of a rule.
use crate::core::border;
use crate::core::Color;
/// The appearance of a rule.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The color of the rule.
pub color: Color,
/// The width (thickness) of the rule line.
pub width: u16,
/// The radius of the line corners.
pub radius: border::Radius,
/// The [`FillMode`] of the rule.
pub fill_mode: FillMode,
}
/// A set of rules that dictate the style of a rule.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the style of a rule.
fn appearance(&self, style: &Self::Style) -> Appearance;
}
/// The fill mode of a rule.
#[derive(Debug, Clone, Copy)]
pub enum FillMode {
/// Fill the whole length of the container.
Full,
/// Fill a percent of the length of the container. The rule
/// will be centered in that container.
///
/// The range is `[0.0, 100.0]`.
Percent(f32),
/// Uniform offset from each end, length units.
Padded(u16),
/// Different offset on each end of the rule, length units.
/// First = top or left.
AsymmetricPadding(u16, u16),
}
impl FillMode {
/// Return the starting offset and length of the rule.
///
/// * `space` - The space to fill.
///
/// # Returns
///
/// * (`starting_offset`, `length`)
pub fn fill(&self, space: f32) -> (f32, f32) {
match *self {
FillMode::Full => (0.0, space),
FillMode::Percent(percent) => {
if percent >= 100.0 {
(0.0, space)
} else {
let percent_width = (space * percent / 100.0).round();
(((space - percent_width) / 2.0).round(), percent_width)
}
}
FillMode::Padded(padding) => {
if padding == 0 {
(0.0, space)
} else {
let padding = padding as f32;
let mut line_width = space - (padding * 2.0);
if line_width < 0.0 {
line_width = 0.0;
}
(padding, line_width)
}
}
FillMode::AsymmetricPadding(first_pad, second_pad) => {
let first_pad = first_pad as f32;
let second_pad = second_pad as f32;
let mut line_width = space - first_pad - second_pad;
if line_width < 0.0 {
line_width = 0.0;
}
(first_pad, line_width)
}
}
}
}

View file

@ -1,55 +0,0 @@
//! Change the appearance of a scrollable.
use crate::container;
use crate::core::{Background, Border, Color};
/// The appearance of a scrolable.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`container::Appearance`] of a scrollable.
pub container: container::Appearance,
/// The [`Scrollbar`] appearance.
pub scrollbar: Scrollbar,
/// The [`Background`] of the gap between a horizontal and vertical scrollbar.
pub gap: Option<Background>,
}
/// The appearance of the scrollbar of a scrollable.
#[derive(Debug, Clone, Copy)]
pub struct Scrollbar {
/// The [`Background`] of a scrollbar.
pub background: Option<Background>,
/// The [`Border`] of a scrollbar.
pub border: Border,
/// The appearance of the [`Scroller`] of a scrollbar.
pub scroller: Scroller,
}
/// The appearance of the scroller of a scrollable.
#[derive(Debug, Clone, Copy)]
pub struct Scroller {
/// The [`Color`] of the scroller.
pub color: Color,
/// The [`Border`] of the scroller.
pub border: Border,
}
/// A set of rules that dictate the style of a scrollable.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the [`Appearance`] of an active scrollable.
fn active(&self, style: &Self::Style) -> Appearance;
/// Produces the [`Appearance`] of a scrollable when it is being hovered.
fn hovered(
&self,
style: &Self::Style,
is_mouse_over_scrollbar: bool,
) -> Appearance;
/// Produces the [`Appearance`] of a scrollable when it is being dragged.
fn dragging(&self, style: &Self::Style) -> Appearance {
self.hovered(style, true)
}
}

View file

@ -1,68 +0,0 @@
//! Change the apperance of a slider.
use crate::core::border;
use crate::core::Color;
/// The appearance of a slider.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The colors of the rail of the slider.
pub rail: Rail,
/// The appearance of the [`Handle`] of the slider.
pub handle: Handle,
}
/// The appearance of a slider rail
#[derive(Debug, Clone, Copy)]
pub struct Rail {
/// The colors of the rail of the slider.
pub colors: (Color, Color),
/// The width of the stroke of a slider rail.
pub width: f32,
/// The border radius of the corners of the rail.
pub border_radius: border::Radius,
}
/// The appearance of the handle of a slider.
#[derive(Debug, Clone, Copy)]
pub struct Handle {
/// The shape of the handle.
pub shape: HandleShape,
/// The [`Color`] of the handle.
pub color: Color,
/// The border width of the handle.
pub border_width: f32,
/// The border [`Color`] of the handle.
pub border_color: Color,
}
/// The shape of the handle of a slider.
#[derive(Debug, Clone, Copy)]
pub enum HandleShape {
/// A circular handle.
Circle {
/// The radius of the circle.
radius: f32,
},
/// A rectangular shape.
Rectangle {
/// The width of the rectangle.
width: u16,
/// The border radius of the corners of the rectangle.
border_radius: border::Radius,
},
}
/// A set of rules that dictate the style of a slider.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the style of an active slider.
fn active(&self, style: &Self::Style) -> Appearance;
/// Produces the style of an hovered slider.
fn hovered(&self, style: &Self::Style) -> Appearance;
/// Produces the style of a slider that is being dragged.
fn dragging(&self, style: &Self::Style) -> Appearance;
}

View file

@ -1,28 +0,0 @@
//! Change the appearance of a svg.
use iced_core::Color;
/// The appearance of an SVG.
#[derive(Debug, Default, Clone, Copy)]
pub struct Appearance {
/// The [`Color`] filter of an SVG.
///
/// Useful for coloring a symbolic icon.
///
/// `None` keeps the original color.
pub color: Option<Color>,
}
/// The stylesheet of a svg.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the [`Appearance`] of the svg.
fn appearance(&self, style: &Self::Style) -> Appearance;
/// Produces the hovered [`Appearance`] of a svg content.
fn hovered(&self, style: &Self::Style) -> Appearance {
self.appearance(style)
}
}

View file

@ -1,43 +0,0 @@
//! Change the appearance of a text editor.
use crate::core::{Background, Border, Color};
/// The appearance of a text input.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the text editor.
pub background: Background,
/// The [`Border`] of the text editor.
pub border: Border,
}
/// A set of rules that dictate the style of a text input.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the style of an active text input.
fn active(&self, style: &Self::Style) -> Appearance;
/// Produces the style of a focused text input.
fn focused(&self, style: &Self::Style) -> Appearance;
/// Produces the [`Color`] of the placeholder of a text input.
fn placeholder_color(&self, style: &Self::Style) -> Color;
/// Produces the [`Color`] of the value of a text input.
fn value_color(&self, style: &Self::Style) -> Color;
/// Produces the [`Color`] of the value of a disabled text input.
fn disabled_color(&self, style: &Self::Style) -> Color;
/// Produces the [`Color`] of the selection of a text input.
fn selection_color(&self, style: &Self::Style) -> Color;
/// Produces the style of an hovered text input.
fn hovered(&self, style: &Self::Style) -> Appearance {
self.focused(style)
}
/// Produces the style of a disabled text input.
fn disabled(&self, style: &Self::Style) -> Appearance;
}

View file

@ -1,45 +0,0 @@
//! Change the appearance of a text input.
use iced_core::{Background, Border, Color};
/// The appearance of a text input.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the text input.
pub background: Background,
/// The [`Border`] of the text input.
pub border: Border,
/// The icon [`Color`] of the text input.
pub icon_color: Color,
}
/// A set of rules that dictate the style of a text input.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Produces the style of an active text input.
fn active(&self, style: &Self::Style) -> Appearance;
/// Produces the style of a focused text input.
fn focused(&self, style: &Self::Style) -> Appearance;
/// Produces the [`Color`] of the placeholder of a text input.
fn placeholder_color(&self, style: &Self::Style) -> Color;
/// Produces the [`Color`] of the value of a text input.
fn value_color(&self, style: &Self::Style) -> Color;
/// Produces the [`Color`] of the value of a disabled text input.
fn disabled_color(&self, style: &Self::Style) -> Color;
/// Produces the [`Color`] of the selection of a text input.
fn selection_color(&self, style: &Self::Style) -> Color;
/// Produces the style of an hovered text input.
fn hovered(&self, style: &Self::Style) -> Appearance {
self.focused(style)
}
/// Produces the style of a disabled text input.
fn disabled(&self, style: &Self::Style) -> Appearance;
}

File diff suppressed because it is too large Load diff

View file

@ -1,35 +0,0 @@
//! Change the appearance of a toggler.
use iced_core::Color;
/// The appearance of a toggler.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The background [`Color`] of the toggler.
pub background: Color,
/// The width of the background border of the toggler.
pub background_border_width: f32,
/// The [`Color`] of the background border of the toggler.
pub background_border_color: Color,
/// The foreground [`Color`] of the toggler.
pub foreground: Color,
/// The width of the foreground border of the toggler.
pub foreground_border_width: f32,
/// The [`Color`] of the foreground border of the toggler.
pub foreground_border_color: Color,
}
/// A set of rules that dictate the style of a toggler.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// Returns the active [`Appearance`] of the toggler for the provided [`Style`].
///
/// [`Style`]: Self::Style
fn active(&self, style: &Self::Style, is_active: bool) -> Appearance;
/// Returns the hovered [`Appearance`] of the toggler for the provided [`Style`].
///
/// [`Style`]: Self::Style
fn hovered(&self, style: &Self::Style, is_active: bool) -> Appearance;
}

View file

@ -158,10 +158,3 @@ pub fn convert(
texture texture
} }
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
struct Vertex {
ndc: [f32; 2],
uv: [f32; 2],
}

View file

@ -25,7 +25,6 @@ wgpu = ["iced_renderer/wgpu"]
[dependencies] [dependencies]
iced_renderer.workspace = true iced_renderer.workspace = true
iced_runtime.workspace = true iced_runtime.workspace = true
iced_style.workspace = true
num-traits.workspace = true num-traits.workspace = true
thiserror.workspace = true thiserror.workspace = true

View file

@ -1,26 +1,22 @@
//! Allow your users to perform actions by pressing a button. //! Allow your users to perform actions by pressing a button.
//!
//! A [`Button`] has some local [`State`].
use crate::core::event::{self, Event}; use crate::core::event::{self, Event};
use crate::core::layout; use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::overlay; use crate::core::overlay;
use crate::core::renderer; use crate::core::renderer;
use crate::core::theme::palette;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation; use crate::core::widget::Operation;
use crate::core::{ use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
Shell, Size, Vector, Widget, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
}; };
pub use crate::style::button::{Appearance, StyleSheet};
/// A generic widget that produces a message when pressed. /// A generic widget that produces a message when pressed.
/// ///
/// ```no_run /// ```no_run
/// # type Button<'a, Message> = /// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
/// # /// #
/// #[derive(Clone)] /// #[derive(Clone)]
/// enum Message { /// enum Message {
@ -34,8 +30,7 @@ pub use crate::style::button::{Appearance, StyleSheet};
/// be disabled: /// be disabled:
/// ///
/// ``` /// ```
/// # type Button<'a, Message> = /// # type Button<'a, Message> = iced_widget::Button<'a, Message>;
/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
/// # /// #
/// #[derive(Clone)] /// #[derive(Clone)]
/// enum Message { /// enum Message {
@ -53,7 +48,6 @@ pub use crate::style::button::{Appearance, StyleSheet};
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
where where
Theme: StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
content: Element<'a, Message, Theme, Renderer>, content: Element<'a, Message, Theme, Renderer>,
@ -62,18 +56,20 @@ where
height: Length, height: Length,
padding: Padding, padding: Padding,
clip: bool, clip: bool,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
/// Creates a new [`Button`] with the given content. /// Creates a new [`Button`] with the given content.
pub fn new( pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>, content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self { ) -> Self
where
Theme: DefaultStyle,
{
let content = content.into(); let content = content.into();
let size = content.as_widget().size_hint(); let size = content.as_widget().size_hint();
@ -82,9 +78,9 @@ where
on_press: None, on_press: None,
width: size.width.fluid(), width: size.width.fluid(),
height: size.height.fluid(), height: size.height.fluid(),
padding: Padding::new(5.0), padding: DEFAULT_PADDING,
clip: false, clip: false,
style: Theme::Style::default(), style: Theme::default_style(),
} }
} }
@ -124,8 +120,8 @@ where
} }
/// Sets the style variant of this [`Button`]. /// Sets the style variant of this [`Button`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style;
self self
} }
@ -137,11 +133,15 @@ where
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
struct State {
is_pressed: bool,
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Button<'a, Message, Theme, Renderer> for Button<'a, Message, Theme, Renderer>
where where
Message: 'a + Clone, Message: 'a + Clone,
Theme: StyleSheet,
Renderer: 'a + crate::core::Renderer, Renderer: 'a + crate::core::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -149,7 +149,7 @@ where
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::new()) tree::State::new(State::default())
} }
fn children(&self) -> Vec<Tree> { fn children(&self) -> Vec<Tree> {
@ -173,13 +173,19 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout(limits, self.width, self.height, self.padding, |limits| { layout::padded(
self.content.as_widget().layout( limits,
&mut tree.children[0], self.width,
renderer, self.height,
limits, self.padding,
) |limits| {
}) self.content.as_widget().layout(
&mut tree.children[0],
renderer,
limits,
)
},
)
} }
fn operate( fn operate(
@ -223,9 +229,48 @@ where
return event::Status::Captured; return event::Status::Captured;
} }
update(event, layout, cursor, shell, &self.on_press, || { match event {
tree.state.downcast_mut::<State>() Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
}) | Event::Touch(touch::Event::FingerPressed { .. }) => {
if self.on_press.is_some() {
let bounds = layout.bounds();
if cursor.is_over(bounds) {
let state = tree.state.downcast_mut::<State>();
state.is_pressed = true;
return event::Status::Captured;
}
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(on_press) = self.on_press.clone() {
let state = tree.state.downcast_mut::<State>();
if state.is_pressed {
state.is_pressed = false;
let bounds = layout.bounds();
if cursor.is_over(bounds) {
shell.publish(on_press);
}
return event::Status::Captured;
}
}
}
Event::Touch(touch::Event::FingerLost { .. }) => {
let state = tree.state.downcast_mut::<State>();
state.is_pressed = false;
}
_ => {}
}
event::Status::Ignored
} }
fn draw( fn draw(
@ -240,16 +285,39 @@ where
) { ) {
let bounds = layout.bounds(); let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap(); let content_layout = layout.children().next().unwrap();
let is_mouse_over = cursor.is_over(bounds);
let styling = draw( let status = if self.on_press.is_none() {
renderer, Status::Disabled
bounds, } else if is_mouse_over {
cursor, let state = tree.state.downcast_ref::<State>();
self.on_press.is_some(),
theme, if state.is_pressed {
&self.style, Status::Pressed
|| tree.state.downcast_ref::<State>(), } else {
); Status::Hovered
}
} else {
Status::Active
};
let styling = (self.style)(theme, status);
if styling.background.is_some()
|| styling.border.width > 0.0
|| styling.shadow.color.a > 0.0
{
renderer.fill_quad(
renderer::Quad {
bounds,
border: styling.border,
shadow: styling.shadow,
},
styling
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
let viewport = if self.clip { let viewport = if self.clip {
bounds.intersection(viewport).unwrap_or(*viewport) bounds.intersection(viewport).unwrap_or(*viewport)
@ -278,7 +346,13 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
_renderer: &Renderer, _renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
mouse_interaction(layout, cursor, self.on_press.is_some()) let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over && self.on_press.is_some() {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
} }
fn overlay<'b>( fn overlay<'b>(
@ -301,7 +375,7 @@ impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: Clone + 'a, Message: Clone + 'a,
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: crate::core::Renderer + 'a, Renderer: crate::core::Renderer + 'a,
{ {
fn from(button: Button<'a, Message, Theme, Renderer>) -> Self { fn from(button: Button<'a, Message, Theme, Renderer>) -> Self {
@ -309,143 +383,182 @@ where
} }
} }
/// The local state of a [`Button`]. /// The default [`Padding`] of a [`Button`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub(crate) const DEFAULT_PADDING: Padding = Padding {
pub struct State { top: 5.0,
is_pressed: bool, bottom: 5.0,
right: 10.0,
left: 10.0,
};
/// The possible status of a [`Button`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Button`] can be pressed.
Active,
/// The [`Button`] can be pressed and it is being hovered.
Hovered,
/// The [`Button`] is being pressed.
Pressed,
/// The [`Button`] cannot be pressed.
Disabled,
} }
impl State { /// The appearance of a button.
/// Creates a new [`State`]. #[derive(Debug, Clone, Copy, PartialEq)]
pub fn new() -> State { pub struct Appearance {
State::default() /// The [`Background`] of the button.
pub background: Option<Background>,
/// The text [`Color`] of the button.
pub text_color: Color,
/// The [`Border`] of the buton.
pub border: Border,
/// The [`Shadow`] of the butoon.
pub shadow: Shadow,
}
impl Appearance {
/// Updates the [`Appearance`] with the given [`Background`].
pub fn with_background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
} }
} }
/// Processes the given [`Event`] and updates the [`State`] of a [`Button`] impl std::default::Default for Appearance {
/// accordingly. fn default() -> Self {
pub fn update<'a, Message: Clone>( Self {
event: Event, background: None,
layout: Layout<'_>, text_color: Color::BLACK,
cursor: mouse::Cursor, border: Border::default(),
shell: &mut Shell<'_, Message>, shadow: Shadow::default(),
on_press: &Option<Message>,
state: impl FnOnce() -> &'a mut State,
) -> event::Status {
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if on_press.is_some() {
let bounds = layout.bounds();
if cursor.is_over(bounds) {
let state = state();
state.is_pressed = true;
return event::Status::Captured;
}
}
} }
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. }) => {
if let Some(on_press) = on_press.clone() {
let state = state();
if state.is_pressed {
state.is_pressed = false;
let bounds = layout.bounds();
if cursor.is_over(bounds) {
shell.publish(on_press);
}
return event::Status::Captured;
}
}
}
Event::Touch(touch::Event::FingerLost { .. }) => {
let state = state();
state.is_pressed = false;
}
_ => {}
} }
event::Status::Ignored
} }
/// Draws a [`Button`]. /// The style of a [`Button`].
pub fn draw<'a, Theme, Renderer: crate::core::Renderer>( pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
renderer: &mut Renderer,
bounds: Rectangle,
cursor: mouse::Cursor,
is_enabled: bool,
theme: &Theme,
style: &Theme::Style,
state: impl FnOnce() -> &'a State,
) -> Appearance
where
Theme: StyleSheet,
{
let is_mouse_over = cursor.is_over(bounds);
let styling = if !is_enabled { /// The default style of a [`Button`].
theme.disabled(style) pub trait DefaultStyle {
} else if is_mouse_over { /// Returns the default style of a [`Button`].
let state = state(); fn default_style() -> Style<Self>;
}
if state.is_pressed { impl DefaultStyle for Theme {
theme.pressed(style) fn default_style() -> Style<Self> {
} else { primary
theme.hovered(style) }
} }
} else {
theme.active(style) impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}
impl DefaultStyle for Color {
fn default_style() -> Style<Self> {
|color, _status| Appearance::default().with_background(*color)
}
}
/// A primary button; denoting a main action.
pub fn primary(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let base = styled(palette.primary.strong);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Appearance {
background: Some(Background::Color(palette.primary.base.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A secondary button; denoting a complementary action.
pub fn secondary(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let base = styled(palette.secondary.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Appearance {
background: Some(Background::Color(palette.secondary.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A success button; denoting a good outcome.
pub fn success(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let base = styled(palette.success.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Appearance {
background: Some(Background::Color(palette.success.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A danger button; denoting a destructive action.
pub fn danger(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let base = styled(palette.danger.base);
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Appearance {
background: Some(Background::Color(palette.danger.strong.color)),
..base
},
Status::Disabled => disabled(base),
}
}
/// A text button; useful for links.
pub fn text(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let base = Appearance {
text_color: palette.background.base.text,
..Appearance::default()
}; };
if styling.background.is_some() match status {
|| styling.border.width > 0.0 Status::Active | Status::Pressed => base,
|| styling.shadow.color.a > 0.0 Status::Hovered => Appearance {
{ text_color: palette.background.base.text.scale_alpha(0.8),
renderer.fill_quad( ..base
renderer::Quad { },
bounds, Status::Disabled => disabled(base),
border: styling.border, }
shadow: styling.shadow, }
},
styling fn styled(pair: palette::Pair) -> Appearance {
.background Appearance {
.unwrap_or(Background::Color(Color::TRANSPARENT)), background: Some(Background::Color(pair.color)),
); text_color: pair.text,
} border: Border::rounded(2),
..Appearance::default()
styling }
} }
/// Computes the layout of a [`Button`]. fn disabled(appearance: Appearance) -> Appearance {
pub fn layout( Appearance {
limits: &layout::Limits, background: appearance
width: Length, .background
height: Length, .map(|background| background.scale_alpha(0.5)),
padding: Padding, text_color: appearance.text_color.scale_alpha(0.5),
layout_content: impl FnOnce(&layout::Limits) -> layout::Node, ..appearance
) -> layout::Node {
layout::padded(limits, width, height, padding, layout_content)
}
/// Returns the [`mouse::Interaction`] of a [`Button`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor: mouse::Cursor,
is_enabled: bool,
) -> mouse::Interaction {
let is_mouse_over = cursor.is_over(layout.bounds());
if is_mouse_over && is_enabled {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
} }
} }

View file

@ -5,22 +5,21 @@ use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::text; use crate::core::text;
use crate::core::theme::palette;
use crate::core::touch; use crate::core::touch;
use crate::core::widget; use crate::core::widget;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
Rectangle, Shell, Size, Theme, Widget,
}; };
pub use crate::style::checkbox::{Appearance, StyleSheet};
/// A box that can be checked. /// A box that can be checked.
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```no_run
/// # type Checkbox<'a, Message> = /// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>;
/// # iced_widget::Checkbox<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
/// # /// #
/// pub enum Message { /// pub enum Message {
/// CheckboxToggled(bool), /// CheckboxToggled(bool),
@ -39,7 +38,6 @@ pub struct Checkbox<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: StyleSheet + crate::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
is_checked: bool, is_checked: bool,
@ -53,26 +51,28 @@ pub struct Checkbox<
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
icon: Icon<Renderer::Font>, icon: Icon<Renderer::Font>,
style: <Theme as StyleSheet>::Style, style: Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer>
where where
Renderer: text::Renderer, Renderer: text::Renderer,
Theme: StyleSheet + crate::text::StyleSheet,
{ {
/// The default size of a [`Checkbox`]. /// The default size of a [`Checkbox`].
const DEFAULT_SIZE: f32 = 20.0; const DEFAULT_SIZE: f32 = 16.0;
/// The default spacing of a [`Checkbox`]. /// The default spacing of a [`Checkbox`].
const DEFAULT_SPACING: f32 = 10.0; const DEFAULT_SPACING: f32 = 8.0;
/// Creates a new [`Checkbox`]. /// Creates a new [`Checkbox`].
/// ///
/// It expects: /// It expects:
/// * the label of the [`Checkbox`] /// * the label of the [`Checkbox`]
/// * a boolean describing whether the [`Checkbox`] is checked or not /// * a boolean describing whether the [`Checkbox`] is checked or not
pub fn new(label: impl Into<String>, is_checked: bool) -> Self { pub fn new(label: impl Into<String>, is_checked: bool) -> Self
where
Theme: DefaultStyle,
{
Checkbox { Checkbox {
is_checked, is_checked,
on_toggle: None, on_toggle: None,
@ -91,7 +91,7 @@ where
line_height: text::LineHeight::default(), line_height: text::LineHeight::default(),
shaping: text::Shaping::Basic, shaping: text::Shaping::Basic,
}, },
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -174,11 +174,8 @@ where
} }
/// Sets the style of the [`Checkbox`]. /// Sets the style of the [`Checkbox`].
pub fn style( pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
mut self, self.style = style;
style: impl Into<<Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self self
} }
} }
@ -186,7 +183,6 @@ where
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Checkbox<'a, Message, Theme, Renderer> for Checkbox<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet + crate::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -293,17 +289,20 @@ where
) { ) {
let is_mouse_over = cursor.is_over(layout.bounds()); let is_mouse_over = cursor.is_over(layout.bounds());
let is_disabled = self.on_toggle.is_none(); let is_disabled = self.on_toggle.is_none();
let is_checked = self.is_checked;
let mut children = layout.children(); let mut children = layout.children();
let custom_style = if is_disabled { let status = if is_disabled {
theme.disabled(&self.style, self.is_checked) Status::Disabled { is_checked }
} else if is_mouse_over { } else if is_mouse_over {
theme.hovered(&self.style, self.is_checked) Status::Hovered { is_checked }
} else { } else {
theme.active(&self.style, self.is_checked) Status::Active { is_checked }
}; };
let appearance = (self.style)(theme, status);
{ {
let layout = children.next().unwrap(); let layout = children.next().unwrap();
let bounds = layout.bounds(); let bounds = layout.bounds();
@ -311,10 +310,10 @@ where
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds, bounds,
border: custom_style.border, border: appearance.border,
..renderer::Quad::default() ..renderer::Quad::default()
}, },
custom_style.background, appearance.background,
); );
let Icon { let Icon {
@ -339,7 +338,7 @@ where
shaping: *shaping, shaping: *shaping,
}, },
bounds.center(), bounds.center(),
custom_style.icon_color, appearance.icon_color,
*viewport, *viewport,
); );
} }
@ -354,7 +353,7 @@ where
label_layout, label_layout,
tree.state.downcast_ref(), tree.state.downcast_ref(),
crate::text::Appearance { crate::text::Appearance {
color: custom_style.text_color, color: appearance.text_color,
}, },
viewport, viewport,
); );
@ -366,7 +365,7 @@ impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: 'a + StyleSheet + crate::text::StyleSheet, Theme: 'a,
Renderer: 'a + text::Renderer, Renderer: 'a + text::Renderer,
{ {
fn from( fn from(
@ -390,3 +389,183 @@ pub struct Icon<Font> {
/// The shaping strategy of the icon. /// The shaping strategy of the icon.
pub shaping: text::Shaping, pub shaping: text::Shaping,
} }
/// The possible status of a [`Checkbox`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Checkbox`] can be interacted with.
Active {
/// Indicates if the [`Checkbox`] is currently checked.
is_checked: bool,
},
/// The [`Checkbox`] can be interacted with and it is being hovered.
Hovered {
/// Indicates if the [`Checkbox`] is currently checked.
is_checked: bool,
},
/// The [`Checkbox`] cannot be interacted with.
Disabled {
/// Indicates if the [`Checkbox`] is currently checked.
is_checked: bool,
},
}
/// The appearance of a checkbox.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the checkbox.
pub background: Background,
/// The icon [`Color`] of the checkbox.
pub icon_color: Color,
/// The [`Border`] of hte checkbox.
pub border: Border,
/// The text [`Color`] of the checkbox.
pub text_color: Option<Color>,
}
/// The style of a [`Checkbox`].
pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
/// The default style of a [`Checkbox`].
pub trait DefaultStyle {
/// Returns the default style of a [`Checkbox`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
primary
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}
/// A primary checkbox; denoting a main toggle.
pub fn primary(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.primary.strong.text,
palette.background.base,
palette.primary.strong,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.primary.strong.text,
palette.background.weak,
palette.primary.base,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.primary.strong.text,
palette.background.weak,
palette.background.strong,
is_checked,
),
}
}
/// A secondary checkbox; denoting a complementary toggle.
pub fn secondary(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.background.base.text,
palette.background.base,
palette.background.strong,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.background.base.text,
palette.background.weak,
palette.background.strong,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.background.strong.color,
palette.background.weak,
palette.background.weak,
is_checked,
),
}
}
/// A success checkbox; denoting a positive toggle.
pub fn success(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.success.base.text,
palette.background.base,
palette.success.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.success.base.text,
palette.background.weak,
palette.success.base,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.success.base.text,
palette.background.weak,
palette.success.weak,
is_checked,
),
}
}
/// A danger checkbox; denoting a negaive toggle.
pub fn danger(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
match status {
Status::Active { is_checked } => styled(
palette.danger.base.text,
palette.background.base,
palette.danger.base,
is_checked,
),
Status::Hovered { is_checked } => styled(
palette.danger.base.text,
palette.background.weak,
palette.danger.base,
is_checked,
),
Status::Disabled { is_checked } => styled(
palette.danger.base.text,
palette.background.weak,
palette.danger.weak,
is_checked,
),
}
}
fn styled(
icon_color: Color,
base: palette::Pair,
accent: palette::Pair,
is_checked: bool,
) -> Appearance {
Appearance {
background: Background::Color(if is_checked {
accent.color
} else {
base.color
}),
icon_color,
border: Border {
radius: 2.0.into(),
width: 1.0,
color: accent.color,
},
text_color: None,
}
}

View file

@ -10,11 +10,11 @@ use crate::core::text;
use crate::core::time::Instant; use crate::core::time::Instant;
use crate::core::widget::{self, Widget}; use crate::core::widget::{self, Widget};
use crate::core::{ use crate::core::{
Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Vector, Clipboard, Element, Length, Padding, Rectangle, Shell, Size, Theme, Vector,
}; };
use crate::overlay::menu; use crate::overlay::menu;
use crate::text::LineHeight; use crate::text::LineHeight;
use crate::{container, scrollable, text_input, TextInput}; use crate::text_input::{self, TextInput};
use std::cell::RefCell; use std::cell::RefCell;
use std::fmt::Display; use std::fmt::Display;
@ -32,7 +32,6 @@ pub struct ComboBox<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: text_input::StyleSheet + menu::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
state: &'a State<T>, state: &'a State<T>,
@ -43,7 +42,7 @@ pub struct ComboBox<
on_option_hovered: Option<Box<dyn Fn(T) -> Message>>, on_option_hovered: Option<Box<dyn Fn(T) -> Message>>,
on_close: Option<Message>, on_close: Option<Message>,
on_input: Option<Box<dyn Fn(String) -> Message>>, on_input: Option<Box<dyn Fn(String) -> Message>>,
menu_style: <Theme as menu::StyleSheet>::Style, menu_style: menu::Style<Theme>,
padding: Padding, padding: Padding,
size: Option<f32>, size: Option<f32>,
} }
@ -51,7 +50,6 @@ pub struct ComboBox<
impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer> impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer>
where where
T: std::fmt::Display + Clone, T: std::fmt::Display + Clone,
Theme: text_input::StyleSheet + menu::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// Creates a new [`ComboBox`] with the given list of options, a placeholder, /// Creates a new [`ComboBox`] with the given list of options, a placeholder,
@ -62,9 +60,18 @@ where
placeholder: &str, placeholder: &str,
selection: Option<&T>, selection: Option<&T>,
on_selected: impl Fn(T) -> Message + 'static, on_selected: impl Fn(T) -> Message + 'static,
) -> Self { ) -> Self
let text_input = TextInput::new(placeholder, &state.value()) where
.on_input(TextInputEvent::TextChanged); Theme: DefaultStyle,
{
let style = Theme::default_style();
let text_input = TextInput::with_style(
placeholder,
&state.value(),
style.text_input,
)
.on_input(TextInputEvent::TextChanged);
let selection = selection.map(T::to_string).unwrap_or_default(); let selection = selection.map(T::to_string).unwrap_or_default();
@ -77,7 +84,7 @@ where
on_option_hovered: None, on_option_hovered: None,
on_input: None, on_input: None,
on_close: None, on_close: None,
menu_style: Default::default(), menu_style: style.menu,
padding: text_input::DEFAULT_PADDING, padding: text_input::DEFAULT_PADDING,
size: None, size: None,
} }
@ -118,24 +125,11 @@ where
} }
/// Sets the style of the [`ComboBox`]. /// Sets the style of the [`ComboBox`].
// TODO: Define its own `StyleSheet` trait pub fn style(mut self, style: impl Into<Style<Theme>>) -> Self {
pub fn style<S>(mut self, style: S) -> Self let style = style.into();
where
S: Into<<Theme as text_input::StyleSheet>::Style>
+ Into<<Theme as menu::StyleSheet>::Style>
+ Clone,
{
self.menu_style = style.clone().into();
self.text_input = self.text_input.style(style);
self
}
/// Sets the style of the [`TextInput`] of the [`ComboBox`]. self.text_input = self.text_input.style(style.text_input);
pub fn text_input_style<S>(mut self, style: S) -> Self self.menu_style = style.menu;
where
S: Into<<Theme as text_input::StyleSheet>::Style> + Clone,
{
self.text_input = self.text_input.style(style);
self self
} }
@ -299,10 +293,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where where
T: Display + Clone + 'static, T: Display + Clone + 'static,
Message: Clone, Message: Clone,
Theme: container::StyleSheet
+ text_input::StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
@ -679,7 +669,7 @@ where
self.state.sync_filtered_options(filtered_options); self.state.sync_filtered_options(filtered_options);
let mut menu = menu::Menu::new( let mut menu = menu::Menu::with_style(
menu, menu,
&filtered_options.options, &filtered_options.options,
hovered_option, hovered_option,
@ -693,10 +683,10 @@ where
(self.on_selected)(x) (self.on_selected)(x)
}, },
self.on_option_hovered.as_deref(), self.on_option_hovered.as_deref(),
self.menu_style,
) )
.width(bounds.width) .width(bounds.width)
.padding(self.padding) .padding(self.padding);
.style(self.menu_style.clone());
if let Some(font) = self.font { if let Some(font) = self.font {
menu = menu.font(font); menu = menu.font(font);
@ -719,11 +709,7 @@ impl<'a, T, Message, Theme, Renderer>
where where
T: Display + Clone + 'static, T: Display + Clone + 'static,
Message: Clone + 'a, Message: Clone + 'a,
Theme: container::StyleSheet Theme: 'a,
+ text_input::StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ 'a,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self { fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self {
@ -731,8 +717,7 @@ where
} }
} }
/// Search list of options for a given query. fn search<'a, T, A>(
pub fn search<'a, T, A>(
options: impl IntoIterator<Item = T> + 'a, options: impl IntoIterator<Item = T> + 'a,
option_matchers: impl IntoIterator<Item = &'a A> + 'a, option_matchers: impl IntoIterator<Item = &'a A> + 'a,
query: &'a str, query: &'a str,
@ -759,8 +744,7 @@ where
}) })
} }
/// Build matchers from given list of options. fn build_matchers<'a, T>(
pub fn build_matchers<'a, T>(
options: impl IntoIterator<Item = T> + 'a, options: impl IntoIterator<Item = T> + 'a,
) -> Vec<String> ) -> Vec<String>
where where
@ -775,3 +759,43 @@ where
}) })
.collect() .collect()
} }
/// The style of a [`ComboBox`].
#[derive(Debug, PartialEq, Eq)]
pub struct Style<Theme> {
/// The style of the [`TextInput`] of the [`ComboBox`].
pub text_input: fn(&Theme, text_input::Status) -> text_input::Appearance,
/// The style of the [`Menu`] of the [`ComboBox`].
///
/// [`Menu`]: menu::Menu
pub menu: menu::Style<Theme>,
}
impl Style<Theme> {
/// The default style of a [`ComboBox`].
pub const DEFAULT: Self = Self {
text_input: text_input::default,
menu: menu::Style::<Theme>::DEFAULT,
};
}
impl<Theme> Clone for Style<Theme> {
fn clone(&self) -> Self {
*self
}
}
impl<Theme> Copy for Style<Theme> {}
/// The default style of a [`ComboBox`].
pub trait DefaultStyle: Sized {
/// Returns the default style of a [`ComboBox`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
Style::<Self>::DEFAULT
}
}

View file

@ -1,6 +1,7 @@
//! Decorate content and apply alignment. //! Decorate content and apply alignment.
use crate::core::alignment::{self, Alignment}; use crate::core::alignment::{self, Alignment};
use crate::core::event::{self, Event}; use crate::core::event::{self, Event};
use crate::core::gradient::{self, Gradient};
use crate::core::layout; use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::overlay; use crate::core::overlay;
@ -8,13 +9,11 @@ use crate::core::renderer;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::widget::{self, Operation}; use crate::core::widget::{self, Operation};
use crate::core::{ use crate::core::{
Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
Point, Rectangle, Shell, Size, Vector, Widget, Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget,
}; };
use crate::runtime::Command; use crate::runtime::Command;
pub use iced_style::container::{Appearance, StyleSheet};
/// An element decorating some content. /// An element decorating some content.
/// ///
/// It is normally used for alignment purposes. /// It is normally used for alignment purposes.
@ -25,7 +24,6 @@ pub struct Container<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
id: Option<Id>, id: Option<Id>,
@ -36,21 +34,30 @@ pub struct Container<
max_height: f32, max_height: f32,
horizontal_alignment: alignment::Horizontal, horizontal_alignment: alignment::Horizontal,
vertical_alignment: alignment::Vertical, vertical_alignment: alignment::Vertical,
style: Theme::Style,
clip: bool, clip: bool,
content: Element<'a, Message, Theme, Renderer>, content: Element<'a, Message, Theme, Renderer>,
style: Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
/// Creates an empty [`Container`]. /// Creates a [`Container`] with the given content.
pub fn new<T>(content: T) -> Self pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self
where where
T: Into<Element<'a, Message, Theme, Renderer>>, Theme: DefaultStyle,
{ {
Self::with_style(content, Theme::default_style())
}
/// Creates a [`Container`] with the given content and style.
pub fn with_style(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
style: fn(&Theme, Status) -> Appearance,
) -> Self {
let content = content.into(); let content = content.into();
let size = content.as_widget().size_hint(); let size = content.as_widget().size_hint();
@ -63,9 +70,9 @@ where
max_height: f32::INFINITY, max_height: f32::INFINITY,
horizontal_alignment: alignment::Horizontal::Left, horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Top, vertical_alignment: alignment::Vertical::Top,
style: Default::default(),
clip: false, clip: false,
content, content,
style,
} }
} }
@ -130,8 +137,8 @@ where
} }
/// Sets the style of the [`Container`]. /// Sets the style of the [`Container`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style;
self self
} }
@ -146,7 +153,6 @@ where
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Container<'a, Message, Theme, Renderer> for Container<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -262,10 +268,18 @@ where
cursor: mouse::Cursor, cursor: mouse::Cursor,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let style = theme.appearance(&self.style); let bounds = layout.bounds();
if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { let status = if cursor.is_over(bounds) {
draw_background(renderer, &style, layout.bounds()); Status::Hovered
} else {
Status::Idle
};
let style = (self.style)(theme, status);
if let Some(clipped_viewport) = bounds.intersection(viewport) {
draw_background(renderer, &style, bounds);
self.content.as_widget().draw( self.content.as_widget().draw(
tree, tree,
@ -307,7 +321,7 @@ impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: 'a + StyleSheet, Theme: 'a,
Renderer: 'a + crate::core::Renderer, Renderer: 'a + crate::core::Renderer,
{ {
fn from( fn from(
@ -482,3 +496,121 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> {
bounds: None, bounds: None,
}) })
} }
/// The appearance of a container.
#[derive(Debug, Clone, Copy, Default)]
pub struct Appearance {
/// The text [`Color`] of the container.
pub text_color: Option<Color>,
/// The [`Background`] of the container.
pub background: Option<Background>,
/// The [`Border`] of the container.
pub border: Border,
/// The [`Shadow`] of the container.
pub shadow: Shadow,
}
impl Appearance {
/// Updates the border of the [`Appearance`] with the given [`Color`] and `width`.
pub fn with_border(
self,
color: impl Into<Color>,
width: impl Into<Pixels>,
) -> Self {
Self {
border: Border {
color: color.into(),
width: width.into().0,
..Border::default()
},
..self
}
}
/// Updates the background of the [`Appearance`].
pub fn with_background(self, background: impl Into<Background>) -> Self {
Self {
background: Some(background.into()),
..self
}
}
}
/// The possible status of a [`Container`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Container`] is idle.
Idle,
/// The [`Container`] is being hovered.
Hovered,
}
/// The style of a [`Container`].
pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
/// The default style of a [`Container`].
pub trait DefaultStyle {
/// Returns the default style of a [`Container`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
transparent
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}
impl DefaultStyle for Color {
fn default_style() -> Style<Self> {
|color, _status| Appearance::default().with_background(*color)
}
}
impl DefaultStyle for Gradient {
fn default_style() -> Style<Self> {
|gradient, _status| Appearance::default().with_background(*gradient)
}
}
impl DefaultStyle for gradient::Linear {
fn default_style() -> Style<Self> {
|gradient, _status| Appearance::default().with_background(*gradient)
}
}
/// A transparent [`Container`].
pub fn transparent<Theme>(_theme: &Theme, _status: Status) -> Appearance {
Appearance::default()
}
/// A rounded [`Container`] with a background.
pub fn box_(theme: &Theme, _status: Status) -> Appearance {
let palette = theme.extended_palette();
Appearance {
background: Some(palette.background.weak.color.into()),
border: Border::rounded(2),
..Appearance::default()
}
}
/// A bordered [`Container`] with a background.
pub fn bordered_box(theme: &Theme, _status: Status) -> Appearance {
let palette = theme.extended_palette();
Appearance {
background: Some(palette.background.weak.color.into()),
border: Border {
width: 1.0,
radius: 0.0.into(),
color: palette.background.strong.color,
},
..Appearance::default()
}
}

View file

@ -7,7 +7,6 @@ use crate::core;
use crate::core::widget::operation; use crate::core::widget::operation;
use crate::core::{Element, Length, Pixels}; use crate::core::{Element, Length, Pixels};
use crate::keyed; use crate::keyed;
use crate::overlay;
use crate::pick_list::{self, PickList}; use crate::pick_list::{self, PickList};
use crate::progress_bar::{self, ProgressBar}; use crate::progress_bar::{self, ProgressBar};
use crate::radio::{self, Radio}; use crate::radio::{self, Radio};
@ -15,13 +14,13 @@ use crate::rule::{self, Rule};
use crate::runtime::Command; use crate::runtime::Command;
use crate::scrollable::{self, Scrollable}; use crate::scrollable::{self, Scrollable};
use crate::slider::{self, Slider}; use crate::slider::{self, Slider};
use crate::style::application; use crate::text::Text;
use crate::text::{self, Text};
use crate::text_editor::{self, TextEditor}; use crate::text_editor::{self, TextEditor};
use crate::text_input::{self, TextInput}; use crate::text_input::{self, TextInput};
use crate::toggler::{self, Toggler}; use crate::toggler::{self, Toggler};
use crate::tooltip::{self, Tooltip}; use crate::tooltip::{self, Tooltip};
use crate::{Column, MouseArea, Row, Space, Themer, VerticalSlider}; use crate::vertical_slider::{self, VerticalSlider};
use crate::{Column, MouseArea, Row, Space, Themer};
use std::borrow::Borrow; use std::borrow::Borrow;
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
@ -59,7 +58,7 @@ pub fn container<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>, content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Container<'a, Message, Theme, Renderer> ) -> Container<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet, Theme: container::DefaultStyle,
Renderer: core::Renderer, Renderer: core::Renderer,
{ {
Container::new(content) Container::new(content)
@ -105,7 +104,7 @@ pub fn scrollable<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>, content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Scrollable<'a, Message, Theme, Renderer> ) -> Scrollable<'a, Message, Theme, Renderer>
where where
Theme: scrollable::StyleSheet, Theme: scrollable::DefaultStyle,
Renderer: core::Renderer, Renderer: core::Renderer,
{ {
Scrollable::new(content) Scrollable::new(content)
@ -118,8 +117,8 @@ pub fn button<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>, content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Button<'a, Message, Theme, Renderer> ) -> Button<'a, Message, Theme, Renderer>
where where
Theme: button::DefaultStyle,
Renderer: core::Renderer, Renderer: core::Renderer,
Theme: button::StyleSheet,
{ {
Button::new(content) Button::new(content)
} }
@ -135,7 +134,7 @@ pub fn tooltip<'a, Message, Theme, Renderer>(
position: tooltip::Position, position: tooltip::Position,
) -> crate::Tooltip<'a, Message, Theme, Renderer> ) -> crate::Tooltip<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet + text::StyleSheet, Theme: container::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
Tooltip::new(content, tooltip, position) Tooltip::new(content, tooltip, position)
@ -148,7 +147,6 @@ pub fn text<'a, Theme, Renderer>(
text: impl ToString, text: impl ToString,
) -> Text<'a, Theme, Renderer> ) -> Text<'a, Theme, Renderer>
where where
Theme: text::StyleSheet,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
Text::new(text.to_string()) Text::new(text.to_string())
@ -162,7 +160,7 @@ pub fn checkbox<'a, Message, Theme, Renderer>(
is_checked: bool, is_checked: bool,
) -> Checkbox<'a, Message, Theme, Renderer> ) -> Checkbox<'a, Message, Theme, Renderer>
where where
Theme: checkbox::StyleSheet + text::StyleSheet, Theme: checkbox::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
Checkbox::new(label, is_checked) Checkbox::new(label, is_checked)
@ -179,7 +177,7 @@ pub fn radio<Message, Theme, Renderer, V>(
) -> Radio<Message, Theme, Renderer> ) -> Radio<Message, Theme, Renderer>
where where
Message: Clone, Message: Clone,
Theme: radio::StyleSheet, Theme: radio::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
V: Copy + Eq, V: Copy + Eq,
{ {
@ -195,8 +193,8 @@ pub fn toggler<'a, Message, Theme, Renderer>(
f: impl Fn(bool) -> Message + 'a, f: impl Fn(bool) -> Message + 'a,
) -> Toggler<'a, Message, Theme, Renderer> ) -> Toggler<'a, Message, Theme, Renderer>
where where
Theme: toggler::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
Theme: toggler::StyleSheet,
{ {
Toggler::new(label, is_checked, f) Toggler::new(label, is_checked, f)
} }
@ -210,7 +208,7 @@ pub fn text_input<'a, Message, Theme, Renderer>(
) -> TextInput<'a, Message, Theme, Renderer> ) -> TextInput<'a, Message, Theme, Renderer>
where where
Message: Clone, Message: Clone,
Theme: text_input::StyleSheet, Theme: text_input::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
TextInput::new(placeholder, value) TextInput::new(placeholder, value)
@ -224,7 +222,7 @@ pub fn text_editor<Message, Theme, Renderer>(
) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Theme, Renderer> ) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Theme, Renderer>
where where
Message: Clone, Message: Clone,
Theme: text_editor::StyleSheet, Theme: text_editor::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
TextEditor::new(content) TextEditor::new(content)
@ -241,7 +239,7 @@ pub fn slider<'a, T, Message, Theme>(
where where
T: Copy + From<u8> + std::cmp::PartialOrd, T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone, Message: Clone,
Theme: slider::StyleSheet, Theme: slider::DefaultStyle,
{ {
Slider::new(range, value, on_change) Slider::new(range, value, on_change)
} }
@ -257,7 +255,7 @@ pub fn vertical_slider<'a, T, Message, Theme>(
where where
T: Copy + From<u8> + std::cmp::PartialOrd, T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone, Message: Clone,
Theme: slider::StyleSheet, Theme: vertical_slider::DefaultStyle,
{ {
VerticalSlider::new(range, value, on_change) VerticalSlider::new(range, value, on_change)
} }
@ -275,13 +273,8 @@ where
L: Borrow<[T]> + 'a, L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a, V: Borrow<T> + 'a,
Message: Clone, Message: Clone,
Theme: pick_list::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
Theme: pick_list::StyleSheet
+ scrollable::StyleSheet
+ overlay::menu::StyleSheet
+ container::StyleSheet,
<Theme as overlay::menu::StyleSheet>::Style:
From<<Theme as pick_list::StyleSheet>::Style>,
{ {
PickList::new(options, selected, on_selected) PickList::new(options, selected, on_selected)
} }
@ -297,7 +290,7 @@ pub fn combo_box<'a, T, Message, Theme, Renderer>(
) -> ComboBox<'a, T, Message, Theme, Renderer> ) -> ComboBox<'a, T, Message, Theme, Renderer>
where where
T: std::fmt::Display + Clone, T: std::fmt::Display + Clone,
Theme: text_input::StyleSheet + overlay::menu::StyleSheet, Theme: combo_box::DefaultStyle,
Renderer: core::text::Renderer, Renderer: core::text::Renderer,
{ {
ComboBox::new(state, placeholder, selection, on_selected) ComboBox::new(state, placeholder, selection, on_selected)
@ -324,7 +317,7 @@ pub fn vertical_space() -> Space {
/// [`Rule`]: crate::Rule /// [`Rule`]: crate::Rule
pub fn horizontal_rule<Theme>(height: impl Into<Pixels>) -> Rule<Theme> pub fn horizontal_rule<Theme>(height: impl Into<Pixels>) -> Rule<Theme>
where where
Theme: rule::StyleSheet, Theme: rule::DefaultStyle,
{ {
Rule::horizontal(height) Rule::horizontal(height)
} }
@ -334,7 +327,7 @@ where
/// [`Rule`]: crate::Rule /// [`Rule`]: crate::Rule
pub fn vertical_rule<Theme>(width: impl Into<Pixels>) -> Rule<Theme> pub fn vertical_rule<Theme>(width: impl Into<Pixels>) -> Rule<Theme>
where where
Theme: rule::StyleSheet, Theme: rule::DefaultStyle,
{ {
Rule::vertical(width) Rule::vertical(width)
} }
@ -351,7 +344,7 @@ pub fn progress_bar<Theme>(
value: f32, value: f32,
) -> ProgressBar<Theme> ) -> ProgressBar<Theme>
where where
Theme: progress_bar::StyleSheet, Theme: progress_bar::DefaultStyle,
{ {
ProgressBar::new(range, value) ProgressBar::new(range, value)
} }
@ -371,7 +364,7 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> {
#[cfg(feature = "svg")] #[cfg(feature = "svg")]
pub fn svg<Theme>(handle: impl Into<core::svg::Handle>) -> crate::Svg<Theme> pub fn svg<Theme>(handle: impl Into<core::svg::Handle>) -> crate::Svg<Theme>
where where
Theme: crate::svg::StyleSheet, Theme: crate::svg::DefaultStyle,
{ {
crate::Svg::new(handle) crate::Svg::new(handle)
} }
@ -397,7 +390,7 @@ where
#[cfg(feature = "qr_code")] #[cfg(feature = "qr_code")]
pub fn qr_code<Theme>(data: &crate::qr_code::Data) -> crate::QRCode<'_, Theme> pub fn qr_code<Theme>(data: &crate::qr_code::Data) -> crate::QRCode<'_, Theme>
where where
Theme: crate::qr_code::StyleSheet, Theme: crate::qr_code::DefaultStyle,
{ {
crate::QRCode::new(data) crate::QRCode::new(data)
} }
@ -440,13 +433,20 @@ where
} }
/// A widget that applies any `Theme` to its contents. /// A widget that applies any `Theme` to its contents.
pub fn themer<'a, Message, Theme, Renderer>( pub fn themer<'a, Message, OldTheme, NewTheme, Renderer>(
theme: Theme, new_theme: NewTheme,
content: impl Into<Element<'a, Message, Theme, Renderer>>, content: impl Into<Element<'a, Message, NewTheme, Renderer>>,
) -> Themer<'a, Message, Theme, Renderer> ) -> Themer<
'a,
Message,
OldTheme,
NewTheme,
impl Fn(&OldTheme) -> NewTheme,
Renderer,
>
where where
Renderer: core::Renderer, Renderer: core::Renderer,
Theme: application::StyleSheet, NewTheme: Clone,
{ {
Themer::new(theme, content) Themer::new(move |_| new_theme.clone(), content)
} }

View file

@ -14,11 +14,11 @@ pub use iced_renderer as renderer;
pub use iced_renderer::graphics; pub use iced_renderer::graphics;
pub use iced_runtime as runtime; pub use iced_runtime as runtime;
pub use iced_runtime::core; pub use iced_runtime::core;
pub use iced_style as style;
mod column; mod column;
mod mouse_area; mod mouse_area;
mod row; mod row;
mod space;
mod themer; mod themer;
pub mod button; pub mod button;
@ -34,7 +34,6 @@ pub mod radio;
pub mod rule; pub mod rule;
pub mod scrollable; pub mod scrollable;
pub mod slider; pub mod slider;
pub mod space;
pub mod text; pub mod text;
pub mod text_editor; pub mod text_editor;
pub mod text_input; pub mod text_input;
@ -135,5 +134,5 @@ pub mod qr_code;
#[doc(no_inline)] #[doc(no_inline)]
pub use qr_code::QRCode; pub use qr_code::QRCode;
pub use crate::core::theme::{self, Theme};
pub use renderer::Renderer; pub use renderer::Renderer;
pub use style::theme::{self, Theme};

View file

@ -10,13 +10,12 @@ use crate::core::text::{self, Text};
use crate::core::touch; use crate::core::touch;
use crate::core::widget::Tree; use crate::core::widget::Tree;
use crate::core::{ use crate::core::{
Border, Clipboard, Length, Padding, Pixels, Point, Rectangle, Size, Vector, Background, Border, Clipboard, Color, Length, Padding, Pixels, Point,
Rectangle, Size, Theme, Vector,
}; };
use crate::core::{Element, Shell, Widget}; use crate::core::{Element, Shell, Widget};
use crate::scrollable::{self, Scrollable}; use crate::scrollable::{self, Scrollable};
pub use iced_style::menu::{Appearance, StyleSheet};
/// A list of selectable options. /// A list of selectable options.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Menu< pub struct Menu<
@ -26,7 +25,6 @@ pub struct Menu<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
state: &'a mut State, state: &'a mut State,
@ -40,14 +38,14 @@ pub struct Menu<
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer> impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer>
where where
T: ToString + Clone, T: ToString + Clone,
Message: 'a, Message: 'a,
Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, Theme: 'a,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
/// Creates a new [`Menu`] with the given [`State`], a list of options, and /// Creates a new [`Menu`] with the given [`State`], a list of options, and
@ -58,6 +56,29 @@ where
hovered_option: &'a mut Option<usize>, hovered_option: &'a mut Option<usize>,
on_selected: impl FnMut(T) -> Message + 'a, on_selected: impl FnMut(T) -> Message + 'a,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>, on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
) -> Self
where
Theme: DefaultStyle,
{
Self::with_style(
state,
options,
hovered_option,
on_selected,
on_option_hovered,
Theme::default_style(),
)
}
/// Creates a new [`Menu`] with the given [`State`], a list of options,
/// the message to produced when an option is selected, and its [`Style`].
pub fn with_style(
state: &'a mut State,
options: &'a [T],
hovered_option: &'a mut Option<usize>,
on_selected: impl FnMut(T) -> Message + 'a,
on_option_hovered: Option<&'a dyn Fn(T) -> Message>,
style: Style<Theme>,
) -> Self { ) -> Self {
Menu { Menu {
state, state,
@ -71,7 +92,7 @@ where
text_line_height: text::LineHeight::default(), text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic, text_shaping: text::Shaping::Basic,
font: None, font: None,
style: Default::default(), style,
} }
} }
@ -115,10 +136,7 @@ where
} }
/// Sets the style of the [`Menu`]. /// Sets the style of the [`Menu`].
pub fn style( pub fn style(mut self, style: impl Into<Style<Theme>>) -> Self {
mut self,
style: impl Into<<Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -165,7 +183,6 @@ impl Default for State {
struct Overlay<'a, Message, Theme, Renderer> struct Overlay<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet + container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
position: Point, position: Point,
@ -173,13 +190,13 @@ where
container: Container<'a, Message, Theme, Renderer>, container: Container<'a, Message, Theme, Renderer>,
width: f32, width: f32,
target_height: f32, target_height: f32,
style: <Theme as StyleSheet>::Style, style: Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, Theme: 'a,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
pub fn new<T>( pub fn new<T>(
@ -205,18 +222,25 @@ where
style, style,
} = menu; } = menu;
let container = Container::new(Scrollable::new(List { let container = Container::with_style(
options, Scrollable::with_direction_and_style(
hovered_option, List {
on_selected, options,
on_option_hovered, hovered_option,
font, on_selected,
text_size, on_option_hovered,
text_line_height, font,
text_shaping, text_size,
padding, text_line_height,
style: style.clone(), text_shaping,
})); padding,
style: style.list,
},
scrollable::Direction::default(),
style.scrollable,
),
container::transparent,
);
state.tree.diff(&container as &dyn Widget<_, _, _>); state.tree.diff(&container as &dyn Widget<_, _, _>);
@ -235,7 +259,6 @@ impl<'a, Message, Theme, Renderer>
crate::core::Overlay<Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer>
for Overlay<'a, Message, Theme, Renderer> for Overlay<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet + container::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
@ -302,9 +325,10 @@ where
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) { ) {
let appearance = StyleSheet::appearance(theme, &self.style);
let bounds = layout.bounds(); let bounds = layout.bounds();
let appearance = (self.style.list)(theme);
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds, bounds,
@ -321,7 +345,6 @@ where
struct List<'a, T, Message, Theme, Renderer> struct List<'a, T, Message, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
options: &'a [T], options: &'a [T],
@ -333,14 +356,13 @@ where
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
style: Theme::Style, style: fn(&Theme) -> Appearance,
} }
impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for List<'a, T, Message, Theme, Renderer> for List<'a, T, Message, Theme, Renderer>
where where
T: Clone + ToString, T: Clone + ToString,
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
@ -483,7 +505,7 @@ where
_cursor: mouse::Cursor, _cursor: mouse::Cursor,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let appearance = theme.appearance(&self.style); let appearance = (self.style)(theme);
let bounds = layout.bounds(); let bounds = layout.bounds();
let text_size = let text_size =
@ -517,7 +539,7 @@ where
width: bounds.width - appearance.border.width * 2.0, width: bounds.width - appearance.border.width * 2.0,
..bounds ..bounds
}, },
border: Border::with_radius(appearance.border.radius), border: Border::rounded(appearance.border.radius),
..renderer::Quad::default() ..renderer::Quad::default()
}, },
appearance.selected_background, appearance.selected_background,
@ -553,10 +575,79 @@ impl<'a, T, Message, Theme, Renderer>
where where
T: ToString + Clone, T: ToString + Clone,
Message: 'a, Message: 'a,
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: 'a + text::Renderer, Renderer: 'a + text::Renderer,
{ {
fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self { fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self {
Element::new(list) Element::new(list)
} }
} }
/// The appearance of a [`Menu`].
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the menu.
pub background: Background,
/// The [`Border`] of the menu.
pub border: Border,
/// The text [`Color`] of the menu.
pub text_color: Color,
/// The text [`Color`] of a selected option in the menu.
pub selected_text_color: Color,
/// The background [`Color`] of a selected option in the menu.
pub selected_background: Background,
}
/// The style of the different parts of a [`Menu`].
#[derive(Debug, PartialEq, Eq)]
pub struct Style<Theme> {
/// The style of the list of the [`Menu`].
pub list: fn(&Theme) -> Appearance,
/// The style of the [`Scrollable`] of the [`Menu`].
pub scrollable: fn(&Theme, scrollable::Status) -> scrollable::Appearance,
}
impl Style<Theme> {
/// The default style of a [`Menu`] with the built-in [`Theme`].
pub const DEFAULT: Self = Self {
list: default,
scrollable: scrollable::default,
};
}
impl<Theme> Clone for Style<Theme> {
fn clone(&self) -> Self {
*self
}
}
impl<Theme> Copy for Style<Theme> {}
/// The default style of a [`Menu`].
pub trait DefaultStyle: Sized {
/// Returns the default style of a [`Menu`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
Style::<Theme>::DEFAULT
}
}
/// The default style of the list of a [`Menu`].
pub fn default(theme: &Theme) -> Appearance {
let palette = theme.extended_palette();
Appearance {
background: palette.background.weak.color.into(),
border: Border {
width: 1.0,
radius: 0.0.into(),
color: palette.background.strong.color,
},
text_color: palette.background.weak.text,
selected_text_color: palette.primary.strong.text,
selected_background: palette.primary.strong.color.into(),
}
}

File diff suppressed because it is too large Load diff

View file

@ -20,25 +20,26 @@ pub struct Content<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>, title_bar: Option<TitleBar<'a, Message, Theme, Renderer>>,
body: Element<'a, Message, Theme, Renderer>, body: Element<'a, Message, Theme, Renderer>,
style: Theme::Style, style: container::Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
/// Creates a new [`Content`] with the provided body. /// Creates a new [`Content`] with the provided body.
pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self { pub fn new(body: impl Into<Element<'a, Message, Theme, Renderer>>) -> Self
where
Theme: container::DefaultStyle,
{
Self { Self {
title_bar: None, title_bar: None,
body: body.into(), body: body.into(),
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -52,7 +53,10 @@ where
} }
/// Sets the style of the [`Content`]. /// Sets the style of the [`Content`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(
mut self,
style: fn(&Theme, container::Status) -> container::Appearance,
) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -60,7 +64,6 @@ where
impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
pub(super) fn state(&self) -> Tree { pub(super) fn state(&self) -> Tree {
@ -104,7 +107,15 @@ where
let bounds = layout.bounds(); let bounds = layout.bounds();
{ {
let style = theme.appearance(&self.style); let style = {
let status = if cursor.is_over(bounds) {
container::Status::Hovered
} else {
container::Status::Idle
};
(self.style)(theme, status)
};
container::draw_background(renderer, &style, bounds); container::draw_background(renderer, &style, bounds);
} }
@ -370,7 +381,6 @@ where
impl<'a, Message, Theme, Renderer> Draggable impl<'a, Message, Theme, Renderer> Draggable
for &Content<'a, Message, Theme, Renderer> for &Content<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
fn can_be_dragged_at( fn can_be_dragged_at(
@ -393,7 +403,7 @@ impl<'a, T, Message, Theme, Renderer> From<T>
for Content<'a, Message, Theme, Renderer> for Content<'a, Message, Theme, Renderer>
where where
T: Into<Element<'a, Message, Theme, Renderer>>, T: Into<Element<'a, Message, Theme, Renderer>>,
Theme: container::StyleSheet, Theme: container::DefaultStyle,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
fn from(element: T) -> Self { fn from(element: T) -> Self {

View file

@ -19,32 +19,32 @@ pub struct TitleBar<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
content: Element<'a, Message, Theme, Renderer>, content: Element<'a, Message, Theme, Renderer>,
controls: Option<Element<'a, Message, Theme, Renderer>>, controls: Option<Element<'a, Message, Theme, Renderer>>,
padding: Padding, padding: Padding,
always_show_controls: bool, always_show_controls: bool,
style: Theme::Style, style: container::Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
/// Creates a new [`TitleBar`] with the given content. /// Creates a new [`TitleBar`] with the given content.
pub fn new<E>(content: E) -> Self pub fn new(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> Self
where where
E: Into<Element<'a, Message, Theme, Renderer>>, Theme: container::DefaultStyle,
{ {
Self { Self {
content: content.into(), content: content.into(),
controls: None, controls: None,
padding: Padding::ZERO, padding: Padding::ZERO,
always_show_controls: false, always_show_controls: false,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -64,7 +64,10 @@ where
} }
/// Sets the style of the [`TitleBar`]. /// Sets the style of the [`TitleBar`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(
mut self,
style: fn(&Theme, container::Status) -> container::Appearance,
) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -85,7 +88,6 @@ where
impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
pub(super) fn state(&self) -> Tree { pub(super) fn state(&self) -> Tree {
@ -128,7 +130,17 @@ where
show_controls: bool, show_controls: bool,
) { ) {
let bounds = layout.bounds(); let bounds = layout.bounds();
let style = theme.appearance(&self.style);
let style = {
let status = if cursor.is_over(bounds) {
container::Status::Hovered
} else {
container::Status::Idle
};
(self.style)(theme, status)
};
let inherited_style = renderer::Style { let inherited_style = renderer::Style {
text_color: style.text_color.unwrap_or(inherited_style.text_color), text_color: style.text_color.unwrap_or(inherited_style.text_color),
}; };

View file

@ -1,5 +1,4 @@
//! Display a dropdown list of selectable values. //! Display a dropdown list of selectable values.
use crate::container;
use crate::core::alignment; use crate::core::alignment;
use crate::core::event::{self, Event}; use crate::core::event::{self, Event};
use crate::core::keyboard; use crate::core::keyboard;
@ -11,15 +10,13 @@ use crate::core::text::{self, Paragraph as _, Text};
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Background, Border, Clipboard, Color, Element, Layout, Length, Padding,
Shell, Size, Vector, Widget, Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget,
}; };
use crate::overlay::menu::{self, Menu}; use crate::overlay::menu::{self, Menu};
use crate::scrollable;
use std::borrow::Borrow; use std::borrow::Borrow;
use std::f32;
pub use crate::style::pick_list::{Appearance, StyleSheet};
/// A widget for selecting a single value from a list of options. /// A widget for selecting a single value from a list of options.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
@ -35,7 +32,6 @@ pub struct PickList<
T: ToString + PartialEq + Clone, T: ToString + PartialEq + Clone,
L: Borrow<[T]> + 'a, L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a, V: Borrow<T> + 'a,
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
on_select: Box<dyn Fn(T) -> Message + 'a>, on_select: Box<dyn Fn(T) -> Message + 'a>,
@ -51,7 +47,7 @@ pub struct PickList<
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
handle: Handle<Renderer::Font>, handle: Handle<Renderer::Font>,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, T, L, V, Message, Theme, Renderer> impl<'a, T, L, V, Message, Theme, Renderer>
@ -61,23 +57,18 @@ where
L: Borrow<[T]> + 'a, L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a, V: Borrow<T> + 'a,
Message: Clone, Message: Clone,
Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// The default padding of a [`PickList`].
pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
/// Creates a new [`PickList`] with the given list of options, the current /// Creates a new [`PickList`] with the given list of options, the current
/// selected value, and the message to produce when an option is selected. /// selected value, and the message to produce when an option is selected.
pub fn new( pub fn new(
options: L, options: L,
selected: Option<V>, selected: Option<V>,
on_select: impl Fn(T) -> Message + 'a, on_select: impl Fn(T) -> Message + 'a,
) -> Self { ) -> Self
where
Theme: DefaultStyle,
{
Self { Self {
on_select: Box::new(on_select), on_select: Box::new(on_select),
on_open: None, on_open: None,
@ -86,13 +77,13 @@ where
placeholder: None, placeholder: None,
selected, selected,
width: Length::Shrink, width: Length::Shrink,
padding: Self::DEFAULT_PADDING, padding: crate::button::DEFAULT_PADDING,
text_size: None, text_size: None,
text_line_height: text::LineHeight::default(), text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic, text_shaping: text::Shaping::Basic,
font: None, font: None,
handle: Handle::default(), handle: Handle::default(),
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -160,10 +151,7 @@ where
} }
/// Sets the style of the [`PickList`]. /// Sets the style of the [`PickList`].
pub fn style( pub fn style(mut self, style: impl Into<Style<Theme>>) -> Self {
mut self,
style: impl Into<<Theme as StyleSheet>::Style>,
) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -176,11 +164,6 @@ where
L: Borrow<[T]>, L: Borrow<[T]>,
V: Borrow<T>, V: Borrow<T>,
Message: Clone + 'a, Message: Clone + 'a,
Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet,
<Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -204,19 +187,77 @@ where
renderer: &Renderer, renderer: &Renderer,
limits: &layout::Limits, limits: &layout::Limits,
) -> layout::Node { ) -> layout::Node {
layout( let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
renderer, let font = self.font.unwrap_or_else(|| renderer.default_font());
limits, let text_size =
self.width, self.text_size.unwrap_or_else(|| renderer.default_size());
self.padding, let options = self.options.borrow();
self.text_size,
self.text_line_height, state.options.resize_with(options.len(), Default::default);
self.text_shaping,
self.font, let option_text = Text {
self.placeholder.as_deref(), content: "",
self.options.borrow(), bounds: Size::new(
) f32::INFINITY,
self.text_line_height.to_absolute(text_size).into(),
),
size: text_size,
line_height: self.text_line_height,
font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut())
{
let label = option.to_string();
paragraph.update(Text {
content: &label,
..option_text
});
}
if let Some(placeholder) = &self.placeholder {
state.placeholder.update(Text {
content: placeholder,
..option_text
});
}
let max_width = match self.width {
Length::Shrink => {
let labels_width =
state.options.iter().fold(0.0, |width, paragraph| {
f32::max(width, paragraph.min_width())
});
labels_width.max(
self.placeholder
.as_ref()
.map(|_| state.placeholder.min_width())
.unwrap_or(0.0),
)
}
_ => 0.0,
};
let size = {
let intrinsic = Size::new(
max_width + text_size.0 + self.padding.left,
f32::from(self.text_line_height.to_absolute(text_size)),
);
limits
.width(self.width)
.shrink(self.padding)
.resolve(self.width, Length::Shrink, intrinsic)
.expand(self.padding)
};
layout::Node::new(size)
} }
fn on_event( fn on_event(
@ -230,18 +271,98 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &Rectangle, _viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
update( match event {
event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
layout, | Event::Touch(touch::Event::FingerPressed { .. }) => {
cursor, let state =
shell, tree.state.downcast_mut::<State<Renderer::Paragraph>>();
self.on_select.as_ref(),
self.on_open.as_ref(), if state.is_open {
self.on_close.as_ref(), // Event wasn't processed by overlay, so cursor was clicked either outside its
self.selected.as_ref().map(Borrow::borrow), // bounds or on the drop-down, either way we close the overlay.
self.options.borrow(), state.is_open = false;
|| tree.state.downcast_mut::<State<Renderer::Paragraph>>(),
) if let Some(on_close) = &self.on_close {
shell.publish(on_close.clone());
}
event::Status::Captured
} else if cursor.is_over(layout.bounds()) {
let selected = self.selected.as_ref().map(Borrow::borrow);
state.is_open = true;
state.hovered_option = self
.options
.borrow()
.iter()
.position(|option| Some(option) == selected);
if let Some(on_open) = &self.on_open {
shell.publish(on_open.clone());
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
let state =
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
if state.keyboard_modifiers.command()
&& cursor.is_over(layout.bounds())
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
mut options: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let _ = options.find(|&option| option == selected);
options.next()
}
let options = self.options.borrow();
let selected = self.selected.as_ref().map(Borrow::borrow);
let next_option = if y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
} else if y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
options.last()
}
} else {
None
};
if let Some(next_option) = next_option {
shell.publish((self.on_select)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state =
tree.state.downcast_mut::<State<Renderer::Paragraph>>();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
}
_ => event::Status::Ignored,
}
} }
fn mouse_interaction( fn mouse_interaction(
@ -252,7 +373,14 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
_renderer: &Renderer, _renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
mouse_interaction(layout, cursor) let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if is_mouse_over {
mouse::Interaction::Pointer
} else {
mouse::Interaction::default()
}
} }
fn draw( fn draw(
@ -266,23 +394,124 @@ where
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let font = self.font.unwrap_or_else(|| renderer.default_font()); let font = self.font.unwrap_or_else(|| renderer.default_font());
draw( let selected = self.selected.as_ref().map(Borrow::borrow);
renderer, let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
theme,
layout, let bounds = layout.bounds();
cursor, let is_mouse_over = cursor.is_over(bounds);
self.padding, let is_selected = selected.is_some();
self.text_size,
self.text_line_height, let status = if state.is_open {
self.text_shaping, Status::Opened
font, } else if is_mouse_over {
self.placeholder.as_deref(), Status::Hovered
self.selected.as_ref().map(Borrow::borrow), } else {
&self.handle, Status::Active
&self.style, };
|| tree.state.downcast_ref::<State<Renderer::Paragraph>>(),
viewport, let appearance = (self.style.field)(theme, status);
renderer.fill_quad(
renderer::Quad {
bounds,
border: appearance.border,
..renderer::Quad::default()
},
appearance.background,
); );
let handle = match &self.handle {
Handle::Arrow { size } => Some((
Renderer::ICON_FONT,
Renderer::ARROW_DOWN_ICON,
*size,
text::LineHeight::default(),
text::Shaping::Basic,
)),
Handle::Static(Icon {
font,
code_point,
size,
line_height,
shaping,
}) => Some((*font, *code_point, *size, *line_height, *shaping)),
Handle::Dynamic { open, closed } => {
if state.is_open {
Some((
open.font,
open.code_point,
open.size,
open.line_height,
open.shaping,
))
} else {
Some((
closed.font,
closed.code_point,
closed.size,
closed.line_height,
closed.shaping,
))
}
}
Handle::None => None,
};
if let Some((font, code_point, size, line_height, shaping)) = handle {
let size = size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(
Text {
content: &code_point.to_string(),
size,
line_height,
font,
bounds: Size::new(
bounds.width,
f32::from(line_height.to_absolute(size)),
),
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
},
Point::new(
bounds.x + bounds.width - self.padding.right,
bounds.center_y(),
),
appearance.handle_color,
*viewport,
);
}
let label = selected.map(ToString::to_string);
if let Some(label) = label.as_deref().or(self.placeholder.as_deref()) {
let text_size =
self.text_size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(
Text {
content: label,
size: text_size,
line_height: self.text_line_height,
font,
bounds: Size::new(
bounds.width - self.padding.horizontal(),
f32::from(self.text_line_height.to_absolute(text_size)),
),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: self.text_shaping,
},
Point::new(bounds.x + self.padding.left, bounds.center_y()),
if is_selected {
appearance.text_color
} else {
appearance.placeholder_color
},
*viewport,
);
}
} }
fn overlay<'b>( fn overlay<'b>(
@ -293,19 +522,38 @@ where
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
let font = self.font.unwrap_or_else(|| renderer.default_font());
overlay( if state.is_open {
layout, let bounds = layout.bounds();
translation,
state, let on_select = &self.on_select;
self.padding,
self.text_size, let mut menu = Menu::with_style(
self.text_shaping, &mut state.menu,
self.font.unwrap_or_else(|| renderer.default_font()), self.options.borrow(),
self.options.borrow(), &mut state.hovered_option,
&self.on_select, |option| {
self.style.clone(), state.is_open = false;
)
(on_select)(option)
},
None,
self.style.menu,
)
.width(bounds.width)
.padding(self.padding)
.font(font)
.text_shaping(self.text_shaping);
if let Some(text_size) = self.text_size {
menu = menu.text_size(text_size);
}
Some(menu.overlay(layout.position() + translation, bounds.height))
} else {
None
}
} }
} }
@ -317,12 +565,7 @@ where
L: Borrow<[T]> + 'a, L: Borrow<[T]> + 'a,
V: Borrow<T> + 'a, V: Borrow<T> + 'a,
Message: Clone + 'a, Message: Clone + 'a,
Theme: StyleSheet Theme: 'a,
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet
+ 'a,
<Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
fn from( fn from(
@ -332,9 +575,8 @@ where
} }
} }
/// The state of a [`PickList`].
#[derive(Debug)] #[derive(Debug)]
pub struct State<P: text::Paragraph> { struct State<P: text::Paragraph> {
menu: menu::State, menu: menu::State,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
is_open: bool, is_open: bool,
@ -407,394 +649,94 @@ pub struct Icon<Font> {
pub shaping: text::Shaping, pub shaping: text::Shaping,
} }
/// Computes the layout of a [`PickList`]. /// The possible status of a [`PickList`].
pub fn layout<Renderer, T>( #[derive(Debug, Clone, Copy, PartialEq, Eq)]
state: &mut State<Renderer::Paragraph>, pub enum Status {
renderer: &Renderer, /// The [`PickList`] can be interacted with.
limits: &layout::Limits, Active,
width: Length, /// The [`PickList`] is being hovered.
padding: Padding, Hovered,
text_size: Option<Pixels>, /// The [`PickList`] is open.
text_line_height: text::LineHeight, Opened,
text_shaping: text::Shaping,
font: Option<Renderer::Font>,
placeholder: Option<&str>,
options: &[T],
) -> layout::Node
where
Renderer: text::Renderer,
T: ToString,
{
use std::f32;
let font = font.unwrap_or_else(|| renderer.default_font());
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
state.options.resize_with(options.len(), Default::default);
let option_text = Text {
content: "",
bounds: Size::new(
f32::INFINITY,
text_line_height.to_absolute(text_size).into(),
),
size: text_size,
line_height: text_line_height,
font,
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text_shaping,
};
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
let label = option.to_string();
paragraph.update(Text {
content: &label,
..option_text
});
}
if let Some(placeholder) = placeholder {
state.placeholder.update(Text {
content: placeholder,
..option_text
});
}
let max_width = match width {
Length::Shrink => {
let labels_width =
state.options.iter().fold(0.0, |width, paragraph| {
f32::max(width, paragraph.min_width())
});
labels_width.max(
placeholder
.map(|_| state.placeholder.min_width())
.unwrap_or(0.0),
)
}
_ => 0.0,
};
let size = {
let intrinsic = Size::new(
max_width + text_size.0 + padding.left,
f32::from(text_line_height.to_absolute(text_size)),
);
limits
.width(width)
.shrink(padding)
.resolve(width, Length::Shrink, intrinsic)
.expand(padding)
};
layout::Node::new(size)
} }
/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] /// The appearance of a pick list.
/// accordingly. #[derive(Debug, Clone, Copy)]
pub fn update<'a, T, P, Message>( pub struct Appearance {
event: Event, /// The text [`Color`] of the pick list.
layout: Layout<'_>, pub text_color: Color,
cursor: mouse::Cursor, /// The placeholder [`Color`] of the pick list.
shell: &mut Shell<'_, Message>, pub placeholder_color: Color,
on_select: &dyn Fn(T) -> Message, /// The handle [`Color`] of the pick list.
on_open: Option<&Message>, pub handle_color: Color,
on_close: Option<&Message>, /// The [`Background`] of the pick list.
selected: Option<&T>, pub background: Background,
options: &[T], /// The [`Border`] of the pick list.
state: impl FnOnce() -> &'a mut State<P>, pub border: Border,
) -> event::Status }
where
T: PartialEq + Clone + 'a,
P: text::Paragraph + 'a,
Message: Clone,
{
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
let state = state();
if state.is_open { /// The styles of the different parts of a [`PickList`].
// Event wasn't processed by overlay, so cursor was clicked either outside it's #[derive(Debug, PartialEq, Eq)]
// bounds or on the drop-down, either way we close the overlay. pub struct Style<Theme> {
state.is_open = false; /// The style of the [`PickList`] itself.
pub field: fn(&Theme, Status) -> Appearance,
if let Some(on_close) = on_close { /// The style of the [`Menu`] of the pick list.
shell.publish(on_close.clone()); pub menu: menu::Style<Theme>,
} }
event::Status::Captured impl Style<Theme> {
} else if cursor.is_over(layout.bounds()) { /// The default style of a [`PickList`] with the built-in [`Theme`].
state.is_open = true; pub const DEFAULT: Self = Self {
state.hovered_option = field: default,
options.iter().position(|option| Some(option) == selected); menu: menu::Style::<Theme>::DEFAULT,
};
}
if let Some(on_open) = on_open { impl<Theme> Clone for Style<Theme> {
shell.publish(on_open.clone()); fn clone(&self) -> Self {
} *self
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Mouse(mouse::Event::WheelScrolled {
delta: mouse::ScrollDelta::Lines { y, .. },
}) => {
let state = state();
if state.keyboard_modifiers.command()
&& cursor.is_over(layout.bounds())
&& !state.is_open
{
fn find_next<'a, T: PartialEq>(
selected: &'a T,
mut options: impl Iterator<Item = &'a T>,
) -> Option<&'a T> {
let _ = options.find(|&option| option == selected);
options.next()
}
let next_option = if y < 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter())
} else {
options.first()
}
} else if y > 0.0 {
if let Some(selected) = selected {
find_next(selected, options.iter().rev())
} else {
options.last()
}
} else {
None
};
if let Some(next_option) = next_option {
shell.publish((on_select)(next_option.clone()));
}
event::Status::Captured
} else {
event::Status::Ignored
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
let state = state();
state.keyboard_modifiers = modifiers;
event::Status::Ignored
}
_ => event::Status::Ignored,
} }
} }
/// Returns the current [`mouse::Interaction`] of a [`PickList`]. impl<Theme> Copy for Style<Theme> {}
pub fn mouse_interaction(
layout: Layout<'_>,
cursor: mouse::Cursor,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if is_mouse_over { /// The default style of a [`PickList`].
mouse::Interaction::Pointer pub trait DefaultStyle: Sized {
} else { /// Returns the default style of a [`PickList`].
mouse::Interaction::default() fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
Style::<Self>::DEFAULT
} }
} }
/// Returns the current overlay of a [`PickList`]. /// The default style of the field of a [`PickList`].
pub fn overlay<'a, T, Message, Theme, Renderer>( pub fn default(theme: &Theme, status: Status) -> Appearance {
layout: Layout<'_>, let palette = theme.extended_palette();
translation: Vector,
state: &'a mut State<Renderer::Paragraph>,
padding: Padding,
text_size: Option<Pixels>,
text_shaping: text::Shaping,
font: Renderer::Font,
options: &'a [T],
on_selected: &'a dyn Fn(T) -> Message,
style: <Theme as StyleSheet>::Style,
) -> Option<overlay::Element<'a, Message, Theme, Renderer>>
where
T: Clone + ToString,
Message: 'a,
Theme: StyleSheet
+ scrollable::StyleSheet
+ menu::StyleSheet
+ container::StyleSheet
+ 'a,
<Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>,
Renderer: text::Renderer + 'a,
{
if state.is_open {
let bounds = layout.bounds();
let mut menu = Menu::new( let active = Appearance {
&mut state.menu, text_color: palette.background.weak.text,
options, background: palette.background.weak.color.into(),
&mut state.hovered_option, placeholder_color: palette.background.strong.color,
|option| { handle_color: palette.background.weak.text,
state.is_open = false; border: Border {
radius: 2.0.into(),
(on_selected)(option) width: 1.0,
}, color: palette.background.strong.color,
None,
)
.width(bounds.width)
.padding(padding)
.font(font)
.text_shaping(text_shaping)
.style(style);
if let Some(text_size) = text_size {
menu = menu.text_size(text_size);
}
Some(menu.overlay(layout.position() + translation, bounds.height))
} else {
None
}
}
/// Draws a [`PickList`].
pub fn draw<'a, T, Theme, Renderer>(
renderer: &mut Renderer,
theme: &Theme,
layout: Layout<'_>,
cursor: mouse::Cursor,
padding: Padding,
text_size: Option<Pixels>,
text_line_height: text::LineHeight,
text_shaping: text::Shaping,
font: Renderer::Font,
placeholder: Option<&str>,
selected: Option<&T>,
handle: &Handle<Renderer::Font>,
style: &Theme::Style,
state: impl FnOnce() -> &'a State<Renderer::Paragraph>,
viewport: &Rectangle,
) where
Renderer: text::Renderer,
Theme: StyleSheet,
T: ToString + 'a,
{
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
let is_selected = selected.is_some();
let style = if is_mouse_over {
theme.hovered(style)
} else {
theme.active(style)
};
renderer.fill_quad(
renderer::Quad {
bounds,
border: style.border,
..renderer::Quad::default()
}, },
style.background,
);
let handle = match handle {
Handle::Arrow { size } => Some((
Renderer::ICON_FONT,
Renderer::ARROW_DOWN_ICON,
*size,
text::LineHeight::default(),
text::Shaping::Basic,
)),
Handle::Static(Icon {
font,
code_point,
size,
line_height,
shaping,
}) => Some((*font, *code_point, *size, *line_height, *shaping)),
Handle::Dynamic { open, closed } => {
if state().is_open {
Some((
open.font,
open.code_point,
open.size,
open.line_height,
open.shaping,
))
} else {
Some((
closed.font,
closed.code_point,
closed.size,
closed.line_height,
closed.shaping,
))
}
}
Handle::None => None,
}; };
if let Some((font, code_point, size, line_height, shaping)) = handle { match status {
let size = size.unwrap_or_else(|| renderer.default_size()); Status::Active => active,
Status::Hovered | Status::Opened => Appearance {
renderer.fill_text( border: Border {
Text { color: palette.primary.strong.color,
content: &code_point.to_string(), ..active.border
size,
line_height,
font,
bounds: Size::new(
bounds.width,
f32::from(line_height.to_absolute(size)),
),
horizontal_alignment: alignment::Horizontal::Right,
vertical_alignment: alignment::Vertical::Center,
shaping,
}, },
Point::new( ..active
bounds.x + bounds.width - padding.horizontal(), },
bounds.center_y(),
),
style.handle_color,
*viewport,
);
}
let label = selected.map(ToString::to_string);
if let Some(label) = label.as_deref().or(placeholder) {
let text_size = text_size.unwrap_or_else(|| renderer.default_size());
renderer.fill_text(
Text {
content: label,
size: text_size,
line_height: text_line_height,
font,
bounds: Size::new(
bounds.width - padding.horizontal(),
f32::from(text_line_height.to_absolute(text_size)),
),
horizontal_alignment: alignment::Horizontal::Left,
vertical_alignment: alignment::Vertical::Center,
shaping: text_shaping,
},
Point::new(bounds.x + padding.left, bounds.center_y()),
if is_selected {
style.text_color
} else {
style.placeholder_color
},
*viewport,
);
} }
} }

View file

@ -3,17 +3,17 @@ use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::widget::Tree; use crate::core::widget::Tree;
use crate::core::{Border, Element, Layout, Length, Rectangle, Size, Widget}; use crate::core::{
Background, Border, Element, Layout, Length, Rectangle, Size, Theme, Widget,
};
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
pub use iced_style::progress_bar::{Appearance, StyleSheet};
/// A bar that displays progress. /// A bar that displays progress.
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// # type ProgressBar = iced_widget::ProgressBar<iced_widget::style::Theme>; /// # type ProgressBar = iced_widget::ProgressBar;
/// # /// #
/// let value = 50.0; /// let value = 50.0;
/// ///
@ -22,21 +22,15 @@ pub use iced_style::progress_bar::{Appearance, StyleSheet};
/// ///
/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png) /// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png)
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct ProgressBar<Theme = crate::Theme> pub struct ProgressBar<Theme = crate::Theme> {
where
Theme: StyleSheet,
{
range: RangeInclusive<f32>, range: RangeInclusive<f32>,
value: f32, value: f32,
width: Length, width: Length,
height: Option<Length>, height: Option<Length>,
style: Theme::Style, style: Style<Theme>,
} }
impl<Theme> ProgressBar<Theme> impl<Theme> ProgressBar<Theme> {
where
Theme: StyleSheet,
{
/// The default height of a [`ProgressBar`]. /// The default height of a [`ProgressBar`].
pub const DEFAULT_HEIGHT: f32 = 30.0; pub const DEFAULT_HEIGHT: f32 = 30.0;
@ -45,13 +39,16 @@ where
/// It expects: /// It expects:
/// * an inclusive range of possible values /// * an inclusive range of possible values
/// * the current value of the [`ProgressBar`] /// * the current value of the [`ProgressBar`]
pub fn new(range: RangeInclusive<f32>, value: f32) -> Self { pub fn new(range: RangeInclusive<f32>, value: f32) -> Self
where
Theme: DefaultStyle,
{
ProgressBar { ProgressBar {
value: value.clamp(*range.start(), *range.end()), value: value.clamp(*range.start(), *range.end()),
range, range,
width: Length::Fill, width: Length::Fill,
height: None, height: None,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -68,7 +65,7 @@ where
} }
/// Sets the style of the [`ProgressBar`]. /// Sets the style of the [`ProgressBar`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -78,7 +75,6 @@ impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for ProgressBar<Theme> for ProgressBar<Theme>
where where
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
Theme: StyleSheet,
{ {
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
Size { Size {
@ -120,15 +116,15 @@ where
/ (range_end - range_start) / (range_end - range_start)
}; };
let style = theme.appearance(&self.style); let appearance = (self.style)(theme);
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds: Rectangle { ..bounds }, bounds: Rectangle { ..bounds },
border: Border::with_radius(style.border_radius), border: appearance.border,
..renderer::Quad::default() ..renderer::Quad::default()
}, },
style.background, appearance.background,
); );
if active_progress_width > 0.0 { if active_progress_width > 0.0 {
@ -138,10 +134,10 @@ where
width: active_progress_width, width: active_progress_width,
..bounds ..bounds
}, },
border: Border::with_radius(style.border_radius), border: Border::rounded(appearance.border.radius),
..renderer::Quad::default() ..renderer::Quad::default()
}, },
style.bar, appearance.bar,
); );
} }
} }
@ -151,7 +147,7 @@ impl<'a, Message, Theme, Renderer> From<ProgressBar<Theme>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: 'a + crate::core::Renderer, Renderer: 'a + crate::core::Renderer,
{ {
fn from( fn from(
@ -160,3 +156,80 @@ where
Element::new(progress_bar) Element::new(progress_bar)
} }
} }
/// The appearance of a progress bar.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the progress bar.
pub background: Background,
/// The [`Background`] of the bar of the progress bar.
pub bar: Background,
/// The [`Border`] of the progress bar.
pub border: Border,
}
/// The style of a [`ProgressBar`].
pub type Style<Theme> = fn(&Theme) -> Appearance;
/// The default style of a [`ProgressBar`].
pub trait DefaultStyle {
/// Returns the default style of a [`ProgressBar`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
primary
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance| *appearance
}
}
/// The primary style of a [`ProgressBar`].
pub fn primary(theme: &Theme) -> Appearance {
let palette = theme.extended_palette();
styled(
palette.background.strong.color,
palette.primary.strong.color,
)
}
/// The secondary style of a [`ProgressBar`].
pub fn secondary(theme: &Theme) -> Appearance {
let palette = theme.extended_palette();
styled(
palette.background.strong.color,
palette.secondary.base.color,
)
}
/// The success style of a [`ProgressBar`].
pub fn success(theme: &Theme) -> Appearance {
let palette = theme.extended_palette();
styled(palette.background.strong.color, palette.success.base.color)
}
/// The danger style of a [`ProgressBar`].
pub fn danger(theme: &Theme) -> Appearance {
let palette = theme.extended_palette();
styled(palette.background.strong.color, palette.danger.base.color)
}
fn styled(
background: impl Into<Background>,
bar: impl Into<Background>,
) -> Appearance {
Appearance {
background: background.into(),
bar: bar.into(),
border: Border::rounded(2),
}
}

View file

@ -5,7 +5,8 @@ use crate::core::mouse;
use crate::core::renderer::{self, Renderer as _}; use crate::core::renderer::{self, Renderer as _};
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Element, Layout, Length, Point, Rectangle, Size, Vector, Widget, Color, Element, Layout, Length, Point, Rectangle, Size, Theme, Vector,
Widget,
}; };
use crate::graphics::geometry::Renderer as _; use crate::graphics::geometry::Renderer as _;
use crate::Renderer; use crate::Renderer;
@ -13,33 +14,28 @@ use crate::Renderer;
use std::cell::RefCell; use std::cell::RefCell;
use thiserror::Error; use thiserror::Error;
pub use crate::style::qr_code::{Appearance, StyleSheet};
const DEFAULT_CELL_SIZE: u16 = 4; const DEFAULT_CELL_SIZE: u16 = 4;
const QUIET_ZONE: usize = 2; const QUIET_ZONE: usize = 2;
/// A type of matrix barcode consisting of squares arranged in a grid which /// A type of matrix barcode consisting of squares arranged in a grid which
/// can be read by an imaging device, such as a camera. /// can be read by an imaging device, such as a camera.
#[derive(Debug)] #[derive(Debug)]
pub struct QRCode<'a, Theme = crate::Theme> pub struct QRCode<'a, Theme = crate::Theme> {
where
Theme: StyleSheet,
{
data: &'a Data, data: &'a Data,
cell_size: u16, cell_size: u16,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, Theme> QRCode<'a, Theme> impl<'a, Theme> QRCode<'a, Theme> {
where
Theme: StyleSheet,
{
/// Creates a new [`QRCode`] with the provided [`Data`]. /// Creates a new [`QRCode`] with the provided [`Data`].
pub fn new(data: &'a Data) -> Self { pub fn new(data: &'a Data) -> Self
where
Theme: DefaultStyle,
{
Self { Self {
data, data,
cell_size: DEFAULT_CELL_SIZE, cell_size: DEFAULT_CELL_SIZE,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -50,15 +46,14 @@ where
} }
/// Sets the style of the [`QRCode`]. /// Sets the style of the [`QRCode`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
} }
impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme> impl<'a, Message, Theme> Widget<Message, Theme, Renderer>
where for QRCode<'a, Theme>
Theme: StyleSheet,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>() tree::Tag::of::<State>()
@ -102,7 +97,7 @@ where
let bounds = layout.bounds(); let bounds = layout.bounds();
let side_length = self.data.width + 2 * QUIET_ZONE; let side_length = self.data.width + 2 * QUIET_ZONE;
let appearance = theme.appearance(&self.style); let appearance = (self.style)(theme);
let mut last_appearance = state.last_appearance.borrow_mut(); let mut last_appearance = state.last_appearance.borrow_mut();
if Some(appearance) != *last_appearance { if Some(appearance) != *last_appearance {
@ -156,7 +151,7 @@ where
impl<'a, Message, Theme> From<QRCode<'a, Theme>> impl<'a, Message, Theme> From<QRCode<'a, Theme>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet + 'a, Theme: 'a,
{ {
fn from(qr_code: QRCode<'a, Theme>) -> Self { fn from(qr_code: QRCode<'a, Theme>) -> Self {
Self::new(qr_code) Self::new(qr_code)
@ -330,3 +325,43 @@ impl From<qrcode::types::QrError> for Error {
struct State { struct State {
last_appearance: RefCell<Option<Appearance>>, last_appearance: RefCell<Option<Appearance>>,
} }
/// The appearance of a QR code.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Appearance {
/// The color of the QR code data cells
pub cell: Color,
/// The color of the QR code background
pub background: Color,
}
/// The style of a [`QRCode`].
pub type Style<Theme> = fn(&Theme) -> Appearance;
/// The default style of a [`QRCode`].
pub trait DefaultStyle {
/// Returns the default style of a [`QRCode`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
default
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance| *appearance
}
}
/// The default style of a [`QRCode`].
pub fn default(theme: &Theme) -> Appearance {
let palette = theme.palette();
Appearance {
cell: palette.text,
background: palette.background,
}
}

View file

@ -9,18 +9,16 @@ use crate::core::touch;
use crate::core::widget; use crate::core::widget;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Background, Border, Clipboard, Color, Element, Layout, Length, Pixels,
Widget, Rectangle, Shell, Size, Theme, Widget,
}; };
pub use iced_style::radio::{Appearance, StyleSheet};
/// A circular button representing a choice. /// A circular button representing a choice.
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// # type Radio<Message> = /// # type Radio<Message> =
/// # iced_widget::Radio<Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; /// # iced_widget::Radio<Message, iced_widget::Theme, iced_widget::renderer::Renderer>;
/// # /// #
/// # use iced_widget::column; /// # use iced_widget::column;
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -71,7 +69,6 @@ pub use iced_style::radio::{Appearance, StyleSheet};
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Radio<Message, Theme = crate::Theme, Renderer = crate::Renderer> pub struct Radio<Message, Theme = crate::Theme, Renderer = crate::Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
is_selected: bool, is_selected: bool,
@ -84,20 +81,19 @@ where
text_line_height: text::LineHeight, text_line_height: text::LineHeight,
text_shaping: text::Shaping, text_shaping: text::Shaping,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
style: Theme::Style, style: Style<Theme>,
} }
impl<Message, Theme, Renderer> Radio<Message, Theme, Renderer> impl<Message, Theme, Renderer> Radio<Message, Theme, Renderer>
where where
Message: Clone, Message: Clone,
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// The default size of a [`Radio`] button. /// The default size of a [`Radio`] button.
pub const DEFAULT_SIZE: f32 = 28.0; pub const DEFAULT_SIZE: f32 = 16.0;
/// The default spacing of a [`Radio`] button. /// The default spacing of a [`Radio`] button.
pub const DEFAULT_SPACING: f32 = 15.0; pub const DEFAULT_SPACING: f32 = 8.0;
/// Creates a new [`Radio`] button. /// Creates a new [`Radio`] button.
/// ///
@ -114,6 +110,7 @@ where
f: F, f: F,
) -> Self ) -> Self
where where
Theme: DefaultStyle,
V: Eq + Copy, V: Eq + Copy,
F: FnOnce(V) -> Message, F: FnOnce(V) -> Message,
{ {
@ -128,7 +125,7 @@ where
text_line_height: text::LineHeight::default(), text_line_height: text::LineHeight::default(),
text_shaping: text::Shaping::Basic, text_shaping: text::Shaping::Basic,
font: None, font: None,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -178,8 +175,8 @@ where
} }
/// Sets the style of the [`Radio`] button. /// Sets the style of the [`Radio`] button.
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style;
self self
} }
} }
@ -188,7 +185,6 @@ impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Radio<Message, Theme, Renderer> for Radio<Message, Theme, Renderer>
where where
Message: Clone, Message: Clone,
Theme: StyleSheet + crate::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -291,15 +287,18 @@ where
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let is_mouse_over = cursor.is_over(layout.bounds()); let is_mouse_over = cursor.is_over(layout.bounds());
let is_selected = self.is_selected;
let mut children = layout.children(); let mut children = layout.children();
let custom_style = if is_mouse_over { let status = if is_mouse_over {
theme.hovered(&self.style, self.is_selected) Status::Hovered { is_selected }
} else { } else {
theme.active(&self.style, self.is_selected) Status::Active { is_selected }
}; };
let appearance = (self.style)(theme, status);
{ {
let layout = children.next().unwrap(); let layout = children.next().unwrap();
let bounds = layout.bounds(); let bounds = layout.bounds();
@ -312,12 +311,12 @@ where
bounds, bounds,
border: Border { border: Border {
radius: (size / 2.0).into(), radius: (size / 2.0).into(),
width: custom_style.border_width, width: appearance.border_width,
color: custom_style.border_color, color: appearance.border_color,
}, },
..renderer::Quad::default() ..renderer::Quad::default()
}, },
custom_style.background, appearance.background,
); );
if self.is_selected { if self.is_selected {
@ -329,10 +328,10 @@ where
width: bounds.width - dot_size, width: bounds.width - dot_size,
height: bounds.height - dot_size, height: bounds.height - dot_size,
}, },
border: Border::with_radius(dot_size / 2.0), border: Border::rounded(dot_size / 2.0),
..renderer::Quad::default() ..renderer::Quad::default()
}, },
custom_style.dot_color, appearance.dot_color,
); );
} }
} }
@ -346,7 +345,7 @@ where
label_layout, label_layout,
tree.state.downcast_ref(), tree.state.downcast_ref(),
crate::text::Appearance { crate::text::Appearance {
color: custom_style.text_color, color: appearance.text_color,
}, },
viewport, viewport,
); );
@ -358,7 +357,7 @@ impl<'a, Message, Theme, Renderer> From<Radio<Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a + Clone, Message: 'a + Clone,
Theme: StyleSheet + crate::text::StyleSheet + 'a, Theme: 'a,
Renderer: 'a + text::Renderer, Renderer: 'a + text::Renderer,
{ {
fn from( fn from(
@ -367,3 +366,76 @@ where
Element::new(radio) Element::new(radio)
} }
} }
/// The possible status of a [`Radio`] button.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Radio`] button can be interacted with.
Active {
/// Indicates whether the [`Radio`] button is currently selected.
is_selected: bool,
},
/// The [`Radio`] button is being hovered.
Hovered {
/// Indicates whether the [`Radio`] button is currently selected.
is_selected: bool,
},
}
/// The appearance of a radio button.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the radio button.
pub background: Background,
/// The [`Color`] of the dot of the radio button.
pub dot_color: Color,
/// The border width of the radio button.
pub border_width: f32,
/// The border [`Color`] of the radio button.
pub border_color: Color,
/// The text [`Color`] of the radio button.
pub text_color: Option<Color>,
}
/// The style of a [`Radio`] button.
pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
/// The default style of a [`Radio`] button.
pub trait DefaultStyle {
/// Returns the default style of a [`Radio`] button.
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
default
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}
/// The default style of a [`Radio`] button.
pub fn default(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let active = Appearance {
background: Color::TRANSPARENT.into(),
dot_color: palette.primary.strong.color,
border_width: 1.0,
border_color: palette.primary.strong.color,
text_color: None,
};
match status {
Status::Active { .. } => active,
Status::Hovered { .. } => Appearance {
dot_color: palette.primary.strong.color,
background: palette.primary.weak.color.into(),
..active
},
}
}

View file

@ -1,53 +1,52 @@
//! Display a horizontal or vertical rule for dividing content. //! Display a horizontal or vertical rule for dividing content.
use crate::core::border::{self, Border};
use crate::core::layout; use crate::core::layout;
use crate::core::mouse; use crate::core::mouse;
use crate::core::renderer; use crate::core::renderer;
use crate::core::widget::Tree; use crate::core::widget::Tree;
use crate::core::{ use crate::core::{
Border, Element, Layout, Length, Pixels, Rectangle, Size, Widget, Color, Element, Layout, Length, Pixels, Rectangle, Size, Theme, Widget,
}; };
pub use crate::style::rule::{Appearance, FillMode, StyleSheet};
/// Display a horizontal or vertical rule for dividing content. /// Display a horizontal or vertical rule for dividing content.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Rule<Theme = crate::Theme> pub struct Rule<Theme = crate::Theme> {
where
Theme: StyleSheet,
{
width: Length, width: Length,
height: Length, height: Length,
is_horizontal: bool, is_horizontal: bool,
style: Theme::Style, style: Style<Theme>,
} }
impl<Theme> Rule<Theme> impl<Theme> Rule<Theme> {
where
Theme: StyleSheet,
{
/// Creates a horizontal [`Rule`] with the given height. /// Creates a horizontal [`Rule`] with the given height.
pub fn horizontal(height: impl Into<Pixels>) -> Self { pub fn horizontal(height: impl Into<Pixels>) -> Self
where
Theme: DefaultStyle,
{
Rule { Rule {
width: Length::Fill, width: Length::Fill,
height: Length::Fixed(height.into().0), height: Length::Fixed(height.into().0),
is_horizontal: true, is_horizontal: true,
style: Default::default(), style: Theme::default_style(),
} }
} }
/// Creates a vertical [`Rule`] with the given width. /// Creates a vertical [`Rule`] with the given width.
pub fn vertical(width: impl Into<Pixels>) -> Self { pub fn vertical(width: impl Into<Pixels>) -> Self
where
Theme: DefaultStyle,
{
Rule { Rule {
width: Length::Fixed(width.into().0), width: Length::Fixed(width.into().0),
height: Length::Fill, height: Length::Fill,
is_horizontal: false, is_horizontal: false,
style: Default::default(), style: Theme::default_style(),
} }
} }
/// Sets the style of the [`Rule`]. /// Sets the style of the [`Rule`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self {
self.style = style.into(); self.style = style;
self self
} }
} }
@ -55,7 +54,6 @@ where
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<Theme> impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<Theme>
where where
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
Theme: StyleSheet,
{ {
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
Size { Size {
@ -84,34 +82,35 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
) { ) {
let bounds = layout.bounds(); let bounds = layout.bounds();
let style = theme.appearance(&self.style); let appearance = (self.style)(theme);
let bounds = if self.is_horizontal { let bounds = if self.is_horizontal {
let line_y = (bounds.y + (bounds.height / 2.0) let line_y = (bounds.y + (bounds.height / 2.0)
- (style.width as f32 / 2.0)) - (appearance.width as f32 / 2.0))
.round(); .round();
let (offset, line_width) = style.fill_mode.fill(bounds.width); let (offset, line_width) = appearance.fill_mode.fill(bounds.width);
let line_x = bounds.x + offset; let line_x = bounds.x + offset;
Rectangle { Rectangle {
x: line_x, x: line_x,
y: line_y, y: line_y,
width: line_width, width: line_width,
height: style.width as f32, height: appearance.width as f32,
} }
} else { } else {
let line_x = (bounds.x + (bounds.width / 2.0) let line_x = (bounds.x + (bounds.width / 2.0)
- (style.width as f32 / 2.0)) - (appearance.width as f32 / 2.0))
.round(); .round();
let (offset, line_height) = style.fill_mode.fill(bounds.height); let (offset, line_height) =
appearance.fill_mode.fill(bounds.height);
let line_y = bounds.y + offset; let line_y = bounds.y + offset;
Rectangle { Rectangle {
x: line_x, x: line_x,
y: line_y, y: line_y,
width: style.width as f32, width: appearance.width as f32,
height: line_height, height: line_height,
} }
}; };
@ -119,10 +118,10 @@ where
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds, bounds,
border: Border::with_radius(style.radius), border: Border::rounded(appearance.radius),
..renderer::Quad::default() ..renderer::Quad::default()
}, },
style.color, appearance.color,
); );
} }
} }
@ -131,10 +130,120 @@ impl<'a, Message, Theme, Renderer> From<Rule<Theme>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: 'a + crate::core::Renderer, Renderer: 'a + crate::core::Renderer,
{ {
fn from(rule: Rule<Theme>) -> Element<'a, Message, Theme, Renderer> { fn from(rule: Rule<Theme>) -> Element<'a, Message, Theme, Renderer> {
Element::new(rule) Element::new(rule)
} }
} }
/// The appearance of a rule.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The color of the rule.
pub color: Color,
/// The width (thickness) of the rule line.
pub width: u16,
/// The radius of the line corners.
pub radius: border::Radius,
/// The [`FillMode`] of the rule.
pub fill_mode: FillMode,
}
/// The fill mode of a rule.
#[derive(Debug, Clone, Copy)]
pub enum FillMode {
/// Fill the whole length of the container.
Full,
/// Fill a percent of the length of the container. The rule
/// will be centered in that container.
///
/// The range is `[0.0, 100.0]`.
Percent(f32),
/// Uniform offset from each end, length units.
Padded(u16),
/// Different offset on each end of the rule, length units.
/// First = top or left.
AsymmetricPadding(u16, u16),
}
impl FillMode {
/// Return the starting offset and length of the rule.
///
/// * `space` - The space to fill.
///
/// # Returns
///
/// * (`starting_offset`, `length`)
pub fn fill(&self, space: f32) -> (f32, f32) {
match *self {
FillMode::Full => (0.0, space),
FillMode::Percent(percent) => {
if percent >= 100.0 {
(0.0, space)
} else {
let percent_width = (space * percent / 100.0).round();
(((space - percent_width) / 2.0).round(), percent_width)
}
}
FillMode::Padded(padding) => {
if padding == 0 {
(0.0, space)
} else {
let padding = padding as f32;
let mut line_width = space - (padding * 2.0);
if line_width < 0.0 {
line_width = 0.0;
}
(padding, line_width)
}
}
FillMode::AsymmetricPadding(first_pad, second_pad) => {
let first_pad = first_pad as f32;
let second_pad = second_pad as f32;
let mut line_width = space - first_pad - second_pad;
if line_width < 0.0 {
line_width = 0.0;
}
(first_pad, line_width)
}
}
}
}
/// The style of a [`Rule`].
pub type Style<Theme> = fn(&Theme) -> Appearance;
/// The default style of a [`Rule`].
pub trait DefaultStyle {
/// Returns the default style of a [`Rule`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
default
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance| *appearance
}
}
/// The default styling of a [`Rule`].
pub fn default(theme: &Theme) -> Appearance {
let palette = theme.extended_palette();
Appearance {
color: palette.background.strong.color,
width: 1,
radius: 0.0.into(),
fill_mode: FillMode::Full,
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,5 @@
//! Display an interactive selector of a single value from a range of values. //! Display an interactive selector of a single value from a range of values.
//! use crate::core::border;
//! A [`Slider`] has some local [`State`].
use crate::core::event::{self, Event}; use crate::core::event::{self, Event};
use crate::core::keyboard; use crate::core::keyboard;
use crate::core::keyboard::key::{self, Key}; use crate::core::keyboard::key::{self, Key};
@ -10,16 +9,12 @@ use crate::core::renderer;
use crate::core::touch; use crate::core::touch;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Border, Clipboard, Color, Element, Layout, Length, Pixels, Point,
Shell, Size, Widget, Rectangle, Shell, Size, Theme, Widget,
}; };
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
pub use iced_style::slider::{
Appearance, Handle, HandleShape, Rail, StyleSheet,
};
/// An horizontal bar and a handle that selects a single value from a range of /// An horizontal bar and a handle that selects a single value from a range of
/// values. /// values.
/// ///
@ -30,8 +25,7 @@ pub use iced_style::slider::{
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// # type Slider<'a, T, Message> = /// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>;
/// # iced_widget::Slider<'a, Message, T, iced_widget::style::Theme>;
/// # /// #
/// #[derive(Clone)] /// #[derive(Clone)]
/// pub enum Message { /// pub enum Message {
@ -45,10 +39,7 @@ pub use iced_style::slider::{
/// ///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true) /// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Slider<'a, T, Message, Theme = crate::Theme> pub struct Slider<'a, T, Message, Theme = crate::Theme> {
where
Theme: StyleSheet,
{
range: RangeInclusive<T>, range: RangeInclusive<T>,
step: T, step: T,
shift_step: Option<T>, shift_step: Option<T>,
@ -58,17 +49,16 @@ where
on_release: Option<Message>, on_release: Option<Message>,
width: Length, width: Length,
height: f32, height: f32,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme>
where where
T: Copy + From<u8> + PartialOrd, T: Copy + From<u8> + PartialOrd,
Message: Clone, Message: Clone,
Theme: StyleSheet,
{ {
/// The default height of a [`Slider`]. /// The default height of a [`Slider`].
pub const DEFAULT_HEIGHT: f32 = 22.0; pub const DEFAULT_HEIGHT: f32 = 16.0;
/// Creates a new [`Slider`]. /// Creates a new [`Slider`].
/// ///
@ -80,6 +70,7 @@ where
/// `Message`. /// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where where
Theme: DefaultStyle,
F: 'a + Fn(T) -> Message, F: 'a + Fn(T) -> Message,
{ {
let value = if value >= *range.start() { let value = if value >= *range.start() {
@ -104,7 +95,7 @@ where
on_release: None, on_release: None,
width: Length::Fill, width: Length::Fill,
height: Self::DEFAULT_HEIGHT, height: Self::DEFAULT_HEIGHT,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -140,7 +131,7 @@ where
} }
/// Sets the style of the [`Slider`]. /// Sets the style of the [`Slider`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -165,7 +156,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where where
T: Copy + Into<f64> + num_traits::FromPrimitive, T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone, Message: Clone,
Theme: StyleSheet,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -173,7 +163,7 @@ where
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::new()) tree::State::new(State::default())
} }
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
@ -203,20 +193,143 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &Rectangle, _viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
update( let state = tree.state.downcast_mut::<State>();
event,
layout, let is_dragging = state.is_dragging;
cursor, let current_value = self.value;
shell,
tree.state.downcast_mut::<State>(), let locate = |cursor_position: Point| -> Option<T> {
&mut self.value, let bounds = layout.bounds();
self.default, let new_value = if cursor_position.x <= bounds.x {
&self.range, Some(*self.range.start())
self.step, } else if cursor_position.x >= bounds.x + bounds.width {
self.shift_step, Some(*self.range.end())
self.on_change.as_ref(), } else {
&self.on_release, let step = if state.keyboard_modifiers.shift() {
) self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let start = (*self.range.start()).into();
let end = (*self.range.end()).into();
let percent = f64::from(cursor_position.x - bounds.x)
/ f64::from(bounds.width);
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
T::from_f64(value)
};
new_value
};
let increment = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps + 1.0);
if new_value > (*self.range.end()).into() {
return Some(*self.range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps - 1.0);
if new_value < (*self.range.start()).into() {
return Some(*self.range.start());
}
T::from_f64(new_value)
};
let change = |new_value: T| {
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((self.on_change)(new_value));
self.value = new_value;
}
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) =
cursor.position_over(layout.bounds())
{
if state.keyboard_modifiers.command() {
let _ = self.default.map(change);
state.is_dragging = false;
} else {
let _ = locate(cursor_position).map(change);
state.is_dragging = true;
}
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if is_dragging {
if let Some(on_release) = self.on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
let _ = cursor.position().and_then(locate).map(change);
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
if cursor.position_over(layout.bounds()).is_some() {
match key {
Key::Named(key::Named::ArrowUp) => {
let _ = increment(current_value).map(change);
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
}
_ => (),
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
}
_ => {}
}
event::Status::Ignored
} }
fn draw( fn draw(
@ -229,15 +342,92 @@ where
cursor: mouse::Cursor, cursor: mouse::Cursor,
_viewport: &Rectangle, _viewport: &Rectangle,
) { ) {
draw( let state = tree.state.downcast_ref::<State>();
renderer, let bounds = layout.bounds();
layout, let is_mouse_over = cursor.is_over(bounds);
cursor,
tree.state.downcast_ref::<State>(), let style = (self.style)(
self.value,
&self.range,
theme, theme,
&self.style, if state.is_dragging {
Status::Dragged
} else if is_mouse_over {
Status::Hovered
} else {
Status::Active
},
);
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
HandleShape::Circle { radius } => {
(radius * 2.0, radius * 2.0, radius.into())
}
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.height, border_radius),
};
let value = self.value.into() as f32;
let (range_start, range_end) = {
let (start, end) = self.range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let offset = if range_start >= range_end {
0.0
} else {
(bounds.width - handle_width) * (value - range_start)
/ (range_end - range_start)
};
let rail_y = bounds.y + bounds.height / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: rail_y - style.rail.width / 2.0,
width: offset + handle_width / 2.0,
height: style.rail.width,
},
border: Border::rounded(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset + handle_width / 2.0,
y: rail_y - style.rail.width / 2.0,
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
},
border: Border::rounded(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.1,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset,
y: rail_y - handle_height / 2.0,
width: handle_width,
height: handle_height,
},
border: Border {
radius: handle_border_radius,
width: style.handle.border_width,
color: style.handle.border_color,
},
..renderer::Quad::default()
},
style.handle.color,
); );
} }
@ -249,7 +439,17 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
_renderer: &Renderer, _renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if state.is_dragging {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::default()
}
} }
} }
@ -258,7 +458,7 @@ impl<'a, T, Message, Theme, Renderer> From<Slider<'a, T, Message, Theme>>
where where
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
Message: Clone + 'a, Message: Clone + 'a,
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: crate::core::Renderer + 'a, Renderer: crate::core::Renderer + 'a,
{ {
fn from( fn from(
@ -268,290 +468,126 @@ where
} }
} }
/// Processes an [`Event`] and updates the [`State`] of a [`Slider`]
/// accordingly.
pub fn update<Message, T>(
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
state: &mut State,
value: &mut T,
default: Option<T>,
range: &RangeInclusive<T>,
step: T,
shift_step: Option<T>,
on_change: &dyn Fn(T) -> Message,
on_release: &Option<Message>,
) -> event::Status
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
{
let is_dragging = state.is_dragging;
let current_value = *value;
let locate = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds();
let new_value = if cursor_position.x <= bounds.x {
Some(*range.start())
} else if cursor_position.x >= bounds.x + bounds.width {
Some(*range.end())
} else {
let step = if state.keyboard_modifiers.shift() {
shift_step.unwrap_or(step)
} else {
step
}
.into();
let start = (*range.start()).into();
let end = (*range.end()).into();
let percent = f64::from(cursor_position.x - bounds.x)
/ f64::from(bounds.width);
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
T::from_f64(value)
};
new_value
};
let increment = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
shift_step.unwrap_or(step)
} else {
step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps + 1.0);
if new_value > (*range.end()).into() {
return Some(*range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
shift_step.unwrap_or(step)
} else {
step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps - 1.0);
if new_value < (*range.start()).into() {
return Some(*range.start());
}
T::from_f64(new_value)
};
let change = |new_value: T| {
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((on_change)(new_value));
*value = new_value;
}
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) = cursor.position_over(layout.bounds())
{
if state.keyboard_modifiers.command() {
let _ = default.map(change);
state.is_dragging = false;
} else {
let _ = locate(cursor_position).map(change);
state.is_dragging = true;
}
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if is_dragging {
if let Some(on_release) = on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
let _ = cursor.position().and_then(locate).map(change);
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
if cursor.position_over(layout.bounds()).is_some() {
match key {
Key::Named(key::Named::ArrowUp) => {
let _ = increment(current_value).map(change);
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
}
_ => (),
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
}
_ => {}
}
event::Status::Ignored
}
/// Draws a [`Slider`].
pub fn draw<T, Theme, Renderer>(
renderer: &mut Renderer,
layout: Layout<'_>,
cursor: mouse::Cursor,
state: &State,
value: T,
range: &RangeInclusive<T>,
theme: &Theme,
style: &Theme::Style,
) where
T: Into<f64> + Copy,
Theme: StyleSheet,
Renderer: crate::core::Renderer,
{
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
let style = if state.is_dragging {
theme.dragging(style)
} else if is_mouse_over {
theme.hovered(style)
} else {
theme.active(style)
};
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
HandleShape::Circle { radius } => {
(radius * 2.0, radius * 2.0, radius.into())
}
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.height, border_radius),
};
let value = value.into() as f32;
let (range_start, range_end) = {
let (start, end) = range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let offset = if range_start >= range_end {
0.0
} else {
(bounds.width - handle_width) * (value - range_start)
/ (range_end - range_start)
};
let rail_y = bounds.y + bounds.height / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x,
y: rail_y - style.rail.width / 2.0,
width: offset + handle_width / 2.0,
height: style.rail.width,
},
border: Border::with_radius(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset + handle_width / 2.0,
y: rail_y - style.rail.width / 2.0,
width: bounds.width - offset - handle_width / 2.0,
height: style.rail.width,
},
border: Border::with_radius(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.1,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: bounds.x + offset,
y: rail_y - handle_height / 2.0,
width: handle_width,
height: handle_height,
},
border: Border {
radius: handle_border_radius,
width: style.handle.border_width,
color: style.handle.border_color,
},
..renderer::Quad::default()
},
style.handle.color,
);
}
/// Computes the current [`mouse::Interaction`] of a [`Slider`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor: mouse::Cursor,
state: &State,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if state.is_dragging {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::default()
}
}
/// The local state of a [`Slider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State { struct State {
is_dragging: bool, is_dragging: bool,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
} }
impl State { /// The possible status of a [`Slider`].
/// Creates a new [`State`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub fn new() -> State { pub enum Status {
State::default() /// The [`Slider`] can be interacted with.
Active,
/// The [`Slider`] is being hovered.
Hovered,
/// The [`Slider`] is being dragged.
Dragged,
}
/// The appearance of a slider.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The colors of the rail of the slider.
pub rail: Rail,
/// The appearance of the [`Handle`] of the slider.
pub handle: Handle,
}
impl Appearance {
/// Changes the [`HandleShape`] of the [`Appearance`] to a circle
/// with the given radius.
pub fn with_circular_handle(mut self, radius: impl Into<Pixels>) -> Self {
self.handle.shape = HandleShape::Circle {
radius: radius.into().0,
};
self
}
}
/// The appearance of a slider rail
#[derive(Debug, Clone, Copy)]
pub struct Rail {
/// The colors of the rail of the slider.
pub colors: (Color, Color),
/// The width of the stroke of a slider rail.
pub width: f32,
/// The border radius of the corners of the rail.
pub border_radius: border::Radius,
}
/// The appearance of the handle of a slider.
#[derive(Debug, Clone, Copy)]
pub struct Handle {
/// The shape of the handle.
pub shape: HandleShape,
/// The [`Color`] of the handle.
pub color: Color,
/// The border width of the handle.
pub border_width: f32,
/// The border [`Color`] of the handle.
pub border_color: Color,
}
/// The shape of the handle of a slider.
#[derive(Debug, Clone, Copy)]
pub enum HandleShape {
/// A circular handle.
Circle {
/// The radius of the circle.
radius: f32,
},
/// A rectangular shape.
Rectangle {
/// The width of the rectangle.
width: u16,
/// The border radius of the corners of the rectangle.
border_radius: border::Radius,
},
}
/// The style of a [`Slider`].
pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
/// The default style of a [`Slider`].
pub trait DefaultStyle {
/// Returns the default style of a [`Slider`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
default
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}
/// The default style of a [`Slider`].
pub fn default(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let color = match status {
Status::Active => palette.primary.strong.color,
Status::Hovered => palette.primary.base.color,
Status::Dragged => palette.primary.strong.color,
};
Appearance {
rail: Rail {
colors: (color, palette.secondary.base.color),
width: 4.0,
border_radius: 2.0.into(),
},
handle: Handle {
shape: HandleShape::Circle { radius: 7.0 },
color,
border_color: Color::TRANSPARENT,
border_width: 0.0,
},
} }
} }

View file

@ -5,13 +5,13 @@ use crate::core::renderer;
use crate::core::svg; use crate::core::svg;
use crate::core::widget::Tree; use crate::core::widget::Tree;
use crate::core::{ use crate::core::{
ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector,
Widget,
}; };
use std::path::PathBuf; use std::path::PathBuf;
pub use crate::style::svg::{Appearance, StyleSheet}; pub use crate::core::svg::Handle;
pub use svg::Handle;
/// A vector graphics image. /// A vector graphics image.
/// ///
@ -20,36 +20,36 @@ pub use svg::Handle;
/// [`Svg`] images can have a considerable rendering cost when resized, /// [`Svg`] images can have a considerable rendering cost when resized,
/// specially when they are complex. /// specially when they are complex.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Svg<Theme = crate::Theme> pub struct Svg<Theme = crate::Theme> {
where
Theme: StyleSheet,
{
handle: Handle, handle: Handle,
width: Length, width: Length,
height: Length, height: Length,
content_fit: ContentFit, content_fit: ContentFit,
style: <Theme as StyleSheet>::Style, style: Style<Theme>,
} }
impl<Theme> Svg<Theme> impl<Theme> Svg<Theme> {
where
Theme: StyleSheet,
{
/// Creates a new [`Svg`] from the given [`Handle`]. /// Creates a new [`Svg`] from the given [`Handle`].
pub fn new(handle: impl Into<Handle>) -> Self { pub fn new(handle: impl Into<Handle>) -> Self
where
Theme: DefaultStyle,
{
Svg { Svg {
handle: handle.into(), handle: handle.into(),
width: Length::Fill, width: Length::Fill,
height: Length::Shrink, height: Length::Shrink,
content_fit: ContentFit::Contain, content_fit: ContentFit::Contain,
style: Default::default(), style: Theme::default_style(),
} }
} }
/// Creates a new [`Svg`] that will display the contents of the file at the /// Creates a new [`Svg`] that will display the contents of the file at the
/// provided path. /// provided path.
#[must_use] #[must_use]
pub fn from_path(path: impl Into<PathBuf>) -> Self { pub fn from_path(path: impl Into<PathBuf>) -> Self
where
Theme: DefaultStyle,
{
Self::new(Handle::from_path(path)) Self::new(Handle::from_path(path))
} }
@ -80,15 +80,14 @@ where
/// Sets the style variant of this [`Svg`]. /// Sets the style variant of this [`Svg`].
#[must_use] #[must_use]
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style;
self self
} }
} }
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Svg<Theme> impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Svg<Theme>
where where
Theme: iced_style::svg::StyleSheet,
Renderer: svg::Renderer, Renderer: svg::Renderer,
{ {
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
@ -158,12 +157,14 @@ where
..bounds ..bounds
}; };
let appearance = if is_mouse_over { let status = if is_mouse_over {
theme.hovered(&self.style) Status::Hovered
} else { } else {
theme.appearance(&self.style) Status::Idle
}; };
let appearance = (self.style)(theme, status);
renderer.draw( renderer.draw(
self.handle.clone(), self.handle.clone(),
appearance.color, appearance.color,
@ -184,10 +185,51 @@ where
impl<'a, Message, Theme, Renderer> From<Svg<Theme>> impl<'a, Message, Theme, Renderer> From<Svg<Theme>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Theme: iced_style::svg::StyleSheet + 'a, Theme: 'a,
Renderer: svg::Renderer + 'a, Renderer: svg::Renderer + 'a,
{ {
fn from(icon: Svg<Theme>) -> Element<'a, Message, Theme, Renderer> { fn from(icon: Svg<Theme>) -> Element<'a, Message, Theme, Renderer> {
Element::new(icon) Element::new(icon)
} }
} }
/// The possible status of an [`Svg`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Svg`] is idle.
Idle,
/// The [`Svg`] is being hovered.
Hovered,
}
/// The appearance of an [`Svg`].
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Appearance {
/// The [`Color`] filter of an [`Svg`].
///
/// Useful for coloring a symbolic icon.
///
/// `None` keeps the original color.
pub color: Option<Color>,
}
/// The style of an [`Svg`].
pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
/// The default style of an [`Svg`].
pub trait DefaultStyle {
/// Returns the default style of an [`Svg`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
|_theme, _status| Appearance::default()
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}

View file

@ -11,7 +11,8 @@ use crate::core::text::highlighter::{self, Highlighter};
use crate::core::text::{self, LineHeight}; use crate::core::text::{self, LineHeight};
use crate::core::widget::{self, Widget}; use crate::core::widget::{self, Widget};
use crate::core::{ use crate::core::{
Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Background, Border, Color, Element, Length, Padding, Pixels, Rectangle,
Shell, Size, Theme, Vector,
}; };
use std::cell::RefCell; use std::cell::RefCell;
@ -19,7 +20,6 @@ use std::fmt;
use std::ops::DerefMut; use std::ops::DerefMut;
use std::sync::Arc; use std::sync::Arc;
pub use crate::style::text_editor::{Appearance, StyleSheet};
pub use text::editor::{Action, Edit, Motion}; pub use text::editor::{Action, Edit, Motion};
/// A multi-line text input. /// A multi-line text input.
@ -32,7 +32,6 @@ pub struct TextEditor<
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Highlighter: text::Highlighter, Highlighter: text::Highlighter,
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
content: &'a Content<Renderer>, content: &'a Content<Renderer>,
@ -42,7 +41,7 @@ pub struct TextEditor<
width: Length, width: Length,
height: Length, height: Length,
padding: Padding, padding: Padding,
style: Theme::Style, style: Style<Theme>,
on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>,
highlighter_settings: Highlighter::Settings, highlighter_settings: Highlighter::Settings,
highlighter_format: fn( highlighter_format: fn(
@ -54,11 +53,13 @@ pub struct TextEditor<
impl<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer>
TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer> TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// Creates new [`TextEditor`] with the given [`Content`]. /// Creates new [`TextEditor`] with the given [`Content`].
pub fn new(content: &'a Content<Renderer>) -> Self { pub fn new(content: &'a Content<Renderer>) -> Self
where
Theme: DefaultStyle,
{
Self { Self {
content, content,
font: None, font: None,
@ -67,7 +68,7 @@ where
width: Length::Fill, width: Length::Fill,
height: Length::Shrink, height: Length::Shrink,
padding: Padding::new(5.0), padding: Padding::new(5.0),
style: Default::default(), style: Theme::default_style(),
on_edit: None, on_edit: None,
highlighter_settings: (), highlighter_settings: (),
highlighter_format: |_highlight, _theme| { highlighter_format: |_highlight, _theme| {
@ -81,7 +82,6 @@ impl<'a, Highlighter, Message, Theme, Renderer>
TextEditor<'a, Highlighter, Message, Theme, Renderer> TextEditor<'a, Highlighter, Message, Theme, Renderer>
where where
Highlighter: text::Highlighter, Highlighter: text::Highlighter,
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// Sets the height of the [`TextEditor`]. /// Sets the height of the [`TextEditor`].
@ -142,7 +142,7 @@ where
} }
/// Sets the style of the [`TextEditor`]. /// Sets the style of the [`TextEditor`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -306,7 +306,6 @@ impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for TextEditor<'a, Highlighter, Message, Theme, Renderer> for TextEditor<'a, Highlighter, Message, Theme, Renderer>
where where
Highlighter: text::Highlighter, Highlighter: text::Highlighter,
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn tag(&self) -> widget::tree::Tag { fn tag(&self) -> widget::tree::Tag {
@ -496,16 +495,18 @@ where
let is_disabled = self.on_edit.is_none(); let is_disabled = self.on_edit.is_none();
let is_mouse_over = cursor.is_over(bounds); let is_mouse_over = cursor.is_over(bounds);
let appearance = if is_disabled { let status = if is_disabled {
theme.disabled(&self.style) Status::Disabled
} else if state.is_focused { } else if state.is_focused {
theme.focused(&self.style) Status::Focused
} else if is_mouse_over { } else if is_mouse_over {
theme.hovered(&self.style) Status::Hovered
} else { } else {
theme.active(&self.style) Status::Active
}; };
let appearance = (self.style)(theme, status);
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds, bounds,
@ -551,7 +552,7 @@ where
}, },
..renderer::Quad::default() ..renderer::Quad::default()
}, },
theme.value_color(&self.style), appearance.value,
); );
} }
} }
@ -564,7 +565,7 @@ where
bounds: range, bounds: range,
..renderer::Quad::default() ..renderer::Quad::default()
}, },
theme.selection_color(&self.style), appearance.selection,
); );
} }
} }
@ -600,7 +601,7 @@ impl<'a, Highlighter, Message, Theme, Renderer>
where where
Highlighter: text::Highlighter, Highlighter: text::Highlighter,
Message: 'a, Message: 'a,
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn from( fn from(
@ -776,3 +777,95 @@ mod platform {
} }
} }
} }
/// The possible status of a [`TextEditor`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`TextEditor`] can be interacted with.
Active,
/// The [`TextEditor`] is being hovered.
Hovered,
/// The [`TextEditor`] is focused.
Focused,
/// The [`TextEditor`] cannot be interacted with.
Disabled,
}
/// The appearance of a text input.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The [`Background`] of the text input.
pub background: Background,
/// The [`Border`] of the text input.
pub border: Border,
/// The [`Color`] of the icon of the text input.
pub icon: Color,
/// The [`Color`] of the placeholder of the text input.
pub placeholder: Color,
/// The [`Color`] of the value of the text input.
pub value: Color,
/// The [`Color`] of the selection of the text input.
pub selection: Color,
}
/// The style of a [`TextEditor`].
pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
/// The default style of a [`TextEditor`].
pub trait DefaultStyle {
/// Returns the default style of a [`TextEditor`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
default
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}
/// The default style of a [`TextEditor`].
pub fn default(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let active = Appearance {
background: Background::Color(palette.background.base.color),
border: Border {
radius: 2.0.into(),
width: 1.0,
color: palette.background.strong.color,
},
icon: palette.background.weak.text,
placeholder: palette.background.strong.color,
value: palette.background.base.text,
selection: palette.primary.weak.color,
};
match status {
Status::Active => active,
Status::Hovered => Appearance {
border: Border {
color: palette.background.base.text,
..active.border
},
..active
},
Status::Focused => Appearance {
border: Border {
color: palette.primary.strong.color,
..active.border
},
..active
},
Status::Disabled => Appearance {
background: Background::Color(palette.background.weak.color),
value: active.placeholder,
..active
},
}
}

File diff suppressed because it is too large Load diff

View file

@ -7,58 +7,68 @@ use crate::core::renderer;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::widget::Operation; use crate::core::widget::Operation;
use crate::core::{ use crate::core::{
Background, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle,
Size, Vector, Widget, Shell, Size, Vector, Widget,
}; };
use crate::style::application;
use std::marker::PhantomData;
/// A widget that applies any `Theme` to its contents. /// A widget that applies any `Theme` to its contents.
/// ///
/// This widget can be useful to leverage multiple `Theme` /// This widget can be useful to leverage multiple `Theme`
/// types in an application. /// types in an application.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct Themer<'a, Message, Theme, Renderer> pub struct Themer<'a, Message, Theme, NewTheme, F, Renderer = crate::Renderer>
where where
F: Fn(&Theme) -> NewTheme,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
Theme: application::StyleSheet,
{ {
content: Element<'a, Message, Theme, Renderer>, content: Element<'a, Message, NewTheme, Renderer>,
theme: Theme, to_theme: F,
style: Theme::Style, text_color: Option<fn(&NewTheme) -> Color>,
show_background: bool, background: Option<fn(&NewTheme) -> Background>,
old_theme: PhantomData<Theme>,
} }
impl<'a, Message, Theme, Renderer> Themer<'a, Message, Theme, Renderer> impl<'a, Message, Theme, NewTheme, F, Renderer>
Themer<'a, Message, Theme, NewTheme, F, Renderer>
where where
F: Fn(&Theme) -> NewTheme,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
Theme: application::StyleSheet,
{ {
/// Creates an empty [`Themer`] that applies the given `Theme` /// Creates an empty [`Themer`] that applies the given `Theme`
/// to the provided `content`. /// to the provided `content`.
pub fn new<T>(theme: Theme, content: T) -> Self pub fn new<T>(to_theme: F, content: T) -> Self
where where
T: Into<Element<'a, Message, Theme, Renderer>>, T: Into<Element<'a, Message, NewTheme, Renderer>>,
{ {
Self { Self {
content: content.into(), content: content.into(),
theme, to_theme,
style: Theme::Style::default(), text_color: None,
show_background: false, background: None,
old_theme: PhantomData,
} }
} }
/// Sets whether to draw the background color of the `Theme`. /// Sets the default text [`Color`] of the [`Themer`].
pub fn background(mut self, background: bool) -> Self { pub fn text_color(mut self, f: fn(&NewTheme) -> Color) -> Self {
self.show_background = background; self.text_color = Some(f);
self
}
/// Sets the [`Background`] of the [`Themer`].
pub fn background(mut self, f: fn(&NewTheme) -> Background) -> Self {
self.background = Some(f);
self self
} }
} }
impl<'a, AnyTheme, Message, Theme, Renderer> Widget<Message, AnyTheme, Renderer> impl<'a, Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer>
for Themer<'a, Message, Theme, Renderer> for Themer<'a, Message, Theme, NewTheme, F, Renderer>
where where
F: Fn(&Theme) -> NewTheme,
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
Theme: application::StyleSheet,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
self.content.as_widget().tag() self.content.as_widget().tag()
@ -134,38 +144,36 @@ where
&self, &self,
tree: &Tree, tree: &Tree,
renderer: &mut Renderer, renderer: &mut Renderer,
_theme: &AnyTheme, theme: &Theme,
_style: &renderer::Style, style: &renderer::Style,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
viewport: &Rectangle, viewport: &Rectangle,
) { ) {
let appearance = self.theme.appearance(&self.style); let theme = (self.to_theme)(theme);
if self.show_background { if let Some(background) = self.background {
container::draw_background( container::draw_background(
renderer, renderer,
&container::Appearance { &container::Appearance {
background: Some(Background::Color( background: Some(background(&theme)),
appearance.background_color,
)),
..container::Appearance::default() ..container::Appearance::default()
}, },
layout.bounds(), layout.bounds(),
); );
} }
self.content.as_widget().draw( let style = if let Some(text_color) = self.text_color {
tree, renderer::Style {
renderer, text_color: text_color(&theme),
&self.theme, }
&renderer::Style { } else {
text_color: appearance.text_color, *style
}, };
layout,
cursor, self.content
viewport, .as_widget()
); .draw(tree, renderer, &theme, &style, layout, cursor, viewport);
} }
fn overlay<'b>( fn overlay<'b>(
@ -174,15 +182,15 @@ where
layout: Layout<'_>, layout: Layout<'_>,
renderer: &Renderer, renderer: &Renderer,
translation: Vector, translation: Vector,
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> { ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
struct Overlay<'a, Message, Theme, Renderer> { struct Overlay<'a, Message, Theme, NewTheme, Renderer> {
theme: &'a Theme, to_theme: &'a dyn Fn(&Theme) -> NewTheme,
content: overlay::Element<'a, Message, Theme, Renderer>, content: overlay::Element<'a, Message, NewTheme, Renderer>,
} }
impl<'a, AnyTheme, Message, Theme, Renderer> impl<'a, Message, Theme, NewTheme, Renderer>
overlay::Overlay<Message, AnyTheme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'a, Message, Theme, Renderer> for Overlay<'a, Message, Theme, NewTheme, Renderer>
where where
Renderer: crate::core::Renderer, Renderer: crate::core::Renderer,
{ {
@ -197,13 +205,18 @@ where
fn draw( fn draw(
&self, &self,
renderer: &mut Renderer, renderer: &mut Renderer,
_theme: &AnyTheme, theme: &Theme,
style: &renderer::Style, style: &renderer::Style,
layout: Layout<'_>, layout: Layout<'_>,
cursor: mouse::Cursor, cursor: mouse::Cursor,
) { ) {
self.content self.content.draw(
.draw(renderer, self.theme, style, layout, cursor); renderer,
&(self.to_theme)(theme),
style,
layout,
cursor,
);
} }
fn on_event( fn on_event(
@ -252,12 +265,12 @@ where
&'b mut self, &'b mut self,
layout: Layout<'_>, layout: Layout<'_>,
renderer: &Renderer, renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> ) -> Option<overlay::Element<'b, Message, Theme, Renderer>>
{ {
self.content self.content
.overlay(layout, renderer) .overlay(layout, renderer)
.map(|content| Overlay { .map(|content| Overlay {
theme: self.theme, to_theme: &self.to_theme,
content, content,
}) })
.map(|overlay| overlay::Element::new(Box::new(overlay))) .map(|overlay| overlay::Element::new(Box::new(overlay)))
@ -268,24 +281,26 @@ where
.as_widget_mut() .as_widget_mut()
.overlay(tree, layout, renderer, translation) .overlay(tree, layout, renderer, translation)
.map(|content| Overlay { .map(|content| Overlay {
theme: &self.theme, to_theme: &self.to_theme,
content, content,
}) })
.map(|overlay| overlay::Element::new(Box::new(overlay))) .map(|overlay| overlay::Element::new(Box::new(overlay)))
} }
} }
impl<'a, AnyTheme, Message, Theme, Renderer> impl<'a, Message, Theme, NewTheme, F, Renderer>
From<Themer<'a, Message, Theme, Renderer>> From<Themer<'a, Message, Theme, NewTheme, F, Renderer>>
for Element<'a, Message, AnyTheme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: 'a + application::StyleSheet, Theme: 'a,
NewTheme: 'a,
F: Fn(&Theme) -> NewTheme + 'a,
Renderer: 'a + crate::core::Renderer, Renderer: 'a + crate::core::Renderer,
{ {
fn from( fn from(
themer: Themer<'a, Message, Theme, Renderer>, themer: Themer<'a, Message, Theme, NewTheme, F, Renderer>,
) -> Element<'a, Message, AnyTheme, Renderer> { ) -> Element<'a, Message, Theme, Renderer> {
Element::new(themer) Element::new(themer)
} }
} }

View file

@ -9,19 +9,16 @@ use crate::core::touch;
use crate::core::widget; use crate::core::widget;
use crate::core::widget::tree::{self, Tree}; use crate::core::widget::tree::{self, Tree};
use crate::core::{ use crate::core::{
Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, Border, Clipboard, Color, Element, Event, Layout, Length, Pixels,
Shell, Size, Widget, Rectangle, Shell, Size, Theme, Widget,
}; };
pub use crate::style::toggler::{Appearance, StyleSheet};
/// A toggler widget. /// A toggler widget.
/// ///
/// # Example /// # Example
/// ///
/// ```no_run /// ```no_run
/// # type Toggler<'a, Message> = /// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>;
/// # iced_widget::Toggler<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>;
/// # /// #
/// pub enum Message { /// pub enum Message {
/// TogglerToggled(bool), /// TogglerToggled(bool),
@ -38,7 +35,6 @@ pub struct Toggler<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
is_toggled: bool, is_toggled: bool,
@ -52,16 +48,15 @@ pub struct Toggler<
text_shaping: text::Shaping, text_shaping: text::Shaping,
spacing: f32, spacing: f32,
font: Option<Renderer::Font>, font: Option<Renderer::Font>,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// The default size of a [`Toggler`]. /// The default size of a [`Toggler`].
pub const DEFAULT_SIZE: f32 = 20.0; pub const DEFAULT_SIZE: f32 = 16.0;
/// Creates a new [`Toggler`]. /// Creates a new [`Toggler`].
/// ///
@ -77,6 +72,7 @@ where
f: F, f: F,
) -> Self ) -> Self
where where
Theme: DefaultStyle,
F: 'a + Fn(bool) -> Message, F: 'a + Fn(bool) -> Message,
{ {
Toggler { Toggler {
@ -91,7 +87,7 @@ where
text_shaping: text::Shaping::Basic, text_shaping: text::Shaping::Basic,
spacing: Self::DEFAULT_SIZE / 2.0, spacing: Self::DEFAULT_SIZE / 2.0,
font: None, font: None,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -149,7 +145,7 @@ where
} }
/// Sets the style of the [`Toggler`]. /// Sets the style of the [`Toggler`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -158,7 +154,6 @@ where
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Toggler<'a, Message, Theme, Renderer> for Toggler<'a, Message, Theme, Renderer>
where where
Theme: StyleSheet + crate::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -294,12 +289,18 @@ where
let bounds = toggler_layout.bounds(); let bounds = toggler_layout.bounds();
let is_mouse_over = cursor.is_over(layout.bounds()); let is_mouse_over = cursor.is_over(layout.bounds());
let style = if is_mouse_over { let status = if is_mouse_over {
theme.hovered(&self.style, self.is_toggled) Status::Hovered {
is_toggled: self.is_toggled,
}
} else { } else {
theme.active(&self.style, self.is_toggled) Status::Active {
is_toggled: self.is_toggled,
}
}; };
let appearance = (self.style)(theme, status);
let border_radius = bounds.height / BORDER_RADIUS_RATIO; let border_radius = bounds.height / BORDER_RADIUS_RATIO;
let space = SPACE_RATIO * bounds.height; let space = SPACE_RATIO * bounds.height;
@ -315,12 +316,12 @@ where
bounds: toggler_background_bounds, bounds: toggler_background_bounds,
border: Border { border: Border {
radius: border_radius.into(), radius: border_radius.into(),
width: style.background_border_width, width: appearance.background_border_width,
color: style.background_border_color, color: appearance.background_border_color,
}, },
..renderer::Quad::default() ..renderer::Quad::default()
}, },
style.background, appearance.background,
); );
let toggler_foreground_bounds = Rectangle { let toggler_foreground_bounds = Rectangle {
@ -340,12 +341,12 @@ where
bounds: toggler_foreground_bounds, bounds: toggler_foreground_bounds,
border: Border { border: Border {
radius: border_radius.into(), radius: border_radius.into(),
width: style.foreground_border_width, width: appearance.foreground_border_width,
color: style.foreground_border_color, color: appearance.foreground_border_color,
}, },
..renderer::Quad::default() ..renderer::Quad::default()
}, },
style.foreground, appearance.foreground,
); );
} }
} }
@ -354,7 +355,7 @@ impl<'a, Message, Theme, Renderer> From<Toggler<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: StyleSheet + crate::text::StyleSheet + 'a, Theme: 'a,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
fn from( fn from(
@ -363,3 +364,100 @@ where
Element::new(toggler) Element::new(toggler)
} }
} }
/// The possible status of a [`Toggler`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Status {
/// The [`Toggler`] can be interacted with.
Active {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
/// The [`Toggler`] is being hovered.
Hovered {
/// Indicates whether the [`Toggler`] is toggled.
is_toggled: bool,
},
}
/// The appearance of a toggler.
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
/// The background [`Color`] of the toggler.
pub background: Color,
/// The width of the background border of the toggler.
pub background_border_width: f32,
/// The [`Color`] of the background border of the toggler.
pub background_border_color: Color,
/// The foreground [`Color`] of the toggler.
pub foreground: Color,
/// The width of the foreground border of the toggler.
pub foreground_border_width: f32,
/// The [`Color`] of the foreground border of the toggler.
pub foreground_border_color: Color,
}
/// The style of a [`Toggler`].
pub type Style<Theme> = fn(&Theme, Status) -> Appearance;
/// The default style of a [`Toggler`].
pub trait DefaultStyle {
/// Returns the default style of a [`Toggler`].
fn default_style() -> Style<Self>;
}
impl DefaultStyle for Theme {
fn default_style() -> Style<Self> {
default
}
}
impl DefaultStyle for Appearance {
fn default_style() -> Style<Self> {
|appearance, _status| *appearance
}
}
/// The default style of a [`Toggler`].
pub fn default(theme: &Theme, status: Status) -> Appearance {
let palette = theme.extended_palette();
let background = match status {
Status::Active { is_toggled } | Status::Hovered { is_toggled } => {
if is_toggled {
palette.primary.strong.color
} else {
palette.background.strong.color
}
}
};
let foreground = match status {
Status::Active { is_toggled } => {
if is_toggled {
palette.primary.strong.text
} else {
palette.background.base.color
}
}
Status::Hovered { is_toggled } => {
if is_toggled {
Color {
a: 0.5,
..palette.primary.strong.text
}
} else {
palette.background.weak.color
}
}
};
Appearance {
background,
foreground,
foreground_border_width: 0.0,
foreground_border_color: Color::TRANSPARENT,
background_border_width: 0.0,
background_border_color: Color::TRANSPARENT,
}
}

View file

@ -20,7 +20,6 @@ pub struct Tooltip<
Theme = crate::Theme, Theme = crate::Theme,
Renderer = crate::Renderer, Renderer = crate::Renderer,
> where > where
Theme: container::StyleSheet + crate::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
content: Element<'a, Message, Theme, Renderer>, content: Element<'a, Message, Theme, Renderer>,
@ -29,12 +28,11 @@ pub struct Tooltip<
gap: f32, gap: f32,
padding: f32, padding: f32,
snap_within_viewport: bool, snap_within_viewport: bool,
style: <Theme as container::StyleSheet>::Style, style: container::Style<Theme>,
} }
impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet + crate::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
/// The default padding of a [`Tooltip`] drawn by this renderer. /// The default padding of a [`Tooltip`] drawn by this renderer.
@ -47,7 +45,10 @@ where
content: impl Into<Element<'a, Message, Theme, Renderer>>, content: impl Into<Element<'a, Message, Theme, Renderer>>,
tooltip: impl Into<Element<'a, Message, Theme, Renderer>>, tooltip: impl Into<Element<'a, Message, Theme, Renderer>>,
position: Position, position: Position,
) -> Self { ) -> Self
where
Theme: container::DefaultStyle,
{
Tooltip { Tooltip {
content: content.into(), content: content.into(),
tooltip: tooltip.into(), tooltip: tooltip.into(),
@ -55,7 +56,7 @@ where
gap: 0.0, gap: 0.0,
padding: Self::DEFAULT_PADDING, padding: Self::DEFAULT_PADDING,
snap_within_viewport: true, snap_within_viewport: true,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -80,7 +81,7 @@ where
/// Sets the style of the [`Tooltip`]. /// Sets the style of the [`Tooltip`].
pub fn style( pub fn style(
mut self, mut self,
style: impl Into<<Theme as container::StyleSheet>::Style>, style: fn(&Theme, container::Status) -> container::Appearance,
) -> Self { ) -> Self {
self.style = style.into(); self.style = style.into();
self self
@ -90,7 +91,6 @@ where
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for Tooltip<'a, Message, Theme, Renderer> for Tooltip<'a, Message, Theme, Renderer>
where where
Theme: container::StyleSheet + crate::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn children(&self) -> Vec<widget::Tree> { fn children(&self) -> Vec<widget::Tree> {
@ -239,7 +239,7 @@ where
positioning: self.position, positioning: self.position,
gap: self.gap, gap: self.gap,
padding: self.padding, padding: self.padding,
style: &self.style, style: self.style,
}))) })))
} else { } else {
None None
@ -262,7 +262,7 @@ impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer>
where where
Message: 'a, Message: 'a,
Theme: container::StyleSheet + crate::text::StyleSheet + 'a, Theme: 'a,
Renderer: text::Renderer + 'a, Renderer: text::Renderer + 'a,
{ {
fn from( fn from(
@ -298,7 +298,6 @@ enum State {
struct Overlay<'a, 'b, Message, Theme, Renderer> struct Overlay<'a, 'b, Message, Theme, Renderer>
where where
Theme: container::StyleSheet + widget::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
position: Point, position: Point,
@ -310,14 +309,13 @@ where
positioning: Position, positioning: Position,
gap: f32, gap: f32,
padding: f32, padding: f32,
style: &'b <Theme as container::StyleSheet>::Style, style: container::Style<Theme>,
} }
impl<'a, 'b, Message, Theme, Renderer> impl<'a, 'b, Message, Theme, Renderer>
overlay::Overlay<Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer>
for Overlay<'a, 'b, Message, Theme, Renderer> for Overlay<'a, 'b, Message, Theme, Renderer>
where where
Theme: container::StyleSheet + widget::text::StyleSheet,
Renderer: text::Renderer, Renderer: text::Renderer,
{ {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
@ -426,7 +424,7 @@ where
layout: Layout<'_>, layout: Layout<'_>,
cursor_position: mouse::Cursor, cursor_position: mouse::Cursor,
) { ) {
let style = container::StyleSheet::appearance(theme, self.style); let style = (self.style)(theme, container::Status::Idle);
container::draw_background(renderer, &style, layout.bounds()); container::draw_background(renderer, &style, layout.bounds());

View file

@ -1,9 +1,9 @@
//! Display an interactive selector of a single value from a range of values. //! Display an interactive selector of a single value from a range of values.
//!
//! A [`VerticalSlider`] has some local [`State`].
use std::ops::RangeInclusive; use std::ops::RangeInclusive;
pub use crate::style::slider::{Appearance, Handle, HandleShape, StyleSheet}; pub use crate::slider::{
default, Appearance, DefaultStyle, Handle, HandleShape, Status, Style,
};
use crate::core; use crate::core;
use crate::core::event::{self, Event}; use crate::core::event::{self, Event};
@ -29,8 +29,7 @@ use crate::core::{
/// ///
/// # Example /// # Example
/// ```no_run /// ```no_run
/// # type VerticalSlider<'a, T, Message> = /// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>;
/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::style::Theme>;
/// # /// #
/// #[derive(Clone)] /// #[derive(Clone)]
/// pub enum Message { /// pub enum Message {
@ -42,10 +41,7 @@ use crate::core::{
/// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); /// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged);
/// ``` /// ```
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> {
where
Theme: StyleSheet,
{
range: RangeInclusive<T>, range: RangeInclusive<T>,
step: T, step: T,
shift_step: Option<T>, shift_step: Option<T>,
@ -55,17 +51,16 @@ where
on_release: Option<Message>, on_release: Option<Message>,
width: f32, width: f32,
height: Length, height: Length,
style: Theme::Style, style: Style<Theme>,
} }
impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme>
where where
T: Copy + From<u8> + std::cmp::PartialOrd, T: Copy + From<u8> + std::cmp::PartialOrd,
Message: Clone, Message: Clone,
Theme: StyleSheet,
{ {
/// The default width of a [`VerticalSlider`]. /// The default width of a [`VerticalSlider`].
pub const DEFAULT_WIDTH: f32 = 22.0; pub const DEFAULT_WIDTH: f32 = 16.0;
/// Creates a new [`VerticalSlider`]. /// Creates a new [`VerticalSlider`].
/// ///
@ -77,6 +72,7 @@ where
/// `Message`. /// `Message`.
pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
where where
Theme: DefaultStyle,
F: 'a + Fn(T) -> Message, F: 'a + Fn(T) -> Message,
{ {
let value = if value >= *range.start() { let value = if value >= *range.start() {
@ -101,7 +97,7 @@ where
on_release: None, on_release: None,
width: Self::DEFAULT_WIDTH, width: Self::DEFAULT_WIDTH,
height: Length::Fill, height: Length::Fill,
style: Default::default(), style: Theme::default_style(),
} }
} }
@ -137,7 +133,7 @@ where
} }
/// Sets the style of the [`VerticalSlider`]. /// Sets the style of the [`VerticalSlider`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self {
self.style = style.into(); self.style = style.into();
self self
} }
@ -162,7 +158,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
where where
T: Copy + Into<f64> + num_traits::FromPrimitive, T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone, Message: Clone,
Theme: StyleSheet,
Renderer: core::Renderer, Renderer: core::Renderer,
{ {
fn tag(&self) -> tree::Tag { fn tag(&self) -> tree::Tag {
@ -170,7 +165,7 @@ where
} }
fn state(&self) -> tree::State { fn state(&self) -> tree::State {
tree::State::new(State::new()) tree::State::new(State::default())
} }
fn size(&self) -> Size<Length> { fn size(&self) -> Size<Length> {
@ -200,20 +195,146 @@ where
shell: &mut Shell<'_, Message>, shell: &mut Shell<'_, Message>,
_viewport: &Rectangle, _viewport: &Rectangle,
) -> event::Status { ) -> event::Status {
update( let state = tree.state.downcast_mut::<State>();
event, let is_dragging = state.is_dragging;
layout, let current_value = self.value;
cursor,
shell, let locate = |cursor_position: Point| -> Option<T> {
tree.state.downcast_mut::<State>(), let bounds = layout.bounds();
&mut self.value,
self.default, let new_value = if cursor_position.y >= bounds.y + bounds.height {
&self.range, Some(*self.range.start())
self.step, } else if cursor_position.y <= bounds.y {
self.shift_step, Some(*self.range.end())
self.on_change.as_ref(), } else {
&self.on_release, let step = if state.keyboard_modifiers.shift() {
) self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let start = (*self.range.start()).into();
let end = (*self.range.end()).into();
let percent = 1.0
- f64::from(cursor_position.y - bounds.y)
/ f64::from(bounds.height);
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
T::from_f64(value)
};
new_value
};
let increment = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps + 1.0);
if new_value > (*self.range.end()).into() {
return Some(*self.range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
self.shift_step.unwrap_or(self.step)
} else {
self.step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps - 1.0);
if new_value < (*self.range.start()).into() {
return Some(*self.range.start());
}
T::from_f64(new_value)
};
let change = |new_value: T| {
if (self.value.into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((self.on_change)(new_value));
self.value = new_value;
}
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) =
cursor.position_over(layout.bounds())
{
if state.keyboard_modifiers.control()
|| state.keyboard_modifiers.command()
{
let _ = self.default.map(change);
state.is_dragging = false;
} else {
let _ = locate(cursor_position).map(change);
state.is_dragging = true;
}
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if is_dragging {
if let Some(on_release) = self.on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
let _ = cursor.position().and_then(locate).map(change);
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
if cursor.position_over(layout.bounds()).is_some() {
match key {
Key::Named(key::Named::ArrowUp) => {
let _ = increment(current_value).map(change);
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
}
_ => (),
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
}
_ => {}
}
event::Status::Ignored
} }
fn draw( fn draw(
@ -226,15 +347,92 @@ where
cursor: mouse::Cursor, cursor: mouse::Cursor,
_viewport: &Rectangle, _viewport: &Rectangle,
) { ) {
draw( let state = tree.state.downcast_ref::<State>();
renderer, let bounds = layout.bounds();
layout, let is_mouse_over = cursor.is_over(bounds);
cursor,
tree.state.downcast_ref::<State>(), let style = (self.style)(
self.value,
&self.range,
theme, theme,
&self.style, if state.is_dragging {
Status::Dragged
} else if is_mouse_over {
Status::Hovered
} else {
Status::Active
},
);
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
HandleShape::Circle { radius } => {
(radius * 2.0, radius * 2.0, radius.into())
}
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.width, border_radius),
};
let value = self.value.into() as f32;
let (range_start, range_end) = {
let (start, end) = self.range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let offset = if range_start >= range_end {
0.0
} else {
(bounds.height - handle_width) * (value - range_end)
/ (range_start - range_end)
};
let rail_x = bounds.x + bounds.width / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - style.rail.width / 2.0,
y: bounds.y,
width: style.rail.width,
height: offset + handle_width / 2.0,
},
border: Border::rounded(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.1,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - style.rail.width / 2.0,
y: bounds.y + offset + handle_width / 2.0,
width: style.rail.width,
height: bounds.height - offset - handle_width / 2.0,
},
border: Border::rounded(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - handle_height / 2.0,
y: bounds.y + offset,
width: handle_height,
height: handle_width,
},
border: Border {
radius: handle_border_radius,
width: style.handle.border_width,
color: style.handle.border_color,
},
..renderer::Quad::default()
},
style.handle.color,
); );
} }
@ -246,7 +444,17 @@ where
_viewport: &Rectangle, _viewport: &Rectangle,
_renderer: &Renderer, _renderer: &Renderer,
) -> mouse::Interaction { ) -> mouse::Interaction {
mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if state.is_dragging {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::default()
}
} }
} }
@ -256,7 +464,7 @@ impl<'a, T, Message, Theme, Renderer>
where where
T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, T: Copy + Into<f64> + num_traits::FromPrimitive + 'a,
Message: Clone + 'a, Message: Clone + 'a,
Theme: StyleSheet + 'a, Theme: 'a,
Renderer: core::Renderer + 'a, Renderer: core::Renderer + 'a,
{ {
fn from( fn from(
@ -266,294 +474,8 @@ where
} }
} }
/// Processes an [`Event`] and updates the [`State`] of a [`VerticalSlider`]
/// accordingly.
pub fn update<Message, T>(
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
shell: &mut Shell<'_, Message>,
state: &mut State,
value: &mut T,
default: Option<T>,
range: &RangeInclusive<T>,
step: T,
shift_step: Option<T>,
on_change: &dyn Fn(T) -> Message,
on_release: &Option<Message>,
) -> event::Status
where
T: Copy + Into<f64> + num_traits::FromPrimitive,
Message: Clone,
{
let is_dragging = state.is_dragging;
let current_value = *value;
let locate = |cursor_position: Point| -> Option<T> {
let bounds = layout.bounds();
let new_value = if cursor_position.y >= bounds.y + bounds.height {
Some(*range.start())
} else if cursor_position.y <= bounds.y {
Some(*range.end())
} else {
let step = if state.keyboard_modifiers.shift() {
shift_step.unwrap_or(step)
} else {
step
}
.into();
let start = (*range.start()).into();
let end = (*range.end()).into();
let percent = 1.0
- f64::from(cursor_position.y - bounds.y)
/ f64::from(bounds.height);
let steps = (percent * (end - start) / step).round();
let value = steps * step + start;
T::from_f64(value)
};
new_value
};
let increment = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
shift_step.unwrap_or(step)
} else {
step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps + 1.0);
if new_value > (*range.end()).into() {
return Some(*range.end());
}
T::from_f64(new_value)
};
let decrement = |value: T| -> Option<T> {
let step = if state.keyboard_modifiers.shift() {
shift_step.unwrap_or(step)
} else {
step
}
.into();
let steps = (value.into() / step).round();
let new_value = step * (steps - 1.0);
if new_value < (*range.start()).into() {
return Some(*range.start());
}
T::from_f64(new_value)
};
let change = |new_value: T| {
if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
shell.publish((on_change)(new_value));
*value = new_value;
}
};
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
| Event::Touch(touch::Event::FingerPressed { .. }) => {
if let Some(cursor_position) = cursor.position_over(layout.bounds())
{
if state.keyboard_modifiers.control()
|| state.keyboard_modifiers.command()
{
let _ = default.map(change);
state.is_dragging = false;
} else {
let _ = locate(cursor_position).map(change);
state.is_dragging = true;
}
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
| Event::Touch(touch::Event::FingerLifted { .. })
| Event::Touch(touch::Event::FingerLost { .. }) => {
if is_dragging {
if let Some(on_release) = on_release.clone() {
shell.publish(on_release);
}
state.is_dragging = false;
return event::Status::Captured;
}
}
Event::Mouse(mouse::Event::CursorMoved { .. })
| Event::Touch(touch::Event::FingerMoved { .. }) => {
if is_dragging {
let _ = cursor.position().and_then(locate).map(change);
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
if cursor.position_over(layout.bounds()).is_some() {
match key {
Key::Named(key::Named::ArrowUp) => {
let _ = increment(current_value).map(change);
}
Key::Named(key::Named::ArrowDown) => {
let _ = decrement(current_value).map(change);
}
_ => (),
}
return event::Status::Captured;
}
}
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
state.keyboard_modifiers = modifiers;
}
_ => {}
}
event::Status::Ignored
}
/// Draws a [`VerticalSlider`].
pub fn draw<T, Theme, Renderer>(
renderer: &mut Renderer,
layout: Layout<'_>,
cursor: mouse::Cursor,
state: &State,
value: T,
range: &RangeInclusive<T>,
style_sheet: &Theme,
style: &Theme::Style,
) where
T: Into<f64> + Copy,
Theme: StyleSheet,
Renderer: core::Renderer,
{
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
let style = if state.is_dragging {
style_sheet.dragging(style)
} else if is_mouse_over {
style_sheet.hovered(style)
} else {
style_sheet.active(style)
};
let (handle_width, handle_height, handle_border_radius) =
match style.handle.shape {
HandleShape::Circle { radius } => {
(radius * 2.0, radius * 2.0, radius.into())
}
HandleShape::Rectangle {
width,
border_radius,
} => (f32::from(width), bounds.width, border_radius),
};
let value = value.into() as f32;
let (range_start, range_end) = {
let (start, end) = range.clone().into_inner();
(start.into() as f32, end.into() as f32)
};
let offset = if range_start >= range_end {
0.0
} else {
(bounds.height - handle_width) * (value - range_end)
/ (range_start - range_end)
};
let rail_x = bounds.x + bounds.width / 2.0;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - style.rail.width / 2.0,
y: bounds.y,
width: style.rail.width,
height: offset + handle_width / 2.0,
},
border: Border::with_radius(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.1,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - style.rail.width / 2.0,
y: bounds.y + offset + handle_width / 2.0,
width: style.rail.width,
height: bounds.height - offset - handle_width / 2.0,
},
border: Border::with_radius(style.rail.border_radius),
..renderer::Quad::default()
},
style.rail.colors.0,
);
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x: rail_x - handle_height / 2.0,
y: bounds.y + offset,
width: handle_height,
height: handle_width,
},
border: Border {
radius: handle_border_radius,
width: style.handle.border_width,
color: style.handle.border_color,
},
..renderer::Quad::default()
},
style.handle.color,
);
}
/// Computes the current [`mouse::Interaction`] of a [`VerticalSlider`].
pub fn mouse_interaction(
layout: Layout<'_>,
cursor: mouse::Cursor,
state: &State,
) -> mouse::Interaction {
let bounds = layout.bounds();
let is_mouse_over = cursor.is_over(bounds);
if state.is_dragging {
mouse::Interaction::Grabbing
} else if is_mouse_over {
mouse::Interaction::Grab
} else {
mouse::Interaction::default()
}
}
/// The local state of a [`VerticalSlider`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct State { struct State {
is_dragging: bool, is_dragging: bool,
keyboard_modifiers: keyboard::Modifiers, keyboard_modifiers: keyboard::Modifiers,
} }
impl State {
/// Creates a new [`State`].
pub fn new() -> State {
State::default()
}
}

View file

@ -24,7 +24,6 @@ multi-window = ["iced_runtime/multi-window"]
[dependencies] [dependencies]
iced_graphics.workspace = true iced_graphics.workspace = true
iced_runtime.workspace = true iced_runtime.workspace = true
iced_style.workspace = true
log.workspace = true log.workspace = true
thiserror.workspace = true thiserror.workspace = true

View file

@ -10,7 +10,7 @@ use crate::core::renderer;
use crate::core::time::Instant; use crate::core::time::Instant;
use crate::core::widget::operation; use crate::core::widget::operation;
use crate::core::window; use crate::core::window;
use crate::core::{Event, Point, Size}; use crate::core::{Color, Event, Point, Size, Theme};
use crate::futures::futures; use crate::futures::futures;
use crate::futures::{Executor, Runtime, Subscription}; use crate::futures::{Executor, Runtime, Subscription};
use crate::graphics::compositor::{self, Compositor}; use crate::graphics::compositor::{self, Compositor};
@ -18,7 +18,6 @@ use crate::runtime::clipboard;
use crate::runtime::program::Program; use crate::runtime::program::Program;
use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::{Command, Debug}; use crate::runtime::{Command, Debug};
use crate::style::application::{Appearance, StyleSheet};
use crate::{Clipboard, Error, Proxy, Settings}; use crate::{Clipboard, Error, Proxy, Settings};
use futures::channel::mpsc; use futures::channel::mpsc;
@ -39,7 +38,7 @@ use std::sync::Arc;
/// can be toggled by pressing `F12`. /// can be toggled by pressing `F12`.
pub trait Application: Program pub trait Application: Program
where where
Self::Theme: StyleSheet, Self::Theme: DefaultStyle,
{ {
/// The data needed to initialize your [`Application`]. /// The data needed to initialize your [`Application`].
type Flags; type Flags;
@ -64,8 +63,8 @@ where
fn theme(&self) -> Self::Theme; fn theme(&self) -> Self::Theme;
/// Returns the `Style` variation of the `Theme`. /// Returns the `Style` variation of the `Theme`.
fn style(&self) -> <Self::Theme as StyleSheet>::Style { fn style(&self, theme: &Self::Theme) -> Appearance {
Default::default() theme.default_style()
} }
/// Returns the event `Subscription` for the current state of the /// Returns the event `Subscription` for the current state of the
@ -95,6 +94,38 @@ where
} }
} }
/// The appearance of an application.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Appearance {
/// The background [`Color`] of the application.
pub background_color: Color,
/// The default text [`Color`] of the application.
pub text_color: Color,
}
/// The default style of an [`Application`].
pub trait DefaultStyle {
/// Returns the default style of an [`Application`].
fn default_style(&self) -> Appearance;
}
impl DefaultStyle for Theme {
fn default_style(&self) -> Appearance {
default(self)
}
}
/// The default [`Appearance`] of an [`Application`] with the built-in [`Theme`].
pub fn default(theme: &Theme) -> Appearance {
let palette = theme.extended_palette();
Appearance {
background_color: palette.background.base.color,
text_color: palette.background.base.text,
}
}
/// Runs an [`Application`] with an executor, compositor, and the provided /// Runs an [`Application`] with an executor, compositor, and the provided
/// settings. /// settings.
pub async fn run<A, E, C>( pub async fn run<A, E, C>(
@ -105,7 +136,7 @@ where
A: Application + 'static, A: Application + 'static,
E: Executor + 'static, E: Executor + 'static,
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
use futures::task; use futures::task;
use futures::Future; use futures::Future;
@ -289,7 +320,7 @@ async fn run_instance<A, E, C>(
A: Application + 'static, A: Application + 'static,
E: Executor + 'static, E: Executor + 'static,
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
use futures::stream::StreamExt; use futures::stream::StreamExt;
use winit::event; use winit::event;
@ -612,7 +643,7 @@ pub fn build_user_interface<'a, A: Application>(
debug: &mut Debug, debug: &mut Debug,
) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer>
where where
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
debug.view_started(); debug.view_started();
let view = application.view(); let view = application.view();
@ -643,7 +674,7 @@ pub fn update<A: Application, C, E: Executor>(
window: &winit::window::Window, window: &winit::window::Window,
) where ) where
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
for message in messages.drain(..) { for message in messages.drain(..) {
debug.log_message(&message); debug.log_message(&message);
@ -694,7 +725,7 @@ pub fn run_command<A, C, E>(
A: Application, A: Application,
E: Executor, E: Executor,
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
use crate::runtime::command; use crate::runtime::command;
use crate::runtime::system; use crate::runtime::system;

View file

@ -1,4 +1,4 @@
use crate::application::{self, StyleSheet as _}; use crate::application;
use crate::conversion; use crate::conversion;
use crate::core::mouse; use crate::core::mouse;
use crate::core::{Color, Size}; use crate::core::{Color, Size};
@ -14,7 +14,7 @@ use winit::window::Window;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct State<A: Application> pub struct State<A: Application>
where where
A::Theme: application::StyleSheet, A::Theme: application::DefaultStyle,
{ {
title: String, title: String,
scale_factor: f64, scale_factor: f64,
@ -29,14 +29,14 @@ where
impl<A: Application> State<A> impl<A: Application> State<A>
where where
A::Theme: application::StyleSheet, A::Theme: application::DefaultStyle,
{ {
/// Creates a new [`State`] for the provided [`Application`] and window. /// Creates a new [`State`] for the provided [`Application`] and window.
pub fn new(application: &A, window: &Window) -> Self { pub fn new(application: &A, window: &Window) -> Self {
let title = application.title(); let title = application.title();
let scale_factor = application.scale_factor(); let scale_factor = application.scale_factor();
let theme = application.theme(); let theme = application.theme();
let appearance = theme.appearance(&application.style()); let appearance = application.style(&theme);
let viewport = { let viewport = {
let physical_size = window.inner_size(); let physical_size = window.inner_size();
@ -216,6 +216,6 @@ where
// Update theme and appearance // Update theme and appearance
self.theme = application.theme(); self.theme = application.theme();
self.appearance = self.theme.appearance(&application.style()); self.appearance = application.style(&self.theme);
} }
} }

View file

@ -30,7 +30,6 @@ pub use iced_graphics as graphics;
pub use iced_runtime as runtime; pub use iced_runtime as runtime;
pub use iced_runtime::core; pub use iced_runtime::core;
pub use iced_runtime::futures; pub use iced_runtime::futures;
pub use iced_style as style;
pub use winit; pub use winit;
#[cfg(feature = "multi-window")] #[cfg(feature = "multi-window")]

View file

@ -22,9 +22,10 @@ use crate::runtime::command::{self, Command};
use crate::runtime::multi_window::Program; use crate::runtime::multi_window::Program;
use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::user_interface::{self, UserInterface};
use crate::runtime::Debug; use crate::runtime::Debug;
use crate::style::application::StyleSheet;
use crate::{Clipboard, Error, Proxy, Settings}; use crate::{Clipboard, Error, Proxy, Settings};
pub use crate::application::{default, Appearance, DefaultStyle};
use std::collections::HashMap; use std::collections::HashMap;
use std::mem::ManuallyDrop; use std::mem::ManuallyDrop;
use std::sync::Arc; use std::sync::Arc;
@ -43,7 +44,7 @@ use std::time::Instant;
/// can be toggled by pressing `F12`. /// can be toggled by pressing `F12`.
pub trait Application: Program pub trait Application: Program
where where
Self::Theme: StyleSheet, Self::Theme: DefaultStyle,
{ {
/// The data needed to initialize your [`Application`]. /// The data needed to initialize your [`Application`].
type Flags; type Flags;
@ -68,8 +69,8 @@ where
fn theme(&self, window: window::Id) -> Self::Theme; fn theme(&self, window: window::Id) -> Self::Theme;
/// Returns the `Style` variation of the `Theme`. /// Returns the `Style` variation of the `Theme`.
fn style(&self) -> <Self::Theme as StyleSheet>::Style { fn style(&self, theme: &Self::Theme) -> Appearance {
Default::default() theme.default_style()
} }
/// Returns the event `Subscription` for the current state of the /// Returns the event `Subscription` for the current state of the
@ -110,7 +111,7 @@ where
A: Application + 'static, A: Application + 'static,
E: Executor + 'static, E: Executor + 'static,
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
use winit::event_loop::EventLoopBuilder; use winit::event_loop::EventLoopBuilder;
@ -352,7 +353,7 @@ async fn run_instance<A, E, C>(
A: Application + 'static, A: Application + 'static,
E: Executor + 'static, E: Executor + 'static,
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
use winit::event; use winit::event;
use winit::event_loop::ControlFlow; use winit::event_loop::ControlFlow;
@ -822,7 +823,7 @@ fn build_user_interface<'a, A: Application>(
id: window::Id, id: window::Id,
) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer>
where where
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
debug.view_started(); debug.view_started();
let view = application.view(id); let view = application.view(id);
@ -850,7 +851,7 @@ fn update<A: Application, C, E: Executor>(
ui_caches: &mut HashMap<window::Id, user_interface::Cache>, ui_caches: &mut HashMap<window::Id, user_interface::Cache>,
) where ) where
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
for message in messages.drain(..) { for message in messages.drain(..) {
debug.log_message(&message); debug.log_message(&message);
@ -893,7 +894,7 @@ fn run_command<A, C, E>(
A: Application, A: Application,
E: Executor, E: Executor,
C: Compositor<Renderer = A::Renderer> + 'static, C: Compositor<Renderer = A::Renderer> + 'static,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
use crate::runtime::clipboard; use crate::runtime::clipboard;
use crate::runtime::system; use crate::runtime::system;
@ -1219,8 +1220,8 @@ pub fn build_user_interfaces<'a, A: Application, C: Compositor>(
mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>, mut cached_user_interfaces: HashMap<window::Id, user_interface::Cache>,
) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>> ) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>>
where where
A::Theme: StyleSheet,
C: Compositor<Renderer = A::Renderer>, C: Compositor<Renderer = A::Renderer>,
A::Theme: DefaultStyle,
{ {
cached_user_interfaces cached_user_interfaces
.drain() .drain()

View file

@ -2,18 +2,16 @@ use crate::conversion;
use crate::core::{mouse, window}; use crate::core::{mouse, window};
use crate::core::{Color, Size}; use crate::core::{Color, Size};
use crate::graphics::Viewport; use crate::graphics::Viewport;
use crate::multi_window::Application; use crate::multi_window::{self, Application};
use crate::style::application;
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use iced_style::application::StyleSheet;
use winit::event::{Touch, WindowEvent}; use winit::event::{Touch, WindowEvent};
use winit::window::Window; use winit::window::Window;
/// The state of a multi-windowed [`Application`]. /// The state of a multi-windowed [`Application`].
pub struct State<A: Application> pub struct State<A: Application>
where where
A::Theme: application::StyleSheet, A::Theme: multi_window::DefaultStyle,
{ {
title: String, title: String,
scale_factor: f64, scale_factor: f64,
@ -22,12 +20,12 @@ where
cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, cursor_position: Option<winit::dpi::PhysicalPosition<f64>>,
modifiers: winit::keyboard::ModifiersState, modifiers: winit::keyboard::ModifiersState,
theme: A::Theme, theme: A::Theme,
appearance: application::Appearance, appearance: multi_window::Appearance,
} }
impl<A: Application> Debug for State<A> impl<A: Application> Debug for State<A>
where where
A::Theme: application::StyleSheet, A::Theme: multi_window::DefaultStyle,
{ {
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("multi_window::State")
@ -43,7 +41,7 @@ where
impl<A: Application> State<A> impl<A: Application> State<A>
where where
A::Theme: application::StyleSheet, A::Theme: multi_window::DefaultStyle,
{ {
/// Creates a new [`State`] for the provided [`Application`]'s `window`. /// Creates a new [`State`] for the provided [`Application`]'s `window`.
pub fn new( pub fn new(
@ -54,7 +52,7 @@ where
let title = application.title(window_id); let title = application.title(window_id);
let scale_factor = application.scale_factor(window_id); let scale_factor = application.scale_factor(window_id);
let theme = application.theme(window_id); let theme = application.theme(window_id);
let appearance = theme.appearance(&application.style()); let appearance = application.style(&theme);
let viewport = { let viewport = {
let physical_size = window.inner_size(); let physical_size = window.inner_size();
@ -236,6 +234,6 @@ where
// Update theme and appearance // Update theme and appearance
self.theme = application.theme(window_id); self.theme = application.theme(window_id);
self.appearance = self.theme.appearance(&application.style()); self.appearance = application.style(&self.theme);
} }
} }

View file

@ -2,8 +2,7 @@ use crate::core::mouse;
use crate::core::window::Id; use crate::core::window::Id;
use crate::core::{Point, Size}; use crate::core::{Point, Size};
use crate::graphics::Compositor; use crate::graphics::Compositor;
use crate::multi_window::{Application, State}; use crate::multi_window::{Application, DefaultStyle, State};
use crate::style::application::StyleSheet;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::sync::Arc; use std::sync::Arc;
@ -12,8 +11,8 @@ use winit::monitor::MonitorHandle;
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct WindowManager<A: Application, C: Compositor> pub struct WindowManager<A: Application, C: Compositor>
where where
A::Theme: StyleSheet,
C: Compositor<Renderer = A::Renderer>, C: Compositor<Renderer = A::Renderer>,
A::Theme: DefaultStyle,
{ {
aliases: BTreeMap<winit::window::WindowId, Id>, aliases: BTreeMap<winit::window::WindowId, Id>,
entries: BTreeMap<Id, Window<A, C>>, entries: BTreeMap<Id, Window<A, C>>,
@ -23,7 +22,7 @@ impl<A, C> WindowManager<A, C>
where where
A: Application, A: Application,
C: Compositor<Renderer = A::Renderer>, C: Compositor<Renderer = A::Renderer>,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -109,7 +108,7 @@ impl<A, C> Default for WindowManager<A, C>
where where
A: Application, A: Application,
C: Compositor<Renderer = A::Renderer>, C: Compositor<Renderer = A::Renderer>,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
@ -121,7 +120,7 @@ pub struct Window<A, C>
where where
A: Application, A: Application,
C: Compositor<Renderer = A::Renderer>, C: Compositor<Renderer = A::Renderer>,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
pub raw: Arc<winit::window::Window>, pub raw: Arc<winit::window::Window>,
pub state: State<A>, pub state: State<A>,
@ -136,7 +135,7 @@ impl<A, C> Window<A, C>
where where
A: Application, A: Application,
C: Compositor<Renderer = A::Renderer>, C: Compositor<Renderer = A::Renderer>,
A::Theme: StyleSheet, A::Theme: DefaultStyle,
{ {
pub fn position(&self) -> Option<Point> { pub fn position(&self) -> Option<Point> {
self.raw self.raw