summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/document.yml1
-rw-r--r--Cargo.toml5
-rw-r--r--core/Cargo.toml5
-rw-r--r--core/src/background.rs13
-rw-r--r--core/src/border.rs35
-rw-r--r--core/src/color.rs14
-rw-r--r--core/src/gradient.rs22
-rw-r--r--core/src/lib.rs2
-rw-r--r--core/src/theme.rs221
-rw-r--r--core/src/theme/palette.rs (renamed from style/src/theme/palette.rs)2
-rw-r--r--core/src/widget/text.rs80
-rw-r--r--examples/bezier_tool/src/main.rs4
-rw-r--r--examples/checkbox/src/main.rs9
-rw-r--r--examples/clock/src/main.rs16
-rw-r--r--examples/color_palette/Cargo.toml2
-rw-r--r--examples/color_palette/src/main.rs27
-rw-r--r--examples/custom_widget/src/main.rs2
-rw-r--r--examples/editor/src/main.rs7
-rw-r--r--examples/game_of_life/src/main.rs23
-rw-r--r--examples/gradient/src/main.rs43
-rw-r--r--examples/integration/src/controls.rs104
-rw-r--r--examples/integration/src/main.rs3
-rw-r--r--examples/layout/src/main.rs7
-rw-r--r--examples/lazy/src/main.rs6
-rw-r--r--examples/modal/src/main.rs3
-rw-r--r--examples/pane_grid/src/main.rs33
-rw-r--r--examples/pokedex/src/main.rs6
-rw-r--r--examples/screenshot/src/main.rs9
-rw-r--r--examples/scrollable/src/main.rs5
-rw-r--r--examples/solar_system/src/main.rs15
-rw-r--r--examples/stopwatch/src/main.rs4
-rw-r--r--examples/svg/src/main.rs7
-rw-r--r--examples/toast/src/main.rs76
-rw-r--r--examples/todos/src/main.rs19
-rw-r--r--examples/tooltip/src/main.rs3
-rw-r--r--examples/tour/src/main.rs32
-rw-r--r--examples/visible_bounds/src/main.rs21
-rw-r--r--examples/websocket/src/main.rs4
-rw-r--r--src/application.rs32
-rw-r--r--src/lib.rs7
-rw-r--r--src/multi_window.rs256
-rw-r--r--src/multi_window/application.rs246
-rw-r--r--src/sandbox.rs18
-rw-r--r--src/time.rs2
-rw-r--r--style/Cargo.toml18
-rw-r--r--style/src/application.rs23
-rw-r--r--style/src/button.rs79
-rw-r--r--style/src/checkbox.rs45
-rw-r--r--style/src/container.rs51
-rw-r--r--style/src/lib.rs38
-rw-r--r--style/src/menu.rs26
-rw-r--r--style/src/pane_grid.rs38
-rw-r--r--style/src/pick_list.rs29
-rw-r--r--style/src/progress_bar.rs23
-rw-r--r--style/src/qr_code.rs20
-rw-r--r--style/src/radio.rs29
-rw-r--r--style/src/rule.rs89
-rw-r--r--style/src/scrollable.rs55
-rw-r--r--style/src/slider.rs68
-rw-r--r--style/src/svg.rs28
-rw-r--r--style/src/text_editor.rs43
-rw-r--r--style/src/text_input.rs45
-rw-r--r--style/src/theme.rs1523
-rw-r--r--style/src/toggler.rs35
-rw-r--r--wgpu/src/color.rs7
-rw-r--r--widget/Cargo.toml1
-rw-r--r--widget/src/button.rs423
-rw-r--r--widget/src/checkbox.rs233
-rw-r--r--widget/src/combo_box.rs108
-rw-r--r--widget/src/container.rs168
-rw-r--r--widget/src/helpers.rs68
-rw-r--r--widget/src/lib.rs5
-rw-r--r--widget/src/overlay/menu.rs159
-rw-r--r--widget/src/pane_grid.rs1085
-rw-r--r--widget/src/pane_grid/content.rs30
-rw-r--r--widget/src/pane_grid/title_bar.rs30
-rw-r--r--widget/src/pick_list.rs862
-rw-r--r--widget/src/progress_bar.rs119
-rw-r--r--widget/src/qr_code.rs75
-rw-r--r--widget/src/radio.rs120
-rw-r--r--widget/src/rule.rs167
-rw-r--r--widget/src/scrollable.rs1374
-rw-r--r--widget/src/slider.rs658
-rw-r--r--widget/src/svg.rs86
-rw-r--r--widget/src/text_editor.rs129
-rw-r--r--widget/src/text_input.rs1695
-rw-r--r--widget/src/themer.rs133
-rw-r--r--widget/src/toggler.rs144
-rw-r--r--widget/src/tooltip.rs24
-rw-r--r--widget/src/vertical_slider.rs570
-rw-r--r--winit/Cargo.toml1
-rw-r--r--winit/src/application.rs51
-rw-r--r--winit/src/application/state.rs10
-rw-r--r--winit/src/lib.rs1
-rw-r--r--winit/src/multi_window.rs21
-rw-r--r--winit/src/multi_window/state.rs16
-rw-r--r--winit/src/multi_window/window_manager.rs13
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 \
diff --git a/Cargo.toml b/Cargo.toml
index 7f55ce0e..f446e2af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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> {
diff --git a/src/lib.rs b/src/lib.rs
index 236c64d3..c596f2a6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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.
-//!
-//! ![The foundations of the Iced ecosystem](https://github.com/iced-rs/iced/blob/0525d76ff94e828b7b21634fa94a747022001c83/docs/graphs/foundations.png?raw=true)
-#![doc(
- html_logo_url = "https://raw.githubusercontent.com/iced-rs/iced/9ab6923e943f784985e9ef9ca28b10278297225d/docs/logo.svg"
-)]
-#![forbid(unsafe_code, rust_2018_idioms)]
-#![deny(
- unused_results,
- missing_docs,
- unused_results,
- rustdoc::broken_intra_doc_links
-)]
-pub use iced_core as core;
-
-pub mod application;
-pub mod button;
-pub mod checkbox;
-pub mod container;
-pub mod menu;
-pub mod pane_grid;
-pub mod pick_list;
-pub mod progress_bar;
-pub mod qr_code;
-pub mod radio;
-pub mod rule;
-pub mod scrollable;
-pub mod slider;
-pub mod svg;
-pub mod text_editor;
-pub mod text_input;
-pub mod theme;
-pub mod toggler;
-
-pub use theme::Theme;
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};
///
/// ![Progress bar drawn with `iced_wgpu`](https://user-images.githubusercontent.com/18618951/71662391-a316c200-2d51-11ea-9cef-52758cab85e3.png)
#[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::{
///
/// ![Slider drawn by Coffee's renderer](https://github.com/hecrj/coffee/blob/bda9818f823dfcb8a7ad0ff4940b4d4b387b5208/images/ui/slider.png?raw=true)
#[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