| 1226 |
1226 |
|
/// that screen rather than a property of the vocabulary, and the bound is
|
| 1227 |
1227 |
|
/// not the place to hold opinions about taste.
|
| 1228 |
1228 |
|
pub body: Vec<Node>,
|
|
1229 |
+ |
/// How many of [`body`](Self::body) are visible at once.
|
|
1230 |
+ |
///
|
|
1231 |
+ |
/// `4dcd241b`. [`layout::Showing::All`] by default, which is what every
|
|
1232 |
+ |
/// region did before this field existed, so a description written against
|
|
1233 |
+ |
/// the previous version says the same thing.
|
|
1234 |
+ |
///
|
|
1235 |
+ |
/// This is the kind. The two fields below are the current answer and the
|
|
1236 |
+ |
/// per-child name, and they are here rather than in `makeover-layout` for
|
|
1237 |
+ |
/// the reason [`Node::Select`]'s `chosen` is: a layer that defers every
|
|
1238 |
+ |
/// address does not hold what is picked either.
|
|
1239 |
+ |
pub showing: layout::Showing,
|
|
1240 |
+ |
/// Which child is up, when only one of them is.
|
|
1241 |
+ |
///
|
|
1242 |
+ |
/// Meaningless under [`layout::Showing::All`] and ignored there. Read
|
|
1243 |
+ |
/// through [`current`](Self::current) rather than directly, which is where
|
|
1244 |
+ |
/// an index past the end of the body is dealt with.
|
|
1245 |
+ |
pub shown: Option<usize>,
|
|
1246 |
+ |
/// What this region is called, when something above it is showing one child
|
|
1247 |
+ |
/// at a time.
|
|
1248 |
+ |
///
|
|
1249 |
+ |
/// The tab's name, and the whole of what separates a tab strip from a
|
|
1250 |
+ |
/// prev/next row: a renderer draws the strip when the children carry these
|
|
1251 |
+ |
/// and the row when they do not. A carousel's frames are [`Node::Image`] and
|
|
1252 |
+ |
/// have nowhere to put one, which is correct rather than a gap — a frame has
|
|
1253 |
+ |
/// a caption, not a tab name.
|
|
1254 |
+ |
pub label: Option<String>,
|
| 1229 |
1255 |
|
}
|
| 1230 |
1256 |
|
|
| 1231 |
1257 |
|
impl Slot {
|
| 1236 |
1262 |
|
kind,
|
| 1237 |
1263 |
|
readiness: layout::Readiness::Ready,
|
| 1238 |
1264 |
|
body: Vec::new(),
|
|
1265 |
+ |
showing: layout::Showing::All,
|
|
1266 |
+ |
shown: None,
|
|
1267 |
+ |
label: None,
|
| 1239 |
1268 |
|
}
|
| 1240 |
1269 |
|
}
|
| 1241 |
1270 |
|
|
| 1276 |
1305 |
|
self
|
| 1277 |
1306 |
|
}
|
| 1278 |
1307 |
|
|
|
1308 |
+ |
/// Show one child at a time, starting at this one.
|
|
1309 |
+ |
///
|
|
1310 |
+ |
/// The carousel and the tab group, which are one thing said twice: whether
|
|
1311 |
+ |
/// a host draws a strip of names or a prev/next row falls out of whether
|
|
1312 |
+ |
/// the children carry a [`label`](Self::label), never out of the widget's
|
|
1313 |
+ |
/// name.
|
|
1314 |
+ |
#[must_use]
|
|
1315 |
+ |
pub fn showing_one(mut self, shown: usize) -> Self {
|
|
1316 |
+ |
self.showing = layout::Showing::One;
|
|
1317 |
+ |
self.shown = Some(shown);
|
|
1318 |
+ |
self
|
|
1319 |
+ |
}
|
|
1320 |
+ |
|
|
1321 |
+ |
/// Show one child or none, starting closed unless a child is named.
|
|
1322 |
+ |
///
|
|
1323 |
+ |
/// Disclosure. `None` is the closed state and is a legal resting place,
|
|
1324 |
+ |
/// which is the whole of what separates this from
|
|
1325 |
+ |
/// [`showing_one`](Self::showing_one).
|
|
1326 |
+ |
#[must_use]
|
|
1327 |
+ |
pub fn showing_at_most_one(mut self, shown: Option<usize>) -> Self {
|
|
1328 |
+ |
self.showing = layout::Showing::AtMostOne;
|
|
1329 |
+ |
self.shown = shown;
|
|
1330 |
+ |
self
|
|
1331 |
+ |
}
|
|
1332 |
+ |
|
|
1333 |
+ |
/// Name this region, for when something above it shows one child at a time.
|
|
1334 |
+ |
#[must_use]
|
|
1335 |
+ |
pub fn label(mut self, label: impl Into<String>) -> Self {
|
|
1336 |
+ |
self.label = Some(label.into());
|
|
1337 |
+ |
self
|
|
1338 |
+ |
}
|
|
1339 |
+ |
|
|
1340 |
+ |
/// Which child to draw, once [`shown`](Self::shown) is read against the body.
|
|
1341 |
+ |
///
|
|
1342 |
+ |
/// `None` means draw them all, which is both [`layout::Showing::All`] and a
|
|
1343 |
+ |
/// closed disclosure — the two cases differ in what chrome sits around them
|
|
1344 |
+ |
/// and not in what a renderer does with the body, so they answer the same
|
|
1345 |
+ |
/// here.
|
|
1346 |
+ |
///
|
|
1347 |
+ |
/// An index past the end is clamped rather than refused. A description
|
|
1348 |
+ |
/// pointing at a frame that is not there is a bug in the app, and a renderer
|
|
1349 |
+ |
/// that answers it by drawing nothing reports it as a region that vanished,
|
|
1350 |
+ |
/// which is the hardest kind of bug to find from what is on the screen.
|
|
1351 |
+ |
/// [`layout::Share::percent`] clamps for the same reason.
|
|
1352 |
+ |
#[must_use]
|
|
1353 |
+ |
pub fn current(&self) -> Option<usize> {
|
|
1354 |
+ |
if self.body.is_empty() {
|
|
1355 |
+ |
return None;
|
|
1356 |
+ |
}
|
|
1357 |
+ |
let last = self.body.len() - 1;
|
|
1358 |
+ |
match self.showing {
|
|
1359 |
+ |
layout::Showing::One => Some(self.shown.unwrap_or(0).min(last)),
|
|
1360 |
+ |
layout::Showing::AtMostOne => self.shown.map(|shown| shown.min(last)),
|
|
1361 |
+ |
layout::Showing::All => None,
|
|
1362 |
+ |
// `Showing` is `#[non_exhaustive]`, so this arm is compulsory even
|
|
1363 |
+ |
// with every member above it named. Drawing the whole body is the
|
|
1364 |
+ |
// right default for a member this crate has not been taught yet:
|
|
1365 |
+ |
// more content rather than less, which is how every other unknown
|
|
1366 |
+ |
// in this vocabulary degrades.
|
|
1367 |
+ |
_ => None,
|
|
1368 |
+ |
}
|
|
1369 |
+ |
}
|
|
1370 |
+ |
|
|
1371 |
+ |
/// The children's names, when they have them.
|
|
1372 |
+ |
///
|
|
1373 |
+ |
/// Empty unless *every* child is a named region, which is the test a
|
|
1374 |
+ |
/// renderer applies before drawing a strip: a strip with a hole in it is
|
|
1375 |
+ |
/// worse than the prev/next row it would otherwise have drawn, and a
|
|
1376 |
+ |
/// half-labelled body is an app bug rather than a third idiom.
|
|
1377 |
+ |
#[must_use]
|
|
1378 |
+ |
pub fn labels(&self) -> Vec<&str> {
|
|
1379 |
+ |
let named: Vec<&str> = self
|
|
1380 |
+ |
.body
|
|
1381 |
+ |
.iter()
|
|
1382 |
+ |
.filter_map(|node| match node {
|
|
1383 |
+ |
Node::Region(slot) => slot.label.as_deref(),
|
|
1384 |
+ |
_ => None,
|
|
1385 |
+ |
})
|
|
1386 |
+ |
.collect();
|
|
1387 |
+ |
|
|
1388 |
+ |
if named.len() == self.body.len() {
|
|
1389 |
+ |
named
|
|
1390 |
+ |
} else {
|
|
1391 |
+ |
Vec::new()
|
|
1392 |
+ |
}
|
|
1393 |
+ |
}
|
|
1394 |
+ |
|
| 1279 |
1395 |
|
/// This slot, or the first slot under this address anywhere inside it.
|
| 1280 |
1396 |
|
#[must_use]
|
| 1281 |
1397 |
|
pub fn find(&self, id: &str) -> Option<&Self> {
|
| 2805 |
2921 |
|
true
|
| 2806 |
2922 |
|
}
|
| 2807 |
2923 |
|
}
|
|
2924 |
+ |
|
|
2925 |
+ |
#[cfg(test)]
|
|
2926 |
+ |
mod tests {
|
|
2927 |
+ |
use super::*;
|
|
2928 |
+ |
|
|
2929 |
+ |
fn frames(count: usize) -> Vec<Node> {
|
|
2930 |
+ |
(0..count)
|
|
2931 |
+ |
.map(|n| {
|
|
2932 |
+ |
Node::Image(Picture::new(
|
|
2933 |
+ |
format!("/frame-{n}.png"),
|
|
2934 |
+ |
format!("frame {n}"),
|
|
2935 |
+ |
))
|
|
2936 |
+ |
})
|
|
2937 |
+ |
.collect()
|
|
2938 |
+ |
}
|
|
2939 |
+ |
|
|
2940 |
+ |
#[test]
|
|
2941 |
+ |
fn a_region_shows_everything_until_it_says_otherwise() {
|
|
2942 |
+ |
// The default has to be the old behaviour, or every description written
|
|
2943 |
+ |
// before this field existed changes meaning when it arrives.
|
|
2944 |
+ |
let pane = Slot::new("content", RegionKind::Pane).extend(frames(3));
|
|
2945 |
+ |
|
|
2946 |
+ |
assert_eq!(pane.showing, layout::Showing::All);
|
|
2947 |
+ |
assert_eq!(pane.current(), None);
|
|
2948 |
+ |
}
|
|
2949 |
+ |
|
|
2950 |
+ |
#[test]
|
|
2951 |
+ |
fn a_carousel_with_no_stated_frame_is_on_its_first() {
|
|
2952 |
+ |
// `Showing::One` says exactly one is up, so there is no honest reading
|
|
2953 |
+ |
// of a missing index other than the first. A renderer never has to
|
|
2954 |
+ |
// decide this for itself, which is the point of the method.
|
|
2955 |
+ |
let mut carousel = Slot::widget("shots", "carousel").extend(frames(3));
|
|
2956 |
+ |
carousel.showing = layout::Showing::One;
|
|
2957 |
+ |
|
|
2958 |
+ |
assert_eq!(carousel.current(), Some(0));
|
|
2959 |
+ |
}
|
|
2960 |
+ |
|
|
2961 |
+ |
#[test]
|
|
2962 |
+ |
fn a_frame_past_the_end_clamps_rather_than_vanishing() {
|
|
2963 |
+ |
// An out-of-range index is an app bug either way. Clamping reports it as
|
|
2964 |
+ |
// a carousel stuck on its last frame, which is findable; drawing nothing
|
|
2965 |
+ |
// reports it as a region that disappeared, which is not.
|
|
2966 |
+ |
let carousel = Slot::widget("shots", "carousel")
|
|
2967 |
+ |
.extend(frames(3))
|
|
2968 |
+ |
.showing_one(9);
|
|
2969 |
+ |
|
|
2970 |
+ |
assert_eq!(carousel.current(), Some(2));
|
|
2971 |
+ |
|
|
2972 |
+ |
// And an empty body has no frame to clamp to.
|
|
2973 |
+ |
assert_eq!(
|
|
2974 |
+ |
Slot::widget("shots", "carousel").showing_one(0).current(),
|
|
2975 |
+ |
None
|
|
2976 |
+ |
);
|
|
2977 |
+ |
}
|
|
2978 |
+ |
|
|
2979 |
+ |
#[test]
|
|
2980 |
+ |
fn a_closed_disclosure_is_the_one_selective_region_showing_nothing() {
|
|
2981 |
+ |
let closed = Slot::widget("details", "disclosure")
|
|
2982 |
+ |
.extend(frames(1))
|
|
2983 |
+ |
.showing_at_most_one(None);
|
|
2984 |
+ |
let open = Slot::widget("details", "disclosure")
|
|
2985 |
+ |
.extend(frames(1))
|
|
2986 |
+ |
.showing_at_most_one(Some(0));
|
|
2987 |
+ |
|
|
2988 |
+ |
assert_eq!(closed.current(), None);
|
|
2989 |
+ |
assert_eq!(open.current(), Some(0));
|
|
2990 |
+ |
|
|
2991 |
+ |
// Closed and `Showing::All` answer the same here on purpose: they differ
|
|
2992 |
+ |
// in the chrome around the body, not in what a renderer does with it.
|
|
2993 |
+ |
assert!(closed.showing.selective());
|
|
2994 |
+ |
}
|
|
2995 |
+ |
|
|
2996 |
+ |
#[test]
|
|
2997 |
+ |
fn labels_are_all_or_nothing() {
|
|
2998 |
+ |
// A strip with a hole in it is worse than the prev/next row it would
|
|
2999 |
+ |
// have replaced, so a half-labelled body gets the row.
|
|
3000 |
+ |
let tabs = Slot::new("detail", RegionKind::TabGroup)
|
|
3001 |
+ |
.with(Node::Region(
|
|
3002 |
+ |
Slot::new("overview", RegionKind::Pane).label("Overview"),
|
|
3003 |
+ |
))
|
|
3004 |
+ |
.with(Node::Region(
|
|
3005 |
+ |
Slot::new("files", RegionKind::Pane).label("Files"),
|
|
3006 |
+ |
));
|
|
3007 |
+ |
assert_eq!(tabs.labels(), ["Overview", "Files"]);
|
|
3008 |
+ |
|
|
3009 |
+ |
let half = tabs
|
|
3010 |
+ |
.clone()
|
|
3011 |
+ |
.with(Node::Region(Slot::new("history", RegionKind::Pane)));
|
|
3012 |
+ |
assert!(half.labels().is_empty());
|
|
3013 |
+ |
}
|
|
3014 |
+ |
|
|
3015 |
+ |
#[test]
|
|
3016 |
+ |
fn a_carousels_frames_carry_no_label_and_that_is_the_switch() {
|
|
3017 |
+ |
// Which idiom a renderer draws falls out of this rather than out of the
|
|
3018 |
+ |
// widget's name. A frame has a caption; only a region has a tab name.
|
|
3019 |
+ |
let carousel = Slot::widget("shots", "carousel")
|
|
3020 |
+ |
.extend(frames(3))
|
|
3021 |
+ |
.showing_one(1);
|
|
3022 |
+ |
|
|
3023 |
+ |
assert!(carousel.labels().is_empty());
|
|
3024 |
+ |
assert_eq!(carousel.current(), Some(1));
|
|
3025 |
+ |
}
|
|
3026 |
+ |
}
|