Skip to main content

max / makeover-webview

0.49.0: a run of members that share their space, and the floor under them Task fb95ebf9, ruling wiki layout-room-and-fallback (Max 2026-08-18). .run is a flex row and .run > * gets min-width: min-content. That floor is most of the fix and it is not a fallback: it applies to every run whatever the group declared. flexbox's default min-width is auto, which lets an item be compressed below its own content in a nowrap row, and that is how a toolbar is drawn over a tab strip even with nothing out of flow. min-content is the derived minimum the ruling asks for, in this renderer's unit, stated by the browser rather than by anybody. No query, no number. Wrap is flex-wrap: wrap and is exact. Stack adds flex: 1 1 max-content, so a member that cannot sit beside its sibling takes a line of its own and fills it. Shed and Menu get wrap, and this renderer is honouring less than the description says. CSS cannot express either without breaking the ruling's own constraint: both need to know the run is out of room, a container query is the only construct that can ask, and @container compares against a <length> -- there is no @container (inline-size < min-content). Every honest spelling needs an authored breakpoint, which is the thing the ruling forbids, and the dishonest ones are worse: a clamped height clips by document order rather than by Priority, and display: none under a viewport @media is the nth-child(n+5) bug the vocabulary replaced. Wrapping keeps every member reachable, which is the property that was broken, and keeps rule 1. Region::Columns already states that a renderer answering with less than was described is honouring it. The no-sizes audit gained five keywords. min-content and max-content are the opposite of a magnitude: they ask the browser what the members come to.
Author: Max Johnson <me@maxj.phd> · 2026-08-18 19:31 UTC
Signed with PGP, not checked
Commit: 63404dc520a9505c385790e8dcac68360a68f8b9
Parent: 8fbb28f
3 files changed, +212 insertions, -2 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-webview"
3 - version = "0.48.0"
3 + version = "0.49.0"
4 4 edition = "2024"
5 5 # One copy of this renderer per dependency graph, enforced by cargo rather than
6 6 # by remembering. Two versions means the generated stylesheet and the emitted
M src/lib.rs +210 -1
@@ -290,7 +290,8 @@
290 290 // rules are as generated as the ones here.
291 291 pub use makeover_geometry::{CSS_LAYER, in_css_layer};
292 292 use makeover_layout::{
293 - Bevel, CellPart, Depth, Fill, Flow, Intent, RowPart, Selector, Sort, State, Token, Tone,
293 + Bevel, CellPart, Depth, Fallback, Fill, Flow, Intent, RowPart, Selector, Sort, State, Token,
294 + Tone,
294 295 };
295 296 use makeover_touch::Affordance;
296 297 use std::fmt::Write as _;
@@ -974,6 +975,125 @@
974 975 css
975 976 }
976 977
978 + /// A row of things that share their space, and what each fallback gets here.
979 + ///
980 + /// Ruling: wiki `layout-room-and-fallback`, Max 2026-08-18. The bug it answers
981 + /// is goingson's `styles.css:702`, which pinned every `.page-header` over the
982 + /// pill strip with `position: absolute` and so took the toolbar out of flow:
983 + /// zero width contributed to the row it shared, nothing able to collide with
984 + /// it, and therefore nothing preventing the collision. Rule 1 of the ruling is
985 + /// that every described member is in flow, and these rules are how that is kept
986 + /// rather than asked for.
987 + ///
988 + /// # The floor, which is most of the fix
989 + ///
990 + /// `.run > *` gets `min-width: min-content`. That is the derived minimum the
991 + /// ruling asks for, in this renderer's own unit and stated by the browser
992 + /// rather than by anybody: a member cannot be squeezed narrower than what is
993 + /// in it, so members in one flow push each other instead of overlapping. It
994 + /// costs no query and no number, and it is what fixes all four measured widths
995 + /// whichever fallback the group declared.
996 + ///
997 + /// # What each fallback gets, exactly
998 + ///
999 + /// [`Fallback::Wrap`] is `flex-wrap: wrap`, which is exact. The browser wraps
1000 + /// the run when the members no longer fit, deciding that from their own
1001 + /// intrinsic widths, which is the derived minimum doing the whole job.
1002 + ///
1003 + /// [`Fallback::Stack`] is wrap plus `flex: 1 1 max-content` on the members, so
1004 + /// a member that cannot sit beside its sibling takes a line of its own and
1005 + /// fills it. For the two-member run this was ruled on -- a tab strip and a
1006 + /// band -- that is precisely "a row becomes a column".
1007 + ///
1008 + /// [`Fallback::Shed`] and [`Fallback::Menu`] get wrap, and this renderer is
1009 + /// honouring less than the description says. **CSS cannot express either one
1010 + /// without breaking the ruling's own first constraint.** Both need to know that
1011 + /// the run is out of room in order to take a member out of it, a container
1012 + /// query is the only construct that can ask, and `@container` compares against
1013 + /// a `<length>` -- there is no `@container (inline-size < min-content)`. So
1014 + /// every honest spelling of Shed here needs an authored breakpoint, which is
1015 + /// the thing the ruling exists to forbid, and the dishonest ones are worse: a
1016 + /// clamped height clips by document order rather than by [`Priority`], and
1017 + /// `display: none` under a viewport `@media` is the `nth-child(n+5)` bug the
1018 + /// vocabulary replaced.
1019 + ///
1020 + /// Wrapping is the right thing to do instead. It keeps every member reachable,
1021 + /// which is the property that was actually broken -- goingson's new-contact
1022 + /// button left the viewport entirely at 560 -- and it keeps rule 1. A renderer
1023 + /// answering with less than was described is precedented and deliberate here:
1024 + /// [`makeover_layout::Region::Columns`] says a terminal stacking a board's
1025 + /// columns is honouring the description rather than degrading it.
1026 + ///
1027 + /// The real mechanism needs the shed members to have somewhere to go, which is
1028 + /// markup and belongs to quasi-webview: an overflow control is a member of the
1029 + /// run, and the description does not yet say that a member *is* one. Filed
1030 + /// rather than guessed at.
1031 + fn run_rules(opts: &Emit) -> String {
1032 + let run = class("run", opts);
1033 + let mut css = String::new();
1034 +
1035 + // `flex-wrap: nowrap` is stated rather than left to the default, because
1036 + // the fallbacks below are read as overrides of this line and a reader
1037 + // should not have to know which way flexbox leans to see that.
1038 + //
1039 + // No gap. Spacing between members is the app's, the same way this crate
1040 + // states no margins anywhere else; a gap here would be a size, and the one
1041 + // hardcoded size in the mechanism is makeover-geometry's contact patch.
1042 + let _ = writeln!(
1043 + css,
1044 + ".{run} {{\n display: flex;\n flex-wrap: nowrap;\n align-items: center;\n}}"
1045 + );
1046 +
1047 + // The derived minimum, and the whole reason a member can no longer be
1048 + // overlapped. `min-width: auto` is flexbox's default for a flex item and is
1049 + // *not* the same thing: auto lets an item be compressed below its content
1050 + // in a nowrap run, which is how a toolbar ends up drawn over a tab strip
1051 + // even without anything leaving the flow.
1052 + let _ = writeln!(css, ".{run} > * {{\n min-width: min-content;\n}}");
1053 +
1054 + for fallback in [
1055 + Fallback::Wrap,
1056 + Fallback::Stack,
1057 + Fallback::Shed,
1058 + Fallback::Menu,
1059 + ] {
1060 + let name = fallback_class(fallback);
1061 + let c = class(name, opts);
1062 + let _ = writeln!(css, ".{c} {{\n flex-wrap: wrap;\n}}");
1063 + if matches!(fallback, Fallback::Stack) {
1064 + let _ = writeln!(css, ".{c} > * {{\n flex: 1 1 max-content;\n}}");
1065 + }
1066 + }
1067 +
1068 + css
1069 + }
1070 +
1071 + /// Every class [`fallback_class`] can return, plus the run itself.
1072 + ///
1073 + /// [`ROW_PART_CLASSES`](crate::list::ROW_PART_CLASSES)'s reasoning and the same
1074 + /// obligation: a `match` over a `#[non_exhaustive]` enum cannot be enumerated
1075 + /// from outside, so the list sits beside it and a test holds the two together.
1076 + /// `run` is in it because it is emitted in its own right rather than only as a
1077 + /// fallback's fallback.
1078 + pub const RUN_CLASSES: &[&str] = &["run", "run-wrap", "run-stack", "run-shed", "run-menu"];
1079 +
1080 + /// The class a run carries for what it does when it is tight.
1081 + ///
1082 + /// A run always carries `.run` as well, so an unrecognised fallback -- the enum
1083 + /// is `#[non_exhaustive]` -- lands as a plain nowrap row with the min-content
1084 + /// floor still under it. That is the safe failure: every member in flow and
1085 + /// none overlapped, which is the property, with only the rearrangement missing.
1086 + #[must_use]
1087 + pub fn fallback_class(fallback: Fallback) -> &'static str {
1088 + match fallback {
1089 + Fallback::Wrap => "run-wrap",
1090 + Fallback::Stack => "run-stack",
1091 + Fallback::Shed => "run-shed",
1092 + Fallback::Menu => "run-menu",
1093 + _ => "run",
1094 + }
1095 + }
1096 +
977 1097 /// The progress trough these rules fill.
978 1098 ///
979 1099 /// This was renderer-local chrome with nothing behind it until makeover-layout
@@ -1507,6 +1627,7 @@
1507 1627 css.push_str(&token_rules(opts));
1508 1628 css.push_str(&selector_rules(opts));
1509 1629 css.push_str(&row_rules(opts));
1630 + css.push_str(&run_rules(opts));
1510 1631 css.push_str(&progress_rules(opts));
1511 1632 css.push_str(&figure_rules(opts));
1512 1633 css.push_str(&picture_rules(opts));
@@ -1547,6 +1668,73 @@
1547 1668 use super::*;
1548 1669 use makeover_layout::Edge;
1549 1670
1671 + #[test]
1672 + fn every_fallback_class_is_one_a_checker_knows_about() {
1673 + // The obligation ROW_PART_CLASSES carries, for the same reason: a class
1674 + // this crate can write and the vocabulary list does not carry is
1675 + // invisible to the dead-vocabulary seal and to the overlap check both.
1676 + for fallback in [
1677 + Fallback::Wrap,
1678 + Fallback::Stack,
1679 + Fallback::Shed,
1680 + Fallback::Menu,
1681 + ] {
1682 + assert!(
1683 + RUN_CLASSES.contains(&fallback_class(fallback)),
1684 + "{fallback:?} is missing from RUN_CLASSES"
1685 + );
1686 + }
1687 + let names = crate::vocabulary::names(&Emit::default());
1688 + for name in RUN_CLASSES {
1689 + assert!(names.contains(*name), "{name} is not in the vocabulary");
1690 + }
1691 + }
1692 +
1693 + #[test]
1694 + fn a_run_gives_every_member_a_floor_it_cannot_be_squeezed_below() {
1695 + // The whole of what stops the overlap, and it is not a fallback: it
1696 + // applies to every run whatever the group declared. flexbox's default
1697 + // min-width is auto, which lets an item be compressed below its own
1698 + // content in a nowrap row, and that is how a toolbar is drawn over a
1699 + // tab strip even with nothing out of flow.
1700 + let css = run_rules(&Emit::default());
1701 + assert!(css.contains(".run > * {\n min-width: min-content;\n}"));
1702 + // No number anywhere in it. The minimum is derived by the browser from
1703 + // what the members contain, which is the ruling's own requirement.
1704 + assert!(!css.contains("px"));
1705 + assert!(!css.contains("rem"));
1706 + assert!(!css.contains("@media"));
1707 + }
1708 +
1709 + #[test]
1710 + fn room_is_never_asked_of_the_viewport() {
1711 + // The 913 case: a window in SizeClass::Expanded holding a group out of
1712 + // room. A viewport query answers about the window and would be wrong
1713 + // about the group, which is why the table's @media walk is not the
1714 + // precedent this follows.
1715 + let css = run_rules(&Emit::default());
1716 + for size in [SizeClass::Compact, SizeClass::Medium] {
1717 + assert!(!css.contains(&size.media_condition()));
1718 + }
1719 + }
1720 +
1721 + #[test]
1722 + fn every_fallback_lands_as_a_class_and_an_unknown_one_lands_plainly() {
1723 + let css = run_rules(&Emit::default());
1724 + for fallback in [
1725 + Fallback::Wrap,
1726 + Fallback::Stack,
1727 + Fallback::Shed,
1728 + Fallback::Menu,
1729 + ] {
1730 + let class = fallback_class(fallback);
1731 + assert!(css.contains(&format!(".{class} {{")), "{class} unemitted");
1732 + }
1733 + // Stack is the one that also says what a member does with the line it
1734 + // took, which is what separates it from wrapping.
1735 + assert!(css.contains(".run-stack > * {\n flex: 1 1 max-content;\n}"));
1736 + }
1737 +
1550 1738 #[test]
1551 1739 fn the_emitted_bevel_matches_what_the_apps_already_hand_write() {
1552 1740 // Balanced Breakfast's styles.css, verbatim. Adoption has to be a
@@ -2449,6 +2637,27 @@
2449 2637 // how tall a day is.
2450 2638 | "relative"
2451 2639 | "absolute"
2640 + // A run's five, 0.49.0. `flex`, `wrap` and
2641 + // `center` are structure and alignment, the same
2642 + // reading `table` gets: which way members are laid
2643 + // out and how they line up, never how much of
2644 + // anything.
2645 + //
2646 + // The two intrinsic keywords are the interesting
2647 + // pair and they are the opposite of a size. A
2648 + // magnitude is a number somebody chose;
2649 + // `min-content` and `max-content` are the browser
2650 + // being asked what the members themselves come to,
2651 + // which is the derived minimum the room ruling
2652 + // requires and the reason no breakpoint appears
2653 + // anywhere in these rules. `1 1 max-content` is
2654 + // grow, shrink and that basis, so its two digits
2655 + // are ratios rather than lengths.
2656 + | "flex"
2657 + | "wrap"
2658 + | "center"
2659 + | "min-content"
2660 + | "1 1 max-content"
2452 2661 // A picture's three, 0.36.0. `block` is structure
2453 2662 // for the reason `table` is: an inline image sits
2454 2663 // on the baseline and carries a descender's worth
@@ -65,6 +65,7 @@
65 65 .iter()
66 66 .chain(CELL_PART_CLASSES)
67 67 .chain(FLOW_CLASSES)
68 + .chain(crate::RUN_CLASSES)
68 69 .map(|name| crate::class(name, opts)),
69 70 );
70 71 all.extend(