Skip to main content

shuyi_ui/
log.rs

1//! The engine's log (ADR-0030): lines with a level, a category and the
2//! frame they were given in, kept in a [`LogRing`] of the last
3//! [`LOG_LINES`].
4//!
5//! A screen or the engine logs through [`Ctx::log`](crate::Ctx::log); the
6//! platform's `App` takes the lines each frame, stamps them with its frame
7//! and keeps them for tests, tools and the backend's sinks. A log is
8//! output only: nothing reads it to decide anything. Its lines are
9//! English, never translated, and not player text.
10
11use crate::prelude::*;
12use alloc::collections::VecDeque;
13use alloc::collections::vec_deque;
14
15/// How serious a line is, from the most serious: `Error < Debug`.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[non_exhaustive]
18pub enum LogLevel {
19    /// Something failed: the game can't do what it was asked.
20    Error,
21    /// Something went wrong and the game made do (the defaults used).
22    Warn,
23    /// What happened, for anyone reading.
24    Info,
25    /// Detail for whoever is chasing a bug.
26    Debug,
27}
28
29/// What part of the engine or game a line is about.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31#[non_exhaustive]
32pub enum LogCategory {
33    /// The game's data files.
34    Assets,
35    /// Sounds, music and voices.
36    Audio,
37    /// Keys, controllers and their bindings.
38    Input,
39    /// The screens.
40    Screens,
41    /// Saves, settings and profiles.
42    Storage,
43    /// The platform: the load, the device.
44    Platform,
45    /// The game's own lines.
46    Game,
47}
48
49impl LogLevel {
50    /// Every level, from the most serious.
51    pub const ALL: [Self; 4] = [Self::Error, Self::Warn, Self::Info, Self::Debug];
52
53    /// Its name: `error`, `warn`, `info` or `debug`.
54    pub fn name(self) -> &'static str {
55        match self {
56            Self::Error => "error",
57            Self::Warn => "warn",
58            Self::Info => "info",
59            Self::Debug => "debug",
60        }
61    }
62
63    /// The level called `name`, if one is.
64    pub fn from_name(name: &str) -> Option<Self> {
65        Self::ALL.into_iter().find(|level| level.name() == name)
66    }
67}
68
69impl LogCategory {
70    /// Every category.
71    pub const ALL: [Self; 7] = [
72        Self::Assets,
73        Self::Audio,
74        Self::Input,
75        Self::Screens,
76        Self::Storage,
77        Self::Platform,
78        Self::Game,
79    ];
80
81    /// Its name, in lowercase: `assets` to `game`.
82    pub fn name(self) -> &'static str {
83        match self {
84            Self::Assets => "assets",
85            Self::Audio => "audio",
86            Self::Input => "input",
87            Self::Screens => "screens",
88            Self::Storage => "storage",
89            Self::Platform => "platform",
90            Self::Game => "game",
91        }
92    }
93
94    /// The category called `name`, if one is.
95    pub fn from_name(name: &str) -> Option<Self> {
96        Self::ALL
97            .into_iter()
98            .find(|category| category.name() == name)
99    }
100}
101
102/// One line of the log. Build it with [`LogLine::new`] (ADR-0039).
103#[derive(Debug, Clone, PartialEq, Eq)]
104#[non_exhaustive]
105pub struct LogLine {
106    /// The frame it was given in (frames count from 1; 0 is before the
107    /// first).
108    pub frame: u32,
109    /// How serious it is.
110    pub level: LogLevel,
111    /// What it is about.
112    pub category: LogCategory,
113    /// What it says, in English.
114    pub text: String,
115}
116
117impl LogLine {
118    /// A line given in `frame`.
119    pub fn new(
120        frame: u32,
121        level: LogLevel,
122        category: LogCategory,
123        text: impl Into<String>,
124    ) -> Self {
125        Self {
126            frame,
127            level,
128            category,
129            text: text.into(),
130        }
131    }
132}
133
134/// How many lines a log keeps: older ones are dropped and counted.
135pub const LOG_LINES: usize = 500;
136
137/// The last lines of a log, oldest first, and how many older ones were
138/// dropped. Each line pushed gets a sequence number, from 0 and never
139/// reused, so a reader takes the lines [`since`](Self::since) the last it
140/// took.
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct LogRing {
143    lines: VecDeque<LogLine>,
144    capacity: usize,
145    dropped: u32,
146    /// The sequence number of the next line pushed.
147    next: u64,
148}
149
150impl LogRing {
151    /// An empty ring that keeps `capacity` lines.
152    pub fn new(capacity: usize) -> Self {
153        Self {
154            lines: VecDeque::new(),
155            capacity,
156            dropped: 0,
157            next: 0,
158        }
159    }
160
161    /// Keeps `line`, dropping (and counting) the oldest when full.
162    pub fn push(&mut self, line: LogLine) {
163        self.next += 1;
164        if self.capacity == 0 {
165            self.add_dropped(1);
166            return;
167        }
168        if self.lines.len() == self.capacity {
169            self.lines.pop_front();
170            self.add_dropped(1);
171        }
172        self.lines.push_back(line);
173    }
174
175    /// The kept lines, oldest first.
176    pub fn lines(&self) -> vec_deque::Iter<'_, LogLine> {
177        self.lines.iter()
178    }
179
180    /// How many lines were dropped to keep the newest.
181    pub fn dropped(&self) -> u32 {
182        self.dropped
183    }
184
185    /// Counts `n` more lines dropped (elsewhere, before they got here).
186    pub fn add_dropped(&mut self, n: u32) {
187        self.dropped = self.dropped.saturating_add(n);
188    }
189
190    /// The sequence number the next line pushed gets.
191    pub fn next_seq(&self) -> u64 {
192        self.next
193    }
194
195    /// The kept lines whose sequence number is `seq` or more, oldest
196    /// first: those after the last a reader took, if it kept
197    /// [`next_seq`](Self::next_seq) then.
198    pub fn since(&self, seq: u64) -> vec_deque::Iter<'_, LogLine> {
199        let skip = skip_before(seq, self.next, self.lines.len());
200        self.lines.range(skip..)
201    }
202
203    /// Takes every kept line and the dropped count, leaving the ring empty
204    /// with none dropped. Sequence numbers go on from where they were.
205    pub fn take(&mut self) -> (Vec<LogLine>, u32) {
206        let lines = self.lines.drain(..).collect();
207        (lines, core::mem::take(&mut self.dropped))
208    }
209}
210
211/// How many of a ring's `kept` items, the newest numbered `next - 1`, are
212/// numbered below `seq`.
213pub(crate) fn skip_before(seq: u64, next: u64, kept: usize) -> usize {
214    let kept_u64 = u64::try_from(kept).unwrap_or(u64::MAX);
215    let skip = seq.saturating_sub(next.saturating_sub(kept_u64));
216    usize::try_from(skip).unwrap_or(usize::MAX).min(kept)
217}
218
219/// Which lines a log keeps: a level for every category, and a level per
220/// category over it. A line passes at its category's level or more
221/// serious.
222#[derive(Debug, Clone, PartialEq, Eq)]
223pub struct LogFilter {
224    level: LogLevel,
225    categories: Vec<(LogCategory, LogLevel)>,
226}
227
228impl Default for LogFilter {
229    /// [`LogLevel::Info`] and more serious, in every category.
230    fn default() -> Self {
231        Self::new(LogLevel::Info)
232    }
233}
234
235impl LogFilter {
236    /// `level` and more serious, in every category.
237    pub fn new(level: LogLevel) -> Self {
238        Self {
239            level,
240            categories: Vec::new(),
241        }
242    }
243
244    /// The same filter, with `category` at `level`.
245    #[must_use]
246    pub fn with(mut self, category: LogCategory, level: LogLevel) -> Self {
247        self.categories.retain(|(c, _)| *c != category);
248        self.categories.push((category, level));
249        self
250    }
251
252    /// Whether a line of `category` at `level` passes.
253    pub fn allows(&self, category: LogCategory, level: LogLevel) -> bool {
254        let at = self
255            .categories
256            .iter()
257            .find(|(c, _)| *c == category)
258            .map_or(self.level, |(_, l)| *l);
259        level <= at
260    }
261}
262
263#[cfg(test)]
264mod tests;