feat(progress_bar): custom style

This commit is contained in:
Vukašin Vojinović 2026-09-15 13:37:20 +02:00 • committed by Michael Murphy
parent d9431dc367
commit d921602cde
3 changed files with 128 additions and 78 deletions

View file

@ -1,6 +1,6 @@
//! Show a circular progress indicator.
use super::animation::{Animation, Progress};
use super::style::StyleSheet;
use super::style::{self, Catalog};
use iced::advanced::widget::tree::{self, Tree};
use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer};
use iced::widget::canvas;
@ -15,11 +15,11 @@ const MAX_WRAP: f32 = 1.0 - MIN_GAP_ANGLE.0 / (2.0 * PI);
#[must_use]
pub struct Circular<Theme>
where
Theme: StyleSheet,
Theme: Catalog,
{
size: f32,
bar_height: Option<f32>,
style: Theme::Style,
class: Theme::Class,
cycle_duration: Duration,
period: Duration,
progress: Option<f32>,
@ -27,14 +27,14 @@ where
impl<Theme> Circular<Theme>
where
Theme: StyleSheet,
Theme: Catalog<Class = style::Class>,
{
/// Creates a new [`Circular`] with the given content.
pub fn new() -> Self {
Circular {
size: 48.0,
bar_height: None,
style: Theme::Style::default(),
class: Theme::Class::default(),
cycle_duration: Duration::from_millis(1500),
period: Duration::from_secs(2),
progress: None,
@ -54,9 +54,9 @@ where
self
}
/// Sets the style variant of this [`Circular`].
pub fn style(mut self, style: Theme::Style) -> Self {
self.style = style;
/// Sets the style class of this [`Circular`].
pub fn class(mut self, class: Theme::Class) -> Self {
self.class = class;
self
}
@ -78,11 +78,29 @@ where
self.progress = Some(progress.clamp(0.0, 1.0));
self
}
/// Sets the track color of this [`Circular`].
pub fn track_color(mut self, color: impl Into<iced::Color>) -> Self {
self.class = self.class.track_color(color);
self
}
/// Sets the bar color of this [`Circular`].
pub fn bar_color(mut self, color: impl Into<iced::Color>) -> Self {
self.class = self.class.bar_color(color);
self
}
/// Sets the border color of this [`Circular`].
pub fn border_color(mut self, color: impl Into<iced::Color>) -> Self {
self.class = self.class.border_color(color);
self
}
}
impl<Theme> Default for Circular<Theme>
where
Theme: StyleSheet,
Theme: Catalog<Class = style::Class>,
{
fn default() -> Self {
Self::new()
@ -99,7 +117,7 @@ struct State {
impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
where
Message: Clone,
Theme: StyleSheet,
Theme: Catalog,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
@ -170,7 +188,7 @@ where
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();
let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true);
let custom_style = theme.style(&self.class, self.progress.is_some(), true);
let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
let bar_height = self.bar_height.unwrap_or((frame.width() / 12.0).max(2.0));
@ -240,7 +258,7 @@ where
impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
where
Message: Clone + 'a,
Theme: StyleSheet + 'a,
Theme: Catalog + 'a,
{
fn from(circular: Circular<Theme>) -> Self {
Self::new(circular)

View file

@ -1,6 +1,6 @@
//! Show a linear progress indicator.
use super::animation::{Animation, Progress};
use super::style::StyleSheet;
use super::style::{self, Catalog};
use iced::advanced::widget::tree::{self, Tree};
use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer};
use iced::{Border, Color, Element, Event, Length, Pixels, Rectangle, Size, mouse, window};
@ -13,11 +13,11 @@ const WRAP_LENGTH: f32 = 0.618; // avoids animation repetition
#[must_use]
pub struct Linear<Theme>
where
Theme: StyleSheet,
Theme: Catalog,
{
width: Length,
girth: Length,
style: Theme::Style,
class: Theme::Class,
cycle_duration: Duration,
period: Duration,
progress: Option<f32>,
@ -27,14 +27,14 @@ where
impl<Theme> Linear<Theme>
where
Theme: StyleSheet,
Theme: Catalog<Class = style::Class>,
{
/// Creates a new [`Linear`] with the given content.
pub fn new() -> Self {
Linear {
width: Length::Fixed(100.0),
girth: Length::Fixed(4.0),
style: Theme::Style::default(),
class: Theme::Class::default(),
cycle_duration: Duration::from_millis(1500),
period: Duration::from_secs(2),
progress: None,
@ -55,9 +55,9 @@ where
self
}
/// Sets the style variant of this [`Linear`].
pub fn style(mut self, style: impl Into<Theme::Style>) -> Self {
self.style = style.into();
/// Sets the style class of this [`Linear`].
pub fn class(mut self, class: Theme::Class) -> Self {
self.class = class;
self
}
@ -99,11 +99,35 @@ where
self.segment_spacing = spacing.into().0.max(1.0);
self
}
/// Sets the track color of this [`Linear`].
pub fn track_color(mut self, color: impl Into<iced::Color>) -> Self {
self.class = self.class.track_color(color);
self
}
/// Sets the bar color of this [`Linear`].
pub fn bar_color(mut self, color: impl Into<iced::Color>) -> Self {
self.class = self.class.bar_color(color);
self
}
/// Sets the border color of this [`Linear`].
pub fn border_color(mut self, color: impl Into<iced::Color>) -> Self {
self.class = self.class.border_color(color);
self
}
/// Sets the border radius of this [`Linear`].
pub fn border_radius(mut self, radius: f32) -> Self {
self.class = self.class.border_radius(radius);
self
}
}
impl<Theme> Default for Linear<Theme>
where
Theme: StyleSheet,
Theme: Catalog<Class = style::Class>,
{
fn default() -> Self {
Self::new()
@ -119,7 +143,7 @@ struct State {
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer> for Linear<Theme>
where
Message: Clone,
Theme: StyleSheet,
Theme: Catalog,
Renderer: advanced::Renderer,
{
fn tag(&self) -> tree::Tag {
@ -186,7 +210,7 @@ where
_viewport: &Rectangle,
) {
let bounds = layout.bounds();
let custom_style = theme.appearance(&self.style, self.progress.is_some(), false);
let custom_style = theme.style(&self.class, self.progress.is_some(), false);
let state = tree.state.downcast_ref::<State>();
let border_width = if custom_style.border_color.is_some() {
@ -307,7 +331,7 @@ where
impl<'a, Message, Theme, Renderer> From<Linear<Theme>> for Element<'a, Message, Theme, Renderer>
where
Message: Clone + 'a,
Theme: StyleSheet + 'a,
Theme: Catalog + 'a,
Renderer: iced::advanced::Renderer + 'a,
{
fn from(linear: Linear<Theme>) -> Self {

View file

@ -1,7 +1,8 @@
use iced::Color;
use palette::WithAlpha;
#[derive(Debug, Clone, Copy)]
pub struct Appearance {
#[derive(Clone, Copy, Debug)]
pub struct Style {
/// The track [`Color`] of the progress indicator.
pub track_color: Color,
/// The bar [`Color`] of the progress indicator.
@ -12,94 +13,101 @@ pub struct Appearance {
pub border_radius: f32,
}
impl std::default::Default for Appearance {
fn default() -> Self {
Self {
track_color: Color::TRANSPARENT,
bar_color: Color::BLACK,
border_color: None,
border_radius: 0.0,
/// [`Style`] field overrides
#[derive(Clone, Copy, Debug, Default)]
pub struct Class {
pub track_color: Option<Color>,
pub bar_color: Option<Color>,
pub border_color: Option<Color>,
pub border_radius: Option<f32>,
}
impl Class {
pub fn track_color(mut self, color: impl Into<Color>) -> Self {
self.track_color = Some(color.into());
self
}
pub fn bar_color(mut self, color: impl Into<Color>) -> Self {
self.bar_color = Some(color.into());
self
}
pub fn border_color(mut self, color: impl Into<Color>) -> Self {
self.border_color = Some(color.into());
self
}
pub fn border_radius(mut self, radius: f32) -> Self {
self.border_radius = Some(radius);
self
}
fn resolve(&self, base: Style) -> Style {
Style {
track_color: self.track_color.unwrap_or(base.track_color),
bar_color: self.bar_color.unwrap_or(base.bar_color),
border_color: self.border_color.or(base.border_color),
border_radius: self.border_radius.unwrap_or(base.border_radius),
}
}
}
/// A set of rules that dictate the style of an indicator.
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
pub trait Catalog: Sized {
/// The supported class of the [`Catalog`].
type Class: Default;
/// Produces the active [`Appearance`] of a indicator.
fn appearance(
&self,
style: &Self::Style,
is_determinate: bool,
is_circular: bool,
) -> Appearance;
/// Produces the active [`Style`] of an indicator.
fn style(&self, class: &Self::Class, is_determinate: bool, is_circular: bool) -> Style;
}
impl StyleSheet for iced::Theme {
type Style = ();
impl Catalog for iced::Theme {
type Class = Class;
fn appearance(
&self,
_style: &Self::Style,
_is_determinate: bool,
_is_circular: bool,
) -> Appearance {
fn style(&self, class: &Self::Class, _is_determinate: bool, _is_circular: bool) -> Style {
let palette = self.extended_palette();
Appearance {
class.resolve(Style {
track_color: palette.background.weak.color,
bar_color: palette.primary.base.color,
border_color: None,
border_radius: 0.0,
}
})
}
}
impl StyleSheet for crate::Theme {
type Style = ();
impl Catalog for crate::Theme {
type Class = Class;
fn appearance(
&self,
_style: &Self::Style,
is_determinate: bool,
is_circular: bool,
) -> Appearance {
let cur = self.current_container();
let mut cur_divider = cur.divider;
cur_divider.alpha = 0.5;
fn style(&self, class: &Self::Class, is_determinate: bool, is_circular: bool) -> Style {
let theme = self.cosmic();
let (mut track_color, bar_color) = if theme.is_dark && theme.is_high_contrast {
(
let (mut track_color, bar_color) = match (theme.is_dark, theme.is_high_contrast) {
(true, true) => (
theme.palette.neutral_6.into(),
theme.accent_text_color().into(),
)
} else if theme.is_dark {
(theme.palette.neutral_5.into(), theme.accent_color().into())
} else if theme.is_high_contrast {
(
),
(true, false) => (theme.palette.neutral_5.into(), theme.accent_color().into()),
(false, true) => (
theme.palette.neutral_4.into(),
theme.accent_text_color().into(),
)
} else {
(theme.palette.neutral_3.into(), theme.accent_color().into())
),
(false, false) => (theme.palette.neutral_3.into(), theme.accent_color().into()),
};
if !is_determinate && is_circular {
track_color = Color::TRANSPARENT;
}
Appearance {
class.resolve(Style {
track_color,
bar_color,
border_color: if is_determinate && theme.is_high_contrast {
Some(cur_divider.into())
Some(self.current_container().divider.with_alpha(0.5).into())
} else {
None
},
border_radius: theme.corner_radii.radius_xl[0],
}
})
}
}