| 1313 |
1313 |
|
}
|
| 1314 |
1314 |
|
}
|
| 1315 |
1315 |
|
|
|
1316 |
+ |
/// How much of the width an arrangement's first region takes.
|
|
1317 |
+ |
///
|
|
1318 |
+ |
/// `e0fd485e`. Nothing said how much room a region got, so every renderer
|
|
1319 |
+ |
/// invented its own number and two hosts showing one screen disagreed about
|
|
1320 |
+ |
/// its proportions. A webview never noticed, because the stylesheet answered
|
|
1321 |
+ |
/// once for every consumer; a terminal has no stylesheet to inherit from, so
|
|
1322 |
+ |
/// `quasi-tui` picked 24 columns for a sidebar and 40% for a list pane and
|
|
1323 |
+ |
/// neither had anything behind it.
|
|
1324 |
+ |
///
|
|
1325 |
+ |
/// # A proportion, never a unit
|
|
1326 |
+ |
///
|
|
1327 |
+ |
/// Held as a percentage, and that is the only form it comes in. A description
|
|
1328 |
+ |
/// carrying columns would be describing a terminal and one carrying pixels a
|
|
1329 |
+ |
/// webview, and the whole point is that both honour the same fact: a terminal
|
|
1330 |
+ |
/// resolves it against a column count, a webview writes it into a grid, and
|
|
1331 |
+ |
/// neither has to know what the other did.
|
|
1332 |
+ |
///
|
|
1333 |
+ |
/// It is not [`makeover_geometry::Ratio`]'s job either, which was the first
|
|
1334 |
+ |
/// guess. Geometry is scales that answer the same for every screen and takes
|
|
1335 |
+ |
/// no input that would let a sidebar screen differ from a list-detail one.
|
|
1336 |
+ |
///
|
|
1337 |
+ |
/// [`makeover_geometry::Ratio`]: https://docs.rs/makeover-geometry
|
|
1338 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
1339 |
+ |
pub struct Share(u8);
|
|
1340 |
+ |
|
|
1341 |
+ |
impl Share {
|
|
1342 |
+ |
/// What a sidebar takes, when nobody says otherwise.
|
|
1343 |
+ |
///
|
|
1344 |
+ |
/// A quarter. `quasi-tui` drew 24 columns, which is a quarter of a
|
|
1345 |
+ |
/// 96-column terminal and about a fifth of a wide one; a quarter is that
|
|
1346 |
+ |
/// number said in the form a webview can honour too.
|
|
1347 |
+ |
pub const SIDEBAR: Self = Self(25);
|
|
1348 |
+ |
|
|
1349 |
+ |
/// What the list side of a list-detail takes, when nobody says otherwise.
|
|
1350 |
+ |
///
|
|
1351 |
+ |
/// `quasi-tui`'s 40%, which was already a proportion and is the one number
|
|
1352 |
+ |
/// this member did not have to invent.
|
|
1353 |
+ |
pub const LIST: Self = Self(40);
|
|
1354 |
+ |
|
|
1355 |
+ |
/// A share of the width, as a percentage.
|
|
1356 |
+ |
///
|
|
1357 |
+ |
/// Clamped to 5..=95 rather than refused. A description that asked for a
|
|
1358 |
+ |
/// region of nothing is a bug in the app, and a renderer drawing a region
|
|
1359 |
+ |
/// zero cells wide reports it as a region that vanished, which is the
|
|
1360 |
+ |
/// hardest kind of bug to find from what is on the screen.
|
|
1361 |
+ |
#[must_use]
|
|
1362 |
+ |
pub const fn percent(percent: u8) -> Self {
|
|
1363 |
+ |
Self(if percent < 5 {
|
|
1364 |
+ |
5
|
|
1365 |
+ |
} else if percent > 95 {
|
|
1366 |
+ |
95
|
|
1367 |
+ |
} else {
|
|
1368 |
+ |
percent
|
|
1369 |
+ |
})
|
|
1370 |
+ |
}
|
|
1371 |
+ |
|
|
1372 |
+ |
/// The share as a percentage.
|
|
1373 |
+ |
#[must_use]
|
|
1374 |
+ |
pub const fn as_percent(self) -> u8 {
|
|
1375 |
+ |
self.0
|
|
1376 |
+ |
}
|
|
1377 |
+ |
|
|
1378 |
+ |
/// This share of a width, rounded to the nearest whole unit.
|
|
1379 |
+ |
///
|
|
1380 |
+ |
/// What a terminal calls to turn the proportion into columns. At least one,
|
|
1381 |
+ |
/// because a region the description named should be visible: a screen
|
|
1382 |
+ |
/// 3 columns wide is unusable either way, and a sidebar that is there is a
|
|
1383 |
+ |
/// truer picture of the description than a sidebar that is not.
|
|
1384 |
+ |
#[must_use]
|
|
1385 |
+ |
pub const fn of(self, whole: u16) -> u16 {
|
|
1386 |
+ |
let taken = (whole as u32 * self.0 as u32).div_ceil(100);
|
|
1387 |
+ |
if taken == 0 { 1 } else { taken as u16 }
|
|
1388 |
+ |
}
|
|
1389 |
+ |
}
|
|
1390 |
+ |
|
| 1316 |
1391 |
|
/// How a screen is laid out.
|
| 1317 |
1392 |
|
///
|
| 1318 |
1393 |
|
/// Two, and the second is not a variant of the first. goingson is list-detail,
|
| 1323 |
1398 |
|
/// This exists at all because the router has to be able to express a screen
|
| 1324 |
1399 |
|
/// rather than only a control. Discovering the arrangement layer missing after
|
| 1325 |
1400 |
|
/// the renderers exist is a redesign; naming two now is a morning.
|
|
1401 |
+ |
///
|
|
1402 |
+ |
/// # Why the share rides here
|
|
1403 |
+ |
///
|
|
1404 |
+ |
/// `e0fd485e`. A share is per-arrangement: how much a sidebar takes and how
|
|
1405 |
+ |
/// much a list side takes are different questions, and this enum is the only
|
|
1406 |
+ |
/// thing that knows which one is being asked. Geometry would have had to invent
|
|
1407 |
+ |
/// a channel to be told.
|
|
1408 |
+ |
///
|
|
1409 |
+ |
/// [`list_detail`](Self::list_detail) and
|
|
1410 |
+ |
/// [`sidebar_content`](Self::sidebar_content) build these with the default
|
|
1411 |
+ |
/// shares, so a screen that has no opinion does not have to have one.
|
| 1326 |
1412 |
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
| 1327 |
1413 |
|
pub enum Arrangement {
|
| 1328 |
1414 |
|
/// A list that chooses what the detail beside it shows.
|
| 1329 |
1415 |
|
ListDetail {
|
| 1330 |
1416 |
|
/// Whether the detail side is a [`Region::TabGroup`].
|
| 1331 |
1417 |
|
tabbed: bool,
|
|
1418 |
+ |
/// How much of the width the list side takes.
|
|
1419 |
+ |
share: Share,
|
| 1332 |
1420 |
|
},
|
| 1333 |
1421 |
|
/// Navigation down the side, content filling the rest.
|
| 1334 |
|
- |
SidebarContent,
|
|
1422 |
+ |
SidebarContent {
|
|
1423 |
+ |
/// How much of the width the sidebar takes.
|
|
1424 |
+ |
share: Share,
|
|
1425 |
+ |
},
|
|
1426 |
+ |
}
|
|
1427 |
+ |
|
|
1428 |
+ |
impl Arrangement {
|
|
1429 |
+ |
/// A list and a detail beside it, at the default share.
|
|
1430 |
+ |
#[must_use]
|
|
1431 |
+ |
pub const fn list_detail(tabbed: bool) -> Self {
|
|
1432 |
+ |
Self::ListDetail {
|
|
1433 |
+ |
tabbed,
|
|
1434 |
+ |
share: Share::LIST,
|
|
1435 |
+ |
}
|
|
1436 |
+ |
}
|
|
1437 |
+ |
|
|
1438 |
+ |
/// A sidebar and content beside it, at the default share.
|
|
1439 |
+ |
#[must_use]
|
|
1440 |
+ |
pub const fn sidebar_content() -> Self {
|
|
1441 |
+ |
Self::SidebarContent {
|
|
1442 |
+ |
share: Share::SIDEBAR,
|
|
1443 |
+ |
}
|
|
1444 |
+ |
}
|
|
1445 |
+ |
|
|
1446 |
+ |
/// How much of the width the first region takes.
|
|
1447 |
+ |
#[must_use]
|
|
1448 |
+ |
pub const fn share(self) -> Share {
|
|
1449 |
+ |
match self {
|
|
1450 |
+ |
Self::ListDetail { share, .. } | Self::SidebarContent { share } => share,
|
|
1451 |
+ |
}
|
|
1452 |
+ |
}
|
|
1453 |
+ |
|
|
1454 |
+ |
/// The same arrangement, at this share.
|
|
1455 |
+ |
#[must_use]
|
|
1456 |
+ |
pub const fn with_share(self, share: Share) -> Self {
|
|
1457 |
+ |
match self {
|
|
1458 |
+ |
Self::ListDetail { tabbed, .. } => Self::ListDetail { tabbed, share },
|
|
1459 |
+ |
Self::SidebarContent { .. } => Self::SidebarContent { share },
|
|
1460 |
+ |
}
|
|
1461 |
+ |
}
|
|
1462 |
+ |
}
|
|
1463 |
+ |
|
|
1464 |
+ |
/// How wide the content of a whole screen runs.
|
|
1465 |
+ |
///
|
|
1466 |
+ |
/// `0eccff0d`, and [`Share`]'s sibling one level up: that one says how a
|
|
1467 |
+ |
/// screen's width is divided between regions, this says how much of the window
|
|
1468 |
+ |
/// the screen uses in the first place. Both are the description's, which is
|
|
1469 |
+ |
/// what answering the two together settled.
|
|
1470 |
+ |
///
|
|
1471 |
+ |
/// Measured in the MNW server, where 69 of 72 templates carry exactly one of
|
|
1472 |
+ |
/// three mutually exclusive classes and the choice is per screen. GoingsOn
|
|
1473 |
+ |
/// reaches for `max-width` 56 times and Balanced Breakfast 12, neither with a
|
|
1474 |
+ |
/// token for it, so three apps were solving one thing by hand.
|
|
1475 |
+ |
///
|
|
1476 |
+ |
/// # Named for the measure, not for MNW's classes
|
|
1477 |
+ |
///
|
|
1478 |
+ |
/// A renderer that is not a browser has to answer this too, and `padded-page`
|
|
1479 |
+ |
/// tells a terminal nothing. The three say how wide the text runs, which is a
|
|
1480 |
+ |
/// question every renderer can answer: a webview with a `max-width`, a terminal
|
|
1481 |
+ |
/// with gutters, an immediate-mode frame with its own width.
|
|
1482 |
+ |
///
|
|
1483 |
+ |
/// `#[non_exhaustive]` for [`Fill`]'s reason. The set is closed today because
|
|
1484 |
+ |
/// the measurement found three, and a fourth arriving should not be a lockstep
|
|
1485 |
+ |
/// release across nine repos.
|
|
1486 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
|
1487 |
+ |
#[non_exhaustive]
|
|
1488 |
+ |
pub enum Measure {
|
|
1489 |
+ |
/// The whole width, with gutters. The default, and 53 of the 69.
|
|
1490 |
+ |
///
|
|
1491 |
+ |
/// What a dashboard, a table and a settings screen want: the content is
|
|
1492 |
+ |
/// wide because the content *is* wide, and constraining it would waste the
|
|
1493 |
+ |
/// window.
|
|
1494 |
+ |
#[default]
|
|
1495 |
+ |
Wide,
|
|
1496 |
+ |
/// Capped at a comfortable page width, centred. 13 of the 69.
|
|
1497 |
+ |
///
|
|
1498 |
+ |
/// A form, a sign-in, a purchase. Content that does not get better by
|
|
1499 |
+ |
/// getting wider, but is not prose either.
|
|
1500 |
+ |
Contained,
|
|
1501 |
+ |
/// Capped at a line length that reads well. 3 of the 69.
|
|
1502 |
+ |
///
|
|
1503 |
+ |
/// Prose. The narrowest of the three, and the one with a reason outside
|
|
1504 |
+ |
/// taste: a line of text past roughly 75 characters costs the reader the
|
|
1505 |
+ |
/// return sweep.
|
|
1506 |
+ |
Reading,
|
|
1507 |
+ |
}
|
|
1508 |
+ |
|
|
1509 |
+ |
impl Measure {
|
|
1510 |
+ |
/// A stable name, for a renderer that needs to spell it.
|
|
1511 |
+ |
///
|
|
1512 |
+ |
/// Here rather than in each renderer for [`Sort::as_str`]'s reason: three
|
|
1513 |
+ |
/// renderers spelling one enum is three chances to spell it differently.
|
|
1514 |
+ |
#[must_use]
|
|
1515 |
+ |
pub const fn as_str(self) -> &'static str {
|
|
1516 |
+ |
match self {
|
|
1517 |
+ |
Self::Wide => "wide",
|
|
1518 |
+ |
Self::Contained => "contained",
|
|
1519 |
+ |
Self::Reading => "reading",
|
|
1520 |
+ |
}
|
|
1521 |
+ |
}
|
| 1335 |
1522 |
|
}
|
| 1336 |
1523 |
|
|
| 1337 |
1524 |
|
/// What kind of value a form field takes.
|
| 2746 |
2933 |
|
fn an_arrangement_carries_the_tab_group_as_a_modifier() {
|
| 2747 |
2934 |
|
// goingson uses the tab group inside the content region rather than
|
| 2748 |
2935 |
|
// instead of one, so it is not a third arrangement.
|
| 2749 |
|
- |
let go = Arrangement::ListDetail { tabbed: true };
|
| 2750 |
|
- |
let plain = Arrangement::ListDetail { tabbed: false };
|
|
2936 |
+ |
let go = Arrangement::list_detail(true);
|
|
2937 |
+ |
let plain = Arrangement::list_detail(false);
|
| 2751 |
2938 |
|
assert_ne!(go, plain);
|
| 2752 |
|
- |
assert_ne!(go, Arrangement::SidebarContent);
|
|
2939 |
+ |
assert_ne!(go, Arrangement::sidebar_content());
|
|
2940 |
+ |
}
|
|
2941 |
+ |
|
|
2942 |
+ |
#[test]
|
|
2943 |
+ |
fn a_share_is_a_proportion_and_resolves_the_same_way_everywhere() {
|
|
2944 |
+ |
// The point of the member: a terminal reading columns and a webview
|
|
2945 |
+ |
// reading a grid honour one fact, so two hosts showing one screen agree
|
|
2946 |
+ |
// about its proportions.
|
|
2947 |
+ |
assert_eq!(Share::LIST.as_percent(), 40);
|
|
2948 |
+ |
assert_eq!(Share::LIST.of(100), 40);
|
|
2949 |
+ |
assert_eq!(
|
|
2950 |
+ |
Share::SIDEBAR.of(96),
|
|
2951 |
+ |
24,
|
|
2952 |
+ |
"quasi-tui's 24 columns, said as a quarter"
|
|
2953 |
+ |
);
|
|
2954 |
+ |
}
|
|
2955 |
+ |
|
|
2956 |
+ |
#[test]
|
|
2957 |
+ |
fn a_region_never_resolves_to_nothing() {
|
|
2958 |
+ |
// A region the description named should be visible. A zero-width one
|
|
2959 |
+ |
// reads on screen as a region that vanished, which is the hardest kind
|
|
2960 |
+ |
// of bug to find from what is drawn.
|
|
2961 |
+ |
assert_eq!(Share::percent(5).of(1), 1);
|
|
2962 |
+ |
assert_eq!(Share::percent(5).of(0), 1);
|
|
2963 |
+ |
}
|
|
2964 |
+ |
|
|
2965 |
+ |
#[test]
|
|
2966 |
+ |
fn a_share_outside_the_range_is_clamped_rather_than_refused() {
|
|
2967 |
+ |
assert_eq!(Share::percent(0), Share::percent(5));
|
|
2968 |
+ |
assert_eq!(Share::percent(200), Share::percent(95));
|
|
2969 |
+ |
}
|
|
2970 |
+ |
|
|
2971 |
+ |
#[test]
|
|
2972 |
+ |
fn the_share_rides_on_the_arrangement_that_knows_which_question_it_is() {
|
|
2973 |
+ |
// How much a sidebar takes and how much a list side takes are different
|
|
2974 |
+ |
// questions, and this enum is the only thing that knows which is being
|
|
2975 |
+ |
// asked.
|
|
2976 |
+ |
assert_eq!(Arrangement::sidebar_content().share(), Share::SIDEBAR);
|
|
2977 |
+ |
assert_eq!(Arrangement::list_detail(false).share(), Share::LIST);
|
|
2978 |
+ |
|
|
2979 |
+ |
let narrow = Arrangement::sidebar_content().with_share(Share::percent(20));
|
|
2980 |
+ |
assert_eq!(narrow.share(), Share::percent(20));
|
|
2981 |
+ |
assert!(matches!(narrow, Arrangement::SidebarContent { .. }));
|
|
2982 |
+ |
}
|
|
2983 |
+ |
|
|
2984 |
+ |
#[test]
|
|
2985 |
+ |
fn a_measure_defaults_to_the_one_53_of_69_templates_asked_for() {
|
|
2986 |
+ |
// The default is meaningful: a screen nobody said anything about uses
|
|
2987 |
+ |
// the window it was given.
|
|
2988 |
+ |
assert_eq!(Measure::default(), Measure::Wide);
|
|
2989 |
+ |
assert_eq!(Measure::Reading.as_str(), "reading");
|
| 2753 |
2990 |
|
}
|
| 2754 |
2991 |
|
|
| 2755 |
2992 |
|
#[test]
|