| 2 |
2 |
|
use std::fs;
|
| 3 |
3 |
|
use std::path::Path;
|
| 4 |
4 |
|
|
| 5 |
|
- |
use makeover_geometry::SizeClass;
|
|
5 |
+ |
use makeover_geometry::{Density, SizeClass};
|
| 6 |
6 |
|
use makeover_layout::{Column, Priority, Width};
|
| 7 |
7 |
|
use makeover_webview::Emit;
|
| 8 |
8 |
|
use makeover_webview::list::{Sizing, narrowing_css};
|
| 463 |
463 |
|
}
|
| 464 |
464 |
|
}
|
| 465 |
465 |
|
|
|
466 |
+ |
/// Fail the build if a JS copy of the touch-density query has drifted from
|
|
467 |
+ |
/// [`Density::Touch`].
|
|
468 |
+ |
///
|
|
469 |
+ |
/// The sibling of [`check_breakpoints`], for the other axis and for the same
|
|
470 |
+ |
/// reason. Density is a capability question -- what is pointing at the screen
|
|
471 |
+ |
/// -- and until 2026-08-10 touch.js answered it with
|
|
472 |
+ |
/// `('ontouchstart' in window) || navigator.maxTouchPoints > 0`, which asks the
|
|
473 |
+ |
/// hardware instead and says yes to a touchscreen laptop driving a mouse. It
|
|
474 |
+ |
/// now asks `matchMedia` with the crate's own condition, which is the string
|
|
475 |
+ |
/// the generated geometry.css already keys the touch gap overrides on, so the
|
|
476 |
+ |
/// gestures and the spacing agree by construction.
|
|
477 |
+ |
///
|
|
478 |
+ |
/// Two files state the string rather than one, so this is what keeps them
|
|
479 |
+ |
/// honest. An assertion and not a substitution, same as the breakpoints: a JS
|
|
480 |
+ |
/// file that has to be generated to be correct stops being readable on its own.
|
|
481 |
+ |
fn check_touch_density(frontend: &Path) {
|
|
482 |
+ |
let want = Density::Touch.media_condition();
|
|
483 |
+ |
let mut wrong: Vec<String> = Vec::new();
|
|
484 |
+ |
let mut found = 0usize;
|
|
485 |
+ |
|
|
486 |
+ |
let js_dir = frontend.join("js");
|
|
487 |
+ |
let mut js_files: Vec<_> = fs::read_dir(&js_dir)
|
|
488 |
+ |
.expect("read js/")
|
|
489 |
+ |
.filter_map(Result::ok)
|
|
490 |
+ |
.map(|e| e.path())
|
|
491 |
+ |
.filter(|p| p.extension().is_some_and(|x| x == "js"))
|
|
492 |
+ |
.collect();
|
|
493 |
+ |
js_files.sort();
|
|
494 |
+ |
|
|
495 |
+ |
for path in &js_files {
|
|
496 |
+ |
let src = fs::read_to_string(path).expect("read js file");
|
|
497 |
+ |
let name = path.file_name().unwrap().to_string_lossy();
|
|
498 |
+ |
|
|
499 |
+ |
for (offset, literal) in touch_density_literals(&src) {
|
|
500 |
+ |
found += 1;
|
|
501 |
+ |
if literal != want {
|
|
502 |
+ |
wrong.push(format!(
|
|
503 |
+ |
" js/{name}:{} TOUCH_DENSITY = '{literal}'",
|
|
504 |
+ |
line_of(&src, offset)
|
|
505 |
+ |
));
|
|
506 |
+ |
}
|
|
507 |
+ |
}
|
|
508 |
+ |
|
|
509 |
+ |
// The sniff this replaced, so it cannot come back by copy-paste.
|
|
510 |
+ |
for needle in ["ontouchstart", "maxTouchPoints"] {
|
|
511 |
+ |
if let Some(offset) = src.find(needle) {
|
|
512 |
+ |
wrong.push(format!(
|
|
513 |
+ |
" js/{name}:{} {needle} -- device sniff, not a density question",
|
|
514 |
+ |
line_of(&src, offset)
|
|
515 |
+ |
));
|
|
516 |
+ |
}
|
|
517 |
+ |
}
|
|
518 |
+ |
}
|
|
519 |
+ |
|
|
520 |
+ |
assert!(
|
|
521 |
+ |
found > 0,
|
|
522 |
+ |
"no TOUCH_DENSITY literal found in frontend/js.\n\n\
|
|
523 |
+ |
touch.js and haptics.js each state makeover_geometry::Density::Touch's\n\
|
|
524 |
+ |
media condition in a const of that name, and this check exists to keep\n\
|
|
525 |
+ |
them equal to it. If the const was renamed, rename it here too rather\n\
|
|
526 |
+ |
than dropping the check."
|
|
527 |
+ |
);
|
|
528 |
+ |
|
|
529 |
+ |
assert!(
|
|
530 |
+ |
wrong.is_empty(),
|
|
531 |
+ |
"hand-written touch detection disagrees with makeover_geometry::Density.\n\n\
|
|
532 |
+ |
Density::Touch.media_condition() is: {want}\n\n\
|
|
533 |
+ |
Wrong:\n{}\n\n\
|
|
534 |
+ |
Fix the JS to state the crate's string. Never widen it to catch a\n\
|
|
535 |
+ |
device the query misses: density is what is pointing at the screen,\n\
|
|
536 |
+ |
and a laptop with a touchscreen and a mouse is a pointer device.",
|
|
537 |
+ |
wrong.join("\n")
|
|
538 |
+ |
);
|
|
539 |
+ |
|
|
540 |
+ |
for path in &js_files {
|
|
541 |
+ |
println!("cargo:rerun-if-changed={}", path.display());
|
|
542 |
+ |
}
|
|
543 |
+ |
}
|
|
544 |
+ |
|
|
545 |
+ |
/// `(byte offset of the literal, its contents)` for every
|
|
546 |
+ |
/// `const TOUCH_DENSITY = '...'` in a JS source.
|
|
547 |
+ |
fn touch_density_literals(src: &str) -> Vec<(usize, &str)> {
|
|
548 |
+ |
let mut out = Vec::new();
|
|
549 |
+ |
let mut at = 0;
|
|
550 |
+ |
while let Some(i) = src[at..].find("TOUCH_DENSITY") {
|
|
551 |
+ |
let start = at + i;
|
|
552 |
+ |
at = start + "TOUCH_DENSITY".len();
|
|
553 |
+ |
// Only the declaration states the string; a use site reads the const.
|
|
554 |
+ |
let Some(rest) = src[at..].strip_prefix(" = ") else {
|
|
555 |
+ |
continue;
|
|
556 |
+ |
};
|
|
557 |
+ |
let open = at + " = ".len();
|
|
558 |
+ |
let quote = match rest.chars().next() {
|
|
559 |
+ |
Some(q @ ('\'' | '"')) => q,
|
|
560 |
+ |
_ => continue,
|
|
561 |
+ |
};
|
|
562 |
+ |
let body = open + 1;
|
|
563 |
+ |
if let Some(j) = src[body..].find(quote) {
|
|
564 |
+ |
out.push((start, &src[body..body + j]));
|
|
565 |
+ |
at = body + j + 1;
|
|
566 |
+ |
}
|
|
567 |
+ |
}
|
|
568 |
+ |
out
|
|
569 |
+ |
}
|
|
570 |
+ |
|
| 466 |
571 |
|
/// `(byte offset of the `@media`, the condition text before the `{`)`.
|
| 467 |
572 |
|
fn media_conditions(css: &str) -> Vec<(usize, &str)> {
|
| 468 |
573 |
|
let mut out = Vec::new();
|
| 553 |
658 |
|
// The generated files above cannot drift from SizeClass. The hand-written
|
| 554 |
659 |
|
// ones can, so they are checked rather than trusted.
|
| 555 |
660 |
|
check_breakpoints(&frontend);
|
|
661 |
+ |
check_touch_density(&frontend);
|
| 556 |
662 |
|
|
| 557 |
663 |
|
println!("cargo:rerun-if-changed=build.rs");
|
| 558 |
664 |
|
|