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