Skip to main content

max / quasi

Say a widget: RegionKind::Widget, and render it RegionKind::Widget plus Slot::widget(), the router half of makeover-layout 0.20.0's third tier. The one member that is a name AND contents: the body is ordinary description, so it is the assembly rather than a hole the host fills. quasi-webview emits class="region widget" and data-widget="<name>". An attribute rather than a class because a widget name is app vocabulary and the classes there are the renderer's. Nothing has to recognise it -- the body under it is already the whole assembly, so a carousel with no script is its frames, which is the JS-off fallback the shipped partial has. quasi-tui needed no change beyond the piece rename, which is the degradation working rather than an omission: it walks the body already. The containment model needed no change either. Containment::Opaque is a claim about knowledge, not about emptiness, and a widget is knowable, so it is Blocks like every other region and the matches!(Bespoke) was already right.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-14 15:16 UTC
Signed with PGP, not checked
Commit: 2bcfe8aed2c6e22a5896e89fd9e3ef5abade4c9f
Parent: 87605d7
8 files changed, +79 insertions, -41 deletions
M Cargo.lock +11 -21
@@ -2297,8 +2297,6 @@
2297 2297 [[package]]
2298 2298 name = "makeover-immediate"
2299 2299 version = "0.18.0"
2300 - source = "registry+https://github.com/rust-lang/crates.io-index"
2301 - checksum = "333aefe3ef5c2c8df2df1fc7adf7a54613afaf58107e89916b0d0d6ca488dd6e"
2302 2300 dependencies = [
2303 2301 "egui",
2304 2302 "egui_extras",
@@ -2307,15 +2305,11 @@
2307 2305
2308 2306 [[package]]
2309 2307 name = "makeover-layout"
2310 - version = "0.19.0"
2311 - source = "registry+https://github.com/rust-lang/crates.io-index"
2312 - checksum = "2c5e6bd13bcf40df8f4eae9736591a1ecef32aedc7205eb497e0633a8a9d2b35"
2308 + version = "0.20.0"
2313 2309
2314 2310 [[package]]
2315 2311 name = "makeover-touch"
2316 2312 version = "0.12.0"
2317 - source = "registry+https://github.com/rust-lang/crates.io-index"
2318 - checksum = "6a9314d98c2a457b901cb93274b3b39686a67a198499eeee407b978dae7aa054"
2319 2313 dependencies = [
2320 2314 "makeover-geometry",
2321 2315 "makeover-layout",
@@ -2323,9 +2317,7 @@
2323 2317
2324 2318 [[package]]
2325 2319 name = "makeover-tui"
2326 - version = "0.18.0"
2327 - source = "registry+https://github.com/rust-lang/crates.io-index"
2328 - checksum = "a9a012eac65ca4284b98b4da74f23bbd7753d046e2cce15b764bf8913b1a9d8d"
2320 + version = "0.19.0"
2329 2321 dependencies = [
2330 2322 "makeover",
2331 2323 "makeover-layout",
@@ -2334,9 +2326,7 @@
2334 2326
2335 2327 [[package]]
2336 2328 name = "makeover-webview"
2337 - version = "0.35.0"
2338 - source = "registry+https://github.com/rust-lang/crates.io-index"
2339 - checksum = "115be68562840a2d96630116c0ef745431e608c5c67557f0025fdd7315da73bd"
2329 + version = "0.35.1"
2340 2330 dependencies = [
2341 2331 "makeover-geometry",
2342 2332 "makeover-layout",
@@ -5514,6 +5504,14 @@
5514 5504 source = "registry+https://github.com/rust-lang/crates.io-index"
5515 5505 checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
5516 5506
5507 + [[patch.unused]]
5508 + name = "synckit-client"
5509 + version = "0.8.0"
5510 +
5511 + [[patch.unused]]
5512 + name = "synckit-config"
5513 + version = "0.2.0"
5514 +
5517 5515 [[patch.unused]]
5518 5516 name = "kberg"
5519 5517 version = "0.1.0"
@@ -5529,11 +5527,3 @@
5529 5527 [[patch.unused]]
5530 5528 name = "tagtree"
5531 5529 version = "0.4.0"
5532 -
5533 - [[patch.unused]]
5534 - name = "synckit-client"
5535 - version = "0.8.0"
5536 -
5537 - [[patch.unused]]
5538 - name = "synckit-config"
5539 - version = "0.2.0"
@@ -13,4 +13,4 @@
13 13 workspace = true
14 14
15 15 [dependencies]
16 - makeover-layout = "0.19.0"
16 + makeover-layout = "0.20.0"
@@ -14,13 +14,13 @@
14 14
15 15 [dependencies]
16 16 quasi-router = { path = "../quasi-router", version = "0.2.0" }
17 - makeover-layout = "0.19.0"
17 + makeover-layout = "0.20.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
21 21 # finds the shape, and the shared crate takes it after it is known rather than
22 22 # before.
23 - makeover-tui = { version = "0.18.0", features = ["theme"] }
23 + makeover-tui = { version = "0.19.0", features = ["theme"] }
24 24 ratatui = { version = "0.30", default-features = false }
25 25 # `Node::Rich` carries markdown source. A terminal has no markup to hand it to,
26 26 # so it takes the runs: `render_runs` is the words with the marks that were over
@@ -15,7 +15,7 @@
15 15 [dependencies]
16 16 quasi-router = { path = "../quasi-router", version = "0.2.0" }
17 17 quasi-http = { path = "../quasi-http", version = "0.2.0" }
18 - makeover-layout = "0.19.0"
18 + makeover-layout = "0.20.0"
19 19 makeover-webview = "0.35.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.
@@ -1043,6 +1043,24 @@
1043 1043 /// What the app calls it. Never interpreted here.
1044 1044 name: String,
1045 1045 },
1046 + /// A named assembly of things the description already says.
1047 + ///
1048 + /// The third tier, and the one member that is a name *and* contents. See
1049 + /// [`layout::Region::Widget`] for what separates it from the two either
1050 + /// side of it; the short form is that a primitive has to be drawable by
1051 + /// every host from scratch and a bespoke carries nothing under it, and a
1052 + /// carousel is neither.
1053 + ///
1054 + /// The body is the assembly and it is ordinary description: a renderer that
1055 + /// does not recognise the name walks it and draws primitives, which is why
1056 + /// naming one costs no renderer release. Contrast
1057 + /// [`Bespoke`](Self::Bespoke), whose body a renderer can draw but whose
1058 + /// *fill* only the host has.
1059 + Widget {
1060 + /// What the assembly is called. Never interpreted here, and a renderer
1061 + /// is free not to know it.
1062 + name: String,
1063 + },
1046 1064 }
1047 1065
1048 1066 impl RegionKind {
@@ -1057,6 +1075,7 @@
1057 1075 Self::TabGroup => layout::Region::TabGroup,
1058 1076 Self::Modal => layout::Region::Modal,
1059 1077 Self::Bespoke { name } => layout::Region::Bespoke { name },
1078 + Self::Widget { name } => layout::Region::Widget { name },
1060 1079 }
1061 1080 }
1062 1081
@@ -1137,6 +1156,17 @@
1137 1156 Self::new(id, RegionKind::Bespoke { name: name.into() })
1138 1157 }
1139 1158
1159 + /// A named assembly, whose body says what it is made of.
1160 + ///
1161 + /// The body is not optional in spirit, though nothing here enforces it: a
1162 + /// widget with an empty body is a [`bespoke`](Self::bespoke) that has
1163 + /// mislaid its host fill, and a renderer that does not know the name will
1164 + /// draw nothing at all. Assemble it out of members the description already
1165 + /// has, the way [`layout::Region::Widget`] describes.
1166 + pub fn widget(id: impl Into<String>, name: impl Into<String>) -> Self {
1167 + Self::new(id, RegionKind::Widget { name: name.into() })
1168 + }
1169 +
1140 1170 /// Add a node, chaining.
1141 1171 #[must_use]
1142 1172 pub fn with(mut self, node: Node) -> Self {
@@ -62,8 +62,8 @@
62 62 pub use runtime::{Key, Runtime, Step};
63 63 pub use view::View;
64 64
65 + use makeover_tui::piece::PieceStyle;
65 66 use makeover_tui::table::TableStyle;
66 - use makeover_tui::widget::WidgetStyle;
67 67 use makeover_tui::{Fidelity, Palette, Theme};
68 68 use quasi_router::{Node, Screen};
69 69 use ratatui::buffer::Buffer;
@@ -72,14 +72,14 @@
72 72 /// A terminal renderer for a described screen.
73 73 ///
74 74 /// Holds what a drawing needs and no screen state: the theme's colours, the
75 - /// terminal's colour fidelity, and the table and widget styles derived from
75 + /// terminal's colour fidelity, and the table and piece styles derived from
76 76 /// both. A host makes one and keeps it.
77 77 #[derive(Debug, Clone)]
78 78 pub struct Tui {
79 79 theme: Theme,
80 80 palette: Palette,
81 81 table: TableStyle,
82 - widget: WidgetStyle,
82 + piece: PieceStyle,
83 83 }
84 84
85 85 impl Tui {
@@ -90,7 +90,7 @@
90 90 Self {
91 91 palette: theme.palette(fidelity),
92 92 table: TableStyle::from_theme(&theme),
93 - widget: WidgetStyle::from_theme(&theme),
93 + piece: PieceStyle::from_theme(&theme),
94 94 theme,
95 95 }
96 96 }
@@ -170,8 +170,8 @@
170 170 /// ratatui `Style` out — so they went to `makeover-tui` 0.16.0 with the
171 171 /// drawings that read them, and every terminal app in the tree now says a
172 172 /// danger tone the same way.
173 - pub(crate) const fn style(&self) -> &WidgetStyle {
174 - &self.widget
173 + pub(crate) const fn style(&self) -> &PieceStyle {
174 + &self.piece
175 175 }
176 176 }
177 177
@@ -5,7 +5,7 @@
5 5 //! no way to honour, and each one is a finding rather than an omission.
6 6
7 7 use makeover_layout as layout;
8 - use makeover_tui::{table, text, widget};
8 + use makeover_tui::{piece, table, text};
9 9 use quasi_router::{Act, Cell, Cells, Field, Figure, Meter, Node, Part, Row, Tag};
10 10 use ratatui::buffer::Buffer;
11 11 use ratatui::layout::Rect;
@@ -559,7 +559,7 @@
559 559 /// `makeover-tui`'s answers now. What is left here is the translation: our
560 560 /// owned [`Tag`] into the parts the shared drawing takes.
561 561 fn tag_span(tui: &Tui, tag: &Tag, focused: bool) -> Span<'static> {
562 - widget::token(
562 + piece::token(
563 563 tui.style(),
564 564 &tag.label,
565 565 tag.kind,
@@ -576,27 +576,27 @@
576 576 /// show, and a confirmation is a question asked after the press, which is the
577 577 /// runtime's. [`quasi_router::Act::as_layout`] says the same at the seam.
578 578 fn act_line(tui: &Tui, act: &Act, focused: bool) -> Line<'static> {
579 - widget::act(tui.style(), &act.as_layout(), focused)
579 + piece::act(tui.style(), &act.as_layout(), focused)
580 580 }
581 581
582 582 /// A meter as a line, bar and label.
583 583 fn meter_line(tui: &Tui, meter: &Meter) -> Line<'static> {
584 - widget::meter(tui.style(), &meter.as_layout())
584 + piece::meter(tui.style(), &meter.as_layout())
585 585 }
586 586
587 587 /// A figure takes two rows: the number, then what it counts.
588 588 fn figure_height(_tui: &Tui, figure: &Figure, width: u16) -> u16 {
589 - widget::figure_height(&figure.as_layout(), width)
589 + piece::figure_height(&figure.as_layout(), width)
590 590 }
591 591
592 592 fn draw_figure(tui: &Tui, figure: &Figure, area: Rect, buf: &mut Buffer) -> u16 {
593 - widget::figure(tui.style(), &figure.as_layout(), area, buf)
593 + piece::figure(tui.style(), &figure.as_layout(), area, buf)
594 594 }
595 595
596 596 /// A question takes its label row, its value row, and a row for whatever went
597 597 /// wrong.
598 598 fn field_height(tui: &Tui, field: &Field, width: u16) -> u16 {
599 - field.with_layout(|field| widget::field_height(tui.style(), &field, width))
599 + field.with_layout(|field| piece::field_height(tui.style(), &field, width))
600 600 }
601 601
602 602 fn draw_field(pass: &mut Pass<'_>, field: &Field, area: Rect, buf: &mut Buffer) -> u16 {
@@ -620,12 +620,12 @@
620 620 // `Node::SELECTED` is quasi's submission convention and not a fact about
621 621 // what a tick looks like.
622 622 let held = if matches!(field.kind, layout::FieldKind::Checkbox) {
623 - widget::Held::On(held == Node::SELECTED)
623 + piece::Held::On(held == Node::SELECTED)
624 624 } else {
625 - widget::Held::Text(held)
625 + piece::Held::Text(held)
626 626 };
627 627
628 - field.with_layout(|described| widget::field(tui.style(), &described, held, focused, area, buf))
628 + field.with_layout(|described| piece::field(tui.style(), &described, held, focused, area, buf))
629 629 }
630 630
631 631 /// A tabs strip, a segmented control and a toggle, all as one row of labels.
@@ -1365,6 +1365,7 @@
1365 1365 quasi_router::RegionKind::TabGroup => "tabgroup",
1366 1366 quasi_router::RegionKind::Modal => "modal",
1367 1367 quasi_router::RegionKind::Bespoke { .. } => "bespoke",
1368 + quasi_router::RegionKind::Widget { .. } => "widget",
1368 1369 };
1369 1370
1370 1371 out.push_str("<div id=\"");
@@ -1383,6 +1384,23 @@
1383 1384 out.push('"');
1384 1385 }
1385 1386
1387 + if let quasi_router::RegionKind::Widget { name } = &slot.kind {
1388 + // Also never interpreted here, and for a different purpose: this is the
1389 + // hook a stylesheet or a script attaches to in order to draw the
1390 + // assembly the way a browser does it. It is an attribute rather than a
1391 + // class because a widget name is app vocabulary and classes here are
1392 + // this renderer's.
1393 + //
1394 + // What makes it safe to emit and ignore is that the body under it is
1395 + // already the whole assembly in primitives. Nothing recognises
1396 + // `data-widget` and the region still renders -- a carousel degrades to
1397 + // its frames, which is the JS-off fallback the shipped partial has and
1398 + // the reason the tier does not need three renderers to release in step.
1399 + out.push_str(" data-widget=\"");
1400 + out.push_str(&escape(name));
1401 + out.push('"');
1402 + }
1403 +
1386 1404 if matches!(slot.readiness, layout::Readiness::Pending) {
1387 1405 out.push_str(" aria-busy=\"true\"");
1388 1406 }