use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use eframe::egui;
use nord_format::fields::Field;
use crate::strings;
#[derive(Default)]
pub struct Ctx {
read: RefCell<HashMap<String, Rc<Vec<String>>>>,
}
impl Ctx {
pub fn legal(&self, field: &Field) -> Rc<Vec<String>> {
if let Some(legal) = self.read.borrow().get(&field.path) {
return Rc::clone(legal);
}
let legal = Rc::new((field.spec.legal)());
self.read
.borrow_mut()
.insert(field.path.clone(), Rc::clone(&legal));
legal
}
}
pub type Sets = Vec<(String, String)>;
pub fn fits(text: &mut String, limit: usize) {
let end = text
.char_indices()
.map(|(at, c)| at + c.len_utf8())
.take_while(|end| *end <= limit)
.last()
.unwrap_or(0);
text.truncate(end);
}
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 heading(ui: &mut egui::Ui, title: &str, note: &str, right: Option<(&str, egui::Color32)>) {
const ROW: f32 = 18.0;
const PAD: f32 = 12.0;
const GAP: f32 = 8.0;
const TITLE: f32 = 12.0;
const NOTE: f32 = 10.5;
const READING: f32 = 10.0;
let above = match ui.min_rect().height() > 0.0 {
true => 12.0,
false => 8.0,
};
ui.add_space(above);
let (rect, _) =
ui.allocate_exact_size(egui::vec2(ui.available_width(), ROW), egui::Sense::hover());
let mut row = ui.new_child(
egui::UiBuilder::new()
.max_rect(rect.shrink2(egui::vec2(PAD, 0.0)))
.layout(egui::Layout::left_to_right(egui::Align::Center)),
);
row.spacing_mut().item_spacing.x = GAP;
let ink = row.visuals().text_color();
row.label(
egui::RichText::new(title)
.font(egui::FontId::new(TITLE, crate::app::bold()))
.color(ink),
);
let caption = crate::app::caption(row.visuals());
row.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if let Some((reading, tint)) = right {
ui.label(
egui::RichText::new(reading)
.font(egui::FontId::monospace(READING))
.color(tint),
);
}
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
ui.add(
egui::Label::new(
egui::RichText::new(note)
.font(egui::FontId::proportional(NOTE))
.color(caption),
)
.truncate(),
);
});
});
ui.add_space(4.0);
}
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);
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"));
}
});
})
.response
}
#[cfg(test)]
mod tests {
use super::*;
use crate::workspace::Fresh;
#[test]
fn a_section_heading_says_its_parts_and_paints_no_bar() {
fn walk(shape: &egui::Shape, into: &mut (Vec<String>, Vec<egui::Color32>)) {
match shape {
egui::Shape::Text(text) => into.0.push(text.galley.text().to_string()),
egui::Shape::Rect(drawn) => into.1.push(drawn.fill),
egui::Shape::Vec(shapes) => shapes.iter().for_each(|shape| walk(shape, into)),
_ => {}
}
}
let ctx = egui::Context::default();
ctx.set_fonts(crate::app::fonts());
let warn = crate::app::warn(&ctx.style().visuals);
let output = ctx.run(egui::RawInput::default(), |ctx| {
ctx.style_mut(crate::app::metrics);
egui::CentralPanel::default().show(ctx, |ui| {
heading(ui, "Key map", "drag a top", Some(("1 silent range", warn)));
});
});
let mut painted = (Vec::new(), Vec::new());
for clipped in &output.shapes {
walk(&clipped.shape, &mut painted);
}
assert_eq!(painted.0, ["Key map", "1 silent range", "drag a top"]);
assert!(
painted
.1
.iter()
.all(|fill| *fill == ctx.style().visuals.panel_fill),
"the heading painted a ground of its own: {:?}",
painted.1,
);
}
#[test]
fn a_name_is_cut_to_the_bytes_the_field_holds() {
let cut = |text: &str, limit: usize| {
let mut held = text.to_string();
fits(&mut held, limit);
held
};
assert_eq!(cut("Marimba", 16), "Marimba");
assert_eq!(cut("Marimba", 4), "Mari");
assert_eq!(cut("Café", 5), "Café", "four letters in five bytes");
assert_eq!(cut("Café", 4), "Caf", "half of é is not a letter");
assert_eq!(cut("é", 1), "");
assert_eq!(cut("", 8), "");
}
#[test]
fn a_field_is_read_as_it_is_drawn_and_only_once() {
let bytes = Fresh::Stage4Program.bytes().unwrap();
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.legal(&fields[0]);
ctx.legal(&fields[0]);
assert_eq!(ctx.read.borrow().len(), 1);
}
}