Skip to main content

max / makeover-layout

Say how much room a group has, and what it does without enough 0.29.0. Room and Fallback, the two halves of what goingson asserted in a stylesheet because the description had no way to say it: a Band and a TabGroup's tab strip occupy one row, and the toolbar was pinned over the pill strip with position: absolute because nothing named that adjacency. Room is renderer-computed and never authored. Ample or Tight, measured against the group's own allocation rather than the window, because the case that produced the type is a 913px window -- SizeClass::Expanded, roomy -- holding a group out of room. Sharing SizeClass would have to be wrong about one of them, and would invite @media thinking back in. Ordered least room first so a group nesting another takes the minimum and relief resolves inside-out. Fallback is authored and required. Wrap, Stack, Shed, Menu, no Default: a group cannot be described without saying what it does when it is tight. Max 2026-08-18 -- more intentionality from layout designers is acceptable so long as the constraints are solvable, since the goal is enabling good layouts rather than rescuing bad ones. Swap, an authored alternate group, is out of the first cut because it doubles the description and the two halves can drift. Priority's doc generalises the rest of the way off Column. Three shapes ask one question -- a narrow table, a narrow row, a group sharing a run of room -- and Shed is what reads it for the third. RowPart::priority already answered the second at 0.28.2.
Ruling
wiki layout-room-and-fallback. Task 45d8d9ee.
Author: Max Johnson <me@maxj.phd> · 2026-08-18 19:08 UTC
Signed with PGP, not checked
Commit: cb58664e30a851b37febb77c2e8295d6bc80fcbd
Parent: 34af865
2 files changed, +144 insertions, -9 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.28.2"
3 + version = "0.29.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 +143 -8
@@ -3546,13 +3546,19 @@
3546 3546 Fill,
3547 3547 }
3548 3548
3549 - /// What a part is worth when there is not room for all of them.
3549 + /// What a member is worth when there is not room for all of them.
3550 3550 ///
3551 - /// Written for table columns and no longer only theirs: a row is an inline run
3552 - /// and a terminal 40 columns wide has to drop something out of it, which is the
3553 - /// same question a narrow table asks. The doc below is the column argument,
3554 - /// which is where the type was measured; the sentence that gave it away is
3555 - /// [`Priority::Essential`]'s, which was already written about a row.
3551 + /// Written for table columns and no longer only theirs. Three shapes ask the
3552 + /// same question and this answers all three: a table too narrow for its
3553 + /// columns, a row too narrow for its parts (see [`RowPart::priority`]), and a
3554 + /// group of regions sharing one run of room -- goingson's tab strip and the
3555 + /// [`Region::Band`] beside it, which is the case wiki `layout-room-and-fallback`
3556 + /// was ruled on. It is what any member of a group is worth, not a table
3557 + /// concept, and [`Fallback::Shed`] is what reads it.
3558 + ///
3559 + /// The doc below is the column argument, which is where the type was measured;
3560 + /// the sentence that gave it away is [`Priority::Essential`]'s, which was
3561 + /// already written about a row.
3556 3562 ///
3557 3563 /// Ordered: [`Priority::Optional`] drops first, [`Priority::Essential`] never
3558 3564 /// drops. This replaces addressing columns by position, which is what both
@@ -3567,12 +3573,95 @@
3567 3573 pub enum Priority {
3568 3574 /// Dropped first.
3569 3575 Optional,
3570 - /// Dropped once the optional columns are gone.
3576 + /// Dropped once the optional members are gone.
3571 3577 Secondary,
3572 - /// Never dropped. Without it the row does not identify itself.
3578 + /// Never dropped. Without it the group does not identify itself.
3573 3579 Essential,
3574 3580 }
3575 3581
3582 + /// How much room a group has, measured against its own allocation.
3583 + ///
3584 + /// Never authored. A renderer computes it from what the group was given and
3585 + /// what the group's own contents ask for, in that renderer's units: a webview
3586 + /// from `min-content` under a container query, a terminal from cell widths,
3587 + /// egui from the galley. Nothing in the description says a number, which is the
3588 + /// point -- an authored breakpoint rots and this cannot.
3589 + ///
3590 + /// # Why not [`Depth`]-style two members and no more
3591 + ///
3592 + /// Two is what the measurement supports. The goingson case that produced this
3593 + /// type is a window 913px wide -- makeover-geometry's `SizeClass::Expanded` --
3594 + /// holding a group that has run out of room. A third tier would be a guess
3595 + /// about a shape nothing in the tree has yet.
3596 + ///
3597 + /// # Why it is not `SizeClass`
3598 + ///
3599 + /// Because 913 is exactly the case that proves they are different facts. The
3600 + /// window is roomy and the group is not, so a type that answered for both would
3601 + /// have to be wrong about one of them. Sharing the name would also invite
3602 + /// `@media` thinking straight back in, which is what put a `position: absolute`
3603 + /// in goingson's stylesheet in the first place. Container semantics instead: a
3604 + /// group narrowed by a sidebar behaves the same as one narrowed by the window,
3605 + /// and there is one code path rather than two.
3606 + ///
3607 + /// Ordered least room first, [`Priority`]'s convention, so a group nesting
3608 + /// another takes the minimum of the two and relief still resolves inside-out.
3609 + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
3610 + #[non_exhaustive]
3611 + pub enum Room {
3612 + /// Not everything the group contains fits, and the group's [`Fallback`]
3613 + /// decides what happens.
3614 + Tight,
3615 + /// Everything fits as described.
3616 + Ample,
3617 + }
3618 +
3619 + /// What a group does when it is [`Room::Tight`].
3620 + ///
3621 + /// Authored, and required: the field carrying this has no `Default` and a group
3622 + /// cannot be described without saying what it does when it runs out of room.
3623 + /// Max ruled on that 2026-08-18 -- more intentionality from layout designers is
3624 + /// acceptable so long as the constraints are solvable, because the goal is
3625 + /// enabling good layouts rather than rescuing bad ones. A default here would be
3626 + /// the crate guessing, and the guess would be silently wrong on the screens
3627 + /// that matter.
3628 + ///
3629 + /// Relief resolves inside-out. A group asks its children to fall back before
3630 + /// falling back itself, or an outer group collapses while an inner one still
3631 + /// had slack.
3632 + ///
3633 + /// # No `Swap`
3634 + ///
3635 + /// An authored alternate group for the tight case is deliberately out of the
3636 + /// first cut. It doubles the description for that group and the two halves can
3637 + /// drift, which is the failure this vocabulary exists to end. Add it when a
3638 + /// site proves it needs one.
3639 + ///
3640 + /// `#[non_exhaustive]`, [`Width`]'s reasoning. Unlike [`Priority`] there is no
3641 + /// order to preserve, so a member can be appended.
3642 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3643 + #[non_exhaustive]
3644 + pub enum Fallback {
3645 + /// One row becomes two. Every member stays, in the order described.
3646 + Wrap,
3647 + /// A row becomes a column. Every member stays, full width.
3648 + Stack,
3649 + /// Members drop by [`Priority`], down to [`Priority::Essential`].
3650 + ///
3651 + /// What a narrow table already does with its columns, applied to a group.
3652 + /// What drops is gone from the screen, so this is right when the dropped
3653 + /// members are facts the reader can do without and wrong when they are the
3654 + /// only way to act.
3655 + Shed,
3656 + /// The members [`Shed`](Self::Shed) would drop move into one overflow
3657 + /// control instead.
3658 + ///
3659 + /// The answer when a group holds actions. A control is not a fact: dropping
3660 + /// it does not cost the reader a detail, it costs them the only way to act,
3661 + /// which is [`RowPart::priority`]'s argument one level up.
3662 + Menu,
3663 + }
3664 +
3576 3665 /// One column of a table.
3577 3666 ///
3578 3667 /// Described once. The grid track, the cell order and the drop behaviour are
@@ -5089,6 +5178,52 @@
5089 5178 assert_eq!(Paging::more(500).of(400).remaining(), Some(0));
5090 5179 }
5091 5180
5181 + #[test]
5182 + fn a_group_out_of_room_is_not_the_same_fact_as_a_narrow_window() {
5183 + // The case the type exists for: 913px is a roomy window holding a group
5184 + // that has run out of room, so room is measured against the group's own
5185 + // allocation and never against the viewport.
5186 + assert!(Room::Tight < Room::Ample);
5187 + // A group nesting another has whichever room is scarcer, which is what
5188 + // makes relief resolve inside-out rather than by declaration order.
5189 + assert_eq!(Room::Ample.min(Room::Tight), Room::Tight);
5190 + }
5191 +
5192 + #[test]
5193 + fn a_fallback_is_authored_and_a_group_cannot_omit_it() {
5194 + // No `Default`. The compiler is what enforces rule 2, so the assertion
5195 + // that matters is one this file cannot write; what it can say is that
5196 + // the four authored answers are distinct and none is privileged.
5197 + let all = [
5198 + Fallback::Wrap,
5199 + Fallback::Stack,
5200 + Fallback::Shed,
5201 + Fallback::Menu,
5202 + ];
5203 + for (i, a) in all.iter().enumerate() {
5204 + for b in &all[i + 1..] {
5205 + assert_ne!(a, b);
5206 + }
5207 + }
5208 + }
5209 +
5210 + #[test]
5211 + fn shedding_stops_at_essential_whatever_the_group_holds() {
5212 + // Priority is read the same way for a group member as for a column,
5213 + // which is the whole claim of generalising it off `Column`.
5214 + let members = [
5215 + ("tabs", Priority::Essential),
5216 + ("search", Priority::Secondary),
5217 + ("count", Priority::Optional),
5218 + ];
5219 + let kept: Vec<_> = members
5220 + .iter()
5221 + .filter(|(_, p)| *p >= Priority::Essential)
5222 + .map(|(n, _)| *n)
5223 + .collect();
5224 + assert_eq!(kept, ["tabs"]);
5225 + }
5226 +
5092 5227 #[test]
5093 5228 fn a_role_says_what_a_part_is_worth_when_the_run_does_not_fit() {
5094 5229 // The row still identifies itself after everything droppable has gone,