Skip to main content

max / makeover-layout

0.23.0: a region says how many of its children show Three findings were one missing sentence: a tab group could not say which tab was open, a carousel which frame was up, a disclosure whether its child showed. While it was missing a renderer had to match on a widget name or draw every child. Showing::All | One | AtMostOne, plus selective() and dismissible(), which are the two questions a renderer asks before deriving chrome. Only the kind lives here: which child is up and what each is called sit with whatever holds the regions, the split Selector already made against Node::Select.
Author: Max Johnson <me@maxj.phd> · 2026-08-14 22:15 UTC
Signed with PGP, not checked
Commit: 4d636ee68e76f30fd493faead6ab5fa794f35009
Parent: 8f3d6dc
3 files changed, +117 insertions, -2 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.22.0"
3 + version = "0.23.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 README.md +4 -1
@@ -38,7 +38,10 @@
38 38 silently hides the wrong one.
39 39
40 40 **Structure**: `Region` for the parts of a screen, `Arrangement` for how a screen
41 - is put together. Two arrangements, because the two apps have two.
41 + is put together. Two arrangements, because the two apps have two. `Showing` says
42 + how many of a region's children are visible at once, which is what lets a
43 + renderer derive a tab strip or a prev/next row for itself instead of recognising
44 + a widget by name.
42 45
43 46 The vocabulary was read off audiofiles, the only implementation written by a
44 47 consumer with no CSS, then checked against both webview apps.
M src/lib.rs +112
@@ -263,6 +263,22 @@
263 263 //! case that proves this cannot be one renderer-wide setting: its first frame
264 264 //! is on screen and its others are not, in one widget, at one moment.
265 265 //!
266 + //! 0.23.0 adds [`Showing`], which is three open findings collapsing into one
267 + //! member. A tab group could not say which tab was open, a carousel could not
268 + //! say which frame was up, and a disclosure could not say whether its child was
269 + //! showing. All three are the same missing sentence, and while it was missing a
270 + //! renderer had two moves: match on a widget name, or draw every child.
271 + //!
272 + //! So the widget tier was taking the blame for a gap one level below it. With
273 + //! this a renderer derives its chrome from the description — labels get a strip,
274 + //! no labels get previous/position/next — once, for every widget there will ever
275 + //! be, and [`Region::Widget`]'s name goes back to being app vocabulary a
276 + //! renderer may decline to know.
277 + //!
278 + //! Only the kind lives here. Which child is up, and what each child is called,
279 + //! sit with whatever holds the regions, the same split [`Selector`] already made
280 + //! against `Node::Select`.
281 + //!
266 282 //! # Reach, focus and the focus ring
267 283 //!
268 284 //! Three terms, and no others, for what 0.19.0 moved out of the description.
@@ -1705,6 +1721,77 @@
1705 1721 }
1706 1722 }
1707 1723
1724 + /// How many of a region's children are visible at once.
1725 + ///
1726 + /// `4dcd241b`. Three findings turned out to be one sentence the vocabulary
1727 + /// could not say: *this region holds several children and shows some of them,
1728 + /// and the reader can change which.* [`Region::TabGroup`] existed with nothing
1729 + /// saying which tab was open, a carousel had nothing saying which frame was up,
1730 + /// and a disclosure had nothing saying whether its one child was showing at all.
1731 + ///
1732 + /// Because the fact lived nowhere, a renderer had two moves: hardcode a widget
1733 + /// name, or draw every child. That is what put per-widget code in renderers, and
1734 + /// it was the missing member rather than the widget tier that put it there.
1735 + ///
1736 + /// # What is here and what is not
1737 + ///
1738 + /// The *kind*, and only the kind. Which child is currently up is the current
1739 + /// answer, and a layer that defers every address does not hold the current
1740 + /// answer either — the split [`Selector`] already makes, where this crate says
1741 + /// what kind of chooser a thing is and the router says which option is picked.
1742 + /// So a holder of regions carries the index and the per-child label beside this.
1743 + ///
1744 + /// # What a renderer does with it
1745 + ///
1746 + /// Derives its chrome, once, for every widget rather than per name:
1747 + ///
1748 + /// - Children carrying labels get a strip of the labels, the current one marked.
1749 + /// - Children carrying none get previous, position, next.
1750 + /// - [`AtMostOne`](Self::AtMostOne) over one child gets a summary line that
1751 + /// opens.
1752 + ///
1753 + /// The name on [`Region::Widget`] survives as app vocabulary, for a renderer
1754 + /// that wants to do something *special* with one, which is what it should have
1755 + /// been from the start.
1756 + ///
1757 + /// Degradation runs the way it already did: a renderer ignoring this draws every
1758 + /// child, which is more content rather than less.
1759 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1760 + #[non_exhaustive]
1761 + pub enum Showing {
1762 + /// Every child, in order. What every region did before this existed.
1763 + #[default]
1764 + All,
1765 + /// Exactly one. A carousel, a tab group.
1766 + One,
1767 + /// One, or none. A disclosure, which is closed until it is opened.
1768 + AtMostOne,
1769 + }
1770 +
1771 + impl Showing {
1772 + /// Whether the reader can change which child is up.
1773 + ///
1774 + /// The question every renderer's region arm asks before deriving any
1775 + /// chrome, and a method rather than a `matches!` at each renderer for
1776 + /// [`Region::name`]'s reason: three renderers writing the same comparison is
1777 + /// how they come to disagree about a member added later.
1778 + #[must_use]
1779 + pub const fn selective(self) -> bool {
1780 + !matches!(self, Self::All)
1781 + }
1782 +
1783 + /// Whether showing nothing is a legal state.
1784 + ///
1785 + /// True only for [`AtMostOne`](Self::AtMostOne). A renderer needs this to
1786 + /// know whether its control closes as well as moves: a carousel's row moves
1787 + /// between frames and never reaches empty, and a disclosure's summary line
1788 + /// is the same control wearing its closed state.
1789 + #[must_use]
1790 + pub const fn dismissible(self) -> bool {
1791 + matches!(self, Self::AtMostOne)
1792 + }
1793 + }
1794 +
1708 1795 /// How much of the width an arrangement's first region takes.
1709 1796 ///
1710 1797 /// `e0fd485e`. Nothing said how much room a region got, so every renderer
@@ -2536,6 +2623,31 @@
2536 2623 }
2537 2624 }
2538 2625
2626 + #[test]
2627 + fn a_region_shows_all_of_its_children_unless_it_says_otherwise() {
2628 + // The default is the behaviour every region had before this member
2629 + // existed, which is what keeps it additive: a description written
2630 + // against 0.22.0 says the same thing under 0.23.0.
2631 + assert_eq!(Showing::default(), Showing::All);
2632 + assert!(!Showing::All.selective());
2633 + }
2634 +
2635 + #[test]
2636 + fn only_a_disclosure_can_show_nothing() {
2637 + // The two derived idioms differ in one respect and this is it. A
2638 + // carousel's row moves between frames and never reaches empty; a
2639 + // disclosure's summary line is the same control wearing its closed
2640 + // state, so a renderer has to know which it is drawing.
2641 + assert!(Showing::AtMostOne.dismissible());
2642 + assert!(!Showing::One.dismissible());
2643 + assert!(!Showing::All.dismissible());
2644 +
2645 + // Both are selective, though. Deriving chrome is one question and
2646 + // whether that chrome closes is another.
2647 + assert!(Showing::One.selective());
2648 + assert!(Showing::AtMostOne.selective());
2649 + }
2650 +
2539 2651 #[test]
2540 2652 fn an_empty_region_is_not_a_broken_one() {
2541 2653 // An empty list is the normal state of a new install. Drawing it in a