summaryrefslogtreecommitdiffstats
path: root/widget
diff options
context:
space:
mode:
authorLibravatar Héctor Ramón <hector@hecrj.dev>2024-03-08 14:00:28 +0100
committerLibravatar GitHub <noreply@github.com>2024-03-08 14:00:28 +0100
commitedf7d7ca7593f660f4b15f154257471c26df87de (patch)
tree7cee3cbfbeb2ae5145f1bf6087b61fce4cbed8c9 /widget
parent2074757cdc65ec16eeb1c7a12a5ff3bb5ed00859 (diff)
parent8919f2593e39f76b273513e959fa6d5ffb78fde2 (diff)
downloadiced-edf7d7ca7593f660f4b15f154257471c26df87de.tar.gz
iced-edf7d7ca7593f660f4b15f154257471c26df87de.tar.bz2
iced-edf7d7ca7593f660f4b15f154257471c26df87de.zip
Merge pull request #2312 from iced-rs/theming-reloaded
Theming reloaded
Diffstat (limited to 'widget')
-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
25 files changed, 4755 insertions, 3711 deletions
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()
- }
-}