返回 CodeWhale
motion.rs
1 //! Live caller-owned motion: `cargo run --example motion`.
2 //!
3 //! Space finishes/restarts the demonstration, v switches working/verification,
4 //! r replays, p selects a terminal profile, m changes motion, q or Escape exits.
5 //! `--frames DIR [--profile NAME]` exports 128 deterministic actual-buffer SVGs.
6 //! Only the host reads the clock and waits. Completion comes from a host event;
7 //! the exported specimen has an explicitly authored completion at 4.2 seconds.
8
9 use std::{
10 io,
11 path::{Path, PathBuf},
12 time::{Duration, Instant},
13 };
14
15 use codewhale_ratatui::{
16 Caps, FrameBudget, KeyHint, KeyHints, MotionDemo, MotionDemoWords, MotionMode, MotionPolicy,
17 MotionSet, Paint, PaneHeader, Receipt, ReceiptValue, Role, Spinner, State, Theme,
18 VerificationSpinner, WorkspaceFrame, spin,
19 testing::{self, Profile},
20 text,
21 };
22 use crossterm::{
23 event::{self, Event, KeyCode, KeyEventKind},
24 execute,
25 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
26 };
27 use ratatui::{
28 Terminal,
29 backend::CrosstermBackend,
30 buffer::Buffer,
31 layout::Rect,
32 text::{Line, Span},
33 };
34
35 const VERIFY_AT: Duration = Duration::from_millis(2_200);
36 const FINISH_AT: Duration = Duration::from_millis(4_200);
37 const FRAME_MS: u64 = 50;
38 const FRAME_COUNT: u64 = 128;
39
40 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
41 enum Stage {
42 Working,
43 Verifying,
44 Done,
45 }
46
47 impl Stage {
48 fn word(self) -> &'static str {
49 match self {
50 Self::Working => "Working",
51 Self::Verifying => "Verifying",
52 Self::Done => "Done",
53 }
54 }
55 }
56
57 /// Display state of this specimen, never a provider/session inference.
58 struct Demo {
59 stage: Stage,
60 run_started: Instant,
61 phase_started: Instant,
62 finished_elapsed: Duration,
63 motions: MotionSet,
64 }
65
66 impl Demo {
67 fn new(now: Instant) -> Self {
68 Self {
69 stage: Stage::Working,
70 run_started: now,
71 phase_started: now,
72 finished_elapsed: Duration::ZERO,
73 motions: MotionSet::new(),
74 }
75 }
76
77 fn phase_elapsed(&self, now: Instant) -> Duration {
78 now.saturating_duration_since(self.phase_started)
79 }
80
81 fn finish(&mut self, now: Instant, policy: MotionPolicy) {
82 if self.stage == Stage::Done {
83 return;
84 }
85 self.finished_elapsed = now.saturating_duration_since(self.run_started);
86 self.stage = Stage::Done;
87 MotionDemo::start(&mut self.motions, true, now, policy);
88 }
89
90 fn restart(&mut self, now: Instant, policy: MotionPolicy) {
91 self.stage = Stage::Working;
92 self.run_started = now;
93 self.phase_started = now;
94 self.finished_elapsed = Duration::ZERO;
95 // Retarget the existing steps: a mid-flight replay preserves spatial
96 // continuity rather than replacing the set with an unrelated frame.
97 MotionDemo::start(&mut self.motions, false, now, policy);
98 }
99
100 fn toggle_verification(&mut self, now: Instant) {
101 self.stage = match self.stage {
102 Stage::Working => Stage::Verifying,
103 Stage::Verifying => Stage::Working,
104 Stage::Done => return,
105 };
106 self.phase_started = now;
107 }
108
109 fn next_frame_in(&self, now: Instant, theme: &Theme, mode: MotionMode) -> Option<Duration> {
110 let mut budget = FrameBudget::new();
111 if self.stage != Stage::Done && budget.claim_spinner() {
112 let elapsed = self.phase_elapsed(now);
113 budget.request(match self.stage {
114 Stage::Working => spin::next_frame_in(elapsed, mode),
115 Stage::Verifying => VerificationSpinner::next_frame_in(elapsed, mode),
116 Stage::Done => None,
117 });
118 }
119 budget.request_at(
120 now,
121 self.motions
122 .next_frame_at(now, MotionPolicy::new(mode, theme)),
123 );
124 budget.next_frame_in()
125 }
126 }
127
128 fn mode_word(mode: MotionMode) -> &'static str {
129 match mode {
130 MotionMode::Full => "Full motion",
131 MotionMode::Reduced => "Reduced motion",
132 MotionMode::Still => "Still",
133 }
134 }
135
136 fn row(area: Rect, buf: &mut Buffer, line: &Line<'_>) {
137 let area = area.intersection(buf.area);
138 if !area.is_empty() {
139 buf.set_line(area.x, area.y, line, area.width);
140 }
141 }
142
143 fn at(area: Rect, offset: u16, height: u16) -> Rect {
144 Rect::new(
145 area.x,
146 area.y.saturating_add(offset.min(area.height)),
147 area.width,
148 height.min(area.height.saturating_sub(offset)),
149 )
150 }
151
152 fn copy_paint(component: &impl Paint, area: Rect, buf: &mut Buffer, theme: &Theme) {
153 let area = area.intersection(buf.area);
154 if area.is_empty() {
155 return;
156 }
157 // The receipt and key-hint components can use ordinary terminal widgets
158 // inside local coordinates, even when the destination reaches u16::MAX.
159 let mut local = Buffer::empty(Rect::new(0, 0, area.width, area.height));
160 for y in 0..area.height {
161 for x in 0..area.width {
162 local[(x, y)] = buf[(area.x + x, area.y + y)].clone();
163 }
164 }
165 component.paint(local.area, &mut local, theme);
166 for y in 0..area.height {
167 for x in 0..area.width {
168 buf[(area.x + x, area.y + y)] = local[(x, y)].clone();
169 }
170 }
171 }
172
173 fn caption(value: &str, area: Rect, buf: &mut Buffer, theme: &Theme) {
174 let safe = text::display_safe(value);
175 row(
176 area,
177 buf,
178 &Line::styled(
179 text::truncate_words(&safe, usize::from(area.width), theme.ascii()).into_owned(),
180 theme.fg(Role::Muted),
181 ),
182 );
183 }
184
185 fn transitions(
186 area: Rect,
187 buf: &mut Buffer,
188 theme: &Theme,
189 demo: &Demo,
190 now: Instant,
191 mode: MotionMode,
192 ) -> u16 {
193 let compact = area.width < 64;
194 let labels = if compact {
195 ["Ink / 180ms", "Focus / 180ms", "Detail / 340ms"]
196 } else {
197 [
198 "State ink / 180ms",
199 "Selection / 180ms",
200 "Detail reveal / 340ms",
201 ]
202 };
203 let words = MotionDemoWords {
204 working: demo.stage.word().into(),
205 done: "Done".into(),
206 slide: "Selected".into(),
207 detail: "Details are ready".into(),
208 };
209 let component = MotionDemo::new(
210 &demo.motions,
211 now,
212 MotionPolicy::new(mode, theme),
213 demo.stage == Stage::Done,
214 )
215 .with_words(words);
216 if area.width >= 30 {
217 let label_width = if compact { 15 } else { 24 };
218 for (index, label) in labels.into_iter().enumerate() {
219 caption(label, at(area, index as u16, 1), buf, theme);
220 }
221 component.paint(
222 Rect::new(
223 area.x.saturating_add(label_width),
224 area.y,
225 area.width.saturating_sub(label_width),
226 area.height.min(3),
227 ),
228 buf,
229 theme,
230 );
231 3
232 } else {
233 // Keep labels legible on unusually narrow terminals. The same
234 // three actual MotionDemo rows are stacked below their own labels.
235 let mut local = Buffer::empty(Rect::new(0, 0, area.width, 3));
236 component.paint(local.area, &mut local, theme);
237 for (index, label) in labels.into_iter().enumerate() {
238 let offset = index as u16 * 2;
239 caption(label, at(area, offset, 1), buf, theme);
240 let target = at(area, offset + 1, 1);
241 if !target.is_empty() {
242 for x in 0..target.width {
243 buf[(target.x + x, target.y)] = local[(x, index as u16)].clone();
244 }
245 }
246 }
247 6
248 }
249 }
250
251 fn held_samples(area: Rect, buf: &mut Buffer, theme: &Theme) {
252 let ascii = Theme::new(Caps {
253 ascii: true,
254 ..theme.caps()
255 })
256 .ground(theme.ground_kind());
257 let specimens = [
258 (
259 "ASCII / captured",
260 "800ms specimen",
261 // This is one frozen frame, not a second live working marker.
262 Spinner::new("Working", Duration::from_millis(800), MotionMode::Full).spans(&ascii),
263 ),
264 (
265 "Reduced motion",
266 "Static marker",
267 Spinner::new("Working", Duration::ZERO, MotionMode::Reduced).spans(theme),
268 ),
269 (
270 "Still",
271 "Static marker",
272 Spinner::new("Working", Duration::ZERO, MotionMode::Still).spans(theme),
273 ),
274 ];
275 if area.width >= 72 {
276 let width = area.width.saturating_sub(4) / 3;
277 for (index, (title, note, spans)) in specimens.into_iter().enumerate() {
278 let column = Rect::new(
279 area.x.saturating_add(index as u16 * (width + 2)),
280 area.y,
281 width,
282 area.height.min(3),
283 );
284 PaneHeader::new(title)
285 .divider(false)
286 .paint(at(column, 0, 1), buf, theme);
287 row(at(column, 1, 1), buf, &Line::from(spans));
288 caption(note, at(column, 2, 1), buf, theme);
289 }
290 } else {
291 for (index, (title, _, mut spans)) in specimens.into_iter().enumerate() {
292 let title = if index == 0 { "ASCII / 800ms" } else { title };
293 let mut line = vec![
294 Span::styled(title, theme.fg(Role::Muted)),
295 Span::raw(" ".repeat(16usize.saturating_sub(text::width(title)))),
296 ];
297 line.append(&mut spans);
298 row(at(area, index as u16, 1), buf, &Line::from(line));
299 }
300 }
301 }
302
303 fn paint(area: Rect, buf: &mut Buffer, theme: &Theme, demo: &Demo, now: Instant, mode: MotionMode) {
304 let area = area.intersection(buf.area);
305 if area.is_empty() {
306 return;
307 }
308 let frame = WorkspaceFrame::new("Motion")
309 .mode("Demonstration")
310 .side_width(0)
311 .footer("Demonstration / caller-owned time and state / q or Esc closes");
312 frame.paint(area, buf, theme);
313 let main = frame.areas(area).main;
314 PaneHeader::new("Motion in Codewhale")
315 .meta(mode_word(mode))
316 .divider(false)
317 .paint(at(main, 0, 1), buf, theme);
318 caption(
319 &format!("{} / caller-owned time and state", mode_word(mode)),
320 at(main, 1, 1),
321 buf,
322 theme,
323 );
324 PaneHeader::new("Work / verification / receipt")
325 .meta(demo.stage.word())
326 .divider(false)
327 .paint(at(main, 3, 1), buf, theme);
328 let elapsed = demo.phase_elapsed(now);
329 match demo.stage {
330 Stage::Working => row(
331 at(main, 4, 1),
332 buf,
333 &Line::from(Spinner::new("Working", elapsed, mode).spans(theme)),
334 ),
335 Stage::Verifying => row(
336 at(main, 4, 1),
337 buf,
338 &Line::from(VerificationSpinner::new("Verifying", elapsed, mode).spans(theme)),
339 ),
340 Stage::Done => copy_paint(
341 &Receipt::new(State::Done, "Motion sample")
342 .value(ReceiptValue::Duration(Some(demo.finished_elapsed))),
343 at(main, 4, 1),
344 buf,
345 theme,
346 ),
347 }
348 caption(
349 if demo.stage == Stage::Done {
350 "A receipt replaces the live marker."
351 } else {
352 "Earned after 400ms / 200ms cadence"
353 },
354 at(main, 5, 1),
355 buf,
356 theme,
357 );
358 PaneHeader::new("Three state transitions")
359 .meta("Space finishes")
360 .divider(false)
361 .paint(at(main, 7, 1), buf, theme);
362 let transition_rows = transitions(at(main, 8, 6), buf, theme, demo, now, mode);
363 let samples_start = 8 + transition_rows + 1;
364 PaneHeader::new("Held mode specimens").divider(false).paint(
365 at(main, samples_start, 1),
366 buf,
367 theme,
368 );
369 held_samples(at(main, samples_start + 1, 3), buf, theme);
370 let hints = KeyHints::new(vec![
371 KeyHint::new("Space", "toggle"),
372 KeyHint::new("v", "verify"),
373 KeyHint::new("r", "replay"),
374 KeyHint::new("p", "profile"),
375 KeyHint::new("m", "motion"),
376 KeyHint::new("q", "close"),
377 ]);
378 let hints_start = samples_start + 5;
379 copy_paint(&hints, at(main, hints_start, 3), buf, theme);
380 caption(
381 if demo.next_frame_in(now, theme, mode).is_some() {
382 "Redraws only while a marker or step moves."
383 } else {
384 "Quiet: waiting for input."
385 },
386 at(main, hints_start + 4, 1),
387 buf,
388 theme,
389 );
390 }
391
392 fn exported_demo(base: Instant, elapsed: Duration, theme: &Theme) -> Demo {
393 let mut demo = Demo::new(base);
394 if elapsed >= VERIFY_AT {
395 demo.toggle_verification(base + VERIFY_AT);
396 }
397 if elapsed >= FINISH_AT {
398 demo.finish(base + FINISH_AT, MotionPolicy::new(MotionMode::Full, theme));
399 }
400 demo
401 }
402
403 fn export_frames(dir: &Path, profile: Profile) -> io::Result<()> {
404 std::fs::create_dir_all(dir)?;
405 let theme = profile.theme();
406 let base = Instant::now();
407 let mut manifest = Vec::new();
408 for index in 0..FRAME_COUNT {
409 let elapsed_ms = index * FRAME_MS;
410 let elapsed = Duration::from_millis(elapsed_ms);
411 let demo = exported_demo(base, elapsed, &theme);
412 let buf = testing::render(96, 30, |area, buf| {
413 paint(area, buf, &theme, &demo, base + elapsed, MotionMode::Full);
414 });
415 let file = format!("frame-{index:03}.svg");
416 std::fs::write(dir.join(&file), testing::svg(&buf, &theme))?;
417 manifest.push(serde_json::json!({
418 "file":file, "elapsed_ms":elapsed_ms, "profile":profile.name(), "width":96, "height":30,
419 }));
420 }
421 std::fs::write(
422 dir.join("manifest.json"),
423 serde_json::to_vec_pretty(&manifest)?,
424 )?;
425 Ok(())
426 }
427
428 fn export_options(args: &[String]) -> io::Result<Option<(PathBuf, Profile)>> {
429 if args.is_empty() {
430 return Ok(None);
431 }
432 let usage = || {
433 io::Error::new(
434 io::ErrorKind::InvalidInput,
435 "usage: motion [--frames DIR [--profile NAME]]",
436 )
437 };
438 if args.first().map(String::as_str) != Some("--frames") || !matches!(args.len(), 2 | 4) {
439 return Err(usage());
440 }
441 let profile = if args.len() == 4 {
442 if args[2] != "--profile" {
443 return Err(usage());
444 }
445 Profile::from_name(&args[3]).ok_or_else(usage)?
446 } else {
447 Profile::DarkTrue
448 };
449 Ok(Some((PathBuf::from(&args[1]), profile)))
450 }
451
452 struct Restore;
453 impl Drop for Restore {
454 fn drop(&mut self) {
455 let _ = disable_raw_mode();
456 let _ = execute!(io::stdout(), LeaveAlternateScreen);
457 }
458 }
459
460 fn main() -> io::Result<()> {
461 let args: Vec<_> = std::env::args().skip(1).collect();
462 if let Some((dir, profile)) = export_options(&args)? {
463 return export_frames(&dir, profile);
464 }
465 enable_raw_mode()?;
466 let _restore = Restore;
467 execute!(io::stdout(), EnterAlternateScreen)?;
468 let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
469 let mut demo = Demo::new(Instant::now());
470 let mut profile = 0;
471 let mut mode = MotionMode::Full;
472 loop {
473 let now = Instant::now();
474 let theme = Profile::ALL[profile].theme();
475 terminal.draw(|frame| paint(frame.area(), frame.buffer_mut(), &theme, &demo, now, mode))?;
476 if let Some(after) = demo.next_frame_in(now, &theme, mode)
477 && !event::poll(after.saturating_sub(now.elapsed()))?
478 {
479 continue;
480 }
481 // No deadline: read blocks until input, including while a reduced
482 // motion specimen is active and after the completed steps settle.
483 if let Event::Key(key) = event::read()?
484 && key.kind == KeyEventKind::Press
485 {
486 let now = Instant::now();
487 let policy = MotionPolicy::new(mode, &theme);
488 match key.code {
489 KeyCode::Char('q') | KeyCode::Esc => break,
490 KeyCode::Char(' ') => {
491 if demo.stage == Stage::Done {
492 demo.restart(now, policy);
493 } else {
494 demo.finish(now, policy);
495 }
496 }
497 KeyCode::Char('v') => demo.toggle_verification(now),
498 KeyCode::Char('r') => demo.restart(now, policy),
499 KeyCode::Char('p') => {
500 profile = (profile + 1) % Profile::ALL.len();
501 demo.motions.settle_all();
502 }
503 KeyCode::Char('m') => {
504 mode = match mode {
505 MotionMode::Full => MotionMode::Reduced,
506 MotionMode::Reduced => MotionMode::Still,
507 MotionMode::Still => MotionMode::Full,
508 };
509 demo.motions.settle_all();
510 }
511 _ => {}
512 }
513 }
514 }
515 Ok(())
516 }
517
518 #[cfg(test)]
519 mod tests {
520 use super::*;
521
522 fn frame(
523 base: Instant,
524 elapsed: Duration,
525 profile: Profile,
526 width: u16,
527 height: u16,
528 ) -> Buffer {
529 let theme = profile.theme();
530 let demo = exported_demo(base, elapsed, &theme);
531 testing::render(width, height, |area, buf| {
532 paint(area, buf, &theme, &demo, base + elapsed, MotionMode::Full);
533 })
534 }
535
536 #[test]
537 fn export_has_fixed_phases_deterministic_frames_and_a_static_completed_hold() {
538 let base = Instant::now();
539 let later_base = base + Duration::from_secs(3_600);
540 let theme = Profile::DarkTrue.theme();
541 for index in 0..FRAME_COUNT {
542 let elapsed = Duration::from_millis(index * FRAME_MS);
543 let demo = exported_demo(base, elapsed, &theme);
544 let stage = if elapsed < VERIFY_AT {
545 Stage::Working
546 } else if elapsed < FINISH_AT {
547 Stage::Verifying
548 } else {
549 Stage::Done
550 };
551 assert_eq!(demo.stage, stage);
552 assert_eq!(
553 frame(base, elapsed, Profile::DarkTrue, 96, 30),
554 frame(later_base, elapsed, Profile::DarkTrue, 96, 30),
555 "frame {index} must depend on elapsed time, not the wall clock",
556 );
557 }
558 let verifying = exported_demo(base, VERIFY_AT, &theme);
559 assert_eq!(verifying.phase_elapsed(base + VERIFY_AT), Duration::ZERO);
560 let finished = exported_demo(base, FINISH_AT, &theme);
561 assert_eq!(finished.finished_elapsed, FINISH_AT);
562 assert!(
563 testing::text(&frame(
564 base,
565 Duration::from_millis(2_600),
566 Profile::DarkTrue,
567 96,
568 30
569 ))
570 .contains("Verifying")
571 );
572 assert_eq!(
573 frame(
574 base,
575 Duration::from_millis(4_550),
576 Profile::DarkTrue,
577 96,
578 30
579 ),
580 frame(
581 base,
582 Duration::from_millis(6_350),
583 Profile::DarkTrue,
584 96,
585 30
586 ),
587 "the final hold cannot keep a spinner, time readout or transition moving",
588 );
589 }
590
591 #[test]
592 fn scheduler_uses_earned_cadence_then_stops_for_quiet_modes_and_settled_completion() {
593 let base = Instant::now();
594 let theme = Profile::DarkTrue.theme();
595 let policy = MotionPolicy::new(MotionMode::Full, &theme);
596 let mut demo = Demo::new(base);
597 assert_eq!(
598 demo.next_frame_in(base, &theme, MotionMode::Full),
599 Some(spin::EARN_DELAY)
600 );
601 assert_eq!(
602 demo.next_frame_in(base + spin::EARN_DELAY, &theme, MotionMode::Full),
603 Some(spin::FRAME_INTERVAL)
604 );
605 for mode in [MotionMode::Reduced, MotionMode::Still] {
606 assert_eq!(demo.next_frame_in(base, &theme, mode), None);
607 }
608 demo.toggle_verification(base + VERIFY_AT);
609 assert_eq!(
610 demo.next_frame_in(base + VERIFY_AT, &theme, MotionMode::Full),
611 VerificationSpinner::next_frame_in(Duration::ZERO, MotionMode::Full)
612 );
613 demo.finish(base + FINISH_AT, policy);
614 assert!(
615 demo.next_frame_in(base + FINISH_AT, &theme, MotionMode::Full)
616 .is_some()
617 );
618 assert_eq!(
619 demo.next_frame_in(
620 base + FINISH_AT + Duration::from_millis(340),
621 &theme,
622 MotionMode::Full
623 ),
624 None
625 );
626 let mut reduced = Demo::new(base);
627 reduced.finish(
628 base + FINISH_AT,
629 MotionPolicy::new(MotionMode::Reduced, &theme),
630 );
631 assert!(reduced.motions.is_settled(base + FINISH_AT));
632 assert_eq!(
633 reduced.next_frame_in(base + FINISH_AT, &theme, MotionMode::Reduced),
634 None
635 );
636 }
637
638 #[test]
639 fn replay_preserves_step_continuity_and_resets_only_caller_owned_elapsed_time() {
640 let base = Instant::now();
641 let theme = Profile::DarkTrue.theme();
642 let policy = MotionPolicy::new(MotionMode::Full, &theme);
643 let mut demo = Demo::new(base);
644 demo.finish(base + FINISH_AT, policy);
645 let midway = base + FINISH_AT + Duration::from_millis(50);
646 let before = demo.motions.step("slide").at(midway);
647 assert!(before > 0.0 && before < 1.0);
648 demo.restart(midway, policy);
649 assert_eq!(demo.stage, Stage::Working);
650 assert_eq!(demo.phase_elapsed(midway), Duration::ZERO);
651 assert_eq!(demo.finished_elapsed, Duration::ZERO);
652 assert_eq!(demo.motions.step("slide").at(midway), before);
653 assert_eq!(demo.motions.step("slide").target(), 0.0);
654 demo.toggle_verification(midway + Duration::from_millis(100));
655 assert_eq!(demo.stage, Stage::Verifying);
656 assert_eq!(
657 demo.phase_elapsed(midway + Duration::from_millis(100)),
658 Duration::ZERO
659 );
660 demo.finish(midway + Duration::from_millis(700), policy);
661 assert_eq!(demo.finished_elapsed, Duration::from_millis(700));
662 let last_step = demo.motions.step("reveal");
663 demo.finish(midway + Duration::from_secs(20), policy);
664 assert_eq!(demo.finished_elapsed, Duration::from_millis(700));
665 assert_eq!(
666 demo.motions.step("reveal"),
667 last_step,
668 "same completion never restarts a transition"
669 );
670 }
671
672 #[test]
673 fn narrow_profiles_keep_state_words_and_partial_tiny_or_maximum_buffers_are_bounded() {
674 let base = Instant::now();
675 for profile in Profile::ALL {
676 let theme = profile.theme();
677 for (elapsed, state) in [(1_000, "Working"), (2_800, "Verifying"), (4_550, "Done")] {
678 let buf = frame(base, Duration::from_millis(elapsed), profile, 40, 28);
679 let shown = testing::text(&buf);
680 assert!(
681 shown.contains("Motion in Codewhale") && shown.contains(state),
682 "{shown}"
683 );
684 assert!(
685 shown.contains("Ink / 180ms") && shown.contains("Detail / 340ms"),
686 "{shown}"
687 );
688 assert!(
689 shown.contains("ASCII / 800ms")
690 && shown.contains("Reduced motion")
691 && shown.contains("Still"),
692 "{shown}"
693 );
694 assert!(
695 shown.contains("v verify") && shown.contains("r replay"),
696 "{shown}"
697 );
698 let checked = testing::Frame::new("narrow motion", profile, buf);
699 assert!(checked.violations().is_empty(), "{}", checked.label());
700 }
701 for origin in [(5, 4), (u16::MAX - 82, u16::MAX - 31)] {
702 for width in [0, 1, 4, 80] {
703 for height in [0, 1, 4, 30] {
704 let bounds = Rect::new(origin.0, origin.1, 82, 31);
705 let requested = Rect::new(origin.0 + 2, origin.1 + 1, width, height);
706 let clipped = requested.intersection(bounds);
707 let mut buf = Buffer::empty(bounds);
708 for cell in &mut buf.content {
709 cell.set_symbol("z");
710 }
711 let before = buf.clone();
712 let elapsed = Duration::from_millis(4_250);
713 let demo = exported_demo(base, elapsed, &theme);
714 paint(
715 requested,
716 &mut buf,
717 &theme,
718 &demo,
719 base + elapsed,
720 MotionMode::Full,
721 );
722 for y in bounds.y..bounds.bottom() {
723 for x in bounds.x..bounds.right() {
724 if !clipped.contains((x, y).into()) {
725 assert_eq!(
726 buf[(x, y)],
727 before[(x, y)],
728 "{} {width}x{height}",
729 profile.name()
730 );
731 }
732 }
733 }
734 }
735 }
736 }
737 }
738 }
739 }
740
740 lines RUST