| 205 |
205 |
|
//! webview draws it from `:focus-visible`, egui refused the variant outright,
|
| 206 |
206 |
|
//! and quasi-tui honoured it once at startup and overrode it thereafter.
|
| 207 |
207 |
|
//!
|
|
208 |
+ |
//! 0.20.0 adds [`Region::Widget`], the third tier, and `#[non_exhaustive]` to
|
|
209 |
+ |
//! [`Region`] with it. Every vocabulary finding until now had two answers
|
|
210 |
+ |
//! available — grow the primitive set, or [`Region::Bespoke`] — and a whole
|
|
211 |
+ |
//! class of thing is wrong for both. A carousel is not a primitive, because a
|
|
212 |
+ |
//! terminal has none and that is the test `Node::Html` failed. It is not
|
|
213 |
+ |
//! bespoke either, because bespoke is what one app owns and every part of a
|
|
214 |
+ |
//! carousel is furniture plus members this crate already has.
|
|
215 |
+ |
//!
|
|
216 |
+ |
//! The cost of the binary was that refusing a primitive was expensive: the app
|
|
217 |
+ |
//! hand-rolls the thing forever, so the pressure always ran toward growing the
|
|
218 |
+ |
//! primitive set with one host's idioms. A named assembly changes what "no"
|
|
219 |
+ |
//! costs without changing what the vocabulary can say.
|
|
220 |
+ |
//!
|
|
221 |
+ |
//! MNW's carousel is the first consumer and was the finding that started it: one
|
|
222 |
+ |
//! partial, three pages, an ordered set of frames with a position, prev/next and
|
|
223 |
+ |
//! a dot strip, all of it sayable already and none of it nameable. See wiki
|
|
224 |
+ |
//! `widget-tier` for the ownership model, which is why this member carries a
|
|
225 |
+ |
//! name a renderer may decline to know.
|
|
226 |
+ |
//!
|
| 208 |
227 |
|
//! # Reach, focus and the focus ring
|
| 209 |
228 |
|
//!
|
| 210 |
229 |
|
//! Three terms, and no others, for what 0.19.0 moved out of the description.
|
| 220 |
239 |
|
//!
|
| 221 |
240 |
|
//! # Where the description stops
|
| 222 |
241 |
|
//!
|
| 223 |
|
- |
//! The bespoke widgets, a day-plan timeline and a kanban board and a calendar,
|
| 224 |
|
- |
//! are not describable here and will not become describable. A description
|
| 225 |
|
- |
//! expressive enough to produce a timeline is a widget library wearing a
|
| 226 |
|
- |
//! description's name. Generate the boring 80% so the bespoke 20% gets the
|
| 227 |
|
- |
//! attention.
|
|
242 |
+ |
//! A day-plan timeline, a kanban board and a calendar are not describable here
|
|
243 |
+ |
//! and will not become describable. A description expressive enough to produce
|
|
244 |
+ |
//! a timeline is a component library wearing a description's name. Generate the
|
|
245 |
+ |
//! boring 80% so the bespoke 20% gets the attention.
|
| 228 |
246 |
|
//!
|
| 229 |
247 |
|
//! [`Region::Bespoke`] is how that limit is stated rather than hidden. The
|
| 230 |
248 |
|
//! description names the *place* and the app owns the contents, so a screen
|
| 232 |
250 |
|
//! it, the four goingson screens that make the app worth using would need a
|
| 233 |
251 |
|
//! second, undescribed path beside the router, and two paths is how a
|
| 234 |
252 |
|
//! vocabulary starts drifting from its app again.
|
|
253 |
+ |
//!
|
|
254 |
+ |
//! [`Region::Widget`] sits between that limit and the primitives, and it does
|
|
255 |
+ |
//! not move the limit. A widget is an assembly of members this crate *already*
|
|
256 |
+ |
//! has, under a name a renderer may or may not recognise. Anything that needs a
|
|
257 |
+ |
//! member the vocabulary does not have is still a finding about the vocabulary
|
|
258 |
+ |
//! or still bespoke; naming an assembly buys no new expressive power, which is
|
|
259 |
+ |
//! exactly why it is safe to let the set grow outside this crate.
|
| 235 |
260 |
|
|
| 236 |
261 |
|
#![forbid(unsafe_code)]
|
| 237 |
262 |
|
|
| 1259 |
1284 |
|
/// `layout.css` currently names exactly two things, `.raised` and `.well`, so
|
| 1260 |
1285 |
|
/// this layer is absent rather than divergent, which makes it the cheapest of
|
| 1261 |
1286 |
|
/// the schemas to add and the easiest to over-build.
|
|
1287 |
+ |
///
|
|
1288 |
+ |
/// `#[non_exhaustive]` arrives with [`Region::Widget`], the pairing [`RowPart`]
|
|
1289 |
+ |
/// made at 0.9.0 and [`Readiness`] at 0.12.0, and for the same reason: the
|
|
1290 |
+ |
/// member after this one should not be a lockstep event across three renderers.
|
| 1262 |
1291 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
1292 |
+ |
#[non_exhaustive]
|
| 1263 |
1293 |
|
pub enum Region<'a> {
|
| 1264 |
1294 |
|
/// A full-width strip with a title slot and an actions cluster, either of
|
| 1265 |
1295 |
|
/// which may be empty. goingson's `.page-header`, Balanced Breakfast's
|
| 1298 |
1328 |
|
/// What the app calls it. Never interpreted here.
|
| 1299 |
1329 |
|
name: &'a str,
|
| 1300 |
1330 |
|
},
|
|
1331 |
+ |
/// A named assembly of things the vocabulary already says.
|
|
1332 |
+ |
///
|
|
1333 |
+ |
/// The third tier, between a primitive and [`Bespoke`](Self::Bespoke).
|
|
1334 |
+ |
/// Stated by Max 2026-08-12 answering the carousel: "something in between a
|
|
1335 |
+ |
/// primitive and a bespoke interface, like a widget, which is just an
|
|
1336 |
+ |
/// assembly of primitives." Full note: wiki `widget-tier`.
|
|
1337 |
+ |
///
|
|
1338 |
+ |
/// # What separates it from the two members either side
|
|
1339 |
+ |
///
|
|
1340 |
+ |
/// A primitive is a thing every renderer draws from scratch, and the test
|
|
1341 |
+ |
/// it has to pass is that every host has an honest answer. A carousel fails
|
|
1342 |
+ |
/// that test — a terminal has no carousel — which is the same refusal
|
|
1343 |
+ |
/// `Node::Html` got and is why the carousel sat unsayable for months.
|
|
1344 |
+ |
///
|
|
1345 |
+ |
/// [`Bespoke`](Self::Bespoke) fails it from the other side. Bespoke is for
|
|
1346 |
+ |
/// what one app owns and nobody will build twice, and it carries *no*
|
|
1347 |
+ |
/// contents: the description names the place and stops. A carousel is
|
|
1348 |
+ |
/// furniture any app would have, and every part of it — an ordered set of
|
|
1349 |
+ |
/// frames, a position, prev and next, a strip of position indicators — is
|
|
1350 |
+ |
/// already sayable. Only the assembly had no name.
|
|
1351 |
+ |
///
|
|
1352 |
+ |
/// So this member is the pair the other two are not: a name **and**
|
|
1353 |
+ |
/// contents. The contents are the assembly, in the region's own body, said
|
|
1354 |
+ |
/// in members that already exist.
|
|
1355 |
+ |
///
|
|
1356 |
+ |
/// # Why the name does not have to be understood
|
|
1357 |
+ |
///
|
|
1358 |
+ |
/// A renderer that recognises the name draws it the way its host does it: a
|
|
1359 |
+ |
/// carousel in a webview, a pager with a count in a terminal, a selector in
|
|
1360 |
+ |
/// egui. A renderer that does not recognise it walks the body, which is
|
|
1361 |
+ |
/// primitives all the way down and which it can already draw.
|
|
1362 |
+ |
///
|
|
1363 |
+ |
/// That is what lets the widget set be **open** without every renderer
|
|
1364 |
+ |
/// knowing every widget. An unrecognised widget degrades to its assembly
|
|
1365 |
+ |
/// instead of failing, so a second or third party can name one without
|
|
1366 |
+ |
/// three renderers releasing in lockstep to accept it. Contrast
|
|
1367 |
+ |
/// [`Bespoke`](Self::Bespoke), which no renderer can degrade: there is
|
|
1368 |
+ |
/// nothing under it to fall back to.
|
|
1369 |
+ |
///
|
|
1370 |
+ |
/// # What it does not do
|
|
1371 |
+ |
///
|
|
1372 |
+ |
/// It does not make a timeline describable, and the refusal in the crate
|
|
1373 |
+ |
/// header stands unchanged. A widget is an assembly of things the
|
|
1374 |
+ |
/// vocabulary *already* says; anything that needs a member the vocabulary
|
|
1375 |
+ |
/// does not have is a finding about the vocabulary or it is
|
|
1376 |
+ |
/// [`Bespoke`](Self::Bespoke). A widget is never the way a primitive gets
|
|
1377 |
+ |
/// added by the back door.
|
|
1378 |
+ |
Widget {
|
|
1379 |
+ |
/// What the assembly is called. This crate never interprets it, and a
|
|
1380 |
+ |
/// renderer is free not to know it.
|
|
1381 |
+ |
name: &'a str,
|
|
1382 |
+ |
},
|
| 1301 |
1383 |
|
}
|
| 1302 |
1384 |
|
|
| 1303 |
|
- |
impl Region<'_> {
|
|
1385 |
+ |
impl<'a> Region<'a> {
|
| 1304 |
1386 |
|
/// How the region sits on what is behind it.
|
| 1305 |
1387 |
|
#[must_use]
|
| 1306 |
1388 |
|
pub const fn depth(self) -> Depth {
|
| 1312 |
1394 |
|
// Flat because it inherits: a bespoke region takes the depth of
|
| 1313 |
1395 |
|
// whatever frames it. An app that wants its timeline in a well puts
|
| 1314 |
1396 |
|
// it in a `Pane`, which composes rather than adding a knob here.
|
| 1315 |
|
- |
Self::Bespoke { .. } => Depth::Flat,
|
|
1397 |
+ |
//
|
|
1398 |
+ |
// A widget inherits for the same reason and it matters more here,
|
|
1399 |
+ |
// because a widget is drawn by whichever renderer recognises it. A
|
|
1400 |
+ |
// depth set here would be this crate deciding that a carousel is
|
|
1401 |
+ |
// raised on every host, which is the kind of value the deferral
|
|
1402 |
+ |
// rule exists to refuse.
|
|
1403 |
+ |
Self::Bespoke { .. } | Self::Widget { .. } => Depth::Flat,
|
| 1316 |
1404 |
|
}
|
| 1317 |
1405 |
|
}
|
| 1318 |
1406 |
|
|
| 1322 |
1410 |
|
/// to the right drawing code. This is how it tells the two apart, and the
|
| 1323 |
1411 |
|
/// reason it is a method rather than a `matches!` at each renderer: there
|
| 1324 |
1412 |
|
/// is exactly one opaque member and there should stay exactly one.
|
|
1413 |
+ |
///
|
|
1414 |
+ |
/// [`Widget`](Self::Widget) is described, and that is the whole of what
|
|
1415 |
+ |
/// separates it from [`Bespoke`](Self::Bespoke) here. Both carry a name
|
|
1416 |
+ |
/// this crate never interprets; only one of them carries contents under it.
|
|
1417 |
+ |
/// A renderer that does not recognise a widget's name still walks its body,
|
|
1418 |
+ |
/// so there is nothing for it to hand over and nothing it cannot draw.
|
| 1325 |
1419 |
|
#[must_use]
|
| 1326 |
1420 |
|
pub const fn described(self) -> bool {
|
| 1327 |
1421 |
|
!matches!(self, Self::Bespoke { .. })
|
| 1328 |
1422 |
|
}
|
|
1423 |
+ |
|
|
1424 |
+ |
/// The name an app gave this region, if it gave one.
|
|
1425 |
+ |
///
|
|
1426 |
+ |
/// [`Bespoke`](Self::Bespoke) and [`Widget`](Self::Widget) are the two
|
|
1427 |
+ |
/// members that carry a name, for two different purposes: one says what the
|
|
1428 |
+ |
/// app will fill the space with, the other says what the assembly under it
|
|
1429 |
+ |
/// is called. A renderer dispatching on either wants the string without
|
|
1430 |
+ |
/// caring which member it came from, and writing that `matches!` at each
|
|
1431 |
+ |
/// renderer is how the two drift apart.
|
|
1432 |
+ |
#[must_use]
|
|
1433 |
+ |
pub const fn name(self) -> Option<&'a str> {
|
|
1434 |
+ |
match self {
|
|
1435 |
+ |
Self::Bespoke { name } | Self::Widget { name } => Some(name),
|
|
1436 |
+ |
// Spelled out rather than a wildcard, so a member added later has
|
|
1437 |
+ |
// to answer whether it carries a name instead of inheriting `None`
|
|
1438 |
+ |
// by sitting under a `_`.
|
|
1439 |
+ |
Self::Band
|
|
1440 |
+ |
| Self::Sidebar
|
|
1441 |
+ |
| Self::Pane
|
|
1442 |
+ |
| Self::Split
|
|
1443 |
+ |
| Self::TabGroup
|
|
1444 |
+ |
| Self::Modal => None,
|
|
1445 |
+ |
}
|
|
1446 |
+ |
}
|
| 1329 |
1447 |
|
}
|
| 1330 |
1448 |
|
|
| 1331 |
1449 |
|
/// How much of the width an arrangement's first region takes.
|
| 2694 |
2812 |
|
Region::Split,
|
| 2695 |
2813 |
|
Region::TabGroup,
|
| 2696 |
2814 |
|
Region::Modal,
|
|
2815 |
+ |
// A widget is described, and that is the whole of what separates it
|
|
2816 |
+ |
// from a bespoke here. Both carry a name this crate never reads;
|
|
2817 |
+ |
// only one of them has contents under it that a renderer which does
|
|
2818 |
+ |
// not know the name can still walk.
|
|
2819 |
+ |
Region::Widget { name: "carousel" },
|
| 2697 |
2820 |
|
] {
|
| 2698 |
2821 |
|
assert!(r.described(), "{r:?} should be describable");
|
| 2699 |
2822 |
|
}
|
| 2700 |
2823 |
|
assert!(!Region::Bespoke { name: "day-plan" }.described());
|
| 2701 |
2824 |
|
}
|
| 2702 |
2825 |
|
|
|
2826 |
+ |
#[test]
|
|
2827 |
+ |
fn a_widget_inherits_its_depth_the_way_a_bespoke_does() {
|
|
2828 |
+ |
// Stronger than the bespoke case: a widget is drawn by whichever
|
|
2829 |
+ |
// renderer recognises the name, so a depth chosen here would be this
|
|
2830 |
+ |
// crate deciding a carousel is raised on every host.
|
|
2831 |
+ |
assert_eq!(Region::Widget { name: "carousel" }.depth(), Depth::Flat);
|
|
2832 |
+ |
assert_eq!(Region::Widget { name: "pager" }.depth(), Depth::Flat);
|
|
2833 |
+ |
}
|
|
2834 |
+ |
|
|
2835 |
+ |
#[test]
|
|
2836 |
+ |
fn a_name_is_readable_without_asking_which_member_carried_it() {
|
|
2837 |
+ |
// A renderer dispatching on a name wants the string, not the member.
|
|
2838 |
+ |
// Writing that `matches!` at each renderer is how the two drift apart.
|
|
2839 |
+ |
assert_eq!(Region::Widget { name: "carousel" }.name(), Some("carousel"));
|
|
2840 |
+ |
assert_eq!(
|
|
2841 |
+ |
Region::Bespoke { name: "day-plan" }.name(),
|
|
2842 |
+ |
Some("day-plan")
|
|
2843 |
+ |
);
|
|
2844 |
+ |
|
|
2845 |
+ |
for r in [
|
|
2846 |
+ |
Region::Band,
|
|
2847 |
+ |
Region::Sidebar,
|
|
2848 |
+ |
Region::Pane,
|
|
2849 |
+ |
Region::Split,
|
|
2850 |
+ |
Region::TabGroup,
|
|
2851 |
+ |
Region::Modal,
|
|
2852 |
+ |
] {
|
|
2853 |
+ |
assert_eq!(r.name(), None, "{r:?} names nothing an app chose");
|
|
2854 |
+ |
}
|
|
2855 |
+ |
}
|
|
2856 |
+ |
|
| 2703 |
2857 |
|
#[test]
|
| 2704 |
2858 |
|
fn a_bespoke_region_inherits_its_depth_rather_than_choosing_one() {
|
| 2705 |
2859 |
|
// The app owns the contents, not the placement. An app that wants its
|