| 2327 |
2327 |
|
}
|
| 2328 |
2328 |
|
}
|
| 2329 |
2329 |
|
|
|
2330 |
+ |
/// A window onto a sequence: where it starts, how much it covers, and how long
|
|
2331 |
+ |
/// the sequence is when that is known.
|
|
2332 |
+ |
///
|
|
2333 |
+ |
/// The mechanism under two things the vocabulary deliberately keeps apart. A
|
|
2334 |
+ |
/// carousel is a window of one frame over children that are all present; a
|
|
2335 |
+ |
/// paged list is a window of a page over rows most of which were never fetched.
|
|
2336 |
+ |
/// Those are different facts and they stay different types — [`Showing`] says
|
|
2337 |
+ |
/// which child is up, [`Paging`] says where a reader is in a query — but the
|
|
2338 |
+ |
/// arithmetic underneath is one piece of code, so a terminal and a browser
|
|
2339 |
+ |
/// cannot come to disagree about which frame is last.
|
|
2340 |
+ |
///
|
|
2341 |
+ |
/// # Why `of` is optional and `count` is not
|
|
2342 |
+ |
///
|
|
2343 |
+ |
/// `count` is what is on screen and is therefore always known. `of` is the
|
|
2344 |
+ |
/// length of the thing being windowed, and a host that cannot count says so by
|
|
2345 |
+ |
/// leaving it empty **for the life of the screen**. It is never "not counted
|
|
2346 |
+ |
/// yet": see "First paint is final paint" in the crate header. A total that
|
|
2347 |
+ |
/// turns up on a later pass widens the text that prints it.
|
|
2348 |
+ |
///
|
|
2349 |
+ |
/// # Clamping
|
|
2350 |
+ |
///
|
|
2351 |
+ |
/// Every derivation clamps rather than refusing, and a zero `count` answers
|
|
2352 |
+ |
/// `None` rather than dividing. A window past the end is a bug in the host, and
|
|
2353 |
+ |
/// a renderer that answered it by drawing nothing would report a region that
|
|
2354 |
+ |
/// vanished, which is the hardest kind of bug to find from what is on screen.
|
|
2355 |
+ |
/// [`Share::percent`] clamps for the same reason.
|
|
2356 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
2357 |
+ |
pub struct Window {
|
|
2358 |
+ |
/// The index into the sequence where the window starts.
|
|
2359 |
+ |
pub from: usize,
|
|
2360 |
+ |
/// How many the window covers. One, for a carousel.
|
|
2361 |
+ |
pub count: usize,
|
|
2362 |
+ |
/// How long the sequence is, when the host can say.
|
|
2363 |
+ |
pub of: Option<usize>,
|
|
2364 |
+ |
}
|
|
2365 |
+ |
|
|
2366 |
+ |
impl Window {
|
|
2367 |
+ |
/// A window of `count`, starting at `from`, over a sequence of unknown
|
|
2368 |
+ |
/// length.
|
|
2369 |
+ |
#[must_use]
|
|
2370 |
+ |
pub const fn new(from: usize, count: usize) -> Self {
|
|
2371 |
+ |
Self {
|
|
2372 |
+ |
from,
|
|
2373 |
+ |
count,
|
|
2374 |
+ |
of: None,
|
|
2375 |
+ |
}
|
|
2376 |
+ |
}
|
|
2377 |
+ |
|
|
2378 |
+ |
/// How long the sequence is.
|
|
2379 |
+ |
#[must_use]
|
|
2380 |
+ |
pub const fn of(mut self, of: usize) -> Self {
|
|
2381 |
+ |
self.of = Some(of);
|
|
2382 |
+ |
self
|
|
2383 |
+ |
}
|
|
2384 |
+ |
|
|
2385 |
+ |
/// One item of a sequence whose length is known. A carousel frame.
|
|
2386 |
+ |
#[must_use]
|
|
2387 |
+ |
pub const fn frame(at: usize, of: usize) -> Self {
|
|
2388 |
+ |
Self {
|
|
2389 |
+ |
from: at,
|
|
2390 |
+ |
count: 1,
|
|
2391 |
+ |
of: Some(of),
|
|
2392 |
+ |
}
|
|
2393 |
+ |
}
|
|
2394 |
+ |
|
|
2395 |
+ |
/// Which window this is, counting from zero.
|
|
2396 |
+ |
///
|
|
2397 |
+ |
/// `None` when `count` is zero, which is the only input with no answer
|
|
2398 |
+ |
/// rather than a clamped one.
|
|
2399 |
+ |
#[must_use]
|
|
2400 |
+ |
pub const fn index(self) -> Option<usize> {
|
|
2401 |
+ |
if self.count == 0 {
|
|
2402 |
+ |
return None;
|
|
2403 |
+ |
}
|
|
2404 |
+ |
Some(self.from / self.count)
|
|
2405 |
+ |
}
|
|
2406 |
+ |
|
|
2407 |
+ |
/// How many windows the sequence holds.
|
|
2408 |
+ |
///
|
|
2409 |
+ |
/// `None` unless both the length and a non-zero `count` are known. A
|
|
2410 |
+ |
/// partial answer here would be a renderer drawing "of 0".
|
|
2411 |
+ |
#[must_use]
|
|
2412 |
+ |
pub const fn windows(self) -> Option<usize> {
|
|
2413 |
+ |
match self.of {
|
|
2414 |
+ |
Some(of) if self.count > 0 => Some(of.div_ceil(self.count)),
|
|
2415 |
+ |
_ => None,
|
|
2416 |
+ |
}
|
|
2417 |
+ |
}
|
|
2418 |
+ |
|
|
2419 |
+ |
/// Whether anything sits before this window.
|
|
2420 |
+ |
#[must_use]
|
|
2421 |
+ |
pub const fn has_before(self) -> bool {
|
|
2422 |
+ |
self.from > 0
|
|
2423 |
+ |
}
|
|
2424 |
+ |
|
|
2425 |
+ |
/// Whether anything sits after it.
|
|
2426 |
+ |
///
|
|
2427 |
+ |
/// `true` when the length is unknown: a host that cannot count cannot rule
|
|
2428 |
+ |
/// out more, and offering a way forward that turns out to be empty is the
|
|
2429 |
+ |
/// cheaper of the two mistakes.
|
|
2430 |
+ |
#[must_use]
|
|
2431 |
+ |
pub const fn has_after(self) -> bool {
|
|
2432 |
+ |
match self.of {
|
|
2433 |
+ |
Some(of) => self.from.saturating_add(self.count) < of,
|
|
2434 |
+ |
None => true,
|
|
2435 |
+ |
}
|
|
2436 |
+ |
}
|
|
2437 |
+ |
|
|
2438 |
+ |
/// The window with `from` brought inside the sequence.
|
|
2439 |
+ |
///
|
|
2440 |
+ |
/// A no-op when the length is unknown, since there is nothing to clamp
|
|
2441 |
+ |
/// against.
|
|
2442 |
+ |
#[must_use]
|
|
2443 |
+ |
pub const fn clamped(mut self) -> Self {
|
|
2444 |
+ |
if let Some(of) = self.of
|
|
2445 |
+ |
&& self.from >= of
|
|
2446 |
+ |
{
|
|
2447 |
+ |
// `max(1)` by hand: `Ord::max` is not const yet, and a zero-count
|
|
2448 |
+ |
// window would otherwise clamp onto the end rather than inside it.
|
|
2449 |
+ |
let step = if self.count == 0 { 1 } else { self.count };
|
|
2450 |
+ |
self.from = of.saturating_sub(step);
|
|
2451 |
+ |
}
|
|
2452 |
+ |
self
|
|
2453 |
+ |
}
|
|
2454 |
+ |
}
|
|
2455 |
+ |
|
|
2456 |
+ |
/// Where a reader is in a set that arrived in parts.
|
|
2457 |
+ |
///
|
|
2458 |
+ |
/// A [`Window`] wearing the paged reading of itself. Distinct from a carousel's
|
|
2459 |
+ |
/// window at the top level on purpose, because the intent differs and a call
|
|
2460 |
+ |
/// site should say which one it means, while the arithmetic below is shared so
|
|
2461 |
+ |
/// the two cannot drift apart.
|
|
2462 |
+ |
///
|
|
2463 |
+ |
/// # The two idioms, and which one a renderer may draw
|
|
2464 |
+ |
///
|
|
2465 |
+ |
/// Load-more and numbered pages are both this type. Which is honest is
|
|
2466 |
+ |
/// [`paged`](Self::paged): a set whose page size is known can be drawn as
|
|
2467 |
+ |
/// "Page 3 of 8", and one without can only be drawn as "150 of 400" and a way
|
|
2468 |
+ |
/// forward. Saying it here rather than letting each renderer guess is the point
|
|
2469 |
+ |
/// — three renderers inferring it from the numbers is how they come to disagree.
|
|
2470 |
+ |
///
|
|
2471 |
+ |
/// # What it does not carry
|
|
2472 |
+ |
///
|
|
2473 |
+ |
/// No addresses. `makeover-layout` cannot name an action, and the way to ask for
|
|
2474 |
+ |
/// the next part is the host's: `quasi_router` pairs this with the addresses the
|
|
2475 |
+ |
/// same way `Row` pairs its parts with `Row::activate`. That split is the reason
|
|
2476 |
+ |
/// this type is reusable by a carousel, which has nothing to ask.
|
|
2477 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
2478 |
+ |
pub struct Paging {
|
|
2479 |
+ |
/// The window onto the set.
|
|
2480 |
+ |
pub window: Window,
|
|
2481 |
+ |
/// Whether the parts are a fixed size, and so whether pages are countable.
|
|
2482 |
+ |
///
|
|
2483 |
+ |
/// `false` for load-more, where the window simply grew and "page 2" would
|
|
2484 |
+ |
/// name nothing.
|
|
2485 |
+ |
pub paged: bool,
|
|
2486 |
+ |
}
|
|
2487 |
+ |
|
|
2488 |
+ |
impl Paging {
|
|
2489 |
+ |
/// A page of `per`, starting at `from`.
|
|
2490 |
+ |
#[must_use]
|
|
2491 |
+ |
pub const fn pages(from: usize, per: usize) -> Self {
|
|
2492 |
+ |
Self {
|
|
2493 |
+ |
window: Window::new(from, per),
|
|
2494 |
+ |
paged: true,
|
|
2495 |
+ |
}
|
|
2496 |
+ |
}
|
|
2497 |
+ |
|
|
2498 |
+ |
/// The first `shown`, with more behind them.
|
|
2499 |
+ |
///
|
|
2500 |
+ |
/// The load-more shape: the window starts at the beginning and grows, so
|
|
2501 |
+ |
/// there is no page to number.
|
|
2502 |
+ |
#[must_use]
|
|
2503 |
+ |
pub const fn more(shown: usize) -> Self {
|
|
2504 |
+ |
Self {
|
|
2505 |
+ |
window: Window::new(0, shown),
|
|
2506 |
+ |
paged: false,
|
|
2507 |
+ |
}
|
|
2508 |
+ |
}
|
|
2509 |
+ |
|
|
2510 |
+ |
/// How many there are altogether.
|
|
2511 |
+ |
///
|
|
2512 |
+ |
/// Left unsaid by a host that cannot count, and left unsaid **for good**:
|
|
2513 |
+ |
/// a total arriving later widens whatever prints it. See "First paint is
|
|
2514 |
+ |
/// final paint" in the crate header.
|
|
2515 |
+ |
#[must_use]
|
|
2516 |
+ |
pub const fn of(mut self, of: usize) -> Self {
|
|
2517 |
+ |
self.window = self.window.of(of);
|
|
2518 |
+ |
self
|
|
2519 |
+ |
}
|
|
2520 |
+ |
|
|
2521 |
+ |
/// Which page this is, counting from one, when pages are countable.
|
|
2522 |
+ |
///
|
|
2523 |
+ |
/// One-based because it is read aloud. [`Window::index`] is the zero-based
|
|
2524 |
+ |
/// form for anyone indexing with it.
|
|
2525 |
+ |
#[must_use]
|
|
2526 |
+ |
pub const fn page(self) -> Option<usize> {
|
|
2527 |
+ |
if !self.paged {
|
|
2528 |
+ |
return None;
|
|
2529 |
+ |
}
|
|
2530 |
+ |
match self.window.index() {
|
|
2531 |
+ |
Some(index) => Some(index + 1),
|
|
2532 |
+ |
None => None,
|
|
2533 |
+ |
}
|
|
2534 |
+ |
}
|
|
2535 |
+ |
|
|
2536 |
+ |
/// How many pages there are, when that is countable.
|
|
2537 |
+ |
#[must_use]
|
|
2538 |
+ |
pub const fn pages_total(self) -> Option<usize> {
|
|
2539 |
+ |
if !self.paged {
|
|
2540 |
+ |
return None;
|
|
2541 |
+ |
}
|
|
2542 |
+ |
self.window.windows()
|
|
2543 |
+ |
}
|
|
2544 |
+ |
|
|
2545 |
+ |
/// How many are on screen.
|
|
2546 |
+ |
#[must_use]
|
|
2547 |
+ |
pub const fn shown(self) -> usize {
|
|
2548 |
+ |
self.window.count
|
|
2549 |
+ |
}
|
|
2550 |
+ |
|
|
2551 |
+ |
/// How many there are, when the host counted.
|
|
2552 |
+ |
#[must_use]
|
|
2553 |
+ |
pub const fn total(self) -> Option<usize> {
|
|
2554 |
+ |
self.window.of
|
|
2555 |
+ |
}
|
|
2556 |
+ |
|
|
2557 |
+ |
/// Whether there is anything further on.
|
|
2558 |
+ |
#[must_use]
|
|
2559 |
+ |
pub const fn has_more(self) -> bool {
|
|
2560 |
+ |
self.window.has_after()
|
|
2561 |
+ |
}
|
|
2562 |
+ |
|
|
2563 |
+ |
/// Whether there is anything back the other way.
|
|
2564 |
+ |
#[must_use]
|
|
2565 |
+ |
pub const fn has_previous(self) -> bool {
|
|
2566 |
+ |
self.window.has_before()
|
|
2567 |
+ |
}
|
|
2568 |
+ |
}
|
|
2569 |
+ |
|
| 2330 |
2570 |
|
/// How much of the width an arrangement's first region takes.
|
| 2331 |
2571 |
|
///
|
| 2332 |
2572 |
|
/// `e0fd485e`. Nothing said how much room a region got, so every renderer
|
| 4300 |
4540 |
|
// the deferral rule has been broken.
|
| 4301 |
4541 |
|
assert_ne!(Readiness::Ready, Readiness::Pending);
|
| 4302 |
4542 |
|
}
|
|
4543 |
+ |
|
|
4544 |
+ |
#[test]
|
|
4545 |
+ |
fn a_window_with_no_length_still_answers_what_it_can() {
|
|
4546 |
+ |
// The uncounted case is the common one, not the degenerate one: a query
|
|
4547 |
+ |
// that asked for 51 to learn there were more than 50 knows there are,
|
|
4548 |
+ |
// and not how many.
|
|
4549 |
+ |
let uncounted = Window::new(100, 50);
|
|
4550 |
+ |
assert_eq!(uncounted.index(), Some(2));
|
|
4551 |
+ |
assert_eq!(uncounted.windows(), None);
|
|
4552 |
+ |
assert!(uncounted.has_before());
|
|
4553 |
+ |
// Unknown length cannot rule out more, and offering a way forward that
|
|
4554 |
+ |
// turns out empty is the cheaper mistake.
|
|
4555 |
+ |
assert!(uncounted.has_after());
|
|
4556 |
+ |
}
|
|
4557 |
+ |
|
|
4558 |
+ |
#[test]
|
|
4559 |
+ |
fn a_counted_window_knows_where_it_ends() {
|
|
4560 |
+ |
let last = Window::new(350, 50).of(400);
|
|
4561 |
+ |
assert_eq!(last.index(), Some(7));
|
|
4562 |
+ |
assert_eq!(last.windows(), Some(8));
|
|
4563 |
+ |
assert!(last.has_before());
|
|
4564 |
+ |
assert!(!last.has_after());
|
|
4565 |
+ |
|
|
4566 |
+ |
let first = Window::new(0, 50).of(400);
|
|
4567 |
+ |
assert!(!first.has_before());
|
|
4568 |
+ |
assert!(first.has_after());
|
|
4569 |
+ |
}
|
|
4570 |
+ |
|
|
4571 |
+ |
#[test]
|
|
4572 |
+ |
fn a_window_that_does_not_divide_evenly_rounds_up() {
|
|
4573 |
+ |
// 401 rows in pages of 50 is eight pages and a straggler, which is nine
|
|
4574 |
+ |
// pages. Rounding down would make the last one unreachable.
|
|
4575 |
+ |
assert_eq!(Window::new(0, 50).of(401).windows(), Some(9));
|
|
4576 |
+ |
}
|
|
4577 |
+ |
|
|
4578 |
+ |
#[test]
|
|
4579 |
+ |
fn a_zero_count_answers_none_rather_than_dividing() {
|
|
4580 |
+ |
let empty = Window::new(0, 0).of(400);
|
|
4581 |
+ |
assert_eq!(empty.index(), None);
|
|
4582 |
+ |
assert_eq!(empty.windows(), None);
|
|
4583 |
+ |
// And it still clamps rather than panicking.
|
|
4584 |
+ |
assert_eq!(Window::new(900, 0).of(400).clamped().from, 399);
|
|
4585 |
+ |
}
|
|
4586 |
+ |
|
|
4587 |
+ |
#[test]
|
|
4588 |
+ |
fn a_window_past_the_end_clamps_inside_rather_than_vanishing() {
|
|
4589 |
+ |
// `Slot::current`'s reasoning, one layer down: a description pointing
|
|
4590 |
+ |
// past the end is a host bug, and answering it by drawing nothing
|
|
4591 |
+ |
// reports a region that vanished.
|
|
4592 |
+ |
assert_eq!(Window::new(900, 50).of(400).clamped().from, 350);
|
|
4593 |
+ |
// Nothing to clamp against when the length is unknown.
|
|
4594 |
+ |
assert_eq!(Window::new(900, 50).clamped().from, 900);
|
|
4595 |
+ |
}
|
|
4596 |
+ |
|
|
4597 |
+ |
#[test]
|
|
4598 |
+ |
fn a_carousel_frame_is_a_window_of_one() {
|
|
4599 |
+ |
// The shape a carousel instantiates. Same code as a paged list, which is
|
|
4600 |
+ |
// the whole reason `Window` exists rather than two copies of it.
|
|
4601 |
+ |
let third = Window::frame(2, 5);
|
|
4602 |
+ |
assert_eq!(third.index(), Some(2));
|
|
4603 |
+ |
assert_eq!(third.windows(), Some(5));
|
|
4604 |
+ |
assert!(third.has_before());
|
|
4605 |
+ |
assert!(third.has_after());
|
|
4606 |
+ |
|
|
4607 |
+ |
let last = Window::frame(4, 5);
|
|
4608 |
+ |
assert!(!last.has_after());
|
|
4609 |
+ |
}
|
|
4610 |
+ |
|
|
4611 |
+ |
#[test]
|
|
4612 |
+ |
fn numbered_pages_read_from_one_and_load_more_has_no_page() {
|
|
4613 |
+ |
// The page number is read aloud, so it is one-based; `Window::index` is
|
|
4614 |
+ |
// the zero-based form for indexing.
|
|
4615 |
+ |
let third = Paging::pages(100, 50).of(400);
|
|
4616 |
+ |
assert_eq!(third.page(), Some(3));
|
|
4617 |
+ |
assert_eq!(third.pages_total(), Some(8));
|
|
4618 |
+ |
assert_eq!(third.total(), Some(400));
|
|
4619 |
+ |
assert!(third.has_previous());
|
|
4620 |
+ |
assert!(third.has_more());
|
|
4621 |
+ |
|
|
4622 |
+ |
// Load-more grew a window from the start, so "page 2" would name
|
|
4623 |
+ |
// nothing and the type says so rather than inventing one.
|
|
4624 |
+ |
let grown = Paging::more(150).of(400);
|
|
4625 |
+ |
assert_eq!(grown.page(), None);
|
|
4626 |
+ |
assert_eq!(grown.pages_total(), None);
|
|
4627 |
+ |
assert_eq!(grown.shown(), 150);
|
|
4628 |
+ |
assert!(!grown.has_previous());
|
|
4629 |
+ |
assert!(grown.has_more());
|
|
4630 |
+ |
}
|
|
4631 |
+ |
|
|
4632 |
+ |
#[test]
|
|
4633 |
+ |
fn an_uncounted_paging_offers_forward_and_admits_no_total() {
|
|
4634 |
+ |
// What a host that will not pay for a COUNT describes. `None` here is
|
|
4635 |
+ |
// permanent: a total arriving later would widen the text that prints it,
|
|
4636 |
+ |
// which is the reflow "first paint is final paint" forbids.
|
|
4637 |
+ |
let feed = Paging::more(50);
|
|
4638 |
+ |
assert_eq!(feed.total(), None);
|
|
4639 |
+ |
assert_eq!(feed.pages_total(), None);
|
|
4640 |
+ |
assert!(feed.has_more());
|
|
4641 |
+ |
}
|
| 4303 |
4642 |
|
}
|