pub mod document;
pub mod ground;
pub mod picker;
use bevy_app::{App, Startup, Update};
use bevy_ecs::change_detection::DetectChanges;
use bevy_ecs::prelude::{IntoScheduleConfigs, Query, Res, ResMut, Resource};
use bevy_ecs::schedule::SystemSet;
use plurimus::core::ratatui_core::style::{Color, Modifier, Style};
use plurimus::core::{Background, TerminalCamera};
use plurimus::ui::UiTheme;
use plurimus::widgets::{TableStripe, WidgetSystems};
use super::journal;
use super::settings::Settings;
pub fn plugin(app: &mut App) {
app.add_plugins((picker::plugin, ground::plugin));
app.insert_resource(Theme::terminal());
app.add_systems(Startup, wear_the_theme_set);
app.configure_sets(Update, Repainted.before(WidgetSystems::Style));
app.add_systems(Update, (sync_look, restripe).in_set(Repainted));
}
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Repainted;
#[derive(Resource, Clone, PartialEq, Eq, Debug)]
pub struct Theme {
pub bg: Option<Color>,
pub fg: Color,
pub dim: Color,
pub border: Color,
pub accent: Color,
pub selection_bg: Option<Color>,
pub stripe: Option<Color>,
pub over: Color,
pub good: Color,
pub caution: Color,
series: Vec<Color>,
}
impl Theme {
#[must_use]
pub fn terminal() -> Self {
Self {
bg: None,
fg: Color::Reset,
dim: Color::DarkGray,
border: Color::DarkGray,
accent: Color::Cyan,
selection_bg: None,
stripe: None,
over: Color::Red,
good: Color::Green,
caution: Color::Yellow,
series: vec![
Color::Cyan,
Color::Green,
Color::Magenta,
Color::Yellow,
Color::Blue,
Color::Red,
],
}
}
#[must_use]
pub fn dimmed(&self) -> Style {
Style::new().fg(self.dim)
}
#[must_use]
pub fn accented(&self) -> Style {
Style::new().fg(self.accent)
}
#[must_use]
pub fn bordered(&self) -> Style {
Style::new().fg(self.border)
}
#[must_use]
pub fn exceeded(&self) -> Style {
Style::new().fg(self.over)
}
#[must_use]
pub fn striped(&self) -> Style {
self.stripe
.map_or_else(Style::new, |stripe| Style::new().bg(stripe))
}
#[must_use]
pub fn series(&self, index: usize) -> Color {
self.series[index % self.series.len()]
}
fn background(&self) -> Background {
self.bg
.map_or(Background::TerminalDefault, Background::Clear)
}
#[must_use]
pub fn ui_theme(&self) -> UiTheme {
let normal = self
.bg
.map_or_else(Style::new, |bg| Style::new().bg(bg))
.fg(self.fg);
let hovered = self.stripe.map_or(normal, |stripe| normal.bg(stripe));
let pressed = self
.selection_bg
.map_or(normal, |selection| normal.bg(selection));
UiTheme::new()
.with_normal(normal)
.with_hovered(hovered)
.with_pressed(pressed)
.with_disabled(normal.fg(self.dim))
.with_focused(Style::new().fg(self.accent).add_modifier(Modifier::BOLD))
}
}
fn wear_the_theme_set(settings: Res<Settings>, mut theme: ResMut<Theme>) {
match document::resolve(&settings.theme, document::wanted_variant()) {
Ok(set) => *theme = set,
Err(error) => journal::warn(format!("config.toml: {error}")),
}
}
fn sync_look(
theme: Res<Theme>,
mut stock: ResMut<UiTheme>,
mut cameras: Query<&mut TerminalCamera>,
) {
if !theme.is_changed() {
return;
}
*stock = theme.ui_theme();
let background = theme.background();
for mut camera in &mut cameras {
camera.background = background;
}
}
fn restripe(theme: Res<Theme>, mut stripes: Query<&mut TableStripe>) {
for mut stripe in &mut stripes {
if theme.is_changed() || stripe.is_added() {
stripe.0 = theme.striped();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_terminal_theme_leaves_the_grounds_to_the_terminal() {
let theme = Theme::terminal();
let ui = theme.ui_theme();
assert_eq!(ui.normal, Style::new().fg(Color::Reset));
assert_eq!(ui.hovered, ui.normal);
assert_eq!(ui.pressed, ui.normal);
assert_eq!(ui.focused.fg, Some(Color::Cyan));
assert_eq!(theme.background(), Background::TerminalDefault);
}
#[test]
fn a_theme_with_grounds_fills_the_states_from_them() {
let theme = Theme {
bg: Some(Color::Rgb(0, 0, 0)),
stripe: Some(Color::Rgb(20, 20, 20)),
selection_bg: Some(Color::Rgb(40, 40, 40)),
..Theme::terminal()
};
let ui = theme.ui_theme();
assert_eq!(ui.normal.bg, Some(Color::Rgb(0, 0, 0)));
assert_eq!(ui.hovered.bg, Some(Color::Rgb(20, 20, 20)));
assert_eq!(ui.pressed.bg, Some(Color::Rgb(40, 40, 40)));
assert_eq!(theme.background(), Background::Clear(Color::Rgb(0, 0, 0)));
}
#[test]
fn a_chart_past_the_colours_the_theme_names_starts_them_again() {
let theme = Theme::terminal();
assert_eq!(theme.series(theme.series.len()), theme.series(0));
assert_ne!(theme.series(0), theme.series(1));
}
}