use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use eframe::egui;
use nord_format::fields::Field;
use crate::fields::Control;
use crate::{drawbar_widget, knob, led, strings, visibility};
#[derive(Default)]
pub struct Ctx {
read: RefCell<HashMap<String, Rc<Entry>>>,
}
struct Entry {
control: Control,
legal: Vec<String>,
}
impl Ctx {
fn entry(&self, field: &Field) -> Rc<Entry> {
if let Some(entry) = self.read.borrow().get(&field.path) {
return Rc::clone(entry);
}
let legal = (field.spec.legal)();
let entry = Rc::new(Entry {
control: Control::of(field, &legal),
legal,
});
self.read
.borrow_mut()
.insert(field.path.clone(), Rc::clone(&entry));
entry
}
pub fn control(&self, field: &Field) -> Control {
self.entry(field).control
}
}
pub type Sets = Vec<(String, String)>;
pub fn section(ui: &mut egui::Ui, title: &str, body: impl FnOnce(&mut egui::Ui)) {
egui::Frame::group(ui.style()).show(ui, |ui| {
ui.set_width(ui.available_width());
ui.label(egui::RichText::new(title).strong());
ui.separator();
body(ui);
});
ui.add_space(2.0);
}
pub fn strip(ui: &mut egui::Ui, body: impl FnOnce(&mut egui::Ui)) {
let row = egui::Layout::left_to_right(egui::Align::TOP).with_main_wrap(true);
ui.with_layout(row, |ui| {
ui.spacing_mut().item_spacing = egui::vec2(10.0, 10.0);
body(ui);
});
}
fn width(control: Control) -> f32 {
match control {
Control::Choice => 156.0,
Control::Stored => 140.0,
Control::Register => 220.0,
Control::Bar => 44.0,
_ => 78.0,
}
}
pub fn cell(ui: &mut egui::Ui, ctx: &Ctx, field: &Field, sets: &mut Sets) {
named_cell(ui, &field.path, width(ctx.control(field)), |ui| {
if let Some(value) = control(ui, ctx, field) {
sets.push((field.path.clone(), value));
}
});
}
pub fn named_cell(
ui: &mut egui::Ui,
path: &str,
width: f32,
body: impl FnOnce(&mut egui::Ui),
) -> egui::Response {
ui.allocate_ui(egui::vec2(width, 0.0), |ui| {
ui.vertical_centered(|ui| {
ui.spacing_mut().item_spacing.y = 3.0;
body(ui);
caption(ui, path);
});
})
.response
}
fn caption(ui: &mut egui::Ui, path: &str) {
let rough = !strings::known(path);
let mut text = egui::RichText::new(strings::label(path)).small();
if rough {
text = text.italics();
}
let response = ui.add(egui::Label::new(text.color(ui.visuals().weak_text_color())));
if rough {
response.on_hover_text(format!("{path} — this app has no name for it yet"));
}
}
pub fn control(ui: &mut egui::Ui, ctx: &Ctx, field: &Field) -> Option<String> {
match ctx.control(field) {
Control::Toggle => toggle(ui, field),
Control::Choice => choice(ui, ctx, field),
Control::Number { min, max } => number(ui, field, min, max),
Control::Bar => bar(ui, field),
Control::Register => register(ui, field, true),
Control::Stored => {
ui.label(
egui::RichText::new(&field.display)
.monospace()
.small()
.weak(),
)
.on_hover_text("stored as-is; see Advanced");
None
}
}
}
fn toggle(ui: &mut egui::Ui, field: &Field) -> Option<String> {
let on = field.value == "true";
led::ui(ui, on, "").map(|want| want.to_string())
}
fn choice(ui: &mut egui::Ui, ctx: &Ctx, field: &Field) -> Option<String> {
let entry = ctx.entry(field);
let offered = visibility::choices(&field.path, &entry.legal, &field.value);
let mut picked = None;
egui::ComboBox::from_id_salt(&field.path)
.selected_text(
egui::RichText::new(strings::value_label(&field.path, &field.value))
.text_style(egui::TextStyle::Small),
)
.width(ui.available_width().min(width(Control::Choice) - 12.0))
.show_ui(ui, |ui| {
for value in &offered {
let label = strings::value_label(&field.path, value);
if ui.selectable_label(*value == field.value, label).clicked() {
picked = Some(value.clone());
}
}
});
picked.filter(|value| *value != field.value)
}
fn number(ui: &mut egui::Ui, field: &Field, min: i64, max: i64) -> Option<String> {
let value: i64 = field.value.trim_start_matches('+').parse().ok()?;
knob::ui(ui, &field.path, value, min, max).map(|moved| moved.to_string())
}
fn bar(ui: &mut egui::Ui, field: &Field) -> Option<String> {
let position = field.value.trim().parse().ok()?;
let moved = drawbar_widget::ui_one(ui, drawbar_widget::rank(&field.path), position, true);
moved.map(|moved| moved.to_string())
}
pub fn register(ui: &mut egui::Ui, field: &Field, live: bool) -> Option<String> {
let bits = drawbar_widget::parse(&field.value)?;
bars(
ui,
drawbar_widget::bars(bits),
live,
&drawbar_widget::ALL_RANKS,
)
.map(|moved| drawbar_widget::spell(drawbar_widget::bits(moved)))
}
pub fn bars(
ui: &mut egui::Ui,
positions: [u8; drawbar_widget::BARS],
live: bool,
ranks: &[usize],
) -> Option<[u8; drawbar_widget::BARS]> {
let mut moved = None;
let count = ranks.len().min(drawbar_widget::BARS);
ui.vertical(|ui| {
moved = drawbar_widget::ui_ranks(ui, positions, live, ranks);
let shown = moved.unwrap_or(positions);
ui.label(
egui::RichText::new(drawbar_widget::digits(&shown[..count]))
.monospace()
.small()
.weak(),
);
});
moved
}
pub fn switch(ui: &mut egui::Ui, field: Option<&Field>, word: &str, sets: &mut Sets) {
let Some(field) = field else {
return;
};
let on = field.value == "true";
if let Some(want) = led::ui(ui, on, word) {
sets.push((field.path.clone(), want.to_string()));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_field_is_read_as_it_is_drawn_and_only_once() {
let bytes = crate::fields::blank::stage4_program();
let (fields, _) = crate::fields::apply(&bytes, &[]).unwrap();
let ctx = Ctx::default();
assert!(fields.len() > 800);
assert_eq!(ctx.read.borrow().len(), 0, "nothing drawn, nothing asked");
ctx.control(&fields[0]);
ctx.control(&fields[0]);
assert_eq!(ctx.read.borrow().len(), 1);
}
}