mirror of
https://github.com/storytold/craft-anything.git
synced 2026-11-02 18:06:58 +00:00
Check Simple-mode text sizes with a test (R2.9) (#354)
A test keeps Simple-mode text at 12 pt or more outside the workbench; eight smaller monospace labels now use the caption size. R2.9.
This commit is contained in:
@@ -57,7 +57,7 @@ fn details(ui: &mut Ui, e: &Evidence, t: &Tokens) {
|
||||
None => l.value.clone(),
|
||||
};
|
||||
egui::Frame::new().fill(t.raised).corner_radius(t.r_sm()).inner_margin(Margin::symmetric(8, 3)).show(ui, |ui| {
|
||||
ui.label(RichText::new(text).font(theme::mono(11.5)).color(t.text)).on_hover_text(&l.kind);
|
||||
ui.label(RichText::new(text).font(theme::mono(theme::CAPTION)).color(t.text)).on_hover_text(&l.kind);
|
||||
});
|
||||
}
|
||||
if e.locations.len() > 40 {
|
||||
@@ -89,7 +89,7 @@ fn details(ui: &mut Ui, e: &Evidence, t: &Tokens) {
|
||||
widgets::pill(ui, &format!("{} · {}", e.environment.engine, e.environment.host), t.text_faint, t.raised);
|
||||
let short = e.id.get(..18).unwrap_or(&e.id).to_string();
|
||||
if ui
|
||||
.add(egui::Button::new(RichText::new(format!("{short}…")).font(theme::mono(11.0)).color(t.text_faint)).frame(false))
|
||||
.add(egui::Button::new(RichText::new(format!("{short}…")).font(theme::mono(theme::CAPTION)).color(t.text_faint)).frame(false))
|
||||
.on_hover_text(tr("Copy the evidence id"))
|
||||
.clicked()
|
||||
{
|
||||
|
||||
@@ -248,7 +248,7 @@ fn form(app: &mut CraftApp, ctx: &egui::Context, tool: &str) {
|
||||
.anchor(egui::Align2::CENTER_TOP, Vec2::new(0.0, 80.0))
|
||||
.show(ctx, |ui| {
|
||||
ui.label(RichText::new(spec.summary).color(t.text_dim));
|
||||
ui.label(RichText::new(spec.id).font(theme::mono(11.5)).color(t.text_faint));
|
||||
ui.label(RichText::new(spec.id).font(theme::mono(theme::CAPTION)).color(t.text_faint));
|
||||
if spec.effects.needs_consent() {
|
||||
ui.horizontal(|ui| {
|
||||
icons::show(ui, "shield", 14.0, t.warning);
|
||||
|
||||
@@ -1063,9 +1063,10 @@ fn run_details(app: &mut CraftApp, ui: &mut Ui, status_row: bool) {
|
||||
};
|
||||
ui.horizontal(|ui| {
|
||||
ui.set_height(LINE);
|
||||
ui.label(RichText::new(crate::usage::duration_words(line.at)).font(theme::mono(11.5)).color(t.text_faint));
|
||||
ui.label(RichText::new(line.kind.tag()).font(theme::mono(11.5)).color(colour));
|
||||
ui.add(egui::Label::new(RichText::new(&line.text).font(theme::mono(11.5)).color(words)).truncate()).on_hover_text(&line.text);
|
||||
ui.label(RichText::new(crate::usage::duration_words(line.at)).font(theme::mono(theme::CAPTION)).color(t.text_faint));
|
||||
ui.label(RichText::new(line.kind.tag()).font(theme::mono(theme::CAPTION)).color(colour));
|
||||
ui.add(egui::Label::new(RichText::new(&line.text).font(theme::mono(theme::CAPTION)).color(words)).truncate())
|
||||
.on_hover_text(&line.text);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -672,7 +672,7 @@ fn plugins(app: &mut CraftApp, ui: &mut Ui) {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(300.0);
|
||||
ui.label(RichText::new(p.display_name()).font(theme::medium(theme::SMALL)));
|
||||
ui.label(RichText::new(p.place()).font(theme::mono(11.5)).color(t.text_faint));
|
||||
ui.label(RichText::new(p.place()).font(theme::mono(theme::CAPTION)).color(t.text_faint));
|
||||
if let Some(problem) = p.problem() {
|
||||
ui.label(RichText::new(problem).color(t.warning).size(theme::CAPTION));
|
||||
}
|
||||
@@ -699,7 +699,7 @@ fn plugins(app: &mut CraftApp, ui: &mut Ui) {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(300.0);
|
||||
ui.label(RichText::new(p.display_name()).font(theme::medium(theme::SMALL)));
|
||||
ui.label(RichText::new(p.path.display().to_string()).font(theme::mono(11.5)).color(t.text_faint));
|
||||
ui.label(RichText::new(p.path.display().to_string()).font(theme::mono(theme::CAPTION)).color(t.text_faint));
|
||||
if let Some(problem) = p.problem() {
|
||||
ui.label(RichText::new(problem).color(t.warning).size(theme::CAPTION));
|
||||
}
|
||||
|
||||
@@ -436,6 +436,195 @@ pub fn apply(ctx: &egui::Context, kind: ThemeKind, roomy: bool) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The type scale is the only place a size is written down. Screens outside the workbench
|
||||
/// use its names (`CAPTION`, `BODY`, …) or a literal of 12 pt or more; the workbench's dense
|
||||
/// lists may use `TINY`. This walks the source so a new screen can't quietly go smaller.
|
||||
#[test]
|
||||
fn no_literal_text_size_under_12_outside_the_workbench() {
|
||||
fn rust_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
for entry in std::fs::read_dir(dir).unwrap().flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
rust_files(&path, out);
|
||||
} else if path.extension().is_some_and(|x| x == "rs") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Markers whose next argument is a text size. `.size` on a `RichText` counts; the
|
||||
// `.size` of a spinner or a galley does not.
|
||||
let texts = ["label_text(", "heading(", "medium(", "semibold(", "mono(", "FontId::proportional(", "FontId::monospace("];
|
||||
|
||||
let mut files = Vec::new();
|
||||
rust_files(&std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src"), &mut files);
|
||||
assert!(files.len() > 10, "the source scan found only {} files", files.len());
|
||||
|
||||
let mut checked = 0;
|
||||
for path in files {
|
||||
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or_default();
|
||||
// `theme.rs` is the scale itself, and `workbench.rs` is the dense workbench.
|
||||
if name == "theme.rs" || name == "workbench.rs" {
|
||||
continue;
|
||||
}
|
||||
let text = std::fs::read_to_string(&path).unwrap();
|
||||
for (n, line) in text.lines().enumerate() {
|
||||
let code = line.trim_start();
|
||||
if code.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
let mut markers: Vec<&str> = texts.iter().copied().filter(|m| code.contains(m)).collect();
|
||||
if code.contains("RichText") && code.contains(".size(") && !code.contains("Spinner") {
|
||||
markers.push(".size(");
|
||||
}
|
||||
for marker in markers {
|
||||
let mut from = 0;
|
||||
while let Some(rel) = code[from..].find(marker) {
|
||||
let at = from + rel;
|
||||
let open = at + marker.len() - 1;
|
||||
if let Some(call) = call_text(code, open) {
|
||||
let arg = if marker == "label_text(" || marker == "heading(" { top_level_parts(call).last().copied().unwrap_or("") } else { call };
|
||||
for size in size_literals(arg) {
|
||||
assert!(
|
||||
size >= 12.0,
|
||||
"{}:{}: a literal text size of {size} pt is below 12; use the theme scale: {}",
|
||||
path.display(),
|
||||
n + 1,
|
||||
code
|
||||
);
|
||||
}
|
||||
checked += 1;
|
||||
}
|
||||
from = at + marker.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(checked > 40, "the source scan checked only {checked} text sizes");
|
||||
}
|
||||
|
||||
/// The text between `open`'s parenthesis and its match, or `None` when it isn't closed on
|
||||
/// this line.
|
||||
fn call_text(line: &str, open: usize) -> Option<&str> {
|
||||
let mut depth = 0i32;
|
||||
let mut in_string = false;
|
||||
let mut escaped = false;
|
||||
for (i, c) in line.get(open..)?.char_indices() {
|
||||
if in_string {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
} else if c == '\\' {
|
||||
escaped = true;
|
||||
} else if c == '"' {
|
||||
in_string = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'"' => in_string = true,
|
||||
'(' => depth += 1,
|
||||
')' => {
|
||||
depth -= 1;
|
||||
if depth == 0 {
|
||||
return line.get(open + 1..open + i);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Split at commas that aren't inside brackets, braces or a string.
|
||||
fn top_level_parts(s: &str) -> Vec<&str> {
|
||||
let mut out = Vec::new();
|
||||
let mut start = 0;
|
||||
let mut depth = 0i32;
|
||||
let mut in_string = false;
|
||||
let mut escaped = false;
|
||||
for (i, c) in s.char_indices() {
|
||||
if in_string {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
} else if c == '\\' {
|
||||
escaped = true;
|
||||
} else if c == '"' {
|
||||
in_string = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'"' => in_string = true,
|
||||
'(' | '[' | '{' => depth += 1,
|
||||
')' | ']' | '}' => depth -= 1,
|
||||
',' if depth == 0 => {
|
||||
out.push(&s[start..i]);
|
||||
start = i + 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.push(&s[start..]);
|
||||
out
|
||||
}
|
||||
|
||||
/// The literals a size argument is written as: one bare number, or every number in an
|
||||
/// `if`/`else`. An expression such as `body.size + 2.0` isn't a size of its own and is left
|
||||
/// to the reviewer.
|
||||
fn size_literals(arg: &str) -> Vec<f64> {
|
||||
let arg = arg.trim();
|
||||
if arg.starts_with("if ") {
|
||||
return floats_outside_strings(arg);
|
||||
}
|
||||
let bare = arg.strip_suffix("f32").unwrap_or(arg).trim();
|
||||
bare.parse::<f64>().ok().into_iter().collect()
|
||||
}
|
||||
|
||||
/// Float literals on a line, ignoring digits inside string literals (a version or a count).
|
||||
fn floats_outside_strings(line: &str) -> Vec<f64> {
|
||||
let mut out = Vec::new();
|
||||
let mut in_string = false;
|
||||
let mut escaped = false;
|
||||
let mut chars = line.chars().peekable();
|
||||
while let Some(c) = chars.next() {
|
||||
if in_string {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
} else if c == '\\' {
|
||||
escaped = true;
|
||||
} else if c == '"' {
|
||||
in_string = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if c == '"' {
|
||||
in_string = true;
|
||||
continue;
|
||||
}
|
||||
if c.is_ascii_digit() {
|
||||
let mut number = String::from(c);
|
||||
while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
|
||||
if let Some(d) = chars.next() {
|
||||
number.push(d);
|
||||
}
|
||||
}
|
||||
if chars.peek() == Some(&'.') {
|
||||
number.push('.');
|
||||
chars.next();
|
||||
while chars.peek().is_some_and(|c| c.is_ascii_digit()) {
|
||||
if let Some(d) = chars.next() {
|
||||
number.push(d);
|
||||
}
|
||||
}
|
||||
if let Ok(v) = number.parse::<f64>() {
|
||||
out.push(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// WCAG relative luminance of an opaque colour.
|
||||
fn luminance(c: Color32) -> f64 {
|
||||
let ch = |v: u8| {
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn show(app: &mut CraftApp, ui: &mut Ui) {
|
||||
}
|
||||
if let Some((name, kind)) = target {
|
||||
icons::show(ui, crate::kind_icon(kind), 14.0, t.icon);
|
||||
ui.add(egui::Label::new(RichText::new(name).font(theme::mono(11.5)).color(t.text)).truncate()).on_hover_text(tr(kind.plain()));
|
||||
ui.add(egui::Label::new(RichText::new(name).font(theme::mono(theme::CAPTION)).color(t.text)).truncate()).on_hover_text(tr(kind.plain()));
|
||||
}
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.spacing_mut().item_spacing.x = 0.0;
|
||||
|
||||
+1
-1
@@ -843,7 +843,7 @@ Gaps found while using the app for real projects.
|
||||
- [x] A consent card's id is a short number, with the call it stands for kept in memory, not the
|
||||
call serialised into the id
|
||||
- [x] The workbench's list isn't copied every frame (it can hold thousands of rows)
|
||||
- [ ] Simple-mode text sizes checked by a test (no literal sizes under 12 pt outside the workbench)
|
||||
- [x] Simple-mode text sizes checked by a test (no literal sizes under 12 pt outside the workbench)
|
||||
- [x] Typed text no longer blinks in the prompt box; Open folder opens the project's folder
|
||||
- [x] Scroll bars show at rest (6 px, the text colour at 3:1 or more on every surface), not
|
||||
only on hover
|
||||
|
||||
Reference in New Issue
Block a user