| 29 |
29 |
|
//! over a recessed fill is unrepresentable.
|
| 30 |
30 |
|
//! - **Components.** [`Token`] (badge against chip), [`Notice`] (toast against
|
| 31 |
31 |
|
//! banner), [`RowPart`], [`CellPart`], [`Heading`], [`Selector`],
|
| 32 |
|
- |
//! [`Readiness`], [`Awaiting`], [`Meter`], [`Figure`], [`Image`], [`Track`],
|
|
32 |
+ |
//! [`Readiness`], [`Awaiting`], [`Meter`], [`Figure`], [`Track`],
|
| 33 |
33 |
|
//! and [`Tone`], the one intent family they share.
|
| 34 |
34 |
|
//! - **Schemas.** [`Field`] for forms, [`Column`] for lists and tables,
|
| 35 |
35 |
|
//! [`Facet`] for the dimensions a set is narrowed by.
|
| 58 |
58 |
|
//! # What this crate cannot say
|
| 59 |
59 |
|
//!
|
| 60 |
60 |
|
//! - **An address.** What a control calls, and where a button goes. [`Act`]
|
| 61 |
|
- |
//! names the act and holds no destination; [`Image`] carries no source.
|
|
61 |
+ |
//! names the act and holds no destination.
|
| 62 |
62 |
|
//! - **A current value.** A webview reads it out of the DOM and an
|
| 63 |
63 |
|
//! immediate-mode renderer holds a `&mut` to the app's own field. A
|
| 64 |
64 |
|
//! description carrying it would be a form model.
|
| 1448 |
1448 |
|
Contain,
|
| 1449 |
1449 |
|
}
|
| 1450 |
1450 |
|
|
| 1451 |
|
- |
/// A picture, and what it says to someone who is not looking at it.
|
| 1452 |
|
- |
///
|
| 1453 |
|
- |
/// # No source
|
| 1454 |
|
- |
///
|
| 1455 |
|
- |
/// [`Act`]'s split, for [`Act`]'s reason. A source is an address, and this
|
| 1456 |
|
- |
/// crate has no notion of an address: it says what a thing *is* and the caller
|
| 1457 |
|
- |
/// keeps what it points at.
|
| 1458 |
|
- |
///
|
| 1459 |
|
- |
/// It matters more here than it does for a control, because a picture is the
|
| 1460 |
|
- |
/// one member where the address is most of what a webview needs and *none* of
|
| 1461 |
|
- |
/// what the description knows. `quasi_router::Node::Image` carries the URL, the
|
| 1462 |
|
- |
/// way it carries an `Action` for a control.
|
| 1463 |
|
- |
///
|
| 1464 |
|
- |
/// # Why [`alt`](Self::alt) is not optional
|
| 1465 |
|
- |
///
|
| 1466 |
|
- |
/// Every other host has to draw something, and for two of the three the alt
|
| 1467 |
|
- |
/// text is not a fallback but the whole rendering: a terminal without a
|
| 1468 |
|
- |
/// graphics protocol has the words and nothing else. Making it optional would
|
| 1469 |
|
- |
/// make "this picture is invisible on a terminal" the default, and the
|
| 1470 |
|
- |
/// description would be carrying a webview assumption in its shape.
|
| 1471 |
|
- |
///
|
| 1472 |
|
- |
/// An image that genuinely says nothing — a rule, a spacer, a decoration
|
| 1473 |
|
- |
/// repeating what the text beside it already said — is an empty `alt`, which is
|
| 1474 |
|
- |
/// the same thing HTML means by it and is a claim rather than an oversight.
|
| 1475 |
|
- |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 1476 |
|
- |
pub struct Image<'a> {
|
| 1477 |
|
- |
/// What the picture says, for anything not showing it.
|
| 1478 |
|
- |
///
|
| 1479 |
|
- |
/// Empty means the picture is decorative and adds nothing to the text
|
| 1480 |
|
- |
/// around it. See the type's own docs on why this is not an `Option`.
|
| 1481 |
|
- |
pub alt: &'a str,
|
| 1482 |
|
- |
/// A visible line under the picture, where the app wants one.
|
| 1483 |
|
- |
///
|
| 1484 |
|
- |
/// Distinct from [`alt`](Self::alt) and the difference is who it is for: a
|
| 1485 |
|
- |
/// caption is content everybody reads, alt text is what stands in for the
|
| 1486 |
|
- |
/// picture. A screenshot captioned "The library view" still needs alt text
|
| 1487 |
|
- |
/// describing what is in the shot.
|
| 1488 |
|
- |
pub caption: Option<&'a str>,
|
| 1489 |
|
- |
/// How it sits in the box it is given.
|
| 1490 |
|
- |
pub fit: Fit,
|
| 1491 |
|
- |
/// The picture's own dimensions, where the app knows them.
|
| 1492 |
|
- |
///
|
| 1493 |
|
- |
/// **Not a display size**, and that distinction is what makes this belong
|
| 1494 |
|
- |
/// here rather than fall foul of the deferral rule. Saying a picture should
|
| 1495 |
|
- |
/// be 320 points wide is a layout value and is not the description's to
|
| 1496 |
|
- |
/// give. Saying the file is 5120x3412 is a fact *about the picture*, the
|
| 1497 |
|
- |
/// same kind of fact [`alt`](Self::alt) is, and no renderer can find it out
|
| 1498 |
|
- |
/// without fetching the bytes.
|
| 1499 |
|
- |
///
|
| 1500 |
|
- |
/// # What it is for, and it is not decoration
|
| 1501 |
|
- |
///
|
| 1502 |
|
- |
/// Without it a renderer cannot reserve room, so the picture occupies
|
| 1503 |
|
- |
/// nothing until it arrives and then takes its full height at once,
|
| 1504 |
|
- |
/// shoving everything below it down the screen.
|
| 1505 |
|
- |
///
|
| 1506 |
|
- |
/// Every host wants it and none can derive it. A webview writes `width` and
|
| 1507 |
|
- |
/// `height` so the browser holds the space; egui sizes a texture; a
|
| 1508 |
|
- |
/// terminal with a graphics protocol scales a blit into cells.
|
| 1509 |
|
- |
///
|
| 1510 |
|
- |
/// `None` is honest and common: a creator-uploaded image whose dimensions
|
| 1511 |
|
- |
/// the app never recorded genuinely does not know. It means the renderer
|
| 1512 |
|
- |
/// cannot reserve, not that the picture has no size.
|
| 1513 |
|
- |
pub intrinsic: Option<Extent>,
|
| 1514 |
|
- |
/// Whether the picture is needed with the screen, or can arrive later.
|
| 1515 |
|
- |
pub loading: Loading,
|
| 1516 |
|
- |
}
|
| 1517 |
|
- |
|
| 1518 |
1451 |
|
/// A picture's own pixel dimensions.
|
| 1519 |
1452 |
|
///
|
| 1520 |
1453 |
|
/// Deliberately not [`makeover_geometry`]'s business. Geometry answers *how
|
| 1577 |
1510 |
|
Lazy,
|
| 1578 |
1511 |
|
}
|
| 1579 |
1512 |
|
|
| 1580 |
|
- |
impl<'a> Image<'a> {
|
| 1581 |
|
- |
/// A picture that carries its own proportions.
|
| 1582 |
|
- |
#[must_use]
|
| 1583 |
|
- |
pub const fn new(alt: &'a str) -> Self {
|
| 1584 |
|
- |
Self {
|
| 1585 |
|
- |
alt,
|
| 1586 |
|
- |
caption: None,
|
| 1587 |
|
- |
fit: Fit::Natural,
|
| 1588 |
|
- |
intrinsic: None,
|
| 1589 |
|
- |
loading: Loading::Eager,
|
| 1590 |
|
- |
}
|
| 1591 |
|
- |
}
|
| 1592 |
|
- |
|
| 1593 |
|
- |
/// The picture's own dimensions, so a renderer can hold its place.
|
| 1594 |
|
- |
#[must_use]
|
| 1595 |
|
- |
pub const fn intrinsic(mut self, width: u32, height: u32) -> Self {
|
| 1596 |
|
- |
self.intrinsic = Some(Extent::new(width, height));
|
| 1597 |
|
- |
self
|
| 1598 |
|
- |
}
|
| 1599 |
|
- |
|
| 1600 |
|
- |
/// This picture is not on screen yet; it can arrive when it is near.
|
| 1601 |
|
- |
#[must_use]
|
| 1602 |
|
- |
pub const fn lazy(mut self) -> Self {
|
| 1603 |
|
- |
self.loading = Loading::Lazy;
|
| 1604 |
|
- |
self
|
| 1605 |
|
- |
}
|
| 1606 |
|
- |
|
| 1607 |
|
- |
/// A visible line under it.
|
| 1608 |
|
- |
#[must_use]
|
| 1609 |
|
- |
pub const fn caption(mut self, caption: &'a str) -> Self {
|
| 1610 |
|
- |
self.caption = Some(caption);
|
| 1611 |
|
- |
self
|
| 1612 |
|
- |
}
|
| 1613 |
|
- |
|
| 1614 |
|
- |
/// How it sits in its box.
|
| 1615 |
|
- |
#[must_use]
|
| 1616 |
|
- |
pub const fn fit(mut self, fit: Fit) -> Self {
|
| 1617 |
|
- |
self.fit = fit;
|
| 1618 |
|
- |
self
|
| 1619 |
|
- |
}
|
| 1620 |
|
- |
|
| 1621 |
|
- |
/// Whether the picture adds anything for someone not looking at it.
|
| 1622 |
|
- |
///
|
| 1623 |
|
- |
/// A renderer with no way to show a picture uses this to decide between
|
| 1624 |
|
- |
/// drawing the alt text and drawing nothing at all. Both are correct and
|
| 1625 |
|
- |
/// the difference is this flag: standing in for a decorative rule with the
|
| 1626 |
|
- |
/// word "decoration" is worse than leaving the space empty.
|
| 1627 |
|
- |
#[must_use]
|
| 1628 |
|
- |
pub const fn speaks(self) -> bool {
|
| 1629 |
|
- |
!self.alt.is_empty()
|
| 1630 |
|
- |
}
|
| 1631 |
|
- |
}
|
| 1632 |
|
- |
|
| 1633 |
1513 |
|
/// What a [`Track`]'s integers count.
|
| 1634 |
1514 |
|
///
|
| 1635 |
1515 |
|
/// `Track::fraction` never needed this -- the arithmetic is the same whatever
|
| 4535 |
4415 |
|
Essential,
|
| 4536 |
4416 |
|
}
|
| 4537 |
4417 |
|
|
| 4538 |
|
- |
/// How much room a group has, measured against its own allocation.
|
| 4539 |
|
- |
///
|
| 4540 |
|
- |
/// Never authored. A renderer computes it from what the group was given and
|
| 4541 |
|
- |
/// what the group's own contents ask for, in that renderer's units: a webview
|
| 4542 |
|
- |
/// from `min-content` under a container query, a terminal from cell widths,
|
| 4543 |
|
- |
/// egui from the galley. Nothing in the description says a number, which is the
|
| 4544 |
|
- |
/// point -- an authored breakpoint rots and this cannot.
|
| 4545 |
|
- |
///
|
| 4546 |
|
- |
/// # Why not [`Depth`]-style two members and no more
|
| 4547 |
|
- |
///
|
| 4548 |
|
- |
/// Two is what the measurement supports. The goingson case that produced this
|
| 4549 |
|
- |
/// type is a window 913px wide -- makeover-geometry's `SizeClass::Expanded` --
|
| 4550 |
|
- |
/// holding a group that has run out of room. A third tier would be a guess
|
| 4551 |
|
- |
/// about a shape nothing in the tree has yet.
|
| 4552 |
|
- |
///
|
| 4553 |
|
- |
/// # Why it is not `SizeClass`
|
| 4554 |
|
- |
///
|
| 4555 |
|
- |
/// Because 913 is exactly the case that proves they are different facts. The
|
| 4556 |
|
- |
/// window is roomy and the group is not, so a type that answered for both would
|
| 4557 |
|
- |
/// have to be wrong about one of them. Sharing the name would also invite
|
| 4558 |
|
- |
/// `@media` thinking straight back in, which is what put a `position: absolute`
|
| 4559 |
|
- |
/// in goingson's stylesheet in the first place. Container semantics instead: a
|
| 4560 |
|
- |
/// group narrowed by a sidebar behaves the same as one narrowed by the window,
|
| 4561 |
|
- |
/// and there is one code path rather than two.
|
| 4562 |
|
- |
///
|
| 4563 |
|
- |
/// Ordered least room first, [`Priority`]'s convention, so a group nesting
|
| 4564 |
|
- |
/// another takes the minimum of the two and relief still resolves inside-out.
|
| 4565 |
|
- |
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
| 4566 |
|
- |
#[non_exhaustive]
|
| 4567 |
|
- |
pub enum Room {
|
| 4568 |
|
- |
/// Not everything the group contains fits, and the group's [`Fallback`]
|
| 4569 |
|
- |
/// decides what happens.
|
| 4570 |
|
- |
Tight,
|
| 4571 |
|
- |
/// Everything fits as described.
|
| 4572 |
|
- |
Ample,
|
| 4573 |
|
- |
}
|
| 4574 |
|
- |
|
| 4575 |
|
- |
/// What a group does when it is [`Room::Tight`].
|
|
4418 |
+ |
/// What a group does when it runs out of room.
|
| 4576 |
4419 |
|
///
|
| 4577 |
4420 |
|
/// Authored, and required: the field carrying this has no `Default` and a group
|
| 4578 |
4421 |
|
/// cannot be described without saying what it does when it runs out of room.
|
| 4876 |
4719 |
|
pub fn reach(&self) -> u8 {
|
| 4877 |
4720 |
|
self.values
|
| 4878 |
4721 |
|
.iter()
|
| 4879 |
|
- |
.map(|value| value.depth)
|
|
4722 |
+ |
.map(|value| value.depth.level)
|
| 4880 |
4723 |
|
.max()
|
| 4881 |
4724 |
|
.unwrap_or(0)
|
| 4882 |
4725 |
|
}
|
| 4985 |
4828 |
|
/// one code path rather than two. A renderer that draws no tree at all still
|
| 4986 |
4829 |
|
/// reads this, since a value's depth is what distinguishes two same-named
|
| 4987 |
4830 |
|
/// leaves under different parents.
|
| 4988 |
|
- |
pub depth: u8,
|
|
4831 |
+ |
pub depth: Nesting,
|
| 4989 |
4832 |
|
/// Whether taking it reveals values under it.
|
| 4990 |
4833 |
|
///
|
| 4991 |
4834 |
|
/// Distinct from having a nonzero [`depth`](Self::depth): a leaf deep in the
|
| 5004 |
4847 |
|
label,
|
| 5005 |
4848 |
|
count: None,
|
| 5006 |
4849 |
|
standing: Standing::Open,
|
| 5007 |
|
- |
depth: 0,
|
|
4850 |
+ |
depth: Nesting::top(),
|
| 5008 |
4851 |
|
branching: false,
|
| 5009 |
4852 |
|
}
|
| 5010 |
4853 |
|
}
|
| 5034 |
4877 |
|
|
| 5035 |
4878 |
|
/// Where it sits in the tree, and whether anything hangs off it.
|
| 5036 |
4879 |
|
#[must_use]
|
| 5037 |
|
- |
pub const fn at(mut self, depth: u8, branching: bool) -> Self {
|
|
4880 |
+ |
pub const fn at(mut self, depth: Nesting, branching: bool) -> Self {
|
| 5038 |
4881 |
|
self.depth = depth;
|
| 5039 |
4882 |
|
self.branching = branching;
|
| 5040 |
4883 |
|
self
|
| 5263 |
5106 |
|
let inherited = [
|
| 5264 |
5107 |
|
FacetValue::of("music")
|
| 5265 |
5108 |
|
.standing(Standing::Taken)
|
| 5266 |
|
- |
.at(0, true),
|
|
5109 |
+ |
.at(Nesting::at(0), true),
|
| 5267 |
5110 |
|
FacetValue::new("music/synths", "synths")
|
| 5268 |
5111 |
|
.standing(Standing::Inherited)
|
| 5269 |
|
- |
.at(1, false),
|
|
5112 |
+ |
.at(Nesting::at(1), false),
|
| 5270 |
5113 |
|
];
|
| 5271 |
5114 |
|
let facet = Facet::new("Tag", Selecting::Subtree, &inherited);
|
| 5272 |
5115 |
|
assert!(facet.engaged());
|
| 5274 |
5117 |
|
assert_eq!(facet.reach(), 1);
|
| 5275 |
5118 |
|
|
| 5276 |
5119 |
|
let untouched = [
|
| 5277 |
|
- |
FacetValue::of("music").at(0, true),
|
|
5120 |
+ |
FacetValue::of("music").at(Nesting::at(0), true),
|
| 5278 |
5121 |
|
FacetValue::new("music/synths", "synths")
|
| 5279 |
5122 |
|
.standing(Standing::Inherited)
|
| 5280 |
|
- |
.at(1, false),
|
|
5123 |
+ |
.at(Nesting::at(1), false),
|
| 5281 |
5124 |
|
];
|
| 5282 |
5125 |
|
// Inherited alone is something further up doing the narrowing, and
|
| 5283 |
5126 |
|
// there is nothing further up here.
|
| 6163 |
6006 |
|
assert!(!Region::Bespoke { name: "day-plan" }.described());
|
| 6164 |
6007 |
|
}
|
| 6165 |
6008 |
|
|
| 6166 |
|
- |
#[test]
|
| 6167 |
|
- |
fn a_picture_that_says_nothing_is_a_claim_and_not_an_oversight() {
|
| 6168 |
|
- |
// The distinction a renderer with no graphics protocol runs on: draw
|
| 6169 |
|
- |
// the words, or draw nothing. Standing in for a decorative rule with
|
| 6170 |
|
- |
// the word "decoration" is worse than leaving the space empty.
|
| 6171 |
|
- |
assert!(Image::new("The library view, mid-import").speaks());
|
| 6172 |
|
- |
assert!(!Image::new("").speaks());
|
| 6173 |
|
- |
}
|
| 6174 |
|
- |
|
| 6175 |
|
- |
#[test]
|
| 6176 |
|
- |
fn a_caption_and_alt_text_are_not_the_same_line() {
|
| 6177 |
|
- |
// A caption is content everybody reads; alt text stands in for the
|
| 6178 |
|
- |
// picture. A screenshot with a caption still needs alt text.
|
| 6179 |
|
- |
let shot = Image::new("A file list with three rows selected").caption("The library view");
|
| 6180 |
|
- |
assert_eq!(shot.caption, Some("The library view"));
|
| 6181 |
|
- |
assert!(shot.speaks());
|
| 6182 |
|
- |
assert_ne!(shot.alt, shot.caption.unwrap());
|
| 6183 |
|
- |
}
|
| 6184 |
|
- |
|
| 6185 |
|
- |
#[test]
|
| 6186 |
|
- |
fn a_picture_can_say_how_much_room_to_hold() {
|
| 6187 |
|
- |
// The whole point: a renderer reserves from the ratio, so the space is
|
| 6188 |
|
- |
// right at any width. A fixed height would only be right at one.
|
| 6189 |
|
- |
let shot = Image::new("a screenshot").intrinsic(5120, 3412);
|
| 6190 |
|
- |
let e = shot.intrinsic.expect("carried");
|
| 6191 |
|
- |
assert_eq!((e.width, e.height), (5120, 3412));
|
| 6192 |
|
- |
assert!((e.ratio().unwrap() - 1.5006).abs() < 0.001);
|
| 6193 |
|
- |
}
|
| 6194 |
|
- |
|
| 6195 |
|
- |
#[test]
|
| 6196 |
|
- |
fn a_picture_with_no_dimensions_reserves_nothing_rather_than_guessing() {
|
| 6197 |
|
- |
// `None` is honest: a creator upload whose size was never recorded does
|
| 6198 |
|
- |
// not know it. A renderer must not invent one.
|
| 6199 |
|
- |
assert_eq!(Image::new("unknown upload").intrinsic, None);
|
| 6200 |
|
- |
assert_eq!(Extent::new(0, 10).ratio(), None);
|
| 6201 |
|
- |
assert_eq!(Extent::new(10, 0).ratio(), None);
|
| 6202 |
|
- |
}
|
| 6203 |
|
- |
|
| 6204 |
|
- |
#[test]
|
| 6205 |
|
- |
fn a_picture_is_wanted_now_unless_the_app_says_otherwise() {
|
| 6206 |
|
- |
// Eager is the safe default and lazy is the opt-in, because deferring
|
| 6207 |
|
- |
// something already on screen saves nothing and moves its shift later.
|
| 6208 |
|
- |
assert_eq!(Image::new("hero").loading, Loading::Eager);
|
| 6209 |
|
- |
assert_eq!(Loading::default(), Loading::Eager);
|
| 6210 |
|
- |
assert_eq!(Image::new("frame 2").lazy().loading, Loading::Lazy);
|
| 6211 |
|
- |
}
|
| 6212 |
|
- |
|
| 6213 |
|
- |
#[test]
|
| 6214 |
|
- |
fn a_picture_keeps_its_own_proportions_unless_told_otherwise() {
|
| 6215 |
|
- |
// The default is the one that shows the whole picture at its own shape,
|
| 6216 |
|
- |
// so a renderer ignoring Fit entirely is still right about the common
|
| 6217 |
|
- |
// case. The shipped MNW carousel sets no object-fit at all, which is
|
| 6218 |
|
- |
// this.
|
| 6219 |
|
- |
assert_eq!(Image::new("a").fit, Fit::Natural);
|
| 6220 |
|
- |
assert_eq!(Fit::default(), Fit::Natural);
|
| 6221 |
|
- |
assert_eq!(Image::new("a").fit(Fit::Cover).fit, Fit::Cover);
|
| 6222 |
|
- |
}
|
| 6223 |
|
- |
|
| 6224 |
6009 |
|
#[test]
|
| 6225 |
6010 |
|
fn a_group_contains_a_section_without_claiming_to_be_a_pane() {
|
| 6226 |
6011 |
|
// The whole of why this is a member rather than a `Pane`. A pane is
|
| 6821 |
6606 |
|
assert_eq!(Paging::more(500).of(400).remaining(), Some(0));
|
| 6822 |
6607 |
|
}
|
| 6823 |
6608 |
|
|
| 6824 |
|
- |
#[test]
|
| 6825 |
|
- |
fn a_group_out_of_room_is_not_the_same_fact_as_a_narrow_window() {
|
| 6826 |
|
- |
// The case the type exists for: 913px is a roomy window holding a group
|
| 6827 |
|
- |
// that has run out of room, so room is measured against the group's own
|
| 6828 |
|
- |
// allocation and never against the viewport.
|
| 6829 |
|
- |
assert!(Room::Tight < Room::Ample);
|
| 6830 |
|
- |
// A group nesting another has whichever room is scarcer, which is what
|
| 6831 |
|
- |
// makes relief resolve inside-out rather than by declaration order.
|
| 6832 |
|
- |
assert_eq!(Room::Ample.min(Room::Tight), Room::Tight);
|
| 6833 |
|
- |
}
|
| 6834 |
|
- |
|
| 6835 |
6609 |
|
#[test]
|
| 6836 |
6610 |
|
fn a_fallback_is_authored_and_a_group_cannot_omit_it() {
|
| 6837 |
6611 |
|
// No `Default`. The compiler is what enforces rule 2, so the assertion
|