Skip to main content

Rect

Struct Rect 

Source
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: u16

Leftmost column.

§y: u16

Topmost row.

§width: u16

Region width in cells.

§height: u16

Region height in cells.

Implementations§

Source§

impl Rect

Source

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
Hide additional 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§

Source§

impl Clone for Rect

Source§

fn clone(&self) -> Rect

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Copy for Rect

Source§

impl Debug for Rect

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Eq for Rect

Source§

impl PartialEq for Rect

Source§

fn eq(&self, other: &Rect) -> bool

Equality operator ==. Read more
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Inequality operator !=. Read more
Source§

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> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

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>

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)

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)

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
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<Q, K> Equivalent<K> for Q
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

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
where Q: Eq + ?Sized, K: Borrow<Q> + ?Sized,

Source§

fn equivalent(&self, key: &K) -> bool

Checks if this value is equivalent to the given key. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

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
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.