max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
10 files changed,
+267 insertions,
-44 deletions
| @@ -2076,7 +2076,7 @@ | |||
| 2076 | 2076 | ||
| 2077 | 2077 | [[package]] | |
| 2078 | 2078 | name = "makeover-layout" | |
| 2079 | - | version = "0.17.0" | |
| 2079 | + | version = "0.18.0" | |
| 2080 | 2080 | ||
| 2081 | 2081 | [[package]] | |
| 2082 | 2082 | name = "makeover-touch" | |
| @@ -5151,22 +5151,6 @@ | |||
| 5151 | 5151 | source = "registry+https://github.com/rust-lang/crates.io-index" | |
| 5152 | 5152 | checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" | |
| 5153 | 5153 | ||
| 5154 | - | [[patch.unused]] | |
| 5155 | - | name = "kberg" | |
| 5156 | - | version = "0.1.0" | |
| 5157 | - | ||
| 5158 | - | [[patch.unused]] | |
| 5159 | - | name = "ops-status" | |
| 5160 | - | version = "0.1.0" | |
| 5161 | - | ||
| 5162 | - | [[patch.unused]] | |
| 5163 | - | name = "painhours" | |
| 5164 | - | version = "0.1.0" | |
| 5165 | - | ||
| 5166 | - | [[patch.unused]] | |
| 5167 | - | name = "tagtree" | |
| 5168 | - | version = "0.4.0" | |
| 5169 | - | ||
| 5170 | 5154 | [[patch.unused]] | |
| 5171 | 5155 | name = "makeover-build" | |
| 5172 | 5156 | version = "0.26.0" | |
| @@ -5182,3 +5166,19 @@ | |||
| 5182 | 5166 | [[patch.unused]] | |
| 5183 | 5167 | name = "synckit-config" | |
| 5184 | 5168 | version = "0.2.0" | |
| 5169 | + | ||
| 5170 | + | [[patch.unused]] | |
| 5171 | + | name = "kberg" | |
| 5172 | + | version = "0.1.0" | |
| 5173 | + | ||
| 5174 | + | [[patch.unused]] | |
| 5175 | + | name = "ops-status" | |
| 5176 | + | version = "0.1.0" | |
| 5177 | + | ||
| 5178 | + | [[patch.unused]] | |
| 5179 | + | name = "painhours" | |
| 5180 | + | version = "0.1.0" | |
| 5181 | + | ||
| 5182 | + | [[patch.unused]] | |
| 5183 | + | name = "tagtree" | |
| 5184 | + | version = "0.4.0" |
| @@ -13,4 +13,4 @@ | |||
| 13 | 13 | workspace = true | |
| 14 | 14 | ||
| 15 | 15 | [dependencies] | |
| 16 | - | makeover-layout = "0.17.0" | |
| 16 | + | makeover-layout = "0.18.0" |
| @@ -14,7 +14,7 @@ | |||
| 14 | 14 | ||
| 15 | 15 | [dependencies] | |
| 16 | 16 | quasi-router = { path = "../quasi-router", version = "0.1.0" } | |
| 17 | - | makeover-layout = "0.17.0" | |
| 17 | + | makeover-layout = "0.18.0" | |
| 18 | 18 | # The depth palette, the theme bridge and the table. Everything else this crate | |
| 19 | 19 | # draws is written here first and lifted upstream once a second consumer wants | |
| 20 | 20 | # it, which is the order the suite has always moved in: the constrained consumer |
| @@ -15,7 +15,7 @@ | |||
| 15 | 15 | [dependencies] | |
| 16 | 16 | quasi-router = { path = "../quasi-router", version = "0.1.0" } | |
| 17 | 17 | quasi-http = { path = "../quasi-http", version = "0.1.0" } | |
| 18 | - | makeover-layout = "0.17.0" | |
| 18 | + | makeover-layout = "0.18.0" | |
| 19 | 19 | makeover-webview = "0.33.0" | |
| 20 | 20 | # `Node::Rich` carries markdown source and this is what turns it into markup. | |
| 21 | 21 | # Sanitising comes with it, which is why the node can carry what a user typed. |
| @@ -271,7 +271,7 @@ | |||
| 271 | 271 | ||
| 272 | 272 | #[test] | |
| 273 | 273 | fn a_slot_is_found_at_any_depth() { | |
| 274 | - | let screen = Screen::new("Tabs", Arrangement::ListDetail { tabbed: true }).with( | |
| 274 | + | let screen = Screen::new("Tabs", Arrangement::list_detail(true)).with( | |
| 275 | 275 | Slot::new("tabs", RegionKind::TabGroup) | |
| 276 | 276 | .with(Node::Region(Slot::new("pane-a", RegionKind::Pane))), | |
| 277 | 277 | ); | |
| @@ -298,7 +298,7 @@ | |||
| 298 | 298 | ||
| 299 | 299 | #[test] | |
| 300 | 300 | fn a_fragment_reaches_a_region_nested_inside_another() { | |
| 301 | - | let mut screen = Screen::new("Tabs", Arrangement::ListDetail { tabbed: true }).with( | |
| 301 | + | let mut screen = Screen::new("Tabs", Arrangement::list_detail(true)).with( | |
| 302 | 302 | Slot::new("tabs", RegionKind::TabGroup) | |
| 303 | 303 | .with(Node::Region(Slot::new("pane-a", RegionKind::Pane))), | |
| 304 | 304 | ); |
| @@ -2377,6 +2377,23 @@ | |||
| 2377 | 2377 | /// [`Row::value`]: Row::value | |
| 2378 | 2378 | /// [`Act::over`]: Act::over | |
| 2379 | 2379 | pub selection: Option<String>, | |
| 2380 | + | /// How wide this screen's content runs. | |
| 2381 | + | /// | |
| 2382 | + | /// `0eccff0d`. Measured in the MNW server, where 69 of 72 templates carry | |
| 2383 | + | /// one of three mutually exclusive CSS classes for it and nothing described | |
| 2384 | + | /// it, so the choice lived in the template rather than in the screen. | |
| 2385 | + | /// | |
| 2386 | + | /// Beside [`arrangement`](Self::arrangement) and answering the level above | |
| 2387 | + | /// it: that one divides the screen's width between regions, this says how | |
| 2388 | + | /// much of the window the screen takes in the first place. Both are the | |
| 2389 | + | /// description's, which is what answering `e0fd485e` and `0eccff0d` | |
| 2390 | + | /// together settled. | |
| 2391 | + | /// | |
| 2392 | + | /// Not an `Option`, for [`discovery`](Self::discovery)'s reason. The | |
| 2393 | + | /// default is meaningful -- a screen nobody said anything about uses the | |
| 2394 | + | /// window it was given -- and an `Option` would make "nobody said" and | |
| 2395 | + | /// "the whole width" two spellings of one thing. | |
| 2396 | + | pub measure: layout::Measure, | |
| 2380 | 2397 | } | |
| 2381 | 2398 | ||
| 2382 | 2399 | /// How a screen is found, shared and indexed. | |
| @@ -2485,9 +2502,20 @@ | |||
| 2485 | 2502 | notices: Vec::new(), | |
| 2486 | 2503 | discovery: Discovery::default(), | |
| 2487 | 2504 | selection: None, | |
| 2505 | + | measure: layout::Measure::default(), | |
| 2488 | 2506 | } | |
| 2489 | 2507 | } | |
| 2490 | 2508 | ||
| 2509 | + | /// How wide this screen's content runs, chaining. | |
| 2510 | + | /// | |
| 2511 | + | /// See [`measure`](Self::measure). [`Measure::Wide`](layout::Measure::Wide) | |
| 2512 | + | /// is the default and does not need saying. | |
| 2513 | + | #[must_use] | |
| 2514 | + | pub const fn measured(mut self, measure: layout::Measure) -> Self { | |
| 2515 | + | self.measure = measure; | |
| 2516 | + | self | |
| 2517 | + | } | |
| 2518 | + | ||
| 2491 | 2519 | /// This screen holds a set of ticks under this name, chaining. | |
| 2492 | 2520 | /// | |
| 2493 | 2521 | /// The rows that join it say so with [`Row::ticking`], and the control that | |
| @@ -2535,12 +2563,12 @@ | |||
| 2535 | 2563 | ||
| 2536 | 2564 | /// A list that chooses what the detail beside it shows. | |
| 2537 | 2565 | pub fn list_detail(title: impl Into<String>, tabbed: bool) -> Self { | |
| 2538 | - | Self::new(title, layout::Arrangement::ListDetail { tabbed }) | |
| 2566 | + | Self::new(title, layout::Arrangement::list_detail(tabbed)) | |
| 2539 | 2567 | } | |
| 2540 | 2568 | ||
| 2541 | 2569 | /// Navigation down the side, content filling the rest. | |
| 2542 | 2570 | pub fn sidebar_content(title: impl Into<String>) -> Self { | |
| 2543 | - | Self::new(title, layout::Arrangement::SidebarContent) | |
| 2571 | + | Self::new(title, layout::Arrangement::sidebar_content()) | |
| 2544 | 2572 | } | |
| 2545 | 2573 | ||
| 2546 | 2574 | /// Add a region, chaining. |
| @@ -28,16 +28,14 @@ | |||
| 28 | 28 | ||
| 29 | 29 | use crate::{Pass, Tui, below}; | |
| 30 | 30 | ||
| 31 | - | /// How wide a sidebar is, in columns. | |
| 32 | - | /// | |
| 33 | - | /// A number this renderer chose. See the module header. | |
| 34 | - | const SIDEBAR: u16 = 24; | |
| 35 | - | ||
| 36 | - | /// What share of the width a list pane takes in a list-detail arrangement. | |
| 37 | - | const LIST_SHARE: u16 = 40; | |
| 31 | + | // The sidebar's 24 columns and the list pane's 40% used to be declared here, | |
| 32 | + | // as two numbers this renderer chose with nothing behind them. `e0fd485e`: | |
| 33 | + | // they are `Arrangement::share` now, so the terminal and the webview honour one | |
| 34 | + | // fact and two hosts showing one screen agree about its proportions. | |
| 38 | 35 | ||
| 39 | 36 | /// Lay a screen's regions out and draw them. | |
| 40 | 37 | pub(crate) fn screen_regions(pass: &mut Pass<'_>, screen: &Screen, area: Rect, buf: &mut Buffer) { | |
| 38 | + | let area = measured(screen.measure, area); | |
| 41 | 39 | let mut rest = area; | |
| 42 | 40 | ||
| 43 | 41 | // Bands stack at the top, full width, in the order they were said. A band | |
| @@ -55,8 +53,8 @@ | |||
| 55 | 53 | let body = body_slots(screen); | |
| 56 | 54 | ||
| 57 | 55 | match screen.arrangement { | |
| 58 | - | layout::Arrangement::SidebarContent => { | |
| 59 | - | let (left, right) = split(rest, SIDEBAR); | |
| 56 | + | layout::Arrangement::SidebarContent { share } => { | |
| 57 | + | let (left, right) = split(rest, share.of(rest.width)); | |
| 60 | 58 | let mut sidebars = 0; | |
| 61 | 59 | let mut content = right; | |
| 62 | 60 | for slot in &body { | |
| @@ -69,7 +67,7 @@ | |||
| 69 | 67 | } | |
| 70 | 68 | } | |
| 71 | 69 | } | |
| 72 | - | layout::Arrangement::ListDetail { tabbed } => { | |
| 70 | + | layout::Arrangement::ListDetail { tabbed, share } => { | |
| 73 | 71 | if tabbed { | |
| 74 | 72 | // One at a time, and nothing says which. `body_slots` has | |
| 75 | 73 | // already cut the rest away, so this is the one region there is. | |
| @@ -77,7 +75,7 @@ | |||
| 77 | 75 | draw(pass, first, rest, buf); | |
| 78 | 76 | } | |
| 79 | 77 | } else { | |
| 80 | - | let (left, right) = split(rest, rest.width * LIST_SHARE / 100); | |
| 78 | + | let (left, right) = split(rest, share.of(rest.width)); | |
| 81 | 79 | let mut detail = right; | |
| 82 | 80 | for (index, slot) in body.iter().enumerate() { | |
| 83 | 81 | if index == 0 { | |
| @@ -103,6 +101,40 @@ | |||
| 103 | 101 | } | |
| 104 | 102 | } | |
| 105 | 103 | ||
| 104 | + | /// The screen's area, narrowed to the measure it asked for. | |
| 105 | + | /// | |
| 106 | + | /// `0eccff0d`, the terminal's half of "every renderer owes an answer". The | |
| 107 | + | /// description says how wide the content should run and this says what that is | |
| 108 | + | /// in columns, the same division the webview makes: the screen chose one of | |
| 109 | + | /// three, and what each one comes to is the renderer's. | |
| 110 | + | /// | |
| 111 | + | /// The two caps are this renderer's numbers, and only the second has a reason | |
| 112 | + | /// outside taste: past roughly 75 characters a line costs the reader the return | |
| 113 | + | /// sweep, which is why [`layout::Measure::Reading`] is the narrowest. Centred | |
| 114 | + | /// rather than left-aligned, because a narrowed column against the left edge of | |
| 115 | + | /// a wide terminal reads as a window that failed to resize. | |
| 116 | + | /// | |
| 117 | + | /// A terminal narrower than the cap is left alone rather than padded. There is | |
| 118 | + | /// no measure to enforce when the window is already tighter than it. | |
| 119 | + | fn measured(measure: layout::Measure, area: Rect) -> Rect { | |
| 120 | + | let cap = match measure { | |
| 121 | + | layout::Measure::Reading => 76, | |
| 122 | + | layout::Measure::Contained => 100, | |
| 123 | + | // Every column there is, which is what `Wide` means. Also the arm a | |
| 124 | + | // member added upstream lands in: a measure this renderer has not | |
| 125 | + | // learned should show the whole screen, not hide part of it. | |
| 126 | + | _ => return area, | |
| 127 | + | }; | |
| 128 | + | if area.width <= cap { | |
| 129 | + | return area; | |
| 130 | + | } | |
| 131 | + | Rect { | |
| 132 | + | x: area.x + (area.width - cap) / 2, | |
| 133 | + | width: cap, | |
| 134 | + | ..area | |
| 135 | + | } | |
| 136 | + | } | |
| 137 | + | ||
| 106 | 138 | /// The regions that fill the body, after the arrangement has had its say. | |
| 107 | 139 | /// | |
| 108 | 140 | /// The tabbed cut lives here and only here. It is the one place a described | |
| @@ -116,7 +148,7 @@ | |||
| 116 | 148 | .filter(|slot| !matches!(slot.kind, RegionKind::Band | RegionKind::Modal)); | |
| 117 | 149 | ||
| 118 | 150 | match screen.arrangement { | |
| 119 | - | layout::Arrangement::ListDetail { tabbed: true } => body.take(1).collect(), | |
| 151 | + | layout::Arrangement::ListDetail { tabbed: true, .. } => body.take(1).collect(), | |
| 120 | 152 | _ => body.collect(), | |
| 121 | 153 | } | |
| 122 | 154 | } |
| @@ -87,6 +87,15 @@ | |||
| 87 | 87 | rows(&buf).join(" ") | |
| 88 | 88 | } | |
| 89 | 89 | ||
| 90 | + | /// The column some text starts at, in whichever row holds it. | |
| 91 | + | /// | |
| 92 | + | /// A region's contents sit below its top border, so the row a word lands on is | |
| 93 | + | /// not the row the region starts on. Asserting against row 0 measures the | |
| 94 | + | /// border and not the content. | |
| 95 | + | fn column_of(rows: &[String], text: &str) -> Option<usize> { | |
| 96 | + | rows.iter().find_map(|row| row.find(text)) | |
| 97 | + | } | |
| 98 | + | ||
| 90 | 99 | fn shown(screen: &Screen, width: u16, height: u16) -> Vec<String> { | |
| 91 | 100 | let area = Rect::new(0, 0, width, height); | |
| 92 | 101 | let mut buf = Buffer::empty(area); | |
| @@ -1122,3 +1131,80 @@ | |||
| 1122 | 1131 | 0 | |
| 1123 | 1132 | ); | |
| 1124 | 1133 | } | |
| 1134 | + | ||
| 1135 | + | #[test] | |
| 1136 | + | fn a_terminal_and_a_webview_agree_about_a_screens_proportions() { | |
| 1137 | + | // The done-condition of `e0fd485e`: a screen described once, rendered by | |
| 1138 | + | // two hosts, agreeing about its proportions. The assertion that did not | |
| 1139 | + | // exist while each renderer held its own number. | |
| 1140 | + | let screen = Screen::sidebar_content("Mail") | |
| 1141 | + | .with(Slot::new("side", RegionKind::Sidebar).with(Node::text("Folders"))) | |
| 1142 | + | .with(Slot::new("main", RegionKind::Pane).with(Node::text("Messages"))); | |
| 1143 | + | ||
| 1144 | + | // A quarter of 100 columns is 25, and the webview writes the same quarter | |
| 1145 | + | // into its grid. Read off the description rather than off either renderer. | |
| 1146 | + | assert_eq!(screen.arrangement.share().as_percent(), 25); | |
| 1147 | + | assert_eq!(screen.arrangement.share().of(100), 25); | |
| 1148 | + | ||
| 1149 | + | // And the terminal honours it rather than a number of its own: "Folders" | |
| 1150 | + | // fits in 25 columns and "Messages" starts after them. | |
| 1151 | + | let out = shown(&screen, 100, 6); | |
| 1152 | + | assert!(column_of(&out, "Folders") == Some(0), "{out:?}"); | |
| 1153 | + | assert!( | |
| 1154 | + | column_of(&out, "Messages").is_some_and(|at| at >= 25), | |
| 1155 | + | "the content should start after the sidebar's quarter: {out:?}" | |
| 1156 | + | ); | |
| 1157 | + | } | |
| 1158 | + | ||
| 1159 | + | #[test] | |
| 1160 | + | fn a_narrower_share_moves_the_boundary_on_the_terminal_too() { | |
| 1161 | + | let screen = Screen::new( | |
| 1162 | + | "Mail", | |
| 1163 | + | layout::Arrangement::sidebar_content().with_share(layout::Share::percent(10)), | |
| 1164 | + | ) | |
| 1165 | + | .with(Slot::new("side", RegionKind::Sidebar).with(Node::text("F"))) | |
| 1166 | + | .with(Slot::new("main", RegionKind::Pane).with(Node::text("Messages"))); | |
| 1167 | + | ||
| 1168 | + | let out = shown(&screen, 100, 6); | |
| 1169 | + | assert!( | |
| 1170 | + | column_of(&out, "Messages").is_some_and(|at| (10..25).contains(&at)), | |
| 1171 | + | "the boundary should follow the described share: {out:?}" | |
| 1172 | + | ); | |
| 1173 | + | } | |
| 1174 | + | ||
| 1175 | + | #[test] | |
| 1176 | + | fn a_reading_measure_narrows_the_screen_and_centres_it() { | |
| 1177 | + | // The terminal's answer to `Measure`, and the one with a reason outside | |
| 1178 | + | // taste: past roughly 75 characters a line costs the reader the return | |
| 1179 | + | // sweep. | |
| 1180 | + | let wide = Screen::sidebar_content("Doc") | |
| 1181 | + | .with(Slot::new("main", RegionKind::Pane).with(Node::text("Words"))); | |
| 1182 | + | let reading = wide.clone().measured(layout::Measure::Reading); | |
| 1183 | + | ||
| 1184 | + | let full = shown(&wide, 120, 4); | |
| 1185 | + | let narrowed = shown(&reading, 120, 4); | |
| 1186 | + | ||
| 1187 | + | let at = |rows: &[String]| column_of(rows, "Words"); | |
| 1188 | + | assert!( | |
| 1189 | + | at(&narrowed) > at(&full), | |
| 1190 | + | "a narrowed screen is centred, so its content starts further in: \ | |
| 1191 | + | {full:?} then {narrowed:?}" | |
| 1192 | + | ); | |
| 1193 | + | } | |
| 1194 | + | ||
| 1195 | + | #[test] | |
| 1196 | + | fn a_terminal_narrower_than_the_measure_is_left_alone() { | |
| 1197 | + | // There is no measure to enforce when the window is already tighter than | |
| 1198 | + | // it, and padding one would waste the only columns there are. | |
| 1199 | + | let screen = Screen::sidebar_content("Doc") | |
| 1200 | + | .with(Slot::new("main", RegionKind::Pane).with(Node::text("Words"))) | |
| 1201 | + | .measured(layout::Measure::Reading); | |
| 1202 | + | ||
| 1203 | + | let out = shown(&screen, 40, 4); | |
| 1204 | + | // Drawn at all, and not squeezed into a centred column of a window that is | |
| 1205 | + | // already narrower than the cap. | |
| 1206 | + | assert!( | |
| 1207 | + | column_of(&out, "Words").is_some_and(|at| at < 15), | |
| 1208 | + | "{out:?}" | |
| 1209 | + | ); | |
| 1210 | + | } |
| @@ -135,11 +135,30 @@ | |||
| 135 | 135 | /// description becomes a framework. | |
| 136 | 136 | fn arrangement_class(arrangement: Arrangement) -> &'static str { | |
| 137 | 137 | match arrangement { | |
| 138 | - | Arrangement::ListDetail { tabbed: false } => "list-detail", | |
| 139 | - | Arrangement::ListDetail { tabbed: true } => "list-detail-tabbed", | |
| 140 | - | Arrangement::SidebarContent => "sidebar-content", | |
| 138 | + | Arrangement::ListDetail { tabbed: false, .. } => "list-detail", | |
| 139 | + | Arrangement::ListDetail { tabbed: true, .. } => "list-detail-tabbed", | |
| 140 | + | Arrangement::SidebarContent { .. } => "sidebar-content", | |
| 141 | 141 | } | |
| 142 | 142 | } | |
| 143 | + | ||
| 144 | + | /// The share, as the grid that honours it. | |
| 145 | + | /// | |
| 146 | + | /// `e0fd485e`. An inline style rather than a class, because the share is a | |
| 147 | + | /// number the description carries and a class can only name a number some | |
| 148 | + | /// stylesheet already fixed. Nothing in `makeover-webview` styled these | |
| 149 | + | /// classes at all before this, so no shipped rule is being overridden: the | |
| 150 | + | /// check the finding asked for was whether adopting the description changes | |
| 151 | + | /// what the webview draws, and there was nothing there to change. | |
| 152 | + | /// | |
| 153 | + | /// `fr` rather than a percentage, so the gap between the regions comes out | |
| 154 | + | /// of the whole rather than out of the second one. | |
| 155 | + | fn share_style(arrangement: Arrangement) -> String { | |
| 156 | + | let first = u16::from(arrangement.share().as_percent()); | |
| 157 | + | format!( | |
| 158 | + | " style=\"--region-share:{first}fr;--region-rest:{}fr\"", | |
| 159 | + | 100 - first | |
| 160 | + | ) | |
| 161 | + | } | |
| 143 | 162 | } | |
| 144 | 163 | ||
| 145 | 164 | impl Serves for Webview { | |
| @@ -153,7 +172,18 @@ | |||
| 153 | 172 | Self::arrangement_class(screen.arrangement), | |
| 154 | 173 | &self.emit, | |
| 155 | 174 | )); | |
| 156 | - | out.push_str("\">"); | |
| 175 | + | // How wide the screen runs, beside how its width is divided. Two | |
| 176 | + | // classes rather than one compound name: they vary independently, and a | |
| 177 | + | // `wide-list-detail` class per pairing is the enumeration `1786cb94` | |
| 178 | + | // settled against one level down. | |
| 179 | + | out.push(' '); | |
| 180 | + | out.push_str(&makeover_webview::class( | |
| 181 | + | &format!("measure-{}", screen.measure.as_str()), | |
| 182 | + | &self.emit, | |
| 183 | + | )); | |
| 184 | + | out.push('"'); | |
| 185 | + | out.push_str(&Self::share_style(screen.arrangement)); | |
| 186 | + | out.push('>'); | |
| 157 | 187 | ||
| 158 | 188 | // Notices before the regions, because a notice belongs to the screen | |
| 159 | 189 | // rather than to a place in it, and the first thing in the document is |
| @@ -1563,9 +1563,37 @@ | |||
| 1563 | 1563 | let tabbed = render(&Screen::list_detail("A", true)); | |
| 1564 | 1564 | let sidebar = render(&Screen::sidebar_content("A")); | |
| 1565 | 1565 | ||
| 1566 | - | assert!(plain.contains("class=\"list-detail\"")); | |
| 1567 | - | assert!(tabbed.contains("class=\"list-detail-tabbed\"")); | |
| 1568 | - | assert!(sidebar.contains("class=\"sidebar-content\"")); | |
| 1566 | + | // The measure rides in the same attribute, and the two vary independently: | |
| 1567 | + | // a compound `wide-list-detail` per pairing is the enumeration `1786cb94` | |
| 1568 | + | // settled against one level down. | |
| 1569 | + | assert!(plain.contains("class=\"list-detail measure-wide\"")); | |
| 1570 | + | assert!(tabbed.contains("class=\"list-detail-tabbed measure-wide\"")); | |
| 1571 | + | assert!(sidebar.contains("class=\"sidebar-content measure-wide\"")); | |
| 1572 | + | ||
| 1573 | + | let reading = render(&Screen::list_detail("A", false).measured(layout::Measure::Reading)); | |
| 1574 | + | assert!(reading.contains("class=\"list-detail measure-reading\"")); | |
| 1575 | + | } | |
| 1576 | + | ||
| 1577 | + | #[test] | |
| 1578 | + | fn a_screen_carries_the_share_as_a_number_rather_than_a_class() { | |
| 1579 | + | // `e0fd485e`. A share is a number the description carries, and a class can | |
| 1580 | + | // only name a number some stylesheet already fixed -- which is the drift | |
| 1581 | + | // the member exists to end, since a terminal has no stylesheet to read it | |
| 1582 | + | // out of. | |
| 1583 | + | let wide = render(&Screen::sidebar_content("A")); | |
| 1584 | + | assert!( | |
| 1585 | + | wide.contains("--region-share:25fr;--region-rest:75fr"), | |
| 1586 | + | "{wide}" | |
| 1587 | + | ); | |
| 1588 | + | ||
| 1589 | + | let narrow = render(&Screen::new( | |
| 1590 | + | "A", | |
| 1591 | + | layout::Arrangement::sidebar_content().with_share(layout::Share::percent(20)), | |
| 1592 | + | )); | |
| 1593 | + | assert!( | |
| 1594 | + | narrow.contains("--region-share:20fr;--region-rest:80fr"), | |
| 1595 | + | "{narrow}" | |
| 1596 | + | ); | |
| 1569 | 1597 | } | |
| 1570 | 1598 | ||
| 1571 | 1599 | #[test] | |
| @@ -1582,7 +1610,7 @@ | |||
| 1582 | 1610 | ); | |
| 1583 | 1611 | let html = Webview::new().with_emit(emit).screen(&screen); | |
| 1584 | 1612 | ||
| 1585 | - | assert!(html.contains("class=\"qs-list-detail\"")); | |
| 1613 | + | assert!(html.contains("class=\"qs-list-detail qs-measure-wide\"")); | |
| 1586 | 1614 | assert!(html.contains("qs-region qs-pane")); | |
| 1587 | 1615 | assert!(html.contains("qs-heading")); | |
| 1588 | 1616 | assert!(html.contains("qs-list")); | |
| @@ -2163,6 +2191,17 @@ | |||
| 2163 | 2191 | Slot::bespoke("bespoke", "map").with(Node::text("beside a fill")), | |
| 2164 | 2192 | ))); | |
| 2165 | 2193 | htmls.push(render(&Screen::list_detail("Tabbed", true))); | |
| 2194 | + | // Every measure, so the accounting below covers all three rather than only | |
| 2195 | + | // the default a screen gets for saying nothing. | |
| 2196 | + | for measure in [ | |
| 2197 | + | layout::Measure::Wide, | |
| 2198 | + | layout::Measure::Contained, | |
| 2199 | + | layout::Measure::Reading, | |
| 2200 | + | ] { | |
| 2201 | + | htmls.push(render( | |
| 2202 | + | &Screen::list_detail("Measured", false).measured(measure), | |
| 2203 | + | )); | |
| 2204 | + | } | |
| 2166 | 2205 | ||
| 2167 | 2206 | let acts = || Act::new("Remove", Action::post("/keys/7/delete")).tone(layout::Tone::Danger); | |
| 2168 | 2207 | for node in [ | |
| @@ -2287,6 +2326,14 @@ | |||
| 2287 | 2326 | "list-detail", | |
| 2288 | 2327 | "list-detail-tabbed", | |
| 2289 | 2328 | "sidebar-content", | |
| 2329 | + | // Measures, from `Screen::measure`. Placement for the same reason an | |
| 2330 | + | // arrangement is: the screen says which of the three it is, and what a | |
| 2331 | + | // measure means in pixels is the app's stylesheet answering once instead of | |
| 2332 | + | // 69 templates answering separately. `0eccff0d` moved the choice, not the | |
| 2333 | + | // number. | |
| 2334 | + | "measure-wide", | |
| 2335 | + | "measure-contained", | |
| 2336 | + | "measure-reading", | |
| 2290 | 2337 | // Regions, from `region_class`. | |
| 2291 | 2338 | "band", | |
| 2292 | 2339 | "bespoke", |