mod common;
use abstracttui::base::palette::XTERM_256;
use abstracttui::prelude::*;
use abstracttui::render::color::{
nearest_xterm256, quantize_set_256_into_with, GroundIntent, PairIntent,
};
use abstracttui::theme::contrast::{floors, ink_on};
use abstracttui::theme::{
register, themes, RegisterMode, Theme, ThemeCandidate, TokenId, TokenSet,
};
use abstracttui::ui::Canvas;
const PANEL: Rgba = Rgba::rgb(41, 49, 62);
const PANEL_BRIGHT: Rgba = Rgba::rgb(240, 200, 90);
const LABEL_W: i32 = 16;
const SWATCH_W: i32 = 10;
fn main() -> abstracttui::base::Result<()> {
if !abstracttui::term::have_tty() {
println!("grounds: needs an interactive terminal — skipping cleanly");
return Ok(());
}
let mut app = App::new(Size::new(96, 30));
let quitter = app.quitter();
app.mount(move |cx| {
let sel = cx.signal(0usize);
let declared = cx.signal(true);
let intent = cx.signal(false);
let n = themes().len();
Element::new()
.style(LayoutStyle::fill())
.shortcut(KeyChord::plain(Key::Char('q')), move |_| quitter.quit())
.shortcut(KeyChord::plain(Key::Char('p')), move |_| {
declared.update(|d| *d = !*d)
})
.shortcut(KeyChord::plain(Key::Char('i')), move |_| {
intent.update(|d| *d = !*d)
})
.shortcut(KeyChord::plain(Key::Up), move |_| {
sel.update(|s| *s = s.saturating_sub(1))
})
.shortcut(KeyChord::plain(Key::Down), move |_| {
sel.update(|s| *s = (*s + 1).min(n - 1))
})
.child(dyn_view(LayoutStyle::fill(), move || {
let (sel, declared, intent) = (sel.get(), declared.get(), intent.get());
let live = active_theme(sel, intent);
abstracttui::app::set_theme(live);
Element::new()
.style(LayoutStyle::fill())
.draw(move |canvas, rect| draw(canvas, rect, live, sel, declared))
.build()
}))
.build()
})?;
app.run_with(RunConfig {
extra_grounds: vec![PANEL, PANEL_BRIGHT],
..RunConfig::default()
})
}
type GroundRow = (String, Rgba, u8, u8);
fn colliding_pairs(t: &TokenSet) -> Vec<(TokenId, TokenId, PairIntent)> {
let g = t.grounds();
let mut out = Vec::new();
for i in 0..g.len() {
for j in (i + 1)..g.len() {
if nearest_xterm256(g[i].1) == nearest_xterm256(g[j].1) {
out.push((g[i].0, g[j].0, PairIntent::Same));
}
}
}
out
}
fn active_theme(sel: usize, intent: bool) -> &'static Theme {
let base = &themes()[sel];
if !intent {
return base;
}
let pairs = colliding_pairs(&base.tokens);
if pairs.is_empty() {
return base;
}
register(
ThemeCandidate {
id: format!("{}-ground-intent", base.id),
label: format!("{} (grounds declared)", base.label),
dark: base.dark,
tokens: base.tokens,
ground_intent: pairs,
},
RegisterMode::Labeled,
)
.map(|r| r.theme)
.unwrap_or(base)
}
fn rows(theme: &'static Theme, declared: bool) -> (Vec<GroundRow>, usize, usize) {
let t = theme.tokens;
let mut names: Vec<String> = t
.grounds()
.iter()
.map(|(id, _)| id.name().to_string())
.collect();
let mut colors: Vec<Rgba> = t.grounds().iter().map(|(_, c)| *c).collect();
names.push("+ panel".into());
colors.push(PANEL);
names.push("+ panel bright".into());
colors.push(PANEL_BRIGHT);
let before: Vec<u8> = colors.iter().map(|c| nearest_xterm256(*c)).collect();
let set_len = if declared { colors.len() } else { 5 };
let pairs = TokenSet::resolve_ground_intent(theme.ground_intent)
.expect("a registered theme's declaration names only grounds");
let mut after = vec![0u8; set_len];
quantize_set_256_into_with(&colors[..set_len], GroundIntent::new(&pairs), &mut after);
if !declared {
after.push(nearest_xterm256(PANEL));
after.push(nearest_xterm256(PANEL_BRIGHT));
}
let distinct = |v: &[u8]| {
let mut s: Vec<u8> = v.to_vec();
s.sort_unstable();
s.dedup();
s.len()
};
let (nb, na) = (distinct(&before), distinct(&after));
let out = (0..colors.len())
.map(|i| (names[i].clone(), colors[i], before[i], after[i]))
.collect();
(out, nb, na)
}
fn band(canvas: &mut dyn Canvas, x: i32, y: i32, t: &TokenSet, ground: Rgba) {
canvas.fill(Rect::new(x, y, SWATCH_W, 1), ' ', t.text, ground);
let ink = ink_on(t, ground);
canvas.print(Point::new(x + 3, y), "Text", ink.color, Rgba::TRANSPARENT);
}
fn draw(canvas: &mut dyn Canvas, rect: Rect, theme: &'static Theme, sel: usize, declared: bool) {
let t = theme.tokens;
if common::too_small(canvas, rect, Size::new(72, 17), &t) {
return;
}
canvas.fill(rect, ' ', t.text, t.bg);
let (rows, n_before, n_after) = rows(theme, declared);
let total = rows.len();
let mut y = rect.y;
canvas.print(
Point::new(rect.x + 2, y),
&format!("{} ({}/{})", theme.id, sel + 1, themes().len()),
t.accent,
Rgba::TRANSPARENT,
);
y += 1;
canvas.print(
Point::new(rect.x + 2, y),
"↑/↓ theme · p declared panel ground · i theme ground intent · q quit",
t.text_faint,
Rgba::TRANSPARENT,
);
y += 2;
let x_true = rect.x + 2 + LABEL_W;
let x_before = x_true + SWATCH_W + 2;
let x_after = x_before + SWATCH_W + 6;
for (label, x) in [
("truecolor", x_true),
("nearest (was)", x_before),
("assigned (is)", x_after),
] {
canvas.print(Point::new(x, y), label, t.text_muted, Rgba::TRANSPARENT);
}
y += 1;
for (name, color, before, after) in &rows {
if y >= rect.bottom() - 6 {
break;
}
canvas.print(Point::new(rect.x + 2, y), name, t.text, Rgba::TRANSPARENT);
band(canvas, x_true, y, &t, *color);
band(canvas, x_before, y, &t, XTERM_256[*before as usize]);
canvas.print(
Point::new(x_before + SWATCH_W + 1, y),
&format!("{before:>3}"),
if collides(&rows, *before, |r| r.2) {
t.error
} else {
t.text_faint
},
Rgba::TRANSPARENT,
);
band(canvas, x_after, y, &t, XTERM_256[*after as usize]);
canvas.print(
Point::new(x_after + SWATCH_W + 1, y),
&format!("{after:>3}"),
if collides(&rows, *after, |r| r.3) {
t.error
} else {
t.text_faint
},
Rgba::TRANSPARENT,
);
y += 1;
}
y += 1;
let verdict =
format!("{n_before} of {total} distinct entries before · {n_after} of {total} after",);
canvas.print(
Point::new(rect.x + 2, y),
&verdict,
if n_before < total { t.warn } else { t.text },
Rgba::TRANSPARENT,
);
y += 1;
let note = if declared {
"panel ground DECLARED (RunConfig::extra_grounds) — it joins the set"
} else {
"panel ground NOT declared — the separator was never handed it"
};
canvas.print(
Point::new(rect.x + 2, y),
note,
if declared { t.ok } else { t.warn },
Rgba::TRANSPARENT,
);
y += 1;
let declaring = theme.ground_intent.len();
let releasable = colliding_pairs(&t).len();
let intent_note = match (declaring, releasable) {
(0, 0) => "theme declares nothing — and no pair of its grounds collides, so `i` has nothing to release".into(),
(0, n) => format!("theme declares nothing — {n} pair(s) the separator is holding apart; press i"),
(n, _) => format!(
"LIVE THEME IS `{}`, declaring {n} pair(s) Same — the driver read that off the theme",
theme.id
),
};
canvas.print(
Point::new(rect.x + 2, y),
&intent_note,
if declaring > 0 { t.ok } else { t.text_faint },
Rgba::TRANSPARENT,
);
y += 1;
canvas.print(
Point::new(rect.x + 2, y),
"a repeated index in red is two grounds arriving as ONE colour",
t.text_faint,
Rgba::TRANSPARENT,
);
y += 1;
let worst = rows
.iter()
.map(|(name, color, _, _)| (name, ink_on(&t, *color).contrast))
.fold(None::<(&String, f32)>, |acc, (n, c)| match acc {
Some((_, best)) if best <= c => acc,
_ => Some((n, c)),
});
if let Some((name, c)) = worst {
let ok = c >= floors::TEXT;
canvas.print(
Point::new(rect.x + 2, y),
&format!(
"text on grounds: worst is {name} at {c:.2}:1 ({}) — ink_on picks the pole",
if ok { "clears 4.5" } else { "MISSES 4.5" }
),
if ok { t.text_faint } else { t.error },
Rgba::TRANSPARENT,
);
}
}
fn collides(
rows: &[(String, Rgba, u8, u8)],
entry: u8,
pick: fn(&(String, Rgba, u8, u8)) -> u8,
) -> bool {
rows.iter().filter(|r| pick(r) == entry).count() > 1
}