Skip to main content

max / makeover-layout

Add Region::Group, the container a section heading names Heading::Section has said "names a block within the screen" since 0.2.0 and there was no block. A section heading is a leaf beside the things it names, so a description could say a section had started and never that one had ended. 41 Heading::Section sites across the ten described screens, none of them contained. Apps reached for Region::Pane instead, which is 28 of the 45 regions in those screens and claims a scroll and a well that a settings group does not have. Group carries no heading, no depth and no colour: the heading is an ordinary body node, the depth inherits the way Bespoke's does, and a renderer distinguishing sibling groups derives the assignment from their order rather than being told.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-15 19:45 UTC
Signed with PGP, not checked
Commit: e0bd2fca1f3445f067632b9a8e0a448c62bc585f
Parent: 0d63421
2 files changed, +104 insertions, -1 deletion
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.26.1"
3 + version = "0.27.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 +103
@@ -279,6 +279,26 @@
279 279 //! sit with whatever holds the regions, the same split [`Selector`] already made
280 280 //! against `Node::Select`.
281 281 //!
282 + //! 0.27.0 adds [`Region::Group`], which closes a gap this crate had carried
283 + //! since 0.2.0 without noticing: [`Heading::Section`] is documented as naming a
284 + //! block within the screen, and there was no block. A section heading is a leaf
285 + //! beside the things it names, so the description could say a section had
286 + //! *started* and never that one had ended.
287 + //!
288 + //! Found by asking how a screen distinguishes groups of settings by colour, and
289 + //! the answer turned out to be two findings rather than one. This is the first
290 + //! and it is the precondition: there is nothing to tint until there is a
291 + //! container. The second — every renderer already resolves `category.one`
292 + //! through `category.six` and no description can reach any of them — is filed
293 + //! and not shipped here.
294 + //!
295 + //! What the colour question settled anyway, because it shapes this member: the
296 + //! group carries no colour and no ordinal. A renderer distinguishing sibling
297 + //! groups derives the assignment from their order, which is
298 + //! [`Region::Columns`]' reasoning about counts applied to colour — the children
299 + //! say, and a value here would be a second source for something the description
300 + //! already states by containing them.
301 + //!
282 302 //! # Reach, focus and the focus ring
283 303 //!
284 304 //! Three terms, and no others, for what 0.19.0 moved out of the description.
@@ -1933,6 +1953,54 @@
1933 1953 Sidebar,
1934 1954 /// A region of content with its own scroll.
1935 1955 Pane,
1956 + /// Things that belong together, and nothing else.
1957 + ///
1958 + /// The block [`Heading::Section`] has been naming since 0.2.0 without the
1959 + /// vocabulary being able to contain it. A section heading is a leaf sitting
1960 + /// *beside* the things it names, so nothing said where a section started or
1961 + /// ended and a renderer learned one had ended only because the next heading
1962 + /// arrived.
1963 + ///
1964 + /// # The measurement
1965 + ///
1966 + /// 41 [`Heading::Section`] sites across the ten screens described through
1967 + /// the router, not one of them contained. audiofiles' settings screen is the
1968 + /// clearest: one pane holding a heading, a field, a heading, two toggles, a
1969 + /// heading, a toggle and a heading, which is four sections and no
1970 + /// containers. Under the hand-written CSS the ports are replacing the same
1971 + /// block is spelled `.settings-section` in goingson, `.form-section` and
1972 + /// `.content-section` in the MNW server, `.help-section` in Balanced
1973 + /// Breakfast: three apps, four names, one shape.
1974 + ///
1975 + /// # Why the existing members were the wrong answer
1976 + ///
1977 + /// [`Pane`](Self::Pane) is what apps reached for, and it is 28 of the 45
1978 + /// regions in the described screens. It claims a scroll of its own and
1979 + /// [`Depth::Well`], so four settings groups inside a pane are four wells
1980 + /// inside a well and four scroll contexts. Neither claim is true of a group.
1981 + ///
1982 + /// [`Widget`](Self::Widget) is wrong from the other side. Its own docs say a
1983 + /// widget is never how a primitive gets added by the back door, and a run of
1984 + /// related controls under a heading is furniture any app would have, which
1985 + /// is the generic-against-bespoke bar a primitive has to clear.
1986 + ///
1987 + /// # What it does not carry
1988 + ///
1989 + /// **A heading.** A group usually has one and it is an ordinary node in the
1990 + /// body, the way it already was. A group of related toggles with no heading
1991 + /// is a real thing and a mandatory slot would forbid it.
1992 + ///
1993 + /// **A depth.** [`Depth::Flat`], on [`Bespoke`](Self::Bespoke)'s reasoning:
1994 + /// it inherits, and an app that wants its group in a well puts it in a
1995 + /// [`Pane`](Self::Pane), which composes rather than adding a knob here.
1996 + ///
1997 + /// **A colour.** Distinguishing sibling groups by colour is the thing this
1998 + /// member was asked for and it is deliberately not stated here. The
1999 + /// description says these things belong together; which of the theme's
2000 + /// categorical colours a renderer reaches for, and whether it reaches for
2001 + /// one at all, is derived from sibling order at the renderer. A terminal
2002 + /// that tints nothing and separates with a rule is honouring this.
2003 + Group,
1936 2004 /// Two panes side by side, where the left chooses what the right shows.
1937 2005 Split,
1938 2006 /// Peer regions across, all of them equals.
@@ -2056,6 +2124,11 @@
2056 2124 pub const fn depth(self) -> Depth {
2057 2125 match self {
2058 2126 Self::Band | Self::Sidebar | Self::Split | Self::TabGroup => Depth::Flat,
2127 + // Flat, and it inherits. A group says its contents belong together
2128 + // and says nothing about the surface they sit on, so a group in a
2129 + // pane is in a well and a group on the page is on the page. An app
2130 + // wanting one lifted puts it in a `Pane`.
2131 + Self::Group => Depth::Flat,
2059 2132 // Flat, and it is the container rather than the columns. Each
2060 2133 // column is its own region and brings its own depth; a well here
2061 2134 // would put a second edge around a row of wells.
@@ -2111,6 +2184,7 @@
2111 2184 Self::Band
2112 2185 | Self::Sidebar
2113 2186 | Self::Pane
2187 + | Self::Group
2114 2188 | Self::Split
2115 2189 | Self::Columns
2116 2190 | Self::TabGroup
@@ -3707,6 +3781,7 @@
3707 3781 for r in [
3708 3782 Region::Band,
3709 3783 Region::Sidebar,
3784 + Region::Group,
3710 3785 Region::Split,
3711 3786 Region::TabGroup,
3712 3787 ] {
@@ -3723,6 +3798,7 @@
3723 3798 Region::Band,
3724 3799 Region::Sidebar,
3725 3800 Region::Pane,
3801 + Region::Group,
3726 3802 Region::Split,
3727 3803 Region::TabGroup,
3728 3804 Region::Modal,
@@ -3795,6 +3871,32 @@
3795 3871 assert_eq!(Image::new("a").fit(Fit::Cover).fit, Fit::Cover);
3796 3872 }
3797 3873
3874 + #[test]
3875 + fn a_group_contains_a_section_without_claiming_to_be_a_pane() {
3876 + // The whole of why this is a member rather than a `Pane`. A pane is
3877 + // looked into and scrolls; a group is neither, and four groups inside a
3878 + // settings pane described as panes are four wells inside a well.
3879 + assert_eq!(Region::Pane.depth(), Depth::Well);
3880 + assert_eq!(Region::Group.depth(), Depth::Flat);
3881 + assert_ne!(Region::Group, Region::Pane);
3882 +
3883 + // Described, and it carries no name: a group is a primitive every
3884 + // renderer draws from scratch, which is what separates it from the two
3885 + // members that do carry one.
3886 + assert!(Region::Group.described());
3887 + assert_eq!(Region::Group.name(), None);
3888 + }
3889 +
3890 + #[test]
3891 + fn a_section_heading_names_a_block_that_now_exists() {
3892 + // `Heading::Section` has said "names a block within the screen" since
3893 + // 0.2.0 and there was no block. The pairing is the point, and it is the
3894 + // reason a group carries no heading of its own: the heading is an
3895 + // ordinary node in the body, and a group without one is legal.
3896 + assert!(Heading::Section.separated());
3897 + assert_eq!(Region::Group.depth(), Depth::Flat);
3898 + }
3899 +
3798 3900 #[test]
3799 3901 fn a_widget_inherits_its_depth_the_way_a_bespoke_does() {
3800 3902 // Stronger than the bespoke case: a widget is drawn by whichever
@@ -3818,6 +3920,7 @@
3818 3920 Region::Band,
3819 3921 Region::Sidebar,
3820 3922 Region::Pane,
3923 + Region::Group,
3821 3924 Region::Split,
3822 3925 Region::TabGroup,
3823 3926 Region::Modal,