Skip to main content

slt/widgets/
feedback.rs

1/// State for the rich log viewer widget.
2#[derive(Debug, Clone)]
3pub struct RichLogState {
4    /// Log entries to display.
5    pub entries: Vec<RichLogEntry>,
6    /// Scroll offset (0 = top).
7    pub(crate) scroll_offset: usize,
8    /// Whether to auto-scroll to bottom when new entries are added.
9    pub auto_scroll: bool,
10    /// Maximum number of entries to keep (None = unlimited).
11    pub max_entries: Option<usize>,
12}
13
14/// A single entry in a RichLog.
15#[derive(Debug, Clone)]
16pub struct RichLogEntry {
17    /// Styled text segments for this entry.
18    pub segments: Vec<(String, Style)>,
19}
20
21impl RichLogState {
22    /// Default maximum entry cap used by [`RichLogState::new`].
23    ///
24    /// Long-running apps that push log entries continuously would otherwise
25    /// accumulate state without bound. Use [`RichLogState::new_unbounded`] to
26    /// opt out explicitly.
27    pub const DEFAULT_MAX_ENTRIES: usize = 10_000;
28
29    /// Create an empty rich log state with the default entry cap
30    /// ([`Self::DEFAULT_MAX_ENTRIES`]).
31    pub fn new() -> Self {
32        Self {
33            max_entries: Some(Self::DEFAULT_MAX_ENTRIES),
34            ..Self::new_unbounded()
35        }
36    }
37
38    /// Create an empty rich log state without an entry cap.
39    ///
40    /// Prefer [`RichLogState::new`] in long-running apps. Use this constructor
41    /// only when the host explicitly bounds growth elsewhere.
42    pub fn new_unbounded() -> Self {
43        Self {
44            entries: Vec::new(),
45            scroll_offset: 0,
46            auto_scroll: true,
47            max_entries: None,
48        }
49    }
50
51    /// Add a single-style entry to the log.
52    pub fn push(&mut self, text: impl Into<String>, style: Style) {
53        self.push_segments(vec![(text.into(), style)]);
54    }
55
56    /// Add a plain text entry using default style.
57    pub fn push_plain(&mut self, text: impl Into<String>) {
58        self.push(text, Style::new());
59    }
60
61    /// Add a multi-segment styled entry to the log.
62    pub fn push_segments(&mut self, segments: Vec<(String, Style)>) {
63        self.entries.push(RichLogEntry { segments });
64
65        if let Some(max_entries) = self.max_entries {
66            if self.entries.len() > max_entries {
67                let remove_count = self.entries.len() - max_entries;
68                self.entries.drain(0..remove_count);
69                self.scroll_offset = self.scroll_offset.saturating_sub(remove_count);
70            }
71        }
72
73        if self.auto_scroll {
74            self.scroll_offset = usize::MAX;
75        }
76    }
77
78    /// Clear all entries and reset scroll position.
79    pub fn clear(&mut self) {
80        self.entries.clear();
81        self.scroll_offset = 0;
82    }
83
84    /// Return number of entries in the log.
85    pub fn len(&self) -> usize {
86        self.entries.len()
87    }
88
89    /// Return true when no entries are present.
90    pub fn is_empty(&self) -> bool {
91        self.entries.is_empty()
92    }
93}
94
95impl Default for RichLogState {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101/// State for the calendar date picker widget.
102#[derive(Debug, Clone)]
103pub struct CalendarState {
104    /// Current display year.
105    pub year: i32,
106    /// Current display month (1–12).
107    pub month: u32,
108    /// Currently selected day, if any.
109    pub selected_day: Option<u32>,
110    pub(crate) cursor_day: u32,
111}
112
113impl CalendarState {
114    /// Create a new `CalendarState` initialized to the current month.
115    pub fn new() -> Self {
116        let (year, month) = Self::current_year_month();
117        Self::from_ym(year, month)
118    }
119
120    /// Create a `CalendarState` for a specific year and month.
121    pub fn from_ym(year: i32, month: u32) -> Self {
122        let month = month.clamp(1, 12);
123        Self {
124            year,
125            month,
126            selected_day: None,
127            cursor_day: 1,
128        }
129    }
130
131    /// Returns the selected date as `(year, month, day)`, if any.
132    pub fn selected_date(&self) -> Option<(i32, u32, u32)> {
133        self.selected_day.map(|day| (self.year, self.month, day))
134    }
135
136    /// Navigate to the previous month.
137    pub fn prev_month(&mut self) {
138        if self.month == 1 {
139            self.month = 12;
140            self.year -= 1;
141        } else {
142            self.month -= 1;
143        }
144        self.clamp_days();
145    }
146
147    /// Navigate to the next month.
148    pub fn next_month(&mut self) {
149        if self.month == 12 {
150            self.month = 1;
151            self.year += 1;
152        } else {
153            self.month += 1;
154        }
155        self.clamp_days();
156    }
157
158    pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
159        match month {
160            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
161            4 | 6 | 9 | 11 => 30,
162            2 => {
163                if Self::is_leap_year(year) {
164                    29
165                } else {
166                    28
167                }
168            }
169            _ => 30,
170        }
171    }
172
173    pub(crate) fn first_weekday(year: i32, month: u32) -> u32 {
174        let month = month.clamp(1, 12);
175        let offsets = [0_i32, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
176        let mut y = year;
177        if month < 3 {
178            y -= 1;
179        }
180        let sunday_based = (y + y / 4 - y / 100 + y / 400 + offsets[(month - 1) as usize] + 1) % 7;
181        ((sunday_based + 6) % 7) as u32
182    }
183
184    fn clamp_days(&mut self) {
185        let max_day = Self::days_in_month(self.year, self.month);
186        self.cursor_day = self.cursor_day.clamp(1, max_day);
187        if let Some(day) = self.selected_day {
188            self.selected_day = Some(day.min(max_day));
189        }
190    }
191
192    fn is_leap_year(year: i32) -> bool {
193        (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
194    }
195
196    fn current_year_month() -> (i32, u32) {
197        let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) else {
198            return (1970, 1);
199        };
200        let days_since_epoch = (duration.as_secs() / 86_400) as i64;
201        let (year, month, _) = Self::civil_from_days(days_since_epoch);
202        (year, month)
203    }
204
205    fn civil_from_days(days_since_epoch: i64) -> (i32, u32, u32) {
206        let z = days_since_epoch + 719_468;
207        let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
208        let doe = z - era * 146_097;
209        let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
210        let mut year = (yoe as i32) + (era as i32) * 400;
211        let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
212        let mp = (5 * doy + 2) / 153;
213        let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
214        let month = (mp + if mp < 10 { 3 } else { -9 }) as u32;
215        if month <= 2 {
216            year += 1;
217        }
218        (year, month, day)
219    }
220}
221
222impl Default for CalendarState {
223    fn default() -> Self {
224        Self::new()
225    }
226}
227
228/// Visual variant for buttons.
229///
230/// Controls the color scheme used when rendering a button. Pass to
231/// [`crate::Context::button_with`] to create styled button variants.
232///
233/// - `Default` — theme text color, primary when focused (same as `button()`)
234/// - `Primary` — primary color background with contrasting text
235/// - `Danger` — error/red color for destructive actions
236/// - `Outline` — bordered appearance without fill
237#[non_exhaustive]
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
239pub enum ButtonVariant {
240    /// Standard button style.
241    #[default]
242    Default,
243    /// Filled button with primary background color.
244    Primary,
245    /// Filled button with error/danger background color.
246    Danger,
247    /// Bordered button without background fill.
248    Outline,
249}
250
251/// Direction indicator for stat widgets.
252#[non_exhaustive]
253#[derive(Debug, Clone, Copy, PartialEq, Eq)]
254pub enum Trend {
255    /// Positive movement.
256    Up,
257    /// Negative movement.
258    Down,
259}
260
261// ── Select / Dropdown ─────────────────────────────────────────────────