Skip to main content

tui_lipan/widgets/progress/
mod.rs

1//! ProgressBar widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_progress_bar;
8pub use node::ProgressNode;
9pub use reconcile::reconcile_progress_bar;
10
11use crate::callback::Callback;
12use crate::core::element::{Element, ElementKind};
13use crate::core::event::MouseEvent;
14use crate::style::{Length, Padding, Style, StyleSlot};
15use crate::utils::gradient::ColorGradient;
16
17/// Event emitted when progress bar value changes (via drag).
18#[derive(Clone, Copy, Debug, PartialEq)]
19pub struct ProgressEvent {
20    /// New progress value (0.0 to 1.0).
21    pub progress: f64,
22}
23
24/// Threshold styling zone for progress bar fill.
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct ProgressZone {
27    /// Upper bound for this zone in the [0.0, 1.0] range.
28    pub upto: f64,
29    /// Optional style patch for this zone.
30    pub style: Style,
31    /// Optional symbol override for this zone.
32    pub symbol: Option<char>,
33}
34
35impl ProgressZone {
36    /// Create a zone that applies up to the given normalized value.
37    pub fn new(upto: f64) -> Self {
38        Self {
39            upto: upto.clamp(0.0, 1.0),
40            style: Style::default(),
41            symbol: None,
42        }
43    }
44
45    /// Set style for this zone.
46    pub fn style(mut self, style: Style) -> Self {
47        self.style = style;
48        self
49    }
50
51    /// Set symbol override for this zone.
52    pub fn symbol(mut self, symbol: char) -> Self {
53        self.symbol = Some(symbol);
54        self
55    }
56}
57
58/// Visual style for a [`ProgressBar`].
59#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
60pub enum ProgressStyle {
61    /// Block style using `█` and `░`.
62    #[default]
63    Block,
64    /// Line style using `─` and `━`.
65    Line,
66    /// Line style with dots: `━` and `┄`.
67    LineDotted,
68    /// Dots style using `●` and `○`.
69    Dots,
70    /// Arrow style using `►` and `─`.
71    Arrow,
72    /// Rect style using `▮` and `▯`.
73    Rect,
74    /// Custom style with user-defined characters.
75    Custom {
76        /// Character for the filled portion.
77        filled: char,
78        /// Character for the empty portion.
79        empty: char,
80    },
81    /// Braille pattern for smooth animation.
82    Braille,
83}
84
85/// Text placement for [`ProgressBar`] percentage and label text.
86#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
87pub enum ProgressTextPosition {
88    /// Render text before the bar track.
89    Left,
90    /// Render text after the bar track.
91    #[default]
92    Right,
93    /// Render text above the bar track.
94    Above,
95    /// Render text below the bar track.
96    Below,
97    /// Render text centered inside the bar track (Block style only).
98    Middle,
99}
100
101impl ProgressStyle {
102    /// Get the filled character for this style.
103    pub fn filled_char(self) -> char {
104        match self {
105            Self::Block => '█',
106            Self::Line | Self::LineDotted => '━',
107            Self::Dots => '●',
108            Self::Arrow => '►',
109            Self::Rect => '▮',
110            Self::Custom { filled, .. } => filled,
111            Self::Braille => '⣿',
112        }
113    }
114
115    /// Get the empty character for this style.
116    pub fn empty_char(self) -> char {
117        match self {
118            Self::Block => '░',
119            Self::Line => '─',
120            Self::LineDotted => '┄',
121            Self::Dots => '○',
122            Self::Arrow => '─',
123            Self::Rect => '▯',
124            Self::Custom { empty, .. } => empty,
125            Self::Braille => '⣀',
126        }
127    }
128
129    /// Get the partial fill characters for smooth rendering (if available).
130    pub fn partial_chars(self) -> Option<&'static [char]> {
131        match self {
132            // Disabled partials for Block to avoid "dark block" artifacts against shade char.
133            // Self::Block => Some(&['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']),
134            Self::Braille => Some(&['⣀', '⣄', '⣤', '⣦', '⣶', '⣷', '⣿']),
135            _ => None,
136        }
137    }
138}
139
140/// A progress bar widget.
141#[derive(Clone)]
142pub struct ProgressBar {
143    /// Progress value (0.0 to 1.0).
144    pub progress: f64,
145    /// Visual style.
146    pub progress_style: ProgressStyle,
147    /// Whether to show the percentage text.
148    pub show_percentage: bool,
149    /// Placement for percentage text.
150    pub percentage_position: ProgressTextPosition,
151    /// Custom label to show in addition to percentage text.
152    pub label: Option<String>,
153    /// Placement for custom label text.
154    pub label_position: ProgressTextPosition,
155    /// Style for the filled portion.
156    pub filled_style: Style,
157    /// Optional gradient for the filled portion (left -> right).
158    pub filled_gradient: Option<ColorGradient>,
159    /// Style for the empty portion.
160    pub empty_style: Style,
161    /// Style for the percentage/label text.
162    pub label_style: Style,
163    /// Base style.
164    pub style: Style,
165    /// Padding.
166    /// Default: `Padding::default()`.
167    pub padding: Padding,
168    /// Requested width.
169    /// Default: `Length::Flex(1)`.
170    pub width: Length,
171    /// Requested height.
172    /// Default: `Length::Auto`.
173    pub height: Length,
174    /// Whether the progress bar is draggable.
175    pub draggable: bool,
176    /// Drag step size (e.g. 0.1). If set, drag values snap to this increment.
177    pub step: Option<f64>,
178    /// Whether to invert the progress bar (fill from right to left).
179    pub inverted: bool,
180    /// Callback fired when progress changes (via drag).
181    pub on_change: Option<Callback<ProgressEvent>>,
182    /// Mouse click handler.
183    pub on_click: Option<Callback<MouseEvent>>,
184    /// Whether the progress bar is focusable.
185    pub focusable: bool,
186    /// Style when focused (applied to filled portion).
187    pub focus_style: StyleSlot,
188    /// Style when hovered.
189    pub hover_style: StyleSlot,
190    /// Optional target marker position in the [0.0, 1.0] range.
191    pub target: Option<f64>,
192    /// Style for the target marker.
193    pub target_style: Style,
194    /// Symbol used for the target marker.
195    pub target_symbol: char,
196    /// Threshold zones for filled segment styling.
197    pub zones: Vec<ProgressZone>,
198    /// Block-mode dim amount for empty track background in `[0.0, 1.0]`.
199    pub block_empty_bg_dim: f32,
200}
201
202impl Default for ProgressBar {
203    fn default() -> Self {
204        Self {
205            progress: 0.0,
206            progress_style: ProgressStyle::Block,
207            show_percentage: false,
208            percentage_position: ProgressTextPosition::Right,
209            label: None,
210            label_position: ProgressTextPosition::Right,
211            filled_style: Style::default(),
212            filled_gradient: None,
213            empty_style: Style::default(),
214            label_style: Style::default(),
215            style: Style::default(),
216            padding: Padding::default(),
217            width: Length::Flex(1),
218            height: Length::Auto,
219            draggable: false,
220            step: None,
221            inverted: false,
222            on_change: None,
223            on_click: None,
224            focusable: false,
225            focus_style: StyleSlot::Inherit,
226            hover_style: StyleSlot::Inherit,
227            target: None,
228            target_style: Style::default(),
229            target_symbol: '◆',
230            zones: Vec::new(),
231            block_empty_bg_dim: 0.85,
232        }
233    }
234}
235
236impl ProgressBar {
237    /// Create a new progress bar with the given progress (0.0 to 1.0).
238    pub fn new(progress: f64) -> Self {
239        Self {
240            progress: progress.clamp(0.0, 1.0),
241            ..Self::default()
242        }
243    }
244
245    /// Set the progress value (0.0 to 1.0).
246    pub fn progress(mut self, progress: f64) -> Self {
247        self.progress = progress.clamp(0.0, 1.0);
248        self
249    }
250
251    /// Set whether to invert the progress bar (fill from right to left).
252    pub fn inverted(mut self, inverted: bool) -> Self {
253        self.inverted = inverted;
254        self
255    }
256
257    /// Set the visual style.
258    pub fn progress_style(mut self, style: ProgressStyle) -> Self {
259        self.progress_style = style;
260        self
261    }
262
263    /// Show percentage text.
264    pub fn show_percentage(mut self, show: bool) -> Self {
265        self.show_percentage = show;
266        self
267    }
268
269    /// Set percentage text placement.
270    pub fn percentage_position(mut self, position: ProgressTextPosition) -> Self {
271        self.percentage_position = position;
272        self
273    }
274
275    /// Set custom label.
276    pub fn label(mut self, label: impl Into<String>) -> Self {
277        self.label = Some(label.into());
278        self
279    }
280
281    /// Set custom label placement.
282    pub fn label_position(mut self, position: ProgressTextPosition) -> Self {
283        self.label_position = position;
284        self
285    }
286
287    /// Set style for the filled portion.
288    pub fn filled_style(mut self, style: Style) -> Self {
289        self.filled_style = style;
290        self
291    }
292
293    /// Set gradient for the filled portion.
294    pub fn filled_gradient(mut self, gradient: ColorGradient) -> Self {
295        self.filled_gradient = Some(gradient);
296        self
297    }
298
299    /// Set style for the empty portion.
300    pub fn empty_style(mut self, style: Style) -> Self {
301        self.empty_style = style;
302        self
303    }
304
305    /// Set style for the label text.
306    pub fn label_style(mut self, style: Style) -> Self {
307        self.label_style = style;
308        self
309    }
310
311    /// Set base style.
312    pub fn style(mut self, style: Style) -> Self {
313        self.style = style;
314        self
315    }
316
317    /// Set padding.
318    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
319        self.padding = padding.into();
320        self
321    }
322
323    /// Set requested width.
324    pub fn width(mut self, width: Length) -> Self {
325        self.width = width;
326        self
327    }
328
329    /// Set requested height.
330    pub fn height(mut self, height: Length) -> Self {
331        self.height = height;
332        self
333    }
334
335    /// Make the progress bar draggable.
336    pub fn draggable(mut self, draggable: bool) -> Self {
337        self.draggable = draggable;
338        self
339    }
340
341    /// Set drag step size.
342    pub fn step(mut self, step: f64) -> Self {
343        self.step = Some(step);
344        self
345    }
346
347    /// Set callback for progress changes (via drag).
348    pub fn on_change(mut self, cb: Callback<ProgressEvent>) -> Self {
349        self.on_change = Some(cb);
350        self
351    }
352
353    /// Set mouse click handler.
354    pub fn on_click(mut self, cb: Callback<MouseEvent>) -> Self {
355        self.on_click = Some(cb);
356        self
357    }
358
359    /// Set whether the progress bar is focusable.
360    pub fn focusable(mut self, focusable: bool) -> Self {
361        self.focusable = focusable;
362        self
363    }
364
365    /// Set style when focused.
366    pub fn focus_style(mut self, style: Style) -> Self {
367        self.focus_style = StyleSlot::Replace(style);
368        self
369    }
370
371    /// Extend the themed focus style with the given style.
372    pub fn extend_focus_style(mut self, style: Style) -> Self {
373        self.focus_style = StyleSlot::Extend(style);
374        self
375    }
376
377    /// Inherit focus style from the active theme.
378    pub fn inherit_focus_style(mut self) -> Self {
379        self.focus_style = StyleSlot::Inherit;
380        self
381    }
382
383    /// Set the focus style slot directly.
384    pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
385        self.focus_style = slot;
386        self
387    }
388
389    /// Set style when hovered.
390    pub fn hover_style(mut self, style: Style) -> Self {
391        self.hover_style = StyleSlot::Replace(style);
392        self
393    }
394
395    /// Extend the themed hover style with the given style.
396    pub fn extend_hover_style(mut self, style: Style) -> Self {
397        self.hover_style = StyleSlot::Extend(style);
398        self
399    }
400
401    /// Inherit hover style from the active theme.
402    pub fn inherit_hover_style(mut self) -> Self {
403        self.hover_style = StyleSlot::Inherit;
404        self
405    }
406
407    /// Set the hover style slot directly.
408    pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
409        self.hover_style = slot;
410        self
411    }
412
413    /// Set optional target marker position in the [0.0, 1.0] range.
414    pub fn target(mut self, target: f64) -> Self {
415        self.target = Some(target.clamp(0.0, 1.0));
416        self
417    }
418
419    /// Clear target marker.
420    pub fn clear_target(mut self) -> Self {
421        self.target = None;
422        self
423    }
424
425    /// Set style for target marker.
426    pub fn target_style(mut self, style: Style) -> Self {
427        self.target_style = style;
428        self
429    }
430
431    /// Set symbol for target marker.
432    pub fn target_symbol(mut self, symbol: char) -> Self {
433        self.target_symbol = symbol;
434        self
435    }
436
437    /// Replace threshold zones.
438    pub fn zones(mut self, zones: impl IntoIterator<Item = ProgressZone>) -> Self {
439        self.zones = zones.into_iter().collect();
440        self
441    }
442
443    /// Add one threshold zone.
444    pub fn add_zone(mut self, zone: ProgressZone) -> Self {
445        self.zones.push(zone);
446        self
447    }
448
449    /// Set empty-track background dim amount for `ProgressStyle::Block`.
450    pub fn block_empty_bg_dim(mut self, amount: f32) -> Self {
451        self.block_empty_bg_dim = amount.clamp(0.0, 1.0);
452        self
453    }
454}
455
456impl From<ProgressBar> for Element {
457    fn from(value: ProgressBar) -> Self {
458        Element::new(ElementKind::ProgressBar(value))
459    }
460}
461
462impl crate::layout::hash::LayoutHash for ProgressBar {
463    fn layout_hash(
464        &self,
465        hasher: &mut impl std::hash::Hasher,
466        _recurse: &dyn Fn(&Element) -> Option<u64>,
467    ) -> Option<()> {
468        use std::hash::Hash;
469        self.width.hash(hasher);
470        self.height.hash(hasher);
471        self.show_percentage.hash(hasher);
472        self.percentage_position.hash(hasher);
473        self.label_position.hash(hasher);
474        self.padding.hash(hasher);
475        self.label.hash(hasher);
476        Some(())
477    }
478}