facett-core/examples/deck_fx_demo.rs
//! **deck_fx_demo** — the deck-level effects + theming, wired through
//! [`FacetDeck`] itself (not the bare `effects` module).
//!
//! Shows the three opt-in deck features from `deckfx.rs`:
//!
//! * **Palette switcher** — [`FacetDeck::palette_picker`] over every [`Theme`] in
//! [`Theme::ALL`]; pick one and the *whole deck* (every facet) re-themes.
//! * **Glow toggle** — flip `deck.fx_mut().glow`; the active facet's content rect
//! blooms with the palette's `glow` colour, pulsing.
//! * **Send the raven** — [`FacetDeck::send_raven`] launches the signature
//! [`RavenSprite`](facett_core::effects::RavenSprite) to **perch on a target
//! rect** (here, the selected row of the "Datasets" facet).
//!
//! Everything is **off by default**: a deck that never calls these is unchanged.
//! Pure egui on the **glow** backend — no wgpu, no custom shader.
//!
//! Run: `cargo run -p facett-core --example deck_fx_demo`
use eframe::egui;
use egui::{Align2, FontId, Rect, Sense, Vec2, vec2};
use facett_core::{Facet, FacetCaps, FacetDeck, theme};
const ROWS: &[&str] = &["bronze.events", "silver.sessions", "gold.kpis", "ml.features", "exports.daily"];
/// A tiny "datasets" facet: status rows that double as **perch targets**. It
/// records each row's rect into a shared cell so the host can summon the raven
/// onto the selected row through the deck.
struct Datasets {
selected: usize,
/// Shared with the app so it can read the live row rects (the deck doesn't
/// know facet internals — the host bridges the rect to `send_raven`).
row_rects: std::rc::Rc<std::cell::RefCell<Vec<Rect>>>,
}
impl Facet for Datasets {
fn title(&self) -> &str {
"Datasets"
}
fn caps(&self) -> FacetCaps {
FacetCaps::NONE.themeable().selectable()
}
fn ui(&mut self, ui: &mut egui::Ui) {
let th = theme(ui);
ui.label("Click a row to aim the raven, then “Send raven” above.");
ui.add_space(6.0);
let mut rects = self.row_rects.borrow_mut();
rects.clear();
for (i, name) in ROWS.iter().enumerate() {
let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width().min(420.0), 34.0), Sense::click());
rects.push(rect);
let selected = i == self.selected;
let p = ui.painter();
let fill = if selected { th.accent.linear_multiply(0.18) } else { th.node_fill };
p.rect_filled(rect, 5.0, fill);
p.rect_stroke(rect, 5.0, egui::Stroke::new(1.0_f32, th.panel_stroke), egui::StrokeKind::Inside);
p.text(rect.left_center() + vec2(12.0, 0.0), Align2::LEFT_CENTER, *name, FontId::proportional(15.0), th.text);
if resp.clicked() {
self.selected = i;
}
}
}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({ "selected": ROWS[self.selected], "rows": ROWS.len() })
}
}
/// A second facet so the palette switch visibly re-themes more than one tab — a
/// little themed "metrics" panel.
struct Metrics;
impl Facet for Metrics {
fn title(&self) -> &str {
"Metrics"
}
fn caps(&self) -> FacetCaps {
FacetCaps::NONE.themeable()
}
fn ui(&mut self, ui: &mut egui::Ui) {
let th = theme(ui);
ui.label("Throughput (rows/s) — colours come from the active palette.");
ui.add_space(8.0);
let bars = [0.9_f32, 0.6, 0.75, 0.4, 0.85, 0.55, 0.7];
let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width().min(420.0), 120.0), Sense::hover());
let p = ui.painter();
p.rect_filled(rect, 4.0, th.node_fill);
let n = bars.len();
let bw = rect.width() / n as f32;
for (i, &h) in bars.iter().enumerate() {
let x0 = rect.left() + i as f32 * bw + 4.0;
let bar = Rect::from_min_max(
egui::pos2(x0, rect.bottom() - h * (rect.height() - 16.0)),
egui::pos2(x0 + bw - 8.0, rect.bottom() - 4.0),
);
p.rect_filled(bar, 3.0, th.accent.gamma_multiply(0.6 + 0.4 * h));
}
}
fn state_json(&self) -> serde_json::Value {
serde_json::json!({ "kind": "metrics" })
}
}
struct App {
deck: FacetDeck,
row_rects: std::rc::Rc<std::cell::RefCell<Vec<Rect>>>,
target_row: usize,
}
impl Default for App {
fn default() -> Self {
let row_rects = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let datasets = Datasets { selected: 1, row_rects: row_rects.clone() };
let mut deck = FacetDeck::new(vec![Box::new(datasets), Box::new(Metrics)]);
deck.set_palette(2); // start on nordic-aurora to show the override works
Self { deck, row_rects, target_row: 1 }
}
}
impl eframe::App for App {
fn ui(&mut self, root: &mut egui::Ui, _frame: &mut eframe::Frame) {
egui::Panel::top("fx_controls").show(root, |ui| {
// The deck's own palette switcher — re-themes every facet.
self.deck.palette_picker(ui);
ui.horizontal(|ui| {
// Opt into glow on the active facet.
let mut glow = self.deck.fx().glow;
if ui.checkbox(&mut glow, "Glow active facet").changed() {
self.deck.fx_mut().glow = glow;
}
ui.separator();
ui.label("Aim row:");
for (i, name) in ROWS.iter().enumerate() {
ui.selectable_value(&mut self.target_row, i, *name);
}
if ui.button("🐦 Send raven").clicked() {
// Bridge the selected row's live rect into the deck.
if let Some(&rect) = self.row_rects.borrow().get(self.target_row) {
self.deck.send_raven(rect);
}
}
if self.deck.has_raven()
&& ui.button(if self.deck.raven_perched() { "Dismiss (perched)" } else { "Dismiss" }).clicked()
{
self.deck.clear_raven();
}
});
});
egui::CentralPanel::default().show(root, |ui| {
self.deck.ui(ui);
});
}
}
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size(Vec2::new(880.0, 600.0)),
..Default::default()
};
eframe::run_native("facett · deck_fx_demo", options, Box::new(|_cc| Ok(Box::<App>::default())))
}