#[derive(Debug, Clone)]
pub struct RichLogState {
entries: std::collections::VecDeque<RichLogEntry>,
pub(crate) scroll_offset: usize,
pub auto_scroll: bool,
pub max_entries: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct RichLogEntry {
pub segments: Vec<(String, Style)>,
}
impl RichLogState {
pub const DEFAULT_MAX_ENTRIES: usize = 10_000;
pub fn new() -> Self {
Self {
max_entries: Some(Self::DEFAULT_MAX_ENTRIES),
..Self::new_unbounded()
}
}
pub fn new_unbounded() -> Self {
Self {
entries: std::collections::VecDeque::new(),
scroll_offset: 0,
auto_scroll: true,
max_entries: None,
}
}
pub fn push(&mut self, text: impl Into<String>, style: Style) {
self.push_segments(vec![(text.into(), style)]);
}
pub fn push_plain(&mut self, text: impl Into<String>) {
self.push(text, Style::new());
}
pub fn push_segments(&mut self, segments: Vec<(String, Style)>) {
self.push_entry(RichLogEntry { segments });
}
pub fn push_entry(&mut self, entry: RichLogEntry) {
self.entries.push_back(entry);
if let Some(max_entries) = self.max_entries
&& self.entries.len() > max_entries
{
let remove_count = self.entries.len() - max_entries;
for _ in 0..remove_count {
let _ = self.entries.pop_front();
}
self.scroll_offset = self.scroll_offset.saturating_sub(remove_count);
}
if self.auto_scroll {
self.scroll_offset = usize::MAX;
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.scroll_offset = 0;
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn entries(
&self,
) -> impl DoubleEndedIterator<Item = &RichLogEntry> + ExactSizeIterator + '_ {
self.entries.iter()
}
pub fn entries_mut(
&mut self,
) -> impl DoubleEndedIterator<Item = &mut RichLogEntry> + ExactSizeIterator + '_ {
self.entries.iter_mut()
}
pub fn entry(&self, index: usize) -> Option<&RichLogEntry> {
self.entries.get(index)
}
pub fn entry_mut(&mut self, index: usize) -> Option<&mut RichLogEntry> {
self.entries.get_mut(index)
}
}
impl Default for RichLogState {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod rich_log_tests {
use super::*;
fn texts(state: &RichLogState) -> Vec<&str> {
state
.entries()
.map(|entry| entry.segments[0].0.as_str())
.collect()
}
#[test]
fn cap_zero_discards_every_push() {
let mut state = RichLogState::new();
state.auto_scroll = false;
state.max_entries = Some(0);
state.push_plain("discarded");
assert!(state.is_empty());
assert_eq!(state.scroll_offset, 0);
}
#[test]
fn cap_one_retains_only_the_newest_entry() {
let mut state = RichLogState::new();
state.max_entries = Some(1);
state.push_plain("first");
state.push_plain("second");
assert_eq!(texts(&state), ["second"]);
}
#[test]
fn default_cap_retains_order_without_front_shifts() {
let mut state = RichLogState::new();
for index in 0..=RichLogState::DEFAULT_MAX_ENTRIES {
state.push_plain(index.to_string());
}
assert_eq!(state.len(), RichLogState::DEFAULT_MAX_ENTRIES);
assert_eq!(state.entry(0).unwrap().segments[0].0, "1");
assert_eq!(
state.entry(state.len() - 1).unwrap().segments[0].0,
RichLogState::DEFAULT_MAX_ENTRIES.to_string()
);
}
#[test]
fn unbounded_state_retains_more_than_the_default_cap() {
let mut state = RichLogState::new_unbounded();
for index in 0..=RichLogState::DEFAULT_MAX_ENTRIES {
state.push_plain(index.to_string());
}
assert_eq!(state.len(), RichLogState::DEFAULT_MAX_ENTRIES + 1);
assert_eq!(state.entry(0).unwrap().segments[0].0, "0");
}
#[test]
fn lowering_cap_evicts_in_bulk_on_the_next_push_and_adjusts_scroll() {
let mut state = RichLogState::new_unbounded();
state.auto_scroll = false;
for index in 0..5 {
state.push_plain(index.to_string());
}
state.scroll_offset = 4;
state.max_entries = Some(2);
state.push_plain("5");
assert_eq!(texts(&state), ["4", "5"]);
assert_eq!(state.scroll_offset, 0);
}
#[test]
fn raising_and_disabling_cap_preserves_existing_order() {
let mut state = RichLogState::new();
state.max_entries = Some(1);
state.push_plain("first");
state.max_entries = Some(3);
state.push_plain("second");
state.push_plain("third");
state.max_entries = None;
state.push_plain("fourth");
assert_eq!(texts(&state), ["first", "second", "third", "fourth"]);
}
#[test]
fn entry_mutation_and_clear_preserve_state_contract() {
let mut state = RichLogState::new();
state.push_plain("before");
state.entry_mut(0).unwrap().segments[0].0 = "after".to_owned();
assert_eq!(texts(&state), ["after"]);
state.clear();
assert!(state.is_empty());
assert_eq!(state.scroll_offset, 0);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CalDate {
pub year: i32,
pub month: u32,
pub day: u32,
}
impl CalDate {
fn key(&self) -> (i32, u32, u32) {
(self.year, self.month, self.day)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CalendarSelect {
#[default]
Single,
Range,
}
#[derive(Debug, Clone)]
pub struct CalendarState {
pub year: i32,
pub month: u32,
pub selected_day: Option<u32>,
pub(crate) cursor_day: u32,
pub(crate) mode: CalendarSelect,
pub(crate) anchor: Option<CalDate>,
pub(crate) extent: Option<CalDate>,
pub(crate) time_enabled: bool,
pub(crate) hour: u8,
pub(crate) minute: u8,
}
impl CalendarState {
pub fn new() -> Self {
let (year, month) = Self::current_year_month();
Self::from_ym(year, month)
}
pub fn from_ym(year: i32, month: u32) -> Self {
let month = month.clamp(1, 12);
Self {
year,
month,
selected_day: None,
cursor_day: 1,
mode: CalendarSelect::Single,
anchor: None,
extent: None,
time_enabled: false,
hour: 0,
minute: 0,
}
}
pub fn with_range(&mut self) -> &mut Self {
self.mode = CalendarSelect::Range;
self
}
pub fn with_time(&mut self) -> &mut Self {
self.time_enabled = true;
self
}
pub fn mode(&self) -> CalendarSelect {
self.mode
}
pub fn selected_date(&self) -> Option<(i32, u32, u32)> {
self.selected_day.map(|day| (self.year, self.month, day))
}
pub fn selected_range(&self) -> Option<(CalDate, CalDate)> {
if self.mode != CalendarSelect::Range {
return None;
}
let anchor = self.anchor?;
let extent = self.extent.unwrap_or(anchor);
if anchor.key() <= extent.key() {
Some((anchor, extent))
} else {
Some((extent, anchor))
}
}
pub fn selected_time(&self) -> Option<(u8, u8)> {
self.time_enabled.then_some((self.hour, self.minute))
}
pub(crate) fn cursor_date(&self) -> CalDate {
CalDate {
year: self.year,
month: self.month,
day: self.cursor_day,
}
}
pub(crate) fn set_anchor_to_cursor(&mut self) {
let cur = self.cursor_date();
self.anchor = Some(cur);
self.extent = None;
}
pub(crate) fn extend_to_cursor(&mut self) {
let cur = self.cursor_date();
if self.anchor.is_none() {
self.anchor = Some(cur);
}
self.extent = Some(cur);
}
pub(crate) fn in_range(&self, d: CalDate) -> bool {
match self.selected_range() {
Some((start, end)) => start.key() <= d.key() && d.key() <= end.key(),
None => false,
}
}
pub(crate) fn is_range_endpoint(&self, d: CalDate) -> bool {
match self.selected_range() {
Some((start, end)) => d == start || d == end,
None => false,
}
}
pub fn prev_month(&mut self) {
if self.month == 1 {
self.month = 12;
self.year -= 1;
} else {
self.month -= 1;
}
self.clamp_days();
}
pub fn next_month(&mut self) {
if self.month == 12 {
self.month = 1;
self.year += 1;
} else {
self.month += 1;
}
self.clamp_days();
}
pub(crate) fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if Self::is_leap_year(year) {
29
} else {
28
}
}
_ => 30,
}
}
pub(crate) fn first_weekday(year: i32, month: u32) -> u32 {
let month = month.clamp(1, 12);
let offsets = [0_i32, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
let mut y = year;
if month < 3 {
y -= 1;
}
let sunday_based = (y + y / 4 - y / 100 + y / 400 + offsets[(month - 1) as usize] + 1) % 7;
((sunday_based + 6) % 7) as u32
}
fn clamp_days(&mut self) {
let max_day = Self::days_in_month(self.year, self.month);
self.cursor_day = self.cursor_day.clamp(1, max_day);
if let Some(day) = self.selected_day {
self.selected_day = Some(day.min(max_day));
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}
fn current_year_month() -> (i32, u32) {
let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) else {
return (1970, 1);
};
let days_since_epoch = (duration.as_secs() / 86_400) as i64;
let (year, month, _) = Self::civil_from_days(days_since_epoch);
(year, month)
}
fn civil_from_days(days_since_epoch: i64) -> (i32, u32, u32) {
let z = days_since_epoch + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let mut year = (yoe as i32) + (era as i32) * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = (doy - (153 * mp + 2) / 5 + 1) as u32;
let month = (mp + if mp < 10 { 3 } else { -9 }) as u32;
if month <= 2 {
year += 1;
}
(year, month, day)
}
}
impl Default for CalendarState {
fn default() -> Self {
Self::new()
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum ButtonVariant {
#[default]
Default,
Primary,
Danger,
Outline,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Trend {
Up,
Down,
}
pub(crate) enum SchedKind {
Once {
dur: std::time::Duration,
fired: bool,
},
Every {
interval: std::time::Duration,
last: std::time::Instant,
},
Debounce {
dur: std::time::Duration,
quiet_started: std::time::Instant,
fired: bool,
},
}
pub(crate) struct SchedulerSlot {
pub(crate) started: std::time::Instant,
pub(crate) kind: SchedKind,
pub(crate) touched_this_frame: bool,
}
#[derive(Default)]
pub struct SchedulerState {
pub(crate) named: std::collections::HashMap<&'static str, SchedulerSlot>,
pub(crate) keyed: std::collections::HashMap<String, SchedulerSlot>,
pub(crate) exclusive: std::collections::HashMap<String, ExclusiveGroup>,
}
pub(crate) const EXCLUSIVE_RETIRED_LIMIT: usize = 256;
#[derive(Default)]
pub(crate) struct ExclusiveGroup {
pub(crate) winner: String,
pub(crate) retired: std::collections::HashSet<String>,
retired_order: std::collections::VecDeque<String>,
touched_this_frame: bool,
}
impl ExclusiveGroup {
pub(crate) fn claim(&mut self, id: &str) -> bool {
self.touched_this_frame = true;
if self.winner == id {
return true;
}
if self.retired.contains(id) {
return false;
}
if !self.winner.is_empty() {
let old = std::mem::take(&mut self.winner);
if self.retired.insert(old.clone()) {
self.retired_order.push_back(old);
}
while self.retired_order.len() > EXCLUSIVE_RETIRED_LIMIT {
if let Some(expired) = self.retired_order.pop_front() {
self.retired.remove(&expired);
}
}
}
self.winner = id.to_owned();
true
}
}
pub(crate) fn intervals_elapsed(
elapsed: std::time::Duration,
interval: std::time::Duration,
) -> u32 {
let nanos = interval.as_nanos().max(1);
let count = elapsed.as_nanos() / nanos;
count.min(u32::MAX as u128) as u32
}
impl SchedulerState {
pub(crate) fn gc_untouched(&mut self) {
self.named.retain(|_, slot| slot.touched_this_frame);
self.keyed.retain(|_, slot| slot.touched_this_frame);
self.exclusive.retain(|_, group| group.touched_this_frame);
for slot in self.named.values_mut() {
slot.touched_this_frame = false;
}
for slot in self.keyed.values_mut() {
slot.touched_this_frame = false;
}
for group in self.exclusive.values_mut() {
group.touched_this_frame = false;
}
}
#[cfg(test)]
pub(crate) fn slot_count(&self) -> usize {
self.named.len() + self.keyed.len()
}
}
#[cfg(test)]
mod exclusive_group_tests {
use super::*;
#[test]
fn exclusive_history_stays_bounded_under_thousands_of_claims() {
let mut group = ExclusiveGroup::default();
for id in 0..10_000 {
assert!(group.claim(&format!("claim-{id}")));
assert!(group.retired.len() <= EXCLUSIVE_RETIRED_LIMIT);
assert!(group.retired_order.len() <= EXCLUSIVE_RETIRED_LIMIT);
}
assert_eq!(group.winner, "claim-9999");
assert!(group.claim("claim-9999"));
assert!(!group.claim("claim-9998"));
}
#[test]
fn untouched_exclusive_groups_are_garbage_collected() {
let mut scheduler = SchedulerState::default();
scheduler
.exclusive
.entry("search".to_owned())
.or_default()
.claim("first");
scheduler.gc_untouched();
assert_eq!(scheduler.exclusive.len(), 1);
scheduler.gc_untouched();
assert!(scheduler.exclusive.is_empty());
}
}