Skip to main content

max / makeover-layout

Name how much vertical room a part may take A row is an inline run and every part in it is a leaf, so a part's text has always been drawn on one line and no description could say otherwise. Two apps say otherwise in their own stylesheets, both to the same number and both with a comment explaining it: Balanced Breakfast clamps a feed row's title to two lines, in a comment that says it is overriding .row-primary's single flex line, and goingson clamps a problem's body to two because "two lines is enough to recognize one". Two named tiers rather than a line count. Both sites want exactly one tier past the default, and a number invites a row whose primary is a paragraph, which is a block and has no business in a run. A third tier is a decision made here rather than something a call site can reach for. Flow::lines is where the count lives, because a renderer needs one and a call site does not. An unknown tier reads as one line: the safe direction, since a part that grows without bound breaks the rows around it while a part that stays on one line only looks like the old rendering. Priority's header widened in the same pass. It was written for table columns and is not only theirs -- a terminal 40 columns wide has to drop something out of a run, which is the same question a narrow table asks -- and the sentence that gave it away is Essential's, which was already about a row. 0.28.1 rather than 0.29.0. This is additive, and makeover-layout carries links = "makeover-layout", so two versions cannot coexist in one graph and a minor bump is a tree-wide forward fix for eleven pins. A compatible bump is what the change actually is, and every existing "0.28.0" resolves to it.
Author: Max Johnson <me@maxj.phd> · 2026-08-17 23:45 UTC
Signed with PGP, not checked
Commit: 33a6addc2ed512f398464c47f4f60c478d4d4735
Parent: 6a9c27f
2 files changed, +82 insertions, -2 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.28.0"
3 + version = "0.28.1"
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 +81 -1
@@ -1003,6 +1003,59 @@
1003 1003 /// renderer can right-align one string and cannot usefully do the same to a
1004 1004 /// strip of chips, and a fact that is not clickable should not be drawn as
1005 1005 /// though it were.
1006 + /// How much vertical room a part's text may take.
1007 + ///
1008 + /// A row is an inline run and every part in it is a leaf, so a part's text has
1009 + /// always been drawn on one line and no description could say otherwise. Two
1010 + /// apps say otherwise in their own stylesheets, both to the same number and
1011 + /// both with a comment explaining it: Balanced Breakfast clamps a feed row's
1012 + /// title to two lines (`.row--article .row-primary`, whose comment reads
1013 + /// "overrides .row-primary's single flex line"), and goingson clamps a
1014 + /// problem's body to two ("two lines is enough to recognize one, and the full
1015 + /// text is in the task once promoted").
1016 + ///
1017 + /// Two named tiers rather than a line count, and the count is what the measured
1018 + /// demand argues against. Both sites want exactly one tier past the default,
1019 + /// and a number invites a row whose primary is a paragraph, which is a block
1020 + /// and has no business in a run. A third tier is a decision, made here, rather
1021 + /// than something a call site can reach for.
1022 + ///
1023 + /// What a renderer owes it: `Tight` is what a run already does and needs no
1024 + /// answer. `Relaxed` is at most two lines and then truncation, however that
1025 + /// renderer truncates -- a webview clamps, a terminal wraps into two rows of
1026 + /// cells, an immediate-mode renderer caps the galley. A renderer that cannot
1027 + /// give two lines may draw one; what it may not do is grow without bound,
1028 + /// because the run is a line and the row's neighbours are relying on that.
1029 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
1030 + #[non_exhaustive]
1031 + pub enum Flow {
1032 + /// One line. What every part did before this type existed.
1033 + #[default]
1034 + Tight,
1035 + /// Up to two lines, then truncated.
1036 + Relaxed,
1037 + }
1038 +
1039 + impl Flow {
1040 + /// How many lines the part may take.
1041 + ///
1042 + /// A number here rather than in the enum, because a renderer needs one and
1043 + /// a call site does not. That asymmetry is the whole argument for the
1044 + /// tiers: the description says how much room the thing deserves and this
1045 + /// says what that costs, so a third tier changes one line rather than every
1046 + /// consumer's arithmetic.
1047 + #[must_use]
1048 + pub const fn lines(self) -> u8 {
1049 + match self {
1050 + Self::Relaxed => 2,
1051 + // Including any tier added later: one line is the safe reading of
1052 + // an unknown flow, since it is what the run guaranteed before flows
1053 + // existed.
1054 + _ => 1,
1055 + }
1056 + }
1057 + }
1058 +
1006 1059 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1007 1060 #[non_exhaustive]
1008 1061 pub enum RowPart {
@@ -3460,7 +3513,13 @@
3460 3513 Fill,
3461 3514 }
3462 3515
3463 - /// What a column is worth when there is not room for all of them.
3516 + /// What a part is worth when there is not room for all of them.
3517 + ///
3518 + /// Written for table columns and no longer only theirs: a row is an inline run
3519 + /// and a terminal 40 columns wide has to drop something out of it, which is the
3520 + /// same question a narrow table asks. The doc below is the column argument,
3521 + /// which is where the type was measured; the sentence that gave it away is
3522 + /// [`Priority::Essential`]'s, which was already written about a row.
3464 3523 ///
3465 3524 /// Ordered: [`Priority::Optional`] drops first, [`Priority::Essential`] never
3466 3525 /// drops. This replaces addressing columns by position, which is what both
@@ -4996,4 +5055,25 @@
4996 5055 // usize, which would print as "18446744073709551516 remaining".
4997 5056 assert_eq!(Paging::more(500).of(400).remaining(), Some(0));
4998 5057 }
5058 +
5059 + #[test]
5060 + fn a_run_is_one_line_unless_the_description_says_two() {
5061 + // The default is what every part did before flows existed, so a
5062 + // description written against the old vocabulary keeps its rendering.
5063 + assert_eq!(Flow::default(), Flow::Tight);
5064 + assert_eq!(Flow::Tight.lines(), 1);
5065 + assert_eq!(Flow::Relaxed.lines(), 2);
5066 + }
5067 +
5068 + #[test]
5069 + fn an_unknown_flow_reads_as_one_line() {
5070 + // `#[non_exhaustive]`'s cost, taken deliberately. A tier added upstream
5071 + // reaches an old renderer as one line rather than as a build break, and
5072 + // one line is the reading that cannot break a neighbour's layout. The
5073 + // match in `lines` is what this holds; it fails if a new tier is given
5074 + // an arm that returns something unbounded.
5075 + for flow in [Flow::Tight, Flow::Relaxed] {
5076 + assert!((1..=2).contains(&flow.lines()));
5077 + }
5078 + }
4999 5079 }