Skip to main content

max / makeover-layout

0.42.0: merge Image into the router, and delete two dead types Three findings from the 2026-09-01 vocabulary audit, landing together because `links = "makeover-layout"` makes a partial migration a resolver error rather than a warning. - `Image` is gone. It carried the same five fields as the router's owned twin plus one method, `speaks()`, had no consumer anywhere in the makeover suite, and was built in exactly one place in the tree. What survives is the rule that produced the split, which `Act` still carries: this crate defers every address. - `Room` is gone. Nothing constructed, matched, stored or returned it. `Fallback`'s doc says "when it runs out of room" rather than linking a type that no longer exists. - `FacetValue::depth` carries `Nesting` rather than a bare `u8`, which is what `edf33114` ruled and the last carrier still holding a number.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01MptwXZ8k65v19rFmdGAyki
Author: Max Johnson <me@maxj.phd> · 2026-09-01 21:03 UTC
Signed with PGP, not checked
Commit: f2059d0f37be87d7a4c521cde2a2fc2b21660c0a
Parent: 315d9b4
2 files changed, +12 insertions, -238 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.41.2"
3 + version = "0.42.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 +11 -237
@@ -29,7 +29,7 @@
29 29 //! over a recessed fill is unrepresentable.
30 30 //! - **Components.** [`Token`] (badge against chip), [`Notice`] (toast against
31 31 //! banner), [`RowPart`], [`CellPart`], [`Heading`], [`Selector`],
32 - //! [`Readiness`], [`Awaiting`], [`Meter`], [`Figure`], [`Image`], [`Track`],
32 + //! [`Readiness`], [`Awaiting`], [`Meter`], [`Figure`], [`Track`],
33 33 //! and [`Tone`], the one intent family they share.
34 34 //! - **Schemas.** [`Field`] for forms, [`Column`] for lists and tables,
35 35 //! [`Facet`] for the dimensions a set is narrowed by.
@@ -58,7 +58,7 @@
58 58 //! # What this crate cannot say
59 59 //!
60 60 //! - **An address.** What a control calls, and where a button goes. [`Act`]
61 - //! names the act and holds no destination; [`Image`] carries no source.
61 + //! names the act and holds no destination.
62 62 //! - **A current value.** A webview reads it out of the DOM and an
63 63 //! immediate-mode renderer holds a `&mut` to the app's own field. A
64 64 //! description carrying it would be a form model.
@@ -1448,73 +1448,6 @@
1448 1448 Contain,
1449 1449 }
1450 1450
1451 - /// A picture, and what it says to someone who is not looking at it.
1452 - ///
1453 - /// # No source
1454 - ///
1455 - /// [`Act`]'s split, for [`Act`]'s reason. A source is an address, and this
1456 - /// crate has no notion of an address: it says what a thing *is* and the caller
1457 - /// keeps what it points at.
1458 - ///
1459 - /// It matters more here than it does for a control, because a picture is the
1460 - /// one member where the address is most of what a webview needs and *none* of
1461 - /// what the description knows. `quasi_router::Node::Image` carries the URL, the
1462 - /// way it carries an `Action` for a control.
1463 - ///
1464 - /// # Why [`alt`](Self::alt) is not optional
1465 - ///
1466 - /// Every other host has to draw something, and for two of the three the alt
1467 - /// text is not a fallback but the whole rendering: a terminal without a
1468 - /// graphics protocol has the words and nothing else. Making it optional would
1469 - /// make "this picture is invisible on a terminal" the default, and the
1470 - /// description would be carrying a webview assumption in its shape.
1471 - ///
1472 - /// An image that genuinely says nothing — a rule, a spacer, a decoration
1473 - /// repeating what the text beside it already said — is an empty `alt`, which is
1474 - /// the same thing HTML means by it and is a claim rather than an oversight.
1475 - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1476 - pub struct Image<'a> {
1477 - /// What the picture says, for anything not showing it.
1478 - ///
1479 - /// Empty means the picture is decorative and adds nothing to the text
1480 - /// around it. See the type's own docs on why this is not an `Option`.
1481 - pub alt: &'a str,
1482 - /// A visible line under the picture, where the app wants one.
1483 - ///
1484 - /// Distinct from [`alt`](Self::alt) and the difference is who it is for: a
1485 - /// caption is content everybody reads, alt text is what stands in for the
1486 - /// picture. A screenshot captioned "The library view" still needs alt text
1487 - /// describing what is in the shot.
1488 - pub caption: Option<&'a str>,
1489 - /// How it sits in the box it is given.
1490 - pub fit: Fit,
1491 - /// The picture's own dimensions, where the app knows them.
1492 - ///
1493 - /// **Not a display size**, and that distinction is what makes this belong
1494 - /// here rather than fall foul of the deferral rule. Saying a picture should
1495 - /// be 320 points wide is a layout value and is not the description's to
1496 - /// give. Saying the file is 5120x3412 is a fact *about the picture*, the
1497 - /// same kind of fact [`alt`](Self::alt) is, and no renderer can find it out
1498 - /// without fetching the bytes.
1499 - ///
1500 - /// # What it is for, and it is not decoration
1501 - ///
1502 - /// Without it a renderer cannot reserve room, so the picture occupies
1503 - /// nothing until it arrives and then takes its full height at once,
1504 - /// shoving everything below it down the screen.
1505 - ///
1506 - /// Every host wants it and none can derive it. A webview writes `width` and
1507 - /// `height` so the browser holds the space; egui sizes a texture; a
1508 - /// terminal with a graphics protocol scales a blit into cells.
1509 - ///
1510 - /// `None` is honest and common: a creator-uploaded image whose dimensions
1511 - /// the app never recorded genuinely does not know. It means the renderer
1512 - /// cannot reserve, not that the picture has no size.
1513 - pub intrinsic: Option<Extent>,
1514 - /// Whether the picture is needed with the screen, or can arrive later.
1515 - pub loading: Loading,
1516 - }
1517 -
1518 1451 /// A picture's own pixel dimensions.
1519 1452 ///
1520 1453 /// Deliberately not [`makeover_geometry`]'s business. Geometry answers *how
@@ -1577,59 +1510,6 @@
1577 1510 Lazy,
1578 1511 }
1579 1512
1580 - impl<'a> Image<'a> {
1581 - /// A picture that carries its own proportions.
1582 - #[must_use]
1583 - pub const fn new(alt: &'a str) -> Self {
1584 - Self {
1585 - alt,
1586 - caption: None,
1587 - fit: Fit::Natural,
1588 - intrinsic: None,
1589 - loading: Loading::Eager,
1590 - }
1591 - }
1592 -
1593 - /// The picture's own dimensions, so a renderer can hold its place.
1594 - #[must_use]
1595 - pub const fn intrinsic(mut self, width: u32, height: u32) -> Self {
1596 - self.intrinsic = Some(Extent::new(width, height));
1597 - self
1598 - }
1599 -
1600 - /// This picture is not on screen yet; it can arrive when it is near.
1601 - #[must_use]
1602 - pub const fn lazy(mut self) -> Self {
1603 - self.loading = Loading::Lazy;
1604 - self
1605 - }
1606 -
1607 - /// A visible line under it.
1608 - #[must_use]
1609 - pub const fn caption(mut self, caption: &'a str) -> Self {
1610 - self.caption = Some(caption);
1611 - self
1612 - }
1613 -
1614 - /// How it sits in its box.
1615 - #[must_use]
1616 - pub const fn fit(mut self, fit: Fit) -> Self {
1617 - self.fit = fit;
1618 - self
1619 - }
1620 -
1621 - /// Whether the picture adds anything for someone not looking at it.
1622 - ///
1623 - /// A renderer with no way to show a picture uses this to decide between
1624 - /// drawing the alt text and drawing nothing at all. Both are correct and
1625 - /// the difference is this flag: standing in for a decorative rule with the
1626 - /// word "decoration" is worse than leaving the space empty.
1627 - #[must_use]
1628 - pub const fn speaks(self) -> bool {
1629 - !self.alt.is_empty()
1630 - }
1631 - }
1632 -
1633 1513 /// What a [`Track`]'s integers count.
1634 1514 ///
1635 1515 /// `Track::fraction` never needed this -- the arithmetic is the same whatever
@@ -4535,44 +4415,7 @@
4535 4415 Essential,
4536 4416 }
4537 4417
4538 - /// How much room a group has, measured against its own allocation.
4539 - ///
4540 - /// Never authored. A renderer computes it from what the group was given and
4541 - /// what the group's own contents ask for, in that renderer's units: a webview
4542 - /// from `min-content` under a container query, a terminal from cell widths,
4543 - /// egui from the galley. Nothing in the description says a number, which is the
4544 - /// point -- an authored breakpoint rots and this cannot.
4545 - ///
4546 - /// # Why not [`Depth`]-style two members and no more
4547 - ///
4548 - /// Two is what the measurement supports. The goingson case that produced this
4549 - /// type is a window 913px wide -- makeover-geometry's `SizeClass::Expanded` --
4550 - /// holding a group that has run out of room. A third tier would be a guess
4551 - /// about a shape nothing in the tree has yet.
4552 - ///
4553 - /// # Why it is not `SizeClass`
4554 - ///
4555 - /// Because 913 is exactly the case that proves they are different facts. The
4556 - /// window is roomy and the group is not, so a type that answered for both would
4557 - /// have to be wrong about one of them. Sharing the name would also invite
4558 - /// `@media` thinking straight back in, which is what put a `position: absolute`
4559 - /// in goingson's stylesheet in the first place. Container semantics instead: a
4560 - /// group narrowed by a sidebar behaves the same as one narrowed by the window,
4561 - /// and there is one code path rather than two.
4562 - ///
4563 - /// Ordered least room first, [`Priority`]'s convention, so a group nesting
4564 - /// another takes the minimum of the two and relief still resolves inside-out.
4565 - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
4566 - #[non_exhaustive]
4567 - pub enum Room {
4568 - /// Not everything the group contains fits, and the group's [`Fallback`]
4569 - /// decides what happens.
4570 - Tight,
4571 - /// Everything fits as described.
4572 - Ample,
4573 - }
4574 -
4575 - /// What a group does when it is [`Room::Tight`].
4418 + /// What a group does when it runs out of room.
4576 4419 ///
4577 4420 /// Authored, and required: the field carrying this has no `Default` and a group
4578 4421 /// cannot be described without saying what it does when it runs out of room.
@@ -4876,7 +4719,7 @@
4876 4719 pub fn reach(&self) -> u8 {
4877 4720 self.values
4878 4721 .iter()
4879 - .map(|value| value.depth)
4722 + .map(|value| value.depth.level)
4880 4723 .max()
4881 4724 .unwrap_or(0)
4882 4725 }
@@ -4985,7 +4828,7 @@
4985 4828 /// one code path rather than two. A renderer that draws no tree at all still
4986 4829 /// reads this, since a value's depth is what distinguishes two same-named
4987 4830 /// leaves under different parents.
4988 - pub depth: u8,
4831 + pub depth: Nesting,
4989 4832 /// Whether taking it reveals values under it.
4990 4833 ///
4991 4834 /// Distinct from having a nonzero [`depth`](Self::depth): a leaf deep in the
@@ -5004,7 +4847,7 @@
5004 4847 label,
5005 4848 count: None,
5006 4849 standing: Standing::Open,
5007 - depth: 0,
4850 + depth: Nesting::top(),
5008 4851 branching: false,
5009 4852 }
5010 4853 }
@@ -5034,7 +4877,7 @@
5034 4877
5035 4878 /// Where it sits in the tree, and whether anything hangs off it.
5036 4879 #[must_use]
5037 - pub const fn at(mut self, depth: u8, branching: bool) -> Self {
4880 + pub const fn at(mut self, depth: Nesting, branching: bool) -> Self {
5038 4881 self.depth = depth;
5039 4882 self.branching = branching;
5040 4883 self
@@ -5263,10 +5106,10 @@
5263 5106 let inherited = [
5264 5107 FacetValue::of("music")
5265 5108 .standing(Standing::Taken)
5266 - .at(0, true),
5109 + .at(Nesting::at(0), true),
5267 5110 FacetValue::new("music/synths", "synths")
5268 5111 .standing(Standing::Inherited)
5269 - .at(1, false),
5112 + .at(Nesting::at(1), false),
5270 5113 ];
5271 5114 let facet = Facet::new("Tag", Selecting::Subtree, &inherited);
5272 5115 assert!(facet.engaged());
@@ -5274,10 +5117,10 @@
5274 5117 assert_eq!(facet.reach(), 1);
5275 5118
5276 5119 let untouched = [
5277 - FacetValue::of("music").at(0, true),
5120 + FacetValue::of("music").at(Nesting::at(0), true),
5278 5121 FacetValue::new("music/synths", "synths")
5279 5122 .standing(Standing::Inherited)
5280 - .at(1, false),
5123 + .at(Nesting::at(1), false),
5281 5124 ];
5282 5125 // Inherited alone is something further up doing the narrowing, and
5283 5126 // there is nothing further up here.
@@ -6163,64 +6006,6 @@
6163 6006 assert!(!Region::Bespoke { name: "day-plan" }.described());
6164 6007 }
6165 6008
6166 - #[test]
6167 - fn a_picture_that_says_nothing_is_a_claim_and_not_an_oversight() {
6168 - // The distinction a renderer with no graphics protocol runs on: draw
6169 - // the words, or draw nothing. Standing in for a decorative rule with
6170 - // the word "decoration" is worse than leaving the space empty.
6171 - assert!(Image::new("The library view, mid-import").speaks());
6172 - assert!(!Image::new("").speaks());
6173 - }
6174 -
6175 - #[test]
6176 - fn a_caption_and_alt_text_are_not_the_same_line() {
6177 - // A caption is content everybody reads; alt text stands in for the
6178 - // picture. A screenshot with a caption still needs alt text.
6179 - let shot = Image::new("A file list with three rows selected").caption("The library view");
6180 - assert_eq!(shot.caption, Some("The library view"));
6181 - assert!(shot.speaks());
6182 - assert_ne!(shot.alt, shot.caption.unwrap());
6183 - }
6184 -
6185 - #[test]
6186 - fn a_picture_can_say_how_much_room_to_hold() {
6187 - // The whole point: a renderer reserves from the ratio, so the space is
6188 - // right at any width. A fixed height would only be right at one.
6189 - let shot = Image::new("a screenshot").intrinsic(5120, 3412);
6190 - let e = shot.intrinsic.expect("carried");
6191 - assert_eq!((e.width, e.height), (5120, 3412));
6192 - assert!((e.ratio().unwrap() - 1.5006).abs() < 0.001);
6193 - }
6194 -
6195 - #[test]
6196 - fn a_picture_with_no_dimensions_reserves_nothing_rather_than_guessing() {
6197 - // `None` is honest: a creator upload whose size was never recorded does
6198 - // not know it. A renderer must not invent one.
6199 - assert_eq!(Image::new("unknown upload").intrinsic, None);
6200 - assert_eq!(Extent::new(0, 10).ratio(), None);
6201 - assert_eq!(Extent::new(10, 0).ratio(), None);
6202 - }
6203 -
6204 - #[test]
6205 - fn a_picture_is_wanted_now_unless_the_app_says_otherwise() {
6206 - // Eager is the safe default and lazy is the opt-in, because deferring
6207 - // something already on screen saves nothing and moves its shift later.
6208 - assert_eq!(Image::new("hero").loading, Loading::Eager);
6209 - assert_eq!(Loading::default(), Loading::Eager);
6210 - assert_eq!(Image::new("frame 2").lazy().loading, Loading::Lazy);
6211 - }
6212 -
6213 - #[test]
6214 - fn a_picture_keeps_its_own_proportions_unless_told_otherwise() {
6215 - // The default is the one that shows the whole picture at its own shape,
6216 - // so a renderer ignoring Fit entirely is still right about the common
6217 - // case. The shipped MNW carousel sets no object-fit at all, which is
6218 - // this.
6219 - assert_eq!(Image::new("a").fit, Fit::Natural);
6220 - assert_eq!(Fit::default(), Fit::Natural);
6221 - assert_eq!(Image::new("a").fit(Fit::Cover).fit, Fit::Cover);
6222 - }
6223 -
6224 6009 #[test]
6225 6010 fn a_group_contains_a_section_without_claiming_to_be_a_pane() {
6226 6011 // The whole of why this is a member rather than a `Pane`. A pane is
@@ -6821,17 +6606,6 @@
6821 6606 assert_eq!(Paging::more(500).of(400).remaining(), Some(0));
6822 6607 }
6823 6608
6824 - #[test]
6825 - fn a_group_out_of_room_is_not_the_same_fact_as_a_narrow_window() {
6826 - // The case the type exists for: 913px is a roomy window holding a group
6827 - // that has run out of room, so room is measured against the group's own
6828 - // allocation and never against the viewport.
6829 - assert!(Room::Tight < Room::Ample);
6830 - // A group nesting another has whichever room is scarcer, which is what
6831 - // makes relief resolve inside-out rather than by declaration order.
6832 - assert_eq!(Room::Ample.min(Room::Tight), Room::Tight);
6833 - }
6834 -
6835 6609 #[test]
6836 6610 fn a_fallback_is_authored_and_a_group_cannot_omit_it() {
6837 6611 // No `Default`. The compiler is what enforces rule 2, so the assertion