| 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 |
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 |
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 |
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 |
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
|