| 751 |
751 |
|
Pending,
|
| 752 |
752 |
|
}
|
| 753 |
753 |
|
|
|
754 |
+ |
/// How much of a set is done.
|
|
755 |
+ |
///
|
|
756 |
+ |
/// Added 0.10.0. Nine sites across the two webview apps drew a bar and nothing
|
|
757 |
+ |
/// here named one, so every described screen concatenated the two numbers into
|
|
758 |
+ |
/// its heading text instead: "Subtasks 3/7", "Time Tracking 45m tracked / 30m
|
|
759 |
+ |
/// est, over". Every fact survives that and the reading does not, which is the
|
|
760 |
+ |
/// same loss `RowPart::Tokens` closed when a toned status badge became prose.
|
|
761 |
+ |
///
|
|
762 |
+ |
/// # Why a pair and not a percentage
|
|
763 |
+ |
///
|
|
764 |
+ |
/// Both numbers, not the percentage the apps compute from them. The percentage
|
|
765 |
+ |
/// was the obvious shape and it had already been tried: goingson's
|
|
766 |
+ |
/// `Task::time_progress` divides, rounds, and then clamps to 100, which throws
|
|
767 |
+ |
/// away the one case the bar exists to show — 45 minutes tracked against a
|
|
768 |
+ |
/// 30-minute estimate. It carries a separate `is_over_estimate` boolean beside
|
|
769 |
+ |
/// it to recover the fact the clamp dropped. A pair keeps the over-run without a
|
|
770 |
+ |
/// companion flag, and [`percent`](Meter::percent) is still one call away for a
|
|
771 |
+ |
/// renderer that wants it.
|
|
772 |
+ |
///
|
|
773 |
+ |
/// The pair is also what the apps already have at every site. All seven
|
|
774 |
+ |
/// determinate bars write the ratio into the accessible layer and never the
|
|
775 |
+ |
/// percentage: `title="3/7 subtasks"`, `aria-label="3 of 7 subtasks completed"`,
|
|
776 |
+ |
/// a milestone's own `3/7` span. Given 43 nothing can recover "3 of 7", so a
|
|
777 |
+ |
/// percentage member would have made [`label`](Meter::label) mandatory at every
|
|
778 |
+ |
/// call site, which is the concatenated text this member removes, moved one
|
|
779 |
+ |
/// layer down.
|
|
780 |
+ |
///
|
|
781 |
+ |
/// # What this is not
|
|
782 |
+ |
///
|
|
783 |
+ |
/// The progress of an *operation*. Two of the nine sites are that — goingson's
|
|
784 |
+ |
/// focus timer, Balanced Breakfast's feed fetch — and they get nothing here, on
|
|
785 |
+ |
/// purpose. Both are imperative controllers over a live handle, driven by a tick
|
|
786 |
+ |
/// or an event stream, and a description is built once and dropped. Holding one
|
|
787 |
+ |
/// would mean growing a way to update a description between renders, which is a
|
|
788 |
+ |
/// different feature. [`Readiness::Pending`] and a [`Notice::Toast`] carry the
|
|
789 |
+ |
/// honest part.
|
|
790 |
+ |
///
|
|
791 |
+ |
/// The two cases are distinguishable in the markup rather than by taste: every
|
|
792 |
+ |
/// determinate bar in both apps carries a tone, and neither operation bar
|
|
793 |
+ |
/// carries one. Two codebases drew that line the same way without coordinating.
|
|
794 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
795 |
+ |
pub struct Meter<'a> {
|
|
796 |
+ |
/// How much is done. May exceed [`total`](Self::total), and that is the
|
|
797 |
+ |
/// case worth drawing.
|
|
798 |
+ |
pub done: u32,
|
|
799 |
+ |
/// How much there is to do. Zero means there is no set, not that the set is
|
|
800 |
+ |
/// complete.
|
|
801 |
+ |
pub total: u32,
|
|
802 |
+ |
/// What the proportion means right now.
|
|
803 |
+ |
///
|
|
804 |
+ |
/// Carried rather than derived, because no renderer can work it out. The
|
|
805 |
+ |
/// same 90% is [`Tone::Success`] on a subtask rollup and [`Tone::Danger`] on
|
|
806 |
+ |
/// a time estimate, and goingson picks between them from `is_over_estimate`,
|
|
807 |
+ |
/// a fact about the data and not about the number.
|
|
808 |
+ |
pub tone: Tone,
|
|
809 |
+ |
/// What is being counted, if the bar says so: "subtasks", "tasks".
|
|
810 |
+ |
///
|
|
811 |
+ |
/// The noun, not the ratio. A renderer builds "3 of 7 subtasks" from this
|
|
812 |
+ |
/// and the two numbers; handing it the assembled string would put the
|
|
813 |
+ |
/// sentence order in the description, where a terminal at one line and a
|
|
814 |
+ |
/// tooltip want different ones.
|
|
815 |
+ |
pub label: Option<&'a str>,
|
|
816 |
+ |
}
|
|
817 |
+ |
|
|
818 |
+ |
impl<'a> Meter<'a> {
|
|
819 |
+ |
/// A proportion with no tone and no label.
|
|
820 |
+ |
#[must_use]
|
|
821 |
+ |
pub const fn new(done: u32, total: u32) -> Self {
|
|
822 |
+ |
Self {
|
|
823 |
+ |
done,
|
|
824 |
+ |
total,
|
|
825 |
+ |
tone: Tone::Neutral,
|
|
826 |
+ |
label: None,
|
|
827 |
+ |
}
|
|
828 |
+ |
}
|
|
829 |
+ |
|
|
830 |
+ |
/// What the proportion means.
|
|
831 |
+ |
#[must_use]
|
|
832 |
+ |
pub const fn tone(mut self, tone: Tone) -> Self {
|
|
833 |
+ |
self.tone = tone;
|
|
834 |
+ |
self
|
|
835 |
+ |
}
|
|
836 |
+ |
|
|
837 |
+ |
/// What is being counted.
|
|
838 |
+ |
#[must_use]
|
|
839 |
+ |
pub const fn label(mut self, label: &'a str) -> Self {
|
|
840 |
+ |
self.label = Some(label);
|
|
841 |
+ |
self
|
|
842 |
+ |
}
|
|
843 |
+ |
|
|
844 |
+ |
/// How full the bar is, 0 to 100, clamped.
|
|
845 |
+ |
///
|
|
846 |
+ |
/// For drawing, which is the only thing a clamped number is good for. Ask
|
|
847 |
+ |
/// [`overflowing`](Self::overflowing) before reporting it as a fact, or this
|
|
848 |
+ |
/// is `time_progress`'s bug again with the clamp moved.
|
|
849 |
+ |
///
|
|
850 |
+ |
/// An empty set reads as 0. Nothing is done, because there is nothing to do
|
|
851 |
+ |
/// and no bar to fill; the apps guard on the count before drawing at all.
|
|
852 |
+ |
#[must_use]
|
|
853 |
+ |
pub const fn percent(&self) -> u8 {
|
|
854 |
+ |
if self.total == 0 {
|
|
855 |
+ |
return 0;
|
|
856 |
+ |
}
|
|
857 |
+ |
let scaled = (self.done as u64 * 100) / self.total as u64;
|
|
858 |
+ |
if scaled > 100 { 100 } else { scaled as u8 }
|
|
859 |
+ |
}
|
|
860 |
+ |
|
|
861 |
+ |
/// Whether more is done than there was to do.
|
|
862 |
+ |
///
|
|
863 |
+ |
/// The fact [`percent`](Self::percent) destroys, kept reachable so a
|
|
864 |
+ |
/// renderer can mark the over-run rather than drawing a full bar and
|
|
865 |
+ |
/// implying it landed exactly.
|
|
866 |
+ |
#[must_use]
|
|
867 |
+ |
pub const fn overflowing(&self) -> bool {
|
|
868 |
+ |
self.done > self.total
|
|
869 |
+ |
}
|
|
870 |
+ |
|
|
871 |
+ |
/// Whether there is a set at all.
|
|
872 |
+ |
///
|
|
873 |
+ |
/// A meter over nothing is sayable on purpose, for the same reason a field
|
|
874 |
+ |
/// with no options is: it is what an app with an unloaded count actually
|
|
875 |
+ |
/// has, and a renderer that shows an empty bar says so on screen rather than
|
|
876 |
+ |
/// dividing by zero.
|
|
877 |
+ |
#[must_use]
|
|
878 |
+ |
pub const fn is_empty(&self) -> bool {
|
|
879 |
+ |
self.total == 0
|
|
880 |
+ |
}
|
|
881 |
+ |
}
|
|
882 |
+ |
|
| 754 |
883 |
|
/// A named part of a screen.
|
| 755 |
884 |
|
///
|
| 756 |
885 |
|
/// The thing `makeover-geometry` deliberately does not name: it names the space
|
| 1202 |
1331 |
|
mod tests {
|
| 1203 |
1332 |
|
use super::*;
|
| 1204 |
1333 |
|
|
|
1334 |
+ |
#[test]
|
|
1335 |
+ |
fn a_meter_keeps_the_over_run_the_percentage_throws_away() {
|
|
1336 |
+ |
// The whole reason this is a pair. goingson's `Task::time_progress`
|
|
1337 |
+ |
// clamps to 100 and then carries `is_over_estimate` beside it to say
|
|
1338 |
+ |
// what the clamp dropped; a meter says both from one fact.
|
|
1339 |
+ |
let over = Meter::new(45, 30);
|
|
1340 |
+ |
assert_eq!(over.percent(), 100);
|
|
1341 |
+ |
assert!(over.overflowing());
|
|
1342 |
+ |
|
|
1343 |
+ |
let exact = Meter::new(30, 30);
|
|
1344 |
+ |
assert_eq!(exact.percent(), over.percent());
|
|
1345 |
+ |
assert!(!exact.overflowing());
|
|
1346 |
+ |
}
|
|
1347 |
+ |
|
|
1348 |
+ |
#[test]
|
|
1349 |
+ |
fn an_empty_set_does_not_divide_by_zero() {
|
|
1350 |
+ |
// Sayable on purpose, so it has to be answerable. A meter over an
|
|
1351 |
+ |
// unloaded count is what an app actually has for a frame.
|
|
1352 |
+ |
let none = Meter::new(0, 0);
|
|
1353 |
+ |
assert_eq!(none.percent(), 0);
|
|
1354 |
+ |
assert!(none.is_empty());
|
|
1355 |
+ |
assert!(!none.overflowing());
|
|
1356 |
+ |
}
|
|
1357 |
+ |
|
|
1358 |
+ |
#[test]
|
|
1359 |
+ |
fn the_ratio_survives_where_a_percentage_would_not() {
|
|
1360 |
+ |
// Given 43 nothing can recover "3 of 7", which is why the numbers are
|
|
1361 |
+ |
// carried and the label names only the noun.
|
|
1362 |
+ |
let m = Meter::new(3, 7).label("subtasks");
|
|
1363 |
+ |
assert_eq!(m.percent(), 42);
|
|
1364 |
+ |
assert_eq!((m.done, m.total), (3, 7));
|
|
1365 |
+ |
assert_eq!(m.label, Some("subtasks"));
|
|
1366 |
+ |
}
|
|
1367 |
+ |
|
|
1368 |
+ |
#[test]
|
|
1369 |
+ |
fn tone_is_carried_because_no_renderer_can_derive_it() {
|
|
1370 |
+ |
// The same fullness means opposite things on two of goingson's bars,
|
|
1371 |
+ |
// and only the app knows which.
|
|
1372 |
+ |
let subtasks = Meter::new(9, 10).tone(Tone::Success);
|
|
1373 |
+ |
let estimate = Meter::new(9, 10).tone(Tone::Danger);
|
|
1374 |
+ |
assert_eq!(subtasks.percent(), estimate.percent());
|
|
1375 |
+ |
assert_ne!(subtasks.tone, estimate.tone);
|
|
1376 |
+ |
// Untoned by default: a bar says nothing about status until something
|
|
1377 |
+ |
// says so, the same way a row is not selectable until told.
|
|
1378 |
+ |
assert_eq!(Meter::new(9, 10).tone, Tone::Neutral);
|
|
1379 |
+ |
}
|
|
1380 |
+ |
|
|
1381 |
+ |
#[test]
|
|
1382 |
+ |
fn a_meter_does_not_overflow_on_large_counts() {
|
|
1383 |
+ |
// done * 100 in u32 would wrap somewhere past 42 million. Counts that
|
|
1384 |
+ |
// size are not tasks, but a description layer that silently reports 3%
|
|
1385 |
+ |
// for a full bar is worse than one that is slow.
|
|
1386 |
+ |
let big = Meter::new(u32::MAX, u32::MAX);
|
|
1387 |
+ |
assert_eq!(big.percent(), 100);
|
|
1388 |
+ |
assert!(!big.overflowing());
|
|
1389 |
+ |
}
|
|
1390 |
+ |
|
| 1205 |
1391 |
|
#[test]
|
| 1206 |
1392 |
|
fn inset_is_raised_with_the_light_moved() {
|
| 1207 |
1393 |
|
let (rl, rd) = Bevel::Raised.edges();
|