max / quasi
3 files changed,
+154 insertions,
-7 deletions
| @@ -105,6 +105,26 @@ | |||
| 105 | 105 | !self.under.is_empty() | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | + | /// What the user has typed and ticked on it. | |
| 109 | + | /// | |
| 110 | + | /// The terminal's runtime has had this since the beginning; here it was | |
| 111 | + | /// missing, which made "a refresh keeps what a navigation drops" a claim | |
| 112 | + | /// nothing outside this module could check. | |
| 113 | + | #[must_use] | |
| 114 | + | pub const fn view(&self) -> &View { | |
| 115 | + | &self.view | |
| 116 | + | } | |
| 117 | + | ||
| 118 | + | /// The same, to write into. | |
| 119 | + | /// | |
| 120 | + | /// What [`View::set`] documents itself for — "put a value in, as a host | |
| 121 | + | /// restoring one would" — was unreachable for a host holding a [`Runtime`]: | |
| 122 | + | /// `show` draws through the runtime's own view and nothing handed it out. | |
| 123 | + | /// Restoring a draft into a screen is the host's job and this is how. | |
| 124 | + | pub const fn view_mut(&mut self) -> &mut View { | |
| 125 | + | &mut self.view | |
| 126 | + | } | |
| 127 | + | ||
| 108 | 128 | /// Draw a frame, and answer what to do about it. | |
| 109 | 129 | /// | |
| 110 | 130 | /// The chrome's keys are read before the drawing, so a screen cannot capture | |
| @@ -298,13 +318,25 @@ | |||
| 298 | 318 | None | |
| 299 | 319 | } | |
| 300 | 320 | Outcome::Screen(screen) => { | |
| 321 | + | // Arriving where you already are is a refresh rather than a | |
| 322 | + | // navigation, and the difference is the whole of what the user | |
| 323 | + | // has done to the screen. `reset` and `seed` are both *arrival* | |
| 324 | + | // behaviour: one drops what was typed and ticked, the other | |
| 325 | + | // applies what the description says is ticked. Running either on | |
| 326 | + | // a refresh would undo the user mid-sentence — a text field | |
| 327 | + | // cleared on every reload, and an untick put back the moment | |
| 328 | + | // anything redrew, which is the exact failure `View::seed`'s own | |
| 329 | + | // documentation says it is applied once to avoid. | |
| 330 | + | let refreshed = self.here.as_ref() == Some(request); | |
| 301 | 331 | // A navigation replaces everything, including any overlay open | |
| 302 | 332 | // over it. | |
| 303 | 333 | self.under.clear(); | |
| 304 | 334 | self.remember(request, address.as_ref()); | |
| 305 | 335 | self.screen = screen; | |
| 306 | - | self.view.reset(); | |
| 307 | - | self.view.seed(&self.screen); | |
| 336 | + | if !refreshed { | |
| 337 | + | self.view.reset(); | |
| 338 | + | self.view.seed(&self.screen); | |
| 339 | + | } | |
| 308 | 340 | self.announce(); | |
| 309 | 341 | None | |
| 310 | 342 | } |
| @@ -351,6 +351,112 @@ | |||
| 351 | 351 | assert_eq!(runtime.back(), Step::Idle); | |
| 352 | 352 | } | |
| 353 | 353 | ||
| 354 | + | #[test] | |
| 355 | + | fn a_reload_keeps_what_the_user_is_in_the_middle_of_typing() { | |
| 356 | + | // The failure this guards, and it is the one that decides whether `reload` | |
| 357 | + | // is usable at all: a refresh goes through `Outcome::Screen`, which resets | |
| 358 | + | // the view on arrival. A host reloading a form every frame would clear the | |
| 359 | + | // box under the caret sixty times a second. | |
| 360 | + | let mut runtime = Runtime::new(screen_of([Node::text("opening")])); | |
| 361 | + | let form = || { | |
| 362 | + | screen_of([Node::Field(Box::new(Field::new( | |
| 363 | + | layout::FieldKind::Text, | |
| 364 | + | "naming-pattern", | |
| 365 | + | "Naming pattern", | |
| 366 | + | )))]) | |
| 367 | + | }; | |
| 368 | + | runtime.apply( | |
| 369 | + | &Request::get("/export"), | |
| 370 | + | Response { | |
| 371 | + | outcome: Outcome::Screen(form()), | |
| 372 | + | notice: None, | |
| 373 | + | address: None, | |
| 374 | + | invalidates: Vec::new(), | |
| 375 | + | }, | |
| 376 | + | ); | |
| 377 | + | ||
| 378 | + | runtime.view_mut().set("naming-pattern", "{name}-{bpm}"); | |
| 379 | + | ||
| 380 | + | // Five refreshes, which is what a host reloading every frame does. | |
| 381 | + | for _ in 0..5 { | |
| 382 | + | let Step::Call(request) = runtime.reload() else { | |
| 383 | + | panic!("a screen that was navigated to can be asked for again"); | |
| 384 | + | }; | |
| 385 | + | runtime.apply( | |
| 386 | + | &request, | |
| 387 | + | Response { | |
| 388 | + | outcome: Outcome::Screen(form()), | |
| 389 | + | notice: None, | |
| 390 | + | address: None, | |
| 391 | + | invalidates: Vec::new(), | |
| 392 | + | }, | |
| 393 | + | ); | |
| 394 | + | } | |
| 395 | + | assert_eq!( | |
| 396 | + | runtime.view().edit("naming-pattern"), | |
| 397 | + | Some("{name}-{bpm}"), | |
| 398 | + | "a refresh is not an arrival, so it does not clear the box being typed into" | |
| 399 | + | ); | |
| 400 | + | ||
| 401 | + | // Going somewhere else is a different matter, and still clears. | |
| 402 | + | runtime.apply( | |
| 403 | + | &Request::get("/settings"), | |
| 404 | + | Response { | |
| 405 | + | outcome: Outcome::Screen(screen_of([Node::text("elsewhere")])), | |
| 406 | + | notice: None, | |
| 407 | + | address: None, | |
| 408 | + | invalidates: Vec::new(), | |
| 409 | + | }, | |
| 410 | + | ); | |
| 411 | + | assert_eq!(runtime.view().edit("naming-pattern"), None); | |
| 412 | + | } | |
| 413 | + | ||
| 414 | + | #[test] | |
| 415 | + | fn a_reload_does_not_put_back_a_tick_the_user_took_off() { | |
| 416 | + | // `View::seed` is arrival behaviour by its own documentation -- "after this | |
| 417 | + | // the user's ticks are the truth" -- so a refresh must not run it. Otherwise | |
| 418 | + | // unticking a row that the description says is ticked lasts exactly until | |
| 419 | + | // the next reload. | |
| 420 | + | let ticked = || { | |
| 421 | + | let mut row = quasi_router::Row::new("One"); | |
| 422 | + | row.selected = Some(true); | |
| 423 | + | row.value = Some("1".to_owned()); | |
| 424 | + | screen_of([Node::list([row])]) | |
| 425 | + | }; | |
| 426 | + | ||
| 427 | + | let mut runtime = Runtime::new(screen_of([Node::text("opening")])); | |
| 428 | + | runtime.apply( | |
| 429 | + | &Request::get("/files"), | |
| 430 | + | Response { | |
| 431 | + | outcome: Outcome::Screen(ticked()), | |
| 432 | + | notice: None, | |
| 433 | + | address: None, | |
| 434 | + | invalidates: Vec::new(), | |
| 435 | + | }, | |
| 436 | + | ); | |
| 437 | + | ||
| 438 | + | // Arrival seeded it, which is the behaviour being distinguished from. | |
| 439 | + | assert!(runtime.view().is_ticked("1")); | |
| 440 | + | runtime.view_mut().tick("1"); | |
| 441 | + | ||
| 442 | + | let Step::Call(request) = runtime.reload() else { | |
| 443 | + | panic!("a screen that was navigated to can be asked for again"); | |
| 444 | + | }; | |
| 445 | + | runtime.apply( | |
| 446 | + | &request, | |
| 447 | + | Response { | |
| 448 | + | outcome: Outcome::Screen(ticked()), | |
| 449 | + | notice: None, | |
| 450 | + | address: None, | |
| 451 | + | invalidates: Vec::new(), | |
| 452 | + | }, | |
| 453 | + | ); | |
| 454 | + | assert!( | |
| 455 | + | !runtime.view().is_ticked("1"), | |
| 456 | + | "the user took it off and a refresh is not an arrival" | |
| 457 | + | ); | |
| 458 | + | } | |
| 459 | + | ||
| 354 | 460 | #[test] | |
| 355 | 461 | fn an_overlay_is_not_a_place_and_dismissing_it_reveals_what_was_under_it() { | |
| 356 | 462 | let mut runtime = Runtime::new(screen_of([Node::text("underneath")])); |
| @@ -556,14 +556,23 @@ | |||
| 556 | 556 | // navigating with a palette still floating over the destination is | |
| 557 | 557 | // the state nobody asked for. | |
| 558 | 558 | Outcome::Screen(screen) => { | |
| 559 | + | // Arriving where you already are is a refresh rather than a | |
| 560 | + | // navigation, and the difference is the whole of what the user | |
| 561 | + | // has done to the screen. `reset` and `seed` below are both | |
| 562 | + | // *arrival* behaviour, so a refresh runs neither: otherwise a | |
| 563 | + | // reload clears the field being typed into and puts back an | |
| 564 | + | // untick the moment anything redraws. | |
| 565 | + | let refreshed = self.here.as_ref() == Some(request); | |
| 559 | 566 | self.under.clear(); | |
| 560 | 567 | self.remember(request, address.as_ref()); | |
| 561 | 568 | self.screen = screen; | |
| 562 | - | self.view.reset(); | |
| 563 | - | // The rows a new screen says are already ticked. After this the | |
| 564 | - | // user's ticks are the truth, which is why it is applied once | |
| 565 | - | // on arrival rather than read on every draw. | |
| 566 | - | self.view.seed(&self.screen); | |
| 569 | + | if !refreshed { | |
| 570 | + | self.view.reset(); | |
| 571 | + | // The rows a new screen says are already ticked. After this | |
| 572 | + | // the user's ticks are the truth, which is why it is applied | |
| 573 | + | // once on arrival rather than read on every draw. | |
| 574 | + | self.view.seed(&self.screen); | |
| 575 | + | } | |
| 567 | 576 | self.announce(); | |
| 568 | 577 | None | |
| 569 | 578 | } |