| 224 |
224 |
|
//! `widget-tier` for the ownership model, which is why this member carries a
|
| 225 |
225 |
|
//! name a renderer may decline to know.
|
| 226 |
226 |
|
//!
|
|
227 |
+ |
//! 0.21.0 adds [`Image`] and [`Fit`], found by trying to describe MNW's
|
|
228 |
+ |
//! carousel under 0.20.0's widget tier and getting one step in. Nothing named a
|
|
229 |
+ |
//! picture. The vocabulary could say a number with a caption, a badge, a meter
|
|
230 |
+ |
//! and a table, and could not say the thing three of MNW's public pages are
|
|
231 |
+ |
//! mostly made of.
|
|
232 |
+ |
//!
|
|
233 |
+ |
//! It reads as an oversight and is a measurement: 24 `<img>` sites across 22
|
|
234 |
+ |
//! MNW templates, against one in goingson and none in Balanced Breakfast or
|
|
235 |
+ |
//! audiofiles. A picture is furniture a *content platform* has, and MNW is the
|
|
236 |
+ |
//! only one in the tree, so the evidence never arrived from the two-app
|
|
237 |
+ |
//! direction the earlier rule looked in. Under the generic-against-bespoke bar
|
|
238 |
+ |
//! it is not close: a picture is not one app's own.
|
|
239 |
+ |
//!
|
|
240 |
+ |
//! A primitive rather than a widget, which is worth stating now that the tier
|
|
241 |
+ |
//! makes it a real question. A widget is an assembly of things already sayable
|
|
242 |
+ |
//! and a picture is a leaf, assembled from nothing. It also passes the test
|
|
243 |
+ |
//! `Node::Html` failed — every host has an honest answer, including a terminal,
|
|
244 |
+ |
//! which has a graphics protocol or has [`Image::alt`].
|
|
245 |
+ |
//!
|
|
246 |
+ |
//! [`Image`] carries no source, the split [`Act`] already makes: an address is
|
|
247 |
+ |
//! not this crate's to hold. See its own docs, which is where the argument is.
|
|
248 |
+ |
//!
|
| 227 |
249 |
|
//! # Reach, focus and the focus ring
|
| 228 |
250 |
|
//!
|
| 229 |
251 |
|
//! Three terms, and no others, for what 0.19.0 moved out of the description.
|
| 1213 |
1235 |
|
/// omission: a question asked *after* a control is pressed belongs to whatever
|
| 1214 |
1236 |
|
/// is holding the interaction, and a renderer that drew it would be asking
|
| 1215 |
1237 |
|
/// before there was anything to answer.
|
|
1238 |
+ |
/// How a picture sits in the box it is given.
|
|
1239 |
+ |
///
|
|
1240 |
+ |
/// An intent rather than a value, so a renderer picks the expression it has:
|
|
1241 |
+ |
/// `object-fit` in a webview, a texture's UV rect in egui, and in a terminal a
|
|
1242 |
+ |
/// choice about how many cells the blit gets. Named because MNW already makes
|
|
1243 |
+ |
/// the distinction deliberately at 17 sites and makes it three different ways,
|
|
1244 |
+ |
/// which is a policy the app decided rather than one a shared crate would be
|
|
1245 |
+ |
/// picking by accident.
|
|
1246 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
1247 |
+ |
#[non_exhaustive]
|
|
1248 |
+ |
pub enum Fit {
|
|
1249 |
+ |
/// The picture's own proportions, and the box takes the height they imply.
|
|
1250 |
+ |
///
|
|
1251 |
+ |
/// The default because it is the only one that shows the whole picture at
|
|
1252 |
+ |
/// its own shape, so a renderer that ignores this enum entirely is still
|
|
1253 |
+ |
/// right about the common case. A screenshot wants this; the shipped MNW
|
|
1254 |
+ |
/// carousel sets no `object-fit` at all, which is this.
|
|
1255 |
+ |
#[default]
|
|
1256 |
+ |
Natural,
|
|
1257 |
+ |
/// Fill the box and crop whatever does not fit.
|
|
1258 |
+ |
///
|
|
1259 |
+ |
/// For a picture in a slot whose shape the layout fixed: a thumbnail, an
|
|
1260 |
+ |
/// avatar, cover art. 15 of MNW's 17 sites.
|
|
1261 |
+ |
Cover,
|
|
1262 |
+ |
/// Fit inside the box whole, leaving space on two sides.
|
|
1263 |
+ |
///
|
|
1264 |
+ |
/// The letterbox. For when the whole picture matters more than filling the
|
|
1265 |
+ |
/// space, and the space is not the picture's shape.
|
|
1266 |
+ |
Contain,
|
|
1267 |
+ |
}
|
|
1268 |
+ |
|
|
1269 |
+ |
/// A picture, and what it says to someone who is not looking at it.
|
|
1270 |
+ |
///
|
|
1271 |
+ |
/// # No source
|
|
1272 |
+ |
///
|
|
1273 |
+ |
/// [`Act`]'s split, for [`Act`]'s reason. A source is an address, and this
|
|
1274 |
+ |
/// crate has no notion of an address: it says what a thing *is* and the caller
|
|
1275 |
+ |
/// keeps what it points at. The three findings dropped from 0.11.0 were all
|
|
1276 |
+ |
/// this same shape.
|
|
1277 |
+ |
///
|
|
1278 |
+ |
/// It matters more here than it does for a control, because a picture is the
|
|
1279 |
+ |
/// one member where the address is most of what a webview needs and *none* of
|
|
1280 |
+ |
/// what the description knows. `quasi_router::Node::Image` carries the URL, the
|
|
1281 |
+ |
/// way it carries an `Action` for a control.
|
|
1282 |
+ |
///
|
|
1283 |
+ |
/// # Why [`alt`](Self::alt) is not optional
|
|
1284 |
+ |
///
|
|
1285 |
+ |
/// Every other host has to draw something, and for two of the three the alt
|
|
1286 |
+ |
/// text is not a fallback but the whole rendering: a terminal without a
|
|
1287 |
+ |
/// graphics protocol has the words and nothing else. Making it optional would
|
|
1288 |
+ |
/// make "this picture is invisible on a terminal" the default, and the
|
|
1289 |
+ |
/// description would be carrying a webview assumption in its shape.
|
|
1290 |
+ |
///
|
|
1291 |
+ |
/// An image that genuinely says nothing — a rule, a spacer, a decoration
|
|
1292 |
+ |
/// repeating what the text beside it already said — is an empty `alt`, which is
|
|
1293 |
+ |
/// the same thing HTML means by it and is a claim rather than an oversight.
|
|
1294 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
1295 |
+ |
pub struct Image<'a> {
|
|
1296 |
+ |
/// What the picture says, for anything not showing it.
|
|
1297 |
+ |
///
|
|
1298 |
+ |
/// Empty means the picture is decorative and adds nothing to the text
|
|
1299 |
+ |
/// around it. See the type's own docs on why this is not an `Option`.
|
|
1300 |
+ |
pub alt: &'a str,
|
|
1301 |
+ |
/// A visible line under the picture, where the app wants one.
|
|
1302 |
+ |
///
|
|
1303 |
+ |
/// Distinct from [`alt`](Self::alt) and the difference is who it is for: a
|
|
1304 |
+ |
/// caption is content everybody reads, alt text is what stands in for the
|
|
1305 |
+ |
/// picture. A screenshot captioned "The library view" still needs alt text
|
|
1306 |
+ |
/// describing what is in the shot.
|
|
1307 |
+ |
pub caption: Option<&'a str>,
|
|
1308 |
+ |
/// How it sits in the box it is given.
|
|
1309 |
+ |
pub fit: Fit,
|
|
1310 |
+ |
}
|
|
1311 |
+ |
|
|
1312 |
+ |
impl<'a> Image<'a> {
|
|
1313 |
+ |
/// A picture that carries its own proportions.
|
|
1314 |
+ |
#[must_use]
|
|
1315 |
+ |
pub const fn new(alt: &'a str) -> Self {
|
|
1316 |
+ |
Self {
|
|
1317 |
+ |
alt,
|
|
1318 |
+ |
caption: None,
|
|
1319 |
+ |
fit: Fit::Natural,
|
|
1320 |
+ |
}
|
|
1321 |
+ |
}
|
|
1322 |
+ |
|
|
1323 |
+ |
/// A visible line under it.
|
|
1324 |
+ |
#[must_use]
|
|
1325 |
+ |
pub const fn caption(mut self, caption: &'a str) -> Self {
|
|
1326 |
+ |
self.caption = Some(caption);
|
|
1327 |
+ |
self
|
|
1328 |
+ |
}
|
|
1329 |
+ |
|
|
1330 |
+ |
/// How it sits in its box.
|
|
1331 |
+ |
#[must_use]
|
|
1332 |
+ |
pub const fn fit(mut self, fit: Fit) -> Self {
|
|
1333 |
+ |
self.fit = fit;
|
|
1334 |
+ |
self
|
|
1335 |
+ |
}
|
|
1336 |
+ |
|
|
1337 |
+ |
/// Whether the picture adds anything for someone not looking at it.
|
|
1338 |
+ |
///
|
|
1339 |
+ |
/// A renderer with no way to show a picture uses this to decide between
|
|
1340 |
+ |
/// drawing the alt text and drawing nothing at all. Both are correct and
|
|
1341 |
+ |
/// the difference is this flag: standing in for a decorative rule with the
|
|
1342 |
+ |
/// word "decoration" is worse than leaving the space empty.
|
|
1343 |
+ |
#[must_use]
|
|
1344 |
+ |
pub const fn speaks(self) -> bool {
|
|
1345 |
+ |
!self.alt.is_empty()
|
|
1346 |
+ |
}
|
|
1347 |
+ |
}
|
|
1348 |
+ |
|
| 1216 |
1349 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 1217 |
1350 |
|
pub struct Act<'a> {
|
| 1218 |
1351 |
|
/// What the control says.
|
| 2823 |
2956 |
|
assert!(!Region::Bespoke { name: "day-plan" }.described());
|
| 2824 |
2957 |
|
}
|
| 2825 |
2958 |
|
|
|
2959 |
+ |
#[test]
|
|
2960 |
+ |
fn a_picture_that_says_nothing_is_a_claim_and_not_an_oversight() {
|
|
2961 |
+ |
// The distinction a renderer with no graphics protocol runs on: draw
|
|
2962 |
+ |
// the words, or draw nothing. Standing in for a decorative rule with
|
|
2963 |
+ |
// the word "decoration" is worse than leaving the space empty.
|
|
2964 |
+ |
assert!(Image::new("The library view, mid-import").speaks());
|
|
2965 |
+ |
assert!(!Image::new("").speaks());
|
|
2966 |
+ |
}
|
|
2967 |
+ |
|
|
2968 |
+ |
#[test]
|
|
2969 |
+ |
fn a_caption_and_alt_text_are_not_the_same_line() {
|
|
2970 |
+ |
// A caption is content everybody reads; alt text stands in for the
|
|
2971 |
+ |
// picture. A screenshot with a caption still needs alt text.
|
|
2972 |
+ |
let shot = Image::new("A file list with three rows selected").caption("The library view");
|
|
2973 |
+ |
assert_eq!(shot.caption, Some("The library view"));
|
|
2974 |
+ |
assert!(shot.speaks());
|
|
2975 |
+ |
assert_ne!(shot.alt, shot.caption.unwrap());
|
|
2976 |
+ |
}
|
|
2977 |
+ |
|
|
2978 |
+ |
#[test]
|
|
2979 |
+ |
fn a_picture_keeps_its_own_proportions_unless_told_otherwise() {
|
|
2980 |
+ |
// The default is the one that shows the whole picture at its own shape,
|
|
2981 |
+ |
// so a renderer ignoring Fit entirely is still right about the common
|
|
2982 |
+ |
// case. The shipped MNW carousel sets no object-fit at all, which is
|
|
2983 |
+ |
// this.
|
|
2984 |
+ |
assert_eq!(Image::new("a").fit, Fit::Natural);
|
|
2985 |
+ |
assert_eq!(Fit::default(), Fit::Natural);
|
|
2986 |
+ |
assert_eq!(Image::new("a").fit(Fit::Cover).fit, Fit::Cover);
|
|
2987 |
+ |
}
|
|
2988 |
+ |
|
| 2826 |
2989 |
|
#[test]
|
| 2827 |
2990 |
|
fn a_widget_inherits_its_depth_the_way_a_bespoke_does() {
|
| 2828 |
2991 |
|
// Stronger than the bespoke case: a widget is drawn by whichever
|