pub struct Style { /* private fields */ }Expand description
Canonical visual attributes for one or more cells.
Implementations§
Source§impl Style
impl Style
Sourcepub const fn new() -> Self
pub const fn new() -> Self
Creates an unstyled terminal style.
Examples found in repository?
398const fn ink(color: Color) -> Style {
399 Style::new().fg(color)
400}
401
402fn width_of(text: &str) -> u16 {
403 u16::try_from(xutf::width_str(text)).unwrap_or(u16::MAX)
404}
405
406/// [`TITLE`] truncated to at most `max` cells, ellipsized when it cannot
407/// fit whole.
408fn fit_title(scene: &Scene, max: u16) -> Str {
409 if width_of(TITLE) <= max {
410 return Str::new_static(TITLE);
411 }
412 let ellipsis = match scene.charset {
413 Charset::Ascii => "...",
414 _ => "…",
415 };
416 let budget = max.saturating_sub(width_of(ellipsis));
417 let mut used = 0_u16;
418 let mut end = 0_usize;
419 for grapheme in xutf::graphemes_str(TITLE) {
420 let cells = width_of(grapheme);
421 if used.saturating_add(cells) > budget {
422 break;
423 }
424 used = used.saturating_add(cells);
425 end += grapheme.len();
426 }
427 if end == 0 {
428 return Str::default();
429 }
430 fmts!("{}{ellipsis}", TITLE[..end].trim_end())
431}
432
433/// Total cells a powerline band with `segments` occupies, mirroring the
434/// `<status>` component's measurement.
435fn band_width(scene: &Scene, segments: &[Seg]) -> u16 {
436 let (left_cap, separator, right_cap) = band_chrome(scene.charset);
437 let text = segments
438 .iter()
439 .map(|segment| width_of(&segment.label))
440 .fold(0_u16, u16::saturating_add);
441 let separators = u16::try_from(segments.len().saturating_sub(1))
442 .unwrap_or(u16::MAX)
443 .saturating_mul(width_of(separator).saturating_add(2));
444 text
445 .saturating_add(separators)
446 .saturating_add(width_of(left_cap))
447 .saturating_add(2)
448 .saturating_add(width_of(right_cap))
449}
450
451/// Paints a powerline band at `x`: cap, padded segments, cap.
452fn draw_band(frame: &mut Frame, x: u16, y: u16, scene: &Scene, segments: &[Seg]) {
453 let (left_cap, separator, right_cap) = band_chrome(scene.charset);
454 let base = Style::new().fg(TEXT).bg(BAND_BG);
455 let edge = ink(BAND_BG);
456 let mut column = frame.put(x, y, left_cap, edge);
457 column = frame.put(column, y, " ", base);
458 for (index, segment) in segments.iter().enumerate() {
459 if index > 0 {
460 column = frame.put(column, y, " ", base.dim());
461 column = frame.put(column, y, separator, base.dim());
462 column = frame.put(column, y, " ", base.dim());
463 }
464 column = frame.put(column, y, &segment.label, base.fg(segment.color));
465 }
466 column = frame.put(column, y, " ", base);
467 frame.put(column, y, right_cap, edge);
468}More examples
1847fn draw_submission_text(
1848 frame: &mut Frame,
1849 x: u16,
1850 y: u16,
1851 width: u16,
1852 line: &str,
1853 charset: Charset,
1854) {
1855 let icon = charset.icon(Icon::Image);
1856 let mut chips: SmallVec<(usize, usize, String, usize), 4> = SmallVec::new();
1857 let mut base = 0;
1858 while let Some((start, end, marker)) = next_ref_tag(&line[base..]) {
1859 chips.push((base + start, base + end, format!("{icon} #{marker}"), marker));
1860 base += end;
1861 }
1862 let mut spans: SmallVec<Span<'_>, 8> = SmallVec::new();
1863 let mut at = 0;
1864 for (start, end, label, marker) in &chips {
1865 if *start > at {
1866 spans.push(Span::new(&line[at..*start], ink(TEXT)));
1867 }
1868 spans.push(Span::new(label, Style::new().fg(PANEL).bg(attachment_color(*marker)).bold()));
1869 at = *end;
1870 }
1871 if at < line.len() {
1872 spans.push(Span::new(&line[at..], ink(TEXT)));
1873 }
1874 draw_line(frame, x, y, width, &spans);
1875}
1876
1877/// Chrome outside a panel is transparent: no `bg`, so the terminal's own
1878/// background (and any image or blur behind it) shows through. Only the
1879/// panel boxes below opt into a fill.
1880const fn base_style() -> Style {
1881 Style::new().fg(TEXT)
1882}
1883
1884const fn panel_style() -> Style {
1885 Style::new().fg(TEXT).bg(PANEL)
1886}
1887
1888const fn ink(color: Color) -> Style {
1889 Style::new().fg(color)
1890}
1891
1892const fn panel_ink(color: Color) -> Style {
1893 Style::new().fg(color).bg(PANEL)
1894}
1895
1896const fn prose_style() -> Style {
1897 Style::new().fg(MUTED).italic()
1898}
1899
1900const fn code_style() -> Style {
1901 Style::new().fg(GREEN)
1902}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}
422
423fn draw_full_hints(frame: &mut Frame, left: u16, y: u16) {
424 frame.put(left + 3, y, "#", on_footer(CYAN));
425 frame.put(left + 5, y, "actions", on_footer(MUTED));
426 frame.put(left + 14, y, "/", on_footer(GREEN));
427 frame.put(left + 16, y, "commands", on_footer(MUTED));
428 frame.put(left + 27, y, "!", on_footer(AMBER));
429 frame.put(left + 29, y, "shell", on_footer(MUTED));
430 frame.put(left + 37, y, "$", on_footer(VIOLET));
431 frame.put(left + 39, y, "python", on_footer(MUTED));
432 frame.put(left + CARD_COLS - 23, y, "↑↓ move", on_footer(FAINT));
433 frame.put(left + CARD_COLS - 13, y, "↵ resume", on_footer(TEXT_STRONG));
434}
435
436fn draw_smol_hints(frame: &mut Frame, left: u16, cols: u16, y: u16) {
437 frame.put(left + 3, y, "#", on_footer(CYAN).bold());
438 frame.put(left + 5, y, "/", on_footer(CYAN).bold());
439 frame.put(left + 7, y, "!", on_footer(AMBER).bold());
440 frame.put(left + 9, y, "$", on_footer(GREEN).bold());
441 frame.put(left + cols - 14, y, "enter", on_footer(FAINT));
442 frame.put(left + cols - 8, y, "resume", on_footer(TEXT_STRONG).bold());
443}
444
445fn blit_logo(frame: &mut Frame, logo: &LogoGrid, left: u16, top: u16, background: Color) {
446 let mut buffer = [0_u8; 4];
447 for (row, cells) in logo.iter().enumerate() {
448 for (column, cell) in cells.iter().enumerate() {
449 let Some((glyph, color)) = cell else { continue };
450 let style = Style::new().fg(*color).bg(background);
451 frame.put(left + column as u16, top + row as u16, glyph.encode_utf8(&mut buffer), style);
452 }
453 }
454}
455
456const fn on_card(fg: Color) -> Style {
457 Style::new().fg(fg).bg(CARD_BG)
458}
459
460const fn on_footer(fg: Color) -> Style {
461 Style::new().fg(fg).bg(FOOTER_BG)
462}
463
464const fn on_selected(fg: Color) -> Style {
465 Style::new().fg(fg).bg(SELECTED_BG)
466}Sourcepub const fn fg(self, color: Color) -> Self
pub const fn fg(self, color: Color) -> Self
Sets the foreground color.
Examples found in repository?
398const fn ink(color: Color) -> Style {
399 Style::new().fg(color)
400}
401
402fn width_of(text: &str) -> u16 {
403 u16::try_from(xutf::width_str(text)).unwrap_or(u16::MAX)
404}
405
406/// [`TITLE`] truncated to at most `max` cells, ellipsized when it cannot
407/// fit whole.
408fn fit_title(scene: &Scene, max: u16) -> Str {
409 if width_of(TITLE) <= max {
410 return Str::new_static(TITLE);
411 }
412 let ellipsis = match scene.charset {
413 Charset::Ascii => "...",
414 _ => "…",
415 };
416 let budget = max.saturating_sub(width_of(ellipsis));
417 let mut used = 0_u16;
418 let mut end = 0_usize;
419 for grapheme in xutf::graphemes_str(TITLE) {
420 let cells = width_of(grapheme);
421 if used.saturating_add(cells) > budget {
422 break;
423 }
424 used = used.saturating_add(cells);
425 end += grapheme.len();
426 }
427 if end == 0 {
428 return Str::default();
429 }
430 fmts!("{}{ellipsis}", TITLE[..end].trim_end())
431}
432
433/// Total cells a powerline band with `segments` occupies, mirroring the
434/// `<status>` component's measurement.
435fn band_width(scene: &Scene, segments: &[Seg]) -> u16 {
436 let (left_cap, separator, right_cap) = band_chrome(scene.charset);
437 let text = segments
438 .iter()
439 .map(|segment| width_of(&segment.label))
440 .fold(0_u16, u16::saturating_add);
441 let separators = u16::try_from(segments.len().saturating_sub(1))
442 .unwrap_or(u16::MAX)
443 .saturating_mul(width_of(separator).saturating_add(2));
444 text
445 .saturating_add(separators)
446 .saturating_add(width_of(left_cap))
447 .saturating_add(2)
448 .saturating_add(width_of(right_cap))
449}
450
451/// Paints a powerline band at `x`: cap, padded segments, cap.
452fn draw_band(frame: &mut Frame, x: u16, y: u16, scene: &Scene, segments: &[Seg]) {
453 let (left_cap, separator, right_cap) = band_chrome(scene.charset);
454 let base = Style::new().fg(TEXT).bg(BAND_BG);
455 let edge = ink(BAND_BG);
456 let mut column = frame.put(x, y, left_cap, edge);
457 column = frame.put(column, y, " ", base);
458 for (index, segment) in segments.iter().enumerate() {
459 if index > 0 {
460 column = frame.put(column, y, " ", base.dim());
461 column = frame.put(column, y, separator, base.dim());
462 column = frame.put(column, y, " ", base.dim());
463 }
464 column = frame.put(column, y, &segment.label, base.fg(segment.color));
465 }
466 column = frame.put(column, y, " ", base);
467 frame.put(column, y, right_cap, edge);
468}More examples
1847fn draw_submission_text(
1848 frame: &mut Frame,
1849 x: u16,
1850 y: u16,
1851 width: u16,
1852 line: &str,
1853 charset: Charset,
1854) {
1855 let icon = charset.icon(Icon::Image);
1856 let mut chips: SmallVec<(usize, usize, String, usize), 4> = SmallVec::new();
1857 let mut base = 0;
1858 while let Some((start, end, marker)) = next_ref_tag(&line[base..]) {
1859 chips.push((base + start, base + end, format!("{icon} #{marker}"), marker));
1860 base += end;
1861 }
1862 let mut spans: SmallVec<Span<'_>, 8> = SmallVec::new();
1863 let mut at = 0;
1864 for (start, end, label, marker) in &chips {
1865 if *start > at {
1866 spans.push(Span::new(&line[at..*start], ink(TEXT)));
1867 }
1868 spans.push(Span::new(label, Style::new().fg(PANEL).bg(attachment_color(*marker)).bold()));
1869 at = *end;
1870 }
1871 if at < line.len() {
1872 spans.push(Span::new(&line[at..], ink(TEXT)));
1873 }
1874 draw_line(frame, x, y, width, &spans);
1875}
1876
1877/// Chrome outside a panel is transparent: no `bg`, so the terminal's own
1878/// background (and any image or blur behind it) shows through. Only the
1879/// panel boxes below opt into a fill.
1880const fn base_style() -> Style {
1881 Style::new().fg(TEXT)
1882}
1883
1884const fn panel_style() -> Style {
1885 Style::new().fg(TEXT).bg(PANEL)
1886}
1887
1888const fn ink(color: Color) -> Style {
1889 Style::new().fg(color)
1890}
1891
1892const fn panel_ink(color: Color) -> Style {
1893 Style::new().fg(color).bg(PANEL)
1894}
1895
1896const fn prose_style() -> Style {
1897 Style::new().fg(MUTED).italic()
1898}
1899
1900const fn code_style() -> Style {
1901 Style::new().fg(GREEN)
1902}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}
422
423fn draw_full_hints(frame: &mut Frame, left: u16, y: u16) {
424 frame.put(left + 3, y, "#", on_footer(CYAN));
425 frame.put(left + 5, y, "actions", on_footer(MUTED));
426 frame.put(left + 14, y, "/", on_footer(GREEN));
427 frame.put(left + 16, y, "commands", on_footer(MUTED));
428 frame.put(left + 27, y, "!", on_footer(AMBER));
429 frame.put(left + 29, y, "shell", on_footer(MUTED));
430 frame.put(left + 37, y, "$", on_footer(VIOLET));
431 frame.put(left + 39, y, "python", on_footer(MUTED));
432 frame.put(left + CARD_COLS - 23, y, "↑↓ move", on_footer(FAINT));
433 frame.put(left + CARD_COLS - 13, y, "↵ resume", on_footer(TEXT_STRONG));
434}
435
436fn draw_smol_hints(frame: &mut Frame, left: u16, cols: u16, y: u16) {
437 frame.put(left + 3, y, "#", on_footer(CYAN).bold());
438 frame.put(left + 5, y, "/", on_footer(CYAN).bold());
439 frame.put(left + 7, y, "!", on_footer(AMBER).bold());
440 frame.put(left + 9, y, "$", on_footer(GREEN).bold());
441 frame.put(left + cols - 14, y, "enter", on_footer(FAINT));
442 frame.put(left + cols - 8, y, "resume", on_footer(TEXT_STRONG).bold());
443}
444
445fn blit_logo(frame: &mut Frame, logo: &LogoGrid, left: u16, top: u16, background: Color) {
446 let mut buffer = [0_u8; 4];
447 for (row, cells) in logo.iter().enumerate() {
448 for (column, cell) in cells.iter().enumerate() {
449 let Some((glyph, color)) = cell else { continue };
450 let style = Style::new().fg(*color).bg(background);
451 frame.put(left + column as u16, top + row as u16, glyph.encode_utf8(&mut buffer), style);
452 }
453 }
454}
455
456const fn on_card(fg: Color) -> Style {
457 Style::new().fg(fg).bg(CARD_BG)
458}
459
460const fn on_footer(fg: Color) -> Style {
461 Style::new().fg(fg).bg(FOOTER_BG)
462}
463
464const fn on_selected(fg: Color) -> Style {
465 Style::new().fg(fg).bg(SELECTED_BG)
466}Sourcepub const fn bg(self, color: Color) -> Self
pub const fn bg(self, color: Color) -> Self
Sets the background color.
Examples found in repository?
452fn draw_band(frame: &mut Frame, x: u16, y: u16, scene: &Scene, segments: &[Seg]) {
453 let (left_cap, separator, right_cap) = band_chrome(scene.charset);
454 let base = Style::new().fg(TEXT).bg(BAND_BG);
455 let edge = ink(BAND_BG);
456 let mut column = frame.put(x, y, left_cap, edge);
457 column = frame.put(column, y, " ", base);
458 for (index, segment) in segments.iter().enumerate() {
459 if index > 0 {
460 column = frame.put(column, y, " ", base.dim());
461 column = frame.put(column, y, separator, base.dim());
462 column = frame.put(column, y, " ", base.dim());
463 }
464 column = frame.put(column, y, &segment.label, base.fg(segment.color));
465 }
466 column = frame.put(column, y, " ", base);
467 frame.put(column, y, right_cap, edge);
468}More examples
1847fn draw_submission_text(
1848 frame: &mut Frame,
1849 x: u16,
1850 y: u16,
1851 width: u16,
1852 line: &str,
1853 charset: Charset,
1854) {
1855 let icon = charset.icon(Icon::Image);
1856 let mut chips: SmallVec<(usize, usize, String, usize), 4> = SmallVec::new();
1857 let mut base = 0;
1858 while let Some((start, end, marker)) = next_ref_tag(&line[base..]) {
1859 chips.push((base + start, base + end, format!("{icon} #{marker}"), marker));
1860 base += end;
1861 }
1862 let mut spans: SmallVec<Span<'_>, 8> = SmallVec::new();
1863 let mut at = 0;
1864 for (start, end, label, marker) in &chips {
1865 if *start > at {
1866 spans.push(Span::new(&line[at..*start], ink(TEXT)));
1867 }
1868 spans.push(Span::new(label, Style::new().fg(PANEL).bg(attachment_color(*marker)).bold()));
1869 at = *end;
1870 }
1871 if at < line.len() {
1872 spans.push(Span::new(&line[at..], ink(TEXT)));
1873 }
1874 draw_line(frame, x, y, width, &spans);
1875}
1876
1877/// Chrome outside a panel is transparent: no `bg`, so the terminal's own
1878/// background (and any image or blur behind it) shows through. Only the
1879/// panel boxes below opt into a fill.
1880const fn base_style() -> Style {
1881 Style::new().fg(TEXT)
1882}
1883
1884const fn panel_style() -> Style {
1885 Style::new().fg(TEXT).bg(PANEL)
1886}
1887
1888const fn ink(color: Color) -> Style {
1889 Style::new().fg(color)
1890}
1891
1892const fn panel_ink(color: Color) -> Style {
1893 Style::new().fg(color).bg(PANEL)
1894}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}
422
423fn draw_full_hints(frame: &mut Frame, left: u16, y: u16) {
424 frame.put(left + 3, y, "#", on_footer(CYAN));
425 frame.put(left + 5, y, "actions", on_footer(MUTED));
426 frame.put(left + 14, y, "/", on_footer(GREEN));
427 frame.put(left + 16, y, "commands", on_footer(MUTED));
428 frame.put(left + 27, y, "!", on_footer(AMBER));
429 frame.put(left + 29, y, "shell", on_footer(MUTED));
430 frame.put(left + 37, y, "$", on_footer(VIOLET));
431 frame.put(left + 39, y, "python", on_footer(MUTED));
432 frame.put(left + CARD_COLS - 23, y, "↑↓ move", on_footer(FAINT));
433 frame.put(left + CARD_COLS - 13, y, "↵ resume", on_footer(TEXT_STRONG));
434}
435
436fn draw_smol_hints(frame: &mut Frame, left: u16, cols: u16, y: u16) {
437 frame.put(left + 3, y, "#", on_footer(CYAN).bold());
438 frame.put(left + 5, y, "/", on_footer(CYAN).bold());
439 frame.put(left + 7, y, "!", on_footer(AMBER).bold());
440 frame.put(left + 9, y, "$", on_footer(GREEN).bold());
441 frame.put(left + cols - 14, y, "enter", on_footer(FAINT));
442 frame.put(left + cols - 8, y, "resume", on_footer(TEXT_STRONG).bold());
443}
444
445fn blit_logo(frame: &mut Frame, logo: &LogoGrid, left: u16, top: u16, background: Color) {
446 let mut buffer = [0_u8; 4];
447 for (row, cells) in logo.iter().enumerate() {
448 for (column, cell) in cells.iter().enumerate() {
449 let Some((glyph, color)) = cell else { continue };
450 let style = Style::new().fg(*color).bg(background);
451 frame.put(left + column as u16, top + row as u16, glyph.encode_utf8(&mut buffer), style);
452 }
453 }
454}
455
456const fn on_card(fg: Color) -> Style {
457 Style::new().fg(fg).bg(CARD_BG)
458}
459
460const fn on_footer(fg: Color) -> Style {
461 Style::new().fg(fg).bg(FOOTER_BG)
462}
463
464const fn on_selected(fg: Color) -> Style {
465 Style::new().fg(fg).bg(SELECTED_BG)
466}Sourcepub const fn bold(self) -> Self
pub const fn bold(self) -> Self
Enables bold intensity.
Examples found in repository?
More examples
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}
1536
1537/// Repaints only changed byte runs within an equal-length ASCII label.
1538fn draw_ascii_changes(
1539 frame: &mut Frame,
1540 x: u16,
1541 y: u16,
1542 width: u16,
1543 previous: &str,
1544 next: &str,
1545 style: Style,
1546) {
1547 if width == 0 || previous == next {
1548 return;
1549 }
1550 if previous.len() != next.len() || !previous.is_ascii() || !next.is_ascii() {
1551 frame.put_clipped(x, y, width, next, style);
1552 return;
1553 }
1554 let previous = previous.as_bytes();
1555 let next_bytes = next.as_bytes();
1556 let limit = previous.len().min(usize::from(width));
1557 let mut index = 0;
1558 while index < limit {
1559 while index < limit && previous[index] == next_bytes[index] {
1560 index += 1;
1561 }
1562 let start = index;
1563 while index < limit && previous[index] != next_bytes[index] {
1564 index += 1;
1565 }
1566 if start < index {
1567 let offset = u16::try_from(start).unwrap_or(u16::MAX);
1568 frame.put_clipped(
1569 x.saturating_add(offset),
1570 y,
1571 u16::try_from(index - start).unwrap_or(u16::MAX),
1572 &next[start..index],
1573 style,
1574 );
1575 }
1576 }
1577}
1578
1579fn draw_edit_box(frame: &mut Frame, rect: Rect, charset: Charset) {
1580 draw_box(frame, rect, ink(FAINT), panel_style(), charset);
1581 if rect.width < 8 || rect.height < EDIT_BOX_HEIGHT {
1582 return;
1583 }
1584
1585 let title = [
1586 Span::new(" Live ", panel_ink(GREEN).bold()),
1587 Span::new("band · selection semantics ", panel_ink(CYAN)),
1588 ];
1589 draw_line(frame, rect.x + 2, rect.y, rect.width.saturating_sub(4), &title);
1590 draw_line(frame, rect.x + 2, rect.y + 1, rect.width.saturating_sub(4), &[
1591 Span::new(charset.check(), panel_ink(GREEN).bold()),
1592 Span::new(" ", panel_ink(GREEN)),
1593 Span::new("Transcript selections ride with the text", panel_ink(TEXT)),
1594 ]);
1595 draw_line(frame, rect.x + 2, rect.y + 2, rect.width.saturating_sub(4), &[Span::new(
1596 " margin commits pin the band on kitty-class terminals",
1597 panel_ink(MUTED),
1598 )]);
1599}
1600
1601/// Paints a submitted message: the prompt gutter, then the rendered
1602/// Markdown document blitted beside it (or the raw lines when the text
1603/// isn't renderable as Markdown).
1604fn draw_submission(frame: &mut Frame, y: u16, submission: &Submission, charset: Charset) {
1605 if frame.size().width < 4 {
1606 return;
1607 }
1608 let Some(view) = &submission.view else {
1609 for (offset, line) in submission.text.split('\n').enumerate() {
1610 let Ok(offset) = u16::try_from(offset) else {
1611 break;
1612 };
1613 let row = y.saturating_add(offset);
1614 if row >= frame.size().height {
1615 break;
1616 }
1617 let prompt = if offset == 0 { charset.cursor() } else { " " };
1618 let text_x = frame.put(1, row, prompt, ink(GREEN).bold());
1619 let width = frame
1620 .size()
1621 .width
1622 .saturating_sub(1)
1623 .saturating_sub(text_x.saturating_sub(1));
1624 draw_submission_text(frame, text_x, row, width, line, charset);
1625 }
1626 return;
1627 };
1628 frame.put(1, y, charset.cursor(), ink(GREEN).bold());
1629 frame.blit(view.frame(), 0, view.height(), 3, y);
1630}
1631fn explicit_line_count(text: &str) -> u16 {
1632 u16::try_from(
1633 text
1634 .bytes()
1635 .filter(|byte| *byte == b'\n')
1636 .count()
1637 .saturating_add(1),
1638 )
1639 .unwrap_or(u16::MAX)
1640}
1641
1642/// Paints a rounded panel box through the tier's border glyphs.
1643fn draw_box(frame: &mut Frame, rect: Rect, border: Style, fill: Style, charset: Charset) {
1644 if rect.width == 0 || rect.height == 0 {
1645 return;
1646 }
1647 let (tl, tr, _, _, horizontal, vertical) = charset.border(Border::Round);
1648 frame.fill(rect, fill);
1649 let mut glyph = [0_u8; 4];
1650 if rect.width == 1 {
1651 frame.put(rect.x, rect.y, vertical.encode_utf8(&mut glyph), border);
1652 return;
1653 }
1654
1655 let right = rect.x + rect.width - 1;
1656 let bottom = rect.y + rect.height - 1;
1657 frame.put(rect.x, rect.y, tl.encode_utf8(&mut glyph), border);
1658 frame.put(right, rect.y, tr.encode_utf8(&mut glyph), border);
1659 for x in rect.x + 1..right {
1660 frame.put(x, rect.y, horizontal.encode_utf8(&mut glyph), border);
1661 }
1662
1663 if rect.height > 1 {
1664 draw_box_bottom(frame, rect, border, charset);
1665 }
1666 for row in rect.y + 1..bottom {
1667 frame.put(rect.x, row, vertical.encode_utf8(&mut glyph), border);
1668 frame.put(right, row, vertical.encode_utf8(&mut glyph), border);
1669 }
1670}
1671
1672fn draw_box_bottom(frame: &mut Frame, rect: Rect, border: Style, charset: Charset) {
1673 if rect.width < 2 || rect.height < 2 {
1674 return;
1675 }
1676 let (_, _, bl, br, horizontal, _) = charset.border(Border::Round);
1677 let mut glyph = [0_u8; 4];
1678 let right = rect.x + rect.width - 1;
1679 let bottom = rect.y + rect.height - 1;
1680 frame.put(rect.x, bottom, bl.encode_utf8(&mut glyph), border);
1681 frame.put(right, bottom, br.encode_utf8(&mut glyph), border);
1682 for x in rect.x + 1..right {
1683 frame.put(x, bottom, horizontal.encode_utf8(&mut glyph), border);
1684 }
1685}
1686
1687fn draw_line(frame: &mut Frame, x: u16, y: u16, width: u16, spans: &[Span<'_>]) -> u16 {
1688 let right = x.saturating_add(width).min(frame.size().width);
1689 let mut column = x;
1690 for span in spans {
1691 column = frame.put_clipped(column, y, right.saturating_sub(column), span.text, span.style);
1692 if column >= right {
1693 break;
1694 }
1695 }
1696 column
1697}
1698
1699/// Flows `spans` grapheme-exact across the rect like a bare terminal,
1700/// preserving all whitespace and flagging each exactly-filled row boundary
1701/// soft so native selection copies the paragraph as one unbroken line.
1702/// Returns the rows used.
1703fn draw_flowed(frame: &mut Frame, rect: Rect, spans: &[Span<'_>]) -> u16 {
1704 if rect.width == 0 || rect.height == 0 {
1705 return 0;
1706 }
1707 let full_row = rect.x == 0 && rect.width == frame.size().width;
1708 let mut row = 0_u16;
1709 let mut column = 0_u16;
1710 let mut drew_anything = false;
1711
1712 for span in spans {
1713 for grapheme in xutf::graphemes_str(span.text) {
1714 let grapheme_width = visible_width(grapheme);
1715 if grapheme_width == 0 || grapheme_width > rect.width {
1716 continue;
1717 }
1718 if column.saturating_add(grapheme_width) > rect.width {
1719 // Only an exactly-filled row is byte-joinable by autowrap.
1720 if full_row && column == rect.width {
1721 frame.set_soft_wrap(rect.y.saturating_add(row));
1722 }
1723 row += 1;
1724 column = 0;
1725 }
1726 if row >= rect.height {
1727 return rect.height;
1728 }
1729 frame.put(rect.x + column, rect.y + row, grapheme, span.style);
1730 column += grapheme_width;
1731 drew_anything = true;
1732 }
1733 }
1734
1735 if drew_anything { row + 1 } else { 0 }
1736}
1737
1738fn visible_width(text: &str) -> u16 {
1739 u16::try_from(xutf::width_str(text)).unwrap_or(u16::MAX)
1740}
1741
1742/// Finds the first `<ref image=N/>` tag in submitted text: its byte range
1743/// plus the marker number `N`.
1744fn next_ref_tag(text: &str) -> Option<(usize, usize, usize)> {
1745 const HEAD: &str = "<ref image=";
1746 let mut from = 0;
1747 while let Some(at) = text[from..].find(HEAD) {
1748 let start = from + at;
1749 let body = &text[start + HEAD.len()..];
1750 let digits = body.bytes().take_while(u8::is_ascii_digit).count();
1751 if digits > 0 && body[digits..].starts_with("/>") {
1752 let marker = body[..digits].parse().unwrap_or(usize::MAX);
1753 return Some((start, start + HEAD.len() + digits + 2, marker));
1754 }
1755 from = start + HEAD.len();
1756 }
1757 None
1758}
1759
1760/// Splits one composer input row into base-styled text and attachment
1761/// chips, chip styling winning over any overlapping XML run.
1762///
1763/// Chips are located through the buffer's atomic ranges — like the XML
1764/// pass, styling happens at paint time, and typed lookalike text is never
1765/// recolored.
1766fn push_row_spans<'a>(
1767 editor: &Editor,
1768 row: &'a str,
1769 runs: &[SyntaxRun],
1770 spans: &mut SmallVec<Span<'a>, 16>,
1771) {
1772 let text = editor.text();
1773 let buffer_start = text.as_ptr() as usize;
1774 let row_start = (row.as_ptr() as usize).saturating_sub(buffer_start);
1775 let row_end = row_start + row.len();
1776 // Chip segments clipped to this row; style derives from the FULL atom
1777 // text, so a chip wrapped across rows keeps its color on every row.
1778 let mut chips: SmallVec<(usize, usize, Style), 4> = SmallVec::new();
1779 for (start, end) in editor.atom_ranges() {
1780 let from = start.max(row_start);
1781 let to = end.min(row_end);
1782 if from < to {
1783 chips.push((from - row_start, to - row_start, chip_style(&text[start..end])));
1784 }
1785 }
1786 // The style and extent of the base segment covering `at`: its XML run,
1787 // or plain text up to the next run.
1788 let base = |at: usize| {
1789 runs
1790 .iter()
1791 .find(|run| run.start <= at && at < run.end)
1792 .map_or_else(
1793 || {
1794 let next = runs
1795 .iter()
1796 .map(|run| run.start)
1797 .filter(|start| *start > at)
1798 .min()
1799 .unwrap_or(row.len());
1800 (next, base_style())
1801 },
1802 |run| (run.end, run.style),
1803 )
1804 };
1805 fn emit<'a>(
1806 row: &'a str,
1807 base: &impl Fn(usize) -> (usize, Style),
1808 from: usize,
1809 to: usize,
1810 spans: &mut SmallVec<Span<'a>, 16>,
1811 ) {
1812 let mut at = from;
1813 while at < to {
1814 let (run_end, style) = base(at);
1815 let end = run_end.min(to);
1816 spans.push(Span::new(&row[at..end], style));
1817 at = end;
1818 }
1819 }
1820 let mut at = 0;
1821 for (start, end, style) in chips {
1822 emit(row, &base, at, start, spans);
1823 spans.push(Span::new(&row[start..end], style));
1824 at = end;
1825 }
1826 emit(row, &base, at, row.len(), spans);
1827}
1828
1829/// Style for one atomic chip: a trailing `#N` selects the marker's
1830/// identity color; other atoms stay plain.
1831fn chip_style(chip: &str) -> Style {
1832 let Some(hash) = chip.rfind('#') else {
1833 return base_style();
1834 };
1835 let digits = &chip[hash + 1..];
1836 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
1837 return base_style();
1838 }
1839 match digits.parse::<usize>() {
1840 Ok(marker) if marker > 0 => ink(attachment_color(marker)).bold(),
1841 _ => base_style(),
1842 }
1843}
1844
1845/// Paints one transcript line, rendering each `<ref image=N/>` tag as a
1846/// compact `<icon> #N` pill filled with the attachment's identity color.
1847fn draw_submission_text(
1848 frame: &mut Frame,
1849 x: u16,
1850 y: u16,
1851 width: u16,
1852 line: &str,
1853 charset: Charset,
1854) {
1855 let icon = charset.icon(Icon::Image);
1856 let mut chips: SmallVec<(usize, usize, String, usize), 4> = SmallVec::new();
1857 let mut base = 0;
1858 while let Some((start, end, marker)) = next_ref_tag(&line[base..]) {
1859 chips.push((base + start, base + end, format!("{icon} #{marker}"), marker));
1860 base += end;
1861 }
1862 let mut spans: SmallVec<Span<'_>, 8> = SmallVec::new();
1863 let mut at = 0;
1864 for (start, end, label, marker) in &chips {
1865 if *start > at {
1866 spans.push(Span::new(&line[at..*start], ink(TEXT)));
1867 }
1868 spans.push(Span::new(label, Style::new().fg(PANEL).bg(attachment_color(*marker)).bold()));
1869 at = *end;
1870 }
1871 if at < line.len() {
1872 spans.push(Span::new(&line[at..], ink(TEXT)));
1873 }
1874 draw_line(frame, x, y, width, &spans);
1875}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}
228
229// ── studies ─────────────────────────────────────────────────────────────────
230
231/// 1: the pi layout verbatim — a bordered composer whose top border
232/// carries the session band on the left and the title on the right, with
233/// the spinner narrating intent on its own row above the box.
234fn study_pi_parity(frame: &mut Frame, y: u16, scene: &Scene) {
235 draw_working_spin(frame, 1, y, scene);
236 let (tl, tr, bl, br, horizontal, vertical) = border_glyphs(scene.charset);
237 let right = scene.right_edge();
238 draw_border_row(frame, y + 1, scene, tl, tr, horizontal);
239 let segments = [model(scene), omp_brand(scene), git(scene), context(scene), cost()];
240 draw_band(frame, 2, y + 1, scene, &segments);
241 let band_end = 2_u16.saturating_add(band_width(scene, &segments));
242 draw_border_title(frame, y + 1, scene, band_end.saturating_add(2));
243 frame.put(0, y + 2, vertical, ink(FAINT));
244 frame.put(2, y + 2, beam(scene.charset), ink(TEXT));
245 frame.put(right, y + 2, vertical, ink(FAINT));
246 draw_border_row(frame, y + 3, scene, bl, br, horizontal);
247}
248
249/// 2: split + air gap as picked, with the title moving into the air row
250/// so the breathing space doubles as identity.
251fn study_gap_title(frame: &mut Frame, y: u16, scene: &Scene) {
252 draw_working(frame, 1, y, scene);
253 let title = fit_title(scene, scene.width.saturating_sub(2));
254 let x = scene
255 .width
256 .saturating_sub(width_of(&title).saturating_add(1));
257 frame.put(x, y + 1, &title, ink(FAINT).italic());
258 draw_split_bands(frame, y + 2, scene);
259 draw_input(frame, 0, y + 3, scene);
260}
261
262/// 3: the title joins the left band beside the brand segment, so the
263/// band row itself answers "what session is this"; session facts keep
264/// the right cap.
265fn study_band_title(frame: &mut Frame, y: u16, scene: &Scene) {
266 draw_working(frame, 1, y, scene);
267 let right = [model(scene), git(scene), context(scene), Seg::new(COST_SHORT, PURPLE)];
268 let right_width = band_width(scene, &right);
269 let brand_seg = brand(scene);
270 let (_, separator, _) = band_chrome(scene.charset);
271 let fixed = band_width(scene, std::slice::from_ref(&brand_seg))
272 .saturating_add(width_of(separator).saturating_add(2));
273 let budget = scene
274 .width
275 .saturating_sub(right_width.saturating_add(2))
276 .saturating_sub(fixed);
277 let left = [brand_seg, Seg::new(fit_title(scene, budget), TEXT)];
278 draw_band(frame, 0, y + 2, scene, &left);
279 draw_band(frame, scene.width.saturating_sub(right_width), y + 2, scene, &right);
280 draw_input(frame, 0, y + 3, scene);
281}
282
283/// 4: split + air untouched; the title borrows the prompt row's right
284/// edge and would yield to long input lines.
285fn study_prompt_title(frame: &mut Frame, y: u16, scene: &Scene) {
286 draw_working(frame, 1, y, scene);
287 draw_split_bands(frame, y + 2, scene);
288 draw_input(frame, 0, y + 3, scene);
289 let title = fit_title(scene, scene.width.saturating_sub(8));
290 let x = scene
291 .width
292 .saturating_sub(width_of(&title).saturating_add(1));
293 frame.put(x, y + 3, &title, ink(FAINT).italic());
294}
295
296/// 5: the title gets its own dim row above the narration, heading the
297/// whole live block like a section title.
298fn study_crown(frame: &mut Frame, y: u16, scene: &Scene) {
299 let title = fit_title(scene, scene.width.saturating_sub(2));
300 frame.put(1, y, &title, ink(MUTED).bold());
301 draw_working(frame, 1, y + 1, scene);
302 draw_split_bands(frame, y + 3, scene);
303 draw_input(frame, 0, y + 4, scene);
304}Sourcepub const fn dim(self) -> Self
pub const fn dim(self) -> Self
Enables faint intensity.
Examples found in repository?
452fn draw_band(frame: &mut Frame, x: u16, y: u16, scene: &Scene, segments: &[Seg]) {
453 let (left_cap, separator, right_cap) = band_chrome(scene.charset);
454 let base = Style::new().fg(TEXT).bg(BAND_BG);
455 let edge = ink(BAND_BG);
456 let mut column = frame.put(x, y, left_cap, edge);
457 column = frame.put(column, y, " ", base);
458 for (index, segment) in segments.iter().enumerate() {
459 if index > 0 {
460 column = frame.put(column, y, " ", base.dim());
461 column = frame.put(column, y, separator, base.dim());
462 column = frame.put(column, y, " ", base.dim());
463 }
464 column = frame.put(column, y, &segment.label, base.fg(segment.color));
465 }
466 column = frame.put(column, y, " ", base);
467 frame.put(column, y, right_cap, edge);
468}More examples
428 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
429 pc.hits
430 .push(Hit { rect, slot: self.slot, tag: HitTag::Press });
431 let editor = self.editor.borrow();
432 let input_x = rect.x.saturating_add(Self::input_offset());
433 let input_width = Self::input_width(rect.width);
434 let input_height = editor.input_height_for(input_width);
435 let theme = Theme::default();
436 let mut in_comment = false;
437 for (offset, row) in editor.view(input_width).iter().enumerate() {
438 let row_y = rect
439 .y
440 .saturating_add(u16::try_from(offset).unwrap_or(u16::MAX));
441 if row_y >= pc.clip {
442 break;
443 }
444 if offset == 0 {
445 pc.frame
446 .put(rect.x, row_y, &Self::input_prompt(pc.ctx.charset), ink(FAINT));
447 }
448 let mut spans: SmallVec<Span<'_>, 16> = SmallVec::new();
449 if editor.options().xml {
450 let (runs, next) = highlight_xml(row.text, &theme, in_comment);
451 in_comment = next;
452 push_row_spans(&editor, row.text, &runs, &mut spans);
453 } else {
454 push_row_spans(&editor, row.text, &[], &mut spans);
455 }
456 draw_line(pc.frame, input_x, row_y, input_width, &spans);
457 if let Some(cursor_column) = row.cursor_column {
458 if cursor_column >= visible_width(row.text)
459 && let Some(hint) = editor.inline_hint()
460 {
461 let hint_x = input_x.saturating_add(cursor_column).saturating_add(1);
462 let width = input_width.saturating_sub(cursor_column.saturating_add(1));
463 draw_line(pc.frame, hint_x, row_y, width, &[Span::new(
464 hint.as_str(),
465 ink(MUTED).dim(),
466 )]);
467 }
468 pc.frame.set_cursor(
469 input_x
470 .saturating_add(cursor_column)
471 .min(rect.x.saturating_add(rect.width.saturating_sub(2))),
472 row_y,
473 );
474 }
475 }
476 Self::paint_picker(pc, rect, rect.y.saturating_add(input_height), &editor);
477 }Sourcepub const fn italic(self) -> Self
pub const fn italic(self) -> Self
Enables italics.
Examples found in repository?
251fn study_gap_title(frame: &mut Frame, y: u16, scene: &Scene) {
252 draw_working(frame, 1, y, scene);
253 let title = fit_title(scene, scene.width.saturating_sub(2));
254 let x = scene
255 .width
256 .saturating_sub(width_of(&title).saturating_add(1));
257 frame.put(x, y + 1, &title, ink(FAINT).italic());
258 draw_split_bands(frame, y + 2, scene);
259 draw_input(frame, 0, y + 3, scene);
260}
261
262/// 3: the title joins the left band beside the brand segment, so the
263/// band row itself answers "what session is this"; session facts keep
264/// the right cap.
265fn study_band_title(frame: &mut Frame, y: u16, scene: &Scene) {
266 draw_working(frame, 1, y, scene);
267 let right = [model(scene), git(scene), context(scene), Seg::new(COST_SHORT, PURPLE)];
268 let right_width = band_width(scene, &right);
269 let brand_seg = brand(scene);
270 let (_, separator, _) = band_chrome(scene.charset);
271 let fixed = band_width(scene, std::slice::from_ref(&brand_seg))
272 .saturating_add(width_of(separator).saturating_add(2));
273 let budget = scene
274 .width
275 .saturating_sub(right_width.saturating_add(2))
276 .saturating_sub(fixed);
277 let left = [brand_seg, Seg::new(fit_title(scene, budget), TEXT)];
278 draw_band(frame, 0, y + 2, scene, &left);
279 draw_band(frame, scene.width.saturating_sub(right_width), y + 2, scene, &right);
280 draw_input(frame, 0, y + 3, scene);
281}
282
283/// 4: split + air untouched; the title borrows the prompt row's right
284/// edge and would yield to long input lines.
285fn study_prompt_title(frame: &mut Frame, y: u16, scene: &Scene) {
286 draw_working(frame, 1, y, scene);
287 draw_split_bands(frame, y + 2, scene);
288 draw_input(frame, 0, y + 3, scene);
289 let title = fit_title(scene, scene.width.saturating_sub(8));
290 let x = scene
291 .width
292 .saturating_sub(width_of(&title).saturating_add(1));
293 frame.put(x, y + 3, &title, ink(FAINT).italic());
294}More examples
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}
1536
1537/// Repaints only changed byte runs within an equal-length ASCII label.
1538fn draw_ascii_changes(
1539 frame: &mut Frame,
1540 x: u16,
1541 y: u16,
1542 width: u16,
1543 previous: &str,
1544 next: &str,
1545 style: Style,
1546) {
1547 if width == 0 || previous == next {
1548 return;
1549 }
1550 if previous.len() != next.len() || !previous.is_ascii() || !next.is_ascii() {
1551 frame.put_clipped(x, y, width, next, style);
1552 return;
1553 }
1554 let previous = previous.as_bytes();
1555 let next_bytes = next.as_bytes();
1556 let limit = previous.len().min(usize::from(width));
1557 let mut index = 0;
1558 while index < limit {
1559 while index < limit && previous[index] == next_bytes[index] {
1560 index += 1;
1561 }
1562 let start = index;
1563 while index < limit && previous[index] != next_bytes[index] {
1564 index += 1;
1565 }
1566 if start < index {
1567 let offset = u16::try_from(start).unwrap_or(u16::MAX);
1568 frame.put_clipped(
1569 x.saturating_add(offset),
1570 y,
1571 u16::try_from(index - start).unwrap_or(u16::MAX),
1572 &next[start..index],
1573 style,
1574 );
1575 }
1576 }
1577}
1578
1579fn draw_edit_box(frame: &mut Frame, rect: Rect, charset: Charset) {
1580 draw_box(frame, rect, ink(FAINT), panel_style(), charset);
1581 if rect.width < 8 || rect.height < EDIT_BOX_HEIGHT {
1582 return;
1583 }
1584
1585 let title = [
1586 Span::new(" Live ", panel_ink(GREEN).bold()),
1587 Span::new("band · selection semantics ", panel_ink(CYAN)),
1588 ];
1589 draw_line(frame, rect.x + 2, rect.y, rect.width.saturating_sub(4), &title);
1590 draw_line(frame, rect.x + 2, rect.y + 1, rect.width.saturating_sub(4), &[
1591 Span::new(charset.check(), panel_ink(GREEN).bold()),
1592 Span::new(" ", panel_ink(GREEN)),
1593 Span::new("Transcript selections ride with the text", panel_ink(TEXT)),
1594 ]);
1595 draw_line(frame, rect.x + 2, rect.y + 2, rect.width.saturating_sub(4), &[Span::new(
1596 " margin commits pin the band on kitty-class terminals",
1597 panel_ink(MUTED),
1598 )]);
1599}
1600
1601/// Paints a submitted message: the prompt gutter, then the rendered
1602/// Markdown document blitted beside it (or the raw lines when the text
1603/// isn't renderable as Markdown).
1604fn draw_submission(frame: &mut Frame, y: u16, submission: &Submission, charset: Charset) {
1605 if frame.size().width < 4 {
1606 return;
1607 }
1608 let Some(view) = &submission.view else {
1609 for (offset, line) in submission.text.split('\n').enumerate() {
1610 let Ok(offset) = u16::try_from(offset) else {
1611 break;
1612 };
1613 let row = y.saturating_add(offset);
1614 if row >= frame.size().height {
1615 break;
1616 }
1617 let prompt = if offset == 0 { charset.cursor() } else { " " };
1618 let text_x = frame.put(1, row, prompt, ink(GREEN).bold());
1619 let width = frame
1620 .size()
1621 .width
1622 .saturating_sub(1)
1623 .saturating_sub(text_x.saturating_sub(1));
1624 draw_submission_text(frame, text_x, row, width, line, charset);
1625 }
1626 return;
1627 };
1628 frame.put(1, y, charset.cursor(), ink(GREEN).bold());
1629 frame.blit(view.frame(), 0, view.height(), 3, y);
1630}
1631fn explicit_line_count(text: &str) -> u16 {
1632 u16::try_from(
1633 text
1634 .bytes()
1635 .filter(|byte| *byte == b'\n')
1636 .count()
1637 .saturating_add(1),
1638 )
1639 .unwrap_or(u16::MAX)
1640}
1641
1642/// Paints a rounded panel box through the tier's border glyphs.
1643fn draw_box(frame: &mut Frame, rect: Rect, border: Style, fill: Style, charset: Charset) {
1644 if rect.width == 0 || rect.height == 0 {
1645 return;
1646 }
1647 let (tl, tr, _, _, horizontal, vertical) = charset.border(Border::Round);
1648 frame.fill(rect, fill);
1649 let mut glyph = [0_u8; 4];
1650 if rect.width == 1 {
1651 frame.put(rect.x, rect.y, vertical.encode_utf8(&mut glyph), border);
1652 return;
1653 }
1654
1655 let right = rect.x + rect.width - 1;
1656 let bottom = rect.y + rect.height - 1;
1657 frame.put(rect.x, rect.y, tl.encode_utf8(&mut glyph), border);
1658 frame.put(right, rect.y, tr.encode_utf8(&mut glyph), border);
1659 for x in rect.x + 1..right {
1660 frame.put(x, rect.y, horizontal.encode_utf8(&mut glyph), border);
1661 }
1662
1663 if rect.height > 1 {
1664 draw_box_bottom(frame, rect, border, charset);
1665 }
1666 for row in rect.y + 1..bottom {
1667 frame.put(rect.x, row, vertical.encode_utf8(&mut glyph), border);
1668 frame.put(right, row, vertical.encode_utf8(&mut glyph), border);
1669 }
1670}
1671
1672fn draw_box_bottom(frame: &mut Frame, rect: Rect, border: Style, charset: Charset) {
1673 if rect.width < 2 || rect.height < 2 {
1674 return;
1675 }
1676 let (_, _, bl, br, horizontal, _) = charset.border(Border::Round);
1677 let mut glyph = [0_u8; 4];
1678 let right = rect.x + rect.width - 1;
1679 let bottom = rect.y + rect.height - 1;
1680 frame.put(rect.x, bottom, bl.encode_utf8(&mut glyph), border);
1681 frame.put(right, bottom, br.encode_utf8(&mut glyph), border);
1682 for x in rect.x + 1..right {
1683 frame.put(x, bottom, horizontal.encode_utf8(&mut glyph), border);
1684 }
1685}
1686
1687fn draw_line(frame: &mut Frame, x: u16, y: u16, width: u16, spans: &[Span<'_>]) -> u16 {
1688 let right = x.saturating_add(width).min(frame.size().width);
1689 let mut column = x;
1690 for span in spans {
1691 column = frame.put_clipped(column, y, right.saturating_sub(column), span.text, span.style);
1692 if column >= right {
1693 break;
1694 }
1695 }
1696 column
1697}
1698
1699/// Flows `spans` grapheme-exact across the rect like a bare terminal,
1700/// preserving all whitespace and flagging each exactly-filled row boundary
1701/// soft so native selection copies the paragraph as one unbroken line.
1702/// Returns the rows used.
1703fn draw_flowed(frame: &mut Frame, rect: Rect, spans: &[Span<'_>]) -> u16 {
1704 if rect.width == 0 || rect.height == 0 {
1705 return 0;
1706 }
1707 let full_row = rect.x == 0 && rect.width == frame.size().width;
1708 let mut row = 0_u16;
1709 let mut column = 0_u16;
1710 let mut drew_anything = false;
1711
1712 for span in spans {
1713 for grapheme in xutf::graphemes_str(span.text) {
1714 let grapheme_width = visible_width(grapheme);
1715 if grapheme_width == 0 || grapheme_width > rect.width {
1716 continue;
1717 }
1718 if column.saturating_add(grapheme_width) > rect.width {
1719 // Only an exactly-filled row is byte-joinable by autowrap.
1720 if full_row && column == rect.width {
1721 frame.set_soft_wrap(rect.y.saturating_add(row));
1722 }
1723 row += 1;
1724 column = 0;
1725 }
1726 if row >= rect.height {
1727 return rect.height;
1728 }
1729 frame.put(rect.x + column, rect.y + row, grapheme, span.style);
1730 column += grapheme_width;
1731 drew_anything = true;
1732 }
1733 }
1734
1735 if drew_anything { row + 1 } else { 0 }
1736}
1737
1738fn visible_width(text: &str) -> u16 {
1739 u16::try_from(xutf::width_str(text)).unwrap_or(u16::MAX)
1740}
1741
1742/// Finds the first `<ref image=N/>` tag in submitted text: its byte range
1743/// plus the marker number `N`.
1744fn next_ref_tag(text: &str) -> Option<(usize, usize, usize)> {
1745 const HEAD: &str = "<ref image=";
1746 let mut from = 0;
1747 while let Some(at) = text[from..].find(HEAD) {
1748 let start = from + at;
1749 let body = &text[start + HEAD.len()..];
1750 let digits = body.bytes().take_while(u8::is_ascii_digit).count();
1751 if digits > 0 && body[digits..].starts_with("/>") {
1752 let marker = body[..digits].parse().unwrap_or(usize::MAX);
1753 return Some((start, start + HEAD.len() + digits + 2, marker));
1754 }
1755 from = start + HEAD.len();
1756 }
1757 None
1758}
1759
1760/// Splits one composer input row into base-styled text and attachment
1761/// chips, chip styling winning over any overlapping XML run.
1762///
1763/// Chips are located through the buffer's atomic ranges — like the XML
1764/// pass, styling happens at paint time, and typed lookalike text is never
1765/// recolored.
1766fn push_row_spans<'a>(
1767 editor: &Editor,
1768 row: &'a str,
1769 runs: &[SyntaxRun],
1770 spans: &mut SmallVec<Span<'a>, 16>,
1771) {
1772 let text = editor.text();
1773 let buffer_start = text.as_ptr() as usize;
1774 let row_start = (row.as_ptr() as usize).saturating_sub(buffer_start);
1775 let row_end = row_start + row.len();
1776 // Chip segments clipped to this row; style derives from the FULL atom
1777 // text, so a chip wrapped across rows keeps its color on every row.
1778 let mut chips: SmallVec<(usize, usize, Style), 4> = SmallVec::new();
1779 for (start, end) in editor.atom_ranges() {
1780 let from = start.max(row_start);
1781 let to = end.min(row_end);
1782 if from < to {
1783 chips.push((from - row_start, to - row_start, chip_style(&text[start..end])));
1784 }
1785 }
1786 // The style and extent of the base segment covering `at`: its XML run,
1787 // or plain text up to the next run.
1788 let base = |at: usize| {
1789 runs
1790 .iter()
1791 .find(|run| run.start <= at && at < run.end)
1792 .map_or_else(
1793 || {
1794 let next = runs
1795 .iter()
1796 .map(|run| run.start)
1797 .filter(|start| *start > at)
1798 .min()
1799 .unwrap_or(row.len());
1800 (next, base_style())
1801 },
1802 |run| (run.end, run.style),
1803 )
1804 };
1805 fn emit<'a>(
1806 row: &'a str,
1807 base: &impl Fn(usize) -> (usize, Style),
1808 from: usize,
1809 to: usize,
1810 spans: &mut SmallVec<Span<'a>, 16>,
1811 ) {
1812 let mut at = from;
1813 while at < to {
1814 let (run_end, style) = base(at);
1815 let end = run_end.min(to);
1816 spans.push(Span::new(&row[at..end], style));
1817 at = end;
1818 }
1819 }
1820 let mut at = 0;
1821 for (start, end, style) in chips {
1822 emit(row, &base, at, start, spans);
1823 spans.push(Span::new(&row[start..end], style));
1824 at = end;
1825 }
1826 emit(row, &base, at, row.len(), spans);
1827}
1828
1829/// Style for one atomic chip: a trailing `#N` selects the marker's
1830/// identity color; other atoms stay plain.
1831fn chip_style(chip: &str) -> Style {
1832 let Some(hash) = chip.rfind('#') else {
1833 return base_style();
1834 };
1835 let digits = &chip[hash + 1..];
1836 if digits.is_empty() || !digits.bytes().all(|byte| byte.is_ascii_digit()) {
1837 return base_style();
1838 }
1839 match digits.parse::<usize>() {
1840 Ok(marker) if marker > 0 => ink(attachment_color(marker)).bold(),
1841 _ => base_style(),
1842 }
1843}
1844
1845/// Paints one transcript line, rendering each `<ref image=N/>` tag as a
1846/// compact `<icon> #N` pill filled with the attachment's identity color.
1847fn draw_submission_text(
1848 frame: &mut Frame,
1849 x: u16,
1850 y: u16,
1851 width: u16,
1852 line: &str,
1853 charset: Charset,
1854) {
1855 let icon = charset.icon(Icon::Image);
1856 let mut chips: SmallVec<(usize, usize, String, usize), 4> = SmallVec::new();
1857 let mut base = 0;
1858 while let Some((start, end, marker)) = next_ref_tag(&line[base..]) {
1859 chips.push((base + start, base + end, format!("{icon} #{marker}"), marker));
1860 base += end;
1861 }
1862 let mut spans: SmallVec<Span<'_>, 8> = SmallVec::new();
1863 let mut at = 0;
1864 for (start, end, label, marker) in &chips {
1865 if *start > at {
1866 spans.push(Span::new(&line[at..*start], ink(TEXT)));
1867 }
1868 spans.push(Span::new(label, Style::new().fg(PANEL).bg(attachment_color(*marker)).bold()));
1869 at = *end;
1870 }
1871 if at < line.len() {
1872 spans.push(Span::new(&line[at..], ink(TEXT)));
1873 }
1874 draw_line(frame, x, y, width, &spans);
1875}
1876
1877/// Chrome outside a panel is transparent: no `bg`, so the terminal's own
1878/// background (and any image or blur behind it) shows through. Only the
1879/// panel boxes below opt into a fill.
1880const fn base_style() -> Style {
1881 Style::new().fg(TEXT)
1882}
1883
1884const fn panel_style() -> Style {
1885 Style::new().fg(TEXT).bg(PANEL)
1886}
1887
1888const fn ink(color: Color) -> Style {
1889 Style::new().fg(color)
1890}
1891
1892const fn panel_ink(color: Color) -> Style {
1893 Style::new().fg(color).bg(PANEL)
1894}
1895
1896const fn prose_style() -> Style {
1897 Style::new().fg(MUTED).italic()
1898}Sourcepub const fn underline_color(self, color: Color) -> Self
pub const fn underline_color(self, color: Color) -> Self
Sets the underline color (SGR 58); Color::Default leaves the
terminal’s underline color untouched.
Sourcepub const fn strikethrough(self) -> Self
pub const fn strikethrough(self) -> Self
Enables strikethrough.
Sourcepub fn link(self, url: &str) -> Self
pub fn link(self, url: &str) -> Self
Attaches a terminal hyperlink target to this style.
The URL is interned once and only its typed identity rides on rich-text runs and frame cells. Empty targets are ignored.
Sourcepub const fn inherit(self, parent: Self) -> Self
pub const fn inherit(self, parent: Self) -> Self
CSS-like cascade: unset properties adopt the parent’s. A
Color::Default foreground counts as unset and attribute flags OR
together. The background never inherits — ancestor fills reach
descendants through the paint underlay instead.
Sourcepub const fn foreground_color(&self) -> Color
pub const fn foreground_color(&self) -> Color
The foreground color, for callers deriving accents from a style.
Sourcepub const fn background_color(&self) -> Color
pub const fn background_color(&self) -> Color
The background color, for callers deciding whether to fill a region.
Trait Implementations§
impl Copy for Style
impl Eq for Style
impl StructuralPartialEq for Style
Auto Trait Implementations§
impl Freeze for Style
impl RefUnwindSafe for Style
impl Send for Style
impl Sync for Style
impl Unpin for Style
impl UnsafeUnpin for Style
impl UnwindSafe for Style
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
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>
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>
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)
&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)
&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
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> ⓘ
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> ⓘ
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