Skip to main content

max / makeover-geometry

0.5.0: Density::media_condition density_css spelled the touch condition inline, so there was nothing for another crate to share and makeover-webview would have had to write its own to gate a hover rule. That is how the pair drifts, and it drifts silently: a wrong condition still parses, still minifies, and shows up only as hover states surviving on a phone. The two are not each other's textual negation, which is the reason they belong in one place. Touch is comma-joined and therefore an OR, so not-touch is an AND with both halves inverted. Symmetric with SizeClass::media_condition, which already existed.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-02 15:34 UTC
Signed with PGP, not checked
Commit: ba22bf5ffe7f1f0eb86348dc3d2e2293e3699f75
Parent: 09d9058
2 files changed, +55 insertions, -2 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-geometry"
3 - version = "0.4.0"
3 + version = "0.5.0"
4 4 edition = "2024"
5 5 description = "The invariant half of the make-family design system: relational spacing, radius, border width and type scale. Geometry never varies by theme, which is why it does not live in makeover."
6 6 license = "MIT"
M src/lib.rs +54 -1
@@ -319,6 +319,33 @@
319 319 Touch,
320 320 }
321 321
322 + impl Density {
323 + /// The media condition selecting exactly this density, without the
324 + /// `@media`.
325 + ///
326 + /// A capability question rather than a width or a device, which is the
327 + /// policy this crate already settled for [`density_css`] and which three
328 + /// consumers previously answered three ways.
329 + ///
330 + /// The two are **not** each other's textual negation, and that is the
331 + /// reason they live in one place. Touch is comma-joined, so it is an OR,
332 + /// and negating an OR gives an AND with both halves inverted. Deriving one
333 + /// from the other by eye is how the pair drifts apart, and it drifts
334 + /// silently: a wrong negation still parses, still minifies, and only shows
335 + /// up as hover states surviving on a phone.
336 + ///
337 + /// Pointer's condition is what a renderer wraps a hover rule in.
338 + /// `makeover-touch` decides *whether* a hover rule should be gated;
339 + /// this decides what the gate is spelled as.
340 + #[must_use]
341 + pub const fn media_condition(self) -> &'static str {
342 + match self {
343 + Self::Pointer => "(hover: hover) and (pointer: fine)",
344 + Self::Touch => "(hover: none), (pointer: coarse)",
345 + }
346 + }
347 + }
348 +
322 349 /// How much screen there is, independent of what is pointing at it.
323 350 ///
324 351 /// The second axis, and the one [`Density`] kept being asked to carry. A phone
@@ -685,7 +712,9 @@
685 712 pub fn density_css(explicit_touch: Option<&str>) -> String {
686 713 let mut css = geometry_css_vars(Density::Pointer);
687 714 css.push_str("\n/* Touch: targets separate, shells hold. */\n");
688 - css.push_str("@media (hover: none), (pointer: coarse) {\n");
715 + css.push_str("@media ");
716 + css.push_str(Density::Touch.media_condition());
717 + css.push_str(" {\n");
689 718 for line in gap_css_overrides(":root", Density::Touch).lines() {
690 719 css.push_str(" ");
691 720 css.push_str(line);
@@ -729,6 +758,30 @@
729 758 assert!(!css.contains("ui-mode"), "a device mode crept in");
730 759 }
731 760
761 + #[test]
762 + fn the_two_density_conditions_are_complements_and_not_negations() {
763 + let pointer = Density::Pointer.media_condition();
764 + let touch = Density::Touch.media_condition();
765 +
766 + // Both halves are inverted, feature for feature.
767 + assert!(pointer.contains("hover: hover") && touch.contains("hover: none"));
768 + assert!(pointer.contains("pointer: fine") && touch.contains("pointer: coarse"));
769 +
770 + // And the joins are inverted too, which is the part that gets written
771 + // wrong by hand: touch is an OR, so not-touch is an AND. A pointer
772 + // condition joined with a comma would match every touchscreen.
773 + assert!(touch.contains(", "), "touch must be an OR");
774 + assert!(pointer.contains(" and "), "pointer must be an AND");
775 + assert!(!pointer.contains(','), "pointer must not be an OR");
776 + }
777 +
778 + #[test]
779 + fn the_emitted_touch_block_is_the_condition_and_not_a_second_copy_of_it() {
780 + // The literal used to be inline here. Nothing may re-spell it.
781 + let css = density_css(None);
782 + assert!(css.contains(&format!("@media {}", Density::Touch.media_condition())));
783 + }
784 +
732 785 #[test]
733 786 fn an_explicit_choice_is_emitted_after_the_detection() {
734 787 let css = density_css(Some(".ui-mode-mobile"));