Skip to main content

teksilo_widgets/styles/
recipe_progress_bar_style.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Default `ProgressBarStyle` impl driven by paint-recipe data.
5//!
6//! `RecipeProgressBarStyle` ships the IntUI progress-bar stationary
7//! chrome: a `surface_sunken` track and (for determinate bars) an
8//! `accent`-colored proportional fill. The indeterminate sweep is
9//! deliberately *not* part of this recipe — it stays widget-owned in
10//! `ProgressBar::build`, which mounts an `IndeterminateSweepLeaf` on
11//! top for both the horizontal-shader path and the vertical /
12//! reduced-motion signal path (principle 6: motion infrastructure is
13//! not chrome).
14//!
15//! Apps that want a different progress look (segmented chunks, gradient
16//! fill, branded colour) write their own `impl ProgressBarStyle` block
17//! and install it per-call (`ProgressBar::style(...)`) or theme-wide
18//! (`theme.style_slots.progress_bar`).
19
20use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
21use teksilo_core::accessibility::AccessNodeBuilder;
22use teksilo_core::binding::BindingLevel;
23use teksilo_core::build_context::BuildContext;
24use teksilo_core::color_prop::ColorProp;
25use teksilo_core::signal::Prop;
26use teksilo_core::styles::{ProgressBarStyle, ProgressBarStyleConfig, ProgressKind};
27use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget};
28use teksilo_core::widget_id::WidgetId;
29use teksilo_tokens::{CornerRadius, Orientation, SurfaceRole};
30
31// IntUI design tokens for ProgressBar. The recipe owns its own dimensions.
32pub const PROGRESS_BAR_CORNER_RADIUS: f32 = 2.0;
33
34/// Configurable dimensions for [`RecipeProgressBarStyle`].
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub struct ProgressBarRecipe {
37    pub corner_radius: f32,
38}
39
40impl Default for ProgressBarRecipe {
41    fn default() -> Self {
42        Self {
43            corner_radius: PROGRESS_BAR_CORNER_RADIUS,
44        }
45    }
46}
47
48/// Default `ProgressBarStyle` shipped with Teksilo. Track is
49/// `SurfaceRole::Sunken`, fill is `SurfaceRole::Accent`.
50#[derive(Debug, Default, Clone, Copy)]
51pub struct RecipeProgressBarStyle {
52    pub recipe: ProgressBarRecipe,
53}
54
55impl RecipeProgressBarStyle {
56    pub fn new(recipe: ProgressBarRecipe) -> Self {
57        Self { recipe }
58    }
59}
60
61impl ProgressBarStyle for RecipeProgressBarStyle {
62    fn make_body(&self, cfg: &ProgressBarStyleConfig, ctx: &mut BuildContext) -> WidgetId {
63        let track = cfg
64            .track_color_override
65            .clone()
66            .unwrap_or_else(|| SurfaceRole::Sunken.into());
67        let fill = cfg
68            .fill_color_override
69            .clone()
70            .unwrap_or_else(|| SurfaceRole::Accent.into());
71        let determinate_value = match &cfg.progress {
72            ProgressKind::Determinate(p) => Some(p.clone()),
73            ProgressKind::Indeterminate => None,
74        };
75        ctx.add(ProgressBarFrame {
76            orientation: cfg.orientation,
77            track,
78            fill,
79            determinate_value,
80            recipe: self.recipe,
81        })
82    }
83}
84
85/// Internal recipe widget that paints the progress bar's stationary
86/// chrome. For determinate bars it paints the track + a fill rect
87/// proportional to the bound value; for indeterminate bars it paints
88/// only the track (the moving sweep is composed on top by the
89/// `ProgressBar` widget's `build()` — see
90/// `IndeterminateSweepLeaf` in `progress_bar.rs`). The shader-quad
91/// horizontal path replaces the entire track-plus-sweep visual in one
92/// procedural draw; the `ProgressBar` widget skips mounting this
93/// frame in that case to avoid double-painting.
94struct ProgressBarFrame {
95    orientation: Orientation,
96    track: ColorProp,
97    fill: ColorProp,
98    /// `Some` for determinate bars; `None` for indeterminate (the
99    /// widget mounts the sweep leaf separately).
100    determinate_value: Option<Prop<f32>>,
101    recipe: ProgressBarRecipe,
102}
103
104impl std::fmt::Debug for ProgressBarFrame {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        f.debug_struct("ProgressBarFrame")
107            .field("orientation", &self.orientation)
108            .finish()
109    }
110}
111
112impl Widget for ProgressBarFrame {
113    fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
114        let id = ctx.self_id();
115        let registry = ctx.binding_registry();
116        if let Some(p) = &self.determinate_value {
117            p.register_if_bound(id, registry, BindingLevel::RepaintOnly);
118        }
119        vec![]
120    }
121
122    fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
123        // The frame fills whatever bounds its parent assigns — the
124        // `ProgressBar` widget owns the intrinsic-size policy.
125        Size::new(
126            proposal.width.unwrap_or(0.0),
127            proposal.height.unwrap_or(0.0),
128        )
129        .into()
130    }
131
132    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
133        let radius = CornerRadius::uniform(self.recipe.corner_radius);
134        let track_color = self.track.resolve(ctx.theme, ctx.effective_enabled);
135        canvas.fill_rounded_rect(bounds, radius, track_color);
136
137        if let Some(value_prop) = &self.determinate_value {
138            let value = value_prop.get().clamp(0.0, 1.0);
139            if value > 0.0 {
140                let fill_color = self.fill.resolve(ctx.theme, ctx.effective_enabled);
141                let fill_rect = match self.orientation {
142                    Orientation::Horizontal => {
143                        Rect::new(bounds.x, bounds.y, bounds.width * value, bounds.height)
144                    }
145                    Orientation::Vertical => {
146                        let fill_h = bounds.height * value;
147                        Rect::new(
148                            bounds.x,
149                            bounds.y + bounds.height - fill_h,
150                            bounds.width,
151                            fill_h,
152                        )
153                    }
154                };
155                canvas.fill_rounded_rect(fill_rect, radius, fill_color);
156            }
157        }
158    }
159
160    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
161        // Presentational — the parent `ProgressBar` emits the
162        // `Role::ProgressIndicator` node with the numeric value.
163        builder.set_hidden();
164    }
165}