| 40 |
40 |
|
//! - Structure. [`Region`] for the parts of a screen, [`Arrangement`] for how
|
| 41 |
41 |
|
//! a screen is put together.
|
| 42 |
42 |
|
//!
|
| 43 |
|
- |
//! Two things are absent on purpose rather than pending. **Validation** is not
|
| 44 |
|
- |
//! part of [`Field`]: neither app has a shared story, and a schema describing
|
| 45 |
|
- |
//! fields but not constraints acquires a constraint layer per app, which is how
|
| 46 |
|
- |
//! the divergence this crate exists to end got started. **The bespoke widgets**
|
| 47 |
|
- |
//! (a day-plan timeline, a kanban board, a calendar) are not here and are not
|
| 48 |
|
- |
//! queued: a description expressive enough to produce them is not a description
|
| 49 |
|
- |
//! any more. Generate the boring 80% so the bespoke 20% gets the attention.
|
|
43 |
+ |
//! **Validation** is absent on purpose rather than pending: neither app has a
|
|
44 |
+ |
//! shared story, and a schema describing fields but not constraints acquires a
|
|
45 |
+ |
//! constraint layer per app, which is how the divergence this crate exists to
|
|
46 |
+ |
//! end got started.
|
|
47 |
+ |
//!
|
|
48 |
+ |
//! # Where the description stops
|
|
49 |
+ |
//!
|
|
50 |
+ |
//! The bespoke widgets, a day-plan timeline and a kanban board and a calendar,
|
|
51 |
+ |
//! are not describable here and will not become describable. A description
|
|
52 |
+ |
//! expressive enough to produce a timeline is a widget library wearing a
|
|
53 |
+ |
//! description's name. Generate the boring 80% so the bespoke 20% gets the
|
|
54 |
+ |
//! attention.
|
|
55 |
+ |
//!
|
|
56 |
+ |
//! [`Region::Bespoke`] is how that limit is stated rather than hidden. The
|
|
57 |
+ |
//! description names the *place* and the app owns the contents, so a screen
|
|
58 |
+ |
//! containing a timeline is still a whole screen and still routable. Without
|
|
59 |
+ |
//! it, the four goingson screens that make the app worth using would need a
|
|
60 |
+ |
//! second, undescribed path beside the router, and two paths is how a
|
|
61 |
+ |
//! vocabulary starts drifting from its app again.
|
| 50 |
62 |
|
|
| 51 |
63 |
|
#![forbid(unsafe_code)]
|
| 52 |
64 |
|
|
| 476 |
488 |
|
/// A named part of a screen.
|
| 477 |
489 |
|
///
|
| 478 |
490 |
|
/// The thing `makeover-geometry` deliberately does not name: it names the space
|
| 479 |
|
- |
/// *between* things by relationship, and nothing named the things. Six members,
|
| 480 |
|
- |
/// taken from what the two webview apps actually use. Both apps' `layout.css`
|
| 481 |
|
- |
/// currently names exactly two things, `.raised` and `.well`, so this layer is
|
| 482 |
|
- |
/// absent rather than divergent, which makes it the cheapest of the schemas to
|
| 483 |
|
- |
/// add and the easiest to over-build.
|
|
491 |
+ |
/// *between* things by relationship, and nothing named the things. Six named
|
|
492 |
+ |
/// members, taken from what the two webview apps actually use, plus
|
|
493 |
+ |
/// [`Region::Bespoke`] for the parts no description should reach. Both apps'
|
|
494 |
+ |
/// `layout.css` currently names exactly two things, `.raised` and `.well`, so
|
|
495 |
+ |
/// this layer is absent rather than divergent, which makes it the cheapest of
|
|
496 |
+ |
/// the schemas to add and the easiest to over-build.
|
| 484 |
497 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 485 |
|
- |
pub enum Region {
|
|
498 |
+ |
pub enum Region<'a> {
|
| 486 |
499 |
|
/// A full-width strip with a title slot and an actions cluster, either of
|
| 487 |
500 |
|
/// which may be empty. goingson's `.page-header`, Balanced Breakfast's
|
| 488 |
501 |
|
/// `.header` and `.detail-header` are all this, differing only in which
|
| 498 |
511 |
|
TabGroup,
|
| 499 |
512 |
|
/// Content over a scrim, taking input until dismissed.
|
| 500 |
513 |
|
Modal,
|
|
514 |
+ |
/// A region this crate names the *place* of and nothing else. The app owns
|
|
515 |
+ |
/// what goes in it.
|
|
516 |
+ |
///
|
|
517 |
+ |
/// The escape hatch, and the thing that keeps the description honest about
|
|
518 |
+ |
/// its own limits. A day-plan timeline, a kanban board, a calendar and the
|
|
519 |
+ |
/// paint interaction over the timeline are not describable here and are not
|
|
520 |
+ |
/// going to become describable: a description expressive enough to produce
|
|
521 |
+ |
/// a timeline is a widget library wearing a description's name.
|
|
522 |
+ |
///
|
|
523 |
+ |
/// But a screen containing one still has to be a screen. Without this
|
|
524 |
+ |
/// member the description covers only the boring screens, and the four that
|
|
525 |
+ |
/// make goingson worth using would need a second, undescribed path beside
|
|
526 |
+ |
/// the router. Two paths is how the vocabulary starts drifting from the app
|
|
527 |
+ |
/// again, which is the exact failure this crate exists to end.
|
|
528 |
+ |
///
|
|
529 |
+ |
/// So the description says "a thing called `day-plan` goes here" and stops.
|
|
530 |
+ |
/// The name is opaque: this crate never interprets it, and no renderer is
|
|
531 |
+ |
/// expected to know what it means beyond handing the space over.
|
|
532 |
+ |
Bespoke {
|
|
533 |
+ |
/// What the app calls it. Never interpreted here.
|
|
534 |
+ |
name: &'a str,
|
|
535 |
+ |
},
|
| 501 |
536 |
|
}
|
| 502 |
537 |
|
|
| 503 |
|
- |
impl Region {
|
|
538 |
+ |
impl Region<'_> {
|
| 504 |
539 |
|
/// How the region sits on what is behind it.
|
| 505 |
540 |
|
#[must_use]
|
| 506 |
541 |
|
pub const fn depth(self) -> Depth {
|
| 509 |
544 |
|
// A pane is looked into, the same as a table body or a tag tree.
|
| 510 |
545 |
|
Self::Pane => Depth::Well,
|
| 511 |
546 |
|
Self::Modal => Depth::Raised,
|
|
547 |
+ |
// Flat because it inherits: a bespoke region takes the depth of
|
|
548 |
+ |
// whatever frames it. An app that wants its timeline in a well puts
|
|
549 |
+ |
// it in a `Pane`, which composes rather than adding a knob here.
|
|
550 |
+ |
Self::Bespoke { .. } => Depth::Flat,
|
| 512 |
551 |
|
}
|
| 513 |
552 |
|
}
|
|
553 |
+ |
|
|
554 |
+ |
/// Whether this crate can say anything about the region's contents.
|
|
555 |
+ |
///
|
|
556 |
+ |
/// A renderer walks the description and hands every region it understands
|
|
557 |
+ |
/// to the right drawing code. This is how it tells the two apart, and the
|
|
558 |
+ |
/// reason it is a method rather than a `matches!` at each renderer: there
|
|
559 |
+ |
/// is exactly one opaque member and there should stay exactly one.
|
|
560 |
+ |
#[must_use]
|
|
561 |
+ |
pub const fn described(self) -> bool {
|
|
562 |
+ |
!matches!(self, Self::Bespoke { .. })
|
|
563 |
+ |
}
|
| 514 |
564 |
|
}
|
| 515 |
565 |
|
|
| 516 |
566 |
|
/// How a screen is laid out.
|
| 854 |
904 |
|
}
|
| 855 |
905 |
|
}
|
| 856 |
906 |
|
|
|
907 |
+ |
#[test]
|
|
908 |
+ |
fn exactly_one_region_is_opaque() {
|
|
909 |
+ |
// The escape hatch is one member and stays one member. If a second
|
|
910 |
+ |
// undescribed region ever appears, the description has started
|
|
911 |
+ |
// conceding rather than deferring.
|
|
912 |
+ |
for r in [
|
|
913 |
+ |
Region::Band,
|
|
914 |
+ |
Region::Sidebar,
|
|
915 |
+ |
Region::Pane,
|
|
916 |
+ |
Region::Split,
|
|
917 |
+ |
Region::TabGroup,
|
|
918 |
+ |
Region::Modal,
|
|
919 |
+ |
] {
|
|
920 |
+ |
assert!(r.described(), "{r:?} should be describable");
|
|
921 |
+ |
}
|
|
922 |
+ |
assert!(!Region::Bespoke { name: "day-plan" }.described());
|
|
923 |
+ |
}
|
|
924 |
+ |
|
|
925 |
+ |
#[test]
|
|
926 |
+ |
fn a_bespoke_region_inherits_its_depth_rather_than_choosing_one() {
|
|
927 |
+ |
// The app owns the contents, not the placement. An app that wants its
|
|
928 |
+ |
// timeline in a well frames it in a Pane.
|
|
929 |
+ |
assert_eq!(Region::Bespoke { name: "day-plan" }.depth(), Depth::Flat);
|
|
930 |
+ |
assert_eq!(Region::Bespoke { name: "kanban" }.depth(), Depth::Flat);
|
|
931 |
+ |
}
|
|
932 |
+ |
|
|
933 |
+ |
#[test]
|
|
934 |
+ |
fn a_screen_with_a_bespoke_region_is_still_a_whole_screen() {
|
|
935 |
+ |
// The argument the member exists for: goingson's day-plan has to be
|
|
936 |
+ |
// routable, or the description covers only the boring screens and the
|
|
937 |
+ |
// interesting four need a second path beside the router.
|
|
938 |
+ |
let day_plan = [
|
|
939 |
+ |
Region::Band,
|
|
940 |
+ |
Region::Bespoke { name: "day-plan" },
|
|
941 |
+ |
Region::Sidebar,
|
|
942 |
+ |
];
|
|
943 |
+ |
assert_eq!(day_plan.iter().filter(|r| r.described()).count(), 2);
|
|
944 |
+ |
assert_eq!(day_plan.iter().filter(|r| !r.described()).count(), 1);
|
|
945 |
+ |
}
|
|
946 |
+ |
|
| 857 |
947 |
|
#[test]
|
| 858 |
948 |
|
fn a_secret_field_is_marked_as_one_and_a_hidden_field_is_not_drawn() {
|
| 859 |
949 |
|
let secret = Field::new(FieldKind::Secret, "password", "Password");
|