diff options
97 files changed, 5710 insertions, 6632 deletions
diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 35bf10f4..4292dbd0 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -16,7 +16,6 @@ jobs: cargo doc --no-deps --all-features \ -p iced_core \ -p iced_highlighter \ - -p iced_style \ -p iced_futures \ -p iced_runtime \ -p iced_graphics \ @@ -39,8 +39,6 @@ tokio = ["iced_futures/tokio"] async-std = ["iced_futures/async-std"] # Enables `smol` as the `executor::Default` on native platforms smol = ["iced_futures/smol"] -# Enables advanced color conversion via `palette` -palette = ["iced_core/palette"] # Enables querying system information system = ["iced_winit/system"] # Enables broken "sRGB linear" blending to reproduce color management of the Web @@ -57,7 +55,6 @@ advanced = [] fira-sans = ["iced_renderer/fira-sans"] [dependencies] -iced_core.workspace = true iced_futures.workspace = true iced_renderer.workspace = true iced_widget.workspace = true @@ -90,7 +87,6 @@ members = [ "highlighter", "renderer", "runtime", - "style", "tiny_skia", "wgpu", "widget", @@ -116,7 +112,6 @@ iced_graphics = { version = "0.13.0-dev", path = "graphics" } iced_highlighter = { version = "0.13.0-dev", path = "highlighter" } iced_renderer = { version = "0.13.0-dev", path = "renderer" } 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_wgpu = { version = "0.13.0-dev", path = "wgpu" } iced_widget = { version = "0.13.0-dev", path = "widget" } diff --git a/core/Cargo.toml b/core/Cargo.toml index 2360e822..c273fcb4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,14 +15,13 @@ bitflags.workspace = true glam.workspace = true log.workspace = true num-traits.workspace = true +once_cell.workspace = true +palette.workspace = true smol_str.workspace = true thiserror.workspace = true web-time.workspace = true xxhash-rust.workspace = true -palette.workspace = true -palette.optional = true - [target.'cfg(windows)'.dependencies] raw-window-handle.workspace = true diff --git a/core/src/background.rs b/core/src/background.rs index 347c52c0..eb4b5021 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -11,6 +11,19 @@ pub enum Background { // 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 { fn from(color: Color) -> Self { Background::Color(color) diff --git a/core/src/border.rs b/core/src/border.rs index 64262471..2df24988 100644 --- a/core/src/border.rs +++ b/core/src/border.rs @@ -1,5 +1,5 @@ //! Draw lines around containers. -use crate::Color; +use crate::{Color, Pixels}; /// A border. #[derive(Debug, Clone, Copy, PartialEq, Default)] @@ -15,11 +15,38 @@ pub struct Border { } impl Border { - /// Creates a new default [`Border`] with the given [`Radius`]. - pub fn with_radius(radius: impl Into<Radius>) -> Self { + /// Creates a new default rounded [`Border`] with the given [`Radius`]. + /// + /// ``` + /// # 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 { 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 } } } diff --git a/core/src/color.rs b/core/src/color.rs index b8db322f..4e79defb 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "palette")] use palette::rgb::{Srgb, Srgba}; /// A color in the `sRGB` color space. @@ -151,6 +150,14 @@ impl Color { pub fn inverse(self) -> Color { 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 { @@ -202,7 +209,6 @@ macro_rules! color { }}; } -#[cfg(feature = "palette")] /// Converts from palette's `Rgba` type to a [`Color`]. impl From<Srgba> for Color { 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. impl From<Color> for Srgba { 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`]. impl From<Srgb> for Color { 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. impl From<Color> for Srgb { fn from(c: Color) -> Self { @@ -234,7 +237,6 @@ impl From<Color> for Srgb { } } -#[cfg(feature = "palette")] #[cfg(test)] mod tests { use super::*; diff --git a/core/src/gradient.rs b/core/src/gradient.rs index 4711b044..ccae0bce 100644 --- a/core/src/gradient.rs +++ b/core/src/gradient.rs @@ -12,17 +12,13 @@ pub enum Gradient { } impl Gradient { - /// Adjust the opacity of the gradient by a multiplier applied to each color stop. - pub fn mul_alpha(mut self, alpha_multiplier: f32) -> Self { - match &mut self { + /// Scales the alpha channel of the [`Gradient`] by the given factor. + pub fn scale_alpha(self, factor: f32) -> Self { + match self { Gradient::Linear(linear) => { - for stop in linear.stops.iter_mut().flatten() { - stop.color.a *= alpha_multiplier; - } + Gradient::Linear(linear.scale_alpha(factor)) } } - - self } } @@ -100,4 +96,14 @@ impl Linear { 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 + } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 002336ee..d076413e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -30,6 +30,7 @@ pub mod overlay; pub mod renderer; pub mod svg; pub mod text; +pub mod theme; pub mod time; pub mod touch; pub mod widget; @@ -76,6 +77,7 @@ pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; pub use text::Text; +pub use theme::Theme; pub use transformation::Transformation; pub use vector::Vector; pub use widget::Widget; diff --git a/core/src/theme.rs b/core/src/theme.rs new file mode 100644 index 00000000..21ba2a37 --- /dev/null +++ b/core/src/theme.rs @@ -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) + } +} diff --git a/style/src/theme/palette.rs b/core/src/theme/palette.rs index 15a964cd..985a54a8 100644 --- a/style/src/theme/palette.rs +++ b/core/src/theme/palette.rs @@ -1,5 +1,5 @@ //! Define the colors of a theme. -use crate::core::{color, Color}; +use crate::{color, Color}; use once_cell::sync::Lazy; use palette::color_difference::Wcag21RelativeContrast; diff --git a/core/src/widget/text.rs b/core/src/widget/text.rs index 0796c4e4..a220127c 100644 --- a/core/src/widget/text.rs +++ b/core/src/widget/text.rs @@ -17,7 +17,6 @@ pub use text::{LineHeight, Shaping}; #[allow(missing_debug_implementations)] pub struct Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { content: Cow<'a, str>, @@ -29,12 +28,11 @@ where vertical_alignment: alignment::Vertical, font: Option<Renderer::Font>, shaping: Shaping, - style: Theme::Style, + style: Style<Theme>, } impl<'a, Theme, Renderer> Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { /// Create a new fragment of [`Text`] with the given contents. @@ -49,7 +47,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, shaping: Shaping::Basic, - style: Default::default(), + style: Style::default(), } } @@ -74,8 +72,20 @@ where } /// Sets the style of the [`Text`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self { + 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 } @@ -123,7 +133,6 @@ pub struct State<P: Paragraph>(P); impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -175,14 +184,12 @@ where ) { let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); - draw( - renderer, - style, - layout, - state, - theme.appearance(self.style.clone()), - viewport, - ); + let appearance = match self.style { + Style::Themed(f) => f(theme), + Style::Colored(color) => Appearance { color }, + }; + + draw(renderer, style, layout, state, appearance, viewport); } } @@ -273,7 +280,7 @@ pub fn draw<Renderer>( impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -285,7 +292,6 @@ where impl<'a, Theme, Renderer> Clone for Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { fn clone(&self) -> Self { @@ -298,7 +304,7 @@ where horizontal_alignment: self.horizontal_alignment, vertical_alignment: self.vertical_alignment, font: self.font, - style: self.style.clone(), + style: self.style, shaping: self.shaping, } } @@ -306,7 +312,6 @@ where impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { fn from(content: &'a str) -> Self { @@ -317,7 +322,7 @@ where impl<'a, Message, Theme, Renderer> From<&'a str> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { 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. #[derive(Debug, Clone, Copy, Default)] pub struct Appearance { @@ -342,3 +338,29 @@ pub struct Appearance { /// The default, `None`, means using the inherited 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) + } +} diff --git a/examples/bezier_tool/src/main.rs b/examples/bezier_tool/src/main.rs index 56cb23ba..897e7df8 100644 --- a/examples/bezier_tool/src/main.rs +++ b/examples/bezier_tool/src/main.rs @@ -49,7 +49,9 @@ impl Sandbox for Example { column![ text("Bezier tool example").width(Length::Shrink).size(50), 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) .spacing(20) diff --git a/examples/checkbox/src/main.rs b/examples/checkbox/src/main.rs index 834a8f5c..121c99ea 100644 --- a/examples/checkbox/src/main.rs +++ b/examples/checkbox/src/main.rs @@ -1,6 +1,5 @@ use iced::executor; use iced::font::{self, Font}; -use iced::theme; use iced::widget::{checkbox, column, container, row, text}; use iced::{Application, Command, Element, Length, Settings, Theme}; @@ -71,10 +70,10 @@ impl Application for Example { }; let checkboxes = row![ - styled_checkbox("Primary", theme::Checkbox::Primary), - styled_checkbox("Secondary", theme::Checkbox::Secondary), - styled_checkbox("Success", theme::Checkbox::Success), - styled_checkbox("Danger", theme::Checkbox::Danger), + styled_checkbox("Primary", checkbox::primary), + styled_checkbox("Secondary", checkbox::secondary), + styled_checkbox("Success", checkbox::success), + styled_checkbox("Danger", checkbox::danger), ] .spacing(20); diff --git a/examples/clock/src/main.rs b/examples/clock/src/main.rs index 13252526..87da0c7e 100644 --- a/examples/clock/src/main.rs +++ b/examples/clock/src/main.rs @@ -3,7 +3,7 @@ use iced::mouse; use iced::widget::canvas::{stroke, Cache, Geometry, LineCap, Path, Stroke}; use iced::widget::{canvas, container}; use iced::{ - Application, Color, Command, Element, Length, Point, Rectangle, Renderer, + Application, Command, Element, Length, Point, Rectangle, Renderer, 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 { @@ -89,16 +93,18 @@ impl<Message> canvas::Program<Message> for Clock { &self, _state: &Self::State, renderer: &Renderer, - _theme: &Theme, + theme: &Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec<Geometry> { let clock = self.clock.draw(renderer, bounds.size(), |frame| { + let palette = theme.extended_palette(); + let center = frame.center(); let radius = frame.width().min(frame.height()) / 2.0; 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 = 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 { Stroke { width, - style: stroke::Style::Solid(Color::WHITE), + style: stroke::Style::Solid(palette.primary.weak.text), line_cap: LineCap::Round, ..Stroke::default() } @@ -120,7 +126,7 @@ impl<Message> canvas::Program<Message> for Clock { let wide_stroke = || -> Stroke { Stroke { width: width * 3.0, - style: stroke::Style::Solid(Color::WHITE), + style: stroke::Style::Solid(palette.primary.weak.text), line_cap: LineCap::Round, ..Stroke::default() } diff --git a/examples/color_palette/Cargo.toml b/examples/color_palette/Cargo.toml index 2da6c6ed..bf9bff19 100644 --- a/examples/color_palette/Cargo.toml +++ b/examples/color_palette/Cargo.toml @@ -7,6 +7,6 @@ publish = false [dependencies] iced.workspace = true -iced.features = ["canvas", "palette"] +iced.features = ["canvas"] palette.workspace = true diff --git a/examples/color_palette/src/main.rs b/examples/color_palette/src/main.rs index a5fd46e0..4150c641 100644 --- a/examples/color_palette/src/main.rs +++ b/examples/color_palette/src/main.rs @@ -3,7 +3,7 @@ use iced::mouse; use iced::widget::canvas::{self, Canvas, Frame, Geometry, Path}; use iced::widget::{column, row, text, Slider}; use iced::{ - Color, Element, Length, Pixels, Point, Rectangle, Renderer, Sandbox, + Color, Element, Font, Length, Pixels, Point, Rectangle, Renderer, Sandbox, Settings, Size, Vector, }; use palette::{ @@ -15,6 +15,7 @@ use std::ops::RangeInclusive; pub fn main() -> iced::Result { ColorPalette::run(Settings { antialiasing: true, + default_font: Font::MONOSPACE, ..Settings::default() }) } @@ -87,6 +88,19 @@ impl Sandbox for ColorPalette { .spacing(10) .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)] @@ -150,7 +164,7 @@ impl Theme { .into() } - fn draw(&self, frame: &mut Frame) { + fn draw(&self, frame: &mut Frame, text_color: Color) { let pad = 20.0; let box_size = Size { @@ -169,6 +183,7 @@ impl Theme { horizontal_alignment: alignment::Horizontal::Center, vertical_alignment: alignment::Vertical::Top, size: Pixels(15.0), + color: text_color, ..canvas::Text::default() }; @@ -246,12 +261,14 @@ impl<Message> canvas::Program<Message> for Theme { &self, _state: &Self::State, renderer: &Renderer, - _theme: &iced::Theme, + theme: &iced::Theme, bounds: Rectangle, _cursor: mouse::Cursor, ) -> Vec<Geometry> { 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] @@ -308,7 +325,7 @@ impl<C: ColorSpace + Copy> ColorPicker<C> { slider(cr1, c1, move |v| C::new(v, c2, c3)), slider(cr2, c2, move |v| C::new(c1, v, c3)), 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) .align_items(Alignment::Center) diff --git a/examples/custom_widget/src/main.rs b/examples/custom_widget/src/main.rs index 25c0bb39..305ef7dd 100644 --- a/examples/custom_widget/src/main.rs +++ b/examples/custom_widget/src/main.rs @@ -62,7 +62,7 @@ mod circle { renderer.fill_quad( renderer::Quad { bounds: layout.bounds(), - border: Border::with_radius(self.radius), + border: Border::rounded(self.radius), ..renderer::Quad::default() }, Color::BLACK, diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 53c9cf7c..60a6ca5a 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -1,14 +1,13 @@ use iced::executor; use iced::highlighter::{self, Highlighter}; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::{ button, column, container, horizontal_space, pick_list, row, text, text_editor, tooltip, }; use iced::{ Alignment, Application, Command, Element, Font, Length, Settings, - Subscription, + Subscription, Theme, }; use std::ffi; @@ -287,10 +286,10 @@ fn action<'a, Message: Clone + 'a>( label, tooltip::Position::FollowCursor, ) - .style(theme::Container::Box) + .style(container::box_) .into() } else { - action.style(theme::Button::Secondary).into() + action.style(button::secondary).into() } } diff --git a/examples/game_of_life/src/main.rs b/examples/game_of_life/src/main.rs index 9cbb7fff..5ec1a11c 100644 --- a/examples/game_of_life/src/main.rs +++ b/examples/game_of_life/src/main.rs @@ -6,7 +6,6 @@ use grid::Grid; use preset::Preset; use iced::executor; -use iced::theme::{self, Theme}; use iced::time; use iced::widget::{ button, checkbox, column, container, pick_list, row, slider, text, @@ -14,6 +13,7 @@ use iced::widget::{ use iced::window; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; use std::time::Duration; @@ -171,7 +171,7 @@ fn view_controls<'a>( .on_press(Message::TogglePlayback), button("Next") .on_press(Message::Next) - .style(theme::Button::Secondary), + .style(button::secondary), ] .spacing(10); @@ -185,17 +185,14 @@ fn view_controls<'a>( row![ playback_controls, speed_controls, - checkbox("Grid", is_grid_enabled) - .on_toggle(Message::ToggleGrid) - .size(16) - .spacing(5) - .text_size(16), - pick_list(preset::ALL, Some(preset), Message::PresetPicked) - .padding(8) - .text_size(16), - button("Clear") - .on_press(Message::Clear) - .style(theme::Button::Destructive), + checkbox("Grid", is_grid_enabled).on_toggle(Message::ToggleGrid), + row![ + pick_list(preset::ALL, Some(preset), Message::PresetPicked), + button("Clear") + .on_press(Message::Clear) + .style(button::danger) + ] + .spacing(10) ] .padding(10) .spacing(20) diff --git a/examples/gradient/src/main.rs b/examples/gradient/src/main.rs index a021c164..4a8b2fa5 100644 --- a/examples/gradient/src/main.rs +++ b/examples/gradient/src/main.rs @@ -1,11 +1,10 @@ use iced::application; -use iced::theme::{self, Theme}; 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::{ - Alignment, Background, Color, Element, Length, Radians, Sandbox, Settings, + Alignment, Color, Element, Length, Radians, Sandbox, Settings, Theme, }; pub fn main() -> iced::Result { @@ -71,20 +70,16 @@ impl Sandbox for Gradient { transparent, } = *self; - let gradient_box = container(horizontal_space()) - .width(Length::Fill) - .height(Length::Fill) - .style(move |_: &_| { - let gradient = gradient::Linear::new(angle) - .add_stop(0.0, start) - .add_stop(1.0, end) - .into(); - - container::Appearance { - background: Some(Background::Gradient(gradient)), - ..Default::default() - } - }); + let gradient = gradient::Linear::new(angle) + .add_stop(0.0, start) + .add_stop(1.0, end); + + let gradient_box = themer( + gradient, + container(horizontal_space()) + .width(Length::Fill) + .height(Length::Fill), + ); let angle_picker = row![ text("Angle").width(64), @@ -111,16 +106,14 @@ impl Sandbox for Gradient { .into() } - fn style(&self) -> theme::Application { + fn style(&self, theme: &Theme) -> application::Appearance { if self.transparent { - theme::Application::custom(|theme: &Theme| { - application::Appearance { - background_color: Color::TRANSPARENT, - text_color: theme.palette().text, - } - }) + application::Appearance { + background_color: Color::TRANSPARENT, + text_color: theme.palette().text, + } } else { - theme::Application::Default + application::default(theme) } } } diff --git a/examples/integration/src/controls.rs b/examples/integration/src/controls.rs index c9bab828..28050f8a 100644 --- a/examples/integration/src/controls.rs +++ b/examples/integration/src/controls.rs @@ -1,25 +1,25 @@ use iced_wgpu::Renderer; -use iced_widget::{slider, text_input, Column, Row, Text}; -use iced_winit::core::{Alignment, Color, Element, Length}; +use iced_widget::{column, container, row, slider, text, text_input}; +use iced_winit::core::alignment; +use iced_winit::core::{Color, Element, Length, Theme}; use iced_winit::runtime::{Command, Program}; -use iced_winit::style::Theme; pub struct Controls { background_color: Color, - text: String, + input: String, } #[derive(Debug, Clone)] pub enum Message { BackgroundColorChanged(Color), - TextChanged(String), + InputChanged(String), } impl Controls { pub fn new() -> Controls { Controls { background_color: Color::BLACK, - text: String::default(), + input: String::default(), } } @@ -38,8 +38,8 @@ impl Program for Controls { Message::BackgroundColorChanged(color) => { self.background_color = color; } - Message::TextChanged(text) => { - self.text = text; + Message::InputChanged(input) => { + self.input = input; } } @@ -48,60 +48,48 @@ impl Program for Controls { fn view(&self) -> Element<Message, Theme, Renderer> { let background_color = self.background_color; - let text = &self.text; - let sliders = Row::new() - .width(500) - .spacing(20) - .push( - slider(0.0..=1.0, background_color.r, move |r| { - Message::BackgroundColorChanged(Color { - r, - ..background_color - }) + let sliders = row![ + slider(0.0..=1.0, background_color.r, move |r| { + Message::BackgroundColorChanged(Color { + r, + ..background_color }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.g, move |g| { - Message::BackgroundColorChanged(Color { - g, - ..background_color - }) + }) + .step(0.01), + slider(0.0..=1.0, background_color.g, move |g| { + Message::BackgroundColorChanged(Color { + g, + ..background_color }) - .step(0.01), - ) - .push( - slider(0.0..=1.0, background_color.b, move |b| { - Message::BackgroundColorChanged(Color { - b, - ..background_color - }) + }) + .step(0.01), + slider(0.0..=1.0, background_color.b, move |b| { + Message::BackgroundColorChanged(Color { + b, + ..background_color }) - .step(0.01), - ); + }) + .step(0.01), + ] + .width(500) + .spacing(20); - Row::new() - .height(Length::Fill) - .align_items(Alignment::End) - .push( - Column::new().align_items(Alignment::End).push( - Column::new() - .padding(10) - .spacing(10) - .push(Text::new("Background color").style(Color::WHITE)) - .push(sliders) - .push( - Text::new(format!("{background_color:?}")) - .size(14) - .style(Color::WHITE), - ) - .push( - text_input("Placeholder", text) - .on_input(Message::TextChanged), - ), - ), - ) - .into() + container( + column![ + text("Background color").color(Color::WHITE), + text(format!("{background_color:?}")) + .size(14) + .color(Color::WHITE), + text_input("Placeholder", &self.input) + .on_input(Message::InputChanged), + sliders, + ] + .spacing(10), + ) + .padding(10) + .height(Length::Fill) + .align_y(alignment::Vertical::Bottom) + .into() } } diff --git a/examples/integration/src/main.rs b/examples/integration/src/main.rs index 0e2e53ac..f53b5bf1 100644 --- a/examples/integration/src/main.rs +++ b/examples/integration/src/main.rs @@ -10,11 +10,10 @@ use iced_winit::conversion; use iced_winit::core::mouse; use iced_winit::core::renderer; 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::runtime::program; use iced_winit::runtime::Debug; -use iced_winit::style::Theme; use iced_winit::winit; use iced_winit::Clipboard; diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 39c8315f..b2d28a1c 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -1,7 +1,6 @@ use iced::executor; use iced::keyboard; use iced::mouse; -use iced::theme; use iced::widget::{ button, canvas, checkbox, column, container, horizontal_space, pick_list, row, scrollable, text, @@ -98,7 +97,7 @@ impl Application for Layout { } else { self.example.view() }) - .style(|theme: &Theme| { + .style(|theme, _status| { let palette = theme.extended_palette(); container::Appearance::default() @@ -262,7 +261,7 @@ fn application<'a>() -> Element<'a, Message> { .padding(10) .align_items(Alignment::Center), ) - .style(|theme: &Theme| { + .style(|theme, _status| { let palette = theme.extended_palette(); container::Appearance::default() @@ -276,7 +275,7 @@ fn application<'a>() -> Element<'a, Message> { .width(200) .align_items(Alignment::Center), ) - .style(theme::Container::Box) + .style(container::box_) .height(Length::Fill) .center_y(); diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 9d8c0e35..8758fa66 100644 --- a/examples/lazy/src/main.rs +++ b/examples/lazy/src/main.rs @@ -1,4 +1,3 @@ -use iced::theme; use iced::widget::{ button, column, horizontal_space, lazy, pick_list, row, scrollable, text, text_input, @@ -181,11 +180,10 @@ impl Sandbox for App { column(items.into_iter().map(|item| { let button = button("Delete") .on_press(Message::DeleteItem(item.clone())) - .style(theme::Button::Destructive); + .style(button::danger); row![ - text(&item.name) - .style(theme::Text::Color(item.color.into())), + text(&item.name).color(item.color), horizontal_space(), pick_list(Color::ALL, Some(item.color), move |color| { Message::ItemColorChanged(item.clone(), color) diff --git a/examples/modal/src/main.rs b/examples/modal/src/main.rs index 938ce32c..df3da1cd 100644 --- a/examples/modal/src/main.rs +++ b/examples/modal/src/main.rs @@ -2,7 +2,6 @@ use iced::event::{self, Event}; use iced::executor; use iced::keyboard; use iced::keyboard::key; -use iced::theme; use iced::widget::{ self, button, column, container, horizontal_space, pick_list, row, text, text_input, @@ -175,7 +174,7 @@ impl Application for App { ) .width(300) .padding(10) - .style(theme::Container::Box); + .style(container::box_); Modal::new(content, modal) .on_blur(Message::HideModal) diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index 39719420..5e728ce1 100644 --- a/examples/pane_grid/src/main.rs +++ b/examples/pane_grid/src/main.rs @@ -1,13 +1,13 @@ use iced::alignment::{self, Alignment}; use iced::executor; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::pane_grid::{self, PaneGrid}; use iced::widget::{ button, column, container, responsive, row, scrollable, text, }; use iced::{ Application, Color, Command, Element, Length, Settings, Size, Subscription, + Theme, }; pub fn main() -> iced::Result { @@ -162,7 +162,7 @@ impl Application for Example { let title = row![ pin_button, "Pane", - text(pane.id.to_string()).style(if is_focused { + text(pane.id.to_string()).color(if is_focused { PANE_ID_COLOR_FOCUSED } else { PANE_ID_COLOR_UNFOCUSED @@ -287,10 +287,7 @@ fn view_content<'a>( ) ] .push_maybe(if total_panes > 1 && !is_pinned { - Some( - button("Close", Message::Close(pane)) - .style(theme::Button::Destructive), - ) + Some(button("Close", Message::Close(pane)).style(button::danger)) } else { None }) @@ -327,7 +324,7 @@ fn view_controls<'a>( Some( button(text(content).size(14)) - .style(theme::Button::Secondary) + .style(button::secondary) .padding(3) .on_press(message), ) @@ -336,7 +333,7 @@ fn view_controls<'a>( }); let close = button(text("Close").size(14)) - .style(theme::Button::Destructive) + .style(button::danger) .padding(3) .on_press_maybe(if total_panes > 1 && !is_pinned { Some(Message::Close(pane)) @@ -351,7 +348,10 @@ mod style { use iced::widget::container; 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(); 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(); 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(); 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(); container::Appearance { diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 8b71a269..193f85f2 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,8 +1,6 @@ use iced::futures; use iced::widget::{self, column, container, image, row, text}; -use iced::{ - Alignment, Application, Color, Command, Element, Length, Settings, Theme, -}; +use iced::{Alignment, Application, Command, Element, Length, Settings, Theme}; pub fn main() -> iced::Result { Pokedex::run(Settings::default()) @@ -116,7 +114,7 @@ impl Pokemon { text(&self.name).size(30).width(Length::Fill), text(format!("#{}", self.number)) .size(20) - .style(Color::from([0.5, 0.5, 0.5])), + .color([0.5, 0.5, 0.5]), ] .align_items(Alignment::Center) .spacing(20), diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 79749956..10a6aea3 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -1,7 +1,6 @@ use iced::alignment; use iced::executor; use iced::keyboard; -use iced::theme; use iced::widget::{button, column, container, image, row, text, text_input}; use iced::window; use iced::window::screenshot::{self, Screenshot}; @@ -149,7 +148,7 @@ impl Application for Example { let image = container(image) .padding(10) - .style(theme::Container::Box) + .style(container::box_) .width(Length::FillPortion(2)) .height(Length::Fill) .center_x() @@ -216,9 +215,9 @@ impl Application for Example { ) } else { button(centered_text("Saving...")) - .style(theme::Button::Secondary) + .style(button::secondary) } - .style(theme::Button::Secondary) + .style(button::secondary) .padding([10, 20, 10, 20]) .width(Length::Fill) ] @@ -227,7 +226,7 @@ impl Application for Example { crop_controls, button(centered_text("Crop")) .on_press(Message::Crop) - .style(theme::Button::Destructive) + .style(button::danger) .padding([10, 20, 10, 20]) .width(Length::Fill), ] diff --git a/examples/scrollable/src/main.rs b/examples/scrollable/src/main.rs index bae23775..2ad7272b 100644 --- a/examples/scrollable/src/main.rs +++ b/examples/scrollable/src/main.rs @@ -5,7 +5,8 @@ use iced::widget::{ scrollable, slider, text, vertical_space, Scrollable, }; use iced::{ - Alignment, Application, Color, Command, Element, Length, Settings, Theme, + Alignment, Application, Border, Color, Command, Element, Length, Settings, + Theme, }; use once_cell::sync::Lazy; @@ -348,6 +349,6 @@ fn progress_bar_custom_style(theme: &Theme) -> progress_bar::Appearance { progress_bar::Appearance { background: theme.extended_palette().background.strong.color.into(), bar: Color::from_rgb8(250, 85, 134).into(), - border_radius: 0.0.into(), + border: Border::default(), } } diff --git a/examples/solar_system/src/main.rs b/examples/solar_system/src/main.rs index a58ca683..4cc625da 100644 --- a/examples/solar_system/src/main.rs +++ b/examples/solar_system/src/main.rs @@ -9,7 +9,6 @@ use iced::application; use iced::executor; use iced::mouse; -use iced::theme::{self, Theme}; use iced::widget::canvas; use iced::widget::canvas::gradient; use iced::widget::canvas::stroke::{self, Stroke}; @@ -17,7 +16,7 @@ use iced::widget::canvas::Path; use iced::window; use iced::{ Application, Color, Command, Element, Length, Point, Rectangle, Renderer, - Settings, Size, Subscription, Vector, + Settings, Size, Subscription, Theme, Vector, }; use std::time::Instant; @@ -80,15 +79,11 @@ impl Application for SolarSystem { Theme::Dark } - fn style(&self) -> theme::Application { - fn dark_background(_theme: &Theme) -> application::Appearance { - application::Appearance { - background_color: Color::BLACK, - text_color: Color::WHITE, - } + fn style(&self, _theme: &Theme) -> application::Appearance { + application::Appearance { + background_color: Color::BLACK, + text_color: Color::WHITE, } - - theme::Application::custom(dark_background) } fn subscription(&self) -> Subscription<Message> { diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 8a0674c1..56b7686e 100644 --- a/examples/stopwatch/src/main.rs +++ b/examples/stopwatch/src/main.rs @@ -1,11 +1,11 @@ use iced::alignment; use iced::executor; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::time; use iced::widget::{button, column, container, row, text}; use iced::{ Alignment, Application, Command, Element, Length, Settings, Subscription, + Theme, }; use std::time::{Duration, Instant}; @@ -136,7 +136,7 @@ impl Application for Stopwatch { }; let reset_button = button("Reset") - .style(theme::Button::Destructive) + .style(button::danger) .on_press(Message::Reset); let controls = row![toggle_button, reset_button].spacing(20); diff --git a/examples/svg/src/main.rs b/examples/svg/src/main.rs index ba93007c..4e238048 100644 --- a/examples/svg/src/main.rs +++ b/examples/svg/src/main.rs @@ -1,4 +1,3 @@ -use iced::theme; use iced::widget::{checkbox, column, container, svg}; 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( if self.apply_color_filter { - theme::Svg::custom_fn(|_theme| svg::Appearance { + |_theme, _status| svg::Appearance { color: Some(color!(0x0000ff)), - }) + } } else { - theme::Svg::Default + |_theme, _status| svg::Appearance::default() }, ); diff --git a/examples/toast/src/main.rs b/examples/toast/src/main.rs index c1d29193..49626710 100644 --- a/examples/toast/src/main.rs +++ b/examples/toast/src/main.rs @@ -209,27 +209,6 @@ mod toast { &[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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { @@ -282,14 +261,17 @@ mod toast { ) .width(Length::Fill) .padding(5) - .style( - theme::Container::Custom(Box::new(toast.status)) - ), + .style(match toast.status { + Status::Primary => primary, + Status::Secondary => secondary, + Status::Success => success, + Status::Danger => danger, + }), horizontal_rule(1), container(text(toast.body.as_str())) .width(Length::Fill) .padding(5) - .style(theme::Container::Box), + .style(container::box_), ]) .max_width(200) .into() @@ -676,4 +658,48 @@ mod toast { 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) + } } diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index eae127f7..aaa86ef8 100644 --- a/examples/todos/src/main.rs +++ b/examples/todos/src/main.rs @@ -1,14 +1,14 @@ use iced::alignment::{self, Alignment}; use iced::font::{self, Font}; use iced::keyboard; -use iced::theme::{self, Theme}; use iced::widget::{ self, button, checkbox, column, container, keyed_column, row, scrollable, text, text_input, Text, }; use iced::window; -use iced::{Application, Element}; -use iced::{Color, Command, Length, Settings, Size, Subscription}; +use iced::{ + Application, Command, Element, Length, Settings, Size, Subscription, Theme, +}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -209,7 +209,7 @@ impl Application for Todos { let title = text("todos") .width(Length::Fill) .size(100) - .style(Color::from([0.5, 0.5, 0.5])) + .color([0.5, 0.5, 0.5]) .horizontal_alignment(alignment::Horizontal::Center); let input = text_input("What needs to be done?", input_value) @@ -355,6 +355,7 @@ impl Task { let checkbox = checkbox(&self.description, self.completed) .on_toggle(TaskMessage::Completed) .width(Length::Fill) + .size(17) .text_shaping(text::Shaping::Advanced); row![ @@ -362,7 +363,7 @@ impl Task { button(edit_icon()) .on_press(TaskMessage::Edit) .padding(10) - .style(theme::Button::Text), + .style(button::text), ] .spacing(20) .align_items(Alignment::Center) @@ -385,7 +386,7 @@ impl Task { ) .on_press(TaskMessage::Delete) .padding(10) - .style(theme::Button::Destructive) + .style(button::danger) ] .spacing(20) .align_items(Alignment::Center) @@ -402,9 +403,9 @@ fn view_controls(tasks: &[Task], current_filter: Filter) -> Element<Message> { let label = text(label); let button = button(label).style(if filter == current_filter { - theme::Button::Primary + button::primary } else { - theme::Button::Text + button::text }); button.on_press(Message::FilterChanged(filter)).padding(8) @@ -467,7 +468,7 @@ fn empty_message(message: &str) -> Element<'_, Message> { .width(Length::Fill) .size(25) .horizontal_alignment(alignment::Horizontal::Center) - .style(Color::from([0.7, 0.7, 0.7])), + .color([0.7, 0.7, 0.7]), ) .height(200) .center_y() diff --git a/examples/tooltip/src/main.rs b/examples/tooltip/src/main.rs index a904cce0..c83b671f 100644 --- a/examples/tooltip/src/main.rs +++ b/examples/tooltip/src/main.rs @@ -1,4 +1,3 @@ -use iced::theme; use iced::widget::tooltip::Position; use iced::widget::{button, container, tooltip}; use iced::{Element, Length, Sandbox, Settings}; @@ -53,7 +52,7 @@ impl Sandbox for Example { self.position, ) .gap(10) - .style(theme::Container::Box); + .style(container::box_); container(tooltip) .width(Length::Fill) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 1e2f1ef8..f5791ad7 100644 --- a/examples/tour/src/main.rs +++ b/examples/tour/src/main.rs @@ -1,7 +1,6 @@ use iced::alignment::{self, Alignment}; -use iced::theme; 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, }; use iced::widget::{Button, Column, Container, Slider}; @@ -56,18 +55,17 @@ impl Sandbox for Tour { fn view(&self) -> Element<Message> { let Tour { steps, .. } = self; - let controls = row![] - .push_maybe(steps.has_previous().then(|| { - button("Back") - .on_press(Message::BackPressed) - .style(theme::Button::Secondary) - })) - .push(horizontal_space()) - .push_maybe( - steps - .can_continue() - .then(|| button("Next").on_press(Message::NextPressed)), - ); + let controls = + row![] + .push_maybe(steps.has_previous().then(|| { + padded_button("Back") + .on_press(Message::BackPressed) + .style(button::secondary) + })) + .push(horizontal_space()) + .push_maybe(steps.can_continue().then(|| { + padded_button("Next").on_press(Message::NextPressed) + })); let content: Element<_> = column![ steps.view(self.debug).map(Message::StepMessage), @@ -474,7 +472,7 @@ impl<'a> Step { let color_section = column![ "And its color:", - text(format!("{color:?}")).style(color), + text(format!("{color:?}")).color(color), color_sliders, ] .padding(20) @@ -676,8 +674,8 @@ fn ferris<'a>( .center_x() } -fn button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { - iced::widget::button(text(label)).padding([12, 24]) +fn padded_button<'a, Message: Clone>(label: &str) -> Button<'a, Message> { + button(text(label)).padding([12, 24]) } fn color_slider<'a>( diff --git a/examples/visible_bounds/src/main.rs b/examples/visible_bounds/src/main.rs index bef5d296..d7f5a81d 100644 --- a/examples/visible_bounds/src/main.rs +++ b/examples/visible_bounds/src/main.rs @@ -1,14 +1,13 @@ use iced::event::{self, Event}; use iced::executor; use iced::mouse; -use iced::theme::{self, Theme}; use iced::widget::{ column, container, horizontal_space, row, scrollable, text, vertical_space, }; use iced::window; use iced::{ Alignment, Application, Color, Command, Element, Font, Length, Point, - Rectangle, Settings, Subscription, + Rectangle, Settings, Subscription, Theme, }; pub fn main() -> iced::Result { @@ -82,7 +81,10 @@ impl Application for Example { row![ text(label), horizontal_space(), - text(value).font(Font::MONOSPACE).size(14).style(color), + text(value) + .font(Font::MONOSPACE) + .size(14) + .color_maybe(color), ] .height(40) .align_items(Alignment::Center) @@ -102,13 +104,12 @@ impl Application for Example { }) .unwrap_or_default() { - Color { + Some(Color { g: 1.0, ..Color::BLACK - } - .into() + }) } else { - theme::Text::Default + None }, ) }; @@ -120,7 +121,7 @@ impl Application for Example { Some(Point { x, y }) => format!("({x}, {y})"), None => "unknown".to_string(), }, - theme::Text::Default, + None, ), view_bounds("Outer container", self.outer_bounds), view_bounds("Inner container", self.inner_bounds), @@ -131,7 +132,7 @@ impl Application for Example { container(text("I am the outer container!")) .id(OUTER_CONTAINER.clone()) .padding(40) - .style(theme::Container::Box), + .style(container::box_), vertical_space().height(400), scrollable( column![ @@ -140,7 +141,7 @@ impl Application for Example { container(text("I am the inner container!")) .id(INNER_CONTAINER.clone()) .padding(40) - .style(theme::Container::Box), + .style(container::box_), vertical_space().height(400), ] .padding(20) diff --git a/examples/websocket/src/main.rs b/examples/websocket/src/main.rs index 38a6db1e..47c1898a 100644 --- a/examples/websocket/src/main.rs +++ b/examples/websocket/src/main.rs @@ -6,7 +6,7 @@ use iced::widget::{ button, column, container, row, scrollable, text, text_input, }; use iced::{ - Application, Color, Command, Element, Length, Settings, Subscription, Theme, + color, Application, Command, Element, Length, Settings, Subscription, Theme, }; use once_cell::sync::Lazy; @@ -99,7 +99,7 @@ impl Application for WebSocket { let message_log: Element<_> = if self.messages.is_empty() { container( text("Your messages will appear here...") - .style(Color::from_rgb8(0x88, 0x88, 0x88)), + .color(color!(0x888888)), ) .width(Length::Fill) .height(Length::Fill) diff --git a/src/application.rs b/src/application.rs index 87c2607a..be0fa0de 100644 --- a/src/application.rs +++ b/src/application.rs @@ -1,7 +1,9 @@ //! Build interactive cross-platform applications. 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. /// @@ -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 [default executor] can be a good starting point! @@ -104,7 +109,7 @@ pub trait Application: Sized { type Message: std::fmt::Debug + Send; /// The theme of your [`Application`]. - type Theme: Default + StyleSheet; + type Theme: Default; /// The data needed to initialize your [`Application`]. type Flags; @@ -148,11 +153,9 @@ pub trait Application: Sized { 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 current [`Appearance`] of the [`Application`]. + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() } /// 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> where A: Application, + A::Theme: DefaultStyle, { type Message = A::Message; 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 A: Application, + A::Theme: DefaultStyle, { type Flags = A::Flags; @@ -267,8 +275,8 @@ where self.0.theme() } - fn style(&self) -> <A::Theme as StyleSheet>::Style { - self.0.style() + fn style(&self, theme: &A::Theme) -> Appearance { + self.0.style(theme) } fn subscription(&self) -> Subscription<Self::Message> { @@ -162,7 +162,6 @@ #![cfg_attr(docsrs, feature(doc_cfg))] use iced_widget::graphics; use iced_widget::renderer; -use iced_widget::style; use iced_winit as shell; use iced_winit::core; use iced_winit::runtime; @@ -186,15 +185,14 @@ pub mod advanced; #[cfg(feature = "multi-window")] pub mod multi_window; -pub use style::theme; - pub use crate::core::alignment; pub use crate::core::border; pub use crate::core::color; pub use crate::core::gradient; +pub use crate::core::theme; pub use crate::core::{ 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, }; @@ -314,7 +312,6 @@ pub use renderer::Renderer; pub use sandbox::Sandbox; pub use settings::Settings; pub use subscription::Subscription; -pub use theme::Theme; /// A generic widget. /// diff --git a/src/multi_window.rs b/src/multi_window.rs index 5b7a00b4..c4063563 100644 --- a/src/multi_window.rs +++ b/src/multi_window.rs @@ -1,4 +1,256 @@ //! 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) + } +} diff --git a/src/multi_window/application.rs b/src/multi_window/application.rs deleted file mode 100644 index ac625281..00000000 --- a/src/multi_window/application.rs +++ /dev/null @@ -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) - } -} diff --git a/src/sandbox.rs b/src/sandbox.rs index 28461929..568b673e 100644 --- a/src/sandbox.rs +++ b/src/sandbox.rs @@ -1,5 +1,5 @@ -use crate::theme::{self, Theme}; -use crate::{Application, Command, Element, Error, Settings, Subscription}; +use crate::application::{self, Application}; +use crate::{Command, Element, Error, Settings, Subscription, Theme}; /// A sandboxed [`Application`]. /// @@ -120,11 +120,11 @@ pub trait Sandbox { Theme::default() } - /// Returns the current style variant of [`theme::Application`]. - /// - /// By default, it returns [`theme::Application::default`]. - fn style(&self) -> theme::Application { - theme::Application::default() + /// Returns the current [`application::Appearance`]. + fn style(&self, theme: &Theme) -> application::Appearance { + use application::DefaultStyle; + + theme.default_style() } /// Returns the scale factor of the [`Sandbox`]. @@ -185,8 +185,8 @@ where T::theme(self) } - fn style(&self) -> theme::Application { - T::style(self) + fn style(&self, theme: &Theme) -> application::Appearance { + T::style(self, theme) } fn subscription(&self) -> Subscription<T::Message> { diff --git a/src/time.rs b/src/time.rs index e255d751..26d31c0a 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,5 +1,5 @@ //! Listen and react to time. -pub use iced_core::time::{Duration, Instant}; +pub use crate::core::time::{Duration, Instant}; #[allow(unused_imports)] #[cfg_attr( diff --git a/style/Cargo.toml b/style/Cargo.toml deleted file mode 100644 index 3f00e787..00000000 --- a/style/Cargo.toml +++ /dev/null @@ -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 diff --git a/style/src/application.rs b/style/src/application.rs deleted file mode 100644 index e9a1f4ff..00000000 --- a/style/src/application.rs +++ /dev/null @@ -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, -} diff --git a/style/src/button.rs b/style/src/button.rs deleted file mode 100644 index 0d7a668a..00000000 --- a/style/src/button.rs +++ /dev/null @@ -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 - } - } -} diff --git a/style/src/checkbox.rs b/style/src/checkbox.rs deleted file mode 100644 index 77093f69..00000000 --- a/style/src/checkbox.rs +++ /dev/null @@ -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 - } - } -} diff --git a/style/src/container.rs b/style/src/container.rs deleted file mode 100644 index 00649c25..00000000 --- a/style/src/container.rs +++ /dev/null @@ -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; -} diff --git a/style/src/lib.rs b/style/src/lib.rs deleted file mode 100644 index 3c2865eb..00000000 --- a/style/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -//! The styling library of Iced. -//! -//! It contains a set of styles and stylesheets for most of the built-in -//! widgets. -//! -//!  -#![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; diff --git a/style/src/menu.rs b/style/src/menu.rs deleted file mode 100644 index be60a3f8..00000000 --- a/style/src/menu.rs +++ /dev/null @@ -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; -} diff --git a/style/src/pane_grid.rs b/style/src/pane_grid.rs deleted file mode 100644 index 35570584..00000000 --- a/style/src/pane_grid.rs +++ /dev/null @@ -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>; -} diff --git a/style/src/pick_list.rs b/style/src/pick_list.rs deleted file mode 100644 index 8f008f4a..00000000 --- a/style/src/pick_list.rs +++ /dev/null @@ -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; -} diff --git a/style/src/progress_bar.rs b/style/src/progress_bar.rs deleted file mode 100644 index b62512d8..00000000 --- a/style/src/progress_bar.rs +++ /dev/null @@ -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; -} diff --git a/style/src/qr_code.rs b/style/src/qr_code.rs deleted file mode 100644 index 02c4709a..00000000 --- a/style/src/qr_code.rs +++ /dev/null @@ -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; -} diff --git a/style/src/radio.rs b/style/src/radio.rs deleted file mode 100644 index 06c49029..00000000 --- a/style/src/radio.rs +++ /dev/null @@ -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; -} diff --git a/style/src/rule.rs b/style/src/rule.rs deleted file mode 100644 index 12980da7..00000000 --- a/style/src/rule.rs +++ /dev/null @@ -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) - } - } - } -} diff --git a/style/src/scrollable.rs b/style/src/scrollable.rs deleted file mode 100644 index d2348510..00000000 --- a/style/src/scrollable.rs +++ /dev/null @@ -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) - } -} diff --git a/style/src/slider.rs b/style/src/slider.rs deleted file mode 100644 index bf1c7329..00000000 --- a/style/src/slider.rs +++ /dev/null @@ -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; -} diff --git a/style/src/svg.rs b/style/src/svg.rs deleted file mode 100644 index 3fe5546b..00000000 --- a/style/src/svg.rs +++ /dev/null @@ -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) - } -} diff --git a/style/src/text_editor.rs b/style/src/text_editor.rs deleted file mode 100644 index 87f481e3..00000000 --- a/style/src/text_editor.rs +++ /dev/null @@ -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; -} diff --git a/style/src/text_input.rs b/style/src/text_input.rs deleted file mode 100644 index 8ba9957f..00000000 --- a/style/src/text_input.rs +++ /dev/null @@ -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; -} diff --git a/style/src/theme.rs b/style/src/theme.rs deleted file mode 100644 index 0b56e101..00000000 --- a/style/src/theme.rs +++ /dev/null @@ -1,1523 +0,0 @@ -//! Use the built-in theme and styles. -pub mod palette; - -pub use palette::Palette; - -use crate::application; -use crate::button; -use crate::checkbox; -use crate::container; -use crate::core::widget::text; -use crate::menu; -use crate::pane_grid; -use crate::pick_list; -use crate::progress_bar; -use crate::qr_code; -use crate::radio; -use crate::rule; -use crate::scrollable; -use crate::slider; -use crate::svg; -use crate::text_editor; -use crate::text_input; -use crate::toggler; - -use crate::core::{Background, Border, Color, Shadow, Vector}; - -use std::fmt; -use std::rc::Rc; -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) - } -} - -/// The style of an application. -#[derive(Default)] -pub enum Application { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn application::StyleSheet<Style = Theme>>), -} - -impl Application { - /// Creates a custom [`Application`] style. - pub fn custom( - custom: impl application::StyleSheet<Style = Theme> + 'static, - ) -> Self { - Self::Custom(Box::new(custom)) - } -} - -impl application::StyleSheet for Theme { - type Style = Application; - - fn appearance(&self, style: &Self::Style) -> application::Appearance { - let palette = self.extended_palette(); - - match style { - Application::Default => application::Appearance { - background_color: palette.background.base.color, - text_color: palette.background.base.text, - }, - Application::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> application::Appearance> application::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> application::Appearance { - (self)(style) - } -} - -/// The style of a button. -#[derive(Default)] -pub enum Button { - /// The primary style. - #[default] - Primary, - /// The secondary style. - Secondary, - /// The positive style. - Positive, - /// The destructive style. - Destructive, - /// The text style. - /// - /// Useful for links! - Text, - /// A custom style. - Custom(Box<dyn button::StyleSheet<Style = Theme>>), -} - -impl Button { - /// Creates a custom [`Button`] style variant. - pub fn custom( - style_sheet: impl button::StyleSheet<Style = Theme> + 'static, - ) -> Self { - Self::Custom(Box::new(style_sheet)) - } -} - -impl button::StyleSheet for Theme { - type Style = Button; - - fn active(&self, style: &Self::Style) -> button::Appearance { - let palette = self.extended_palette(); - - let appearance = button::Appearance { - border: Border::with_radius(2), - ..button::Appearance::default() - }; - - let from_pair = |pair: palette::Pair| button::Appearance { - background: Some(pair.color.into()), - text_color: pair.text, - ..appearance - }; - - match style { - Button::Primary => from_pair(palette.primary.strong), - Button::Secondary => from_pair(palette.secondary.base), - Button::Positive => from_pair(palette.success.base), - Button::Destructive => from_pair(palette.danger.base), - Button::Text => button::Appearance { - text_color: palette.background.base.text, - ..appearance - }, - Button::Custom(custom) => custom.active(self), - } - } - - fn hovered(&self, style: &Self::Style) -> button::Appearance { - let palette = self.extended_palette(); - - if let Button::Custom(custom) = style { - return custom.hovered(self); - } - - let active = self.active(style); - - let background = match style { - Button::Primary => Some(palette.primary.base.color), - Button::Secondary => Some(palette.background.strong.color), - Button::Positive => Some(palette.success.strong.color), - Button::Destructive => Some(palette.danger.strong.color), - Button::Text | Button::Custom(_) => None, - }; - - button::Appearance { - background: background.map(Background::from), - ..active - } - } - - fn pressed(&self, style: &Self::Style) -> button::Appearance { - if let Button::Custom(custom) = style { - return custom.pressed(self); - } - - button::Appearance { - shadow_offset: Vector::default(), - ..self.active(style) - } - } - - fn disabled(&self, style: &Self::Style) -> button::Appearance { - if let Button::Custom(custom) = style { - return custom.disabled(self); - } - - let active = self.active(style); - - button::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 - } - } -} - -/// The style of a checkbox. -#[derive(Default)] -pub enum Checkbox { - /// The primary style. - #[default] - Primary, - /// The secondary style. - Secondary, - /// The success style. - Success, - /// The danger style. - Danger, - /// A custom style. - Custom(Box<dyn checkbox::StyleSheet<Style = Theme>>), -} - -impl checkbox::StyleSheet for Theme { - type Style = Checkbox; - - fn active( - &self, - style: &Self::Style, - is_checked: bool, - ) -> checkbox::Appearance { - let palette = self.extended_palette(); - - match style { - Checkbox::Primary => checkbox_appearance( - palette.primary.strong.text, - palette.background.base, - palette.primary.strong, - is_checked, - ), - Checkbox::Secondary => checkbox_appearance( - palette.background.base.text, - palette.background.base, - palette.background.strong, - is_checked, - ), - Checkbox::Success => checkbox_appearance( - palette.success.base.text, - palette.background.base, - palette.success.base, - is_checked, - ), - Checkbox::Danger => checkbox_appearance( - palette.danger.base.text, - palette.background.base, - palette.danger.base, - is_checked, - ), - Checkbox::Custom(custom) => custom.active(self, is_checked), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_checked: bool, - ) -> checkbox::Appearance { - let palette = self.extended_palette(); - - match style { - Checkbox::Primary => checkbox_appearance( - palette.primary.strong.text, - palette.background.weak, - palette.primary.base, - is_checked, - ), - Checkbox::Secondary => checkbox_appearance( - palette.background.base.text, - palette.background.weak, - palette.background.strong, - is_checked, - ), - Checkbox::Success => checkbox_appearance( - palette.success.base.text, - palette.background.weak, - palette.success.base, - is_checked, - ), - Checkbox::Danger => checkbox_appearance( - palette.danger.base.text, - palette.background.weak, - palette.danger.base, - is_checked, - ), - Checkbox::Custom(custom) => custom.hovered(self, is_checked), - } - } - - fn disabled( - &self, - style: &Self::Style, - is_checked: bool, - ) -> checkbox::Appearance { - let palette = self.extended_palette(); - - match style { - Checkbox::Primary => checkbox_appearance( - palette.primary.strong.text, - palette.background.weak, - palette.background.strong, - is_checked, - ), - Checkbox::Secondary => checkbox_appearance( - palette.background.strong.color, - palette.background.weak, - palette.background.weak, - is_checked, - ), - Checkbox::Success => checkbox_appearance( - palette.success.base.text, - palette.background.weak, - palette.success.weak, - is_checked, - ), - Checkbox::Danger => checkbox_appearance( - palette.danger.base.text, - palette.background.weak, - palette.danger.weak, - is_checked, - ), - Checkbox::Custom(custom) => custom.active(self, is_checked), - } - } -} - -fn checkbox_appearance( - icon_color: Color, - base: palette::Pair, - accent: palette::Pair, - is_checked: bool, -) -> checkbox::Appearance { - checkbox::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, - } -} - -/// The style of a container. -#[derive(Default)] -pub enum Container { - /// No style. - #[default] - Transparent, - /// A simple box. - Box, - /// A custom style. - Custom(Box<dyn container::StyleSheet<Style = Theme>>), -} - -impl From<container::Appearance> for Container { - fn from(appearance: container::Appearance) -> Self { - Self::Custom(Box::new(move |_: &_| appearance)) - } -} - -impl<T: Fn(&Theme) -> container::Appearance + 'static> From<T> for Container { - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl container::StyleSheet for Theme { - type Style = Container; - - fn appearance(&self, style: &Self::Style) -> container::Appearance { - match style { - Container::Transparent => container::Appearance::default(), - Container::Box => { - let palette = self.extended_palette(); - - container::Appearance { - text_color: None, - background: Some(palette.background.weak.color.into()), - border: Border::with_radius(2), - shadow: Shadow::default(), - } - } - Container::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> container::Appearance> container::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> container::Appearance { - (self)(style) - } -} - -/// The style of a slider. -#[derive(Default)] -pub enum Slider { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn slider::StyleSheet<Style = Theme>>), -} - -impl slider::StyleSheet for Theme { - type Style = Slider; - - fn active(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let palette = self.extended_palette(); - - let handle = slider::Handle { - shape: slider::HandleShape::Rectangle { - width: 8, - border_radius: 4.0.into(), - }, - color: Color::WHITE, - border_color: Color::WHITE, - border_width: 1.0, - }; - - slider::Appearance { - rail: slider::Rail { - colors: ( - palette.primary.base.color, - palette.secondary.base.color, - ), - width: 4.0, - border_radius: 2.0.into(), - }, - handle: slider::Handle { - color: palette.background.base.color, - border_color: palette.primary.base.color, - ..handle - }, - } - } - Slider::Custom(custom) => custom.active(self), - } - } - - fn hovered(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let active = self.active(style); - let palette = self.extended_palette(); - - slider::Appearance { - handle: slider::Handle { - color: palette.primary.weak.color, - ..active.handle - }, - ..active - } - } - Slider::Custom(custom) => custom.hovered(self), - } - } - - fn dragging(&self, style: &Self::Style) -> slider::Appearance { - match style { - Slider::Default => { - let active = self.active(style); - let palette = self.extended_palette(); - - slider::Appearance { - handle: slider::Handle { - color: palette.primary.base.color, - ..active.handle - }, - ..active - } - } - Slider::Custom(custom) => custom.dragging(self), - } - } -} - -/// The style of a menu. -#[derive(Clone, Default)] -pub enum Menu { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Rc<dyn menu::StyleSheet<Style = Theme>>), -} - -impl menu::StyleSheet for Theme { - type Style = Menu; - - fn appearance(&self, style: &Self::Style) -> menu::Appearance { - match style { - Menu::Default => { - let palette = self.extended_palette(); - - menu::Appearance { - text_color: palette.background.weak.text, - background: palette.background.weak.color.into(), - border: Border { - width: 1.0, - radius: 0.0.into(), - color: palette.background.strong.color, - }, - selected_text_color: palette.primary.strong.text, - selected_background: palette.primary.strong.color.into(), - } - } - Menu::Custom(custom) => custom.appearance(self), - } - } -} - -impl From<PickList> for Menu { - fn from(pick_list: PickList) -> Self { - match pick_list { - PickList::Default => Self::Default, - PickList::Custom(_, menu) => Self::Custom(menu), - } - } -} - -/// The style of a pick list. -#[derive(Clone, Default)] -pub enum PickList { - /// The default style. - #[default] - Default, - /// A custom style. - Custom( - Rc<dyn pick_list::StyleSheet<Style = Theme>>, - Rc<dyn menu::StyleSheet<Style = Theme>>, - ), -} - -impl pick_list::StyleSheet for Theme { - type Style = PickList; - - fn active(&self, style: &Self::Style) -> pick_list::Appearance { - match style { - PickList::Default => { - let palette = self.extended_palette(); - - pick_list::Appearance { - text_color: palette.background.weak.text, - background: palette.background.weak.color.into(), - placeholder_color: palette.background.strong.color, - handle_color: palette.background.weak.text, - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - } - } - PickList::Custom(custom, _) => custom.active(self), - } - } - - fn hovered(&self, style: &Self::Style) -> pick_list::Appearance { - match style { - PickList::Default => { - let palette = self.extended_palette(); - - pick_list::Appearance { - text_color: palette.background.weak.text, - background: palette.background.weak.color.into(), - placeholder_color: palette.background.strong.color, - handle_color: palette.background.weak.text, - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.primary.strong.color, - }, - } - } - PickList::Custom(custom, _) => custom.hovered(self), - } - } -} - -/// The style of a radio button. -#[derive(Default)] -pub enum Radio { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn radio::StyleSheet<Style = Theme>>), -} - -impl radio::StyleSheet for Theme { - type Style = Radio; - - fn active( - &self, - style: &Self::Style, - is_selected: bool, - ) -> radio::Appearance { - match style { - Radio::Default => { - let palette = self.extended_palette(); - - radio::Appearance { - background: Color::TRANSPARENT.into(), - dot_color: palette.primary.strong.color, - border_width: 1.0, - border_color: palette.primary.strong.color, - text_color: None, - } - } - Radio::Custom(custom) => custom.active(self, is_selected), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_selected: bool, - ) -> radio::Appearance { - match style { - Radio::Default => { - let active = self.active(style, is_selected); - let palette = self.extended_palette(); - - radio::Appearance { - dot_color: palette.primary.strong.color, - background: palette.primary.weak.color.into(), - ..active - } - } - Radio::Custom(custom) => custom.hovered(self, is_selected), - } - } -} - -/// The style of a toggler. -#[derive(Default)] -pub enum Toggler { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn toggler::StyleSheet<Style = Theme>>), -} - -impl toggler::StyleSheet for Theme { - type Style = Toggler; - - fn active( - &self, - style: &Self::Style, - is_active: bool, - ) -> toggler::Appearance { - match style { - Toggler::Default => { - let palette = self.extended_palette(); - - toggler::Appearance { - background: if is_active { - palette.primary.strong.color - } else { - palette.background.strong.color - }, - background_border_width: 0.0, - background_border_color: Color::TRANSPARENT, - foreground: if is_active { - palette.primary.strong.text - } else { - palette.background.base.color - }, - foreground_border_width: 0.0, - foreground_border_color: Color::TRANSPARENT, - } - } - Toggler::Custom(custom) => custom.active(self, is_active), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_active: bool, - ) -> toggler::Appearance { - match style { - Toggler::Default => { - let palette = self.extended_palette(); - - toggler::Appearance { - foreground: if is_active { - Color { - a: 0.5, - ..palette.primary.strong.text - } - } else { - palette.background.weak.color - }, - ..self.active(style, is_active) - } - } - Toggler::Custom(custom) => custom.hovered(self, is_active), - } - } -} - -/// The style of a pane grid. -#[derive(Default)] -pub enum PaneGrid { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn pane_grid::StyleSheet<Style = Theme>>), -} - -impl pane_grid::StyleSheet for Theme { - type Style = PaneGrid; - - fn hovered_region(&self, style: &Self::Style) -> pane_grid::Appearance { - match style { - PaneGrid::Default => { - let palette = self.extended_palette(); - - pane_grid::Appearance { - background: Background::Color(Color { - a: 0.5, - ..palette.primary.base.color - }), - border: Border { - width: 2.0, - color: palette.primary.strong.color, - radius: 0.0.into(), - }, - } - } - PaneGrid::Custom(custom) => custom.hovered_region(self), - } - } - - fn picked_split(&self, style: &Self::Style) -> Option<pane_grid::Line> { - match style { - PaneGrid::Default => { - let palette = self.extended_palette(); - - Some(pane_grid::Line { - color: palette.primary.strong.color, - width: 2.0, - }) - } - PaneGrid::Custom(custom) => custom.picked_split(self), - } - } - - fn hovered_split(&self, style: &Self::Style) -> Option<pane_grid::Line> { - match style { - PaneGrid::Default => { - let palette = self.extended_palette(); - - Some(pane_grid::Line { - color: palette.primary.base.color, - width: 2.0, - }) - } - PaneGrid::Custom(custom) => custom.hovered_split(self), - } - } -} - -/// The style of a progress bar. -#[derive(Default)] -pub enum ProgressBar { - /// The primary style. - #[default] - Primary, - /// The success style. - Success, - /// The danger style. - Danger, - /// A custom style. - Custom(Box<dyn progress_bar::StyleSheet<Style = Theme>>), -} - -impl<T: Fn(&Theme) -> progress_bar::Appearance + 'static> From<T> - for ProgressBar -{ - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl progress_bar::StyleSheet for Theme { - type Style = ProgressBar; - - fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { - if let ProgressBar::Custom(custom) = style { - return custom.appearance(self); - } - - let palette = self.extended_palette(); - - let from_palette = |bar: Color| progress_bar::Appearance { - background: palette.background.strong.color.into(), - bar: bar.into(), - border_radius: 2.0.into(), - }; - - match style { - ProgressBar::Primary => from_palette(palette.primary.base.color), - ProgressBar::Success => from_palette(palette.success.base.color), - ProgressBar::Danger => from_palette(palette.danger.base.color), - ProgressBar::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> progress_bar::Appearance> progress_bar::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { - (self)(style) - } -} - -/// The style of a QR Code. -#[derive(Default)] -pub enum QRCode { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn qr_code::StyleSheet<Style = Theme>>), -} - -impl<T: Fn(&Theme) -> qr_code::Appearance + 'static> From<T> for QRCode { - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl qr_code::StyleSheet for Theme { - type Style = QRCode; - - fn appearance(&self, style: &Self::Style) -> qr_code::Appearance { - let palette = self.palette(); - - match style { - QRCode::Default => qr_code::Appearance { - cell: palette.text, - background: palette.background, - }, - QRCode::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> qr_code::Appearance> qr_code::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> qr_code::Appearance { - (self)(style) - } -} - -/// The style of a rule. -#[derive(Default)] -pub enum Rule { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn rule::StyleSheet<Style = Theme>>), -} - -impl<T: Fn(&Theme) -> rule::Appearance + 'static> From<T> for Rule { - fn from(f: T) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl rule::StyleSheet for Theme { - type Style = Rule; - - fn appearance(&self, style: &Self::Style) -> rule::Appearance { - let palette = self.extended_palette(); - - match style { - Rule::Default => rule::Appearance { - color: palette.background.strong.color, - width: 1, - radius: 0.0.into(), - fill_mode: rule::FillMode::Full, - }, - Rule::Custom(custom) => custom.appearance(self), - } - } -} - -impl<T: Fn(&Theme) -> rule::Appearance> rule::StyleSheet for T { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> rule::Appearance { - (self)(style) - } -} - -/** - * Svg - */ -#[derive(Default)] -pub enum Svg { - /// No filtering to the rendered SVG. - #[default] - Default, - /// A custom style. - Custom(Box<dyn svg::StyleSheet<Style = Theme>>), -} - -impl Svg { - /// Creates a custom [`Svg`] style. - pub fn custom_fn(f: fn(&Theme) -> svg::Appearance) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl svg::StyleSheet for Theme { - type Style = Svg; - - fn appearance(&self, style: &Self::Style) -> svg::Appearance { - match style { - Svg::Default => svg::Appearance::default(), - Svg::Custom(custom) => custom.appearance(self), - } - } - - fn hovered(&self, style: &Self::Style) -> svg::Appearance { - self.appearance(style) - } -} - -impl svg::StyleSheet for fn(&Theme) -> svg::Appearance { - type Style = Theme; - - fn appearance(&self, style: &Self::Style) -> svg::Appearance { - (self)(style) - } - - fn hovered(&self, style: &Self::Style) -> svg::Appearance { - self.appearance(style) - } -} - -/// The style of a scrollable. -#[derive(Default)] -pub enum Scrollable { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn scrollable::StyleSheet<Style = Theme>>), -} - -impl Scrollable { - /// Creates a custom [`Scrollable`] theme. - pub fn custom<T: scrollable::StyleSheet<Style = Theme> + 'static>( - style: T, - ) -> Self { - Self::Custom(Box::new(style)) - } -} - -impl scrollable::StyleSheet for Theme { - type Style = Scrollable; - - fn active(&self, style: &Self::Style) -> scrollable::Appearance { - match style { - Scrollable::Default => { - let palette = self.extended_palette(); - - scrollable::Appearance { - container: container::Appearance::default(), - scrollbar: scrollable::Scrollbar { - background: Some(palette.background.weak.color.into()), - border: Border::with_radius(2), - scroller: scrollable::Scroller { - color: palette.background.strong.color, - border: Border::with_radius(2), - }, - }, - gap: None, - } - } - Scrollable::Custom(custom) => custom.active(self), - } - } - - fn hovered( - &self, - style: &Self::Style, - is_mouse_over_scrollbar: bool, - ) -> scrollable::Appearance { - match style { - Scrollable::Default => { - if is_mouse_over_scrollbar { - let palette = self.extended_palette(); - - scrollable::Appearance { - scrollbar: scrollable::Scrollbar { - background: Some( - palette.background.weak.color.into(), - ), - border: Border::with_radius(2), - scroller: scrollable::Scroller { - color: palette.primary.strong.color, - border: Border::with_radius(2), - }, - }, - ..self.active(style) - } - } else { - self.active(style) - } - } - Scrollable::Custom(custom) => { - custom.hovered(self, is_mouse_over_scrollbar) - } - } - } - - fn dragging(&self, style: &Self::Style) -> scrollable::Appearance { - match style { - Scrollable::Default => self.hovered(style, true), - Scrollable::Custom(custom) => custom.dragging(self), - } - } -} - -/// The style of text. -#[derive(Clone, Copy, Default)] -pub enum Text { - /// The default style. - #[default] - Default, - /// Colored text. - Color(Color), -} - -impl From<Color> for Text { - fn from(color: Color) -> Self { - Text::Color(color) - } -} - -impl text::StyleSheet for Theme { - type Style = Text; - - fn appearance(&self, style: Self::Style) -> text::Appearance { - match style { - Text::Default => text::Appearance::default(), - Text::Color(c) => text::Appearance { color: Some(c) }, - } - } -} - -/// The style of a text input. -#[derive(Default)] -pub enum TextInput { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn text_input::StyleSheet<Style = Theme>>), -} - -impl text_input::StyleSheet for Theme { - type Style = TextInput; - - fn active(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.active(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - icon_color: palette.background.weak.text, - } - } - - fn hovered(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.hovered(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.base.text, - }, - icon_color: palette.background.weak.text, - } - } - - fn focused(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.focused(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.primary.strong.color, - }, - icon_color: palette.background.weak.text, - } - } - - fn placeholder_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.placeholder_color(self); - } - - let palette = self.extended_palette(); - - palette.background.strong.color - } - - fn value_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.value_color(self); - } - - let palette = self.extended_palette(); - - palette.background.base.text - } - - fn selection_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.selection_color(self); - } - - let palette = self.extended_palette(); - - palette.primary.weak.color - } - - fn disabled(&self, style: &Self::Style) -> text_input::Appearance { - if let TextInput::Custom(custom) = style { - return custom.disabled(self); - } - - let palette = self.extended_palette(); - - text_input::Appearance { - background: palette.background.weak.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - icon_color: palette.background.strong.color, - } - } - - fn disabled_color(&self, style: &Self::Style) -> Color { - if let TextInput::Custom(custom) = style { - return custom.disabled_color(self); - } - - self.placeholder_color(style) - } -} - -/// The style of a text input. -#[derive(Default)] -pub enum TextEditor { - /// The default style. - #[default] - Default, - /// A custom style. - Custom(Box<dyn text_editor::StyleSheet<Style = Theme>>), -} - -impl text_editor::StyleSheet for Theme { - type Style = TextEditor; - - fn active(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.active(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - } - } - - fn hovered(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.hovered(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.base.text, - }, - } - } - - fn focused(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.focused(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.base.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.primary.strong.color, - }, - } - } - - fn placeholder_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.placeholder_color(self); - } - - let palette = self.extended_palette(); - - palette.background.strong.color - } - - fn value_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.value_color(self); - } - - let palette = self.extended_palette(); - - palette.background.base.text - } - - fn selection_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.selection_color(self); - } - - let palette = self.extended_palette(); - - palette.primary.weak.color - } - - fn disabled(&self, style: &Self::Style) -> text_editor::Appearance { - if let TextEditor::Custom(custom) = style { - return custom.disabled(self); - } - - let palette = self.extended_palette(); - - text_editor::Appearance { - background: palette.background.weak.color.into(), - border: Border { - radius: 2.0.into(), - width: 1.0, - color: palette.background.strong.color, - }, - } - } - - fn disabled_color(&self, style: &Self::Style) -> Color { - if let TextEditor::Custom(custom) = style { - return custom.disabled_color(self); - } - - self.placeholder_color(style) - } -} diff --git a/style/src/toggler.rs b/style/src/toggler.rs deleted file mode 100644 index 731e87ce..00000000 --- a/style/src/toggler.rs +++ /dev/null @@ -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; -} diff --git a/wgpu/src/color.rs b/wgpu/src/color.rs index 4598b0a6..890f3f89 100644 --- a/wgpu/src/color.rs +++ b/wgpu/src/color.rs @@ -158,10 +158,3 @@ pub fn convert( texture } - -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -#[repr(C)] -struct Vertex { - ndc: [f32; 2], - uv: [f32; 2], -} diff --git a/widget/Cargo.toml b/widget/Cargo.toml index e8e363c4..3c9ffddb 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -25,7 +25,6 @@ wgpu = ["iced_renderer/wgpu"] [dependencies] iced_renderer.workspace = true iced_runtime.workspace = true -iced_style.workspace = true num-traits.workspace = true thiserror.workspace = true diff --git a/widget/src/button.rs b/widget/src/button.rs index 867fbfaf..e265aa1f 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -1,26 +1,22 @@ //! 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::layout; use crate::core::mouse; use crate::core::overlay; use crate::core::renderer; +use crate::core::theme::palette; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Rectangle, - Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Padding, + Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; -pub use crate::style::button::{Appearance, StyleSheet}; - /// A generic widget that produces a message when pressed. /// /// ```no_run -/// # type Button<'a, Message> = -/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; /// # /// #[derive(Clone)] /// enum Message { @@ -34,8 +30,7 @@ pub use crate::style::button::{Appearance, StyleSheet}; /// be disabled: /// /// ``` -/// # type Button<'a, Message> = -/// # iced_widget::Button<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Button<'a, Message> = iced_widget::Button<'a, Message>; /// # /// #[derive(Clone)] /// enum Message { @@ -53,7 +48,6 @@ pub use crate::style::button::{Appearance, StyleSheet}; #[allow(missing_debug_implementations)] pub struct Button<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -62,18 +56,20 @@ where height: Length, padding: Padding, clip: bool, - style: Theme::Style, + style: Style<Theme>, } impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { /// Creates a new [`Button`] with the given content. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, - ) -> Self { + ) -> Self + where + Theme: DefaultStyle, + { let content = content.into(); let size = content.as_widget().size_hint(); @@ -82,9 +78,9 @@ where on_press: None, width: size.width.fluid(), height: size.height.fluid(), - padding: Padding::new(5.0), + padding: DEFAULT_PADDING, clip: false, - style: Theme::Style::default(), + style: Theme::default_style(), } } @@ -124,8 +120,8 @@ where } /// Sets the style variant of this [`Button`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { + self.style = style; 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> for Button<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: StyleSheet, Renderer: 'a + crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -149,7 +149,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn children(&self) -> Vec<Tree> { @@ -173,13 +173,19 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout(limits, self.width, self.height, self.padding, |limits| { - self.content.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ) - }) + layout::padded( + limits, + self.width, + self.height, + self.padding, + |limits| { + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + limits, + ) + }, + ) } fn operate( @@ -223,9 +229,48 @@ where return event::Status::Captured; } - update(event, layout, cursor, shell, &self.on_press, || { - tree.state.downcast_mut::<State>() - }) + match event { + 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( @@ -240,16 +285,39 @@ where ) { let bounds = layout.bounds(); let content_layout = layout.children().next().unwrap(); + let is_mouse_over = cursor.is_over(bounds); - let styling = draw( - renderer, - bounds, - cursor, - self.on_press.is_some(), - theme, - &self.style, - || tree.state.downcast_ref::<State>(), - ); + let status = if self.on_press.is_none() { + Status::Disabled + } else if is_mouse_over { + let state = tree.state.downcast_ref::<State>(); + + if state.is_pressed { + Status::Pressed + } 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 { bounds.intersection(viewport).unwrap_or(*viewport) @@ -278,7 +346,13 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> 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>( @@ -301,7 +375,7 @@ impl<'a, Message, Theme, Renderer> From<Button<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: Clone + 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: crate::core::Renderer + 'a, { fn from(button: Button<'a, Message, Theme, Renderer>) -> Self { @@ -309,143 +383,182 @@ where } } -/// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { - is_pressed: bool, +/// The default [`Padding`] of a [`Button`]. +pub(crate) const DEFAULT_PADDING: Padding = Padding { + top: 5.0, + 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 { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() +/// The appearance of a button. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// 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`] -/// accordingly. -pub fn update<'a, Message: Clone>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - 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; - } - } +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + background: None, + text_color: Color::BLACK, + border: Border::default(), + shadow: Shadow::default(), } - 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; +/// The style of a [`Button`]. +pub type Style<Theme> = fn(&Theme, Status) -> Appearance; - let bounds = layout.bounds(); +/// The default style of a [`Button`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Button`]. + fn default_style() -> Style<Self>; +} - if cursor.is_over(bounds) { - shell.publish(on_press); - } +impl DefaultStyle for Theme { + fn default_style() -> Style<Self> { + primary + } +} - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = state(); +impl DefaultStyle for Appearance { + fn default_style() -> Style<Self> { + |appearance, _status| *appearance + } +} - state.is_pressed = false; - } - _ => {} +impl DefaultStyle for Color { + fn default_style() -> Style<Self> { + |color, _status| Appearance::default().with_background(*color) } +} - event::Status::Ignored +/// 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), + } } -/// Draws a [`Button`]. -pub fn draw<'a, Theme, Renderer: crate::core::Renderer>( - 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); +/// 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), + } +} - let styling = if !is_enabled { - theme.disabled(style) - } else if is_mouse_over { - let state = state(); +/// 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), + } +} - if state.is_pressed { - theme.pressed(style) - } else { - theme.hovered(style) - } - } else { - theme.active(style) +/// 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() - || 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)), - ); + match status { + Status::Active | Status::Pressed => base, + Status::Hovered => Appearance { + text_color: palette.background.base.text.scale_alpha(0.8), + ..base + }, + Status::Disabled => disabled(base), } - - styling } -/// Computes the layout of a [`Button`]. -pub fn layout( - limits: &layout::Limits, - width: Length, - height: Length, - padding: Padding, - layout_content: impl FnOnce(&layout::Limits) -> layout::Node, -) -> layout::Node { - layout::padded(limits, width, height, padding, layout_content) +fn styled(pair: palette::Pair) -> Appearance { + Appearance { + background: Some(Background::Color(pair.color)), + text_color: pair.text, + border: Border::rounded(2), + ..Appearance::default() + } } -/// 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() +fn disabled(appearance: Appearance) -> Appearance { + Appearance { + background: appearance + .background + .map(|background| background.scale_alpha(0.5)), + text_color: appearance.text_color.scale_alpha(0.5), + ..appearance } } diff --git a/widget/src/checkbox.rs b/widget/src/checkbox.rs index 0ff4d58b..f0c7357b 100644 --- a/widget/src/checkbox.rs +++ b/widget/src/checkbox.rs @@ -5,22 +5,21 @@ use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::text; +use crate::core::theme::palette; use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; 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. /// /// # Example /// /// ```no_run -/// # type Checkbox<'a, Message> = -/// # iced_widget::Checkbox<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Checkbox<'a, Message> = iced_widget::Checkbox<'a, Message>; /// # /// pub enum Message { /// CheckboxToggled(bool), @@ -39,7 +38,6 @@ pub struct Checkbox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { is_checked: bool, @@ -53,26 +51,28 @@ pub struct Checkbox< text_shaping: text::Shaping, font: Option<Renderer::Font>, icon: Icon<Renderer::Font>, - style: <Theme as StyleSheet>::Style, + style: Style<Theme>, } impl<'a, Message, Theme, Renderer> Checkbox<'a, Message, Theme, Renderer> where Renderer: text::Renderer, - Theme: StyleSheet + crate::text::StyleSheet, { /// The default size of a [`Checkbox`]. - const DEFAULT_SIZE: f32 = 20.0; + const DEFAULT_SIZE: f32 = 16.0; /// The default spacing of a [`Checkbox`]. - const DEFAULT_SPACING: f32 = 10.0; + const DEFAULT_SPACING: f32 = 8.0; /// Creates a new [`Checkbox`]. /// /// It expects: /// * the label of the [`Checkbox`] /// * 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 { is_checked, on_toggle: None, @@ -91,7 +91,7 @@ where line_height: text::LineHeight::default(), shaping: text::Shaping::Basic, }, - style: Default::default(), + style: Theme::default_style(), } } @@ -174,11 +174,8 @@ where } /// Sets the style of the [`Checkbox`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { + self.style = style; self } } @@ -186,7 +183,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Checkbox<'a, Message, Theme, Renderer> where - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -293,17 +289,20 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); let is_disabled = self.on_toggle.is_none(); + let is_checked = self.is_checked; let mut children = layout.children(); - let custom_style = if is_disabled { - theme.disabled(&self.style, self.is_checked) + let status = if is_disabled { + Status::Disabled { is_checked } } else if is_mouse_over { - theme.hovered(&self.style, self.is_checked) + Status::Hovered { is_checked } } else { - theme.active(&self.style, self.is_checked) + Status::Active { is_checked } }; + let appearance = (self.style)(theme, status); + { let layout = children.next().unwrap(); let bounds = layout.bounds(); @@ -311,10 +310,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: custom_style.border, + border: appearance.border, ..renderer::Quad::default() }, - custom_style.background, + appearance.background, ); let Icon { @@ -339,7 +338,7 @@ where shaping: *shaping, }, bounds.center(), - custom_style.icon_color, + appearance.icon_color, *viewport, ); } @@ -354,7 +353,7 @@ where label_layout, tree.state.downcast_ref(), crate::text::Appearance { - color: custom_style.text_color, + color: appearance.text_color, }, viewport, ); @@ -366,7 +365,7 @@ impl<'a, Message, Theme, Renderer> From<Checkbox<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a + StyleSheet + crate::text::StyleSheet, + Theme: 'a, Renderer: 'a + text::Renderer, { fn from( @@ -390,3 +389,183 @@ pub struct Icon<Font> { /// The shaping strategy of the icon. 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, + } +} diff --git a/widget/src/combo_box.rs b/widget/src/combo_box.rs index e3862174..bddf2789 100644 --- a/widget/src/combo_box.rs +++ b/widget/src/combo_box.rs @@ -10,11 +10,11 @@ use crate::core::text; use crate::core::time::Instant; use crate::core::widget::{self, Widget}; 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::text::LineHeight; -use crate::{container, scrollable, text_input, TextInput}; +use crate::text_input::{self, TextInput}; use std::cell::RefCell; use std::fmt::Display; @@ -32,7 +32,6 @@ pub struct ComboBox< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: text_input::StyleSheet + menu::StyleSheet, Renderer: text::Renderer, { state: &'a State<T>, @@ -43,7 +42,7 @@ pub struct ComboBox< on_option_hovered: Option<Box<dyn Fn(T) -> Message>>, on_close: Option<Message>, on_input: Option<Box<dyn Fn(String) -> Message>>, - menu_style: <Theme as menu::StyleSheet>::Style, + menu_style: menu::Style<Theme>, padding: Padding, size: Option<f32>, } @@ -51,7 +50,6 @@ pub struct ComboBox< impl<'a, T, Message, Theme, Renderer> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, - Theme: text_input::StyleSheet + menu::StyleSheet, Renderer: text::Renderer, { /// Creates a new [`ComboBox`] with the given list of options, a placeholder, @@ -62,9 +60,18 @@ where placeholder: &str, selection: Option<&T>, on_selected: impl Fn(T) -> Message + 'static, - ) -> Self { - let text_input = TextInput::new(placeholder, &state.value()) - .on_input(TextInputEvent::TextChanged); + ) -> Self + where + 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(); @@ -77,7 +84,7 @@ where on_option_hovered: None, on_input: None, on_close: None, - menu_style: Default::default(), + menu_style: style.menu, padding: text_input::DEFAULT_PADDING, size: None, } @@ -118,24 +125,11 @@ where } /// Sets the style of the [`ComboBox`]. - // TODO: Define its own `StyleSheet` trait - pub fn style<S>(mut self, style: S) -> Self - 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 - } + pub fn style(mut self, style: impl Into<Style<Theme>>) -> Self { + let style = style.into(); - /// Sets the style of the [`TextInput`] of the [`ComboBox`]. - pub fn text_input_style<S>(mut self, style: S) -> Self - where - S: Into<<Theme as text_input::StyleSheet>::Style> + Clone, - { - self.text_input = self.text_input.style(style); + self.text_input = self.text_input.style(style.text_input); + self.menu_style = style.menu; self } @@ -299,10 +293,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone, - Theme: container::StyleSheet - + text_input::StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -679,7 +669,7 @@ where self.state.sync_filtered_options(filtered_options); - let mut menu = menu::Menu::new( + let mut menu = menu::Menu::with_style( menu, &filtered_options.options, hovered_option, @@ -693,10 +683,10 @@ where (self.on_selected)(x) }, self.on_option_hovered.as_deref(), + self.menu_style, ) .width(bounds.width) - .padding(self.padding) - .style(self.menu_style.clone()); + .padding(self.padding); if let Some(font) = self.font { menu = menu.font(font); @@ -719,11 +709,7 @@ impl<'a, T, Message, Theme, Renderer> where T: Display + Clone + 'static, Message: Clone + 'a, - Theme: container::StyleSheet - + text_input::StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from(combo_box: ComboBox<'a, T, Message, Theme, Renderer>) -> Self { @@ -731,8 +717,7 @@ where } } -/// Search list of options for a given query. -pub fn search<'a, T, A>( +fn search<'a, T, A>( options: impl IntoIterator<Item = T> + 'a, option_matchers: impl IntoIterator<Item = &'a A> + 'a, query: &'a str, @@ -759,8 +744,7 @@ where }) } -/// Build matchers from given list of options. -pub fn build_matchers<'a, T>( +fn build_matchers<'a, T>( options: impl IntoIterator<Item = T> + 'a, ) -> Vec<String> where @@ -775,3 +759,43 @@ where }) .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 + } +} diff --git a/widget/src/container.rs b/widget/src/container.rs index e0174177..81b9a29e 100644 --- a/widget/src/container.rs +++ b/widget/src/container.rs @@ -1,6 +1,7 @@ //! Decorate content and apply alignment. use crate::core::alignment::{self, Alignment}; use crate::core::event::{self, Event}; +use crate::core::gradient::{self, Gradient}; use crate::core::layout; use crate::core::mouse; use crate::core::overlay; @@ -8,13 +9,11 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::{self, Operation}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Padding, Pixels, - Point, Rectangle, Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Padding, + Pixels, Point, Rectangle, Shadow, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; -pub use iced_style::container::{Appearance, StyleSheet}; - /// An element decorating some content. /// /// It is normally used for alignment purposes. @@ -25,7 +24,6 @@ pub struct Container< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: crate::core::Renderer, { id: Option<Id>, @@ -36,21 +34,30 @@ pub struct Container< max_height: f32, horizontal_alignment: alignment::Horizontal, vertical_alignment: alignment::Vertical, - style: Theme::Style, clip: bool, content: Element<'a, Message, Theme, Renderer>, + style: Style<Theme>, } impl<'a, Message, Theme, Renderer> Container<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { - /// Creates an empty [`Container`]. - pub fn new<T>(content: T) -> Self + /// Creates a [`Container`] with the given content. + pub fn new( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + ) -> Self 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 size = content.as_widget().size_hint(); @@ -63,9 +70,9 @@ where max_height: f32::INFINITY, horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Top, - style: Default::default(), clip: false, content, + style, } } @@ -130,8 +137,8 @@ where } /// Sets the style of the [`Container`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { + self.style = style; self } @@ -146,7 +153,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Container<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -262,10 +268,18 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - let style = theme.appearance(&self.style); + let bounds = layout.bounds(); - if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { - draw_background(renderer, &style, layout.bounds()); + let status = if cursor.is_over(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( tree, @@ -307,7 +321,7 @@ impl<'a, Message, Theme, Renderer> From<Container<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a + StyleSheet, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { fn from( @@ -482,3 +496,121 @@ pub fn visible_bounds(id: Id) -> Command<Option<Rectangle>> { 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() + } +} diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index ed385ea5..75072d2e 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -7,7 +7,6 @@ use crate::core; use crate::core::widget::operation; use crate::core::{Element, Length, Pixels}; use crate::keyed; -use crate::overlay; use crate::pick_list::{self, PickList}; use crate::progress_bar::{self, ProgressBar}; use crate::radio::{self, Radio}; @@ -15,13 +14,13 @@ use crate::rule::{self, Rule}; use crate::runtime::Command; use crate::scrollable::{self, Scrollable}; use crate::slider::{self, Slider}; -use crate::style::application; -use crate::text::{self, Text}; +use crate::text::Text; use crate::text_editor::{self, TextEditor}; use crate::text_input::{self, TextInput}; use crate::toggler::{self, Toggler}; 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::ops::RangeInclusive; @@ -59,7 +58,7 @@ pub fn container<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Container<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, + Theme: container::DefaultStyle, Renderer: core::Renderer, { Container::new(content) @@ -105,7 +104,7 @@ pub fn scrollable<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Scrollable<'a, Message, Theme, Renderer> where - Theme: scrollable::StyleSheet, + Theme: scrollable::DefaultStyle, Renderer: core::Renderer, { Scrollable::new(content) @@ -118,8 +117,8 @@ pub fn button<'a, Message, Theme, Renderer>( content: impl Into<Element<'a, Message, Theme, Renderer>>, ) -> Button<'a, Message, Theme, Renderer> where + Theme: button::DefaultStyle, Renderer: core::Renderer, - Theme: button::StyleSheet, { Button::new(content) } @@ -135,7 +134,7 @@ pub fn tooltip<'a, Message, Theme, Renderer>( position: tooltip::Position, ) -> crate::Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + text::StyleSheet, + Theme: container::DefaultStyle, Renderer: core::text::Renderer, { Tooltip::new(content, tooltip, position) @@ -148,7 +147,6 @@ pub fn text<'a, Theme, Renderer>( text: impl ToString, ) -> Text<'a, Theme, Renderer> where - Theme: text::StyleSheet, Renderer: core::text::Renderer, { Text::new(text.to_string()) @@ -162,7 +160,7 @@ pub fn checkbox<'a, Message, Theme, Renderer>( is_checked: bool, ) -> Checkbox<'a, Message, Theme, Renderer> where - Theme: checkbox::StyleSheet + text::StyleSheet, + Theme: checkbox::DefaultStyle, Renderer: core::text::Renderer, { Checkbox::new(label, is_checked) @@ -179,7 +177,7 @@ pub fn radio<Message, Theme, Renderer, V>( ) -> Radio<Message, Theme, Renderer> where Message: Clone, - Theme: radio::StyleSheet, + Theme: radio::DefaultStyle, Renderer: core::text::Renderer, V: Copy + Eq, { @@ -195,8 +193,8 @@ pub fn toggler<'a, Message, Theme, Renderer>( f: impl Fn(bool) -> Message + 'a, ) -> Toggler<'a, Message, Theme, Renderer> where + Theme: toggler::DefaultStyle, Renderer: core::text::Renderer, - Theme: toggler::StyleSheet, { Toggler::new(label, is_checked, f) } @@ -210,7 +208,7 @@ pub fn text_input<'a, Message, Theme, Renderer>( ) -> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: text_input::StyleSheet, + Theme: text_input::DefaultStyle, Renderer: core::text::Renderer, { TextInput::new(placeholder, value) @@ -224,7 +222,7 @@ pub fn text_editor<Message, Theme, Renderer>( ) -> TextEditor<'_, core::text::highlighter::PlainText, Message, Theme, Renderer> where Message: Clone, - Theme: text_editor::StyleSheet, + Theme: text_editor::DefaultStyle, Renderer: core::text::Renderer, { TextEditor::new(content) @@ -241,7 +239,7 @@ pub fn slider<'a, T, Message, Theme>( where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: slider::StyleSheet, + Theme: slider::DefaultStyle, { Slider::new(range, value, on_change) } @@ -257,7 +255,7 @@ pub fn vertical_slider<'a, T, Message, Theme>( where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: slider::StyleSheet, + Theme: vertical_slider::DefaultStyle, { VerticalSlider::new(range, value, on_change) } @@ -275,13 +273,8 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone, + Theme: pick_list::DefaultStyle, 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) } @@ -297,7 +290,7 @@ pub fn combo_box<'a, T, Message, Theme, Renderer>( ) -> ComboBox<'a, T, Message, Theme, Renderer> where T: std::fmt::Display + Clone, - Theme: text_input::StyleSheet + overlay::menu::StyleSheet, + Theme: combo_box::DefaultStyle, Renderer: core::text::Renderer, { ComboBox::new(state, placeholder, selection, on_selected) @@ -324,7 +317,7 @@ pub fn vertical_space() -> Space { /// [`Rule`]: crate::Rule pub fn horizontal_rule<Theme>(height: impl Into<Pixels>) -> Rule<Theme> where - Theme: rule::StyleSheet, + Theme: rule::DefaultStyle, { Rule::horizontal(height) } @@ -334,7 +327,7 @@ where /// [`Rule`]: crate::Rule pub fn vertical_rule<Theme>(width: impl Into<Pixels>) -> Rule<Theme> where - Theme: rule::StyleSheet, + Theme: rule::DefaultStyle, { Rule::vertical(width) } @@ -351,7 +344,7 @@ pub fn progress_bar<Theme>( value: f32, ) -> ProgressBar<Theme> where - Theme: progress_bar::StyleSheet, + Theme: progress_bar::DefaultStyle, { ProgressBar::new(range, value) } @@ -371,7 +364,7 @@ pub fn image<Handle>(handle: impl Into<Handle>) -> crate::Image<Handle> { #[cfg(feature = "svg")] pub fn svg<Theme>(handle: impl Into<core::svg::Handle>) -> crate::Svg<Theme> where - Theme: crate::svg::StyleSheet, + Theme: crate::svg::DefaultStyle, { crate::Svg::new(handle) } @@ -397,7 +390,7 @@ where #[cfg(feature = "qr_code")] pub fn qr_code<Theme>(data: &crate::qr_code::Data) -> crate::QRCode<'_, Theme> where - Theme: crate::qr_code::StyleSheet, + Theme: crate::qr_code::DefaultStyle, { crate::QRCode::new(data) } @@ -440,13 +433,20 @@ where } /// A widget that applies any `Theme` to its contents. -pub fn themer<'a, Message, Theme, Renderer>( - theme: Theme, - content: impl Into<Element<'a, Message, Theme, Renderer>>, -) -> Themer<'a, Message, Theme, Renderer> +pub fn themer<'a, Message, OldTheme, NewTheme, Renderer>( + new_theme: NewTheme, + content: impl Into<Element<'a, Message, NewTheme, Renderer>>, +) -> Themer< + 'a, + Message, + OldTheme, + NewTheme, + impl Fn(&OldTheme) -> NewTheme, + Renderer, +> where Renderer: core::Renderer, - Theme: application::StyleSheet, + NewTheme: Clone, { - Themer::new(theme, content) + Themer::new(move |_| new_theme.clone(), content) } diff --git a/widget/src/lib.rs b/widget/src/lib.rs index cefafdbe..209dfad9 100644 --- a/widget/src/lib.rs +++ b/widget/src/lib.rs @@ -14,11 +14,11 @@ pub use iced_renderer as renderer; pub use iced_renderer::graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; -pub use iced_style as style; mod column; mod mouse_area; mod row; +mod space; mod themer; pub mod button; @@ -34,7 +34,6 @@ pub mod radio; pub mod rule; pub mod scrollable; pub mod slider; -pub mod space; pub mod text; pub mod text_editor; pub mod text_input; @@ -135,5 +134,5 @@ pub mod qr_code; #[doc(no_inline)] pub use qr_code::QRCode; +pub use crate::core::theme::{self, Theme}; pub use renderer::Renderer; -pub use style::theme::{self, Theme}; diff --git a/widget/src/overlay/menu.rs b/widget/src/overlay/menu.rs index 8a4d6a98..746407c6 100644 --- a/widget/src/overlay/menu.rs +++ b/widget/src/overlay/menu.rs @@ -10,13 +10,12 @@ use crate::core::text::{self, Text}; use crate::core::touch; use crate::core::widget::Tree; 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::scrollable::{self, Scrollable}; -pub use iced_style::menu::{Appearance, StyleSheet}; - /// A list of selectable options. #[allow(missing_debug_implementations)] pub struct Menu< @@ -26,7 +25,6 @@ pub struct Menu< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: text::Renderer, { state: &'a mut State, @@ -40,14 +38,14 @@ pub struct Menu< text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + style: Style<Theme>, } impl<'a, T, Message, Theme, Renderer> Menu<'a, T, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { /// Creates a new [`Menu`] with the given [`State`], a list of options, and @@ -58,6 +56,29 @@ where hovered_option: &'a mut Option<usize>, on_selected: impl FnMut(T) -> Message + 'a, 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 { Menu { state, @@ -71,7 +92,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Default::default(), + style, } } @@ -115,10 +136,7 @@ where } /// Sets the style of the [`Menu`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { + pub fn style(mut self, style: impl Into<Style<Theme>>) -> Self { self.style = style.into(); self } @@ -165,7 +183,6 @@ impl Default for State { struct Overlay<'a, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, Renderer: crate::core::Renderer, { position: Point, @@ -173,13 +190,13 @@ where container: Container<'a, Message, Theme, Renderer>, width: f32, target_height: f32, - style: <Theme as StyleSheet>::Style, + style: Style<Theme>, } impl<'a, Message, Theme, Renderer> Overlay<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + container::StyleSheet + scrollable::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { pub fn new<T>( @@ -205,18 +222,25 @@ where style, } = menu; - let container = Container::new(Scrollable::new(List { - options, - hovered_option, - on_selected, - on_option_hovered, - font, - text_size, - text_line_height, - text_shaping, - padding, - style: style.clone(), - })); + let container = Container::with_style( + Scrollable::with_direction_and_style( + List { + options, + hovered_option, + on_selected, + on_option_hovered, + font, + text_size, + text_line_height, + text_shaping, + padding, + style: style.list, + }, + scrollable::Direction::default(), + style.scrollable, + ), + container::transparent, + ); state.tree.diff(&container as &dyn Widget<_, _, _>); @@ -235,7 +259,6 @@ impl<'a, Message, Theme, Renderer> crate::core::Overlay<Message, Theme, Renderer> for Overlay<'a, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -302,9 +325,10 @@ where layout: Layout<'_>, cursor: mouse::Cursor, ) { - let appearance = StyleSheet::appearance(theme, &self.style); let bounds = layout.bounds(); + let appearance = (self.style.list)(theme); + renderer.fill_quad( renderer::Quad { bounds, @@ -321,7 +345,6 @@ where struct List<'a, T, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { options: &'a [T], @@ -333,14 +356,13 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + style: fn(&Theme) -> Appearance, } impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for List<'a, T, Message, Theme, Renderer> where T: Clone + ToString, - Theme: StyleSheet, Renderer: text::Renderer, { fn size(&self) -> Size<Length> { @@ -483,7 +505,7 @@ where _cursor: mouse::Cursor, viewport: &Rectangle, ) { - let appearance = theme.appearance(&self.style); + let appearance = (self.style)(theme); let bounds = layout.bounds(); let text_size = @@ -517,7 +539,7 @@ where width: bounds.width - appearance.border.width * 2.0, ..bounds }, - border: Border::with_radius(appearance.border.radius), + border: Border::rounded(appearance.border.radius), ..renderer::Quad::default() }, appearance.selected_background, @@ -553,10 +575,79 @@ impl<'a, T, Message, Theme, Renderer> where T: ToString + Clone, Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + text::Renderer, { fn from(list: List<'a, T, Message, Theme, Renderer>) -> Self { 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(), + } +} diff --git a/widget/src/pane_grid.rs b/widget/src/pane_grid.rs index 478a7024..d60d5e3b 100644 --- a/widget/src/pane_grid.rs +++ b/widget/src/pane_grid.rs @@ -30,9 +30,6 @@ pub use split::Split; pub use state::State; pub use title_bar::TitleBar; -pub use crate::style::pane_grid::{Appearance, Line, StyleSheet}; - -use crate::container; use crate::core::event::{self, Event}; use crate::core::layout; use crate::core::mouse; @@ -42,8 +39,8 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; const DRAG_DEADBAND_DISTANCE: f32 = 10.0; @@ -73,8 +70,7 @@ const THICKNESS_RATIO: f32 = 25.0; /// ```no_run /// # use iced_widget::{pane_grid, text}; /// # -/// # type PaneGrid<'a, Message> = -/// # iced_widget::PaneGrid<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type PaneGrid<'a, Message> = iced_widget::PaneGrid<'a, Message>; /// # /// enum PaneState { /// SomePane, @@ -105,7 +101,6 @@ pub struct PaneGrid< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet + container::StyleSheet, Renderer: crate::core::Renderer, { contents: Contents<'a, Content<'a, Message, Theme, Renderer>>, @@ -115,12 +110,11 @@ pub struct PaneGrid< on_click: Option<Box<dyn Fn(Pane) -> Message + 'a>>, on_drag: Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, on_resize: Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, - style: <Theme as StyleSheet>::Style, + style: Style<Theme>, } impl<'a, Message, Theme, Renderer> PaneGrid<'a, Message, Theme, Renderer> where - Theme: StyleSheet + container::StyleSheet, Renderer: crate::core::Renderer, { /// Creates a [`PaneGrid`] with the given [`State`] and view function. @@ -130,7 +124,10 @@ where pub fn new<T>( state: &'a State<T>, view: impl Fn(Pane, &'a T, bool) -> Content<'a, Message, Theme, Renderer>, - ) -> Self { + ) -> Self + where + Theme: DefaultStyle, + { let contents = if let Some((pane, pane_state)) = state.maximized.and_then(|pane| { state.panes.get(&pane).map(|pane_state| (pane, pane_state)) @@ -161,7 +158,7 @@ where on_click: None, on_drag: None, on_resize: None, - style: Default::default(), + style: Theme::default_style(), } } @@ -221,11 +218,8 @@ where } /// Sets the style of the [`PaneGrid`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self { + self.style = style; self } @@ -240,7 +234,6 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for PaneGrid<'a, Message, Theme, Renderer> where Renderer: crate::core::Renderer, - Theme: StyleSheet + container::StyleSheet, { fn tag(&self) -> tree::Tag { tree::Tag::of::<state::Action>() @@ -285,19 +278,29 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - tree, - renderer, - limits, - self.contents.layout(), - self.width, - self.height, - self.spacing, - self.contents.iter(), - |content, tree, renderer, limits| { - content.layout(tree, renderer, limits) - }, - ) + let size = limits.resolve(self.width, self.height, Size::ZERO); + let node = self.contents.layout(); + let regions = node.pane_regions(self.spacing, size); + + let children = self + .contents + .iter() + .zip(tree.children.iter_mut()) + .filter_map(|((pane, content), tree)| { + let region = regions.get(&pane)?; + let size = Size::new(region.width, region.height); + + let node = content.layout( + tree, + renderer, + &layout::Limits::new(size, size), + ); + + Some(node.move_to(Point::new(region.x, region.y))) + }) + .collect(); + + layout::Node::with_children(size, children) } fn operate( @@ -329,7 +332,10 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) -> event::Status { + let mut event_status = event::Status::Ignored; + let action = tree.state.downcast_mut::<state::Action>(); + let node = self.contents.layout(); let on_drag = if self.drag_enabled() { &self.on_drag @@ -337,19 +343,164 @@ where &None }; - let event_status = update( - action, - self.contents.layout(), - &event, - layout, - cursor, - shell, - self.spacing, - self.contents.iter(), - &self.on_click, - on_drag, - &self.on_resize, - ); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let bounds = layout.bounds(); + + if let Some(cursor_position) = cursor.position_over(bounds) { + event_status = event::Status::Captured; + + match &self.on_resize { + Some((leeway, _)) => { + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = node.split_regions( + self.spacing, + Size::new(bounds.width, bounds.height), + ); + + let clicked_split = hovered_split( + splits.iter(), + self.spacing + leeway, + relative_cursor, + ); + + if let Some((split, axis, _)) = clicked_split { + if action.picked_pane().is_none() { + *action = + state::Action::Resizing { split, axis }; + } + } else { + click_pane( + action, + layout, + cursor_position, + shell, + self.contents.iter(), + &self.on_click, + on_drag, + ); + } + } + None => { + click_pane( + action, + layout, + cursor_position, + shell, + self.contents.iter(), + &self.on_click, + on_drag, + ); + } + } + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + if let Some((pane, origin)) = action.picked_pane() { + if let Some(on_drag) = on_drag { + if let Some(cursor_position) = cursor.position() { + if cursor_position.distance(origin) + > DRAG_DEADBAND_DISTANCE + { + let event = if let Some(edge) = + in_edge(layout, cursor_position) + { + DragEvent::Dropped { + pane, + target: Target::Edge(edge), + } + } else { + let dropped_region = self + .contents + .iter() + .zip(layout.children()) + .find_map(|(target, layout)| { + layout_region( + layout, + cursor_position, + ) + .map(|region| (target, region)) + }); + + match dropped_region { + Some(((target, _), region)) + if pane != target => + { + DragEvent::Dropped { + pane, + target: Target::Pane( + target, region, + ), + } + } + _ => DragEvent::Canceled { pane }, + } + }; + + shell.publish(on_drag(event)); + } + } + } + + event_status = event::Status::Captured; + } else if action.picked_split().is_some() { + event_status = event::Status::Captured; + } + + *action = state::Action::Idle; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some((_, on_resize)) = &self.on_resize { + if let Some((split, _)) = action.picked_split() { + let bounds = layout.bounds(); + + let splits = node.split_regions( + self.spacing, + Size::new(bounds.width, bounds.height), + ); + + if let Some((axis, rectangle, _)) = splits.get(&split) { + if let Some(cursor_position) = cursor.position() { + let ratio = match axis { + Axis::Horizontal => { + let position = cursor_position.y + - bounds.y + - rectangle.y; + + (position / rectangle.height) + .clamp(0.1, 0.9) + } + Axis::Vertical => { + let position = cursor_position.x + - bounds.x + - rectangle.x; + + (position / rectangle.width) + .clamp(0.1, 0.9) + } + }; + + shell.publish(on_resize(ResizeEvent { + split, + ratio, + })); + + event_status = event::Status::Captured; + } + } + } + } + } + _ => {} + } let picked_pane = action.picked_pane().map(|(pane, _)| pane); @@ -383,32 +534,61 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - ) - .unwrap_or_else(|| { - self.contents - .iter() - .zip(&tree.children) - .zip(layout.children()) - .map(|(((_pane, content), tree), layout)| { - content.mouse_interaction( - tree, - layout, - cursor, - viewport, - renderer, - self.drag_enabled(), + let action = tree.state.downcast_ref::<state::Action>(); + + if action.picked_pane().is_some() { + return mouse::Interaction::Grabbing; + } + + let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); + let node = self.contents.layout(); + + let resize_axis = + action.picked_split().map(|(_, axis)| axis).or_else(|| { + resize_leeway.and_then(|leeway| { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let splits = + node.split_regions(self.spacing, bounds.size()); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + hovered_split( + splits.iter(), + self.spacing + leeway, + relative_cursor, ) + .map(|(_, axis, _)| axis) }) - .max() - .unwrap_or_default() - }) + }); + + if let Some(resize_axis) = resize_axis { + return match resize_axis { + Axis::Horizontal => mouse::Interaction::ResizingVertically, + Axis::Vertical => mouse::Interaction::ResizingHorizontally, + }; + } + + self.contents + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|(((_pane, content), tree), layout)| { + content.mouse_interaction( + tree, + layout, + cursor, + viewport, + renderer, + self.drag_enabled(), + ) + }) + .max() + .unwrap_or_default() } fn draw( @@ -421,28 +601,210 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - draw( - tree.state.downcast_ref(), - self.contents.layout(), - layout, - cursor, - renderer, - theme, - style, - viewport, - self.spacing, - self.on_resize.as_ref().map(|(leeway, _)| *leeway), - &self.style, - self.contents - .iter() - .zip(&tree.children) - .map(|((pane, content), tree)| (pane, (content, tree))), - |(content, tree), renderer, style, layout, cursor, rectangle| { - content.draw( - tree, renderer, theme, style, layout, cursor, rectangle, + let action = tree.state.downcast_ref::<state::Action>(); + let node = self.contents.layout(); + let resize_leeway = self.on_resize.as_ref().map(|(leeway, _)| *leeway); + + let contents = self + .contents + .iter() + .zip(&tree.children) + .map(|((pane, content), tree)| (pane, (content, tree))); + + let picked_pane = action.picked_pane().filter(|(_, origin)| { + cursor + .position() + .map(|position| position.distance(*origin)) + .unwrap_or_default() + > DRAG_DEADBAND_DISTANCE + }); + + let picked_split = action + .picked_split() + .and_then(|(split, axis)| { + let bounds = layout.bounds(); + + let splits = node.split_regions(self.spacing, bounds.size()); + + let (_axis, region, ratio) = splits.get(&split)?; + + let region = + axis.split_line_bounds(*region, *ratio, self.spacing); + + Some((axis, region + Vector::new(bounds.x, bounds.y), true)) + }) + .or_else(|| match resize_leeway { + Some(leeway) => { + let cursor_position = cursor.position()?; + let bounds = layout.bounds(); + + let relative_cursor = Point::new( + cursor_position.x - bounds.x, + cursor_position.y - bounds.y, + ); + + let splits = + node.split_regions(self.spacing, bounds.size()); + + let (_split, axis, region) = hovered_split( + splits.iter(), + self.spacing + leeway, + relative_cursor, + )?; + + Some(( + axis, + region + Vector::new(bounds.x, bounds.y), + false, + )) + } + None => None, + }); + + let pane_cursor = if picked_pane.is_some() { + mouse::Cursor::Unavailable + } else { + cursor + }; + + let mut render_picked_pane = None; + + let pane_in_edge = if picked_pane.is_some() { + cursor + .position() + .and_then(|cursor_position| in_edge(layout, cursor_position)) + } else { + None + }; + + let appearance = (self.style)(theme); + + for ((id, (content, tree)), pane_layout) in + contents.zip(layout.children()) + { + match picked_pane { + Some((dragging, origin)) if id == dragging => { + render_picked_pane = + Some(((content, tree), origin, pane_layout)); + } + Some((dragging, _)) if id != dragging => { + content.draw( + tree, + renderer, + theme, + style, + pane_layout, + pane_cursor, + viewport, + ); + + if picked_pane.is_some() && pane_in_edge.is_none() { + if let Some(region) = + cursor.position().and_then(|cursor_position| { + layout_region(pane_layout, cursor_position) + }) + { + let bounds = + layout_region_bounds(pane_layout, region); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.hovered_region.border, + ..renderer::Quad::default() + }, + appearance.hovered_region.background, + ); + } + } + } + _ => { + content.draw( + tree, + renderer, + theme, + style, + pane_layout, + pane_cursor, + viewport, + ); + } + } + } + + if let Some(edge) = pane_in_edge { + let bounds = edge_bounds(layout, edge); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.hovered_region.border, + ..renderer::Quad::default() + }, + appearance.hovered_region.background, + ); + } + + // Render picked pane last + if let Some(((content, tree), origin, layout)) = render_picked_pane { + if let Some(cursor_position) = cursor.position() { + let bounds = layout.bounds(); + + let translation = + cursor_position - Point::new(origin.x, origin.y); + + renderer.with_translation(translation, |renderer| { + renderer.with_layer(bounds, |renderer| { + content.draw( + tree, + renderer, + theme, + style, + layout, + pane_cursor, + viewport, + ); + }); + }); + } + } + + if picked_pane.is_none() { + if let Some((axis, split_region, is_picked)) = picked_split { + let highlight = if is_picked { + appearance.picked_split + } else { + appearance.hovered_split + }; + + renderer.fill_quad( + renderer::Quad { + bounds: match axis { + Axis::Horizontal => Rectangle { + x: split_region.x, + y: (split_region.y + + (split_region.height - highlight.width) + / 2.0) + .round(), + width: split_region.width, + height: highlight.width, + }, + Axis::Vertical => Rectangle { + x: (split_region.x + + (split_region.width - highlight.width) + / 2.0) + .round(), + y: split_region.y, + width: highlight.width, + height: split_region.height, + }, + }, + ..renderer::Quad::default() + }, + highlight.color, ); - }, - ); + } + } } fn overlay<'b>( @@ -470,7 +832,7 @@ impl<'a, Message, Theme, Renderer> From<PaneGrid<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + container::StyleSheet + 'a, + Theme: 'a, Renderer: crate::core::Renderer + 'a, { fn from( @@ -480,219 +842,6 @@ where } } -/// Calculates the [`Layout`] of a [`PaneGrid`]. -pub fn layout<Renderer, T>( - tree: &mut Tree, - renderer: &Renderer, - limits: &layout::Limits, - node: &Node, - width: Length, - height: Length, - spacing: f32, - contents: impl Iterator<Item = (Pane, T)>, - layout_content: impl Fn( - T, - &mut Tree, - &Renderer, - &layout::Limits, - ) -> layout::Node, -) -> layout::Node { - let size = limits.resolve(width, height, Size::ZERO); - - let regions = node.pane_regions(spacing, size); - let children = contents - .zip(tree.children.iter_mut()) - .filter_map(|((pane, content), tree)| { - let region = regions.get(&pane)?; - let size = Size::new(region.width, region.height); - - let node = layout_content( - content, - tree, - renderer, - &layout::Limits::new(size, size), - ); - - Some(node.move_to(Point::new(region.x, region.y))) - }) - .collect(); - - layout::Node::with_children(size, children) -} - -/// Processes an [`Event`] and updates the [`state`] of a [`PaneGrid`] -/// accordingly. -pub fn update<'a, Message, T: Draggable>( - action: &mut state::Action, - node: &Node, - event: &Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - spacing: f32, - contents: impl Iterator<Item = (Pane, T)>, - on_click: &Option<Box<dyn Fn(Pane) -> Message + 'a>>, - on_drag: &Option<Box<dyn Fn(DragEvent) -> Message + 'a>>, - on_resize: &Option<(f32, Box<dyn Fn(ResizeEvent) -> Message + 'a>)>, -) -> event::Status { - let mut event_status = event::Status::Ignored; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let bounds = layout.bounds(); - - if let Some(cursor_position) = cursor.position_over(bounds) { - event_status = event::Status::Captured; - - match on_resize { - Some((leeway, _)) => { - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - let clicked_split = hovered_split( - splits.iter(), - spacing + leeway, - relative_cursor, - ); - - if let Some((split, axis, _)) = clicked_split { - if action.picked_pane().is_none() { - *action = - state::Action::Resizing { split, axis }; - } - } else { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - on_click, - on_drag, - ); - } - } - None => { - click_pane( - action, - layout, - cursor_position, - shell, - contents, - on_click, - on_drag, - ); - } - } - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - if let Some((pane, origin)) = action.picked_pane() { - if let Some(on_drag) = on_drag { - if let Some(cursor_position) = cursor.position() { - if cursor_position.distance(origin) - > DRAG_DEADBAND_DISTANCE - { - let event = if let Some(edge) = - in_edge(layout, cursor_position) - { - DragEvent::Dropped { - pane, - target: Target::Edge(edge), - } - } else { - let dropped_region = contents - .zip(layout.children()) - .find_map(|(target, layout)| { - layout_region(layout, cursor_position) - .map(|region| (target, region)) - }); - - match dropped_region { - Some(((target, _), region)) - if pane != target => - { - DragEvent::Dropped { - pane, - target: Target::Pane( - target, region, - ), - } - } - _ => DragEvent::Canceled { pane }, - } - }; - - shell.publish(on_drag(event)); - } - } - } - - event_status = event::Status::Captured; - } else if action.picked_split().is_some() { - event_status = event::Status::Captured; - } - - *action = state::Action::Idle; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some((_, on_resize)) = on_resize { - if let Some((split, _)) = action.picked_split() { - let bounds = layout.bounds(); - - let splits = node.split_regions( - spacing, - Size::new(bounds.width, bounds.height), - ); - - if let Some((axis, rectangle, _)) = splits.get(&split) { - if let Some(cursor_position) = cursor.position() { - let ratio = match axis { - Axis::Horizontal => { - let position = cursor_position.y - - bounds.y - - rectangle.y; - - (position / rectangle.height) - .clamp(0.1, 0.9) - } - Axis::Vertical => { - let position = cursor_position.x - - bounds.x - - rectangle.x; - - (position / rectangle.width).clamp(0.1, 0.9) - } - }; - - shell.publish(on_resize(ResizeEvent { - split, - ratio, - })); - - event_status = event::Status::Captured; - } - } - } - } - } - _ => {} - } - - event_status -} - fn layout_region(layout: Layout<'_>, cursor_position: Point) -> Option<Region> { let bounds = layout.bounds(); @@ -748,257 +897,6 @@ fn click_pane<'a, Message, T>( } } -/// Returns the current [`mouse::Interaction`] of a [`PaneGrid`]. -pub fn mouse_interaction( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor: mouse::Cursor, - spacing: f32, - resize_leeway: Option<f32>, -) -> Option<mouse::Interaction> { - if action.picked_pane().is_some() { - return Some(mouse::Interaction::Grabbing); - } - - let resize_axis = - action.picked_split().map(|(_, axis)| axis).or_else(|| { - resize_leeway.and_then(|leeway| { - let cursor_position = cursor.position()?; - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - hovered_split(splits.iter(), spacing + leeway, relative_cursor) - .map(|(_, axis, _)| axis) - }) - }); - - if let Some(resize_axis) = resize_axis { - return Some(match resize_axis { - Axis::Horizontal => mouse::Interaction::ResizingVertically, - Axis::Vertical => mouse::Interaction::ResizingHorizontally, - }); - } - - None -} - -/// Draws a [`PaneGrid`]. -pub fn draw<Theme, Renderer, T>( - action: &state::Action, - node: &Node, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &mut Renderer, - theme: &Theme, - default_style: &renderer::Style, - viewport: &Rectangle, - spacing: f32, - resize_leeway: Option<f32>, - style: &Theme::Style, - contents: impl Iterator<Item = (Pane, T)>, - draw_pane: impl Fn( - T, - &mut Renderer, - &renderer::Style, - Layout<'_>, - mouse::Cursor, - &Rectangle, - ), -) where - Theme: StyleSheet, - Renderer: crate::core::Renderer, -{ - let picked_pane = action.picked_pane().filter(|(_, origin)| { - cursor - .position() - .map(|position| position.distance(*origin)) - .unwrap_or_default() - > DRAG_DEADBAND_DISTANCE - }); - - let picked_split = action - .picked_split() - .and_then(|(split, axis)| { - let bounds = layout.bounds(); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_axis, region, ratio) = splits.get(&split)?; - - let region = axis.split_line_bounds(*region, *ratio, spacing); - - Some((axis, region + Vector::new(bounds.x, bounds.y), true)) - }) - .or_else(|| match resize_leeway { - Some(leeway) => { - let cursor_position = cursor.position()?; - let bounds = layout.bounds(); - - let relative_cursor = Point::new( - cursor_position.x - bounds.x, - cursor_position.y - bounds.y, - ); - - let splits = node.split_regions(spacing, bounds.size()); - - let (_split, axis, region) = hovered_split( - splits.iter(), - spacing + leeway, - relative_cursor, - )?; - - Some((axis, region + Vector::new(bounds.x, bounds.y), false)) - } - None => None, - }); - - let pane_cursor = if picked_pane.is_some() { - mouse::Cursor::Unavailable - } else { - cursor - }; - - let mut render_picked_pane = None; - - let pane_in_edge = if picked_pane.is_some() { - cursor - .position() - .and_then(|cursor_position| in_edge(layout, cursor_position)) - } else { - None - }; - - for ((id, pane), pane_layout) in contents.zip(layout.children()) { - match picked_pane { - Some((dragging, origin)) if id == dragging => { - render_picked_pane = Some((pane, origin, pane_layout)); - } - Some((dragging, _)) if id != dragging => { - draw_pane( - pane, - renderer, - default_style, - pane_layout, - pane_cursor, - viewport, - ); - - if picked_pane.is_some() && pane_in_edge.is_none() { - if let Some(region) = - cursor.position().and_then(|cursor_position| { - layout_region(pane_layout, cursor_position) - }) - { - let bounds = layout_region_bounds(pane_layout, region); - let hovered_region_style = theme.hovered_region(style); - - renderer.fill_quad( - renderer::Quad { - bounds, - border: hovered_region_style.border, - ..renderer::Quad::default() - }, - theme.hovered_region(style).background, - ); - } - } - } - _ => { - draw_pane( - pane, - renderer, - default_style, - pane_layout, - pane_cursor, - viewport, - ); - } - } - } - - if let Some(edge) = pane_in_edge { - let hovered_region_style = theme.hovered_region(style); - let bounds = edge_bounds(layout, edge); - - renderer.fill_quad( - renderer::Quad { - bounds, - border: hovered_region_style.border, - ..renderer::Quad::default() - }, - theme.hovered_region(style).background, - ); - } - - // Render picked pane last - if let Some((pane, origin, layout)) = render_picked_pane { - if let Some(cursor_position) = cursor.position() { - let bounds = layout.bounds(); - - let translation = cursor_position - Point::new(origin.x, origin.y); - - renderer.with_translation(translation, |renderer| { - renderer.with_layer(bounds, |renderer| { - draw_pane( - pane, - renderer, - default_style, - layout, - pane_cursor, - viewport, - ); - }); - }); - } - } - - if picked_pane.is_none() { - if let Some((axis, split_region, is_picked)) = picked_split { - let highlight = if is_picked { - theme.picked_split(style) - } else { - theme.hovered_split(style) - }; - - if let Some(highlight) = highlight { - renderer.fill_quad( - renderer::Quad { - bounds: match axis { - Axis::Horizontal => Rectangle { - x: split_region.x, - y: (split_region.y - + (split_region.height - highlight.width) - / 2.0) - .round(), - width: split_region.width, - height: highlight.width, - }, - Axis::Vertical => Rectangle { - x: (split_region.x - + (split_region.width - highlight.width) - / 2.0) - .round(), - y: split_region.y, - width: highlight.width, - height: split_region.height, - }, - }, - ..renderer::Quad::default() - }, - highlight.color, - ); - } - } - } -} - fn in_edge(layout: Layout<'_>, cursor: Point) -> Option<Edge> { let bounds = layout.bounds(); @@ -1215,3 +1113,82 @@ impl<'a, T> Contents<'a, T> { matches!(self, Self::Maximized(..)) } } + +/// The appearance of a [`PaneGrid`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Appearance { + /// The appearance of a hovered region highlight. + hovered_region: Highlight, + /// The appearance of a picked split. + picked_split: Line, + /// The appearance of a hovered split. + hovered_split: Line, +} + +/// The appearance of a highlight of the [`PaneGrid`]. +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Highlight { + /// 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, +} + +/// The style of a [`PaneGrid`]. +pub type Style<Theme> = fn(&Theme) -> Appearance; + +/// The default style of a [`PaneGrid`]. +pub trait DefaultStyle { + /// Returns the default style of a [`PaneGrid`]. + 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 [`PaneGrid`]. +pub fn default(theme: &Theme) -> Appearance { + let palette = theme.extended_palette(); + + Appearance { + hovered_region: Highlight { + background: Background::Color(Color { + a: 0.5, + ..palette.primary.base.color + }), + border: Border { + width: 2.0, + color: palette.primary.strong.color, + radius: 0.0.into(), + }, + }, + hovered_split: Line { + color: palette.primary.base.color, + width: 2.0, + }, + picked_split: Line { + color: palette.primary.strong.color, + width: 2.0, + }, + } +} diff --git a/widget/src/pane_grid/content.rs b/widget/src/pane_grid/content.rs index dfe0fdcf..aecec777 100644 --- a/widget/src/pane_grid/content.rs +++ b/widget/src/pane_grid/content.rs @@ -20,25 +20,26 @@ pub struct Content< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { title_bar: Option<TitleBar<'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> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { /// 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 { title_bar: None, body: body.into(), - style: Default::default(), + style: Theme::default_style(), } } @@ -52,7 +53,10 @@ where } /// 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 } @@ -60,7 +64,6 @@ where impl<'a, Message, Theme, Renderer> Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { pub(super) fn state(&self) -> Tree { @@ -104,7 +107,15 @@ where 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); } @@ -370,7 +381,6 @@ where impl<'a, Message, Theme, Renderer> Draggable for &Content<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { fn can_be_dragged_at( @@ -393,7 +403,7 @@ impl<'a, T, Message, Theme, Renderer> From<T> for Content<'a, Message, Theme, Renderer> where T: Into<Element<'a, Message, Theme, Renderer>>, - Theme: container::StyleSheet, + Theme: container::DefaultStyle, Renderer: crate::core::Renderer, { fn from(element: T) -> Self { diff --git a/widget/src/pane_grid/title_bar.rs b/widget/src/pane_grid/title_bar.rs index 5b57509b..37f0f160 100644 --- a/widget/src/pane_grid/title_bar.rs +++ b/widget/src/pane_grid/title_bar.rs @@ -19,32 +19,32 @@ pub struct TitleBar< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { content: Element<'a, Message, Theme, Renderer>, controls: Option<Element<'a, Message, Theme, Renderer>>, padding: Padding, always_show_controls: bool, - style: Theme::Style, + style: container::Style<Theme>, } impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { /// 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 - E: Into<Element<'a, Message, Theme, Renderer>>, + Theme: container::DefaultStyle, { Self { content: content.into(), controls: None, padding: Padding::ZERO, always_show_controls: false, - style: Default::default(), + style: Theme::default_style(), } } @@ -64,7 +64,10 @@ where } /// 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 } @@ -85,7 +88,6 @@ where impl<'a, Message, Theme, Renderer> TitleBar<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet, Renderer: crate::core::Renderer, { pub(super) fn state(&self) -> Tree { @@ -128,7 +130,17 @@ where show_controls: bool, ) { 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 { text_color: style.text_color.unwrap_or(inherited_style.text_color), }; diff --git a/widget/src/pick_list.rs b/widget/src/pick_list.rs index 1f20e2bc..beb4e0c1 100644 --- a/widget/src/pick_list.rs +++ b/widget/src/pick_list.rs @@ -1,5 +1,4 @@ //! Display a dropdown list of selectable values. -use crate::container; use crate::core::alignment; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -11,15 +10,13 @@ use crate::core::text::{self, Paragraph as _, Text}; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Padding, Pixels, Point, Rectangle, - Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Padding, + Pixels, Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::overlay::menu::{self, Menu}; -use crate::scrollable; use std::borrow::Borrow; - -pub use crate::style::pick_list::{Appearance, StyleSheet}; +use std::f32; /// A widget for selecting a single value from a list of options. #[allow(missing_debug_implementations)] @@ -35,7 +32,6 @@ pub struct PickList< T: ToString + PartialEq + Clone, L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, - Theme: StyleSheet, Renderer: text::Renderer, { on_select: Box<dyn Fn(T) -> Message + 'a>, @@ -51,7 +47,7 @@ pub struct PickList< text_shaping: text::Shaping, font: Option<Renderer::Font>, handle: Handle<Renderer::Font>, - style: Theme::Style, + style: Style<Theme>, } impl<'a, T, L, V, Message, Theme, Renderer> @@ -61,23 +57,18 @@ where L: Borrow<[T]> + 'a, V: Borrow<T> + 'a, Message: Clone, - Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet, - <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, 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 /// selected value, and the message to produce when an option is selected. pub fn new( options: L, selected: Option<V>, on_select: impl Fn(T) -> Message + 'a, - ) -> Self { + ) -> Self + where + Theme: DefaultStyle, + { Self { on_select: Box::new(on_select), on_open: None, @@ -86,13 +77,13 @@ where placeholder: None, selected, width: Length::Shrink, - padding: Self::DEFAULT_PADDING, + padding: crate::button::DEFAULT_PADDING, text_size: None, text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, handle: Handle::default(), - style: Default::default(), + style: Theme::default_style(), } } @@ -160,10 +151,7 @@ where } /// Sets the style of the [`PickList`]. - pub fn style( - mut self, - style: impl Into<<Theme as StyleSheet>::Style>, - ) -> Self { + pub fn style(mut self, style: impl Into<Style<Theme>>) -> Self { self.style = style.into(); self } @@ -176,11 +164,6 @@ where L: Borrow<[T]>, V: Borrow<T>, 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, { fn tag(&self) -> tree::Tag { @@ -204,19 +187,77 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - renderer, - limits, - self.width, - self.padding, - self.text_size, - self.text_line_height, - self.text_shaping, - self.font, - self.placeholder.as_deref(), - self.options.borrow(), - ) + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + + let font = self.font.unwrap_or_else(|| renderer.default_font()); + let text_size = + self.text_size.unwrap_or_else(|| renderer.default_size()); + let options = self.options.borrow(); + + state.options.resize_with(options.len(), Default::default); + + let option_text = Text { + content: "", + 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( @@ -230,18 +271,98 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - self.on_select.as_ref(), - self.on_open.as_ref(), - self.on_close.as_ref(), - self.selected.as_ref().map(Borrow::borrow), - self.options.borrow(), - || tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - ) + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = + tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + + if state.is_open { + // Event wasn't processed by overlay, so cursor was clicked either outside its + // bounds or on the drop-down, either way we close the overlay. + state.is_open = false; + + 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( @@ -252,7 +373,14 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> 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( @@ -266,23 +394,124 @@ where viewport: &Rectangle, ) { let font = self.font.unwrap_or_else(|| renderer.default_font()); - draw( - renderer, - theme, - layout, - cursor, - self.padding, - self.text_size, - self.text_line_height, - self.text_shaping, - font, - self.placeholder.as_deref(), - self.selected.as_ref().map(Borrow::borrow), - &self.handle, - &self.style, - || tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - viewport, + let selected = self.selected.as_ref().map(Borrow::borrow); + let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + let is_selected = selected.is_some(); + + let status = if state.is_open { + Status::Opened + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }; + + 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>( @@ -293,19 +522,38 @@ where translation: Vector, ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + let font = self.font.unwrap_or_else(|| renderer.default_font()); - overlay( - layout, - translation, - state, - self.padding, - self.text_size, - self.text_shaping, - self.font.unwrap_or_else(|| renderer.default_font()), - self.options.borrow(), - &self.on_select, - self.style.clone(), - ) + if state.is_open { + let bounds = layout.bounds(); + + let on_select = &self.on_select; + + let mut menu = Menu::with_style( + &mut state.menu, + self.options.borrow(), + &mut state.hovered_option, + |option| { + 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, V: Borrow<T> + 'a, Message: Clone + 'a, - Theme: StyleSheet - + scrollable::StyleSheet - + menu::StyleSheet - + container::StyleSheet - + 'a, - <Theme as menu::StyleSheet>::Style: From<<Theme as StyleSheet>::Style>, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -332,9 +575,8 @@ where } } -/// The state of a [`PickList`]. #[derive(Debug)] -pub struct State<P: text::Paragraph> { +struct State<P: text::Paragraph> { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: bool, @@ -407,394 +649,94 @@ pub struct Icon<Font> { pub shaping: text::Shaping, } -/// Computes the layout of a [`PickList`]. -pub fn layout<Renderer, T>( - state: &mut State<Renderer::Paragraph>, - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - text_size: Option<Pixels>, - text_line_height: text::LineHeight, - 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 - }); - } +/// The possible status of a [`PickList`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`PickList`] can be interacted with. + Active, + /// The [`PickList`] is being hovered. + Hovered, + /// The [`PickList`] is open. + Opened, +} - if let Some(placeholder) = placeholder { - state.placeholder.update(Text { - content: placeholder, - ..option_text - }); - } +/// 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, +} - 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, - }; +/// The styles of the different parts of a [`PickList`]. +#[derive(Debug, PartialEq, Eq)] +pub struct Style<Theme> { + /// The style of the [`PickList`] itself. + pub field: fn(&Theme, Status) -> Appearance, - let size = { - let intrinsic = Size::new( - max_width + text_size.0 + padding.left, - f32::from(text_line_height.to_absolute(text_size)), - ); + /// The style of the [`Menu`] of the pick list. + pub menu: menu::Style<Theme>, +} - limits - .width(width) - .shrink(padding) - .resolve(width, Length::Shrink, intrinsic) - .expand(padding) +impl Style<Theme> { + /// The default style of a [`PickList`] with the built-in [`Theme`]. + pub const DEFAULT: Self = Self { + field: default, + menu: menu::Style::<Theme>::DEFAULT, }; - - layout::Node::new(size) } -/// Processes an [`Event`] and updates the [`State`] of a [`PickList`] -/// accordingly. -pub fn update<'a, T, P, Message>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - shell: &mut Shell<'_, Message>, - on_select: &dyn Fn(T) -> Message, - on_open: Option<&Message>, - on_close: Option<&Message>, - selected: Option<&T>, - options: &[T], - state: impl FnOnce() -> &'a mut State<P>, -) -> 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 { - // Event wasn't processed by overlay, so cursor was clicked either outside it's - // bounds or on the drop-down, either way we close the overlay. - state.is_open = false; - - if let Some(on_close) = on_close { - shell.publish(on_close.clone()); - } - - event::Status::Captured - } else if cursor.is_over(layout.bounds()) { - state.is_open = true; - state.hovered_option = - options.iter().position(|option| Some(option) == selected); - - if let Some(on_open) = 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 = 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, +impl<Theme> Clone for Style<Theme> { + fn clone(&self) -> Self { + *self } } -/// Returns the current [`mouse::Interaction`] of a [`PickList`]. -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 { - mouse::Interaction::Pointer - } else { - mouse::Interaction::default() - } -} +impl<Theme> Copy for Style<Theme> {} -/// Returns the current overlay of a [`PickList`]. -pub fn overlay<'a, T, Message, Theme, Renderer>( - layout: Layout<'_>, - 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( - &mut state.menu, - options, - &mut state.hovered_option, - |option| { - state.is_open = false; - - (on_selected)(option) - }, - 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); - } +/// The default style of a [`PickList`]. +pub trait DefaultStyle: Sized { + /// Returns the default style of a [`PickList`]. + fn default_style() -> Style<Self>; +} - Some(menu.overlay(layout.position() + translation, bounds.height)) - } else { - None +impl DefaultStyle for Theme { + fn default_style() -> Style<Self> { + Style::<Self>::DEFAULT } } -/// 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() +/// The default style of the field of a [`PickList`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let active = Appearance { + text_color: palette.background.weak.text, + background: palette.background.weak.color.into(), + placeholder_color: palette.background.strong.color, + handle_color: palette.background.weak.text, + border: Border { + radius: 2.0.into(), + width: 1.0, + color: palette.background.strong.color, }, - 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 { - 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 - 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 + match status { + Status::Active => active, + Status::Hovered | Status::Opened => Appearance { + border: Border { + color: palette.primary.strong.color, + ..active.border }, - *viewport, - ); + ..active + }, } } diff --git a/widget/src/progress_bar.rs b/widget/src/progress_bar.rs index 694fdd28..7b0ea63f 100644 --- a/widget/src/progress_bar.rs +++ b/widget/src/progress_bar.rs @@ -3,17 +3,17 @@ use crate::core::layout; use crate::core::mouse; use crate::core::renderer; 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; -pub use iced_style::progress_bar::{Appearance, StyleSheet}; - /// A bar that displays progress. /// /// # Example /// ```no_run -/// # type ProgressBar = iced_widget::ProgressBar<iced_widget::style::Theme>; +/// # type ProgressBar = iced_widget::ProgressBar; /// # /// let value = 50.0; /// @@ -22,21 +22,15 @@ pub use iced_style::progress_bar::{Appearance, StyleSheet}; /// ///  #[allow(missing_debug_implementations)] -pub struct ProgressBar<Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct ProgressBar<Theme = crate::Theme> { range: RangeInclusive<f32>, value: f32, width: Length, height: Option<Length>, - style: Theme::Style, + style: Style<Theme>, } -impl<Theme> ProgressBar<Theme> -where - Theme: StyleSheet, -{ +impl<Theme> ProgressBar<Theme> { /// The default height of a [`ProgressBar`]. pub const DEFAULT_HEIGHT: f32 = 30.0; @@ -45,13 +39,16 @@ where /// It expects: /// * an inclusive range of possible values /// * 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 { value: value.clamp(*range.start(), *range.end()), range, width: Length::Fill, height: None, - style: Default::default(), + style: Theme::default_style(), } } @@ -68,7 +65,7 @@ where } /// 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 } @@ -78,7 +75,6 @@ impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for ProgressBar<Theme> where Renderer: crate::core::Renderer, - Theme: StyleSheet, { fn size(&self) -> Size<Length> { Size { @@ -120,15 +116,15 @@ where / (range_end - range_start) }; - let style = theme.appearance(&self.style); + let appearance = (self.style)(theme); renderer.fill_quad( renderer::Quad { bounds: Rectangle { ..bounds }, - border: Border::with_radius(style.border_radius), + border: appearance.border, ..renderer::Quad::default() }, - style.background, + appearance.background, ); if active_progress_width > 0.0 { @@ -138,10 +134,10 @@ where width: active_progress_width, ..bounds }, - border: Border::with_radius(style.border_radius), + border: Border::rounded(appearance.border.radius), ..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> where Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { fn from( @@ -160,3 +156,80 @@ where 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), + } +} diff --git a/widget/src/qr_code.rs b/widget/src/qr_code.rs index eeb1526f..41bcb83e 100644 --- a/widget/src/qr_code.rs +++ b/widget/src/qr_code.rs @@ -5,7 +5,8 @@ use crate::core::mouse; use crate::core::renderer::{self, Renderer as _}; use crate::core::widget::tree::{self, Tree}; 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::Renderer; @@ -13,33 +14,28 @@ use crate::Renderer; use std::cell::RefCell; use thiserror::Error; -pub use crate::style::qr_code::{Appearance, StyleSheet}; - const DEFAULT_CELL_SIZE: u16 = 4; const QUIET_ZONE: usize = 2; /// A type of matrix barcode consisting of squares arranged in a grid which /// can be read by an imaging device, such as a camera. #[derive(Debug)] -pub struct QRCode<'a, Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct QRCode<'a, Theme = crate::Theme> { data: &'a Data, cell_size: u16, - style: Theme::Style, + style: Style<Theme>, } -impl<'a, Theme> QRCode<'a, Theme> -where - Theme: StyleSheet, -{ +impl<'a, Theme> QRCode<'a, Theme> { /// 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 { data, cell_size: DEFAULT_CELL_SIZE, - style: Default::default(), + style: Theme::default_style(), } } @@ -50,15 +46,14 @@ where } /// 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 } } -impl<'a, Message, Theme> Widget<Message, Theme, Renderer> for QRCode<'a, Theme> -where - Theme: StyleSheet, +impl<'a, Message, Theme> Widget<Message, Theme, Renderer> + for QRCode<'a, Theme> { fn tag(&self) -> tree::Tag { tree::Tag::of::<State>() @@ -102,7 +97,7 @@ where let bounds = layout.bounds(); 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(); if Some(appearance) != *last_appearance { @@ -156,7 +151,7 @@ where impl<'a, Message, Theme> From<QRCode<'a, Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: StyleSheet + 'a, + Theme: 'a, { fn from(qr_code: QRCode<'a, Theme>) -> Self { Self::new(qr_code) @@ -330,3 +325,43 @@ impl From<qrcode::types::QrError> for Error { struct State { 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, + } +} diff --git a/widget/src/radio.rs b/widget/src/radio.rs index 68e9bc7e..5e4a3c1f 100644 --- a/widget/src/radio.rs +++ b/widget/src/radio.rs @@ -9,18 +9,16 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, - Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Rectangle, Shell, Size, Theme, Widget, }; -pub use iced_style::radio::{Appearance, StyleSheet}; - /// A circular button representing a choice. /// /// # Example /// ```no_run /// # 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; /// #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -71,7 +69,6 @@ pub use iced_style::radio::{Appearance, StyleSheet}; #[allow(missing_debug_implementations)] pub struct Radio<Message, Theme = crate::Theme, Renderer = crate::Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { is_selected: bool, @@ -84,20 +81,19 @@ where text_line_height: text::LineHeight, text_shaping: text::Shaping, font: Option<Renderer::Font>, - style: Theme::Style, + style: Style<Theme>, } impl<Message, Theme, Renderer> Radio<Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { /// 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. - pub const DEFAULT_SPACING: f32 = 15.0; + pub const DEFAULT_SPACING: f32 = 8.0; /// Creates a new [`Radio`] button. /// @@ -114,6 +110,7 @@ where f: F, ) -> Self where + Theme: DefaultStyle, V: Eq + Copy, F: FnOnce(V) -> Message, { @@ -128,7 +125,7 @@ where text_line_height: text::LineHeight::default(), text_shaping: text::Shaping::Basic, font: None, - style: Default::default(), + style: Theme::default_style(), } } @@ -178,8 +175,8 @@ where } /// Sets the style of the [`Radio`] button. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { + self.style = style; self } } @@ -188,7 +185,6 @@ impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Radio<Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -291,15 +287,18 @@ where viewport: &Rectangle, ) { let is_mouse_over = cursor.is_over(layout.bounds()); + let is_selected = self.is_selected; let mut children = layout.children(); - let custom_style = if is_mouse_over { - theme.hovered(&self.style, self.is_selected) + let status = if is_mouse_over { + Status::Hovered { is_selected } } else { - theme.active(&self.style, self.is_selected) + Status::Active { is_selected } }; + let appearance = (self.style)(theme, status); + { let layout = children.next().unwrap(); let bounds = layout.bounds(); @@ -312,12 +311,12 @@ where bounds, border: Border { radius: (size / 2.0).into(), - width: custom_style.border_width, - color: custom_style.border_color, + width: appearance.border_width, + color: appearance.border_color, }, ..renderer::Quad::default() }, - custom_style.background, + appearance.background, ); if self.is_selected { @@ -329,10 +328,10 @@ where width: bounds.width - 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() }, - custom_style.dot_color, + appearance.dot_color, ); } } @@ -346,7 +345,7 @@ where label_layout, tree.state.downcast_ref(), crate::text::Appearance { - color: custom_style.text_color, + color: appearance.text_color, }, viewport, ); @@ -358,7 +357,7 @@ impl<'a, Message, Theme, Renderer> From<Radio<Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: StyleSheet + crate::text::StyleSheet + 'a, + Theme: 'a, Renderer: 'a + text::Renderer, { fn from( @@ -367,3 +366,76 @@ where 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 + }, + } +} diff --git a/widget/src/rule.rs b/widget/src/rule.rs index bca34541..8580d4c7 100644 --- a/widget/src/rule.rs +++ b/widget/src/rule.rs @@ -1,53 +1,52 @@ //! Display a horizontal or vertical rule for dividing content. +use crate::core::border::{self, Border}; use crate::core::layout; use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; 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. #[allow(missing_debug_implementations)] -pub struct Rule<Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct Rule<Theme = crate::Theme> { width: Length, height: Length, is_horizontal: bool, - style: Theme::Style, + style: Style<Theme>, } -impl<Theme> Rule<Theme> -where - Theme: StyleSheet, -{ +impl<Theme> Rule<Theme> { /// 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 { width: Length::Fill, height: Length::Fixed(height.into().0), is_horizontal: true, - style: Default::default(), + style: Theme::default_style(), } } /// 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 { width: Length::Fixed(width.into().0), height: Length::Fill, is_horizontal: false, - style: Default::default(), + style: Theme::default_style(), } } /// Sets the style of the [`Rule`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme) -> Appearance) -> Self { + self.style = style; self } } @@ -55,7 +54,6 @@ where impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Rule<Theme> where Renderer: crate::core::Renderer, - Theme: StyleSheet, { fn size(&self) -> Size<Length> { Size { @@ -84,34 +82,35 @@ where _viewport: &Rectangle, ) { let bounds = layout.bounds(); - let style = theme.appearance(&self.style); + let appearance = (self.style)(theme); let bounds = if self.is_horizontal { let line_y = (bounds.y + (bounds.height / 2.0) - - (style.width as f32 / 2.0)) + - (appearance.width as f32 / 2.0)) .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; Rectangle { x: line_x, y: line_y, width: line_width, - height: style.width as f32, + height: appearance.width as f32, } } else { let line_x = (bounds.x + (bounds.width / 2.0) - - (style.width as f32 / 2.0)) + - (appearance.width as f32 / 2.0)) .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; Rectangle { x: line_x, y: line_y, - width: style.width as f32, + width: appearance.width as f32, height: line_height, } }; @@ -119,10 +118,10 @@ where renderer.fill_quad( renderer::Quad { bounds, - border: Border::with_radius(style.radius), + border: Border::rounded(appearance.radius), ..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> where Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { fn from(rule: Rule<Theme>) -> Element<'a, Message, Theme, Renderer> { 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, + } +} diff --git a/widget/src/scrollable.rs b/widget/src/scrollable.rs index f736d92e..9770ce57 100644 --- a/widget/src/scrollable.rs +++ b/widget/src/scrollable.rs @@ -1,4 +1,5 @@ //! Navigate an endless amount of content with a scrollbar. +// use crate::container; use crate::container; use crate::core::event::{self, Event}; use crate::core::keyboard; @@ -11,14 +12,11 @@ use crate::core::widget; use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Background, Clipboard, Color, Element, Layout, Length, Pixels, Point, - Rectangle, Shell, Size, Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Pixels, + Point, Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; -pub use crate::style::scrollable::{ - Appearance, Scrollbar, Scroller, StyleSheet, -}; pub use operation::scrollable::{AbsoluteOffset, RelativeOffset}; /// A widget that can vertically display an infinite amount of content with a @@ -30,7 +28,6 @@ pub struct Scrollable< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: crate::core::Renderer, { id: Option<Id>, @@ -39,18 +36,20 @@ pub struct Scrollable< direction: Direction, content: Element<'a, Message, Theme, Renderer>, on_scroll: Option<Box<dyn Fn(Viewport) -> Message + 'a>>, - style: Theme::Style, + style: Style<Theme>, } impl<'a, Message, Theme, Renderer> Scrollable<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { /// Creates a new vertical [`Scrollable`]. pub fn new( content: impl Into<Element<'a, Message, Theme, Renderer>>, - ) -> Self { + ) -> Self + where + Theme: DefaultStyle, + { Self::with_direction(content, Direction::default()) } @@ -58,6 +57,22 @@ where pub fn with_direction( content: impl Into<Element<'a, Message, Theme, Renderer>>, direction: Direction, + ) -> Self + where + Theme: DefaultStyle, + { + Self::with_direction_and_style( + content, + direction, + Theme::default_style(), + ) + } + + /// Creates a new [`Scrollable`] with the given [`Direction`] and style. + pub fn with_direction_and_style( + content: impl Into<Element<'a, Message, Theme, Renderer>>, + direction: Direction, + style: fn(&Theme, Status) -> Appearance, ) -> Self { let content = content.into(); @@ -80,7 +95,7 @@ where direction, content, on_scroll: None, - style: Default::default(), + style: style.into(), } } @@ -111,7 +126,7 @@ where } /// Sets the style of the [`Scrollable`] . - 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 } @@ -223,7 +238,6 @@ pub enum Alignment { impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Scrollable<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -255,20 +269,29 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.height, - &self.direction, - |renderer, limits| { - self.content.as_widget().layout( - &mut tree.children[0], - renderer, - limits, - ) - }, - ) + layout::contained(limits, self.width, self.height, |limits| { + let child_limits = layout::Limits::new( + Size::new(limits.min().width, limits.min().height), + Size::new( + if self.direction.horizontal().is_some() { + f32::INFINITY + } else { + limits.max().width + }, + if self.direction.vertical().is_some() { + f32::MAX + } else { + limits.max().height + }, + ), + ); + + self.content.as_widget().layout( + &mut tree.children[0], + renderer, + &child_limits, + ) + }) } fn operate( @@ -318,28 +341,316 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - tree.state.downcast_mut::<State>(), - event, - layout, - cursor, - clipboard, - shell, - self.direction, - &self.on_scroll, - |event, layout, cursor, clipboard, shell, viewport| { - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, + let state = tree.state.downcast_mut::<State>(); + let bounds = layout.bounds(); + let cursor_over_scrollable = cursor.position_over(bounds); + + let content = layout.children().next().unwrap(); + let content_bounds = content.bounds(); + + let scrollbars = + Scrollbars::new(state, self.direction, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + let mut event_status = { + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available( + cursor_position + + state.translation( + self.direction, + bounds, + content_bounds, + ), + ) + } + _ => mouse::Cursor::Unavailable, + }; + + let translation = + state.translation(self.direction, bounds, content_bounds); + + self.content.as_widget_mut().on_event( + &mut tree.children[0], + event.clone(), + layout, + cursor, + renderer, + clipboard, + shell, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ) + }; + + if let event::Status::Captured = event_status { + return event::Status::Captured; + } + + if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = + event + { + state.keyboard_modifiers = modifiers; + + return event::Status::Ignored; + } + + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) => { + if cursor_over_scrollable.is_none() { + return event::Status::Ignored; + } + + let delta = match delta { + mouse::ScrollDelta::Lines { x, y } => { + // TODO: Configurable speed/friction (?) + let movement = if state.keyboard_modifiers.shift() { + Vector::new(y, x) + } else { + Vector::new(x, y) + }; + + movement * 60.0 + } + mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), + }; + + state.scroll(delta, self.direction, bounds, content_bounds); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, shell, - viewport, - ) - }, - ) + ); + + event_status = event::Status::Captured; + } + Event::Touch(event) + if state.scroll_area_touched_at.is_some() + || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => + { + match event { + touch::Event::FingerPressed { .. } => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + state.scroll_area_touched_at = Some(cursor_position); + } + touch::Event::FingerMoved { .. } => { + if let Some(scroll_box_touched_at) = + state.scroll_area_touched_at + { + let Some(cursor_position) = cursor.position() + else { + return event::Status::Ignored; + }; + + let delta = Vector::new( + cursor_position.x - scroll_box_touched_at.x, + cursor_position.y - scroll_box_touched_at.y, + ); + + state.scroll( + delta, + self.direction, + bounds, + content_bounds, + ); + + state.scroll_area_touched_at = + Some(cursor_position); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + } + touch::Event::FingerLifted { .. } + | touch::Event::FingerLost { .. } => { + state.scroll_area_touched_at = None; + } + } + + event_status = event::Status::Captured; + } + _ => {} + } + + if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.y_scroller_grabbed_at = None; + + event_status = event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + if let Some(scrollbar) = scrollbars.y { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + event_status = event::Status::Captured; + } + } + _ => {} + } + } else if mouse_over_y_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_y_scroller(cursor_position), + scrollbars.y, + ) { + state.scroll_y_to( + scrollbar.scroll_percentage_y( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.y_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + event_status = event::Status::Captured; + } + _ => {} + } + } + + if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state.x_scroller_grabbed_at = None; + + event_status = event::Status::Captured; + } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let Some(scrollbar) = scrollbars.x { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + } + + event_status = event::Status::Captured; + } + _ => {} + } + } else if mouse_over_x_scrollbar { + match event { + Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let Some(cursor_position) = cursor.position() else { + return event::Status::Ignored; + }; + + if let (Some(scroller_grabbed_at), Some(scrollbar)) = ( + scrollbars.grab_x_scroller(cursor_position), + scrollbars.x, + ) { + state.scroll_x_to( + scrollbar.scroll_percentage_x( + scroller_grabbed_at, + cursor_position, + ), + bounds, + content_bounds, + ); + + state.x_scroller_grabbed_at = Some(scroller_grabbed_at); + + notify_on_scroll( + state, + &self.on_scroll, + bounds, + content_bounds, + shell, + ); + + event_status = event::Status::Captured; + } + } + _ => {} + } + } + + event_status } fn draw( @@ -352,26 +663,181 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - tree.state.downcast_ref::<State>(), + let state = tree.state.downcast_ref::<State>(); + + let bounds = layout.bounds(); + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + let scrollbars = + Scrollbars::new(state, self.direction, bounds, content_bounds); + + let cursor_over_scrollable = cursor.position_over(bounds); + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + let translation = + state.translation(self.direction, bounds, content_bounds); + + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + translation) + } + _ => mouse::Cursor::Unavailable, + }; + + let status = if state.y_scroller_grabbed_at.is_some() + || state.x_scroller_grabbed_at.is_some() + { + Status::Dragged { + is_horizontal_scrollbar_dragged: state + .x_scroller_grabbed_at + .is_some(), + is_vertical_scrollbar_dragged: state + .y_scroller_grabbed_at + .is_some(), + } + } else if cursor_over_scrollable.is_some() { + Status::Hovered { + is_horizontal_scrollbar_hovered: mouse_over_x_scrollbar, + is_vertical_scrollbar_hovered: mouse_over_y_scrollbar, + } + } else { + Status::Active + }; + + let appearance = (self.style)(theme, status); + + container::draw_background( renderer, - theme, - layout, - cursor, - self.direction, - &self.style, - |renderer, layout, cursor, viewport| { - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); - }, + &appearance.container, + layout.bounds(), ); + + // Draw inner content + if scrollbars.active() { + renderer.with_layer(bounds, |renderer| { + renderer.with_translation( + Vector::new(-translation.x, -translation.y), + |renderer| { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + content_layout, + cursor, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + ); + }, + ); + }); + + let draw_scrollbar = + |renderer: &mut Renderer, + style: Scrollbar, + scrollbar: &internals::Scrollbar| { + if scrollbar.bounds.width > 0.0 + && scrollbar.bounds.height > 0.0 + && (style.background.is_some() + || (style.border.color != Color::TRANSPARENT + && style.border.width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.bounds, + border: style.border, + ..renderer::Quad::default() + }, + style.background.unwrap_or(Background::Color( + Color::TRANSPARENT, + )), + ); + } + + if scrollbar.scroller.bounds.width > 0.0 + && scrollbar.scroller.bounds.height > 0.0 + && (style.scroller.color != Color::TRANSPARENT + || (style.scroller.border.color + != Color::TRANSPARENT + && style.scroller.border.width > 0.0)) + { + renderer.fill_quad( + renderer::Quad { + bounds: scrollbar.scroller.bounds, + border: style.scroller.border, + ..renderer::Quad::default() + }, + style.scroller.color, + ); + } + }; + + renderer.with_layer( + Rectangle { + width: bounds.width + 2.0, + height: bounds.height + 2.0, + ..bounds + }, + |renderer| { + if let Some(scrollbar) = scrollbars.y { + draw_scrollbar( + renderer, + appearance.vertical_scrollbar, + &scrollbar, + ); + } + + if let Some(scrollbar) = scrollbars.x { + draw_scrollbar( + renderer, + appearance.horizontal_scrollbar, + &scrollbar, + ); + } + + if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) { + let background = + appearance.gap.or(appearance.container.background); + + if let Some(background) = background { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: y.bounds.x, + y: x.bounds.y, + width: y.bounds.width, + height: x.bounds.height, + }, + ..renderer::Quad::default() + }, + background, + ); + } + } + }, + ); + } else { + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + content_layout, + cursor, + &Rectangle { + x: bounds.x + translation.x, + y: bounds.y + translation.y, + ..bounds + }, + ); + } } fn mouse_interaction( @@ -382,21 +848,48 @@ where _viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction( - tree.state.downcast_ref::<State>(), - layout, - cursor, - self.direction, - |layout, cursor, viewport| { - self.content.as_widget().mouse_interaction( - &tree.children[0], - layout, - cursor, - viewport, - renderer, - ) - }, - ) + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let cursor_over_scrollable = cursor.position_over(bounds); + + let content_layout = layout.children().next().unwrap(); + let content_bounds = content_layout.bounds(); + + let scrollbars = + Scrollbars::new(state, self.direction, bounds, content_bounds); + + let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = + scrollbars.is_mouse_over(cursor); + + if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) + || state.scrollers_grabbed() + { + mouse::Interaction::Idle + } else { + let translation = + state.translation(self.direction, bounds, content_bounds); + + let cursor = match cursor_over_scrollable { + Some(cursor_position) + if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => + { + mouse::Cursor::Available(cursor_position + translation) + } + _ => mouse::Cursor::Unavailable, + }; + + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + &Rectangle { + y: bounds.y + translation.y, + x: bounds.x + translation.x, + ..bounds + }, + renderer, + ) + } } fn overlay<'b>( @@ -430,7 +923,7 @@ impl<'a, Message, Theme, Renderer> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: 'a + crate::core::Renderer, { fn from( @@ -482,570 +975,6 @@ pub fn scroll_to<Message: 'static>( Command::widget(operation::scrollable::scroll_to(id.0, offset)) } -/// Computes the layout of a [`Scrollable`]. -pub fn layout<Renderer>( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - height: Length, - direction: &Direction, - layout_content: impl FnOnce(&Renderer, &layout::Limits) -> layout::Node, -) -> layout::Node { - layout::contained(limits, width, height, |limits| { - let child_limits = layout::Limits::new( - Size::new(limits.min().width, limits.min().height), - Size::new( - if direction.horizontal().is_some() { - f32::INFINITY - } else { - limits.max().width - }, - if direction.vertical().is_some() { - f32::MAX - } else { - limits.max().height - }, - ), - ); - - layout_content(renderer, &child_limits) - }) -} - -/// Processes an [`Event`] and updates the [`State`] of a [`Scrollable`] -/// accordingly. -pub fn update<Message>( - state: &mut State, - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - direction: Direction, - on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, - update_content: impl FnOnce( - Event, - Layout<'_>, - mouse::Cursor, - &mut dyn Clipboard, - &mut Shell<'_, Message>, - &Rectangle, - ) -> event::Status, -) -> event::Status { - let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(bounds); - - let content = layout.children().next().unwrap(); - let content_bounds = content.bounds(); - - let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor); - - let mut event_status = { - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available( - cursor_position - + state.translation(direction, bounds, content_bounds), - ) - } - _ => mouse::Cursor::Unavailable, - }; - - let translation = state.translation(direction, bounds, content_bounds); - - update_content( - event.clone(), - content, - cursor, - clipboard, - shell, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ) - }; - - if let event::Status::Captured = event_status { - return event::Status::Captured; - } - - if let Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) = event - { - state.keyboard_modifiers = modifiers; - - return event::Status::Ignored; - } - - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - if cursor_over_scrollable.is_none() { - return event::Status::Ignored; - } - - let delta = match delta { - mouse::ScrollDelta::Lines { x, y } => { - // TODO: Configurable speed/friction (?) - let movement = if state.keyboard_modifiers.shift() { - Vector::new(y, x) - } else { - Vector::new(x, y) - }; - - movement * 60.0 - } - mouse::ScrollDelta::Pixels { x, y } => Vector::new(x, y), - }; - - state.scroll(delta, direction, bounds, content_bounds); - - notify_on_scroll(state, on_scroll, bounds, content_bounds, shell); - - event_status = event::Status::Captured; - } - Event::Touch(event) - if state.scroll_area_touched_at.is_some() - || !mouse_over_y_scrollbar && !mouse_over_x_scrollbar => - { - match event { - touch::Event::FingerPressed { .. } => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_area_touched_at = Some(cursor_position); - } - touch::Event::FingerMoved { .. } => { - if let Some(scroll_box_touched_at) = - state.scroll_area_touched_at - { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - let delta = Vector::new( - cursor_position.x - scroll_box_touched_at.x, - cursor_position.y - scroll_box_touched_at.y, - ); - - state.scroll(delta, direction, bounds, content_bounds); - - state.scroll_area_touched_at = Some(cursor_position); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - } - touch::Event::FingerLifted { .. } - | touch::Event::FingerLost { .. } => { - state.scroll_area_touched_at = None; - } - } - - event_status = event::Status::Captured; - } - _ => {} - } - - if let Some(scroller_grabbed_at) = state.y_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.y_scroller_grabbed_at = None; - - event_status = event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - if let Some(scrollbar) = scrollbars.y { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } else if mouse_over_y_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_y_scroller(cursor_position), scrollbars.y) - { - state.scroll_y_to( - scrollbar.scroll_percentage_y( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.y_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } - - if let Some(scroller_grabbed_at) = state.x_scroller_grabbed_at { - match event { - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state.x_scroller_grabbed_at = None; - - event_status = event::Status::Captured; - } - Event::Mouse(mouse::Event::CursorMoved { .. }) - | Event::Touch(touch::Event::FingerMoved { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let Some(scrollbar) = scrollbars.x { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - } - - event_status = event::Status::Captured; - } - _ => {} - } - } else if mouse_over_x_scrollbar { - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let Some(cursor_position) = cursor.position() else { - return event::Status::Ignored; - }; - - if let (Some(scroller_grabbed_at), Some(scrollbar)) = - (scrollbars.grab_x_scroller(cursor_position), scrollbars.x) - { - state.scroll_x_to( - scrollbar.scroll_percentage_x( - scroller_grabbed_at, - cursor_position, - ), - bounds, - content_bounds, - ); - - state.x_scroller_grabbed_at = Some(scroller_grabbed_at); - - notify_on_scroll( - state, - on_scroll, - bounds, - content_bounds, - shell, - ); - - event_status = event::Status::Captured; - } - } - _ => {} - } - } - - event_status -} - -/// Computes the current [`mouse::Interaction`] of a [`Scrollable`]. -pub fn mouse_interaction( - state: &State, - layout: Layout<'_>, - cursor: mouse::Cursor, - direction: Direction, - content_interaction: impl FnOnce( - Layout<'_>, - mouse::Cursor, - &Rectangle, - ) -> mouse::Interaction, -) -> mouse::Interaction { - let bounds = layout.bounds(); - let cursor_over_scrollable = cursor.position_over(bounds); - - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor); - - if (mouse_over_x_scrollbar || mouse_over_y_scrollbar) - || state.scrollers_grabbed() - { - mouse::Interaction::Idle - } else { - let translation = state.translation(direction, bounds, content_bounds); - - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available(cursor_position + translation) - } - _ => mouse::Cursor::Unavailable, - }; - - content_interaction( - content_layout, - cursor, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ) - } -} - -/// Draws a [`Scrollable`]. -pub fn draw<Theme, Renderer>( - state: &State, - renderer: &mut Renderer, - theme: &Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - direction: Direction, - style: &Theme::Style, - draw_content: impl FnOnce(&mut Renderer, Layout<'_>, mouse::Cursor, &Rectangle), -) where - Theme: StyleSheet, - Renderer: crate::core::Renderer, -{ - let bounds = layout.bounds(); - let content_layout = layout.children().next().unwrap(); - let content_bounds = content_layout.bounds(); - - let scrollbars = Scrollbars::new(state, direction, bounds, content_bounds); - - let cursor_over_scrollable = cursor.position_over(bounds); - let (mouse_over_y_scrollbar, mouse_over_x_scrollbar) = - scrollbars.is_mouse_over(cursor); - - let translation = state.translation(direction, bounds, content_bounds); - - let cursor = match cursor_over_scrollable { - Some(cursor_position) - if !(mouse_over_x_scrollbar || mouse_over_y_scrollbar) => - { - mouse::Cursor::Available(cursor_position + translation) - } - _ => mouse::Cursor::Unavailable, - }; - - let appearance = if state.y_scroller_grabbed_at.is_some() - || state.x_scroller_grabbed_at.is_some() - { - theme.dragging(style) - } else if cursor_over_scrollable.is_some() { - theme.hovered(style, mouse_over_y_scrollbar || mouse_over_x_scrollbar) - } else { - theme.active(style) - }; - - let scrollbar_style = |is_dragging: bool, mouse_over_scrollbar: bool| { - if is_dragging { - theme.dragging(style).scrollbar - } else if cursor_over_scrollable.is_some() { - theme.hovered(style, mouse_over_scrollbar).scrollbar - } else { - theme.active(style).scrollbar - } - }; - - container::draw_background( - renderer, - &appearance.container, - layout.bounds(), - ); - - // Draw inner content - if scrollbars.active() { - renderer.with_layer(bounds, |renderer| { - renderer.with_translation( - Vector::new(-translation.x, -translation.y), - |renderer| { - draw_content( - renderer, - content_layout, - cursor, - &Rectangle { - y: bounds.y + translation.y, - x: bounds.x + translation.x, - ..bounds - }, - ); - }, - ); - }); - - let draw_scrollbar = - |renderer: &mut Renderer, - style: Scrollbar, - scrollbar: &internals::Scrollbar| { - if scrollbar.bounds.width > 0.0 - && scrollbar.bounds.height > 0.0 - && (style.background.is_some() - || (style.border.color != Color::TRANSPARENT - && style.border.width > 0.0)) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.bounds, - border: style.border, - ..renderer::Quad::default() - }, - style - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } - - if scrollbar.scroller.bounds.width > 0.0 - && scrollbar.scroller.bounds.height > 0.0 - && (style.scroller.color != Color::TRANSPARENT - || (style.scroller.border.color != Color::TRANSPARENT - && style.scroller.border.width > 0.0)) - { - renderer.fill_quad( - renderer::Quad { - bounds: scrollbar.scroller.bounds, - border: style.scroller.border, - ..renderer::Quad::default() - }, - style.scroller.color, - ); - } - }; - - renderer.with_layer( - Rectangle { - width: bounds.width + 2.0, - height: bounds.height + 2.0, - ..bounds - }, - |renderer| { - if let Some(scrollbar) = scrollbars.y { - draw_scrollbar( - renderer, - scrollbar_style( - state.y_scroller_grabbed_at.is_some(), - mouse_over_y_scrollbar, - ), - &scrollbar, - ); - } - - if let Some(scrollbar) = scrollbars.x { - draw_scrollbar( - renderer, - scrollbar_style( - state.x_scroller_grabbed_at.is_some(), - mouse_over_x_scrollbar, - ), - &scrollbar, - ); - } - - if let (Some(x), Some(y)) = (scrollbars.x, scrollbars.y) { - let background = - appearance.gap.or(appearance.container.background); - - if let Some(background) = background { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: y.bounds.x, - y: x.bounds.y, - width: y.bounds.width, - height: x.bounds.height, - }, - ..renderer::Quad::default() - }, - background, - ); - } - } - }, - ); - } else { - draw_content( - renderer, - content_layout, - cursor, - &Rectangle { - x: bounds.x + translation.x, - y: bounds.y + translation.y, - ..bounds - }, - ); - } -} - fn notify_on_scroll<Message>( state: &mut State, on_scroll: &Option<Box<dyn Fn(Viewport) -> Message + '_>>, @@ -1093,9 +1022,8 @@ fn notify_on_scroll<Message>( } } -/// The local state of a [`Scrollable`]. #[derive(Debug, Clone, Copy)] -pub struct State { +struct State { scroll_area_touched_at: Option<Point>, offset_y: Offset, y_scroller_grabbed_at: Option<f32>, @@ -1625,3 +1553,155 @@ pub(super) mod internals { pub bounds: Rectangle, } } + +/// The possible status of a [`Scrollable`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Scrollable`] can be interacted with. + Active, + /// The [`Scrollable`] is being hovered. + Hovered { + /// Indicates if the horizontal scrollbar is being hovered. + is_horizontal_scrollbar_hovered: bool, + /// Indicates if the vertical scrollbar is being hovered. + is_vertical_scrollbar_hovered: bool, + }, + /// The [`Scrollable`] is being dragged. + Dragged { + /// Indicates if the horizontal scrollbar is being dragged. + is_horizontal_scrollbar_dragged: bool, + /// Indicates if the vertical scrollbar is being dragged. + is_vertical_scrollbar_dragged: bool, + }, +} + +/// The appearance of a scrolable. +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The [`container::Appearance`] of a scrollable. + pub container: container::Appearance, + /// The vertical [`Scrollbar`] appearance. + pub vertical_scrollbar: Scrollbar, + /// The horizontal [`Scrollbar`] appearance. + pub horizontal_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, +} + +/// The style of a [`Scrollable`]. +pub type Style<Theme> = fn(&Theme, Status) -> Appearance; + +/// The default style of a [`Scrollable`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Scrollable`]. + 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 [`Scrollable`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); + + let scrollbar = Scrollbar { + background: Some(palette.background.weak.color.into()), + border: Border::rounded(2), + scroller: Scroller { + color: palette.background.strong.color, + border: Border::rounded(2), + }, + }; + + match status { + Status::Active => Appearance { + container: container::Appearance::default(), + vertical_scrollbar: scrollbar, + horizontal_scrollbar: scrollbar, + gap: None, + }, + Status::Hovered { + is_horizontal_scrollbar_hovered, + is_vertical_scrollbar_hovered, + } => { + let hovered_scrollbar = Scrollbar { + scroller: Scroller { + color: palette.primary.strong.color, + ..scrollbar.scroller + }, + ..scrollbar + }; + + Appearance { + container: container::Appearance::default(), + vertical_scrollbar: if is_vertical_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + horizontal_scrollbar: if is_horizontal_scrollbar_hovered { + hovered_scrollbar + } else { + scrollbar + }, + gap: None, + } + } + Status::Dragged { + is_horizontal_scrollbar_dragged, + is_vertical_scrollbar_dragged, + } => { + let dragged_scrollbar = Scrollbar { + scroller: Scroller { + color: palette.primary.base.color, + ..scrollbar.scroller + }, + ..scrollbar + }; + + Appearance { + container: container::Appearance::default(), + vertical_scrollbar: if is_vertical_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + horizontal_scrollbar: if is_horizontal_scrollbar_dragged { + dragged_scrollbar + } else { + scrollbar + }, + gap: None, + } + } + } +} diff --git a/widget/src/slider.rs b/widget/src/slider.rs index 65bc1772..f3ea9bfd 100644 --- a/widget/src/slider.rs +++ b/widget/src/slider.rs @@ -1,6 +1,5 @@ //! Display an interactive selector of a single value from a range of values. -//! -//! A [`Slider`] has some local [`State`]. +use crate::core::border; use crate::core::event::{self, Event}; use crate::core::keyboard; use crate::core::keyboard::key::{self, Key}; @@ -10,16 +9,12 @@ use crate::core::renderer; use crate::core::touch; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, - Shell, Size, Widget, + Border, Clipboard, Color, Element, Layout, Length, Pixels, Point, + Rectangle, Shell, Size, Theme, Widget, }; 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 /// values. /// @@ -30,8 +25,7 @@ pub use iced_style::slider::{ /// /// # Example /// ```no_run -/// # type Slider<'a, T, Message> = -/// # iced_widget::Slider<'a, Message, T, iced_widget::style::Theme>; +/// # type Slider<'a, T, Message> = iced_widget::Slider<'a, Message, T>; /// # /// #[derive(Clone)] /// pub enum Message { @@ -45,10 +39,7 @@ pub use iced_style::slider::{ /// ///  #[allow(missing_debug_implementations)] -pub struct Slider<'a, T, Message, Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct Slider<'a, T, Message, Theme = crate::Theme> { range: RangeInclusive<T>, step: T, shift_step: Option<T>, @@ -58,17 +49,16 @@ where on_release: Option<Message>, width: Length, height: f32, - style: Theme::Style, + style: Style<Theme>, } impl<'a, T, Message, Theme> Slider<'a, T, Message, Theme> where T: Copy + From<u8> + PartialOrd, Message: Clone, - Theme: StyleSheet, { /// The default height of a [`Slider`]. - pub const DEFAULT_HEIGHT: f32 = 22.0; + pub const DEFAULT_HEIGHT: f32 = 16.0; /// Creates a new [`Slider`]. /// @@ -80,6 +70,7 @@ where /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where + Theme: DefaultStyle, F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { @@ -104,7 +95,7 @@ where on_release: None, width: Length::Fill, height: Self::DEFAULT_HEIGHT, - style: Default::default(), + style: Theme::default_style(), } } @@ -140,7 +131,7 @@ where } /// 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 } @@ -165,7 +156,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Theme: StyleSheet, Renderer: crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -173,7 +163,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size<Length> { @@ -203,20 +193,143 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - tree.state.downcast_mut::<State>(), - &mut self.value, - self.default, - &self.range, - self.step, - self.shift_step, - self.on_change.as_ref(), - &self.on_release, - ) + let state = tree.state.downcast_mut::<State>(); + + let is_dragging = state.is_dragging; + let current_value = self.value; + + let locate = |cursor_position: Point| -> Option<T> { + let bounds = layout.bounds(); + let new_value = if cursor_position.x <= bounds.x { + Some(*self.range.start()) + } else if cursor_position.x >= bounds.x + bounds.width { + Some(*self.range.end()) + } else { + 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( @@ -229,15 +342,92 @@ where cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::<State>(), - self.value, - &self.range, + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); + + let style = (self.style)( 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, _renderer: &Renderer, ) -> 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 T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, Message: Clone + 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: crate::core::Renderer + 'a, { 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(); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +struct State { + is_dragging: bool, + keyboard_modifiers: keyboard::Modifiers, +} - let percent = f64::from(cursor_position.x - bounds.x) - / f64::from(bounds.width); +/// The possible status of a [`Slider`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`Slider`] can be interacted with. + Active, + /// The [`Slider`] is being hovered. + Hovered, + /// The [`Slider`] is being dragged. + Dragged, +} - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; +/// 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, +} - T::from_f64(value) +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 + } +} - 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; - } - }; +/// 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, +} - 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; - } +/// 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, +} - 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; +/// 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, + }, +} - 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); +/// The style of a [`Slider`]. +pub type Style<Theme> = fn(&Theme, Status) -> Appearance; - 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); - } - _ => (), - } +/// The default style of a [`Slider`]. +pub trait DefaultStyle { + /// Returns the default style of a [`Slider`]. + fn default_style() -> Style<Self>; +} - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; - } - _ => {} +impl DefaultStyle for Theme { + fn default_style() -> Style<Self> { + default } - - 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(); +impl DefaultStyle for Appearance { + fn default_style() -> Style<Self> { + |appearance, _status| *appearance + } +} - (start.into() as f32, end.into() as f32) - }; +/// The default style of a [`Slider`]. +pub fn default(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); - let offset = if range_start >= range_end { - 0.0 - } else { - (bounds.width - handle_width) * (value - range_start) - / (range_end - range_start) + let color = match status { + Status::Active => palette.primary.strong.color, + Status::Hovered => palette.primary.base.color, + Status::Dragged => palette.primary.strong.color, }; - 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() + Appearance { + rail: Rail { + colors: (color, palette.secondary.base.color), + width: 4.0, + border_radius: 2.0.into(), }, - 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() + handle: Handle { + shape: HandleShape::Circle { radius: 7.0 }, + color, + border_color: Color::TRANSPARENT, + border_width: 0.0, }, - 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)] -pub struct State { - is_dragging: bool, - keyboard_modifiers: keyboard::Modifiers, -} - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() } } diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 12ef3d92..6e61d27a 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,13 +5,13 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; 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; -pub use crate::style::svg::{Appearance, StyleSheet}; -pub use svg::Handle; +pub use crate::core::svg::Handle; /// A vector graphics image. /// @@ -20,36 +20,36 @@ pub use svg::Handle; /// [`Svg`] images can have a considerable rendering cost when resized, /// specially when they are complex. #[allow(missing_debug_implementations)] -pub struct Svg<Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct Svg<Theme = crate::Theme> { handle: Handle, width: Length, height: Length, content_fit: ContentFit, - style: <Theme as StyleSheet>::Style, + style: Style<Theme>, } -impl<Theme> Svg<Theme> -where - Theme: StyleSheet, -{ +impl<Theme> Svg<Theme> { /// 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 { handle: handle.into(), width: Length::Fill, height: Length::Shrink, 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 /// provided path. #[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)) } @@ -80,15 +80,14 @@ where /// Sets the style variant of this [`Svg`]. #[must_use] - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { - self.style = style.into(); + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { + self.style = style; self } } impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Svg<Theme> where - Theme: iced_style::svg::StyleSheet, Renderer: svg::Renderer, { fn size(&self) -> Size<Length> { @@ -158,12 +157,14 @@ where ..bounds }; - let appearance = if is_mouse_over { - theme.hovered(&self.style) + let status = if is_mouse_over { + Status::Hovered } else { - theme.appearance(&self.style) + Status::Idle }; + let appearance = (self.style)(theme, status); + renderer.draw( self.handle.clone(), appearance.color, @@ -184,10 +185,51 @@ where impl<'a, Message, Theme, Renderer> From<Svg<Theme>> for Element<'a, Message, Theme, Renderer> where - Theme: iced_style::svg::StyleSheet + 'a, + Theme: 'a, Renderer: svg::Renderer + 'a, { fn from(icon: Svg<Theme>) -> Element<'a, Message, Theme, Renderer> { 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 + } +} diff --git a/widget/src/text_editor.rs b/widget/src/text_editor.rs index bad3ef4d..018ffd9c 100644 --- a/widget/src/text_editor.rs +++ b/widget/src/text_editor.rs @@ -11,7 +11,8 @@ use crate::core::text::highlighter::{self, Highlighter}; use crate::core::text::{self, LineHeight}; use crate::core::widget::{self, Widget}; 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; @@ -19,7 +20,6 @@ use std::fmt; use std::ops::DerefMut; use std::sync::Arc; -pub use crate::style::text_editor::{Appearance, StyleSheet}; pub use text::editor::{Action, Edit, Motion}; /// A multi-line text input. @@ -32,7 +32,6 @@ pub struct TextEditor< Renderer = crate::Renderer, > where Highlighter: text::Highlighter, - Theme: StyleSheet, Renderer: text::Renderer, { content: &'a Content<Renderer>, @@ -42,7 +41,7 @@ pub struct TextEditor< width: Length, height: Length, padding: Padding, - style: Theme::Style, + style: Style<Theme>, on_edit: Option<Box<dyn Fn(Action) -> Message + 'a>>, highlighter_settings: Highlighter::Settings, highlighter_format: fn( @@ -54,11 +53,13 @@ pub struct TextEditor< impl<'a, Message, Theme, Renderer> TextEditor<'a, highlighter::PlainText, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { /// 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 { content, font: None, @@ -67,7 +68,7 @@ where width: Length::Fill, height: Length::Shrink, padding: Padding::new(5.0), - style: Default::default(), + style: Theme::default_style(), on_edit: None, highlighter_settings: (), highlighter_format: |_highlight, _theme| { @@ -81,7 +82,6 @@ impl<'a, Highlighter, Message, Theme, Renderer> TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, - Theme: StyleSheet, Renderer: text::Renderer, { /// Sets the height of the [`TextEditor`]. @@ -142,7 +142,7 @@ where } /// 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 } @@ -306,7 +306,6 @@ impl<'a, Highlighter, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, - Theme: StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> widget::tree::Tag { @@ -496,16 +495,18 @@ where let is_disabled = self.on_edit.is_none(); let is_mouse_over = cursor.is_over(bounds); - let appearance = if is_disabled { - theme.disabled(&self.style) + let status = if is_disabled { + Status::Disabled } else if state.is_focused { - theme.focused(&self.style) + Status::Focused } else if is_mouse_over { - theme.hovered(&self.style) + Status::Hovered } else { - theme.active(&self.style) + Status::Active }; + let appearance = (self.style)(theme, status); + renderer.fill_quad( renderer::Quad { bounds, @@ -551,7 +552,7 @@ where }, ..renderer::Quad::default() }, - theme.value_color(&self.style), + appearance.value, ); } } @@ -564,7 +565,7 @@ where bounds: range, ..renderer::Quad::default() }, - theme.selection_color(&self.style), + appearance.selection, ); } } @@ -600,7 +601,7 @@ impl<'a, Highlighter, Message, Theme, Renderer> where Highlighter: text::Highlighter, Message: 'a, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer, { 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 + }, + } +} diff --git a/widget/src/text_input.rs b/widget/src/text_input.rs index 92c4892c..449524fc 100644 --- a/widget/src/text_input.rs +++ b/widget/src/text_input.rs @@ -27,19 +27,16 @@ use crate::core::widget::operation::{self, Operation}; use crate::core::widget::tree::{self, Tree}; use crate::core::window; use crate::core::{ - Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Background, Border, Color, Element, Layout, Length, Padding, Pixels, Point, + Rectangle, Shell, Size, Theme, Vector, Widget, }; use crate::runtime::Command; -pub use iced_style::text_input::{Appearance, StyleSheet}; - /// A field that can be filled with text. /// /// # Example /// ```no_run -/// # pub type TextInput<'a, Message> = -/// # iced_widget::TextInput<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # pub type TextInput<'a, Message> = iced_widget::TextInput<'a, Message>; /// # /// #[derive(Debug, Clone)] /// enum Message { @@ -63,7 +60,6 @@ pub struct TextInput< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: text::Renderer, { id: Option<Id>, @@ -79,7 +75,7 @@ pub struct TextInput< on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>, on_submit: Option<Message>, icon: Option<Icon<Renderer::Font>>, - style: Theme::Style, + style: Style<Theme>, } /// The default [`Padding`] of a [`TextInput`]. @@ -88,15 +84,24 @@ pub const DEFAULT_PADDING: Padding = Padding::new(5.0); impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { - /// Creates a new [`TextInput`]. - /// - /// It expects: - /// - a placeholder, - /// - the current value - pub fn new(placeholder: &str, value: &str) -> Self { + /// Creates a new [`TextInput`] with the given placeholder and + /// its current value. + pub fn new(placeholder: &str, value: &str) -> Self + where + Theme: DefaultStyle, + { + Self::with_style(placeholder, value, Theme::default_style()) + } + + /// Creates a new [`TextInput`] with the given placeholder, + /// its current value, and its style. + pub fn with_style( + placeholder: &str, + value: &str, + style: fn(&Theme, Status) -> Appearance, + ) -> Self { TextInput { id: None, placeholder: String::from(placeholder), @@ -111,7 +116,7 @@ where on_paste: None, on_submit: None, icon: None, - style: Default::default(), + style: style.into(), } } @@ -198,7 +203,7 @@ where } /// Sets the style of the [`TextInput`]. - 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 } @@ -213,20 +218,90 @@ where limits: &layout::Limits, value: Option<&Value>, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.size, - self.font, - self.line_height, - self.icon.as_ref(), - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - value.unwrap_or(&self.value), - &self.placeholder, - self.is_secure, - ) + let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>(); + let value = value.unwrap_or(&self.value); + + let font = self.font.unwrap_or_else(|| renderer.default_font()); + let text_size = self.size.unwrap_or_else(|| renderer.default_size()); + let padding = self.padding.fit(Size::ZERO, limits.max()); + let height = self.line_height.to_absolute(text_size); + + let limits = limits.width(self.width).shrink(padding); + let text_bounds = limits.resolve(self.width, height, Size::ZERO); + + let placeholder_text = Text { + font, + line_height: self.line_height, + content: &self.placeholder, + bounds: Size::new(f32::INFINITY, text_bounds.height), + size: text_size, + horizontal_alignment: alignment::Horizontal::Left, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + state.placeholder.update(placeholder_text); + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + state.value.update(Text { + content: &value.to_string(), + ..placeholder_text + }); + + if let Some(icon) = &self.icon { + let icon_text = Text { + line_height: self.line_height, + content: &icon.code_point.to_string(), + font: icon.font, + size: icon.size.unwrap_or_else(|| renderer.default_size()), + bounds: Size::new(f32::INFINITY, text_bounds.height), + horizontal_alignment: alignment::Horizontal::Center, + vertical_alignment: alignment::Vertical::Center, + shaping: text::Shaping::Advanced, + }; + + state.icon.update(icon_text); + + let icon_width = state.icon.min_width(); + + let (text_position, icon_position) = match icon.side { + Side::Left => ( + Point::new( + padding.left + icon_width + icon.spacing, + padding.top, + ), + Point::new(padding.left, padding.top), + ), + Side::Right => ( + Point::new(padding.left, padding.top), + Point::new( + padding.left + text_bounds.width - icon_width, + padding.top, + ), + ), + }; + + let text_node = layout::Node::new( + text_bounds - Size::new(icon_width + icon.spacing, 0.0), + ) + .move_to(text_position); + + let icon_node = + layout::Node::new(Size::new(icon_width, text_bounds.height)) + .move_to(icon_position); + + layout::Node::with_children( + text_bounds.expand(padding), + vec![text_node, icon_node], + ) + } else { + let text = layout::Node::new(text_bounds) + .move_to(Point::new(padding.left, padding.top)); + + layout::Node::with_children(text_bounds.expand(padding), vec![text]) + } } /// Draws the [`TextInput`] with the given [`Renderer`], overriding its @@ -243,19 +318,173 @@ where value: Option<&Value>, viewport: &Rectangle, ) { - draw( - renderer, - theme, - layout, - cursor, - tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - value.unwrap_or(&self.value), - self.on_input.is_none(), - self.is_secure, - self.icon.as_ref(), - &self.style, - viewport, + let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>(); + let value = value.unwrap_or(&self.value); + let is_disabled = self.on_input.is_none(); + + let secure_value = self.is_secure.then(|| value.secure()); + let value = secure_value.as_ref().unwrap_or(value); + + let bounds = layout.bounds(); + + let mut children_layout = layout.children(); + let text_bounds = children_layout.next().unwrap().bounds(); + + let is_mouse_over = cursor.is_over(bounds); + + let status = if is_disabled { + Status::Disabled + } else if state.is_focused() { + Status::Focused + } else if is_mouse_over { + Status::Hovered + } else { + Status::Active + }; + + let appearance = (self.style)(theme, status); + + renderer.fill_quad( + renderer::Quad { + bounds, + border: appearance.border, + ..renderer::Quad::default() + }, + appearance.background, ); + + if self.icon.is_some() { + let icon_layout = children_layout.next().unwrap(); + + renderer.fill_paragraph( + &state.icon, + icon_layout.bounds().center(), + appearance.icon, + *viewport, + ); + } + + let text = value.to_string(); + + let (cursor, offset) = if let Some(focus) = state + .is_focused + .as_ref() + .filter(|focus| focus.is_window_focused) + { + match state.cursor.state(value) { + cursor::State::Index(position) => { + let (text_value_width, offset) = + measure_cursor_and_scroll_offset( + &state.value, + text_bounds, + position, + ); + + let is_cursor_visible = ((focus.now - focus.updated_at) + .as_millis() + / CURSOR_BLINK_INTERVAL_MILLIS) + % 2 + == 0; + + let cursor = if is_cursor_visible { + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + text_value_width, + y: text_bounds.y, + width: 1.0, + height: text_bounds.height, + }, + ..renderer::Quad::default() + }, + appearance.value, + )) + } else { + None + }; + + (cursor, offset) + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + let (left_position, left_offset) = + measure_cursor_and_scroll_offset( + &state.value, + text_bounds, + left, + ); + + let (right_position, right_offset) = + measure_cursor_and_scroll_offset( + &state.value, + text_bounds, + right, + ); + + let width = right_position - left_position; + + ( + Some(( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + left_position, + y: text_bounds.y, + width, + height: text_bounds.height, + }, + ..renderer::Quad::default() + }, + appearance.selection, + )), + if end == right { + right_offset + } else { + left_offset + }, + ) + } + } + } else { + (None, 0.0) + }; + + let draw = |renderer: &mut Renderer, viewport| { + if let Some((cursor, color)) = cursor { + renderer.with_translation( + Vector::new(-offset, 0.0), + |renderer| { + renderer.fill_quad(cursor, color); + }, + ); + } else { + renderer.with_translation(Vector::ZERO, |_| {}); + } + + renderer.fill_paragraph( + if text.is_empty() { + &state.placeholder + } else { + &state.value + }, + Point::new(text_bounds.x, text_bounds.center_y()) + - Vector::new(offset, 0.0), + if text.is_empty() { + appearance.placeholder + } else { + appearance.value + }, + viewport, + ); + }; + + if cursor.is_some() { + renderer + .with_layer(text_bounds, |renderer| draw(renderer, *viewport)); + } else { + draw(renderer, text_bounds); + } } } @@ -263,7 +492,6 @@ impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for TextInput<'a, Message, Theme, Renderer> where Message: Clone, - Theme: StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -299,20 +527,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout( - renderer, - limits, - self.width, - self.padding, - self.size, - self.font, - self.line_height, - self.icon.as_ref(), - tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - &self.value, - &self.placeholder, - self.is_secure, - ) + self.layout(tree, renderer, limits, None) } fn operate( @@ -339,23 +554,468 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - renderer, - clipboard, - shell, - &mut self.value, - self.size, - self.line_height, - self.font, - self.is_secure, - self.on_input.as_deref(), - self.on_paste.as_deref(), - &self.on_submit, - || tree.state.downcast_mut::<State<Renderer::Paragraph>>(), - ) + let update_cache = |state, value| { + replace_paragraph( + renderer, + state, + layout, + value, + self.font, + self.size, + self.line_height, + ); + }; + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { + let state = state::<Renderer>(tree); + + let click_position = if self.on_input.is_some() { + cursor.position_over(layout.bounds()) + } else { + None + }; + + state.is_focused = if click_position.is_some() { + state.is_focused.or_else(|| { + let now = Instant::now(); + + Some(Focus { + updated_at: now, + now, + is_window_focused: true, + }) + }) + } else { + None + }; + + if let Some(cursor_position) = click_position { + let text_layout = layout.children().next().unwrap(); + let target = cursor_position.x - text_layout.bounds().x; + + let click = + mouse::Click::new(cursor_position, state.last_click); + + match click.kind() { + click::Kind::Single => { + let position = if target > 0.0 { + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + find_cursor_position( + text_layout.bounds(), + &value, + state, + target, + ) + } else { + None + } + .unwrap_or(0); + + if state.keyboard_modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + position, + ); + } else { + state.cursor.move_to(position); + } + state.is_dragging = true; + } + click::Kind::Double => { + if self.is_secure { + state.cursor.select_all(&self.value); + } else { + let position = find_cursor_position( + text_layout.bounds(), + &self.value, + state, + target, + ) + .unwrap_or(0); + + state.cursor.select_range( + self.value.previous_start_of_word(position), + self.value.next_end_of_word(position), + ); + } + + state.is_dragging = false; + } + click::Kind::Triple => { + state.cursor.select_all(&self.value); + state.is_dragging = false; + } + } + + state.last_click = Some(click); + + return event::Status::Captured; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) + | Event::Touch(touch::Event::FingerLost { .. }) => { + state::<Renderer>(tree).is_dragging = false; + } + Event::Mouse(mouse::Event::CursorMoved { position }) + | Event::Touch(touch::Event::FingerMoved { position, .. }) => { + let state = state::<Renderer>(tree); + + if state.is_dragging { + let text_layout = layout.children().next().unwrap(); + let target = position.x - text_layout.bounds().x; + + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + + let position = find_cursor_position( + text_layout.bounds(), + &value, + state, + target, + ) + .unwrap_or(0); + + state + .cursor + .select_range(state.cursor.start(&value), position); + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyPressed { + key, text, .. + }) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + let Some(on_input) = &self.on_input else { + return event::Status::Ignored; + }; + + let modifiers = state.keyboard_modifiers; + focus.updated_at = Instant::now(); + + match key.as_ref() { + keyboard::Key::Character("c") + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(&self.value) + { + clipboard.write( + clipboard::Kind::Standard, + self.value.select(start, end).to_string(), + ); + } + + return event::Status::Captured; + } + keyboard::Key::Character("x") + if state.keyboard_modifiers.command() => + { + if let Some((start, end)) = + state.cursor.selection(&self.value) + { + clipboard.write( + clipboard::Kind::Standard, + self.value.select(start, end).to_string(), + ); + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + keyboard::Key::Character("v") + if state.keyboard_modifiers.command() + && !state.keyboard_modifiers.alt() => + { + let content = match state.is_pasting.take() { + Some(content) => content, + None => { + let content: String = clipboard + .read(clipboard::Kind::Standard) + .unwrap_or_default() + .chars() + .filter(|c| !c.is_control()) + .collect(); + + Value::new(&content) + } + }; + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + + editor.paste(content.clone()); + + let message = if let Some(paste) = &self.on_paste { + (paste)(editor.contents()) + } else { + (on_input)(editor.contents()) + }; + shell.publish(message); + + state.is_pasting = Some(content); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + keyboard::Key::Character("a") + if state.keyboard_modifiers.command() => + { + state.cursor.select_all(&self.value); + + return event::Status::Captured; + } + _ => {} + } + + if let Some(text) = text { + state.is_pasting = None; + + if let Some(c) = + text.chars().next().filter(|c| !c.is_control()) + { + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + + editor.insert(c); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + focus.updated_at = Instant::now(); + + update_cache(state, &self.value); + + return event::Status::Captured; + } + } + + match key.as_ref() { + keyboard::Key::Named(key::Named::Enter) => { + if let Some(on_submit) = self.on_submit.clone() { + shell.publish(on_submit); + } + } + keyboard::Key::Named(key::Named::Backspace) => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(&self.value).is_none() + { + if self.is_secure { + let cursor_pos = + state.cursor.end(&self.value); + state.cursor.select_range(0, cursor_pos); + } else { + state + .cursor + .select_left_by_words(&self.value); + } + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.backspace(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + } + keyboard::Key::Named(key::Named::Delete) => { + if platform::is_jump_modifier_pressed(modifiers) + && state.cursor.selection(&self.value).is_none() + { + if self.is_secure { + let cursor_pos = + state.cursor.end(&self.value); + state.cursor.select_range( + cursor_pos, + self.value.len(), + ); + } else { + state + .cursor + .select_right_by_words(&self.value); + } + } + + let mut editor = + Editor::new(&mut self.value, &mut state.cursor); + editor.delete(); + + let message = (on_input)(editor.contents()); + shell.publish(message); + + update_cache(state, &self.value); + } + keyboard::Key::Named(key::Named::ArrowLeft) => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + state + .cursor + .select_left_by_words(&self.value); + } else { + state + .cursor + .move_left_by_words(&self.value); + } + } else if modifiers.shift() { + state.cursor.select_left(&self.value); + } else { + state.cursor.move_left(&self.value); + } + } + keyboard::Key::Named(key::Named::ArrowRight) => { + if platform::is_jump_modifier_pressed(modifiers) + && !self.is_secure + { + if modifiers.shift() { + state + .cursor + .select_right_by_words(&self.value); + } else { + state + .cursor + .move_right_by_words(&self.value); + } + } else if modifiers.shift() { + state.cursor.select_right(&self.value); + } else { + state.cursor.move_right(&self.value); + } + } + keyboard::Key::Named(key::Named::Home) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + 0, + ); + } else { + state.cursor.move_to(0); + } + } + keyboard::Key::Named(key::Named::End) => { + if modifiers.shift() { + state.cursor.select_range( + state.cursor.start(&self.value), + self.value.len(), + ); + } else { + state.cursor.move_to(self.value.len()); + } + } + keyboard::Key::Named(key::Named::Escape) => { + state.is_focused = None; + state.is_dragging = false; + state.is_pasting = None; + + state.keyboard_modifiers = + keyboard::Modifiers::default(); + } + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + } + Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { + let state = state::<Renderer>(tree); + + if state.is_focused.is_some() { + match key.as_ref() { + keyboard::Key::Character("v") => { + state.is_pasting = None; + } + keyboard::Key::Named( + key::Named::Tab + | key::Named::ArrowUp + | key::Named::ArrowDown, + ) => { + return event::Status::Ignored; + } + _ => {} + } + + return event::Status::Captured; + } + + state.is_pasting = None; + } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + let state = state::<Renderer>(tree); + + state.keyboard_modifiers = modifiers; + } + Event::Window(_, window::Event::Unfocused) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + focus.is_window_focused = false; + } + } + Event::Window(_, window::Event::Focused) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + focus.is_window_focused = true; + focus.updated_at = Instant::now(); + + shell.request_redraw(window::RedrawRequest::NextFrame); + } + } + Event::Window(_, window::Event::RedrawRequested(now)) => { + let state = state::<Renderer>(tree); + + if let Some(focus) = &mut state.is_focused { + if focus.is_window_focused { + focus.now = now; + + let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS + - (now - focus.updated_at).as_millis() + % CURSOR_BLINK_INTERVAL_MILLIS; + + shell.request_redraw(window::RedrawRequest::At( + now + Duration::from_millis( + millis_until_redraw as u64, + ), + )); + } + } + } + _ => {} + } + + event::Status::Ignored } fn draw( @@ -368,19 +1028,7 @@ where cursor: mouse::Cursor, viewport: &Rectangle, ) { - draw( - renderer, - theme, - layout, - cursor, - tree.state.downcast_ref::<State<Renderer::Paragraph>>(), - &self.value, - self.on_input.is_none(), - self.is_secure, - self.icon.as_ref(), - &self.style, - viewport, - ); + self.draw(tree, renderer, theme, layout, cursor, None, viewport); } fn mouse_interaction( @@ -391,7 +1039,15 @@ where _viewport: &Rectangle, _renderer: &Renderer, ) -> mouse::Interaction { - mouse_interaction(layout, cursor, self.on_input.is_none()) + if cursor.is_over(layout.bounds()) { + if self.on_input.is_none() { + mouse::Interaction::NotAllowed + } else { + mouse::Interaction::Text + } + } else { + mouse::Interaction::default() + } } } @@ -399,7 +1055,7 @@ impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a + Clone, - Theme: StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -488,767 +1144,6 @@ pub fn select_all<Message: 'static>(id: Id) -> Command<Message> { Command::widget(operation::text_input::select_all(id.0)) } -/// Computes the layout of a [`TextInput`]. -pub fn layout<Renderer>( - renderer: &Renderer, - limits: &layout::Limits, - width: Length, - padding: Padding, - size: Option<Pixels>, - font: Option<Renderer::Font>, - line_height: text::LineHeight, - icon: Option<&Icon<Renderer::Font>>, - state: &mut State<Renderer::Paragraph>, - value: &Value, - placeholder: &str, - is_secure: bool, -) -> layout::Node -where - Renderer: text::Renderer, -{ - let font = font.unwrap_or_else(|| renderer.default_font()); - let text_size = size.unwrap_or_else(|| renderer.default_size()); - let padding = padding.fit(Size::ZERO, limits.max()); - let height = line_height.to_absolute(text_size); - - let limits = limits.width(width).shrink(padding); - let text_bounds = limits.resolve(width, height, Size::ZERO); - - let placeholder_text = Text { - font, - line_height, - content: placeholder, - bounds: Size::new(f32::INFINITY, text_bounds.height), - size: text_size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }; - - state.placeholder.update(placeholder_text); - - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - state.value.update(Text { - content: &value.to_string(), - ..placeholder_text - }); - - if let Some(icon) = icon { - let icon_text = Text { - line_height, - content: &icon.code_point.to_string(), - font: icon.font, - size: icon.size.unwrap_or_else(|| renderer.default_size()), - bounds: Size::new(f32::INFINITY, text_bounds.height), - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, - shaping: text::Shaping::Advanced, - }; - - state.icon.update(icon_text); - - let icon_width = state.icon.min_width(); - - let (text_position, icon_position) = match icon.side { - Side::Left => ( - Point::new( - padding.left + icon_width + icon.spacing, - padding.top, - ), - Point::new(padding.left, padding.top), - ), - Side::Right => ( - Point::new(padding.left, padding.top), - Point::new( - padding.left + text_bounds.width - icon_width, - padding.top, - ), - ), - }; - - let text_node = layout::Node::new( - text_bounds - Size::new(icon_width + icon.spacing, 0.0), - ) - .move_to(text_position); - - let icon_node = - layout::Node::new(Size::new(icon_width, text_bounds.height)) - .move_to(icon_position); - - layout::Node::with_children( - text_bounds.expand(padding), - vec![text_node, icon_node], - ) - } else { - let text = layout::Node::new(text_bounds) - .move_to(Point::new(padding.left, padding.top)); - - layout::Node::with_children(text_bounds.expand(padding), vec![text]) - } -} - -/// Processes an [`Event`] and updates the [`State`] of a [`TextInput`] -/// accordingly. -pub fn update<'a, Message, Renderer>( - event: Event, - layout: Layout<'_>, - cursor: mouse::Cursor, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - value: &mut Value, - size: Option<Pixels>, - line_height: text::LineHeight, - font: Option<Renderer::Font>, - is_secure: bool, - on_input: Option<&dyn Fn(String) -> Message>, - on_paste: Option<&dyn Fn(String) -> Message>, - on_submit: &Option<Message>, - state: impl FnOnce() -> &'a mut State<Renderer::Paragraph>, -) -> event::Status -where - Message: Clone, - Renderer: text::Renderer, -{ - let update_cache = |state, value| { - replace_paragraph( - renderer, - state, - layout, - value, - font, - size, - line_height, - ); - }; - - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); - - let click_position = if on_input.is_some() { - cursor.position_over(layout.bounds()) - } else { - None - }; - - state.is_focused = if click_position.is_some() { - state.is_focused.or_else(|| { - let now = Instant::now(); - - Some(Focus { - updated_at: now, - now, - is_window_focused: true, - }) - }) - } else { - None - }; - - if let Some(cursor_position) = click_position { - let text_layout = layout.children().next().unwrap(); - let target = cursor_position.x - text_layout.bounds().x; - - let click = - mouse::Click::new(cursor_position, state.last_click); - - match click.kind() { - click::Kind::Single => { - let position = if target > 0.0 { - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - find_cursor_position( - text_layout.bounds(), - &value, - state, - target, - ) - } else { - None - } - .unwrap_or(0); - - if state.keyboard_modifiers.shift() { - state.cursor.select_range( - state.cursor.start(value), - position, - ); - } else { - state.cursor.move_to(position); - } - state.is_dragging = true; - } - click::Kind::Double => { - if is_secure { - state.cursor.select_all(value); - } else { - let position = find_cursor_position( - text_layout.bounds(), - value, - state, - target, - ) - .unwrap_or(0); - - state.cursor.select_range( - value.previous_start_of_word(position), - value.next_end_of_word(position), - ); - } - - state.is_dragging = false; - } - click::Kind::Triple => { - state.cursor.select_all(value); - state.is_dragging = false; - } - } - - state.last_click = Some(click); - - return event::Status::Captured; - } - } - Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) - | Event::Touch(touch::Event::FingerLost { .. }) => { - state().is_dragging = false; - } - Event::Mouse(mouse::Event::CursorMoved { position }) - | Event::Touch(touch::Event::FingerMoved { position, .. }) => { - let state = state(); - - if state.is_dragging { - let text_layout = layout.children().next().unwrap(); - let target = position.x - text_layout.bounds().x; - - let value = if is_secure { - value.secure() - } else { - value.clone() - }; - - let position = find_cursor_position( - text_layout.bounds(), - &value, - state, - target, - ) - .unwrap_or(0); - - state - .cursor - .select_range(state.cursor.start(&value), position); - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyPressed { key, text, .. }) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - let Some(on_input) = on_input else { - return event::Status::Ignored; - }; - - let modifiers = state.keyboard_modifiers; - focus.updated_at = Instant::now(); - - match key.as_ref() { - keyboard::Key::Character("c") - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard.write( - clipboard::Kind::Standard, - value.select(start, end).to_string(), - ); - } - - return event::Status::Captured; - } - keyboard::Key::Character("x") - if state.keyboard_modifiers.command() => - { - if let Some((start, end)) = - state.cursor.selection(value) - { - clipboard.write( - clipboard::Kind::Standard, - value.select(start, end).to_string(), - ); - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - - return event::Status::Captured; - } - keyboard::Key::Character("v") - if state.keyboard_modifiers.command() - && !state.keyboard_modifiers.alt() => - { - let content = match state.is_pasting.take() { - Some(content) => content, - None => { - let content: String = clipboard - .read(clipboard::Kind::Standard) - .unwrap_or_default() - .chars() - .filter(|c| !c.is_control()) - .collect(); - - Value::new(&content) - } - }; - - let mut editor = Editor::new(value, &mut state.cursor); - - editor.paste(content.clone()); - - let message = if let Some(paste) = &on_paste { - (paste)(editor.contents()) - } else { - (on_input)(editor.contents()) - }; - shell.publish(message); - - state.is_pasting = Some(content); - - update_cache(state, value); - - return event::Status::Captured; - } - keyboard::Key::Character("a") - if state.keyboard_modifiers.command() => - { - state.cursor.select_all(value); - - return event::Status::Captured; - } - _ => {} - } - - if let Some(text) = text { - state.is_pasting = None; - - if let Some(c) = - text.chars().next().filter(|c| !c.is_control()) - { - let mut editor = Editor::new(value, &mut state.cursor); - - editor.insert(c); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - focus.updated_at = Instant::now(); - - update_cache(state, value); - - return event::Status::Captured; - } - } - - match key.as_ref() { - keyboard::Key::Named(key::Named::Enter) => { - if let Some(on_submit) = on_submit.clone() { - shell.publish(on_submit); - } - } - keyboard::Key::Named(key::Named::Backspace) => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(value); - state.cursor.select_range(0, cursor_pos); - } else { - state.cursor.select_left_by_words(value); - } - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.backspace(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - } - keyboard::Key::Named(key::Named::Delete) => { - if platform::is_jump_modifier_pressed(modifiers) - && state.cursor.selection(value).is_none() - { - if is_secure { - let cursor_pos = state.cursor.end(value); - state - .cursor - .select_range(cursor_pos, value.len()); - } else { - state.cursor.select_right_by_words(value); - } - } - - let mut editor = Editor::new(value, &mut state.cursor); - editor.delete(); - - let message = (on_input)(editor.contents()); - shell.publish(message); - - update_cache(state, value); - } - keyboard::Key::Named(key::Named::ArrowLeft) => { - if platform::is_jump_modifier_pressed(modifiers) - && !is_secure - { - if modifiers.shift() { - state.cursor.select_left_by_words(value); - } else { - state.cursor.move_left_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_left(value); - } else { - state.cursor.move_left(value); - } - } - keyboard::Key::Named(key::Named::ArrowRight) => { - if platform::is_jump_modifier_pressed(modifiers) - && !is_secure - { - if modifiers.shift() { - state.cursor.select_right_by_words(value); - } else { - state.cursor.move_right_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_right(value); - } else { - state.cursor.move_right(value); - } - } - keyboard::Key::Named(key::Named::Home) => { - if modifiers.shift() { - state - .cursor - .select_range(state.cursor.start(value), 0); - } else { - state.cursor.move_to(0); - } - } - keyboard::Key::Named(key::Named::End) => { - if modifiers.shift() { - state.cursor.select_range( - state.cursor.start(value), - value.len(), - ); - } else { - state.cursor.move_to(value.len()); - } - } - keyboard::Key::Named(key::Named::Escape) => { - state.is_focused = None; - state.is_dragging = false; - state.is_pasting = None; - - state.keyboard_modifiers = - keyboard::Modifiers::default(); - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => {} - } - - return event::Status::Captured; - } - } - Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { - let state = state(); - - if state.is_focused.is_some() { - match key.as_ref() { - keyboard::Key::Character("v") => { - state.is_pasting = None; - } - keyboard::Key::Named( - key::Named::Tab - | key::Named::ArrowUp - | key::Named::ArrowDown, - ) => { - return event::Status::Ignored; - } - _ => {} - } - - return event::Status::Captured; - } - - state.is_pasting = None; - } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - - state.keyboard_modifiers = modifiers; - } - Event::Window(_, window::Event::Unfocused) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - focus.is_window_focused = false; - } - } - Event::Window(_, window::Event::Focused) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - focus.is_window_focused = true; - focus.updated_at = Instant::now(); - - shell.request_redraw(window::RedrawRequest::NextFrame); - } - } - Event::Window(_, window::Event::RedrawRequested(now)) => { - let state = state(); - - if let Some(focus) = &mut state.is_focused { - if focus.is_window_focused { - focus.now = now; - - let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() - % CURSOR_BLINK_INTERVAL_MILLIS; - - shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis(millis_until_redraw as u64), - )); - } - } - } - _ => {} - } - - event::Status::Ignored -} - -/// Draws the [`TextInput`] with the given [`Renderer`], overriding its -/// [`Value`] if provided. -/// -/// [`Renderer`]: text::Renderer -pub fn draw<Theme, Renderer>( - renderer: &mut Renderer, - theme: &Theme, - layout: Layout<'_>, - cursor: mouse::Cursor, - state: &State<Renderer::Paragraph>, - value: &Value, - is_disabled: bool, - is_secure: bool, - icon: Option<&Icon<Renderer::Font>>, - style: &Theme::Style, - viewport: &Rectangle, -) where - Theme: StyleSheet, - Renderer: text::Renderer, -{ - let secure_value = is_secure.then(|| value.secure()); - let value = secure_value.as_ref().unwrap_or(value); - - let bounds = layout.bounds(); - - let mut children_layout = layout.children(); - let text_bounds = children_layout.next().unwrap().bounds(); - - let is_mouse_over = cursor.is_over(bounds); - - let appearance = if is_disabled { - theme.disabled(style) - } else if state.is_focused() { - theme.focused(style) - } else if is_mouse_over { - theme.hovered(style) - } else { - theme.active(style) - }; - - renderer.fill_quad( - renderer::Quad { - bounds, - border: appearance.border, - ..renderer::Quad::default() - }, - appearance.background, - ); - - if icon.is_some() { - let icon_layout = children_layout.next().unwrap(); - - renderer.fill_paragraph( - &state.icon, - icon_layout.bounds().center(), - appearance.icon_color, - *viewport, - ); - } - - let text = value.to_string(); - - let (cursor, offset) = if let Some(focus) = state - .is_focused - .as_ref() - .filter(|focus| focus.is_window_focused) - { - match state.cursor.state(value) { - cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset( - &state.value, - text_bounds, - position, - ); - - let is_cursor_visible = ((focus.now - focus.updated_at) - .as_millis() - / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; - - let cursor = if is_cursor_visible { - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - ..renderer::Quad::default() - }, - theme.value_color(style), - )) - } else { - None - }; - - (cursor, offset) - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - - let (left_position, left_offset) = - measure_cursor_and_scroll_offset( - &state.value, - text_bounds, - left, - ); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset( - &state.value, - text_bounds, - right, - ); - - let width = right_position - left_position; - - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + left_position, - y: text_bounds.y, - width, - height: text_bounds.height, - }, - ..renderer::Quad::default() - }, - theme.selection_color(style), - )), - if end == right { - right_offset - } else { - left_offset - }, - ) - } - } - } else { - (None, 0.0) - }; - - let draw = |renderer: &mut Renderer, viewport| { - if let Some((cursor, color)) = cursor { - renderer.with_translation(Vector::new(-offset, 0.0), |renderer| { - renderer.fill_quad(cursor, color); - }); - } else { - renderer.with_translation(Vector::ZERO, |_| {}); - } - - renderer.fill_paragraph( - if text.is_empty() { - &state.placeholder - } else { - &state.value - }, - Point::new(text_bounds.x, text_bounds.center_y()) - - Vector::new(offset, 0.0), - if text.is_empty() { - theme.placeholder_color(style) - } else if is_disabled { - theme.disabled_color(style) - } else { - theme.value_color(style) - }, - viewport, - ); - }; - - if cursor.is_some() { - renderer.with_layer(text_bounds, |renderer| draw(renderer, *viewport)); - } else { - draw(renderer, text_bounds); - } -} - -/// Computes the current [`mouse::Interaction`] of the [`TextInput`]. -pub fn mouse_interaction( - layout: Layout<'_>, - cursor: mouse::Cursor, - is_disabled: bool, -) -> mouse::Interaction { - if cursor.is_over(layout.bounds()) { - if is_disabled { - mouse::Interaction::NotAllowed - } else { - mouse::Interaction::Text - } - } else { - mouse::Interaction::default() - } -} - /// The state of a [`TextInput`]. #[derive(Debug, Default, Clone)] pub struct State<P: text::Paragraph> { @@ -1264,6 +1159,12 @@ pub struct State<P: text::Paragraph> { // TODO: Add stateful horizontal scrolling offset } +fn state<Renderer: text::Renderer>( + tree: &mut Tree, +) -> &mut State<Renderer::Paragraph> { + tree.state.downcast_mut::<State<Renderer::Paragraph>>() +} + #[derive(Debug, Clone, Copy)] struct Focus { updated_at: Instant, @@ -1479,3 +1380,95 @@ fn replace_paragraph<Renderer>( } const CURSOR_BLINK_INTERVAL_MILLIS: u128 = 500; + +/// The possible status of a [`TextInput`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Status { + /// The [`TextInput`] can be interacted with. + Active, + /// The [`TextInput`] is being hovered. + Hovered, + /// The [`TextInput`] is focused. + Focused, + /// The [`TextInput`] 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 [`TextInput`]. +pub type Style<Theme> = fn(&Theme, Status) -> Appearance; + +/// The default style of a [`TextInput`]. +pub trait DefaultStyle { + /// Returns the default style of a [`TextInput`]. + 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 [`TextInput`]. +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 + }, + } +} diff --git a/widget/src/themer.rs b/widget/src/themer.rs index 3a5fd823..a7eabd2c 100644 --- a/widget/src/themer.rs +++ b/widget/src/themer.rs @@ -7,58 +7,68 @@ use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::widget::Operation; use crate::core::{ - Background, Clipboard, Element, Layout, Length, Point, Rectangle, Shell, - Size, Vector, Widget, + Background, Clipboard, Color, Element, Layout, Length, Point, Rectangle, + Shell, Size, Vector, Widget, }; -use crate::style::application; + +use std::marker::PhantomData; /// A widget that applies any `Theme` to its contents. /// /// This widget can be useful to leverage multiple `Theme` /// types in an application. #[allow(missing_debug_implementations)] -pub struct Themer<'a, Message, Theme, Renderer> +pub struct Themer<'a, Message, Theme, NewTheme, F, Renderer = crate::Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, - Theme: application::StyleSheet, { - content: Element<'a, Message, Theme, Renderer>, - theme: Theme, - style: Theme::Style, - show_background: bool, + content: Element<'a, Message, NewTheme, Renderer>, + to_theme: F, + text_color: Option<fn(&NewTheme) -> Color>, + 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 + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, - Theme: application::StyleSheet, { /// Creates an empty [`Themer`] that applies the given `Theme` /// to the provided `content`. - pub fn new<T>(theme: Theme, content: T) -> Self + pub fn new<T>(to_theme: F, content: T) -> Self where - T: Into<Element<'a, Message, Theme, Renderer>>, + T: Into<Element<'a, Message, NewTheme, Renderer>>, { Self { content: content.into(), - theme, - style: Theme::Style::default(), - show_background: false, + to_theme, + text_color: None, + background: None, + old_theme: PhantomData, } } - /// Sets whether to draw the background color of the `Theme`. - pub fn background(mut self, background: bool) -> Self { - self.show_background = background; + /// Sets the default text [`Color`] of the [`Themer`]. + pub fn text_color(mut self, f: fn(&NewTheme) -> Color) -> Self { + 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 } } -impl<'a, AnyTheme, Message, Theme, Renderer> Widget<Message, AnyTheme, Renderer> - for Themer<'a, Message, Theme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> Widget<Message, Theme, Renderer> + for Themer<'a, Message, Theme, NewTheme, F, Renderer> where + F: Fn(&Theme) -> NewTheme, Renderer: crate::core::Renderer, - Theme: application::StyleSheet, { fn tag(&self) -> tree::Tag { self.content.as_widget().tag() @@ -134,38 +144,36 @@ where &self, tree: &Tree, renderer: &mut Renderer, - _theme: &AnyTheme, - _style: &renderer::Style, + theme: &Theme, + style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, 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( renderer, &container::Appearance { - background: Some(Background::Color( - appearance.background_color, - )), + background: Some(background(&theme)), ..container::Appearance::default() }, layout.bounds(), ); } - self.content.as_widget().draw( - tree, - renderer, - &self.theme, - &renderer::Style { - text_color: appearance.text_color, - }, - layout, - cursor, - viewport, - ); + let style = if let Some(text_color) = self.text_color { + renderer::Style { + text_color: text_color(&theme), + } + } else { + *style + }; + + self.content + .as_widget() + .draw(tree, renderer, &theme, &style, layout, cursor, viewport); } fn overlay<'b>( @@ -174,15 +182,15 @@ where layout: Layout<'_>, renderer: &Renderer, translation: Vector, - ) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> { - struct Overlay<'a, Message, Theme, Renderer> { - theme: &'a Theme, - content: overlay::Element<'a, Message, Theme, Renderer>, + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { + struct Overlay<'a, Message, Theme, NewTheme, Renderer> { + to_theme: &'a dyn Fn(&Theme) -> NewTheme, + content: overlay::Element<'a, Message, NewTheme, Renderer>, } - impl<'a, AnyTheme, Message, Theme, Renderer> - overlay::Overlay<Message, AnyTheme, Renderer> - for Overlay<'a, Message, Theme, Renderer> + impl<'a, Message, Theme, NewTheme, Renderer> + overlay::Overlay<Message, Theme, Renderer> + for Overlay<'a, Message, Theme, NewTheme, Renderer> where Renderer: crate::core::Renderer, { @@ -197,13 +205,18 @@ where fn draw( &self, renderer: &mut Renderer, - _theme: &AnyTheme, + theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, ) { - self.content - .draw(renderer, self.theme, style, layout, cursor); + self.content.draw( + renderer, + &(self.to_theme)(theme), + style, + layout, + cursor, + ); } fn on_event( @@ -252,12 +265,12 @@ where &'b mut self, layout: Layout<'_>, renderer: &Renderer, - ) -> Option<overlay::Element<'b, Message, AnyTheme, Renderer>> + ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> { self.content .overlay(layout, renderer) .map(|content| Overlay { - theme: self.theme, + to_theme: &self.to_theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) @@ -268,24 +281,26 @@ where .as_widget_mut() .overlay(tree, layout, renderer, translation) .map(|content| Overlay { - theme: &self.theme, + to_theme: &self.to_theme, content, }) .map(|overlay| overlay::Element::new(Box::new(overlay))) } } -impl<'a, AnyTheme, Message, Theme, Renderer> - From<Themer<'a, Message, Theme, Renderer>> - for Element<'a, Message, AnyTheme, Renderer> +impl<'a, Message, Theme, NewTheme, F, Renderer> + From<Themer<'a, Message, Theme, NewTheme, F, Renderer>> + for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: 'a + application::StyleSheet, + Theme: 'a, + NewTheme: 'a, + F: Fn(&Theme) -> NewTheme + 'a, Renderer: 'a + crate::core::Renderer, { fn from( - themer: Themer<'a, Message, Theme, Renderer>, - ) -> Element<'a, Message, AnyTheme, Renderer> { + themer: Themer<'a, Message, Theme, NewTheme, F, Renderer>, + ) -> Element<'a, Message, Theme, Renderer> { Element::new(themer) } } diff --git a/widget/src/toggler.rs b/widget/src/toggler.rs index 4e3925ba..9e81ba33 100644 --- a/widget/src/toggler.rs +++ b/widget/src/toggler.rs @@ -9,19 +9,16 @@ use crate::core::touch; use crate::core::widget; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Border, Clipboard, Element, Event, Layout, Length, Pixels, Rectangle, - Shell, Size, Widget, + Border, Clipboard, Color, Element, Event, Layout, Length, Pixels, + Rectangle, Shell, Size, Theme, Widget, }; -pub use crate::style::toggler::{Appearance, StyleSheet}; - /// A toggler widget. /// /// # Example /// /// ```no_run -/// # type Toggler<'a, Message> = -/// # iced_widget::Toggler<'a, Message, iced_widget::style::Theme, iced_widget::renderer::Renderer>; +/// # type Toggler<'a, Message> = iced_widget::Toggler<'a, Message>; /// # /// pub enum Message { /// TogglerToggled(bool), @@ -38,7 +35,6 @@ pub struct Toggler< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: StyleSheet, Renderer: text::Renderer, { is_toggled: bool, @@ -52,16 +48,15 @@ pub struct Toggler< text_shaping: text::Shaping, spacing: f32, font: Option<Renderer::Font>, - style: Theme::Style, + style: Style<Theme>, } impl<'a, Message, Theme, Renderer> Toggler<'a, Message, Theme, Renderer> where - Theme: StyleSheet, Renderer: text::Renderer, { /// The default size of a [`Toggler`]. - pub const DEFAULT_SIZE: f32 = 20.0; + pub const DEFAULT_SIZE: f32 = 16.0; /// Creates a new [`Toggler`]. /// @@ -77,6 +72,7 @@ where f: F, ) -> Self where + Theme: DefaultStyle, F: 'a + Fn(bool) -> Message, { Toggler { @@ -91,7 +87,7 @@ where text_shaping: text::Shaping::Basic, spacing: Self::DEFAULT_SIZE / 2.0, font: None, - style: Default::default(), + style: Theme::default_style(), } } @@ -149,7 +145,7 @@ where } /// 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 } @@ -158,7 +154,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Toggler<'a, Message, Theme, Renderer> where - Theme: StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn tag(&self) -> tree::Tag { @@ -294,12 +289,18 @@ where let bounds = toggler_layout.bounds(); let is_mouse_over = cursor.is_over(layout.bounds()); - let style = if is_mouse_over { - theme.hovered(&self.style, self.is_toggled) + let status = if is_mouse_over { + Status::Hovered { + is_toggled: self.is_toggled, + } } 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 space = SPACE_RATIO * bounds.height; @@ -315,12 +316,12 @@ where bounds: toggler_background_bounds, border: Border { radius: border_radius.into(), - width: style.background_border_width, - color: style.background_border_color, + width: appearance.background_border_width, + color: appearance.background_border_color, }, ..renderer::Quad::default() }, - style.background, + appearance.background, ); let toggler_foreground_bounds = Rectangle { @@ -340,12 +341,12 @@ where bounds: toggler_foreground_bounds, border: Border { radius: border_radius.into(), - width: style.foreground_border_width, - color: style.foreground_border_color, + width: appearance.foreground_border_width, + color: appearance.foreground_border_color, }, ..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> where Message: 'a, - Theme: StyleSheet + crate::text::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -363,3 +364,100 @@ where 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, + } +} diff --git a/widget/src/tooltip.rs b/widget/src/tooltip.rs index d8a1e131..8c8ee983 100644 --- a/widget/src/tooltip.rs +++ b/widget/src/tooltip.rs @@ -20,7 +20,6 @@ pub struct Tooltip< Theme = crate::Theme, Renderer = crate::Renderer, > where - Theme: container::StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -29,12 +28,11 @@ pub struct Tooltip< gap: f32, padding: f32, snap_within_viewport: bool, - style: <Theme as container::StyleSheet>::Style, + style: container::Style<Theme>, } impl<'a, Message, Theme, Renderer> Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { /// The default padding of a [`Tooltip`] drawn by this renderer. @@ -47,7 +45,10 @@ where content: impl Into<Element<'a, Message, Theme, Renderer>>, tooltip: impl Into<Element<'a, Message, Theme, Renderer>>, position: Position, - ) -> Self { + ) -> Self + where + Theme: container::DefaultStyle, + { Tooltip { content: content.into(), tooltip: tooltip.into(), @@ -55,7 +56,7 @@ where gap: 0.0, padding: Self::DEFAULT_PADDING, snap_within_viewport: true, - style: Default::default(), + style: Theme::default_style(), } } @@ -80,7 +81,7 @@ where /// Sets the style of the [`Tooltip`]. pub fn style( mut self, - style: impl Into<<Theme as container::StyleSheet>::Style>, + style: fn(&Theme, container::Status) -> container::Appearance, ) -> Self { self.style = style.into(); self @@ -90,7 +91,6 @@ where impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Tooltip<'a, Message, Theme, Renderer> where - Theme: container::StyleSheet + crate::text::StyleSheet, Renderer: text::Renderer, { fn children(&self) -> Vec<widget::Tree> { @@ -239,7 +239,7 @@ where positioning: self.position, gap: self.gap, padding: self.padding, - style: &self.style, + style: self.style, }))) } else { None @@ -262,7 +262,7 @@ impl<'a, Message, Theme, Renderer> From<Tooltip<'a, Message, Theme, Renderer>> for Element<'a, Message, Theme, Renderer> where Message: 'a, - Theme: container::StyleSheet + crate::text::StyleSheet + 'a, + Theme: 'a, Renderer: text::Renderer + 'a, { fn from( @@ -298,7 +298,6 @@ enum State { struct Overlay<'a, 'b, Message, Theme, Renderer> where - Theme: container::StyleSheet + widget::text::StyleSheet, Renderer: text::Renderer, { position: Point, @@ -310,14 +309,13 @@ where positioning: Position, gap: f32, padding: f32, - style: &'b <Theme as container::StyleSheet>::Style, + style: container::Style<Theme>, } impl<'a, 'b, Message, Theme, Renderer> overlay::Overlay<Message, Theme, Renderer> for Overlay<'a, 'b, Message, Theme, Renderer> where - Theme: container::StyleSheet + widget::text::StyleSheet, Renderer: text::Renderer, { fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node { @@ -426,7 +424,7 @@ where layout: Layout<'_>, 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()); diff --git a/widget/src/vertical_slider.rs b/widget/src/vertical_slider.rs index 8f7c88da..f7030584 100644 --- a/widget/src/vertical_slider.rs +++ b/widget/src/vertical_slider.rs @@ -1,9 +1,9 @@ //! Display an interactive selector of a single value from a range of values. -//! -//! A [`VerticalSlider`] has some local [`State`]. 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::event::{self, Event}; @@ -29,8 +29,7 @@ use crate::core::{ /// /// # Example /// ```no_run -/// # type VerticalSlider<'a, T, Message> = -/// # iced_widget::VerticalSlider<'a, T, Message, iced_widget::style::Theme>; +/// # type VerticalSlider<'a, T, Message> = iced_widget::VerticalSlider<'a, T, Message>; /// # /// #[derive(Clone)] /// pub enum Message { @@ -42,10 +41,7 @@ use crate::core::{ /// VerticalSlider::new(0.0..=100.0, value, Message::SliderChanged); /// ``` #[allow(missing_debug_implementations)] -pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> -where - Theme: StyleSheet, -{ +pub struct VerticalSlider<'a, T, Message, Theme = crate::Theme> { range: RangeInclusive<T>, step: T, shift_step: Option<T>, @@ -55,17 +51,16 @@ where on_release: Option<Message>, width: f32, height: Length, - style: Theme::Style, + style: Style<Theme>, } impl<'a, T, Message, Theme> VerticalSlider<'a, T, Message, Theme> where T: Copy + From<u8> + std::cmp::PartialOrd, Message: Clone, - Theme: StyleSheet, { /// The default width of a [`VerticalSlider`]. - pub const DEFAULT_WIDTH: f32 = 22.0; + pub const DEFAULT_WIDTH: f32 = 16.0; /// Creates a new [`VerticalSlider`]. /// @@ -77,6 +72,7 @@ where /// `Message`. pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self where + Theme: DefaultStyle, F: 'a + Fn(T) -> Message, { let value = if value >= *range.start() { @@ -101,7 +97,7 @@ where on_release: None, width: Self::DEFAULT_WIDTH, height: Length::Fill, - style: Default::default(), + style: Theme::default_style(), } } @@ -137,7 +133,7 @@ where } /// 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 } @@ -162,7 +158,6 @@ impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> where T: Copy + Into<f64> + num_traits::FromPrimitive, Message: Clone, - Theme: StyleSheet, Renderer: core::Renderer, { fn tag(&self) -> tree::Tag { @@ -170,7 +165,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn size(&self) -> Size<Length> { @@ -200,360 +195,287 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - update( - event, - layout, - cursor, - shell, - tree.state.downcast_mut::<State>(), - &mut self.value, - self.default, - &self.range, - self.step, - self.shift_step, - self.on_change.as_ref(), - &self.on_release, - ) - } + let state = tree.state.downcast_mut::<State>(); + let is_dragging = state.is_dragging; + let current_value = self.value; - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &Theme, - _style: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - ) { - draw( - renderer, - layout, - cursor, - tree.state.downcast_ref::<State>(), - self.value, - &self.range, - theme, - &self.style, - ); - } + let locate = |cursor_position: Point| -> Option<T> { + let bounds = layout.bounds(); - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - _viewport: &Rectangle, - _renderer: &Renderer, - ) -> mouse::Interaction { - mouse_interaction(layout, cursor, tree.state.downcast_ref::<State>()) - } -} + let new_value = if cursor_position.y >= bounds.y + bounds.height { + Some(*self.range.start()) + } else if cursor_position.y <= bounds.y { + Some(*self.range.end()) + } else { + let step = if state.keyboard_modifiers.shift() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); -impl<'a, T, Message, Theme, Renderer> - From<VerticalSlider<'a, T, Message, Theme>> - for Element<'a, Message, Theme, Renderer> -where - T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, - Message: Clone + 'a, - Theme: StyleSheet + 'a, - Renderer: core::Renderer + 'a, -{ - fn from( - slider: VerticalSlider<'a, T, Message, Theme>, - ) -> Element<'a, Message, Theme, Renderer> { - Element::new(slider) - } -} + let start = (*self.range.start()).into(); + let end = (*self.range.end()).into(); -/// 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 percent = 1.0 + - f64::from(cursor_position.y - bounds.y) + / f64::from(bounds.height); - let locate = |cursor_position: Point| -> Option<T> { - let bounds = layout.bounds(); + let steps = (percent * (end - start) / step).round(); + let value = steps * step + start; - 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 { + T::from_f64(value) + }; + + new_value + }; + + let increment = |value: T| -> Option<T> { let step = if state.keyboard_modifiers.shift() { - shift_step.unwrap_or(step) + self.shift_step.unwrap_or(self.step) } else { - step + self.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 = (value.into() / step).round(); + let new_value = step * (steps + 1.0); - let steps = (percent * (end - start) / step).round(); - let value = steps * step + start; + if new_value > (*self.range.end()).into() { + return Some(*self.range.end()); + } - T::from_f64(value) + T::from_f64(new_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() { + self.shift_step.unwrap_or(self.step) + } else { + self.step + } + .into(); - 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); - 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()); + } - if new_value < (*range.start()).into() { - return Some(*range.start()); - } + T::from_f64(new_value) + }; - 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)); - let change = |new_value: T| { - if ((*value).into() - new_value.into()).abs() > f64::EPSILON { - shell.publish((on_change)(new_value)); + self.value = 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() + 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()) { - let _ = default.map(change); - state.is_dragging = false; - } else { - let _ = locate(cursor_position).map(change); - state.is_dragging = true; - } + 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) = on_release.clone() { - shell.publish(on_release); + return event::Status::Captured; } - state.is_dragging = false; + } + 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; + 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); + 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; + 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); + 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); + } + _ => (), } - Key::Named(key::Named::ArrowDown) => { - let _ = decrement(current_value).map(change); - } - _ => (), - } - return event::Status::Captured; + return event::Status::Captured; + } } + Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { + state.keyboard_modifiers = modifiers; + } + _ => {} } - Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - state.keyboard_modifiers = modifiers; - } - _ => {} + + event::Status::Ignored } - event::Status::Ignored -} + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::<State>(); + let bounds = layout.bounds(); + let is_mouse_over = cursor.is_over(bounds); -/// 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 style = (self.style)( + theme, + 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 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, + 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() }, - 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, + 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() }, - border: Border { - radius: handle_border_radius, - width: style.handle.border_width, - color: style.handle.border_color, + 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() }, - ..renderer::Quad::default() - }, - style.handle.color, - ); + style.handle.color, + ); + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &Renderer, + ) -> mouse::Interaction { + 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() + } + } } -/// 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() +impl<'a, T, Message, Theme, Renderer> + From<VerticalSlider<'a, T, Message, Theme>> + for Element<'a, Message, Theme, Renderer> +where + T: Copy + Into<f64> + num_traits::FromPrimitive + 'a, + Message: Clone + 'a, + Theme: 'a, + Renderer: core::Renderer + 'a, +{ + fn from( + slider: VerticalSlider<'a, T, Message, Theme>, + ) -> Element<'a, Message, Theme, Renderer> { + Element::new(slider) } } -/// The local state of a [`VerticalSlider`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { +struct State { is_dragging: bool, keyboard_modifiers: keyboard::Modifiers, } - -impl State { - /// Creates a new [`State`]. - pub fn new() -> State { - State::default() - } -} diff --git a/winit/Cargo.toml b/winit/Cargo.toml index 87e600ae..9d65cc1b 100644 --- a/winit/Cargo.toml +++ b/winit/Cargo.toml @@ -24,7 +24,6 @@ multi-window = ["iced_runtime/multi-window"] [dependencies] iced_graphics.workspace = true iced_runtime.workspace = true -iced_style.workspace = true log.workspace = true thiserror.workspace = true diff --git a/winit/src/application.rs b/winit/src/application.rs index 29ee1a71..13d9282d 100644 --- a/winit/src/application.rs +++ b/winit/src/application.rs @@ -10,7 +10,7 @@ use crate::core::renderer; use crate::core::time::Instant; use crate::core::widget::operation; 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::{Executor, Runtime, Subscription}; use crate::graphics::compositor::{self, Compositor}; @@ -18,7 +18,6 @@ use crate::runtime::clipboard; use crate::runtime::program::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::{Command, Debug}; -use crate::style::application::{Appearance, StyleSheet}; use crate::{Clipboard, Error, Proxy, Settings}; use futures::channel::mpsc; @@ -39,7 +38,7 @@ use std::sync::Arc; /// can be toggled by pressing `F12`. pub trait Application: Program where - Self::Theme: StyleSheet, + Self::Theme: DefaultStyle, { /// The data needed to initialize your [`Application`]. type Flags; @@ -64,8 +63,8 @@ where fn theme(&self) -> Self::Theme; /// Returns the `Style` variation of the `Theme`. - fn style(&self) -> <Self::Theme as StyleSheet>::Style { - Default::default() + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() } /// 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 /// settings. pub async fn run<A, E, C>( @@ -105,7 +136,7 @@ where A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use futures::task; use futures::Future; @@ -289,7 +320,7 @@ async fn run_instance<A, E, C>( A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use futures::stream::StreamExt; use winit::event; @@ -612,7 +643,7 @@ pub fn build_user_interface<'a, A: Application>( debug: &mut Debug, ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> where - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { debug.view_started(); let view = application.view(); @@ -643,7 +674,7 @@ pub fn update<A: Application, C, E: Executor>( window: &winit::window::Window, ) where C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); @@ -694,7 +725,7 @@ pub fn run_command<A, C, E>( A: Application, E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use crate::runtime::command; use crate::runtime::system; diff --git a/winit/src/application/state.rs b/winit/src/application/state.rs index c17a3bcc..a0a06933 100644 --- a/winit/src/application/state.rs +++ b/winit/src/application/state.rs @@ -1,4 +1,4 @@ -use crate::application::{self, StyleSheet as _}; +use crate::application; use crate::conversion; use crate::core::mouse; use crate::core::{Color, Size}; @@ -14,7 +14,7 @@ use winit::window::Window; #[allow(missing_debug_implementations)] pub struct State<A: Application> where - A::Theme: application::StyleSheet, + A::Theme: application::DefaultStyle, { title: String, scale_factor: f64, @@ -29,14 +29,14 @@ where impl<A: Application> State<A> where - A::Theme: application::StyleSheet, + A::Theme: application::DefaultStyle, { /// Creates a new [`State`] for the provided [`Application`] and window. pub fn new(application: &A, window: &Window) -> Self { let title = application.title(); let scale_factor = application.scale_factor(); let theme = application.theme(); - let appearance = theme.appearance(&application.style()); + let appearance = application.style(&theme); let viewport = { let physical_size = window.inner_size(); @@ -216,6 +216,6 @@ where // Update theme and appearance self.theme = application.theme(); - self.appearance = self.theme.appearance(&application.style()); + self.appearance = application.style(&self.theme); } } diff --git a/winit/src/lib.rs b/winit/src/lib.rs index 3b1b0d3a..64912b3f 100644 --- a/winit/src/lib.rs +++ b/winit/src/lib.rs @@ -30,7 +30,6 @@ pub use iced_graphics as graphics; pub use iced_runtime as runtime; pub use iced_runtime::core; pub use iced_runtime::futures; -pub use iced_style as style; pub use winit; #[cfg(feature = "multi-window")] diff --git a/winit/src/multi_window.rs b/winit/src/multi_window.rs index 49d4b8e8..18db1fb5 100644 --- a/winit/src/multi_window.rs +++ b/winit/src/multi_window.rs @@ -22,9 +22,10 @@ use crate::runtime::command::{self, Command}; use crate::runtime::multi_window::Program; use crate::runtime::user_interface::{self, UserInterface}; use crate::runtime::Debug; -use crate::style::application::StyleSheet; use crate::{Clipboard, Error, Proxy, Settings}; +pub use crate::application::{default, Appearance, DefaultStyle}; + use std::collections::HashMap; use std::mem::ManuallyDrop; use std::sync::Arc; @@ -43,7 +44,7 @@ use std::time::Instant; /// can be toggled by pressing `F12`. pub trait Application: Program where - Self::Theme: StyleSheet, + Self::Theme: DefaultStyle, { /// The data needed to initialize your [`Application`]. type Flags; @@ -68,8 +69,8 @@ where fn theme(&self, window: window::Id) -> Self::Theme; /// Returns the `Style` variation of the `Theme`. - fn style(&self) -> <Self::Theme as StyleSheet>::Style { - Default::default() + fn style(&self, theme: &Self::Theme) -> Appearance { + theme.default_style() } /// Returns the event `Subscription` for the current state of the @@ -110,7 +111,7 @@ where A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use winit::event_loop::EventLoopBuilder; @@ -352,7 +353,7 @@ async fn run_instance<A, E, C>( A: Application + 'static, E: Executor + 'static, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use winit::event; use winit::event_loop::ControlFlow; @@ -822,7 +823,7 @@ fn build_user_interface<'a, A: Application>( id: window::Id, ) -> UserInterface<'a, A::Message, A::Theme, A::Renderer> where - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { debug.view_started(); 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>, ) where C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { for message in messages.drain(..) { debug.log_message(&message); @@ -893,7 +894,7 @@ fn run_command<A, C, E>( A: Application, E: Executor, C: Compositor<Renderer = A::Renderer> + 'static, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { use crate::runtime::clipboard; 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>, ) -> HashMap<window::Id, UserInterface<'a, A::Message, A::Theme, A::Renderer>> where - A::Theme: StyleSheet, C: Compositor<Renderer = A::Renderer>, + A::Theme: DefaultStyle, { cached_user_interfaces .drain() diff --git a/winit/src/multi_window/state.rs b/winit/src/multi_window/state.rs index 2e97a13d..dfd8e696 100644 --- a/winit/src/multi_window/state.rs +++ b/winit/src/multi_window/state.rs @@ -2,18 +2,16 @@ use crate::conversion; use crate::core::{mouse, window}; use crate::core::{Color, Size}; use crate::graphics::Viewport; -use crate::multi_window::Application; -use crate::style::application; +use crate::multi_window::{self, Application}; use std::fmt::{Debug, Formatter}; -use iced_style::application::StyleSheet; use winit::event::{Touch, WindowEvent}; use winit::window::Window; /// The state of a multi-windowed [`Application`]. pub struct State<A: Application> where - A::Theme: application::StyleSheet, + A::Theme: multi_window::DefaultStyle, { title: String, scale_factor: f64, @@ -22,12 +20,12 @@ where cursor_position: Option<winit::dpi::PhysicalPosition<f64>>, modifiers: winit::keyboard::ModifiersState, theme: A::Theme, - appearance: application::Appearance, + appearance: multi_window::Appearance, } impl<A: Application> Debug for State<A> where - A::Theme: application::StyleSheet, + A::Theme: multi_window::DefaultStyle, { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("multi_window::State") @@ -43,7 +41,7 @@ where impl<A: Application> State<A> where - A::Theme: application::StyleSheet, + A::Theme: multi_window::DefaultStyle, { /// Creates a new [`State`] for the provided [`Application`]'s `window`. pub fn new( @@ -54,7 +52,7 @@ where let title = application.title(window_id); let scale_factor = application.scale_factor(window_id); let theme = application.theme(window_id); - let appearance = theme.appearance(&application.style()); + let appearance = application.style(&theme); let viewport = { let physical_size = window.inner_size(); @@ -236,6 +234,6 @@ where // Update theme and appearance self.theme = application.theme(window_id); - self.appearance = self.theme.appearance(&application.style()); + self.appearance = application.style(&self.theme); } } diff --git a/winit/src/multi_window/window_manager.rs b/winit/src/multi_window/window_manager.rs index 23f3c0ba..71c1688b 100644 --- a/winit/src/multi_window/window_manager.rs +++ b/winit/src/multi_window/window_manager.rs @@ -2,8 +2,7 @@ use crate::core::mouse; use crate::core::window::Id; use crate::core::{Point, Size}; use crate::graphics::Compositor; -use crate::multi_window::{Application, State}; -use crate::style::application::StyleSheet; +use crate::multi_window::{Application, DefaultStyle, State}; use std::collections::BTreeMap; use std::sync::Arc; @@ -12,8 +11,8 @@ use winit::monitor::MonitorHandle; #[allow(missing_debug_implementations)] pub struct WindowManager<A: Application, C: Compositor> where - A::Theme: StyleSheet, C: Compositor<Renderer = A::Renderer>, + A::Theme: DefaultStyle, { aliases: BTreeMap<winit::window::WindowId, Id>, entries: BTreeMap<Id, Window<A, C>>, @@ -23,7 +22,7 @@ impl<A, C> WindowManager<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { pub fn new() -> Self { Self { @@ -109,7 +108,7 @@ impl<A, C> Default for WindowManager<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { fn default() -> Self { Self::new() @@ -121,7 +120,7 @@ pub struct Window<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { pub raw: Arc<winit::window::Window>, pub state: State<A>, @@ -136,7 +135,7 @@ impl<A, C> Window<A, C> where A: Application, C: Compositor<Renderer = A::Renderer>, - A::Theme: StyleSheet, + A::Theme: DefaultStyle, { pub fn position(&self) -> Option<Point> { self.raw |