mod common;
use abstracttui::prelude::*;
use abstracttui::theme::themes;
use abstracttui::ui::{MouseKind, Phase, UiEvent};
fn main() -> abstracttui::base::Result<()> {
if !abstracttui::term::have_tty() {
println!("components: 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 theme = use_theme(cx);
let deploys = cx.signal(12u32);
let alerts = cx.signal(2u32);
let uptime_days = cx.signal(97u32);
let name = cx.signal(String::from("orion"));
let notify = cx.signal(true);
let channel = cx.signal(0usize);
let theme_ix = cx.signal(0usize);
let features = cx.signal(Vec::<String>::new());
let ext_folded = cx.signal(true);
Element::new()
.style(LayoutStyle::column().padding(Edges::all(1)).gap(1))
.shortcut(KeyChord::plain(Key::Char('q')), move |_| quitter.quit())
.shortcut(KeyChord::new(Mods::CTRL, Key::Char('t')), move |_| {
theme_ix.update(|i| *i = (*i + 1) % themes().len());
set_theme_by_id(themes()[theme_ix.get_untracked()].id);
})
.child(dyn_view_scoped(LayoutStyle::default().grow(1.0), move |gcx| {
let t = theme.get().tokens;
Element::new()
.style(LayoutStyle::column().gap(1))
.child(toolbar(
&t,
vec![
Button::new("deploy")
.on_click(move || deploys.update(|n| *n += 1))
.element(gcx, &t)
.build(),
Button::new("ack alert")
.on_click(move || alerts.update(|n| *n = n.saturating_sub(1)))
.element(gcx, &t)
.build(),
Button::new("disabled").disabled(true).element(gcx, &t).build(),
],
))
.child(
Element::new()
.style(LayoutStyle::row().gap(2).h(6))
.child(stat_card(&t, "deploys", deploys, Trend::Up, move || {
deploys.update(|n| *n += 1)
}))
.child(stat_card(&t, "alerts", alerts, Trend::Down, move || {
alerts.update(|n| *n = n.saturating_sub(1))
}))
.child(stat_card(&t, "uptime d", uptime_days, Trend::Flat, move || {
uptime_days.update(|n| *n += 1)
}))
.build(),
)
.child(
Block::new()
.title("settings")
.fill(t.surface)
.shadow(t.shadow_ground)
.layout(LayoutStyle::column().gap(1).grow(1.0))
.child(field(
&t,
"name",
TextInput::new()
.value(name)
.placeholder("service name…")
.layout(LayoutStyle::default().w(32).h(1))
.element(gcx, &t)
.build(),
))
.child(field(
&t,
"notifications",
Checkbox::new("page the on-call")
.checked(notify)
.element(gcx, &t)
.build(),
))
.child(field(
&t,
"channel",
RadioGroup::new(vec!["stable".to_string(), "beta".to_string(), "nightly".to_string()])
.selection(channel)
.element(gcx, &t)
.build(),
))
.child(field(
&t,
"channel select",
Select::new(vec![
SelectOption::new("stable").hint("lts"),
SelectOption::new("beta"),
SelectOption::new("nightly").hint("daily"),
])
.value(channel)
.layout(LayoutStyle::default().w(28).h(1).shrink(0.0))
.element(gcx, &t)
.build(),
))
.child(field(
&t,
"theme",
Combobox::new(
themes()
.iter()
.map(|th| {
SelectOption::new(th.label)
.hint(if th.dark { "dark" } else { "light" })
})
.collect(),
)
.value(theme_ix)
.placeholder("type to search themes…")
.on_change(|i| {
set_theme_by_id(themes()[i].id);
})
.layout(LayoutStyle::default().w(28).h(1).shrink(0.0))
.element(gcx, &t)
.build(),
))
.child(field(
&t,
"features",
MultiSelect::new(vec![
SelectOption::new("autosave"),
SelectOption::new("telemetry"),
SelectOption::new("backups"),
SelectOption::new("beta api").disabled(true),
])
.values(features)
.placeholder("enable features…")
.layout(LayoutStyle::default().w(28).h(1).shrink(0.0))
.element(gcx, &t)
.build(),
))
.child(dyn_view(LayoutStyle::default().h(1), move || {
let ch = ["stable", "beta", "nightly"][channel.get().min(2)];
text(format!(
"→ {} · notifications {} · {} channel · {} features",
if name.get().is_empty() { "unnamed".into() } else { name.get() },
if notify.get() { "on" } else { "off" },
ch,
features.get().len(),
))
}))
.element(&t)
.build(),
)
.child(
Element::new()
.style(LayoutStyle::row().gap(2))
.child(
Button::new("toggle 3rd")
.on_click(move || ext_folded.update(|f| *f = !*f))
.element(gcx, &t)
.build(),
)
.child(
Disclosure::markdown("welcome", "**hello** — this card starts open")
.initially_folded(false)
.max_body_rows(3)
.layout(card_slot())
.element(gcx, &t)
.build(),
)
.child(
Disclosure::text("build log", build_log())
.detail("14 lines")
.max_body_rows(4)
.layout(card_slot())
.element(gcx, &t)
.build(),
)
.child(
Disclosure::text("external", "fold state lives in an app signal")
.folded(ext_folded)
.layout(card_slot())
.element(gcx, &t)
.build(),
)
.build(),
)
.child(text(
"tab focus · enter/space activate · click cards · ctrl+t theme · q quit",
))
.build()
}))
.build()
})?;
app.run()
}
#[derive(Copy, Clone)]
enum Trend {
Up,
Down,
Flat,
}
fn stat_card(
t: &TokenSet,
label: &'static str,
value: Signal<u32>,
trend: Trend,
mut on_click: impl FnMut() + 'static,
) -> View {
let (arrow, arrow_ink) = match trend {
Trend::Up => ("▲", t.ok),
Trend::Down => ("▼", t.error),
Trend::Flat => ("→", t.text_muted),
};
let label_ink = t.text_muted;
let value_ink = t.accent;
Block::new()
.border(BorderKind::Rounded)
.fill(t.surface)
.shadow(t.shadow_ground)
.layout(LayoutStyle::column().grow(1.0))
.child(
Element::new()
.style(LayoutStyle::default().h(1))
.draw(move |canvas, rect| {
canvas.print(
Point::new(rect.x, rect.y),
label,
label_ink,
Rgba::TRANSPARENT,
);
})
.build(),
)
.child(dyn_view(LayoutStyle::default().h(1), move || {
let shown = format!("{}", value.get());
Element::new()
.style(LayoutStyle::default().h(1))
.draw(move |canvas, rect| {
canvas.print(
Point::new(rect.x, rect.y),
&shown,
value_ink,
Rgba::TRANSPARENT,
);
})
.build()
}))
.child(
Element::new()
.style(LayoutStyle::default().h(1))
.draw(move |canvas, rect| {
canvas.print(
Point::new(rect.x, rect.y),
arrow,
arrow_ink,
Rgba::TRANSPARENT,
);
})
.build(),
)
.element(t)
.focusable()
.on(Phase::Bubble, move |ctx, ev| match ev {
UiEvent::Mouse(m) if matches!(m.kind, MouseKind::Down(_)) => {
on_click();
ctx.stop_propagation();
}
UiEvent::Key(k) if k.key == Key::Enter || k.key == Key::Char(' ') => {
on_click();
ctx.stop_propagation();
}
_ => {}
})
.build()
}
fn field(t: &TokenSet, label: &'static str, child: View) -> View {
let ink = t.text_muted;
Element::new()
.style(LayoutStyle::row().gap(1))
.child(
Element::new()
.style(LayoutStyle::default().w(15).h(1))
.draw(move |canvas, rect| {
canvas.print(Point::new(rect.x, rect.y), label, ink, Rgba::TRANSPARENT);
})
.build(),
)
.child(child)
.build()
}
fn card_slot() -> LayoutStyle {
LayoutStyle::column()
.grow(1.0)
.basis(Dimension::Cells(0))
.shrink(0.0)
}
fn build_log() -> String {
(0..14)
.map(|i| format!("compiling unit {i:>2} … ok"))
.collect::<Vec<_>>()
.join("\n")
}
fn toolbar(t: &TokenSet, children: Vec<View>) -> View {
let ground = t.surface_raised;
let ink = t.text;
Element::new()
.style(LayoutStyle::row().gap(2).h(1).padding(Edges::hv(1, 0)))
.draw(move |canvas, rect| {
canvas.fill(rect, ' ', ink, ground);
})
.children(children)
.build()
}