diff options
author | 2024-03-04 20:42:37 +0100 | |
---|---|---|
committer | 2024-03-04 20:42:37 +0100 | |
commit | f4a4845ddbdced81ae4ff60bfa19f0e602d84709 (patch) | |
tree | b532017384eb9e43e57bf73be372aea0d55af652 | |
parent | db92e1c942154bee474fee5e2c187f8a52a1bb96 (diff) | |
download | iced-f4a4845ddbdced81ae4ff60bfa19f0e602d84709.tar.gz iced-f4a4845ddbdced81ae4ff60bfa19f0e602d84709.tar.bz2 iced-f4a4845ddbdced81ae4ff60bfa19f0e602d84709.zip |
Simplify theming for `Button` widget
-rw-r--r-- | core/src/background.rs | 13 | ||||
-rw-r--r-- | core/src/color.rs | 8 | ||||
-rw-r--r-- | core/src/gradient.rs | 4 | ||||
-rw-r--r-- | examples/editor/src/main.rs | 2 | ||||
-rw-r--r-- | examples/game_of_life/src/main.rs | 6 | ||||
-rw-r--r-- | examples/lazy/src/main.rs | 3 | ||||
-rw-r--r-- | examples/pane_grid/src/main.rs | 11 | ||||
-rw-r--r-- | examples/screenshot/src/main.rs | 6 | ||||
-rw-r--r-- | examples/stopwatch/src/main.rs | 4 | ||||
-rw-r--r-- | examples/todos/src/main.rs | 14 | ||||
-rw-r--r-- | examples/tour/src/main.rs | 30 | ||||
-rw-r--r-- | style/src/button.rs | 78 | ||||
-rw-r--r-- | style/src/checkbox.rs | 10 | ||||
-rw-r--r-- | style/src/theme.rs | 123 | ||||
-rw-r--r-- | widget/src/button.rs | 382 | ||||
-rw-r--r-- | widget/src/helpers.rs | 2 |
16 files changed, 295 insertions, 401 deletions
diff --git a/core/src/background.rs b/core/src/background.rs index 347c52c0..2e28e560 100644 --- a/core/src/background.rs +++ b/core/src/background.rs @@ -11,6 +11,19 @@ pub enum Background { // TODO: Add image variant } +impl Background { + /// Increases the translucency of the [`Background`] + /// by the given factor. + pub fn transparentize(self, factor: f32) -> Self { + match self { + Self::Color(color) => Self::Color(color.transparentize(factor)), + Self::Gradient(gradient) => { + Self::Gradient(gradient.transparentize(factor)) + } + } + } +} + impl From<Color> for Background { fn from(color: Color) -> Self { Background::Color(color) diff --git a/core/src/color.rs b/core/src/color.rs index b8db322f..6526e220 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -151,6 +151,14 @@ impl Color { pub fn inverse(self) -> Color { Color::new(1.0f32 - self.r, 1.0f32 - self.g, 1.0f32 - self.b, self.a) } + + /// Transparentizes the [`Color`] by the given factor. + pub fn transparentize(self, factor: f32) -> Color { + Self { + a: self.a * factor, + ..self + } + } } impl From<[f32; 3]> for Color { diff --git a/core/src/gradient.rs b/core/src/gradient.rs index 4711b044..ecf7830f 100644 --- a/core/src/gradient.rs +++ b/core/src/gradient.rs @@ -13,11 +13,11 @@ 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 { + pub fn transparentize(mut self, factor: f32) -> Self { match &mut self { Gradient::Linear(linear) => { for stop in linear.stops.iter_mut().flatten() { - stop.color.a *= alpha_multiplier; + stop.color.a *= factor; } } } diff --git a/examples/editor/src/main.rs b/examples/editor/src/main.rs index 53c9cf7c..b5870e9e 100644 --- a/examples/editor/src/main.rs +++ b/examples/editor/src/main.rs @@ -290,7 +290,7 @@ fn action<'a, Message: Clone + 'a>( .style(theme::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..b362381c 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); @@ -195,7 +195,7 @@ fn view_controls<'a>( .text_size(16), button("Clear") .on_press(Message::Clear) - .style(theme::Button::Destructive), + .style(button::destructive), ] .padding(10) .spacing(20) diff --git a/examples/lazy/src/main.rs b/examples/lazy/src/main.rs index 37b5d52c..1c5f59d5 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,7 +180,7 @@ 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::destructive); row![ text(&item.name).color(item.color), diff --git a/examples/pane_grid/src/main.rs b/examples/pane_grid/src/main.rs index c4bedccc..2bed5a03 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 { @@ -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::destructive)) } 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::destructive) .padding(3) .on_press_maybe(if total_panes > 1 && !is_pinned { Some(Message::Close(pane)) diff --git a/examples/screenshot/src/main.rs b/examples/screenshot/src/main.rs index 79749956..dc4684d4 100644 --- a/examples/screenshot/src/main.rs +++ b/examples/screenshot/src/main.rs @@ -216,9 +216,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 +227,7 @@ impl Application for Example { crop_controls, button(centered_text("Crop")) .on_press(Message::Crop) - .style(theme::Button::Destructive) + .style(button::destructive) .padding([10, 20, 10, 20]) .width(Length::Fill), ] diff --git a/examples/stopwatch/src/main.rs b/examples/stopwatch/src/main.rs index 8a0674c1..7a097e90 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::destructive) .on_press(Message::Reset); let controls = row![toggle_button, reset_button].spacing(20); diff --git a/examples/todos/src/main.rs b/examples/todos/src/main.rs index b1aeb4a7..b3b5d87a 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::{Command, Length, Settings, Size, Subscription}; +use iced::{ + Application, Command, Element, Length, Settings, Size, Subscription, Theme, +}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; @@ -362,7 +362,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 +385,7 @@ impl Task { ) .on_press(TaskMessage::Delete) .padding(10) - .style(theme::Button::Destructive) + .style(button::destructive) ] .spacing(20) .align_items(Alignment::Center) @@ -402,9 +402,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) diff --git a/examples/tour/src/main.rs b/examples/tour/src/main.rs index 52e1bbb7..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), @@ -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/style/src/button.rs b/style/src/button.rs index 0d7a668a..8b137891 100644 --- a/style/src/button.rs +++ b/style/src/button.rs @@ -1,79 +1 @@ -//! 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 index 77093f69..5e1c8374 100644 --- a/style/src/checkbox.rs +++ b/style/src/checkbox.rs @@ -30,15 +30,7 @@ pub trait StyleSheet { 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)) - } - }, + background: active.background.transparentize(0.5), ..active } } diff --git a/style/src/theme.rs b/style/src/theme.rs index 43e7cafd..f967aebc 100644 --- a/style/src/theme.rs +++ b/style/src/theme.rs @@ -4,7 +4,6 @@ pub mod palette; pub use palette::Palette; use crate::application; -use crate::button; use crate::checkbox; use crate::container; use crate::core::widget::text; @@ -22,7 +21,7 @@ use crate::text_editor; use crate::text_input; use crate::toggler; -use crate::core::{Background, Border, Color, Shadow, Vector}; +use crate::core::{Background, Border, Color, Shadow}; use std::fmt; use std::rc::Rc; @@ -285,126 +284,6 @@ impl<T: Fn(&Theme) -> application::Appearance> application::StyleSheet for T { } } -/// 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 { diff --git a/widget/src/button.rs b/widget/src/button.rs index 867fbfaf..798a8206 100644 --- a/widget/src/button.rs +++ b/widget/src/button.rs @@ -10,11 +10,11 @@ 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, Vector, Widget, }; - -pub use crate::style::button::{Appearance, StyleSheet}; +use crate::style::theme::palette; +use crate::style::Theme; /// A generic widget that produces a message when pressed. /// @@ -53,7 +53,7 @@ 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, + Theme: Style, Renderer: crate::core::Renderer, { content: Element<'a, Message, Theme, Renderer>, @@ -62,12 +62,12 @@ where height: Length, padding: Padding, clip: bool, - style: Theme::Style, + style: fn(&Theme, Status) -> Appearance, } impl<'a, Message, Theme, Renderer> Button<'a, Message, Theme, Renderer> where - Theme: StyleSheet, + Theme: Style, Renderer: crate::core::Renderer, { /// Creates a new [`Button`] with the given content. @@ -84,7 +84,7 @@ where height: size.height.fluid(), padding: Padding::new(5.0), clip: false, - style: Theme::Style::default(), + style: Theme::DEFAULT, } } @@ -124,7 +124,7 @@ where } /// Sets the style variant of this [`Button`]. - pub fn style(mut self, style: impl Into<Theme::Style>) -> Self { + pub fn style(mut self, style: fn(&Theme, Status) -> Appearance) -> Self { self.style = style.into(); self } @@ -137,11 +137,16 @@ 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, + Theme: Style, Renderer: 'a + crate::core::Renderer, { fn tag(&self) -> tree::Tag { @@ -149,7 +154,7 @@ where } fn state(&self) -> tree::State { - tree::State::new(State::new()) + tree::State::new(State::default()) } fn children(&self) -> Vec<Tree> { @@ -173,13 +178,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 +234,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 +290,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 +351,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 +380,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: Style + 'a, Renderer: crate::core::Renderer + 'a, { fn from(button: Button<'a, Message, Theme, Renderer>) -> Self { @@ -309,143 +388,150 @@ where } } -/// The local state of a [`Button`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -pub struct State { - is_pressed: bool, +/// 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)] +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, } -/// 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 { + shadow_offset: Vector::default(), + 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; + } +} - let bounds = layout.bounds(); +/// The default style of a [`Button`] for a given theme. +pub trait Style { + /// The default style. + const DEFAULT: fn(&Self, Status) -> Appearance; +} - if cursor.is_over(bounds) { - shell.publish(on_press); - } +impl Style for Theme { + const DEFAULT: fn(&Self, Status) -> Appearance = primary; +} - return event::Status::Captured; - } - } - } - Event::Touch(touch::Event::FingerLost { .. }) => { - let state = state(); +/// 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), + } +} - state.is_pressed = false; - } - _ => {} +/// 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), } +} - event::Status::Ignored +/// A positive button; denoting a good outcome. +pub fn positive(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), + } } -/// 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 destructive button; denoting a dangerous action. +pub fn destructive(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), + } +} - let styling = if !is_enabled { - theme.disabled(style) - } else if is_mouse_over { - let state = state(); +/// A text button; useful for links. +pub fn text(theme: &Theme, status: Status) -> Appearance { + let palette = theme.extended_palette(); - if state.is_pressed { - theme.pressed(style) - } else { - theme.hovered(style) - } - } else { - theme.active(style) + 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.transparentize(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::with_radius(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.transparentize(0.5)), + text_color: appearance.text_color.transparentize(0.5), + ..appearance } } diff --git a/widget/src/helpers.rs b/widget/src/helpers.rs index e6322926..86331e14 100644 --- a/widget/src/helpers.rs +++ b/widget/src/helpers.rs @@ -118,7 +118,7 @@ pub fn button<'a, Message, Theme, Renderer>( ) -> Button<'a, Message, Theme, Renderer> where Renderer: core::Renderer, - Theme: button::StyleSheet, + Theme: button::Style, { Button::new(content) } |