pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}Expand description
A rectangular cell region clipped by drawing operations to the frame.
Fields§
§x: u16Leftmost column.
y: u16Topmost row.
width: u16Region width in cells.
height: u16Region height in cells.
Implementations§
Source§impl Rect
impl Rect
Sourcepub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self
Creates a cell rectangle.
Examples found in repository?
examples/chat/welcome.rs (line 266)
257 fn draw_backdrop(&mut self, viewport: Size, clock: Duration, elapsed: f32) {
258 let interval = ambient_interval(clock, BACKDROP_IDLE_INTERVAL);
259 if self
260 .backdrop_at
261 .is_none_or(|rendered_at| clock.saturating_sub(rendered_at) >= interval)
262 {
263 let fade = smooth((elapsed / BACKDROP_FADE).clamp(0.0, 1.0));
264 self
265 .backdrop_frame
266 .fill(Rect::new(0, 0, viewport.width, viewport.height), Style::default());
267 let frame = &mut self.backdrop_frame;
268 let mut buffer = [0_u8; 4];
269 let dim = |color: Color| Color::Rgb(0, 0, 0).lerp(color, fade);
270 self.surface.render(
271 &mut self.backdrop,
272 clock,
273 viewport.width,
274 viewport.height,
275 |x, y, glyph, fg, bg| {
276 let style = Style::new().fg(dim(fg));
277 let style = match bg {
278 Some(bg) => style.bg(dim(bg)),
279 None => style,
280 };
281 frame.put(x, y, glyph.encode_utf8(&mut buffer), style);
282 },
283 );
284 self.backdrop_at = Some(clock);
285 }
286 self.frame.clone_from(&self.backdrop_frame);
287 }
288
289 fn draw_card(&mut self, cols: u16, left: u16, top: u16, elapsed: f32, hover: f32) {
290 let full = cols == CARD_COLS;
291 let logo_left = if full {
292 left + 3
293 } else {
294 left + (cols - LOGO_COLS as u16) / 2
295 };
296 self.logo_origin = (logo_left, top + 2);
297 // Pointer-tracking border glow: the brand gradient sampled by angle
298 // around the card center (the disk's own palette), strongest near
299 // the pointer, scaled by the eased hover amount.
300 let pointer = self.pointer;
301 let center =
302 (f32::from(left) + f32::from(cols) / 2.0, f32::from(top) + f32::from(CARD_ROWS) / 2.0);
303 let edge_at = move |x: u16, y: u16| -> Style {
304 let Some((px, py)) = pointer.filter(|_| hover > 0.02) else {
305 return on_card(CARD_BORDER);
306 };
307 let dx = (f32::from(x) - f32::from(px)) * 0.5;
308 let dy = f32::from(y) - f32::from(py);
309 let glow = hover * (-(dx * dx + dy * dy) / 34.0).exp();
310 if glow < 0.02 {
311 return on_card(CARD_BORDER);
312 }
313 let angle = (f32::from(y) - center.1).atan2((f32::from(x) - center.0) * 0.5);
314 let brand = vec3_color(gradient(angle - elapsed * 0.5));
315 on_card(CARD_BORDER.lerp(brand, glow))
316 };
317 let frame = &mut self.frame;
318 frame.fill(Rect::new(left, top, cols, CARD_ROWS), on_card(TEXT));
319
320 let right = left + cols - 1;
321 let bottom = top + CARD_ROWS - 1;
322 let divider = bottom - 2;
323 let (tl, tr, bl, br, horizontal, vertical) = self.charset.border(Border::Round);
324 let grid = self.charset.grid();
325 let mut glyph = [0_u8; 4];
326 frame.put(left, top, tl.encode_utf8(&mut glyph), edge_at(left, top));
327 frame.put(right, top, tr.encode_utf8(&mut glyph), edge_at(right, top));
328 frame.put(left, divider, grid.middle.0.encode_utf8(&mut glyph), edge_at(left, divider));
329 frame.put(right, divider, grid.middle.2.encode_utf8(&mut glyph), edge_at(right, divider));
330 frame.put(left, bottom, bl.encode_utf8(&mut glyph), edge_at(left, bottom));
331 frame.put(right, bottom, br.encode_utf8(&mut glyph), edge_at(right, bottom));
332 for x in left + 1..right {
333 frame.put(x, top, horizontal.encode_utf8(&mut glyph), edge_at(x, top));
334 frame.put(x, divider, horizontal.encode_utf8(&mut glyph), edge_at(x, divider));
335 frame.put(x, bottom, horizontal.encode_utf8(&mut glyph), edge_at(x, bottom));
336 }
337 for y in top + 1..bottom {
338 if y != divider {
339 frame.put(left, y, vertical.encode_utf8(&mut glyph), edge_at(left, y));
340 frame.put(right, y, vertical.encode_utf8(&mut glyph), edge_at(right, y));
341 }
342 }
343
344 frame.put(left + 2, top, self.title.as_str(), on_card(TEXT_STRONG));
345 if full {
346 frame.put(left + 34, top, " SESSION ORBIT ", on_card(FAINT));
347 draw_dust(frame, left, top, elapsed);
348 draw_sessions(frame, left, top, self.charset);
349 draw_beam(frame, left, top, elapsed);
350 }
351
352 blit_logo(frame, &self.logo, logo_left, top + 2, CARD_BG);
353
354 let footer = divider + 1;
355 frame.fill(Rect::new(left + 1, footer, cols - 2, 1), on_footer(TEXT));
356 if full {
357 frame.put(left + 3, divider, " SHORTCUTS ", on_card(FAINT));
358 let dot = fmts!(" {} ", self.charset.icon(Icon::Enabled));
359 let x = frame.put(left + cols - 21, top, &dot, on_card(GREEN));
360 frame.put(x, top, "rust-analyzer ", on_card(MUTED));
361 draw_full_hints(frame, left, footer);
362 } else {
363 draw_smol_hints(frame, left, cols, footer);
364 }
365 }
366}
367
368impl Default for Welcome {
369 fn default() -> Self {
370 Self::new(Charset::NerdFont)
371 }
372}
373
374fn draw_dust(frame: &mut Frame, left: u16, top: u16, elapsed: f32) {
375 for &(x, y, offset) in &DUST {
376 let pulse = 0.5 + 0.5 * (elapsed * 1.4 + offset).sin();
377 let color = FAINT.lerp(CYAN, pulse * 0.28);
378 frame.put(left + x, top + y, "·", on_card(color));
379 }
380 frame.put(left + 1, top + 7, HORIZON, on_card(FAINT.lerp(INDIGO, 0.16)));
381 frame.put(left + 14, top + 1, "+Z", on_card(FAINT));
382}
383
384fn draw_beam(frame: &mut Frame, left: u16, top: u16, elapsed: f32) {
385 let phase = (elapsed * 9.0) as usize % BEAM.len();
386 for (index, &(x, y, glyph)) in BEAM.iter().enumerate() {
387 let direct = index.abs_diff(phase);
388 let distance = direct.min(BEAM.len() - direct);
389 let color = match distance {
390 0 => TEXT_STRONG,
391 1 => CYAN,
392 _ => FAINT.lerp(INDIGO, 0.34),
393 };
394 frame.put(left + x, top + y, glyph, on_card(color));
395 }
396}
397
398fn draw_sessions(frame: &mut Frame, left: u16, top: u16, charset: Charset) {
399 let (_, _, _, _, _, vertical) = charset.border(Border::Round);
400 let mut glyph = [0_u8; 4];
401 let panel_x = left + 36;
402 frame.put(panel_x, top + 2, "RECENT SESSIONS", on_card(MUTED));
403 frame.put(left + CARD_COLS - 14, top + 2, "4 / LOCAL", on_card(FAINT));
404 for y in top + 4..=top + 10 {
405 frame.put(panel_x, y, vertical.encode_utf8(&mut glyph), on_card(FAINT.lerp(INDIGO, 0.18)));
406 }
407 for (index, (label, age)) in SESSIONS.iter().enumerate() {
408 let y = top + 4 + index as u16 * 2;
409 if index == 0 {
410 frame.fill(Rect::new(panel_x - 2, y, CARD_COLS - 35, 1), on_selected(TEXT));
411 frame.put(panel_x - 2, y, charset.rail(), on_selected(GREEN));
412 frame.put(panel_x, y, charset.radio(true), on_selected(GREEN));
413 frame.put(panel_x + 2, y, age, on_selected(GREEN));
414 frame.put(panel_x + 7, y, label, on_selected(TEXT_STRONG));
415 } else {
416 frame.put(panel_x, y, charset.radio(false), on_card(FAINT));
417 frame.put(panel_x + 2, y, age, on_card(FAINT));
418 frame.put(panel_x + 7, y, label, on_card(MUTED));
419 }
420 }
421}More examples
examples/chat/demo.rs (line 669)
664 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
665 let mut left = self.left_group(pc.now);
666 let (_, left_width) = left.measure(pc.ctx);
667 let (_, right_width) = self.right.measure(pc.ctx);
668 if left_width.saturating_add(2).saturating_add(right_width) <= rect.width {
669 left.paint(pc, Rect::new(rect.x, rect.y, left_width, 1));
670 let dock = rect
671 .x
672 .saturating_add(rect.width)
673 .saturating_sub(right_width);
674 self
675 .right
676 .paint(pc, Rect::new(dock, rect.y, right_width, 1));
677 } else {
678 let mut combined = self.combined(pc.now);
679 combined.paint(pc, rect);
680 }
681 let work = self.work.borrow();
682 let fade_frame = work
683 .fade
684 .settles_at()
685 .min(pc.now.saturating_add(FADE_FRAME));
686 let deadline = match (work.working, work.fade.is_settled(pc.now)) {
687 (true, true) => Some(pc.ctx.charset.spinner().next_change(pc.now)),
688 (true, false) => Some(pc.ctx.charset.spinner().next_change(pc.now).min(fade_frame)),
689 (false, false) => Some(fade_frame),
690 (false, true) => None,
691 };
692 if let Some(at) = deadline {
693 pc.wake(self.slot, at);
694 }
695 }
696
697 fn paints_background(&self) -> bool {
698 false
699 }
700}
701
702/// One retained chat document update and its exact repainted row ranges.
703pub struct RenderedFrame<'a> {
704 pub(crate) frame: &'a Frame,
705 pub(crate) stable_rows: u16,
706 pub(crate) damage: SmallVec<(u16, u16), 4>,
707}
708
709/// Produces the animated transcript, work indicator, editor, and status
710/// line demo.
711pub struct Demo {
712 started_at: Instant,
713 /// Detected presentation context shared by every retained subtree.
714 ctx: UiContext,
715 /// Cancel hint resolved once through the context's charset.
716 cancel_hint: &'static str,
717 editor_ui: Ui,
718 editor: Rc<RefCell<Editor>>,
719 edit_outcome: Rc<RefCell<Option<EditOutcome>>>,
720 work: Rc<RefCell<WorkState>>,
721 last_working: bool,
722 model: Rc<RefCell<Str>>,
723 /// Images staged on the composer, previewed above the status line.
724 attachments: Attachments,
725 /// Append-only transcript log, replayed in full on geometry rebuilds.
726 transcript: Vec<Entry>,
727 /// Entries already painted into the retained frame.
728 drawn_entries: usize,
729 /// Rows covered by the drawn entries; doubles as `stable_rows`.
730 transcript_rows: u16,
731 appended_messages: usize,
732 emitted_shards: u16,
733 last_viewport: Size,
734 height_floor: u16,
735 frame: Frame,
736 /// Geometry of the retained live panel chrome.
737 live_panel: Option<Rect>,
738 /// Reusable text and placement state for the animated shard rows.
739 live_rows: [LiveRowCache; LIVE_SHARD_ROWS as usize],
740 /// One build buffer rotated through the row caches without reallocating.
741 live_label_scratch: StrMut,
742 /// Columns reserved at the right edge for a composited rail; the
743 /// editor and title dock against the remaining visible width.
744 right_inset: u16,
745 /// The composer submitted `/switch`; the host opens the model picker.
746 switch_requested: bool,
747}
748
749impl Demo {
750 /// Starts the demo's animation clock, presenting through the host's
751 /// detected context.
752 pub fn new(ctx: &UiContext) -> Self {
753 let editor = Rc::new(RefCell::new({
754 let mut editor = Editor::new(EditorOptions::default());
755 editor.set_completion(Box::new(SlashCommands::new(demo_commands())));
756 editor
757 }));
758 let edit_outcome = Rc::new(RefCell::new(None));
759 let work = Rc::new(RefCell::new(WorkState {
760 working: true,
761 since: Duration::ZERO,
762 fade: Tween::settled(GREEN),
763 }));
764 let model = Rc::new(RefCell::new(Str::new_static("Fable 5++")));
765 let pane = EditorPane::new()
766 .input(DemoInput::new(Rc::clone(&editor), Rc::clone(&edit_outcome)))
767 .status(DemoStatus::new(Rc::clone(&work), Rc::clone(&model), ctx.charset));
768 let attachments = pane.attachments();
769 let editor_ui = Ui::from_root(pane, 0, ctx.clone());
770 Self {
771 started_at: Instant::now(),
772 ctx: ctx.clone(),
773 cancel_hint: ctx.charset.icon(Icon::Cancellable),
774 editor_ui,
775 editor,
776 edit_outcome,
777 work,
778 last_working: true,
779 model,
780 attachments,
781 transcript: vec![Entry::Command],
782 drawn_entries: 0,
783 transcript_rows: 0,
784 appended_messages: 0,
785 emitted_shards: 0,
786 last_viewport: Size::new(0, 0),
787 height_floor: 0,
788 frame: Frame::new(Size::new(0, 0)),
789 live_panel: None,
790 live_rows: std::array::from_fn(|_| LiveRowCache::new()),
791 live_label_scratch: StrMut::with_capacity(40),
792 right_inset: 0,
793 switch_requested: false,
794 }
795 }
796
797 /// Routes a key through the editor and reports whether the demo should
798 /// exit. Quit policy lives here, not in the editor: once the editor
799 /// reports a key unused, `esc` first cancels running work and only quits
800 /// at rest; `ctrl-c` always quits.
801 pub fn handle_key(&mut self, key: Key) -> bool {
802 *self.edit_outcome.borrow_mut() = None;
803 let _ = self.editor_ui.handle_key(key);
804 let outcome = self
805 .edit_outcome
806 .borrow_mut()
807 .take()
808 .unwrap_or(EditOutcome::Ignored);
809 match outcome {
810 EditOutcome::Submitted(text) => {
811 let trimmed = text.trim();
812 if trimmed == "/switch" {
813 self.switch_requested = true;
814 return false;
815 }
816 if let Some(path) = trimmed
817 .strip_prefix("/attach")
818 .filter(|rest| rest.is_empty() || rest.starts_with(' '))
819 {
820 let path = path.trim().to_string();
821 if !path.is_empty() {
822 self.attach_image(&path);
823 }
824 return false;
825 }
826 let _ = self.attachments.take();
827 self.refresh_composer();
828 self
829 .transcript
830 .push(Entry::Submitted(Box::new(Submission::new(
831 text,
832 Self::message_width(self.last_viewport.width),
833 &self.ctx,
834 ))));
835 self.set_working(true, self.started_at.elapsed());
836 false
837 },
838 EditOutcome::Changed => {
839 self.reconcile_attachments();
840 false
841 },
842 EditOutcome::Ignored => {
843 if key == Key::Ctrl('c') {
844 return true;
845 }
846 if key != Key::Esc {
847 return false;
848 }
849 if self.work.borrow().working {
850 self.set_working(false, self.started_at.elapsed());
851 return false;
852 }
853 true
854 },
855 }
856 }
857
858 /// Consumes a pending `/switch` request submitted through the composer.
859 pub fn take_switch_request(&mut self) -> bool {
860 std::mem::take(&mut self.switch_requested)
861 }
862
863 /// Routes a document-space mouse report into the editor UI.
864 pub fn handle_mouse(&mut self, report: &MouseReport) {
865 let editor_height = self.editor_ui.height();
866 let editor_y = self.frame.size().height.saturating_sub(editor_height);
867 let editor_bottom = editor_y.saturating_add(editor_height);
868 if report.row < editor_y || report.row >= editor_bottom {
869 return;
870 }
871 let _ = self
872 .editor_ui
873 .handle_mouse(report.col, report.row - editor_y, report.kind);
874 }
875
876 /// Switches the work state and retargets the brand fade. The status bar
877 /// repaints immediately and the fade departs from whatever color is on
878 /// screen, so rapid cancel/resume never snaps.
879 fn set_working(&mut self, working: bool, now: Duration) {
880 {
881 let mut work = self.work.borrow_mut();
882 if work.working == working {
883 return;
884 }
885 work.working = working;
886 work.since = now;
887 let target = if working { GREEN } else { MUTED };
888 work
889 .fade
890 .retarget(now, target, BRAND_FADE, Easing::EaseInOut);
891 }
892 self.editor_ui.invalidate(STATUS_ID);
893 }
894
895 /// Reflects a session model switch in the status bar's model segment.
896 pub fn set_model(&mut self, name: &str) {
897 *self.model.borrow_mut() = Str::from(name);
898 self.editor_ui.invalidate(STATUS_ID);
899 }
900
901 /// Routes sanitized bracketed paste text through the editor. Dropped
902 /// paths to existing image files (quoted, escaped, `file://`, or
903 /// multi-file) and any large paste collapse into composer attachment
904 /// chips instead of raw text.
905 pub fn handle_paste(&mut self, text: &str) {
906 let paths = omp_tui::paste::dropped_paths(text);
907 if !paths.is_empty()
908 && paths.iter().all(|path| {
909 omp_tui::paste::is_image_path(path) && std::path::Path::new(path.as_str()).is_file()
910 }) {
911 for path in &paths {
912 self.attach_image(path);
913 }
914 return;
915 }
916 if text.lines().count() > 10 || text.len() > 1000 {
917 self.attach_paste(text);
918 return;
919 }
920 let _ = self.editor_ui.handle_paste(text);
921 }
922
923 /// Routes Ctrl+Shift+V clipboard text into the composer verbatim: no
924 /// attachment staging, no large-paste collapse — the text stays inline
925 /// and editable.
926 pub fn handle_paste_raw(&mut self, text: &str) {
927 let _ = self.editor_ui.handle_paste_raw(text);
928 }
929
930 /// Stages `path` on the composer and mentions it in the prompt as an
931 /// atomic `<icon> #N` chip expanding to `<ref image=N/>` on submit.
932 fn attach_image(&mut self, path: &str) {
933 let attachment = self.attachments.push_image(path);
934 let payload = format!("<ref image={}/>", attachment.marker);
935 self.insert_chip(&attachment, &payload);
936 }
937
938 /// Collapses a large paste into a staged attachment card and an atomic
939 /// composer chip expanding back to the pasted text on submit.
940 fn attach_paste(&mut self, text: &str) {
941 let attachment = self.attachments.push_text(text);
942 self.insert_chip(&attachment, text);
943 }
944
945 /// Inserts one attachment chip as an atomic editor reference.
946 fn insert_chip(&mut self, attachment: &Attachment, payload: &str) {
947 let chip = chip_label(attachment, self.ctx.charset);
948 {
949 let mut editor = self.editor.borrow_mut();
950 let _ = editor.insert_reference(&chip, payload);
951 let _ = editor.insert_text(" ");
952 }
953 self.refresh_composer();
954 }
955
956 /// Hides staged attachments whose chip the user deleted from the
957 /// composer (and re-shows them after an undo). Presence is derived
958 /// from the buffer's atomic ranges, never from text matching.
959 fn reconcile_attachments(&mut self) {
960 let charset = self.ctx.charset;
961 let changed = {
962 let editor = self.editor.borrow();
963 let text = editor.text();
964 let ranges = editor.atom_ranges();
965 self.attachments.set_visible(|attachment| {
966 let chip = chip_label(attachment, charset);
967 ranges
968 .iter()
969 .any(|&(start, end)| text.get(start..end) == Some(chip.as_str()))
970 })
971 };
972 if changed {
973 self.refresh_composer();
974 }
975 }
976
977 /// Relayouts the composer after out-of-band state changed its height.
978 fn refresh_composer(&mut self) {
979 let width = self.editor_ui.frame().size().width;
980 if width > 0 {
981 self.editor_ui.resize(width);
982 }
983 }
984
985 /// Reserves `cols` at the right edge for a composited rail, so the
986 /// composer's right-docked chrome stays visible beside it. The next
987 /// render relayouts the editor at the narrowed width.
988 pub const fn set_right_inset(&mut self, cols: u16) {
989 self.right_inset = cols;
990 }
991
992 /// The width the composer may actually occupy at `viewport`.
993 fn composer_width(&self, viewport: Size) -> u16 {
994 viewport.width.saturating_sub(self.right_inset).max(1)
995 }
996
997 /// Updates the retained logical document and reports its repainted rows.
998 pub fn render(&mut self, viewport: Size) -> RenderedFrame<'_> {
999 self.render_at(viewport, self.started_at.elapsed())
1000 }
1001
1002 fn render_at(&mut self, viewport: Size, elapsed: Duration) -> RenderedFrame<'_> {
1003 if viewport.width == 0 || viewport.height == 0 {
1004 self.last_viewport = viewport;
1005 self.height_floor = 0;
1006 self.drawn_entries = 0;
1007 self.transcript_rows = 0;
1008 self.live_panel = None;
1009 self.frame = Frame::new(viewport);
1010 return RenderedFrame {
1011 frame: &self.frame,
1012 stable_rows: 0,
1013 damage: SmallVec::new(),
1014 };
1015 }
1016 let composer_width = self.composer_width(viewport);
1017 if self.editor_ui.frame().size().width != composer_width {
1018 self.editor_ui.resize(composer_width);
1019 }
1020 // Fires due animation wakes (the status bar's spinner and brand
1021 // fade) so the blit below picks up fresh retained pixels.
1022 self.editor_ui.tick(elapsed);
1023 let editor_changed = self.editor_ui.take_frame_damage();
1024
1025 // A viewport change starts a fresh renderer session: replay the
1026 // whole transcript log at the new width. Between rebuilds the log
1027 // is append-only and every drawn row is final, so selections over
1028 // transcript text stay anchored to it in every terminal.
1029 let rebuild = self.last_viewport != viewport;
1030 if rebuild {
1031 self.last_viewport = viewport;
1032 self.height_floor = 0;
1033 self.drawn_entries = 0;
1034 self.transcript_rows = 0;
1035 let message_width = Self::message_width(viewport.width);
1036 for entry in &mut self.transcript {
1037 if let Entry::Submitted(submission) = entry {
1038 submission.resize(message_width, &self.ctx);
1039 }
1040 }
1041 }
1042 while self.appended_messages < Self::visible_messages(elapsed) {
1043 self.transcript.push(Entry::Message(self.appended_messages));
1044 self.appended_messages += 1;
1045 }
1046 while self.emitted_shards < Self::finished_shards(elapsed) {
1047 self.emitted_shards += 1;
1048 self.transcript.push(Entry::ShardDone(self.emitted_shards));
1049 }
1050
1051 let mut new_rows = 0_u16;
1052 for entry in &self.transcript[self.drawn_entries..] {
1053 new_rows = new_rows.saturating_add(Self::entry_height(entry, viewport.width, &self.ctx));
1054 }
1055 let transcript_rows = self.transcript_rows.saturating_add(new_rows);
1056 let editor_height = self.editor_ui.height();
1057 // Native scrollback is append-only, so the logical document may
1058 // never shrink while the seam is live: band rows that close again
1059 // (extra input lines) become blank padding that heals as the
1060 // transcript grows.
1061 let natural_height = transcript_rows.saturating_add(Self::band_height(editor_height));
1062 self.height_floor = self.height_floor.max(natural_height);
1063 let document_height = self.height_floor.max(viewport.height);
1064 let transcript_damage_start = if rebuild { 0 } else { self.transcript_rows };
1065 let margin = u16::from(viewport.width >= 50);
1066 let content_width = viewport.width.saturating_sub(margin * 2);
1067 let editor_y = document_height.saturating_sub(editor_height);
1068 let title_y = editor_y.saturating_sub(1);
1069 let working_y = title_y.saturating_sub(1);
1070 let panel_height = LIVE_SHARD_ROWS + 2;
1071 let panel_y = working_y.saturating_sub(1).saturating_sub(panel_height);
1072 let panel = Rect::new(margin, panel_y, content_width, panel_height);
1073 let repaint_suffix = rebuild || new_rows > 0 || self.live_panel != Some(panel);
1074 if rebuild {
1075 self.frame = Frame::new(Size::new(viewport.width, document_height));
1076 } else {
1077 self.frame.resize_height(document_height, base_style());
1078 }
1079 if repaint_suffix {
1080 self.frame.fill(
1081 Rect::new(
1082 0,
1083 transcript_damage_start,
1084 viewport.width,
1085 document_height.saturating_sub(transcript_damage_start),
1086 ),
1087 base_style(),
1088 );
1089 }
1090
1091 // Paint the new transcript entries; rows above `transcript_rows`
1092 // are final and never repainted.
1093 let mut y = self.transcript_rows;
1094 for index in self.drawn_entries..self.transcript.len() {
1095 let used = self.draw_entry_at(index, y, viewport.width);
1096 y = y.saturating_add(used);
1097 }
1098 self.drawn_entries = self.transcript.len();
1099 self.transcript_rows = y;
1100
1101 // The live band repaints in place at the bottom of the document.
1102 let animation_frame = Self::animation_frame(elapsed);
1103 let panel_changed = draw_live_panel(
1104 &mut self.frame,
1105 &mut self.live_rows,
1106 &mut self.live_label_scratch,
1107 panel,
1108 repaint_suffix,
1109 self.emitted_shards,
1110 animation_frame,
1111 self.ctx.charset,
1112 );
1113 let working = self.work.borrow().working;
1114 let working_changed = self.last_working != working;
1115 if !repaint_suffix && self.last_working && !working {
1116 self
1117 .frame
1118 .fill(Rect::new(0, working_y, viewport.width, 1), base_style());
1119 }
1120 if working {
1121 Self::draw_working(&mut self.frame, working_y, elapsed, self.cancel_hint);
1122 }
1123 Self::draw_session_title(&mut self.frame, title_y, self.right_inset);
1124 if repaint_suffix || editor_changed {
1125 self
1126 .frame
1127 .blit(self.editor_ui.frame(), 0, editor_height, 0, editor_y);
1128 }
1129 let mut damage = SmallVec::new();
1130 if repaint_suffix {
1131 damage.push((transcript_damage_start, document_height));
1132 } else {
1133 if panel_changed {
1134 damage.push((panel_y, panel_y.saturating_add(panel_height)));
1135 }
1136 if working || working_changed {
1137 damage.push((working_y, working_y.saturating_add(1)));
1138 }
1139 if editor_changed {
1140 damage.push((editor_y, document_height));
1141 }
1142 }
1143 self.last_working = working;
1144 self.live_panel = Some(panel);
1145
1146 RenderedFrame { frame: &self.frame, stable_rows: self.transcript_rows, damage }
1147 }
1148
1149 fn generation(elapsed: Duration) -> u64 {
1150 u64::try_from(elapsed.as_millis() / EMIT_INTERVAL.as_millis()).unwrap_or(u64::MAX)
1151 }
1152
1153 fn animation_frame(elapsed: Duration) -> u64 {
1154 u64::try_from(elapsed.as_millis() / 80).unwrap_or(u64::MAX)
1155 }
1156
1157 fn visible_messages(elapsed: Duration) -> usize {
1158 let interval = MESSAGE_INTERVAL.as_millis();
1159 usize::try_from(elapsed.as_millis() / interval + 1)
1160 .unwrap_or(usize::MAX)
1161 .min(4)
1162 }
1163
1164 /// Shards whose permanent result line has been appended by `elapsed`:
1165 /// two per emit tick, capped well inside `u16` document heights.
1166 fn finished_shards(elapsed: Duration) -> u16 {
1167 u16::try_from(Self::generation(elapsed).saturating_mul(2).min(60_000))
1168 .expect("finished shard count is clamped")
1169 }
1170
1171 /// Rows the bottom live band occupies: the shard panel, a blank
1172 /// separator, the activity row, the title air row, and the editor
1173 /// block.
1174 const fn band_height(editor_height: u16) -> u16 {
1175 LIVE_SHARD_ROWS + 2 + 3 + editor_height
1176 }
1177
1178 /// Rows `entry` will occupy at `width`, including its trailing blank.
1179 fn entry_height(entry: &Entry, width: u16, ctx: &UiContext) -> u16 {
1180 match entry {
1181 Entry::Command => 5,
1182 Entry::Message(message) => {
1183 let mut scratch = Frame::new(Size::new(width, 48));
1184 Self::draw_message(&mut scratch, 0, *message, width, ctx.charset)
1185 },
1186 Entry::ShardDone(_) => 1,
1187 Entry::Submitted(submission) => submission.height().saturating_add(1),
1188 }
1189 }
1190
1191 const fn message_width(width: u16) -> u16 {
1192 let narrowed = width.saturating_sub(3);
1193 if narrowed == 0 { 1 } else { narrowed }
1194 }
1195
1196 /// Paints one transcript entry at `y` and returns the rows it used.
1197 fn draw_entry_at(&mut self, index: usize, y: u16, width: u16) -> u16 {
1198 Self::draw_entry(&mut self.frame, &self.transcript[index], y, width, &self.ctx)
1199 }
1200
1201 /// Paints `entry` into any frame at `y` and returns the rows it used,
1202 /// including the trailing blank.
1203 fn draw_entry(frame: &mut Frame, entry: &Entry, y: u16, width: u16, ctx: &UiContext) -> u16 {
1204 let margin = u16::from(width >= 50);
1205 let content_width = width.saturating_sub(margin * 2);
1206 match entry {
1207 Entry::Command => {
1208 draw_command_box(frame, Rect::new(margin, y, content_width, 4), ctx.charset);
1209 5
1210 },
1211 Entry::Message(message) => Self::draw_message(frame, y, *message, width, ctx.charset),
1212 Entry::ShardDone(shard) => {
1213 Self::draw_shard_done(frame, y, *shard, width, ctx.charset);
1214 1
1215 },
1216 Entry::Submitted(submission) => {
1217 draw_submission(frame, y, submission, ctx.charset);
1218 submission.height().saturating_add(1)
1219 },
1220 }
1221 }
1222
1223 /// Composes exactly one viewport of throwaway resize-drag content at the
1224 /// new geometry: the live band anchors to the bottom, then transcript
1225 /// entries are walked backward and rewrapped at `viewport.width` until
1226 /// the screen is full — O(viewport) work per drag frame, with the
1227 /// topmost entry sliced when it only partially fits. Retained transcript
1228 /// state is untouched, so the settle rebuild replays full history
1229 /// exactly once.
1230 pub fn render_resize_preview(&mut self, viewport: Size) -> Frame {
1231 let elapsed = self.started_at.elapsed();
1232 let mut frame = Frame::new(viewport);
1233 if viewport.width == 0 || viewport.height == 0 {
1234 return frame;
1235 }
1236 frame.fill(Rect::new(0, 0, viewport.width, viewport.height), base_style());
1237 let composer_width = self.composer_width(viewport);
1238 if self.editor_ui.frame().size().width != composer_width {
1239 self.editor_ui.resize(composer_width);
1240 }
1241 self.editor_ui.tick(elapsed);
1242
1243 // The live band, laid out exactly like the retained document's.
1244 let margin = u16::from(viewport.width >= 50);
1245 let content_width = viewport.width.saturating_sub(margin * 2);
1246 let editor_height = self.editor_ui.height();
1247 let editor_y = viewport.height.saturating_sub(editor_height);
1248 let title_y = editor_y.saturating_sub(1);
1249 let working_y = title_y.saturating_sub(1);
1250 let panel_height = LIVE_SHARD_ROWS + 2;
1251 let panel_y = working_y.saturating_sub(1).saturating_sub(panel_height);
1252 draw_live_panel(
1253 &mut frame,
1254 &mut self.live_rows,
1255 &mut self.live_label_scratch,
1256 Rect::new(margin, panel_y, content_width, panel_height),
1257 true,
1258 self.emitted_shards,
1259 Self::animation_frame(elapsed),
1260 self.ctx.charset,
1261 );
1262 if self.work.borrow().working {
1263 Self::draw_working(&mut frame, working_y, elapsed, self.cancel_hint);
1264 }
1265 Self::draw_session_title(&mut frame, title_y, self.right_inset);
1266 frame.blit(self.editor_ui.frame(), 0, editor_height, 0, editor_y);
1267
1268 // Transcript tail, bottom-up above the band.
1269 let mut remaining = panel_y;
1270 for entry in self.transcript.iter().rev() {
1271 if remaining == 0 {
1272 break;
1273 }
1274 let height = Self::entry_height(entry, viewport.width, &self.ctx);
1275 if height == 0 {
1276 continue;
1277 }
1278 if height <= remaining {
1279 remaining -= height;
1280 Self::draw_entry(&mut frame, entry, remaining, viewport.width, &self.ctx);
1281 } else {
1282 // Slice the bottom rows of the partially visible entry.
1283 let mut scratch = Frame::new(Size::new(viewport.width, height));
1284 scratch.fill(Rect::new(0, 0, viewport.width, height), base_style());
1285 Self::draw_entry(&mut scratch, entry, 0, viewport.width, &self.ctx);
1286 frame.blit(&scratch, height - remaining, remaining, 0, 0);
1287 remaining = 0;
1288 }
1289 }
1290 frame
1291 }
1292
1293 /// Paints the n-th scripted message and returns rows used including
1294 /// the trailing blank. Measurement draws into a scratch frame.
1295 fn draw_message(frame: &mut Frame, y: u16, message: usize, width: u16, charset: Charset) -> u16 {
1296 let margin = u16::from(width >= 50);
1297 let content_width = width.saturating_sub(margin * 2);
1298 if message == 2 {
1299 draw_edit_box(frame, Rect::new(margin, y, content_width, EDIT_BOX_HEIGHT), charset);
1300 return EDIT_BOX_HEIGHT + 1;
1301 }
1302 let bottom = frame.size().height;
1303 let spans = Self::message_spans(message);
1304 // Prose flows edge-to-edge grapheme-exact — no side pads — so every
1305 // wrapped row re-joins byte-for-byte in native selection.
1306 let used = draw_flowed(frame, Rect::new(0, y, width, bottom.saturating_sub(y)), &spans);
1307 used.saturating_add(1)
1308 }
1309
1310 fn message_spans(message: usize) -> SmallVec<Span<'static>, 3> {
1311 let mut spans = SmallVec::new();
1312 match message {
1313 0 => {
1314 spans.push(Span::new("Transcript rows are ", prose_style()));
1315 spans.push(Span::new("append-only", code_style()));
1316 spans.push(Span::new(
1317 ": every line is painted once, becomes stable, and rides into native scrollback \
1318 with any selection anchored to it.",
1319 prose_style(),
1320 ));
1321 },
1322 1 => {
1323 spans.push(Span::new(
1324 "Only the bottom band repaints in place — the live shard panel, the activity \
1325 shimmer, and the composer. Rows above it are never rewritten.",
1326 prose_style(),
1327 ));
1328 },
1329 _ => {
1330 spans.push(Span::new(
1331 "On terminals that move margin-scrolled rows into scrollback, commits scroll only \
1332 the stable transcript through a ",
1333 prose_style(),
1334 ));
1335 spans.push(Span::new("DECSTBM top region", code_style()));
1336 spans.push(Span::new(", so the live band never shifts on screen.", prose_style()));
1337 },
1338 }
1339 spans
1340 }
1341
1342 /// Appends a finished shard's permanent one-line result.
1343 fn draw_shard_done(frame: &mut Frame, y: u16, shard: u16, width: u16, charset: Charset) {
1344 let margin = u16::from(width >= 50);
1345 let prefix = fmts!(" {} shard {shard:03} passed", charset.check());
1346 let detail = fmts!(" workspace-{shard:03}.test.ts [100%]");
1347 draw_line(frame, margin + 1, y, width.saturating_sub(margin * 2).saturating_sub(2), &[
1348 Span::new(prefix.as_str(), ink(GREEN)),
1349 Span::new(detail.as_str(), ink(MUTED)),
1350 ]);
1351 }
1352
1353 /// Shimmering activity line above the editor. The spinner and timer
1354 /// live in the status bar's brand segment; this row only narrates.
1355 fn draw_working(frame: &mut Frame, y: u16, elapsed: Duration, hint: &str) {
1356 if y >= frame.size().height || frame.size().width < 4 {
1357 return;
1358 }
1359 let start = u16::from(frame.size().width >= 50);
1360 let mut column = start;
1361 let length = xutf::graphemes_str(WORKING_MESSAGE)
1362 .count()
1363 .saturating_add(xutf::graphemes_str(hint).count())
1364 .saturating_add(1);
1365 let length = u16::try_from(length).unwrap_or(u16::MAX);
1366 let shimmer = Shimmer::new(elapsed, SHIMMER_PERIOD, length);
1367 let right = frame.size().width.saturating_sub(1);
1368 draw_shimmer(frame, &mut column, start, y, right, hint, shimmer, ink(CYAN));
1369 draw_shimmer(frame, &mut column, start, y, right, " ", shimmer, ink(GREEN));
1370 draw_shimmer(frame, &mut column, start, y, right, WORKING_MESSAGE, shimmer, ink(GREEN));
1371 }
1372
1373 /// The session title resting right-aligned in the air row between
1374 /// the working narration and the status band — against the visible
1375 /// right bound, inside any rail reservation — so the gap reads as
1376 /// session identity instead of dead space.
1377 fn draw_session_title(frame: &mut Frame, y: u16, right_inset: u16) {
1378 let width = frame.size().width.saturating_sub(right_inset);
1379 let title_width = visible_width(SESSION_TITLE);
1380 if y >= frame.size().height || width < title_width.saturating_add(2) {
1381 return;
1382 }
1383 let x = width.saturating_sub(title_width.saturating_add(1));
1384 draw_line(frame, x, y, title_width, &[Span::new(SESSION_TITLE, ink(FAINT).italic())]);
1385 }
1386}
1387
1388/// The closed four-row command box that opens the transcript.
1389fn draw_command_box(frame: &mut Frame, rect: Rect, charset: Charset) {
1390 draw_box(frame, rect, ink(FAINT), panel_style(), charset);
1391 if rect.width < 4 || rect.height < 4 {
1392 return;
1393 }
1394
1395 let content_x = rect.x + 2;
1396 let content_width = rect.width.saturating_sub(4);
1397 let header = [
1398 Span::new(" PARALLEL TEST RUN ", panel_ink(GREEN).bold()),
1399 Span::new("results append below · live rows in the bottom panel", panel_ink(MUTED)),
1400 ];
1401 draw_line(frame, content_x, rect.y + 1, content_width, &header);
1402 let command = [
1403 Span::new("$ ", panel_ink(MUTED)),
1404 Span::new("bun test --parallel=8", panel_ink(CYAN)),
1405 Span::new(" --timeout=30000 --all-workspaces", panel_ink(TEXT)),
1406 ];
1407 draw_line(frame, content_x, rect.y + 2, content_width, &command);
1408}
1409
1410/// The live band's shard panel: twelve mutable rows that repaint in place
1411/// every frame and never enter native scrollback.
1412fn draw_live_panel(
1413 frame: &mut Frame,
1414 rows: &mut [LiveRowCache; LIVE_SHARD_ROWS as usize],
1415 label_scratch: &mut StrMut,
1416 rect: Rect,
1417 repaint_chrome: bool,
1418 emitted_shards: u16,
1419 animation_frame: u64,
1420 charset: Charset,
1421) -> bool {
1422 let mut changed = repaint_chrome;
1423 if repaint_chrome {
1424 draw_box(frame, rect, ink(FAINT), panel_style(), charset);
1425 }
1426 if rect.width < 4 || rect.height < 3 {
1427 return changed;
1428 }
1429
1430 if repaint_chrome {
1431 let title = [
1432 Span::new(" LIVE SHARDS ", panel_ink(GREEN).bold()),
1433 Span::new("mutable rows repaint in place ", panel_ink(MUTED)),
1434 ];
1435 draw_line(frame, rect.x + 2, rect.y, rect.width.saturating_sub(4), &title);
1436 }
1437 let content_x = rect.x + 2;
1438 let content_width = rect.width.saturating_sub(4);
1439 for row in 0..rect.height.saturating_sub(2) {
1440 let shard = emitted_shards.saturating_add(row).saturating_add(1);
1441 let phase = (u64::from(row) + animation_frame) % 11;
1442 let (prefix_phase, symbol, state, state_style, progress) = match phase {
1443 0 => (
1444 0,
1445 "⠼",
1446 "running",
1447 panel_ink(GREEN).bold(),
1448 (u64::from(row) * 17 + animation_frame * 7) % 100,
1449 ),
1450 1..=7 => {
1451 (1, "·", "working", panel_ink(MUTED), (u64::from(row) * 17 + animation_frame * 7) % 100)
1452 },
1453 _ => (2, "·", "queued ", panel_ink(FAINT), 0),
1454 };
1455 let row_y = rect.y + 1 + row;
1456 let right = content_x
1457 .saturating_add(content_width)
1458 .min(frame.size().width);
1459 let cache = &mut rows[usize::from(row)];
1460 let prefix_changed = repaint_chrome
1461 || !cache.prefix_valid
1462 || cache.prefix_shard != shard
1463 || cache.prefix_phase != prefix_phase;
1464 changed |= prefix_changed;
1465 let label_x = if prefix_changed {
1466 let prefix = fmts!(" {symbol} shard {shard:03} ");
1467 let prefix_width = prefix.len().saturating_sub(symbol.len()).saturating_add(1);
1468 let next_x = content_x
1469 .saturating_add(u16::try_from(prefix_width).unwrap_or(u16::MAX))
1470 .saturating_add(u16::try_from(state.len()).unwrap_or(u16::MAX))
1471 .saturating_add(2)
1472 .min(right);
1473 if !repaint_chrome && cache.label_x != next_x {
1474 clear_cached_label(frame, cache, row_y, right);
1475 }
1476 let next_x = draw_line(frame, content_x, row_y, content_width, &[
1477 Span::new(prefix.as_str(), state_style),
1478 Span::new(state, state_style),
1479 Span::new(" ", panel_ink(FAINT)),
1480 ]);
1481 cache.prefix_shard = shard;
1482 cache.prefix_phase = prefix_phase;
1483 cache.prefix_valid = true;
1484 next_x
1485 } else {
1486 cache.label_x
1487 };
1488 let moved = cache.label_x != label_x;
1489 let label_changed = repaint_chrome
1490 || moved
1491 || !cache.label_valid
1492 || cache.label_shard != shard
1493 || cache.label_progress != progress;
1494 changed |= label_changed;
1495 if label_changed {
1496 label_scratch.truncate(0);
1497 write!(label_scratch, "workspace-{shard:03}.test.ts [{progress:>3}%]")
1498 .expect("shard label formatting is infallible");
1499 let resized = cache.label.len() != label_scratch.len();
1500 if !repaint_chrome && resized && !moved {
1501 clear_cached_label(frame, cache, row_y, right);
1502 }
1503 let width = right.saturating_sub(label_x);
1504 if repaint_chrome || moved || resized {
1505 frame.put_clipped(label_x, row_y, width, label_scratch.as_str(), panel_ink(MUTED));
1506 } else {
1507 draw_ascii_changes(
1508 frame,
1509 label_x,
1510 row_y,
1511 width,
1512 cache.label.as_str(),
1513 label_scratch.as_str(),
1514 panel_ink(MUTED),
1515 );
1516 }
1517 std::mem::swap(&mut cache.label, label_scratch);
1518 cache.label_shard = shard;
1519 cache.label_progress = progress;
1520 cache.label_valid = true;
1521 }
1522 cache.label_x = label_x;
1523 }
1524 changed
1525}
1526
1527fn clear_cached_label(frame: &mut Frame, cache: &LiveRowCache, y: u16, right: u16) {
1528 if cache.label.is_empty() {
1529 return;
1530 }
1531 let width = u16::try_from(cache.label.len())
1532 .unwrap_or(u16::MAX)
1533 .min(right.saturating_sub(cache.label_x));
1534 frame.fill(Rect::new(cache.label_x, y, width, 1), panel_style());
1535}examples/footers.rs (line 118)
68async fn run<'a>(
69 terminal: &'a mut Terminal,
70 renderer: &'a mut Renderer<TtyOut>,
71 charset: Charset,
72) -> io::Result<()> {
73 let started = Instant::now();
74 let mut viewport = terminal.size()?;
75 let mut scroll: u16 = 0;
76 let mut alt_enter = terminal.stage_alt_enter(AltScreenUse::Interactive);
77 loop {
78 tokio::select! {
79 event = terminal.next() => match event? {
80 TerminalEvent::Input(event) => {
81 match event {
82 InputEvent::Key(key) => match key {
83 Key::Char('q') | Key::Esc | Key::Ctrl('c') => return Ok(()),
84 Key::Up | Key::Char('k') => scroll = scroll.saturating_sub(1),
85 Key::Down | Key::Char('j') => scroll = scroll.saturating_add(1),
86 Key::PageUp => scroll = scroll.saturating_sub(viewport.height),
87 Key::PageDown => scroll = scroll.saturating_add(viewport.height),
88 Key::Home => scroll = 0,
89 Key::End => scroll = u16::MAX,
90 _ => {},
91 },
92 InputEvent::Mouse(report) => match report.kind {
93 Mouse::WheelUp => scroll = scroll.saturating_sub(2),
94 Mouse::WheelDown => scroll = scroll.saturating_add(2),
95 _ => {},
96 },
97 InputEvent::Paste(_) | InputEvent::Focus(_) | InputEvent::Response(_) => {},
98 }
99 terminal.sync_renderer(renderer)?;
100 },
101 TerminalEvent::Resize => {
102 if let Some(size) = terminal.take_resize()? {
103 viewport = size;
104 }
105 },
106 TerminalEvent::Debug(_) => {},
107 TerminalEvent::Closed => return Ok(()),
108 },
109 () = tokio::time::sleep(FRAME_INTERVAL) => {},
110 }
111 if viewport.width == 0 || viewport.height == 0 {
112 continue;
113 }
114 let scene = Scene { charset, width: viewport.width, elapsed: started.elapsed() };
115 let document = compose(&scene);
116 scroll = scroll.min(document.size().height.saturating_sub(viewport.height));
117 let mut screen = Frame::new(viewport);
118 screen.fill(Rect::new(0, 0, viewport.width, viewport.height), ink(TEXT));
119 screen.blit(&document, scroll, viewport.height, 0, 0);
120 renderer.preview(&screen, viewport.height, alt_enter.take().as_deref().unwrap_or(""))?;
121 }
122}
123
124/// Per-frame paint inputs shared by every study.
125struct Scene {
126 charset: Charset,
127 width: u16,
128 elapsed: Duration,
129}
130
131impl Scene {
132 const fn spinner(&self) -> &'static str {
133 self.charset.spinner().at(self.elapsed)
134 }
135
136 fn timer(&self) -> Str {
137 let seconds = self.elapsed.as_secs();
138 if seconds < 60 {
139 fmts!("{seconds}s")
140 } else {
141 fmts!("{}m", seconds / 60)
142 }
143 }
144
145 const fn right_edge(&self) -> u16 {
146 self.width.saturating_sub(1)
147 }
148}
149
150/// One study: a header line and a `rows`-tall live mock beneath it.
151struct Study {
152 title: &'static str,
153 note: &'static str,
154 rows: u16,
155 draw: fn(&mut Frame, u16, &Scene),
156}
157
158const STUDIES: [Study; 6] = [
159 Study {
160 title: "pi parity",
161 note: "border carries the band left and the title right; the intent rides its own spinner \
162 row",
163 rows: 4,
164 draw: study_pi_parity,
165 },
166 Study {
167 title: "gap title",
168 note: "the air row earns its keep — session title idles right-aligned in the gap",
169 rows: 4,
170 draw: study_gap_title,
171 },
172 Study {
173 title: "band title",
174 note: "title as the left band's second segment; session facts dock right",
175 rows: 4,
176 draw: study_band_title,
177 },
178 Study {
179 title: "prompt title",
180 note: "split + air untouched; the title rests on the prompt row and yields while typing",
181 rows: 4,
182 draw: study_prompt_title,
183 },
184 Study {
185 title: "crown",
186 note: "title crowns the whole block; narration, gap, and split bands breathe below it",
187 rows: 5,
188 draw: study_crown,
189 },
190 Study {
191 title: "hem",
192 note: "title stitched into the top border, band into the bottom hem",
193 rows: 4,
194 draw: study_hem,
195 },
196];
197
198/// Renders the full document — title block plus every study — at width.
199fn compose(scene: &Scene) -> Frame {
200 let height = STUDIES
201 .iter()
202 .map(|study| study.rows + 3)
203 .fold(3_u16, u16::saturating_add);
204 let mut frame = Frame::new(Size::new(scene.width, height));
205 frame.fill(Rect::new(0, 0, scene.width, height), ink(TEXT));
206
207 let column = frame.put(1, 0, "composer footer studies", ink(TEXT).bold());
208 frame.put(
209 column.saturating_add(2),
210 0,
211 "split + air gap, six session-title placements",
212 ink(MUTED),
213 );
214 frame.put(1, 1, "↑/↓ scroll · PgUp/PgDn page · Home/End jump · q quits", ink(FAINT));
215
216 let mut y = 3_u16;
217 for (index, study) in STUDIES.iter().enumerate() {
218 let number = fmts!("{:>2} ", index + 1);
219 let mut column = frame.put(1, y, &number, ink(GOLD).bold());
220 column = frame.put(column, y, study.title, ink(TEXT).bold());
221 column = frame.put(column, y, " ", ink(FAINT));
222 frame.put(column, y, study.note, ink(MUTED));
223 (study.draw)(&mut frame, y + 1, scene);
224 y = y.saturating_add(study.rows + 3);
225 }
226 frame
227}Trait Implementations§
impl Copy for Rect
impl Eq for Rect
impl StructuralPartialEq for Rect
Auto Trait Implementations§
impl Freeze for Rect
impl RefUnwindSafe for Rect
impl Send for Rect
impl Sync for Rect
impl Unpin for Rect
impl UnsafeUnpin for Rect
impl UnwindSafe for Rect
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
Convert
Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can
then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.Source§fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
Convert
Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be
further downcast into Rc<ConcreteType> where ConcreteType implements Trait.Source§fn as_any(&self) -> &(dyn Any + 'static)
fn as_any(&self) -> &(dyn Any + 'static)
Convert
&Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &Any’s vtable from &Trait’s.Source§fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
Convert
&mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot
generate &mut Any’s vtable from &mut Trait’s.Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
Compare self to
key and return true if they are equal.Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
Converts
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more