| 47 |
47 |
|
//! The mobile and desktop builds of a Tauri app should differ mostly by which
|
| 48 |
48 |
|
//! preset they emit, not by a parallel set of hand-written mode-scoped rules.
|
| 49 |
49 |
|
//!
|
| 50 |
|
- |
//! **[`Density::Touch`] has no preset of its own right now.** It resolves to
|
| 51 |
|
- |
//! the same steps as [`Density::Pointer`] until one is derived. The previous
|
| 52 |
|
- |
//! Touch numbers were thrown out on 2026-07-29: they opened `Peer` and
|
| 53 |
|
- |
//! `Section` while tightening `Pane` and `Page`, which put Touch's `Section` at
|
| 54 |
|
- |
//! 16 and made any Pointer `Pane` at or below 16 an inversion. That blocked the
|
| 55 |
|
- |
//! Pointer retune to `pane 14 / page 16` and the values were derived rather
|
| 56 |
|
- |
//! than quoted, so the preset goes rather than the retune. The axis stays: the
|
| 57 |
|
- |
//! [`Density`] argument, [`density_css`] and every consumer's mode-scoped block
|
| 58 |
|
- |
//! are all still wired, and a re-derived preset lands here without touching a
|
| 59 |
|
- |
//! call site. That is the claim this section opens with, demonstrated the hard
|
| 60 |
|
- |
//! way.
|
|
50 |
+ |
//! ## What Touch is a claim about
|
|
51 |
+ |
//!
|
|
52 |
+ |
//! Touch is a claim about the **contact patch and nothing else**. A fingertip
|
|
53 |
+ |
//! is coarse where a cursor hotspot is a point, and the only consequence of
|
|
54 |
+ |
//! that is mis-tap cost: when two adjacent things do different things, an
|
|
55 |
+ |
//! imprecise contact needs more room between them to land on the intended one.
|
|
56 |
+ |
//!
|
|
57 |
+ |
//! Shells are not tap targets. Panel padding and the outer page margin separate
|
|
58 |
+ |
//! a region from the edge of the screen, and no amount of coarseness in the
|
|
59 |
+ |
//! pointing device makes that separation riskier. So **Touch opens the gaps
|
|
60 |
+ |
//! that separate targets and leaves the shells exactly where Pointer put
|
|
61 |
+ |
//! them**:
|
|
62 |
+ |
//!
|
|
63 |
+ |
//! | Gap | Pointer | Touch | why |
|
|
64 |
+ |
//! |---|---|---|---|
|
|
65 |
+ |
//! | bound | 4 | 4 | not a separation at all |
|
|
66 |
+ |
//! | peer | 6 | 10 | adjacent distinct targets, the whole point |
|
|
67 |
+ |
//! | group | 10 | 12 | holds the peer/group distinction open |
|
|
68 |
+ |
//! | section | 12 | 16 | a deliberate break stays legible as one |
|
|
69 |
+ |
//! | pane | 24 | 24 | a shell is not a target |
|
|
70 |
+ |
//! | page | 32 | 32 | a shell is not a target |
|
|
71 |
+ |
//!
|
|
72 |
+ |
//! The previous Touch preset was thrown out on 2026-07-29 because it also
|
|
73 |
+ |
//! *tightened* `Pane` and `Page`, on the argument that outer margin is screen
|
|
74 |
+ |
//! you do not get. **That is a claim about screen budget, not about the input
|
|
75 |
+ |
//! device**, and smuggling it into this axis is what broke: opening `Section`
|
|
76 |
+ |
//! to 16 while tightening `Pane` below it made any Pointer `Pane` at or under
|
|
77 |
+ |
//! 16 an inversion, so a derived preset silently set a floor under the one
|
|
78 |
+ |
//! quoted from the HIG. A phone is small *and* touch; a tablet and a
|
|
79 |
+ |
//! touchscreen laptop are big and touch. Screen budget is a separate axis and
|
|
80 |
+ |
//! does not belong here.
|
|
81 |
+ |
//!
|
|
82 |
+ |
//! ## The one cross-density rule
|
|
83 |
+ |
//!
|
|
84 |
+ |
//! **Touch never resolves tighter than Pointer**, at any gap. Stated as a
|
|
85 |
+ |
//! deliberate claim rather than inherited, and chosen for its direction: it
|
|
86 |
+ |
//! constrains the *derived* preset by the *quoted* one, never the reverse. A
|
|
87 |
+ |
//! Pointer retune downward moves freely and cannot be blocked by Touch, which
|
|
88 |
+ |
//! is the exact failure this replaces. Only a Pointer move upward can push
|
|
89 |
+ |
//! Touch, and that is the correct direction of authority.
|
| 61 |
90 |
|
//!
|
| 62 |
91 |
|
//! # Surfaces, and why a TUI is not a third density
|
| 63 |
92 |
|
//!
|
| 262 |
291 |
|
/// Mouse or trackpad. Resolves to the Mac OS 8 HIG's own proportions.
|
| 263 |
292 |
|
#[default]
|
| 264 |
293 |
|
Pointer,
|
| 265 |
|
- |
/// Finger. **No preset of its own yet**: resolves to [`Self::Pointer`]'s
|
| 266 |
|
- |
/// steps until one is derived. See the crate-level "Density presets"
|
| 267 |
|
- |
/// section for why the previous one was thrown out.
|
|
294 |
+ |
/// Finger. Opens the gaps that separate distinct tap targets and leaves
|
|
295 |
+ |
/// the shells where Pointer put them, because a coarse contact patch
|
|
296 |
+ |
/// raises mis-tap cost and a panel margin is not something you tap. See
|
|
297 |
+ |
/// the crate-level "Density presets" section for the derivation.
|
| 268 |
298 |
|
Touch,
|
| 269 |
299 |
|
}
|
| 270 |
300 |
|
|
| 388 |
418 |
|
impl Gap {
|
| 389 |
419 |
|
/// The step this relationship resolves to at a given density.
|
| 390 |
420 |
|
///
|
| 391 |
|
- |
/// Only [`Density::Pointer`] has values, and they are the HIG's own.
|
| 392 |
|
- |
/// [`Density::Touch`] was thrown out on 2026-07-29 and falls through to the
|
| 393 |
|
- |
/// same steps; the `density` argument is kept because the axis is still
|
| 394 |
|
- |
/// real, and re-deriving Touch means adding arms here and nothing else.
|
|
421 |
+ |
/// [`Density::Pointer`]'s values are the HIG's own. [`Density::Touch`]
|
|
422 |
+ |
/// opens the three gaps that separate distinct tap targets and holds the
|
|
423 |
+ |
/// rest, per the crate-level "Density presets" section.
|
| 395 |
424 |
|
#[must_use]
|
| 396 |
425 |
|
pub const fn step_at(self, density: Density) -> Step {
|
| 397 |
|
- |
let _ = density;
|
| 398 |
426 |
|
match self {
|
| 399 |
|
- |
// Binding is not a separation, so it would not open up on touch
|
| 400 |
|
- |
// either. The one arm a re-derived preset should leave alone.
|
|
427 |
+ |
// Binding is not a separation, so it does not open up on touch
|
|
428 |
+ |
// either: separating these would say they are two objects.
|
| 401 |
429 |
|
Self::Bound => Step::Tight,
|
| 402 |
430 |
|
|
| 403 |
|
- |
Self::Peer => Step::Snug,
|
| 404 |
|
- |
Self::Group => Step::Roomy,
|
| 405 |
|
- |
Self::Section => Step::Wide,
|
|
431 |
+ |
// The three that carry mis-tap cost. Peer is the one that matters
|
|
432 |
+ |
// most (stacked rows, adjacent chips) and moves furthest; Group
|
|
433 |
+ |
// and Section follow only far enough to stay distinct from it.
|
|
434 |
+ |
Self::Peer => match density {
|
|
435 |
+ |
Density::Pointer => Step::Snug,
|
|
436 |
+ |
Density::Touch => Step::Roomy,
|
|
437 |
+ |
},
|
|
438 |
+ |
Self::Group => match density {
|
|
439 |
+ |
Density::Pointer => Step::Roomy,
|
|
440 |
+ |
Density::Touch => Step::Wide,
|
|
441 |
+ |
},
|
|
442 |
+ |
Self::Section => match density {
|
|
443 |
+ |
Density::Pointer => Step::Wide,
|
|
444 |
+ |
Density::Touch => Step::Loose,
|
|
445 |
+ |
},
|
|
446 |
+ |
|
|
447 |
+ |
// Shells. Not tap targets, so the contact patch has no opinion,
|
|
448 |
+ |
// and screen budget is a different axis than this one.
|
| 406 |
449 |
|
Self::Pane => Step::Broad,
|
| 407 |
450 |
|
Self::Page => Step::Vast,
|
| 408 |
451 |
|
}
|
| 528 |
571 |
|
#[must_use]
|
| 529 |
572 |
|
pub fn density_css(explicit_touch: Option<&str>) -> String {
|
| 530 |
573 |
|
let mut css = geometry_css_vars(Density::Pointer);
|
| 531 |
|
- |
// The block is emitted even though Touch currently resolves to Pointer's
|
| 532 |
|
- |
// steps, so it is a no-op repeat of the base layer. Deliberate: it keeps the
|
| 533 |
|
- |
// selection mechanism proven and in front of a reader, and a re-derived
|
| 534 |
|
- |
// Touch preset then reaches every consumer on a rebuild.
|
| 535 |
|
- |
css.push_str("\n/* Touch has no preset of its own yet: identical to the base layer. */\n");
|
|
574 |
+ |
css.push_str("\n/* Touch: targets separate, shells hold. */\n");
|
| 536 |
575 |
|
css.push_str("@media (hover: none), (pointer: coarse) {\n");
|
| 537 |
576 |
|
for line in gap_css_overrides(":root", Density::Touch).lines() {
|
| 538 |
577 |
|
css.push_str(" ");
|
| 646 |
685 |
|
}
|
| 647 |
686 |
|
|
| 648 |
687 |
|
#[test]
|
| 649 |
|
- |
fn touch_has_no_preset_of_its_own() {
|
| 650 |
|
- |
// Replaces touch_separates_targets_and_tightens_shells, which asserted
|
| 651 |
|
- |
// the derived preset thrown out on 2026-07-29: Peer and Section opened
|
| 652 |
|
- |
// while Pane and Page tightened. That put Touch's Section at 16 and made
|
| 653 |
|
- |
// any Pointer Pane at or below 16 an inversion, blocking the retune to
|
| 654 |
|
- |
// pane 14 / page 16.
|
| 655 |
|
- |
//
|
| 656 |
|
- |
// Asserted rather than left implicit so re-deriving Touch has to come
|
| 657 |
|
- |
// here and say so, the same way the theme degeneracy lists work.
|
| 658 |
|
- |
for gap in Gap::all() {
|
|
688 |
+ |
fn touch_separates_targets_and_holds_the_shells() {
|
|
689 |
+ |
// The derivation, asserted so that changing it has to come here and say
|
|
690 |
+ |
// so. Touch is a claim about the contact patch: the gaps between
|
|
691 |
+ |
// distinct tap targets open, and the gaps that are not tap targets do
|
|
692 |
+ |
// not move.
|
|
693 |
+ |
for gap in [Gap::Peer, Gap::Group, Gap::Section] {
|
|
694 |
+ |
assert!(
|
|
695 |
+ |
gap.px_at(Density::Touch) > gap.px_at(Density::Pointer),
|
|
696 |
+ |
"{gap:?} separates tap targets and must open on touch"
|
|
697 |
+ |
);
|
|
698 |
+ |
}
|
|
699 |
+ |
for gap in [Gap::Bound, Gap::Pane, Gap::Page] {
|
| 659 |
700 |
|
assert_eq!(
|
| 660 |
701 |
|
gap.px_at(Density::Touch),
|
| 661 |
702 |
|
gap.px_at(Density::Pointer),
|
| 662 |
|
- |
"{gap:?}: Touch resolves to Pointer until a preset is derived"
|
|
703 |
+ |
"{gap:?} is not a tap target and must not move with the input device"
|
| 663 |
704 |
|
);
|
| 664 |
705 |
|
}
|
| 665 |
706 |
|
}
|
| 666 |
707 |
|
|
|
708 |
+ |
#[test]
|
|
709 |
+ |
fn touch_never_resolves_tighter_than_pointer() {
|
|
710 |
+ |
// The one cross-density rule, and its direction is the point. The
|
|
711 |
+ |
// preset thrown out on 2026-07-29 tightened Pane and Page on touch,
|
|
712 |
+ |
// which combined with an opened Section to make any Pointer Pane at or
|
|
713 |
+ |
// below 16 an inversion: a derived preset set a floor under the one
|
|
714 |
+ |
// quoted from the HIG, blocking the retune to pane 14 / page 16.
|
|
715 |
+ |
//
|
|
716 |
+ |
// Constraining Touch by Pointer instead cannot do that. A Pointer
|
|
717 |
+ |
// retune downward moves freely; only a Pointer move upward pushes
|
|
718 |
+ |
// Touch, which is the correct direction of authority.
|
|
719 |
+ |
for gap in Gap::all() {
|
|
720 |
+ |
assert!(
|
|
721 |
+ |
gap.px_at(Density::Touch) >= gap.px_at(Density::Pointer),
|
|
722 |
+ |
"{gap:?}: Touch resolved tighter than Pointer"
|
|
723 |
+ |
);
|
|
724 |
+ |
}
|
|
725 |
+ |
}
|
|
726 |
+ |
|
|
727 |
+ |
#[test]
|
|
728 |
+ |
fn the_pointer_retune_is_not_blocked_by_touch() {
|
|
729 |
+ |
// Guards the specific regression above rather than trusting the general
|
|
730 |
+ |
// rule to imply it. Touch's own ordering must hold using Touch values
|
|
731 |
+ |
// only, so that a Pointer Pane at or below Touch's Section is legal.
|
|
732 |
+ |
assert!(
|
|
733 |
+ |
Gap::Section.px_at(Density::Touch) <= Gap::Pane.px_at(Density::Touch),
|
|
734 |
+ |
"Touch inverted internally, which is what set the old floor"
|
|
735 |
+ |
);
|
|
736 |
+ |
// The retune wants Pointer pane 14 / page 16, both under Touch's
|
|
737 |
+ |
// Section of 16. Nothing in this crate may object to that.
|
|
738 |
+ |
assert!(Gap::Section.px_at(Density::Touch) >= 16);
|
|
739 |
+ |
}
|
|
740 |
+ |
|
| 667 |
741 |
|
#[test]
|
| 668 |
742 |
|
fn a_terminal_resolves_the_vocabulary_to_whole_cells() {
|
| 669 |
743 |
|
let t = Surface::terminal();
|
| 783 |
857 |
|
#[test]
|
| 784 |
858 |
|
fn a_density_override_emits_only_the_relational_layer() {
|
| 785 |
859 |
|
let css = gap_css_overrides(".ui-mode-mobile", Density::Touch);
|
| 786 |
|
- |
// Whichever step it is: Touch has no preset of its own at the moment, so
|
| 787 |
|
- |
// this says the gap is emitted and references a step, not which one.
|
|
860 |
+ |
// Which step Peer lands on is the preset's business, asserted in
|
|
861 |
+ |
// touch_separates_targets_and_holds_the_shells. This says only that the
|
|
862 |
+ |
// gap is emitted and references a step.
|
| 788 |
863 |
|
assert!(css.contains("--gap-peer: var(--step-"));
|
| 789 |
864 |
|
// Referencing a step is the point; re-declaring one would fork the
|
| 790 |
865 |
|
// scale, so the check is on declarations, not on mentions.
|