Skip to main content

max / makeover-layout

Remove State::Focus and name reach, focus and the focus ring Focus is decided per renderer, after the description, because what focus is differs per host: a browser has document.activeElement, a terminal has an index the runtime moves on Tab, egui has its own id-based focus. Every renderer had already reached that conclusion privately and none of them said so. The crate header now carries the three terms and who owns each, since this is where the deleted variant was and so where a reader looks. The one-ring rule the variant's doc carried moves to the focus-ring token in makeover, which makeover-immediate depends on being stated somewhere. Breaking: 0.18.0 -> 0.19.0.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-13 13:54 UTC
Signed with PGP, not checked
Commit: 5b168a2e780f704f95522c13771b4ea4b1652a70
Parent: 104788a
2 files changed, +39 insertions, -28 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.18.0"
3 + version = "0.19.0"
4 4 edition = "2024"
5 5 # One copy of this vocabulary per dependency graph, enforced by cargo rather
6 6 # than by remembering. Two versions of a description layer in one build means
M src/lib.rs +38 -27
@@ -199,6 +199,25 @@
199 199 //! goes. That is the boundary this crate is defined by, and four findings moved
200 200 //! across it rather than being answered here.
201 201 //!
202 + //! 0.19.0 narrows [`State`] to [`State::Disabled`] alone. `State::Focus` is
203 + //! gone: a description never states what has focus, because what focus *is*
204 + //! differs per host and every renderer had already decided for itself — the
205 + //! webview draws it from `:focus-visible`, egui refused the variant outright,
206 + //! and quasi-tui honoured it once at startup and overrode it thereafter.
207 + //!
208 + //! # Reach, focus and the focus ring
209 + //!
210 + //! Three terms, and no others, for what 0.19.0 moved out of the description.
211 + //! **Reach** is which things can take focus and in what order; a browser reads
212 + //! it off the document, a TUI derives it from draw order, egui from its own id
213 + //! stack. **Focus** is which reached thing has the keyboard right now: the
214 + //! renderer's, live, never described and never round-tripped through a
215 + //! description. The **focus ring** is the visible cue; the token (`focus-ring`,
216 + //! derived by `makeover` from the action colour) is the one shared artifact and
217 + //! the drawing is the renderer's. Retired as names for any of this: "focus
218 + //! stroke", "focus cue", "wants focus". "Caret" is a different thing — the text
219 + //! cursor inside a field — and keeps its name.
220 + //!
202 221 //! # Where the description stops
203 222 //!
204 223 //! The bespoke widgets, a day-plan timeline and a kanban board and a calendar,
@@ -491,23 +510,19 @@
491 510 /// # The principle this encodes
492 511 ///
493 512 /// A primitive owns every state it implies. A renderer that emits a hover rule
494 - /// for a thing owes disabled, focus and the capability answer for that same
495 - /// thing, because anything less exports the completion work to N consumers who
496 - /// will each do it differently.
513 + /// for a thing owes disabled and the capability answer for that same thing,
514 + /// because anything less exports the completion work to N consumers who will
515 + /// each do it differently.
516 + ///
517 + /// Focus is not on that list and was removed from this axis in 0.19.0. It is
518 + /// the renderer's, decided after the description; see the crate header, "Reach,
519 + /// focus and the focus ring", for the three terms and who owns each.
497 520 ///
498 521 /// `#[non_exhaustive]` for the reason [`Fill`] and [`Depth`] carry it: growth
499 522 /// must not be a lockstep event across the three renderers.
500 523 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
501 524 #[non_exhaustive]
502 525 pub enum State {
503 - /// Keyboard focus, as distinct from the pointer having landed on something.
504 - ///
505 - /// One ring, not one per primitive. Where the ring sits is [`Depth`]'s
506 - /// question and not a per-component choice: a well takes it inside its own
507 - /// edge and a raised surface takes it outside. That is one decision with
508 - /// two renderings rather than one decision per component, which is how the
509 - /// three apps ended up with three rings.
510 - Focus,
511 526 /// Present, visible, and not answering.
512 527 ///
513 528 /// Not the same as absent, and deliberately not a [`Fill`]: a disabled
@@ -525,9 +540,11 @@
525 540 /// means resolving it once per consumer and disagreeing.
526 541 #[must_use]
527 542 pub const fn suppresses_interaction(self) -> bool {
543 + // A match rather than a bare `true`, so a member added to this
544 + // `#[non_exhaustive]` axis has to answer the question rather than
545 + // inheriting an answer.
528 546 match self {
529 547 Self::Disabled => true,
530 - Self::Focus => false,
531 548 }
532 549 }
533 550 }
@@ -535,10 +552,6 @@
535 552 impl Intent for State {
536 553 fn token(self) -> &'static str {
537 554 match self {
538 - // Already derived by `makeover` from `action.primary`, and unused
539 - // until now for the same reason `hover-surface` was: nothing
540 - // emitted the rule that would consume it.
541 - Self::Focus => "focus-ring",
542 555 // Reusing the muted content intent rather than minting a
543 556 // `disabled` colour. Disabled is a reduction and not a status, and
544 557 // `makeover-webview`'s progress rules already record the reading
@@ -2305,11 +2318,11 @@
2305 2318
2306 2319 #[test]
2307 2320 fn an_act_is_reachable_until_it_is_disabled() {
2308 - // The one member a renderer must branch on. Focus is a state and does
2309 - // not stop the control answering, which is the distinction `State`
2310 - // makes and every hand-rolled button in the tree had to remember.
2321 + // The one member a renderer must branch on, and since 0.19.0 the only
2322 + // member there is. A stated state is not by itself a reason to stop
2323 + // answering, which is the distinction `State` makes and every
2324 + // hand-rolled button in the tree had to remember.
2311 2325 assert!(!Act::new("Save").disabled());
2312 - assert!(!Act::new("Save").state(State::Focus).disabled());
2313 2326 assert!(Act::new("Save").state(State::Disabled).disabled());
2314 2327 }
2315 2328
@@ -2369,16 +2382,15 @@
2369 2382
2370 2383 #[test]
2371 2384 fn only_disabled_stops_answering() {
2372 - // Focus is a thing you can still click. Getting this backwards is how
2373 - // a focus ring ends up on something inert.
2374 - assert!(!State::Focus.suppresses_interaction());
2385 + // Kept in spirit from the version where `Focus` was the counter-example:
2386 + // suppressing interaction is `Disabled`'s alone, so a member added here
2387 + // later does not get to inherit it by being a state.
2375 2388 assert!(State::Disabled.suppresses_interaction());
2376 2389 }
2377 2390
2378 2391 #[test]
2379 - fn both_states_resolve_against_intents_makeover_already_derives() {
2380 - // Neither needs a new token, so this costs no `makeover` release.
2381 - assert_eq!(State::Focus.token(), "focus-ring");
2392 + fn disabled_resolves_against_an_intent_makeover_already_derives() {
2393 + // No new token, so this costs no `makeover` release.
2382 2394 assert_eq!(State::Disabled.token(), "content-muted");
2383 2395 }
2384 2396
@@ -2486,7 +2498,6 @@
2486 2498 Edge::Dark.token(),
2487 2499 Tone::Danger.token(),
2488 2500 Tone::Neutral.token(),
2489 - State::Focus.token(),
2490 2501 State::Disabled.token(),
2491 2502 ] {
2492 2503 assert!(!t.starts_with('#'), "{t} looks like a value");