use std::collections::VecDeque;
use eframe::egui;
const CAPACITY: usize = 500;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Level {
Info,
Warn,
Error,
}
impl Level {
pub fn color(self, visuals: &egui::Visuals) -> egui::Color32 {
match self {
Level::Info => visuals.weak_text_color(),
Level::Warn => crate::app::warn(visuals),
Level::Error => crate::app::bad(visuals),
}
}
}
pub struct Entry {
pub level: Level,
pub text: String,
pub at: f64,
}
pub struct Log {
entries: VecDeque<Entry>,
clock: f64,
status: (Level, String),
pub open: bool,
}
impl Default for Log {
fn default() -> Log {
Log {
entries: VecDeque::new(),
clock: 0.0,
status: (Level::Info, "Ready.".to_string()),
open: false,
}
}
}
impl Log {
pub fn tick(&mut self, ctx: &egui::Context) {
self.clock = ctx.input(|i| i.time);
}
pub fn info(&mut self, text: impl Into<String>) {
self.push(Level::Info, text);
}
pub fn warn(&mut self, text: impl Into<String>) {
self.push(Level::Warn, text);
}
pub fn error(&mut self, text: impl Into<String>) {
self.push(Level::Error, text);
}
pub fn say(&mut self, text: impl Into<String>) {
let text = text.into();
self.status = (Level::Info, text.clone());
self.push(Level::Info, text);
}
pub fn trouble(&mut self, text: impl Into<String>) {
let text = text.into();
self.status = (Level::Error, text.clone());
self.push(Level::Error, text);
}
pub fn status(&self) -> (Level, &str) {
(self.status.0, self.status.1.as_str())
}
fn push(&mut self, level: Level, text: impl Into<String>) {
if self.entries.len() == CAPACITY {
self.entries.pop_front();
}
self.entries.push_back(Entry {
level,
text: text.into(),
at: self.clock,
});
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Entry> {
self.entries.iter()
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn last(&self) -> Option<&Entry> {
self.entries.back()
}
pub fn ui(&mut self, ui: &mut egui::Ui) {
egui::ScrollArea::vertical()
.auto_shrink([false; 2])
.stick_to_bottom(true)
.show(ui, |ui| {
for entry in &self.entries {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
ui.label(
egui::RichText::new(format!("{:>8.1}s", entry.at))
.monospace()
.weak(),
);
ui.label(
egui::RichText::new(&entry.text)
.monospace()
.color(entry.level.color(ui.visuals())),
);
});
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_plain_line_reaches_the_strip_and_the_log_alike() {
let mut log = Log::default();
assert_eq!(log.status(), (Level::Info, "Ready."));
log.say("Sent “Africa Split” to Programs 7:4.");
assert_eq!(log.status().0, Level::Info);
assert_eq!(
log.last().unwrap().text,
"Sent “Africa Split” to Programs 7:4."
);
log.trouble("Could not read the instrument.");
assert_eq!(
log.status(),
(Level::Error, "Could not read the instrument.")
);
assert_eq!(log.len(), 2);
}
#[test]
fn protocol_detail_stays_out_of_the_status_line() {
let mut log = Log::default();
log.say("Reading Programs — bank 1…");
log.info("bank 1: 43 of 50 slots hold something");
assert_eq!(log.status().1, "Reading Programs — bank 1…");
}
#[test]
fn the_oldest_entry_is_dropped_once_the_ring_is_full() {
let mut log = Log::default();
for n in 0..CAPACITY + 10 {
log.info(format!("line {n}"));
}
assert_eq!(log.len(), CAPACITY);
assert_eq!(log.iter().next().unwrap().text, "line 10");
assert_eq!(log.last().unwrap().text, format!("line {}", CAPACITY + 9));
}
}