Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-form/examples/form_demo.rs

//! **form_demo** — a small admin record form, live. A surveillance "Case" with a
//! title, status enum, assignee, due date (YYYY-MM-DD masked), priority, an
//! "open" checkbox, and a notes block — with **per-field validation** (empty
//! title / out-of-range priority / malformed date go red), **dirty-tracking**
//! (the unsaved marker + Save/Reset enablement), a theme switcher, and the
//! facet-level **Ctrl-C / Ctrl-V** that copies / fills the whole model as JSON.
//!
//! Clipboard is routed exactly the way a [`facett_core::FacetDeck`] does it: drain
//! the frame's semantic clipboard events and dispatch to the facet, gated by its
//! `caps()`. Here we keep a concrete `FormView<Case>` handle so the host can also
//! pull `take_submitted()` after a Save.
//!
//! Run: `cargo run -p facett-form --example form_demo`

use eframe::egui;
use facett_core::{Facet, Theme, clipboard, set_theme};
use facett_form::{FormModel, FormUi, FormView, ValidationReport};
use serde::{Deserialize, Serialize};

/// The admin record. `garde` rules drive the per-field validation outlines.
#[derive(Clone, PartialEq, Serialize, Deserialize, garde::Validate)]
struct Case {
    #[garde(length(min = 1))]
    title: String,
    #[garde(skip)]
    status: String,
    #[garde(length(min = 1))]
    assignee: String,
    #[garde(custom(facett_form::valid_ymd))]
    due: String,
    #[garde(range(min = 0, max = 5))]
    priority: i64,
    #[garde(skip)]
    open: bool,
    #[garde(skip)]
    notes: String,
}

impl Default for Case {
    fn default() -> Self {
        Self {
            title: "Investigate vessel Nordkapp".into(),
            status: "open".into(),
            assignee: "hugin".into(),
            due: "2026-06-30".into(),
            priority: 2,
            open: true,
            notes: "Cross-ref AIS gaps with port calls.".into(),
        }
    }
}

impl FormModel for Case {
    fn fields(&mut self, f: &mut FormUi<'_>) {
        f.section("Case", |f| {
            f.text("title", "Title", &mut self.title);
            f.select("status", "Status", &mut self.status, &["open", "closed", "escalated"]);
            f.text("assignee", "Assignee", &mut self.assignee);
            f.date("due", "Due (YYYY-MM-DD)", &mut self.due);
            f.number_i64("priority", "Priority (0–5)", &mut self.priority, Some(0..=5));
            f.checkbox("open", "Open", &mut self.open);
        });
        f.section("Notes", |f| {
            f.multiline("notes", "Notes", &mut self.notes);
        });
    }
    fn validate(&self) -> ValidationReport {
        ValidationReport::from_garde(garde::Validate::validate(self))
    }
    fn to_clip(&self) -> String {
        serde_json::to_string_pretty(self).unwrap_or_default()
    }
    fn from_clip(&mut self, text: &str) -> bool {
        serde_json::from_str::<Case>(text).map(|m| *self = m).is_ok()
    }
}

struct Demo {
    theme_idx: usize,
    form: FormView<Case>,
    /// The last record the form submitted (host-side "persisted" copy).
    saved: Option<String>,
}

impl Default for Demo {
    fn default() -> Self {
        Self { theme_idx: 2, form: FormView::new("New Case", Case::default()), saved: None }
    }
}

impl eframe::App for Demo {
    fn ui(&mut self, root: &mut egui::Ui, _frame: &mut eframe::Frame) {
        let ctx = root.ctx().clone();
        let theme = Theme::ALL[self.theme_idx]();
        set_theme(&ctx, theme);

        egui::Panel::top("controls").show(root, |ui| {
            ui.horizontal_wrapped(|ui| {
                ui.label("Theme:");
                for (i, ctor) in Theme::ALL.iter().enumerate() {
                    ui.selectable_value(&mut self.theme_idx, i, ctor().name);
                }
            });
            ui.label("Edit a field to go dirty · Ctrl-C copies the record as JSON · Ctrl-V fills it");
        });

        // Deck-style clipboard routing, gated by the facet's caps.
        let caps = self.form.caps();
        for action in clipboard::poll(&ctx) {
            match action {
                clipboard::ClipAction::Copy if caps.copyable => {
                    if let Some(t) = self.form.copy() {
                        clipboard::put(&ctx, t);
                    }
                }
                clipboard::ClipAction::Cut if caps.cuttable => {
                    if let Some(t) = self.form.cut() {
                        clipboard::put(&ctx, t);
                    }
                }
                clipboard::ClipAction::Paste(s) if caps.pasteable => {
                    self.form.paste(&s);
                }
                _ => {}
            }
        }

        egui::CentralPanel::default().show(root, |ui| {
            self.form.ui(ui);
            if let Some(saved) = self.form.take_submitted() {
                self.saved = Some(saved.to_clip());
            }
            if let Some(s) = &self.saved {
                ui.separator();
                ui.label("Last saved record:");
                ui.code(s);
            }
        });
    }
}

fn main() -> eframe::Result {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default().with_inner_size(egui::vec2(560.0, 640.0)),
        ..Default::default()
    };
    eframe::run_native("facett · form_demo", options, Box::new(|_cc| Ok(Box::<Demo>::default())))
}