| 117 |
117 |
|
|
| 118 |
118 |
|
fn srgb_to_linear(c: u8) -> f32 {
|
| 119 |
119 |
|
let c = c as f32 / 255.0;
|
| 120 |
|
- |
if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
|
|
120 |
+ |
if c <= 0.04045 {
|
|
121 |
+ |
c / 12.92
|
|
122 |
+ |
} else {
|
|
123 |
+ |
((c + 0.055) / 1.055).powf(2.4)
|
|
124 |
+ |
}
|
| 121 |
125 |
|
}
|
| 122 |
126 |
|
|
| 123 |
127 |
|
fn linear_to_srgb(c: f32) -> u8 {
|
| 124 |
128 |
|
let c = c.clamp(0.0, 1.0);
|
| 125 |
|
- |
let v = if c <= 0.0031308 { c * 12.92 } else { 1.055 * c.powf(1.0 / 2.4) - 0.055 };
|
|
129 |
+ |
let v = if c <= 0.0031308 {
|
|
130 |
+ |
c * 12.92
|
|
131 |
+ |
} else {
|
|
132 |
+ |
1.055 * c.powf(1.0 / 2.4) - 0.055
|
|
133 |
+ |
};
|
| 126 |
134 |
|
(v * 255.0).round().clamp(0.0, 255.0) as u8
|
| 127 |
135 |
|
}
|
| 128 |
136 |
|
|
| 129 |
137 |
|
impl Rgb {
|
| 130 |
138 |
|
/// Convert to OKLab (Ottosson's sRGB matrices).
|
| 131 |
139 |
|
pub fn to_oklab(self) -> Oklab {
|
| 132 |
|
- |
let (r, g, b) = (srgb_to_linear(self.r), srgb_to_linear(self.g), srgb_to_linear(self.b));
|
|
140 |
+ |
let (r, g, b) = (
|
|
141 |
+ |
srgb_to_linear(self.r),
|
|
142 |
+ |
srgb_to_linear(self.g),
|
|
143 |
+ |
srgb_to_linear(self.b),
|
|
144 |
+ |
);
|
| 133 |
145 |
|
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
| 134 |
146 |
|
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
| 135 |
147 |
|
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
| 170 |
182 |
|
/// Pick black or white for legible text on `bg`, by the higher WCAG contrast
|
| 171 |
183 |
|
/// ratio (so the choice meets AA wherever the background allows it).
|
| 172 |
184 |
|
pub fn readable_on(bg: Rgb) -> Rgb {
|
| 173 |
|
- |
let white = Rgb { r: 255, g: 255, b: 255 };
|
|
185 |
+ |
let white = Rgb {
|
|
186 |
+ |
r: 255,
|
|
187 |
+ |
g: 255,
|
|
188 |
+ |
b: 255,
|
|
189 |
+ |
};
|
| 174 |
190 |
|
let black = Rgb { r: 0, g: 0, b: 0 };
|
| 175 |
|
- |
if wcag_contrast(white, bg) >= wcag_contrast(black, bg) { white } else { black }
|
|
191 |
+ |
if wcag_contrast(white, bg) >= wcag_contrast(black, bg) {
|
|
192 |
+ |
white
|
|
193 |
+ |
} else {
|
|
194 |
+ |
black
|
|
195 |
+ |
}
|
| 176 |
196 |
|
}
|
| 177 |
197 |
|
|
| 178 |
198 |
|
/// Shift OKLab lightness by `delta` (perceptually uniform). Positive lightens.
|
| 213 |
233 |
|
/// keeps it, which is the case that matters: a console app cannot fall back to
|
| 214 |
234 |
|
/// 24-bit color there.
|
| 215 |
235 |
|
pub const ANSI_16: [Rgb; 16] = [
|
| 216 |
|
- |
Rgb { r: 0x00, g: 0x00, b: 0x00 },
|
| 217 |
|
- |
Rgb { r: 0xaa, g: 0x00, b: 0x00 },
|
| 218 |
|
- |
Rgb { r: 0x00, g: 0xaa, b: 0x00 },
|
| 219 |
|
- |
Rgb { r: 0xaa, g: 0x55, b: 0x00 },
|
| 220 |
|
- |
Rgb { r: 0x00, g: 0x00, b: 0xaa },
|
| 221 |
|
- |
Rgb { r: 0xaa, g: 0x00, b: 0xaa },
|
| 222 |
|
- |
Rgb { r: 0x00, g: 0xaa, b: 0xaa },
|
| 223 |
|
- |
Rgb { r: 0xaa, g: 0xaa, b: 0xaa },
|
| 224 |
|
- |
Rgb { r: 0x55, g: 0x55, b: 0x55 },
|
| 225 |
|
- |
Rgb { r: 0xff, g: 0x55, b: 0x55 },
|
| 226 |
|
- |
Rgb { r: 0x55, g: 0xff, b: 0x55 },
|
| 227 |
|
- |
Rgb { r: 0xff, g: 0xff, b: 0x55 },
|
| 228 |
|
- |
Rgb { r: 0x55, g: 0x55, b: 0xff },
|
| 229 |
|
- |
Rgb { r: 0xff, g: 0x55, b: 0xff },
|
| 230 |
|
- |
Rgb { r: 0x55, g: 0xff, b: 0xff },
|
| 231 |
|
- |
Rgb { r: 0xff, g: 0xff, b: 0xff },
|
|
236 |
+ |
Rgb {
|
|
237 |
+ |
r: 0x00,
|
|
238 |
+ |
g: 0x00,
|
|
239 |
+ |
b: 0x00,
|
|
240 |
+ |
},
|
|
241 |
+ |
Rgb {
|
|
242 |
+ |
r: 0xaa,
|
|
243 |
+ |
g: 0x00,
|
|
244 |
+ |
b: 0x00,
|
|
245 |
+ |
},
|
|
246 |
+ |
Rgb {
|
|
247 |
+ |
r: 0x00,
|
|
248 |
+ |
g: 0xaa,
|
|
249 |
+ |
b: 0x00,
|
|
250 |
+ |
},
|
|
251 |
+ |
Rgb {
|
|
252 |
+ |
r: 0xaa,
|
|
253 |
+ |
g: 0x55,
|
|
254 |
+ |
b: 0x00,
|
|
255 |
+ |
},
|
|
256 |
+ |
Rgb {
|
|
257 |
+ |
r: 0x00,
|
|
258 |
+ |
g: 0x00,
|
|
259 |
+ |
b: 0xaa,
|
|
260 |
+ |
},
|
|
261 |
+ |
Rgb {
|
|
262 |
+ |
r: 0xaa,
|
|
263 |
+ |
g: 0x00,
|
|
264 |
+ |
b: 0xaa,
|
|
265 |
+ |
},
|
|
266 |
+ |
Rgb {
|
|
267 |
+ |
r: 0x00,
|
|
268 |
+ |
g: 0xaa,
|
|
269 |
+ |
b: 0xaa,
|
|
270 |
+ |
},
|
|
271 |
+ |
Rgb {
|
|
272 |
+ |
r: 0xaa,
|
|
273 |
+ |
g: 0xaa,
|
|
274 |
+ |
b: 0xaa,
|
|
275 |
+ |
},
|
|
276 |
+ |
Rgb {
|
|
277 |
+ |
r: 0x55,
|
|
278 |
+ |
g: 0x55,
|
|
279 |
+ |
b: 0x55,
|
|
280 |
+ |
},
|
|
281 |
+ |
Rgb {
|
|
282 |
+ |
r: 0xff,
|
|
283 |
+ |
g: 0x55,
|
|
284 |
+ |
b: 0x55,
|
|
285 |
+ |
},
|
|
286 |
+ |
Rgb {
|
|
287 |
+ |
r: 0x55,
|
|
288 |
+ |
g: 0xff,
|
|
289 |
+ |
b: 0x55,
|
|
290 |
+ |
},
|
|
291 |
+ |
Rgb {
|
|
292 |
+ |
r: 0xff,
|
|
293 |
+ |
g: 0xff,
|
|
294 |
+ |
b: 0x55,
|
|
295 |
+ |
},
|
|
296 |
+ |
Rgb {
|
|
297 |
+ |
r: 0x55,
|
|
298 |
+ |
g: 0x55,
|
|
299 |
+ |
b: 0xff,
|
|
300 |
+ |
},
|
|
301 |
+ |
Rgb {
|
|
302 |
+ |
r: 0xff,
|
|
303 |
+ |
g: 0x55,
|
|
304 |
+ |
b: 0xff,
|
|
305 |
+ |
},
|
|
306 |
+ |
Rgb {
|
|
307 |
+ |
r: 0x55,
|
|
308 |
+ |
g: 0xff,
|
|
309 |
+ |
b: 0xff,
|
|
310 |
+ |
},
|
|
311 |
+ |
Rgb {
|
|
312 |
+ |
r: 0xff,
|
|
313 |
+ |
g: 0xff,
|
|
314 |
+ |
b: 0xff,
|
|
315 |
+ |
},
|
| 232 |
316 |
|
];
|
| 233 |
317 |
|
|
| 234 |
318 |
|
/// The contrast ratio two colors must clear to read as separate areas.
|
| 297 |
381 |
|
|
| 298 |
382 |
|
let mut order: Vec<usize> = (0..palette.len()).collect();
|
| 299 |
383 |
|
order.sort_by(|a, b| {
|
| 300 |
|
- |
oklab_distance(fg, palette[*a])
|
| 301 |
|
- |
.total_cmp(&oklab_distance(fg, palette[*b]))
|
|
384 |
+ |
oklab_distance(fg, palette[*a]).total_cmp(&oklab_distance(fg, palette[*b]))
|
| 302 |
385 |
|
});
|
| 303 |
386 |
|
|
| 304 |
387 |
|
order
|
| 310 |
393 |
|
.iter()
|
| 311 |
394 |
|
.copied()
|
| 312 |
395 |
|
.max_by(|a, b| {
|
| 313 |
|
- |
wcag_contrast(palette[*a], shown)
|
| 314 |
|
- |
.total_cmp(&wcag_contrast(palette[*b], shown))
|
|
396 |
+ |
wcag_contrast(palette[*a], shown).total_cmp(&wcag_contrast(palette[*b], shown))
|
| 315 |
397 |
|
})
|
| 316 |
398 |
|
.expect("the palette is not empty")
|
| 317 |
399 |
|
})
|
| 364 |
446 |
|
|
| 365 |
447 |
|
/// Resolved RGB tuple for a token key (for egui / native consumers).
|
| 366 |
448 |
|
pub fn rgb(&self, key: &str) -> Option<(u8, u8, u8)> {
|
| 367 |
|
- |
self.intents.get(key).and_then(|h| Rgb::from_hex(h)).map(Rgb::tuple)
|
|
449 |
+ |
self.intents
|
|
450 |
+ |
.get(key)
|
|
451 |
+ |
.and_then(|h| Rgb::from_hex(h))
|
|
452 |
+ |
.map(Rgb::tuple)
|
| 368 |
453 |
|
}
|
| 369 |
454 |
|
}
|
| 370 |
455 |
|
|
| 408 |
493 |
|
let mut o = page.to_oklab();
|
| 409 |
494 |
|
o.l = 0.08;
|
| 410 |
495 |
|
let s = Rgb::from_oklab(o);
|
| 411 |
|
- |
intents.insert("overlay".into(), format!("rgba({}, {}, {}, 0.5)", s.r, s.g, s.b));
|
|
496 |
+ |
intents.insert(
|
|
497 |
+ |
"overlay".into(),
|
|
498 |
+ |
format!("rgba({}, {}, {}, 0.5)", s.r, s.g, s.b),
|
|
499 |
+ |
);
|
| 412 |
500 |
|
}
|
| 413 |
501 |
|
if let Some(sunken) = get(&intents, "surface-sunken") {
|
| 414 |
502 |
|
derived.push(("hover-surface".into(), sunken));
|
| 421 |
509 |
|
intents.insert(token, rgb.to_hex());
|
| 422 |
510 |
|
}
|
| 423 |
511 |
|
|
| 424 |
|
- |
SemanticTokens { meta: theme.meta.clone(), intents }
|
|
512 |
+ |
SemanticTokens {
|
|
513 |
+ |
meta: theme.meta.clone(),
|
|
514 |
+ |
intents,
|
|
515 |
+ |
}
|
| 425 |
516 |
|
}
|
| 426 |
517 |
|
|
| 427 |
518 |
|
/// Emit the resolved intent layer as CSS declarations (no selector), one
|
| 475 |
566 |
|
.unwrap_or("dark")
|
| 476 |
567 |
|
.to_string();
|
| 477 |
568 |
|
|
| 478 |
|
- |
ThemeMeta { id: id.to_string(), name, variant, is_custom }
|
|
569 |
+ |
ThemeMeta {
|
|
570 |
+ |
id: id.to_string(),
|
|
571 |
+ |
name,
|
|
572 |
+ |
variant,
|
|
573 |
+ |
is_custom,
|
|
574 |
+ |
}
|
| 479 |
575 |
|
}
|
| 480 |
576 |
|
|
| 481 |
577 |
|
/// Extract the intent color sections into a flat `HashMap` with dotted keys
|
| 628 |
724 |
|
.map_err(|e| format!("Failed to create {}: {}", custom_dir.display(), e))?;
|
| 629 |
725 |
|
|
| 630 |
726 |
|
let dest = custom_dir.join(format!("{}.toml", id));
|
| 631 |
|
- |
std::fs::copy(source_path, &dest)
|
| 632 |
|
- |
.map_err(|e| format!("Failed to copy theme: {}", e))?;
|
|
727 |
+ |
std::fs::copy(source_path, &dest).map_err(|e| format!("Failed to copy theme: {}", e))?;
|
| 633 |
728 |
|
|
| 634 |
729 |
|
Ok(parse_meta(&id, &table, true))
|
| 635 |
730 |
|
}
|
| 646 |
741 |
|
return Err(format!("Custom theme '{}' not found", id));
|
| 647 |
742 |
|
}
|
| 648 |
743 |
|
|
| 649 |
|
- |
std::fs::remove_file(&path)
|
| 650 |
|
- |
.map_err(|e| format!("Failed to delete {}: {}", path.display(), e))
|
|
744 |
+ |
std::fs::remove_file(&path).map_err(|e| format!("Failed to delete {}: {}", path.display(), e))
|
| 651 |
745 |
|
}
|
| 652 |
746 |
|
|
| 653 |
747 |
|
/// A four-color preview for theme thumbnails: the representative swatch from
|
| 705 |
799 |
|
let (source, _) =
|
| 706 |
800 |
|
find_theme_path(dirs, id).ok_or_else(|| format!("Theme '{}' not found", id))?;
|
| 707 |
801 |
|
|
| 708 |
|
- |
std::fs::copy(&source, dest_path)
|
| 709 |
|
- |
.map_err(|e| format!("Failed to export theme: {}", e))?;
|
|
802 |
+ |
std::fs::copy(&source, dest_path).map_err(|e| format!("Failed to export theme: {}", e))?;
|
| 710 |
803 |
|
|
| 711 |
804 |
|
Ok(())
|
| 712 |
805 |
|
}
|
| 797 |
890 |
|
#[test]
|
| 798 |
891 |
|
fn quantize_picks_the_obvious_entry() {
|
| 799 |
892 |
|
let black = Rgb { r: 0, g: 0, b: 0 };
|
| 800 |
|
- |
let white = Rgb { r: 255, g: 255, b: 255 };
|
|
893 |
+ |
let white = Rgb {
|
|
894 |
+ |
r: 255,
|
|
895 |
+ |
g: 255,
|
|
896 |
+ |
b: 255,
|
|
897 |
+ |
};
|
| 801 |
898 |
|
assert_eq!(quantize(black, &ANSI_16), 0);
|
| 802 |
899 |
|
assert_eq!(quantize(white, &ANSI_16), 15);
|
| 803 |
900 |
|
}
|
| 865 |
962 |
|
|
| 866 |
963 |
|
#[test]
|
| 867 |
964 |
|
fn parse_meta_with_name_and_variant() {
|
| 868 |
|
- |
let table: toml::Table = "[meta]\nname = \"Nord\"\nvariant = \"light\"\n".parse().unwrap();
|
|
965 |
+ |
let table: toml::Table = "[meta]\nname = \"Nord\"\nvariant = \"light\"\n"
|
|
966 |
+ |
.parse()
|
|
967 |
+ |
.unwrap();
|
| 869 |
968 |
|
let meta = parse_meta("nord", &table, false);
|
| 870 |
969 |
|
assert_eq!(meta.id, "nord");
|
| 871 |
970 |
|
assert_eq!(meta.name, "Nord");
|
| 886 |
985 |
|
|
| 887 |
986 |
|
#[test]
|
| 888 |
987 |
|
fn rgb_hex_roundtrip() {
|
| 889 |
|
- |
assert_eq!(Rgb::from_hex("#6196FF").unwrap(), Rgb { r: 0x61, g: 0x96, b: 0xff });
|
| 890 |
|
- |
assert_eq!(Rgb::from_hex("#abc").unwrap(), Rgb { r: 0xaa, g: 0xbb, b: 0xcc });
|
| 891 |
|
- |
assert_eq!(Rgb { r: 0x61, g: 0x96, b: 0xff }.to_hex(), "#6196ff");
|
|
988 |
+ |
assert_eq!(
|
|
989 |
+ |
Rgb::from_hex("#6196FF").unwrap(),
|
|
990 |
+ |
Rgb {
|
|
991 |
+ |
r: 0x61,
|
|
992 |
+ |
g: 0x96,
|
|
993 |
+ |
b: 0xff
|
|
994 |
+ |
}
|
|
995 |
+ |
);
|
|
996 |
+ |
assert_eq!(
|
|
997 |
+ |
Rgb::from_hex("#abc").unwrap(),
|
|
998 |
+ |
Rgb {
|
|
999 |
+ |
r: 0xaa,
|
|
1000 |
+ |
g: 0xbb,
|
|
1001 |
+ |
b: 0xcc
|
|
1002 |
+ |
}
|
|
1003 |
+ |
);
|
|
1004 |
+ |
assert_eq!(
|
|
1005 |
+ |
Rgb {
|
|
1006 |
+ |
r: 0x61,
|
|
1007 |
+ |
g: 0x96,
|
|
1008 |
+ |
b: 0xff
|
|
1009 |
+ |
}
|
|
1010 |
+ |
.to_hex(),
|
|
1011 |
+ |
"#6196ff"
|
|
1012 |
+ |
);
|
| 892 |
1013 |
|
assert!(Rgb::from_hex("not-a-color").is_none());
|
| 893 |
1014 |
|
}
|
| 894 |
1015 |
|
|
| 906 |
1027 |
|
|
| 907 |
1028 |
|
#[test]
|
| 908 |
1029 |
|
fn wcag_contrast_known_pairs() {
|
| 909 |
|
- |
let white = Rgb { r: 255, g: 255, b: 255 };
|
|
1030 |
+ |
let white = Rgb {
|
|
1031 |
+ |
r: 255,
|
|
1032 |
+ |
g: 255,
|
|
1033 |
+ |
b: 255,
|
|
1034 |
+ |
};
|
| 910 |
1035 |
|
let black = Rgb { r: 0, g: 0, b: 0 };
|
| 911 |
1036 |
|
assert!((wcag_contrast(white, black) - 21.0).abs() < 0.01);
|
| 912 |
1037 |
|
assert!((wcag_contrast(white, white) - 1.0).abs() < 0.01);
|
| 914 |
1039 |
|
|
| 915 |
1040 |
|
#[test]
|
| 916 |
1041 |
|
fn readable_on_picks_by_wcag() {
|
| 917 |
|
- |
assert_eq!(readable_on(Rgb { r: 255, g: 255, b: 255 }), Rgb { r: 0, g: 0, b: 0 });
|
| 918 |
|
- |
assert_eq!(readable_on(Rgb { r: 0, g: 0, b: 0 }), Rgb { r: 255, g: 255, b: 255 });
|
|
1042 |
+ |
assert_eq!(
|
|
1043 |
+ |
readable_on(Rgb {
|
|
1044 |
+ |
r: 255,
|
|
1045 |
+ |
g: 255,
|
|
1046 |
+ |
b: 255
|
|
1047 |
+ |
}),
|
|
1048 |
+ |
Rgb { r: 0, g: 0, b: 0 }
|
|
1049 |
+ |
);
|
|
1050 |
+ |
assert_eq!(
|
|
1051 |
+ |
readable_on(Rgb { r: 0, g: 0, b: 0 }),
|
|
1052 |
+ |
Rgb {
|
|
1053 |
+ |
r: 255,
|
|
1054 |
+ |
g: 255,
|
|
1055 |
+ |
b: 255
|
|
1056 |
+ |
}
|
|
1057 |
+ |
);
|
| 919 |
1058 |
|
// A light blue action -> black text reads better.
|
| 920 |
1059 |
|
let action = Rgb::from_hex("#6196ff").unwrap();
|
| 921 |
1060 |
|
assert_eq!(readable_on(action), Rgb { r: 0, g: 0, b: 0 });
|
| 1014 |
1153 |
|
let action = Rgb::from_hex("#81a1c1").unwrap();
|
| 1015 |
1154 |
|
let page = Rgb::from_hex("#2e3440").unwrap();
|
| 1016 |
1155 |
|
let _ = page;
|
| 1017 |
|
- |
assert_eq!(t.hex("action-hover").unwrap(), lighten(action, 0.05).to_hex());
|
| 1018 |
|
- |
assert_eq!(t.hex("content-on-action").unwrap(), readable_on(action).to_hex());
|
|
1156 |
+ |
assert_eq!(
|
|
1157 |
+ |
t.hex("action-hover").unwrap(),
|
|
1158 |
+ |
lighten(action, 0.05).to_hex()
|
|
1159 |
+ |
);
|
|
1160 |
+ |
assert_eq!(
|
|
1161 |
+ |
t.hex("content-on-action").unwrap(),
|
|
1162 |
+ |
readable_on(action).to_hex()
|
|
1163 |
+ |
);
|
| 1019 |
1164 |
|
assert_eq!(t.hex("focus-ring"), Some("#81a1c1"));
|
| 1020 |
1165 |
|
assert_eq!(t.hex("hover-surface"), Some("#434c5e")); // = surface.sunken
|
| 1021 |
1166 |
|
// Pruned by the usage audit (0 consumers): action-active, the *-surface
|
| 1032 |
1177 |
|
let theme = parse_theme_str("nord", nord_toml(), false).unwrap();
|
| 1033 |
1178 |
|
let t = resolve(&theme);
|
| 1034 |
1179 |
|
let overlay = t.hex("overlay").unwrap();
|
| 1035 |
|
- |
assert!(overlay.starts_with("rgba("), "overlay is translucent: {overlay}");
|
|
1180 |
+ |
assert!(
|
|
1181 |
+ |
overlay.starts_with("rgba("),
|
|
1182 |
+ |
"overlay is translucent: {overlay}"
|
|
1183 |
+ |
);
|
| 1036 |
1184 |
|
assert!(overlay.ends_with(", 0.5)"));
|
| 1037 |
1185 |
|
// The scrim tone is anchored very dark regardless of theme.
|
| 1038 |
|
- |
let inner = overlay.trim_start_matches("rgba(").trim_end_matches(", 0.5)");
|
|
1186 |
+ |
let inner = overlay
|
|
1187 |
+ |
.trim_start_matches("rgba(")
|
|
1188 |
+ |
.trim_end_matches(", 0.5)");
|
| 1039 |
1189 |
|
let parts: Vec<u8> = inner.split(", ").map(|p| p.parse().unwrap()).collect();
|
| 1040 |
|
- |
let scrim = Rgb { r: parts[0], g: parts[1], b: parts[2] };
|
|
1190 |
+ |
let scrim = Rgb {
|
|
1191 |
+ |
r: parts[0],
|
|
1192 |
+ |
g: parts[1],
|
|
1193 |
+ |
b: parts[2],
|
|
1194 |
+ |
};
|
| 1041 |
1195 |
|
assert!(scrim.to_oklab().l < 0.2, "scrim must be near-black");
|
| 1042 |
1196 |
|
}
|
| 1043 |
1197 |
|
|
| 1053 |
1207 |
|
)
|
| 1054 |
1208 |
|
.unwrap();
|
| 1055 |
1209 |
|
let t = resolve(&theme);
|
| 1056 |
|
- |
assert!(t.hex("surface-page").is_none(), "non-hex base intent leaked");
|
|
1210 |
+ |
assert!(
|
|
1211 |
+ |
t.hex("surface-page").is_none(),
|
|
1212 |
+ |
"non-hex base intent leaked"
|
|
1213 |
+ |
);
|
| 1057 |
1214 |
|
assert_eq!(t.hex("content").unwrap(), "#111111");
|
| 1058 |
1215 |
|
// The injected markup appears in no resolved value.
|
| 1059 |
1216 |
|
assert!(!t.intents.values().any(|v| v.contains('<')));
|
| 1072 |
1229 |
|
assert!(t.hex("action").is_none());
|
| 1073 |
1230 |
|
assert!(t.hex("action-hover").is_none());
|
| 1074 |
1231 |
|
assert!(t.hex("selection").is_none());
|
| 1075 |
|
- |
assert_eq!(t.hex("border-strong").unwrap(), darken(Rgb::from_hex("#222222").unwrap(), 0.05).to_hex());
|
|
1232 |
+ |
assert_eq!(
|
|
1233 |
+ |
t.hex("border-strong").unwrap(),
|
|
1234 |
+ |
darken(Rgb::from_hex("#222222").unwrap(), 0.05).to_hex()
|
|
1235 |
+ |
);
|
| 1076 |
1236 |
|
}
|
| 1077 |
1237 |
|
|
| 1078 |
1238 |
|
#[test]
|
| 1130 |
1290 |
|
let d2 = tempfile::tempdir().unwrap();
|
| 1131 |
1291 |
|
fs::write(d1.path().join("s.toml"), "[meta]\n").unwrap();
|
| 1132 |
1292 |
|
fs::write(d2.path().join("s.toml"), "[meta]\n").unwrap();
|
| 1133 |
|
- |
let dirs = vec![(d1.path().to_path_buf(), false), (d2.path().to_path_buf(), true)];
|
|
1293 |
+ |
let dirs = vec![
|
|
1294 |
+ |
(d1.path().to_path_buf(), false),
|
|
1295 |
+ |
(d2.path().to_path_buf(), true),
|
|
1296 |
+ |
];
|
| 1134 |
1297 |
|
let (path, is_custom) = find_theme_path(&dirs, "s").unwrap();
|
| 1135 |
1298 |
|
assert!(is_custom);
|
| 1136 |
1299 |
|
assert_eq!(path, d2.path().join("s.toml"));
|
| 1229 |
1392 |
|
fn adapted_themes_carry_inline_attribution() {
|
| 1230 |
1393 |
|
// Each adapted file must name its upstream in-file, so the credit
|
| 1231 |
1394 |
|
// survives someone copying a single .toml out of the crate.
|
| 1232 |
|
- |
const ORIGINALS: [&str; 5] =
|
| 1233 |
|
- |
["makenotwork", "goingson", "audiofiles", "high-contrast", "neobrute"];
|
|
1395 |
+ |
const ORIGINALS: [&str; 5] = [
|
|
1396 |
+ |
"makenotwork",
|
|
1397 |
+ |
"goingson",
|
|
1398 |
+ |
"audiofiles",
|
|
1399 |
+ |
"high-contrast",
|
|
1400 |
+ |
"neobrute",
|
|
1401 |
+ |
];
|
| 1234 |
1402 |
|
for (id, source) in embedded_themes() {
|
| 1235 |
1403 |
|
if ORIGINALS.contains(&id) {
|
| 1236 |
1404 |
|
continue;
|
| 1259 |
1427 |
|
Some(path.file_stem()?.to_str()?.to_string())
|
| 1260 |
1428 |
|
})
|
| 1261 |
1429 |
|
.collect();
|
| 1262 |
|
- |
let mut embedded: Vec<String> =
|
| 1263 |
|
- |
embedded_themes().map(|(id, _)| id.to_string()).collect();
|
|
1430 |
+ |
let mut embedded: Vec<String> = embedded_themes().map(|(id, _)| id.to_string()).collect();
|
| 1264 |
1431 |
|
on_disk.sort();
|
| 1265 |
1432 |
|
embedded.sort();
|
| 1266 |
1433 |
|
assert_eq!(embedded, on_disk, "embedded theme set drifted from themes/");
|
| 1287 |
1454 |
|
let dir = bundled_themes_dir().unwrap();
|
| 1288 |
1455 |
|
let dirs = vec![(dir.clone(), false)];
|
| 1289 |
1456 |
|
let themes = list_themes_from_dirs(&dirs);
|
| 1290 |
|
- |
assert!(themes.len() >= 30, "expected the full theme set, got {}", themes.len());
|
|
1457 |
+ |
assert!(
|
|
1458 |
+ |
themes.len() >= 30,
|
|
1459 |
+ |
"expected the full theme set, got {}",
|
|
1460 |
+ |
themes.len()
|
|
1461 |
+ |
);
|
| 1291 |
1462 |
|
for meta in &themes {
|
| 1292 |
1463 |
|
load_theme(&dirs, &meta.id)
|
| 1293 |
1464 |
|
.unwrap_or_else(|e| panic!("shipped theme `{}` failed to load: {e}", meta.id));
|