use std::collections::VecDeque;
use std::hash::Hash;
use egui::Id;
use crate::theme::Theme;
use crate::{Button, ButtonSize, CollapsingSection};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogKind {
Sys,
Out,
In,
Err,
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub time: String,
pub kind: LogKind,
pub msg: String,
}
const DEFAULT_CAPACITY: usize = 100;
const SCROLL_MAX_HEIGHT: f32 = 200.0;
#[derive(Debug)]
pub struct LogBar {
entries: VecDeque<LogEntry>,
open: bool,
capacity: usize,
id_salt: Id,
heading: String,
}
impl Default for LogBar {
fn default() -> Self {
Self::new()
}
}
impl LogBar {
pub fn new() -> Self {
Self {
entries: VecDeque::new(),
open: false,
capacity: DEFAULT_CAPACITY,
id_salt: Id::new("elegance::log_bar"),
heading: "Message Log".into(),
}
}
pub fn heading(mut self, heading: impl Into<String>) -> Self {
self.heading = heading.into();
self
}
pub fn max_entries(mut self, n: usize) -> Self {
self.capacity = n.max(1);
while self.entries.len() > self.capacity {
self.entries.pop_back();
}
self
}
pub fn id_salt(mut self, salt: impl Hash) -> Self {
self.id_salt = Id::new(("elegance::log_bar", salt));
self
}
pub fn push(&mut self, kind: LogKind, msg: impl Into<String>) {
self.entries.push_front(LogEntry {
time: now_hms(),
kind,
msg: msg.into(),
});
while self.entries.len() > self.capacity {
self.entries.pop_back();
}
}
pub fn sys(&mut self, msg: impl Into<String>) {
self.push(LogKind::Sys, msg);
}
pub fn out(&mut self, msg: impl Into<String>) {
self.push(LogKind::Out, msg);
}
pub fn recv(&mut self, msg: impl Into<String>) {
self.push(LogKind::In, msg);
}
pub fn err(&mut self, msg: impl Into<String>) {
self.push(LogKind::Err, msg);
}
pub fn clear(&mut self) {
self.entries.clear();
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn is_open(&self) -> bool {
self.open
}
pub fn set_open(&mut self, open: bool) {
self.open = open;
}
pub fn entries(&self) -> impl Iterator<Item = &LogEntry> {
self.entries.iter()
}
pub fn show(&mut self, ui: &mut egui::Ui) {
let fill = Theme::current(ui.ctx()).palette.card;
egui::Panel::bottom(self.id_salt)
.resizable(false)
.frame(
egui::Frame::new()
.fill(fill)
.inner_margin(egui::Margin::symmetric(16, 6)),
)
.show_inside(ui, |ui| {
let theme = Theme::current(ui.ctx());
let count = self.entries.len();
let label = if count == 0 {
self.heading.clone()
} else {
format!("{} \u{00b7} {count}", self.heading)
};
let mut clear = false;
CollapsingSection::new(self.id_salt.with("collapse"), label)
.open(&mut self.open)
.show(ui, |ui| {
ui.add_space(2.0);
egui::ScrollArea::vertical()
.max_height(SCROLL_MAX_HEIGHT)
.auto_shrink([false, true])
.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 2.0;
if self.entries.is_empty() {
ui.add(egui::Label::new(theme.faint_text("(no messages)")));
} else {
for entry in self.entries.iter() {
log_row(ui, &theme, entry);
}
}
});
ui.add_space(6.0);
if ui
.add(Button::new("Clear").outline().size(ButtonSize::Small))
.clicked()
{
clear = true;
}
});
if clear {
self.entries.clear();
}
});
}
}
fn now_hms() -> String {
let day = now_unix_secs() % 86400;
format!("{:02}:{:02}:{:02}", day / 3600, (day / 60) % 60, day % 60)
}
#[cfg(not(target_family = "wasm"))]
fn now_unix_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(target_family = "wasm")]
fn now_unix_secs() -> u64 {
(js_sys::Date::now() / 1000.0) as u64
}
fn log_row(ui: &mut egui::Ui, theme: &Theme, entry: &LogEntry) {
let p = &theme.palette;
let t = &theme.typography;
let (color, arrow) = match entry.kind {
LogKind::Sys => (p.text_faint, ""),
LogKind::Out => (p.text_muted, "\u{2192} "),
LogKind::In => (p.success, "\u{2190} "),
LogKind::Err => (p.danger, ""),
};
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add(egui::Label::new(
egui::RichText::new(&entry.time)
.monospace()
.color(p.text_faint)
.size(t.small),
));
ui.add_space(10.0);
ui.add(egui::Label::new(
egui::RichText::new(format!("{arrow}{}", entry.msg))
.monospace()
.color(color)
.size(t.small),
));
});
}