mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 02:25:55 +00:00
* Lists many PRs add to: no written lengths, one file per theme (#7747) * xtask lists: the fixed-table allowlist in order, each with its own reason * Lists: the wide-character blocks and the drawing scale presets as slices too (#7747) * Lists: the drawing units test reads UNITS as a slice (#7747)
This commit is contained in:
@@ -278,6 +278,11 @@ Some files gain an item in many pull requests at once. They are kept so that two
|
||||
each add an item only meet when their items are neighbours, which spares everyone a conflict
|
||||
and a fresh CI run:
|
||||
|
||||
- **Growing Rust lists**: use slices (`const ITEMS: &[Item] = &[...];`) without a written
|
||||
length. `cargo xtask lists` rejects non-test const/static arrays with more than ten lines
|
||||
of entries, except fixed standard or algorithm tables listed with reasons in `xtask/src/lists.rs`.
|
||||
Built-in themes need only a JSON file in `assets/styles/ui/themes/`, with a unique nonnegative
|
||||
integer `order` (the picker order) and its own `provenance` line; keep existing order values.
|
||||
- **Message catalogs** (`assets/locales/*.json`): one message per line, sorted by id. Add your
|
||||
message anywhere and run `cargo xtask lists --fix`; `cargo xtask lists` (in `cargo xtask ci`
|
||||
and the Lists workflow) fails on an unsorted catalog or an id that appears twice.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Built-in model appearances: names, sRGB colours and metalness.
|
||||
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
|
||||
pub const LIBRARY: &[(&str, [u8; 3], f64)] = &[
|
||||
("Aluminum - Satin", [204, 208, 214], 1.0),
|
||||
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
|
||||
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Original illustrative opaque finishes; not measured optical or engineering data.
|
||||
//! (name, sRGB color, metal fraction, perceptual roughness). IOR is an illustrative 1.5.
|
||||
//! Separate names preserve the scalar library's appearance and saved assignments.
|
||||
pub const OPAQUE_FINISHES: [(&str, [u8; 3], f64, f64); 6] = [
|
||||
pub const OPAQUE_FINISHES: &[(&str, [u8; 3], f64, f64)] = &[
|
||||
("Steel - Satin PBR", [158, 164, 172], 1.0, 0.4),
|
||||
("Aluminum - Satin PBR", [204, 208, 214], 1.0, 0.3),
|
||||
("Copper - Clean PBR", [204, 122, 80], 1.0, 0.25),
|
||||
|
||||
@@ -16,7 +16,7 @@ const fn glass(transmission_linear_rgb: [f64; 3], ior: f64, reference_path_mm: f
|
||||
GlassRecipe { transmission_linear_rgb, dielectric_ior: ior, reference_path_mm, boundary }
|
||||
}
|
||||
|
||||
pub const STARTER_PACK: [VisualPreset; 40] = [
|
||||
pub const STARTER_PACK: &[VisualPreset] = &[
|
||||
VisualPreset::opaque(
|
||||
"starter.wood.oak-honey",
|
||||
"Wood - Honey Oak Uniform Preview",
|
||||
|
||||
@@ -7,7 +7,7 @@ pub const DEFAULT_MATERIAL: &str = "Default";
|
||||
pub const DEFAULT_DENSITY: f64 = 1.29;
|
||||
|
||||
/// Physical materials: name and density (g/cm³).
|
||||
pub const MATERIALS: [(&str, f64); 14] = [
|
||||
pub const MATERIALS: &[(&str, f64)] = &[
|
||||
(DEFAULT_MATERIAL, DEFAULT_DENSITY),
|
||||
("Steel", 7.85),
|
||||
("Stainless Steel", 8.0),
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 76,
|
||||
"provenance": "Original black-and-white palette",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#ffffff",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 75,
|
||||
"provenance": "Original black-and-white palette",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"accent": "#000000",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 220,
|
||||
"provenance": "Maps the Okibi design system",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"accent": "#c63a00",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 110,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#ffbc48",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 120,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#66ddff",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 200,
|
||||
"provenance": "Original SolidCraft mapping of Catppuccin (MIT palette)",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#cba6f7",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 250,
|
||||
"provenance": "Original mapping of the IBM Color Graphics Adapter palette",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#55ffff",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 240,
|
||||
"provenance": "Original warm paper and charcoal palette",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#e8845e",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 230,
|
||||
"provenance": "Original warm paper and charcoal palette",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"accent": "#bf5634",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 90,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"accent": "#007aff",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 180,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#4aa3d8",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 100,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#4dff79",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 20,
|
||||
"provenance": "Original SolidCraft mapping of Gruvbox (MIT palette)",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Dark Hard",
|
||||
"base": "dark",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 30,
|
||||
"provenance": "Original SolidCraft mapping of Gruvbox (MIT palette)",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Dark Soft",
|
||||
"base": "dark",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 10,
|
||||
"provenance": "Original SolidCraft mapping of Gruvbox (MIT palette)",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Dark",
|
||||
"base": "dark",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 50,
|
||||
"provenance": "Original SolidCraft mapping of Gruvbox (MIT palette)",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Light Hard",
|
||||
"base": "light",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 60,
|
||||
"provenance": "Original SolidCraft mapping of Gruvbox (MIT palette)",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Light Soft",
|
||||
"base": "light",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 40,
|
||||
"provenance": "Original SolidCraft mapping of Gruvbox (MIT palette)",
|
||||
"format": "solidcraft-theme/1",
|
||||
"name": "Gruvbox Light",
|
||||
"base": "light",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 170,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#00bfff",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 160,
|
||||
"provenance": "Original SolidCraft mapping of Nord (MIT palette)",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#88c0d0",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 190,
|
||||
"provenance": "Original SolidCraft mapping of Dracula (MIT palette)",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#bd93f9",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 210,
|
||||
"provenance": "Maps the Okibi design system",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#ff7c50",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 130,
|
||||
"provenance": "Original SolidCraft mapping of Atom One Dark (MIT palette)",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#61afef",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 80,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"accent": "#555599",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 150,
|
||||
"provenance": "Original SolidCraft mapping of Solarized (MIT palette)",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"accent": "#268bd2",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 70,
|
||||
"provenance": "Original SolidCraft theme",
|
||||
"base": "light",
|
||||
"colors": {
|
||||
"accent": "#000080",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"order": 140,
|
||||
"provenance": "Original SolidCraft mapping of Tokyo Night (MIT palette)",
|
||||
"base": "dark",
|
||||
"colors": {
|
||||
"accent": "#7aa2f7",
|
||||
|
||||
@@ -747,7 +747,7 @@ mod tests {
|
||||
fn stable_ids_categories_and_original_licences_cover_the_requested_pack() {
|
||||
let mut ids = BTreeSet::new();
|
||||
let mut names = BTreeSet::new();
|
||||
for preset in &STARTER_PACK {
|
||||
for preset in STARTER_PACK {
|
||||
assert!(preset.id.starts_with("starter.") && preset.id.len() <= 128);
|
||||
assert!(preset.id.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'-'));
|
||||
assert!(ids.insert(preset.id));
|
||||
@@ -767,14 +767,14 @@ mod tests {
|
||||
(Category::Solar, 2),
|
||||
(Category::Regolith, 4),
|
||||
] {
|
||||
assert_eq!(STARTER_PACK.iter().filter(|p| p.category == category).count(), count);
|
||||
assert!(STARTER_PACK.iter().filter(|p| p.category == category).count() >= count);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opaque_construction_and_owned_look_roundtrip_preserve_current_response() {
|
||||
let mut admitted = 0;
|
||||
for preset in &STARTER_PACK {
|
||||
for preset in STARTER_PACK {
|
||||
if let Recipe::Opaque(recipe) = preset.recipe {
|
||||
let look = preset.opaque_look().unwrap();
|
||||
assert_eq!(look.opacity, 1.0);
|
||||
@@ -789,7 +789,7 @@ mod tests {
|
||||
admitted += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(admitted, 34);
|
||||
assert!(admitted >= 34);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -821,8 +821,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn legacy_library_and_six_existing_finish_names_are_not_overridden_or_duplicated() {
|
||||
assert_eq!(crate::appearance::LIBRARY.len(), 20);
|
||||
for (name, color, opacity) in crate::appearance::LIBRARY {
|
||||
assert!(!crate::appearance::LIBRARY.is_empty());
|
||||
let mut names = BTreeSet::new();
|
||||
for &(name, color, opacity) in crate::appearance::LIBRARY {
|
||||
assert!(!name.is_empty() && names.insert(name.to_ascii_lowercase()));
|
||||
let look = crate::appearance::library(name).unwrap();
|
||||
assert_eq!(look.color, color);
|
||||
assert_eq!(look.opacity, opacity);
|
||||
@@ -844,7 +846,7 @@ mod tests {
|
||||
#[test]
|
||||
fn uniform_previews_and_planned_resource_scale_are_explicit_and_not_bindings() {
|
||||
let mut recipes = 0;
|
||||
for preset in &STARTER_PACK {
|
||||
for preset in STARTER_PACK {
|
||||
if let Some(recipe) = preset.future_texture_recipe {
|
||||
assert!(preset.name.contains("Uniform Preview"));
|
||||
assert!(preset.preview_note.contains("Uniform opaque preview only"));
|
||||
@@ -855,7 +857,7 @@ mod tests {
|
||||
recipes += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(recipes, 14);
|
||||
assert!(recipes >= 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ fn a_fixed_unit_is_a_length_unit_of_the_expression_table() {
|
||||
read(&v).map(|d| d.units)
|
||||
};
|
||||
for (unit, kind, _) in expr::UNITS {
|
||||
assert_eq!(units(json!({"fixed": {"unit": unit}})).is_ok(), kind == expr::Kind::Length, "{unit}");
|
||||
assert_eq!(units(json!({"fixed": {"unit": unit}})).is_ok(), *kind == expr::Kind::Length, "{unit}");
|
||||
}
|
||||
assert_eq!(units(json!({"fixed": {"unit": "in"}})).unwrap(), Units::Fixed { unit: "in".into() });
|
||||
assert_eq!(units(json!("follow_model")).unwrap(), Units::FollowModel);
|
||||
|
||||
@@ -127,7 +127,7 @@ impl Kind {
|
||||
}
|
||||
|
||||
/// Units a parameter or an expression may use: name, kind and size (mm or radians).
|
||||
pub const UNITS: [(&str, Kind, f64); 14] = [
|
||||
pub const UNITS: &[(&str, Kind, f64)] = &[
|
||||
("mm", Kind::Length, 1.0),
|
||||
("cm", Kind::Length, 10.0),
|
||||
("m", Kind::Length, 1000.0),
|
||||
@@ -145,7 +145,7 @@ pub const UNITS: [(&str, Kind, f64); 14] = [
|
||||
];
|
||||
|
||||
/// Functions an expression may call (name, signature) — for autocomplete and help.
|
||||
pub const FUNCTIONS: [(&str, &str); 23] = [
|
||||
pub const FUNCTIONS: &[(&str, &str)] = &[
|
||||
("sin", "sin(angle)"),
|
||||
("cos", "cos(angle)"),
|
||||
("tan", "tan(angle)"),
|
||||
@@ -172,7 +172,7 @@ pub const FUNCTIONS: [(&str, &str); 23] = [
|
||||
];
|
||||
|
||||
/// Constants an expression may use.
|
||||
pub const CONSTANTS: [&str; 2] = ["pi", "PI"];
|
||||
pub const CONSTANTS: &[&str] = &["pi", "PI"];
|
||||
|
||||
/// Kind and size (mm or radians per unit) of a parameter's unit; empty means unit-less.
|
||||
pub fn unit_info(u: &str) -> Option<(Kind, f64)> {
|
||||
|
||||
@@ -717,7 +717,7 @@ fn appearance_library(_s: &mut Session, _p: &Value) -> Result<Value> {
|
||||
value["category"] = json!(name.split(" - ").next().unwrap_or(name));
|
||||
Some(value)
|
||||
}));
|
||||
for preset in &solidcraft_doc::appearance_pack::STARTER_PACK {
|
||||
for preset in solidcraft_doc::appearance_pack::STARTER_PACK {
|
||||
let c = preset.preview_color();
|
||||
let pbr = preset.opaque_look().ok().and_then(|look| look.pbr);
|
||||
list.push(json!({"id":preset.id,"name":preset.name,"category":preset.category,"revision":preset.revision,
|
||||
|
||||
@@ -308,7 +308,7 @@ mod tests {
|
||||
fn material_inspection_preserves_legacy_entries_and_reports_unknowns_and_density_separately() {
|
||||
let mut s = fixture();
|
||||
let built_in = s.execute("material.list", &json!({})).unwrap();
|
||||
for (name, density) in solidcraft_doc::MATERIALS {
|
||||
for &(name, density) in solidcraft_doc::MATERIALS {
|
||||
let row = built_in["materials"].as_array().unwrap().iter().find(|m| m["name"] == name).unwrap();
|
||||
assert_eq!(*row, json!({"name":name,"density_g_cm3":density}));
|
||||
let inspected = s.execute("material.inspect", &json!({"material":name.to_lowercase()})).unwrap();
|
||||
|
||||
@@ -31,7 +31,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
];
|
||||
|
||||
/// A family: (id, name, standard).
|
||||
const FAMILIES: [(&str, &str, &str); 7] = [
|
||||
const FAMILIES: &[(&str, &str, &str)] = &[
|
||||
("socket_head_cap_screw", "Socket head cap screw", "ISO 4762"),
|
||||
("hex_bolt", "Hex head bolt / screw", "ISO 4014 / ISO 4017"),
|
||||
("hex_nut", "Hex nut", "ISO 4032"),
|
||||
|
||||
@@ -215,7 +215,7 @@ fn named_opaque_finishes_reach_the_scene_and_survive_undo_and_reopen_without_cha
|
||||
("Wood - Oak", [190, 146, 92], 1.0),
|
||||
("Paint - Gloss White", [246, 246, 244], 1.0),
|
||||
];
|
||||
assert_eq!(solidcraft_doc::appearance::LIBRARY, legacy);
|
||||
assert!(solidcraft_doc::appearance::LIBRARY.len() >= legacy.len());
|
||||
for (name, color, opacity) in legacy {
|
||||
let matching: Vec<_> = entries.iter().filter(|entry| entry["name"] == name).collect();
|
||||
assert_eq!(matching.len(), 1, "legacy row must occur exactly once: {name}");
|
||||
@@ -224,11 +224,12 @@ fn named_opaque_finishes_reach_the_scene_and_survive_undo_and_reopen_without_cha
|
||||
assert_eq!(row["opacity"], opacity);
|
||||
assert!(row.get("pbr").is_none() && row.get("id").is_none(), "legacy scalar meaning changed: {name}");
|
||||
}
|
||||
assert_eq!(
|
||||
solidcraft_doc::appearance::OPAQUE_FINISHES.map(|(name, ..)| name),
|
||||
for name in
|
||||
["Steel - Satin PBR", "Aluminum - Satin PBR", "Copper - Clean PBR", "ABS - Molded PBR", "PLA - Smooth PBR", "Wood - Oak Uniform PBR Preview"]
|
||||
);
|
||||
for (name, ..) in solidcraft_doc::appearance::OPAQUE_FINISHES {
|
||||
{
|
||||
assert!(solidcraft_doc::appearance::OPAQUE_FINISHES.iter().any(|(entry, ..)| *entry == name));
|
||||
}
|
||||
for &(name, ..) in solidcraft_doc::appearance::OPAQUE_FINISHES {
|
||||
let expected = solidcraft_doc::appearance::library(name).unwrap();
|
||||
let matching: Vec<_> = entries.iter().filter(|entry| entry["name"] == name).collect();
|
||||
assert_eq!(matching.len(), 1, "finish row must occur exactly once: {name}");
|
||||
@@ -435,7 +436,7 @@ fn pack_listing_keeps_legacy_rows_and_labels_unavailable_recipes_without_mutatio
|
||||
("Wood - Oak", [190, 146, 92], 1.0),
|
||||
("Paint - Gloss White", [246, 246, 244], 1.0),
|
||||
];
|
||||
assert_eq!(solidcraft_doc::appearance::LIBRARY, legacy);
|
||||
assert!(solidcraft_doc::appearance::LIBRARY.len() >= legacy.len());
|
||||
for (name, color, opacity) in legacy {
|
||||
let matching: Vec<_> = rows.iter().filter(|row| row["name"] == name).collect();
|
||||
assert_eq!(matching.len(), 1, "legacy appearance must occur exactly once: {name}");
|
||||
@@ -452,7 +453,7 @@ fn pack_listing_keeps_legacy_rows_and_labels_unavailable_recipes_without_mutatio
|
||||
("PLA - Smooth PBR", [110, 160, 222], 0.0, 0.35),
|
||||
("Wood - Oak Uniform PBR Preview", [190, 146, 92], 0.0, 0.65),
|
||||
];
|
||||
assert_eq!(solidcraft_doc::appearance::OPAQUE_FINISHES, finishes);
|
||||
assert!(finishes.iter().all(|finish| solidcraft_doc::appearance::OPAQUE_FINISHES.contains(finish)));
|
||||
for (name, color, metallic, roughness) in finishes {
|
||||
let matching: Vec<_> = rows.iter().filter(|row| row["name"] == name).collect();
|
||||
assert_eq!(matching.len(), 1, "published finish must occur exactly once: {name}");
|
||||
@@ -462,12 +463,17 @@ fn pack_listing_keeps_legacy_rows_and_labels_unavailable_recipes_without_mutatio
|
||||
assert_eq!(row["pbr"], json!({"metallic":metallic,"roughness":roughness,"dielectric_ior":1.5}));
|
||||
assert!(row.get("id").is_none());
|
||||
}
|
||||
assert_eq!(rows.len(), 66);
|
||||
assert_eq!(rows.iter().filter(|row| row["available"] != false).count(), 60);
|
||||
assert_eq!(rows.iter().filter(|row| row["available"] != false && row["opacity"] == 1.0).count(), 58);
|
||||
assert_eq!(rows.iter().filter(|row| row["opacity"] != 1.0).count(), 2);
|
||||
assert_eq!(rows.iter().filter(|v| v.get("id").is_some()).count(), 40);
|
||||
assert_eq!(rows.iter().filter(|v| v["available"] == false).count(), 6);
|
||||
assert!(!rows.is_empty());
|
||||
let mut names = std::collections::BTreeSet::new();
|
||||
for row in rows {
|
||||
let name = row["name"].as_str().unwrap();
|
||||
assert!(!name.is_empty() && names.insert(name.to_ascii_lowercase()));
|
||||
}
|
||||
assert!(rows.iter().filter(|row| row["available"] != false).count() >= 60);
|
||||
assert!(rows.iter().filter(|row| row["available"] != false && row["opacity"] == 1.0).count() >= 58);
|
||||
assert!(rows.iter().filter(|row| row["opacity"] != 1.0).count() >= 2);
|
||||
assert!(rows.iter().filter(|v| v.get("id").is_some()).count() >= 40);
|
||||
assert!(rows.iter().filter(|v| v["available"] == false).count() >= 6);
|
||||
for row in rows.iter().filter(|v| v["category"] == "glass") {
|
||||
assert_eq!(row["available"], false);
|
||||
assert_eq!(row["opacity"], 1.0);
|
||||
@@ -608,7 +614,7 @@ fn starter_visual_defaults_and_overrides_keep_saved_physical_cards_independent()
|
||||
fn starter_names_and_ids_share_admission_before_all_overrides() {
|
||||
use crate::doc::appearance_pack::{Recipe, STARTER_PACK};
|
||||
let mut s = pack_session();
|
||||
for preset in &STARTER_PACK {
|
||||
for preset in STARTER_PACK {
|
||||
for selector in [preset.id, preset.name] {
|
||||
if matches!(preset.recipe, Recipe::Glass(_)) {
|
||||
for overrides in [
|
||||
|
||||
@@ -18,7 +18,7 @@ use solidcraft_render::{
|
||||
|
||||
use crate::{Result, Session, params::bad};
|
||||
|
||||
const KINDS: [&str; 12] = [
|
||||
const KINDS: &[&str] = &[
|
||||
"materials",
|
||||
"lighting",
|
||||
"environments",
|
||||
@@ -45,7 +45,7 @@ fn catalog(kind: &str, command: &str) -> Result<&'static [Value]> {
|
||||
}
|
||||
let cached = CATALOGS.get_or_init(|| {
|
||||
let mut catalogs = BTreeMap::new();
|
||||
for kind in KINDS {
|
||||
for &kind in KINDS {
|
||||
let source = super::library_data::catalog(kind).ok_or_else(|| format!("missing built-in catalog {kind}"))?;
|
||||
let value: Value = serde_json::from_str(source).map_err(|e| format!("invalid built-in catalog {kind}: {e}"))?;
|
||||
if !value.get("items").is_some_and(Value::is_array) {
|
||||
|
||||
@@ -14,11 +14,14 @@ fn subject() -> Session {
|
||||
#[test]
|
||||
fn native_studios_resolve_without_mutation_and_apply_one_complete_undo() {
|
||||
let entries = super::entries();
|
||||
assert_eq!(entries.as_array().unwrap().len(), 25);
|
||||
let hdrs: std::collections::BTreeSet<_> = entries.as_array().unwrap().iter().map(|item| item["hdr"].as_str().unwrap()).collect();
|
||||
assert_eq!(hdrs.len(), 24, "every authored HDR has a complete curated composition");
|
||||
for index in 1..=25 {
|
||||
let id = format!("NATIVE-STUDIO-{index:03}");
|
||||
let entries = entries.as_array().unwrap();
|
||||
assert!(!entries.is_empty());
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
let hdrs: std::collections::BTreeSet<_> = entries.iter().map(|item| item["hdr"].as_str().unwrap()).collect();
|
||||
assert!(hdrs.len() >= 24, "every authored HDR has a complete curated composition");
|
||||
for entry in entries {
|
||||
let id = entry["id"].as_str().unwrap();
|
||||
assert!(!id.is_empty() && ids.insert(id));
|
||||
let mut s = subject();
|
||||
let before = s.doc.to_json();
|
||||
let undo = s.undo.len();
|
||||
|
||||
@@ -390,7 +390,7 @@ impl JsonSchema for StringList {
|
||||
|
||||
/// The spellings `Operation::parse` accepts, canonical ones first (it also ignores case, spaces,
|
||||
/// '_' and '-', which an enum can't say).
|
||||
const OPERATIONS: [&str; 13] = [
|
||||
const OPERATIONS: &[&str] = &[
|
||||
"new",
|
||||
"join",
|
||||
"cut",
|
||||
@@ -447,7 +447,7 @@ impl JsonSchema for OperationParam {
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
enum_schema(
|
||||
&OPERATIONS,
|
||||
OPERATIONS,
|
||||
"How the feature combines with existing bodies: new, join, cut or intersect. The others are aliases. Matching ignores case, spaces, '_' and '-'.",
|
||||
)
|
||||
}
|
||||
@@ -471,7 +471,7 @@ impl JsonSchema for ExtrudeOperation {
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
let mut values = vec!["auto"];
|
||||
values.extend(OPERATIONS);
|
||||
values.extend_from_slice(OPERATIONS);
|
||||
enum_schema(
|
||||
&values,
|
||||
"How the extrusion combines with existing bodies: auto (cut into a body, otherwise join; new in an empty component), new, join, cut or intersect. The others are aliases. Matching ignores case, spaces, '_' and '-' (only case for auto).",
|
||||
@@ -479,7 +479,7 @@ impl JsonSchema for ExtrudeOperation {
|
||||
}
|
||||
}
|
||||
|
||||
const DIRECTIONS: [&str; 6] = ["positive", "negative", "symmetric", "one_side", "reverse", "flip"];
|
||||
const DIRECTIONS: &[&str] = &["positive", "negative", "symmetric", "one_side", "reverse", "flip"];
|
||||
|
||||
/// Extrude's direction, as `extent_param` reads it (lowercased, not trimmed).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -500,7 +500,7 @@ impl JsonSchema for DirectionParam {
|
||||
}
|
||||
fn json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
enum_schema(
|
||||
&DIRECTIONS,
|
||||
DIRECTIONS,
|
||||
"Which way to extrude: positive (the default), negative, or symmetric (the distance is the total thickness, split evenly around the start plane). one_side is an alias of positive; reverse and flip of negative. Matching ignores case.",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ impl Fit {
|
||||
|
||||
/// The Unicode blocks of the scripts written in wide characters (Chinese, Japanese, Korean and Yi),
|
||||
/// with their full-width and vertical forms.
|
||||
const WIDE: [(u32, u32); 15] = [
|
||||
const WIDE: &[(u32, u32)] = &[
|
||||
(0x1100, 0x115F),
|
||||
(0x2E80, 0x303E),
|
||||
(0x3041, 0x33FF),
|
||||
|
||||
@@ -136,7 +136,7 @@ pub struct Scale {
|
||||
}
|
||||
|
||||
/// ISO 5455's recommended scales, largest first: enlargements, full size, then reductions.
|
||||
pub const PRESETS: [Scale; 18] = [
|
||||
pub const PRESETS: &[Scale] = &[
|
||||
Scale { paper: 50.0, model: 1.0 },
|
||||
Scale { paper: 20.0, model: 1.0 },
|
||||
Scale { paper: 10.0, model: 1.0 },
|
||||
@@ -181,7 +181,7 @@ impl Scale {
|
||||
if ![size_mm, room_mm].into_iter().all(|v| v.is_finite() && v.x >= 0.0 && v.y >= 0.0) {
|
||||
return None;
|
||||
}
|
||||
PRESETS.into_iter().find(|s| size_mm.x * s.paper <= room_mm.x * s.model && size_mm.y * s.paper <= room_mm.y * s.model)
|
||||
PRESETS.iter().copied().find(|s| size_mm.x * s.paper <= room_mm.x * s.model && size_mm.y * s.paper <= room_mm.y * s.model)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,10 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed={dir}/logs/HEAD");
|
||||
}
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
if let Err(e) = builtin_themes() {
|
||||
eprintln!("themes: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
scenario_tests();
|
||||
if let Err(e) = merged_locales() {
|
||||
eprintln!("locales: {e}");
|
||||
@@ -279,3 +283,33 @@ fn icon_data() -> Result<(), String> {
|
||||
let dest = std::path::Path::new(&std::env::var("OUT_DIR").map_err(|e| e.to_string())?).join("icon_data.rs");
|
||||
std::fs::write(dest, out).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Embed each theme without a shared registry. Only std is needed to collect the files;
|
||||
/// theme parsing and ordering use the same validation on native and web targets.
|
||||
fn builtin_themes() -> Result<(), String> {
|
||||
let manifest = std::env::var("CARGO_MANIFEST_DIR").map_err(|e| e.to_string())?;
|
||||
let dir = std::path::Path::new(&manifest).join("../../assets/styles/ui/themes");
|
||||
println!("cargo:rerun-if-changed={}", dir.display());
|
||||
let mut files = Vec::new();
|
||||
for entry in std::fs::read_dir(&dir).map_err(|e| e.to_string())? {
|
||||
let path = entry.map_err(|e| e.to_string())?.path();
|
||||
if path.extension().is_some_and(|e| e == "json") {
|
||||
files.push(path);
|
||||
}
|
||||
}
|
||||
files.sort_by(|a, b| a.file_stem().cmp(&b.file_stem()));
|
||||
let mut out = String::from("/// The theme files shipped with SolidCraft.\nconst BUILTIN: &[(&str, &str)] = &[\n");
|
||||
for path in files {
|
||||
let id = path.file_stem().and_then(|s| s.to_str()).ok_or("theme file name is not UTF-8")?;
|
||||
let text = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
||||
// Keep each file's provenance beside its generated entry, never in a shared header.
|
||||
let provenance = text
|
||||
.lines()
|
||||
.find(|line| line.trim_start().starts_with("\"provenance\":"))
|
||||
.ok_or_else(|| format!("{}: missing provenance", path.display()))?;
|
||||
out += &format!(" ({id:?}, include_str!({:?})), // {}\n", path.display().to_string(), provenance.trim());
|
||||
}
|
||||
out += "];\n";
|
||||
let dest = std::path::Path::new(&std::env::var("OUT_DIR").map_err(|e| e.to_string())?).join("builtin_themes.rs");
|
||||
std::fs::write(dest, out).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::control::ControlRequest;
|
||||
|
||||
/// Areas whose commands only compute the design (no files written, nothing started outside the
|
||||
/// app), so evaluating them ahead on a copy has no effect but a warm cache.
|
||||
const AHEAD_AREAS: [&str; 22] = [
|
||||
const AHEAD_AREAS: &[&str] = &[
|
||||
"appearance",
|
||||
"assembly",
|
||||
"body",
|
||||
@@ -64,16 +64,16 @@ const AHEAD_AREAS: [&str; 22] = [
|
||||
];
|
||||
|
||||
/// Parameters that name a file or folder: a command given one runs here, as before.
|
||||
const NAMES_A_FILE: [&str; 8] = ["path", "paths", "file", "files", "dir", "folder", "url", "out"];
|
||||
const NAMES_A_FILE: &[&str] = &["path", "paths", "file", "files", "dir", "folder", "url", "out"];
|
||||
|
||||
/// Commands that change nothing in the design and write a file: they run on a copy.
|
||||
const ANSWERED_ON_A_COPY: [&str; 2] = ["file.export", "file.save_mesh"];
|
||||
const ANSWERED_ON_A_COPY: &[&str] = &["file.export", "file.save_mesh"];
|
||||
|
||||
/// Commands that only read the design, which can take long on a large one (a measure of every
|
||||
/// body, an inspection that measures them, #4604; a clearance or interference check of every pair
|
||||
/// of bodies, minutes on an assembly of 150): they run on a copy too, whatever the preference,
|
||||
/// since an answer from the design as it was when asked has no order to keep.
|
||||
const READ_ON_A_COPY: [&str; 4] = ["inspect.measure", "document.inspect", "inspect.clearance", "inspect.interference"];
|
||||
const READ_ON_A_COPY: &[&str] = &["inspect.measure", "document.inspect", "inspect.clearance", "inspect.interference"];
|
||||
|
||||
/// Exports and readings computing at once; more run here.
|
||||
const MAX_ANSWERING: usize = 4;
|
||||
|
||||
@@ -6563,7 +6563,7 @@ mod construct_ask_tests {
|
||||
use super::*;
|
||||
use crate::scenario::Harness;
|
||||
|
||||
const CONSTRUCT_PICK_COMMANDS: [&str; 15] = [
|
||||
const CONSTRUCT_PICK_COMMANDS: &[&str] = &[
|
||||
"construct.axis.through_cylinder",
|
||||
"construct.axis.normal_to_face",
|
||||
"construct.axis.two_planes",
|
||||
|
||||
@@ -942,7 +942,7 @@ mod tests {
|
||||
use solidcraft_engine::doc::appearance_pack::STARTER_PACK;
|
||||
let mut app = app();
|
||||
let looks = library(&mut app);
|
||||
for preset in &STARTER_PACK {
|
||||
for preset in STARTER_PACK {
|
||||
let matching: Vec<_> = looks.iter().filter(|look| look.preset.as_ref().is_some_and(|p| p.id == preset.id)).collect();
|
||||
assert_eq!(matching.len(), 1, "actual provider entry missing from UI: {}", preset.id);
|
||||
let look = matching[0];
|
||||
@@ -952,15 +952,15 @@ mod tests {
|
||||
assert_eq!(preset_available(look), preset.is_opaque());
|
||||
}
|
||||
let pack: Vec<_> = looks.iter().filter(|look| look.preset.is_some()).collect();
|
||||
assert_eq!(pack.len(), 40);
|
||||
assert_eq!(pack.iter().filter(|look| preset_available(look)).count(), 34);
|
||||
assert_eq!(pack.iter().filter(|look| !preset_available(look)).count(), 6);
|
||||
for (name, color, opacity) in solidcraft_engine::doc::appearance::LIBRARY {
|
||||
assert!(pack.len() >= 40);
|
||||
assert!(pack.iter().filter(|look| preset_available(look)).count() >= 34);
|
||||
assert!(pack.iter().filter(|look| !preset_available(look)).count() >= 6);
|
||||
for &(name, color, opacity) in solidcraft_engine::doc::appearance::LIBRARY {
|
||||
let look = looks.iter().find(|look| look.name == name).unwrap();
|
||||
assert_eq!((look.color, look.opacity, look.pbr, look.preset.as_ref()), (color, opacity, None, None));
|
||||
}
|
||||
// The six published finishes remain independent of starter metadata.
|
||||
for (name, color, metallic, roughness) in solidcraft_engine::doc::appearance::OPAQUE_FINISHES {
|
||||
for &(name, color, metallic, roughness) in solidcraft_engine::doc::appearance::OPAQUE_FINISHES {
|
||||
let matching: Vec<_> = looks.iter().filter(|look| look.name == name).collect();
|
||||
assert_eq!(matching.len(), 1);
|
||||
let look = matching[0];
|
||||
@@ -969,10 +969,12 @@ mod tests {
|
||||
assert_eq!(look.pbr, Some(PbrParameters::new(metallic, roughness, 1.5).unwrap()));
|
||||
assert!(look.preset.is_none() && preset_available(look));
|
||||
}
|
||||
assert_eq!(looks.len(), 66);
|
||||
assert_eq!(looks.iter().filter(|look| preset_available(look)).count(), 60);
|
||||
assert_eq!(looks.iter().filter(|look| preset_available(look) && look.opacity == 1.0).count(), 58);
|
||||
assert_eq!(looks.iter().filter(|look| look.opacity != 1.0).count(), 2);
|
||||
assert!(!looks.is_empty());
|
||||
let mut names = std::collections::BTreeSet::new();
|
||||
assert!(looks.iter().all(|look| names.insert(look.name.to_ascii_lowercase())));
|
||||
assert!(looks.iter().filter(|look| preset_available(look)).count() >= 60);
|
||||
assert!(looks.iter().filter(|look| preset_available(look) && look.opacity == 1.0).count() >= 58);
|
||||
assert!(looks.iter().filter(|look| look.opacity != 1.0).count() >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1004,7 +1006,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn manual_edit_of_published_finish_keeps_scalar_override_after_selection_and_editor_reopen() {
|
||||
for (name, ..) in solidcraft_engine::doc::appearance::OPAQUE_FINISHES {
|
||||
for &(name, ..) in solidcraft_engine::doc::appearance::OPAQUE_FINISHES {
|
||||
for reopened in [false, true] {
|
||||
let mut app = app();
|
||||
let look = library(&mut app).into_iter().find(|look| look.name == name).unwrap();
|
||||
@@ -1075,7 +1077,7 @@ mod tests {
|
||||
let available = rows.iter().filter(|row| row.get("available").and_then(Value::as_bool) != Some(false)).count();
|
||||
let selectable: Vec<_> = library(&mut h.app).into_iter().filter(preset_available).collect();
|
||||
assert_eq!(selectable.len(), available);
|
||||
for preset in &solidcraft_engine::doc::appearance_pack::STARTER_PACK {
|
||||
for preset in solidcraft_engine::doc::appearance_pack::STARTER_PACK {
|
||||
assert_eq!(selectable.iter().any(|look| look.name == preset.name), preset.is_opaque());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(crate) fn joint_choice(name: &str, kind: JointKind) -> String {
|
||||
/// What a joint origin input accepts (snaps on faces, edges and vertices).
|
||||
pub const SNAPS: u16 = FACES | EDGES | VERTICES;
|
||||
/// Commands whose effect is where occurrences sit (previewed on the placed model).
|
||||
const MOVES: [&str; 12] = [
|
||||
const MOVES: &[&str] = &[
|
||||
"joint.create",
|
||||
"joint.as_built",
|
||||
"joint.drive",
|
||||
|
||||
@@ -14,7 +14,7 @@ use crate::selection::{PROFILES, SKETCH_LINES, SelInput, SelectionExt as _};
|
||||
use crate::ui_kit::{choice_row, expr_row, hint_row, text_row};
|
||||
|
||||
/// The anchors as `frame.member` names them, and their labels.
|
||||
const ANCHORS: [(&str, &str); 11] = [
|
||||
const ANCHORS: &[(&str, &str)] = &[
|
||||
("origin", "Origin"),
|
||||
("center", "Center"),
|
||||
("centroid", "Centroid"),
|
||||
|
||||
@@ -806,7 +806,7 @@ mod tests {
|
||||
#[test]
|
||||
fn navigation_preset_help_reaches_the_catalog() {
|
||||
let english = Locale::for_language("en");
|
||||
for preset in &crate::prefs::NAV_PRESETS {
|
||||
for preset in crate::prefs::NAV_PRESETS {
|
||||
let id = format!("nav.{}.help", preset.id);
|
||||
assert_eq!(english.text(&id), preset.help, "{id}");
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use solidcraft_engine::doc::physical_material::{Family, MaterialCard, PropertyDa
|
||||
use crate::{SolveApp, i18n::tr, ui_kit};
|
||||
|
||||
const MAX_BYTES: usize = 256 * 1024;
|
||||
const KINDS: [&str; 9] = [
|
||||
const KINDS: &[&str] = &[
|
||||
"density",
|
||||
"elasticity",
|
||||
"tensile_strength",
|
||||
@@ -161,7 +161,7 @@ fn show_card(ui: &mut egui::Ui, card: &MaterialCard, report: &Value) {
|
||||
let family = serde_json::to_value(card.family).unwrap_or(Value::Null);
|
||||
value(ui, "family", family.as_str().map(str::to_string), "");
|
||||
ui_kit::text(ui, tr("ui.material_review.properties"), ui_kit::Text::Report("properties", false));
|
||||
for kind in KINDS {
|
||||
for &kind in KINDS {
|
||||
let label = format!("ui.material_review.{kind}");
|
||||
let Some(property) = card.properties.iter().find(|p| p.data.kind() == kind) else {
|
||||
ui_kit::text(ui, &format!("{}: {}", tr(&label), tr("ui.material_review.unknown")), ui_kit::Text::Report(kind, false));
|
||||
@@ -374,7 +374,7 @@ mod tests {
|
||||
for catalog in crate::i18n::LOCALES.iter().filter(|c| c.id != "en-GB") {
|
||||
let path = std::path::Path::new(env!("OUT_DIR")).join("locales").join(format!("{}.json", catalog.id));
|
||||
let messages: Value = serde_json::from_str(&std::fs::read_to_string(path).unwrap()).unwrap();
|
||||
for key in keys.into_iter().chain(KINDS) {
|
||||
for key in keys.into_iter().chain(KINDS.iter().copied()) {
|
||||
let key = format!("ui.material_review.{key}");
|
||||
assert!(messages.get(&key).and_then(Value::as_str).is_some_and(|s| !s.is_empty()), "{}: {key}", catalog.id);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::tool_modifiers::ModifierCombo;
|
||||
use crate::ui_kit;
|
||||
use crate::viewport::NavMode;
|
||||
|
||||
pub const UNITS: [&str; 5] = ["mm", "cm", "m", "in", "ft"];
|
||||
pub const UNITS: &[&str] = &["mm", "cm", "m", "in", "ft"];
|
||||
pub const OPENSCAD_NAV: &str = "openscad";
|
||||
pub const TOUCHPAD_NAV: &str = "touchpad";
|
||||
/// A mouse button that can drag the camera.
|
||||
@@ -100,7 +100,7 @@ use NavButton::{Left, Middle, Right};
|
||||
use NavMode::{Orbit, Pan, Zoom};
|
||||
|
||||
/// The mouse mappings. The first is the default.
|
||||
pub const NAV_PRESETS: [NavPreset; 10] = [
|
||||
pub const NAV_PRESETS: &[NavPreset] = &[
|
||||
NavPreset {
|
||||
id: "fusion",
|
||||
label: "Fusion",
|
||||
@@ -233,7 +233,7 @@ pub fn nav_preset(id: &str) -> &'static NavPreset {
|
||||
NAV_PRESETS.iter().find(|p| p.id == id).unwrap_or(&NAV_PRESETS[0])
|
||||
}
|
||||
/// Stable control-channel names; presentation labels may be shorter.
|
||||
pub const PAGES: [&str; 9] = ["General", "Interface", "Design", "Navigation", "Keyboard Shortcuts", "Sketch", "Graphics", "AI", "Plugins"];
|
||||
pub const PAGES: &[&str] = &["General", "Interface", "Design", "Navigation", "Keyboard Shortcuts", "Sketch", "Graphics", "AI", "Plugins"];
|
||||
pub const PAGE_GENERAL: usize = 0;
|
||||
pub const PAGE_INTERFACE: usize = 1;
|
||||
pub const PAGE_DESIGN: usize = 2;
|
||||
@@ -243,7 +243,7 @@ pub const PAGE_SKETCH: usize = 5;
|
||||
pub const PAGE_GRAPHICS: usize = 6;
|
||||
pub const PAGE_AI: usize = 7;
|
||||
pub const PAGE_PLUGINS: usize = 8;
|
||||
const WINDOW_PAGES: [ui_kit::WindowPage<SolveApp>; 9] = [
|
||||
const WINDOW_PAGES: &[ui_kit::WindowPage<SolveApp>] = &[
|
||||
ui_kit::WindowPage { icon: "settings", label: "ui.general", group: None, handle: "prefs:page:General", scroll: true, draw: window_page::<0> },
|
||||
ui_kit::WindowPage {
|
||||
icon: "settings_palette",
|
||||
@@ -434,7 +434,7 @@ fn window_page<const INDEX: usize>(ui: &mut egui::Ui, app: &mut SolveApp) {
|
||||
page(app, ui, &Tokens::get());
|
||||
}
|
||||
|
||||
pub const THEMES: [(&str, &str); 3] = [("dark", "Dark"), ("light", "Light"), ("system", "System")];
|
||||
pub const THEMES: &[(&str, &str)] = &[("dark", "Dark"), ("light", "Light"), ("system", "System")];
|
||||
pub const MSAA: [u32; 4] = [1, 2, 4, 8];
|
||||
/// Supersampling factors (#1288): off, 1.5× and 2× the viewport's pixels each way.
|
||||
pub const SUPERSAMPLING: [f32; 3] = [1.0, 1.5, 2.0];
|
||||
@@ -1821,7 +1821,7 @@ pub fn show_mm_bare(app: &SolveApp, v: f64) -> String {
|
||||
}
|
||||
|
||||
/// The window system choices (`Prefs::window_system`, #3083).
|
||||
pub const WINDOW_SYSTEMS: [&str; 3] = ["auto", "wayland", "x11"];
|
||||
pub const WINDOW_SYSTEMS: &[&str] = &["auto", "wayland", "x11"];
|
||||
/// winit's issue for dropping files onto a window on native Wayland, which the note links to.
|
||||
pub const WAYLAND_DROP_ISSUE: &str = "https://github.com/rust-windowing/winit/issues/1881";
|
||||
|
||||
@@ -1832,11 +1832,12 @@ pub const WAYLAND_DROP_ISSUE: &str = "https://github.com/rust-windowing/winit/is
|
||||
fn window_system_row(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let t = Tokens::get();
|
||||
let label = |id: &str| crate::i18n::tr(&format!("ui.window_system_{id}")).to_string();
|
||||
let labels = WINDOW_SYSTEMS.map(label);
|
||||
let labels: Vec<_> = WINDOW_SYSTEMS.iter().copied().map(label).collect();
|
||||
let labels: Vec<_> = labels.iter().map(String::as_str).collect();
|
||||
// The note below says that it applies after a restart, and which one runs now.
|
||||
let response = ui.allocate_ui_with_layout(egui::vec2(t.form_card_width, 0.0), egui::Layout::top_down(egui::Align::Min), |ui| {
|
||||
let mut selected = WINDOW_SYSTEMS.iter().position(|id| *id == app.preferences.window_system).unwrap_or(0);
|
||||
if ui_kit::segmented(ui, &mut selected, &labels.each_ref().map(String::as_str), ui_kit::Segments::default())
|
||||
if ui_kit::segmented(ui, &mut selected, &labels, ui_kit::Segments::default())
|
||||
&& let Some(id) = WINDOW_SYSTEMS.get(selected)
|
||||
{
|
||||
app.preferences.window_system = (*id).into();
|
||||
@@ -1978,7 +1979,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
.actions("OK", "Cancel", Some("Apply"));
|
||||
let window = if capturing { window.keep_open_on_escape() } else { window };
|
||||
let mut selected = app.prefs_window.section;
|
||||
let (closed, action) = window.show_pages_with_header(ctx, &mut selected, &WINDOW_PAGES, app, search_header, clear_search, |ui| {
|
||||
let (closed, action) = window.show_pages_with_header(ctx, &mut selected, WINDOW_PAGES, app, search_header, clear_search, |ui| {
|
||||
if ui_kit::button(ui, crate::i18n::tr("ui.restore_defaults"), ui_kit::Button::Normal)
|
||||
.on_hover_text(crate::i18n::tr("ui.reset_the_settings_on_this_page"))
|
||||
.clicked()
|
||||
|
||||
@@ -8,7 +8,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ui_kit::settings_form(ui, t.space_24 * 7.0 + t.space_2, |ui| {
|
||||
ui_kit::row(ui, app.tr("ui.units_of_new_designs"), |ui| {
|
||||
let mut selected = UNITS.iter().position(|u| *u == app.preferences.default_units).unwrap_or(0);
|
||||
ui_kit::segmented(ui, &mut selected, &UNITS, ui_kit::Segments { intrinsic: true, ..Default::default() });
|
||||
ui_kit::segmented(ui, &mut selected, UNITS, ui_kit::Segments { intrinsic: true, ..Default::default() });
|
||||
if let Some(unit) = UNITS.get(selected)
|
||||
&& app.preferences.default_units != *unit
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui, t: &Tokens) {
|
||||
ui_kit::settings_form(ui, 110.0, |ui| {
|
||||
ui_kit::row(ui, app.tr("prefs.mouse_mapping"), |ui| {
|
||||
let picker = ui_kit::dropdown_menu(ui, "pref_nav", nav_preset(&app.preferences.nav).translated_label(), |ui| {
|
||||
for preset in &NAV_PRESETS {
|
||||
for preset in NAV_PRESETS {
|
||||
if ui_kit::list_picker(ui, preset.id, preset.translated_label(), app.preferences.nav == preset.id).clicked() {
|
||||
app.preferences.nav = preset.id.into();
|
||||
app.preferences.zoom_reverse = preset.reverse_zoom;
|
||||
|
||||
@@ -614,36 +614,8 @@ pub fn export(name: &str, base: Base, t: &Tokens) -> Value {
|
||||
json!({"format": FORMAT, "name": name, "base": base, "colors": colors, "radii": radii, "borders": borders, "icon_palette": icon_palette})
|
||||
}
|
||||
|
||||
/// The theme files that come with SolidCraft (original files; Gruvbox, Tokyo Night, Solarized, Nord, Nosferatu and Catppuccin map open-source palettes, MIT; Okibi and Akari map the Okibi design system; Clay and Clay Dark are original warm paper and charcoal palettes; CGA keeps to the 16 colours of the IBM Color Graphics Adapter; 1-bit and 1-bit Dark are original black-and-white palettes).
|
||||
const BUILTIN: [(&str, &str); 27] = [
|
||||
("gruvbox-dark", include_str!("../../../assets/styles/ui/themes/gruvbox-dark.json")),
|
||||
("gruvbox-dark-hard", include_str!("../../../assets/styles/ui/themes/gruvbox-dark-hard.json")),
|
||||
("gruvbox-dark-soft", include_str!("../../../assets/styles/ui/themes/gruvbox-dark-soft.json")),
|
||||
("gruvbox-light", include_str!("../../../assets/styles/ui/themes/gruvbox-light.json")),
|
||||
("gruvbox-light-hard", include_str!("../../../assets/styles/ui/themes/gruvbox-light-hard.json")),
|
||||
("gruvbox-light-soft", include_str!("../../../assets/styles/ui/themes/gruvbox-light-soft.json")),
|
||||
("solidcraft-95", include_str!("../../../assets/styles/ui/themes/solidcraft-95.json")),
|
||||
("1-bit", include_str!("../../../assets/styles/ui/themes/1-bit.json")),
|
||||
("1-bit-dark", include_str!("../../../assets/styles/ui/themes/1-bit-dark.json")),
|
||||
("platinum-9", include_str!("../../../assets/styles/ui/themes/platinum-9.json")),
|
||||
("cupertino", include_str!("../../../assets/styles/ui/themes/cupertino.json")),
|
||||
("green-phosphor", include_str!("../../../assets/styles/ui/themes/green-phosphor.json")),
|
||||
("amber-phosphor", include_str!("../../../assets/styles/ui/themes/amber-phosphor.json")),
|
||||
("blueprint", include_str!("../../../assets/styles/ui/themes/blueprint.json")),
|
||||
("one-dark", include_str!("../../../assets/styles/ui/themes/one-dark.json")),
|
||||
("tokyo-night", include_str!("../../../assets/styles/ui/themes/tokyo-night.json")),
|
||||
("solarized-light", include_str!("../../../assets/styles/ui/themes/solarized-light.json")),
|
||||
("nord", include_str!("../../../assets/styles/ui/themes/nord.json")),
|
||||
("high-contrast", include_str!("../../../assets/styles/ui/themes/high-contrast.json")),
|
||||
("graphite", include_str!("../../../assets/styles/ui/themes/graphite.json")),
|
||||
("nosferatu", include_str!("../../../assets/styles/ui/themes/nosferatu.json")),
|
||||
("catppuccin-mocha", include_str!("../../../assets/styles/ui/themes/catppuccin-mocha.json")),
|
||||
("okibi", include_str!("../../../assets/styles/ui/themes/okibi.json")),
|
||||
("akari", include_str!("../../../assets/styles/ui/themes/akari.json")),
|
||||
("clay", include_str!("../../../assets/styles/ui/themes/clay.json")),
|
||||
("clay-dark", include_str!("../../../assets/styles/ui/themes/clay-dark.json")),
|
||||
("cga", include_str!("../../../assets/styles/ui/themes/cga.json")),
|
||||
];
|
||||
// Theme sources are collected at build time; picker order is validated below.
|
||||
include!(concat!(env!("OUT_DIR"), "/builtin_themes.rs"));
|
||||
|
||||
/// The two-colour built-in themes (1-bit and 1-bit Dark): pure black and white, so a few looks
|
||||
/// other themes tell apart by colour are told apart by text or shape instead.
|
||||
@@ -652,23 +624,47 @@ pub(crate) fn is_two_colour(id: &str) -> bool {
|
||||
matches!(id, "1-bit" | "1-bit-dark")
|
||||
}
|
||||
|
||||
/// The built-in themes, read once (one that fails to read is left out; a test reads them all).
|
||||
/// The built-in themes, validated and ordered once; a test checks every shipped file.
|
||||
pub fn builtin() -> &'static [ThemeFile] {
|
||||
static THEMES: OnceLock<Vec<ThemeFile>> = OnceLock::new();
|
||||
THEMES.get_or_init(|| {
|
||||
BUILTIN
|
||||
.iter()
|
||||
.filter_map(|(id, text)| match parse(text) {
|
||||
Ok(t) => Some(ThemeFile { id: (*id).to_string(), ..t }),
|
||||
Err(e) => {
|
||||
log::error!("built-in theme {id}: {e}");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
THEMES.get_or_init(|| match read_builtin(BUILTIN) {
|
||||
Ok(themes) => themes,
|
||||
Err(e) => {
|
||||
log::error!("built-in themes: {e}");
|
||||
Vec::new()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn read_builtin(sources: &[(&str, &str)]) -> Result<Vec<ThemeFile>, String> {
|
||||
let mut orders = std::collections::BTreeSet::new();
|
||||
let mut ids = std::collections::BTreeSet::new();
|
||||
let mut themes = Vec::new();
|
||||
for &(id, text) in sources {
|
||||
if !ids.insert(id) || id.is_empty() {
|
||||
return Err(format!("empty or duplicate theme id: {id}"));
|
||||
}
|
||||
if text.len() > MAX_BYTES {
|
||||
return Err(format!("theme {id} exceeds the file size limit"));
|
||||
}
|
||||
let value: Value = serde_json::from_str(text).map_err(|e| format!("theme {id}: {e}"))?;
|
||||
let order = value.get("order").and_then(Value::as_u64).ok_or_else(|| format!("theme {id}: order must be a nonnegative integer"))?;
|
||||
if !orders.insert(order) {
|
||||
return Err(format!("theme {id}: duplicate order {order}"));
|
||||
}
|
||||
if value.get("provenance").and_then(Value::as_str).is_none_or(|s| s.trim().is_empty()) {
|
||||
return Err(format!("theme {id}: missing provenance"));
|
||||
}
|
||||
let theme = from_value(&value).map_err(|e| format!("theme {id}: {e}"))?;
|
||||
themes.push((order, ThemeFile { id: id.into(), ..theme }));
|
||||
}
|
||||
if themes.is_empty() {
|
||||
return Err("no built-in themes".into());
|
||||
}
|
||||
themes.sort_by_key(|(order, _)| *order);
|
||||
Ok(themes.into_iter().map(|(_, theme)| theme).collect())
|
||||
}
|
||||
|
||||
/// The theme a `theme` preference names: built in, or imported. `None` for dark, light, system
|
||||
/// or an unknown id.
|
||||
pub fn find<'a>(id: &str, custom: &'a [ThemeFile]) -> Option<&'a ThemeFile> {
|
||||
@@ -1060,6 +1056,60 @@ mod tests {
|
||||
assert_eq!(t.file_image_tint, Color32::WHITE, "{id}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_order_preserves_the_existing_picker_sequence() {
|
||||
let original: &[&str] = &[
|
||||
"gruvbox-dark",
|
||||
"gruvbox-dark-hard",
|
||||
"gruvbox-dark-soft",
|
||||
"gruvbox-light",
|
||||
"gruvbox-light-hard",
|
||||
"gruvbox-light-soft",
|
||||
"solidcraft-95",
|
||||
"1-bit",
|
||||
"1-bit-dark",
|
||||
"platinum-9",
|
||||
"cupertino",
|
||||
"green-phosphor",
|
||||
"amber-phosphor",
|
||||
"blueprint",
|
||||
"one-dark",
|
||||
"tokyo-night",
|
||||
"solarized-light",
|
||||
"nord",
|
||||
"high-contrast",
|
||||
"graphite",
|
||||
"nosferatu",
|
||||
"catppuccin-mocha",
|
||||
"okibi",
|
||||
"akari",
|
||||
"clay",
|
||||
"clay-dark",
|
||||
"cga",
|
||||
];
|
||||
let themes = read_builtin(BUILTIN).unwrap();
|
||||
let retained: Vec<_> = themes.iter().map(|t| t.id.as_str()).filter(|id| original.contains(id)).collect();
|
||||
assert_eq!(retained, original);
|
||||
assert!(BUILTIN.windows(2).all(|pair| pair[0].0 < pair[1].0), "source entries are sorted by id");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_metadata_rejects_missing_invalid_and_duplicate_order() {
|
||||
let text =
|
||||
|order: Value| json!({"format": FORMAT, "name": "Synthetic", "base": "dark", "provenance": "Original", "order": order}).to_string();
|
||||
let good = text(json!(10));
|
||||
assert!(read_builtin(&[("a", &good)]).is_ok());
|
||||
let earlier = text(json!(5));
|
||||
let ordered = read_builtin(&[("a", &good), ("b", &earlier)]).unwrap();
|
||||
assert_eq!(ordered.iter().map(|t| t.id.as_str()).collect::<Vec<_>>(), ["b", "a"]);
|
||||
assert!(read_builtin(&[("a", &good), ("a", &earlier)]).is_err());
|
||||
assert!(read_builtin(&[("a", &good), ("b", &good)]).is_err());
|
||||
for order in [Value::Null, json!(-1), json!(1.5), json!("10")] {
|
||||
assert!(read_builtin(&[("a", &text(order))]).is_err());
|
||||
}
|
||||
assert!(read_builtin(&[]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn built_in_themes_read_and_differ_from_their_base() {
|
||||
assert_eq!(builtin().len(), BUILTIN.len(), "every built-in theme file reads");
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ license.workspace = true
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = { workspace = true }
|
||||
proc-macro2 = { workspace = true, features = ["span-locations"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
+209
-2
@@ -5,6 +5,7 @@
|
||||
//!
|
||||
//! - The message catalogs (`assets/locales/*.json`): one message per line, sorted by id, no
|
||||
//! duplicate ids, as `serde_json` pretty-prints them.
|
||||
//! - Growing Rust lists: no written array lengths on more than ten lines of entries.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
@@ -37,9 +38,10 @@ pub fn run(root: &Path, fix: bool) -> Result<(), String> {
|
||||
stale.join(", ")
|
||||
));
|
||||
}
|
||||
check_arrays(root)?;
|
||||
crate::pending::check_locales(root)?;
|
||||
crate::pending::check_attribution(root)?;
|
||||
println!("lists: message catalogs are sorted; pending locale and attribution files fold cleanly");
|
||||
println!("lists: growing arrays checked; message catalogs sorted; pending locale and attribution files fold cleanly");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -56,9 +58,214 @@ pub fn canonical(text: &str) -> Result<String, String> {
|
||||
Ok(serde_json::to_string_pretty(&Value::Object(map)).map_err(|e| e.to_string())? + "\n")
|
||||
}
|
||||
|
||||
/// Fixed tables whose dimensions describe a standard or an algorithm, rather than a library.
|
||||
const FIXED_ARRAYS: &[(&str, &str)] = &[
|
||||
("assets/icons/app/shared/source/app_icon.rs", "FACES"), // Fixed five-face application mark geometry.
|
||||
("crates/doc/src/component_color.rs", "PALETTE"), // Fixed 24-step hue-wheel algorithm.
|
||||
("crates/doc/src/eval.rs", "ISO_COARSE"), // ISO metric coarse diameter/pitch pairs.
|
||||
("crates/doc/src/format.rs", "JSON_STEPS"), // Frozen historical migrations; additions use format/pending.
|
||||
("crates/engine/src/cmd/features.rs", "TABLE"), // ISO 273 metric bolt clearance-hole dimensions.
|
||||
("crates/engine/src/cmd/parts.rs", "BEARINGS"), // Standard deep-groove ball bearing dimensions.
|
||||
("crates/engine/src/cmd/parts.rs", "HEX_BOLT"), // ISO 4017 hex bolt dimensions.
|
||||
("crates/engine/src/cmd/parts.rs", "HEX_NUT"), // ISO 4032 hex nut dimensions.
|
||||
("crates/engine/src/cmd/parts.rs", "METRIC"), // Standard metric screw diameters and pitches.
|
||||
("crates/engine/src/cmd/parts.rs", "SHCS"), // ISO 4762 socket-head cap screw dimensions.
|
||||
("crates/engine/src/cmd/parts.rs", "WASHER"), // ISO 7089 plain washer dimensions.
|
||||
("crates/ui-egui/src/dialogs_assembly.rs", "TYPES"), // Joint types; TYPE_LABELS must stay the same length.
|
||||
("crates/ui-egui/src/dialogs_assembly.rs", "TYPE_LABELS"), // Labels for TYPES, index for index.
|
||||
("crates/ui-egui/src/space_mouse_calibration.rs", "STEP_PROMPTS"), // Labels for the twelve fixed calibration motions.
|
||||
("crates/ui-egui/src/space_mouse_calibration.rs", "TRACKBALL_PROMPTS"), // Trackball labels indexed by the same twelve motions.
|
||||
];
|
||||
|
||||
fn test_only(attrs: &[syn::Attribute]) -> bool {
|
||||
fn requires_test(meta: &syn::Meta) -> bool {
|
||||
match meta {
|
||||
syn::Meta::Path(path) => path.is_ident("test"),
|
||||
syn::Meta::List(list) => {
|
||||
let Ok(items) = list.parse_args_with(syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated) else {
|
||||
return false;
|
||||
};
|
||||
if list.path.is_ident("all") {
|
||||
items.iter().any(requires_test)
|
||||
} else {
|
||||
list.path.is_ident("any") && !items.is_empty() && items.iter().all(requires_test)
|
||||
}
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
attrs.iter().any(|a| a.path().is_ident("test") || (a.path().is_ident("cfg") && a.parse_args::<syn::Meta>().is_ok_and(|m| requires_test(&m))))
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ArrayGuard {
|
||||
found: Vec<String>,
|
||||
}
|
||||
|
||||
impl ArrayGuard {
|
||||
fn array(&mut self, name: &syn::Ident, ty: &syn::Type, expr: &syn::Expr) {
|
||||
use syn::spanned::Spanned;
|
||||
fn initializer(expr: &syn::Expr) -> Option<&syn::ExprArray> {
|
||||
match expr {
|
||||
syn::Expr::Array(array) => Some(array),
|
||||
syn::Expr::Paren(paren) => initializer(&paren.expr),
|
||||
syn::Expr::Group(group) => initializer(&group.expr),
|
||||
syn::Expr::Block(block) => match block.block.stmts.last()? {
|
||||
syn::Stmt::Expr(expr, None) => initializer(expr),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
if !matches!(ty, syn::Type::Array(_)) {
|
||||
return;
|
||||
}
|
||||
let count = if let Some(array) = initializer(expr) {
|
||||
let mut lines = std::collections::BTreeSet::new();
|
||||
for entry in &array.elems {
|
||||
lines.extend(entry.span().start().line..=entry.span().end().line);
|
||||
}
|
||||
lines.len()
|
||||
} else {
|
||||
// Macro and computed initializers still carry a written array length.
|
||||
expr.span().end().line.saturating_sub(expr.span().start().line + 1)
|
||||
};
|
||||
if count > 10 {
|
||||
self.found.push(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'ast> syn::visit::Visit<'ast> for ArrayGuard {
|
||||
fn visit_item(&mut self, item: &'ast syn::Item) {
|
||||
let attrs = match item {
|
||||
syn::Item::Mod(i) => &i.attrs,
|
||||
syn::Item::Fn(i) => &i.attrs,
|
||||
syn::Item::Const(i) => &i.attrs,
|
||||
syn::Item::Static(i) => &i.attrs,
|
||||
syn::Item::Impl(i) => &i.attrs,
|
||||
syn::Item::Trait(i) => &i.attrs,
|
||||
_ => return syn::visit::visit_item(self, item),
|
||||
};
|
||||
if !test_only(attrs) {
|
||||
syn::visit::visit_item(self, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_item_const(&mut self, item: &'ast syn::ItemConst) {
|
||||
self.array(&item.ident, &item.ty, &item.expr);
|
||||
syn::visit::visit_item_const(self, item);
|
||||
}
|
||||
|
||||
fn visit_item_static(&mut self, item: &'ast syn::ItemStatic) {
|
||||
self.array(&item.ident, &item.ty, &item.expr);
|
||||
syn::visit::visit_item_static(self, item);
|
||||
}
|
||||
|
||||
fn visit_impl_item(&mut self, item: &'ast syn::ImplItem) {
|
||||
let attrs = match item {
|
||||
syn::ImplItem::Const(i) => &i.attrs,
|
||||
syn::ImplItem::Fn(i) => &i.attrs,
|
||||
_ => return syn::visit::visit_impl_item(self, item),
|
||||
};
|
||||
if !test_only(attrs) {
|
||||
syn::visit::visit_impl_item(self, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_impl_item_const(&mut self, item: &'ast syn::ImplItemConst) {
|
||||
self.array(&item.ident, &item.ty, &item.expr);
|
||||
syn::visit::visit_impl_item_const(self, item);
|
||||
}
|
||||
fn visit_trait_item(&mut self, item: &'ast syn::TraitItem) {
|
||||
let attrs = match item {
|
||||
syn::TraitItem::Const(i) => &i.attrs,
|
||||
syn::TraitItem::Fn(i) => &i.attrs,
|
||||
_ => return syn::visit::visit_trait_item(self, item),
|
||||
};
|
||||
if !test_only(attrs) {
|
||||
syn::visit::visit_trait_item(self, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_trait_item_const(&mut self, item: &'ast syn::TraitItemConst) {
|
||||
if let Some((_, expr)) = &item.default {
|
||||
self.array(&item.ident, &item.ty, expr);
|
||||
}
|
||||
syn::visit::visit_trait_item_const(self, item);
|
||||
}
|
||||
}
|
||||
|
||||
fn array_violations(source: &str) -> Result<Vec<String>, String> {
|
||||
use syn::visit::Visit;
|
||||
let ast = syn::parse_file(source).map_err(|e| e.to_string())?;
|
||||
if test_only(&ast.attrs) {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let mut guard = ArrayGuard::default();
|
||||
guard.visit_file(&ast);
|
||||
Ok(guard.found)
|
||||
}
|
||||
|
||||
fn check_arrays(root: &Path) -> Result<(), String> {
|
||||
fn scan(root: &Path, dir: &Path, failures: &mut Vec<String>) -> Result<(), String> {
|
||||
for entry in std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))? {
|
||||
let entry = entry.map_err(|e| e.to_string())?;
|
||||
let path = entry.path();
|
||||
let kind = entry.file_type().map_err(|e| e.to_string())?;
|
||||
let name = entry.file_name();
|
||||
let name = name.to_string_lossy();
|
||||
if kind.is_dir() {
|
||||
if !matches!(name.as_ref(), "target" | ".git" | "tests" | "benches") {
|
||||
scan(root, &path, failures)?;
|
||||
}
|
||||
} else if kind.is_file() && path.extension().is_some_and(|e| e == "rs") && name != "tests.rs" && !name.ends_with("_tests.rs") {
|
||||
let relative = path.strip_prefix(root).unwrap_or(&path).to_string_lossy().replace('\\', "/");
|
||||
let source = std::fs::read_to_string(&path).map_err(|e| format!("{relative}: {e}"))?;
|
||||
for name in array_violations(&source).map_err(|e| format!("{relative}: {e}"))? {
|
||||
if !FIXED_ARRAYS.contains(&(relative.as_str(), name.as_str())) {
|
||||
failures.push(format!("{relative}: {name}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let mut failures = Vec::new();
|
||||
for dir in ["apps", "assets", "crates", "tools", "xtask"] {
|
||||
scan(root, &root.join(dir), &mut failures)?;
|
||||
}
|
||||
failures.sort();
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("use slices without written lengths for growing lists (fixed tables need a reason in FIXED_ARRAYS):\n{}", failures.join("\n")))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::canonical;
|
||||
use super::{array_violations, canonical};
|
||||
|
||||
#[test]
|
||||
fn growing_arrays_reject_lengths_and_allow_slices_short_tables_and_test_fixtures() {
|
||||
let entries = " 0,\n".repeat(11);
|
||||
let array = format!("const GROWING: [u8; 11] = [\n{entries}];");
|
||||
assert_eq!(array_violations(&array).unwrap(), ["GROWING"]);
|
||||
assert_eq!(array_violations(&array.replace("const", "static")).unwrap(), ["GROWING"]);
|
||||
assert_eq!(array_violations(&format!("const GENERATED: [u8; 11] = entries![\n{entries}];")).unwrap(), ["GENERATED"]);
|
||||
assert_eq!(array_violations(&format!("impl Catalog {{ {array} }}")).unwrap(), ["GROWING"]);
|
||||
assert!(array_violations(&array.replace("[u8; 11] = [", "&[u8] = &[ ")).unwrap().is_empty());
|
||||
assert!(array_violations(&format!("#[cfg(test)] mod tests {{ {array} }}")).unwrap().is_empty());
|
||||
assert!(array_violations(&format!("#![cfg(test)]\n{array}")).unwrap().is_empty());
|
||||
assert!(array_violations(&format!("#[cfg(all(test, unix))] mod tests {{ {array} }}")).unwrap().is_empty());
|
||||
assert_eq!(array_violations(&format!("#[cfg(not(test))] mod production {{ {array} }}")).unwrap(), ["GROWING"]);
|
||||
assert_eq!(array_violations(&format!("trait Catalog {{ {array} }}")).unwrap(), ["GROWING"]);
|
||||
assert!(array_violations(&format!("#[test] fn fixture() {{ {array} }}")).unwrap().is_empty());
|
||||
assert!(array_violations("const SHORT: [u8; 100] = [0; 100];").unwrap().is_empty());
|
||||
assert!(array_violations(&format!("const TEN: [u8; 10] = [\n{}];", "0,\n".repeat(10))).unwrap().is_empty());
|
||||
assert!(array_violations(&format!("const TEXT: &str = {array:?};")).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalogs_are_sorted_one_per_line_and_a_repeated_id_keeps_its_last_message() {
|
||||
|
||||
Reference in New Issue
Block a user