1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::borrow::Cow;
5
6use scrin::{
7 Color, Frame, Rect,
8 core::buffer::{Buffer, Cell},
9 interaction::{
10 HitRegion, MouseCursor, SelectionGroup, WidgetAction, WidgetId, WidgetRole, WidgetState,
11 WidgetValue,
12 },
13 style::{Modifier, Style},
14 theme::{Theme, ThemeTokens},
15 widgets::{
16 Widget,
17 block::{Block, BorderStyle},
18 },
19};
20
21pub use scrin;
22
23pub mod prelude {
25 pub use crate::{
26 Aisling, AislingEffect, AislingExt, AislingPalette, Align, Bordered, FlickerPanel, Gauge,
27 GlyphRain, List, NebulaGauge, NeonBorder, OrbField, Paragraph, PulseRing, Radar,
28 SignalPanel, Sparkline, SplitDirection, SplitPane, StatusBar, StreamPanel, TabBar, Table,
29 WaveType, Waveform, scrin,
30 };
31 pub use scrin::interaction::{HitRegion, WidgetId, WidgetRole};
32 pub use scrin::theme::{Theme, ThemeTokens};
33 pub use scrin::widgets::Widget;
34}
35
36impl Default for AislingPalette {
38 fn default() -> Self {
39 Self::cypherpunk()
40 }
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct AislingPalette {
46 pub low: Color,
47 pub mid: Color,
48 pub high: Color,
49 pub pulse: Color,
50 pub shadow: Color,
51}
52
53impl AislingPalette {
54 #[must_use]
56 pub const fn cypherpunk() -> Self {
57 Self {
58 low: Color::rgb(0, 230, 255),
59 mid: Color::rgb(0, 255, 136),
60 high: Color::rgb(240, 255, 255),
61 pulse: Color::rgb(255, 200, 0),
62 shadow: Color::rgb(8, 12, 20),
63 }
64 }
65
66 #[must_use]
68 pub const fn dream() -> Self {
69 Self {
70 low: Color::rgb(58, 160, 220),
71 mid: Color::rgb(120, 100, 180),
72 high: Color::rgb(220, 210, 170),
73 pulse: Color::rgb(0, 200, 200),
74 shadow: Color::rgb(12, 14, 24),
75 }
76 }
77
78 #[must_use]
80 pub const fn phosphor() -> Self {
81 Self {
82 low: Color::rgb(61, 255, 142),
83 mid: Color::rgb(19, 189, 112),
84 high: Color::rgb(210, 255, 181),
85 pulse: Color::rgb(135, 255, 221),
86 shadow: Color::rgb(7, 22, 16),
87 }
88 }
89
90 #[must_use]
92 pub const fn flare() -> Self {
93 Self {
94 low: Color::rgb(255, 170, 50),
95 mid: Color::rgb(255, 120, 30),
96 high: Color::rgb(255, 230, 140),
97 pulse: Color::rgb(255, 80, 60),
98 shadow: Color::rgb(24, 12, 8),
99 }
100 }
101
102 #[must_use]
104 pub const fn theme_tokens(self) -> ThemeTokens {
105 ThemeTokens::new(
106 self.shadow,
107 self.high,
108 self.low,
109 self.mid,
110 self.mid,
111 self.pulse,
112 self.pulse,
113 )
114 }
115
116 #[must_use]
118 pub const fn from_theme_tokens(tokens: ThemeTokens) -> Self {
119 Self {
120 low: tokens.dim,
121 mid: tokens.accent,
122 high: tokens.text,
123 pulse: tokens.warning,
124 shadow: tokens.panel,
125 }
126 }
127
128 #[must_use]
130 pub const fn theme(self) -> Theme {
131 Theme {
132 bg: self.shadow,
133 fg: self.high,
134 accent: self.mid,
135 accent_bright: self.low,
136 muted: self.low,
137 surface: self.shadow,
138 surface_bright: self.shadow,
139 error: self.pulse,
140 warning: self.pulse,
141 success: self.mid,
142 info: self.low,
143 border: self.low,
144 border_focus: self.pulse,
145 text_primary: self.high,
146 text_secondary: self.mid,
147 text_dim: self.low,
148 highlight_bg: self.mid,
149 highlight_fg: self.shadow,
150 glow: self.pulse,
151 }
152 }
153
154 #[must_use]
156 pub const fn from_theme(theme: Theme) -> Self {
157 Self {
158 low: theme.info,
159 mid: theme.accent,
160 high: theme.text_primary,
161 pulse: theme.glow,
162 shadow: theme.surface,
163 }
164 }
165
166 #[must_use]
168 pub fn block<'a>(self, title: &'a str) -> Block<'a> {
169 Block::new(title)
170 .with_borders(BorderStyle::Plain)
171 .with_theme_tokens(self.theme_tokens())
172 .with_border_color(self.low)
173 .with_inner_margin(Rect::ZERO)
174 }
175
176 fn lane(self, value: u64) -> Color {
177 match value % 4 {
178 0 => self.low,
179 1 => self.mid,
180 2 => self.high,
181 _ => self.pulse,
182 }
183 }
184}
185
186impl From<AislingPalette> for ThemeTokens {
187 fn from(value: AislingPalette) -> Self {
188 value.theme_tokens()
189 }
190}
191
192impl From<ThemeTokens> for AislingPalette {
193 fn from(value: ThemeTokens) -> Self {
194 Self::from_theme_tokens(value)
195 }
196}
197
198impl From<AislingPalette> for Theme {
199 fn from(value: AislingPalette) -> Self {
200 value.theme()
201 }
202}
203
204impl From<Theme> for AislingPalette {
205 fn from(value: Theme) -> Self {
206 Self::from_theme(value)
207 }
208}
209
210#[derive(Clone, Copy, Debug, Eq, PartialEq)]
212pub struct AislingEffect {
213 tick: u64,
214 intensity: u16,
215 palette: AislingPalette,
216 shimmer: bool,
217 scanlines: bool,
218 glow: bool,
219}
220
221impl AislingEffect {
222 #[must_use]
224 pub fn new(tick: u64) -> Self {
225 Self {
226 tick,
227 ..Self::default()
228 }
229 }
230
231 #[must_use]
233 pub fn tick(mut self, tick: u64) -> Self {
234 self.tick = tick;
235 self
236 }
237
238 #[must_use]
240 pub fn palette(mut self, palette: AislingPalette) -> Self {
241 self.palette = palette;
242 self
243 }
244
245 #[must_use]
247 pub fn intensity(mut self, intensity: u16) -> Self {
248 self.intensity = intensity.min(10);
249 self
250 }
251
252 #[must_use]
254 pub fn shimmer(mut self, enabled: bool) -> Self {
255 self.shimmer = enabled;
256 self
257 }
258
259 #[must_use]
261 pub fn scanlines(mut self, enabled: bool) -> Self {
262 self.scanlines = enabled;
263 self
264 }
265
266 #[must_use]
268 pub fn glow(mut self, enabled: bool) -> Self {
269 self.glow = enabled;
270 self
271 }
272
273 pub fn apply(self, area: Rect, buf: &mut Buffer) {
275 if is_empty(area) || self.intensity == 0 {
276 return;
277 }
278
279 let right = area.x.saturating_add(area.width);
280 let bottom = area.y.saturating_add(area.height);
281 let edge_phase = self.tick / 2;
282 let shimmer_gate = 11_u64.saturating_sub(u64::from(self.intensity.min(10)));
283
284 for y in area.y..bottom {
285 for x in area.x..right {
286 if self.scanlines && (u64::from(y) + edge_phase).is_multiple_of(3) {
287 set_cell_bg(buf, x, y, self.palette.shadow);
288 }
289
290 if self.shimmer {
291 let phase = u64::from(x) * 3 + u64::from(y) * 5 + self.tick;
292 if phase % 11 >= shimmer_gate {
293 set_cell_style(
294 buf,
295 x,
296 y,
297 Style::default()
298 .fg(self.palette.lane(phase))
299 .add_modifier(Modifier::BOLD),
300 );
301 }
302 }
303
304 if self.glow
305 && is_edge(area, x, y)
306 && (u64::from(x) + u64::from(y) + edge_phase) % 5 == 0
307 {
308 set_cell_style(
309 buf,
310 x,
311 y,
312 Style::default()
313 .fg(self.palette.pulse)
314 .add_modifier(Modifier::BOLD),
315 );
316 }
317 }
318 }
319 }
320}
321
322impl Default for AislingEffect {
323 fn default() -> Self {
324 Self {
325 tick: 0,
326 intensity: 5,
327 palette: AislingPalette::default(),
328 shimmer: true,
329 scanlines: true,
330 glow: true,
331 }
332 }
333}
334
335#[derive(Clone, Debug, Eq, PartialEq)]
337pub struct Aisling<W> {
338 inner: W,
339 effect: AislingEffect,
340}
341
342impl<W> Aisling<W> {
343 #[must_use]
345 pub fn new(inner: W) -> Self {
346 Self {
347 inner,
348 effect: AislingEffect::default(),
349 }
350 }
351
352 #[must_use]
354 pub fn effect(mut self, effect: AislingEffect) -> Self {
355 self.effect = effect;
356 self
357 }
358
359 #[must_use]
361 pub fn tick(mut self, tick: u64) -> Self {
362 self.effect = self.effect.tick(tick);
363 self
364 }
365
366 #[must_use]
368 pub fn palette(mut self, palette: AislingPalette) -> Self {
369 self.effect = self.effect.palette(palette);
370 self
371 }
372
373 #[must_use]
375 pub fn intensity(mut self, intensity: u16) -> Self {
376 self.effect = self.effect.intensity(intensity);
377 self
378 }
379}
380
381impl<W: Widget> Widget for Aisling<W> {
382 fn render(&self, buf: &mut Buffer, area: Rect) {
383 self.inner.render(buf, area);
384 self.effect.apply(area, buf);
385 }
386}
387
388pub trait AislingExt: Widget + Sized {
390 #[must_use]
392 fn aisling(self) -> Aisling<Self> {
393 Aisling::new(self)
394 }
395}
396
397impl<W: Widget> AislingExt for W {}
398
399#[derive(Clone, Debug)]
401pub struct GlyphRain<'a> {
402 tick: u64,
403 density: u16,
404 glyphs: Cow<'a, str>,
405 palette: AislingPalette,
406 block: Option<Block<'a>>,
407}
408
409impl PartialEq for GlyphRain<'_> {
410 fn eq(&self, other: &Self) -> bool {
411 self.tick == other.tick
412 && self.density == other.density
413 && self.glyphs == other.glyphs
414 && self.palette == other.palette
415 && option_block_eq(self.block.as_ref(), other.block.as_ref())
416 }
417}
418
419impl Eq for GlyphRain<'_> {}
420
421impl<'a> GlyphRain<'a> {
422 #[must_use]
424 pub fn new(tick: u64) -> Self {
425 Self {
426 tick,
427 density: 34,
428 glyphs: Cow::Borrowed("01#$*+<>[]{}"),
429 palette: AislingPalette::phosphor(),
430 block: None,
431 }
432 }
433
434 #[must_use]
436 pub fn tick(mut self, tick: u64) -> Self {
437 self.tick = tick;
438 self
439 }
440
441 #[must_use]
443 pub fn density(mut self, density: u16) -> Self {
444 self.density = density.min(100);
445 self
446 }
447
448 #[must_use]
450 pub fn glyphs(mut self, glyphs: impl Into<Cow<'a, str>>) -> Self {
451 self.glyphs = glyphs.into();
452 self
453 }
454
455 #[must_use]
457 pub fn palette(mut self, palette: AislingPalette) -> Self {
458 self.palette = palette;
459 self
460 }
461
462 #[must_use]
464 pub fn block(mut self, block: Block<'a>) -> Self {
465 self.block = Some(block);
466 self
467 }
468}
469
470impl Widget for GlyphRain<'_> {
471 fn render(&self, buf: &mut Buffer, area: Rect) {
472 let inner = self
473 .block
474 .as_ref()
475 .map_or(area, |block| block_content_area(block, area));
476 if let Some(block) = &self.block {
477 block.render(buf, area);
478 }
479 if is_empty(inner) || self.density == 0 {
480 return;
481 }
482
483 let glyphs: Vec<char> = self.glyphs.chars().collect();
484 if glyphs.is_empty() {
485 return;
486 }
487
488 let right = inner.x.saturating_add(inner.width);
489 let bottom = inner.y.saturating_add(inner.height);
490 for y in inner.y..bottom {
491 for x in inner.x..right {
492 let noise = field_noise(x, y, self.tick);
493 if noise % 100 >= u64::from(self.density) {
494 continue;
495 }
496
497 let glyph = glyphs[(noise as usize + usize::from(y)) % glyphs.len()];
498 let head = (noise + self.tick) % 9 == 0;
499 let style = if head {
500 Style::default()
501 .fg(self.palette.high)
502 .add_modifier(Modifier::BOLD)
503 } else {
504 Style::default().fg(self.palette.lane(noise + self.tick))
505 };
506
507 set_styled_char(buf, x, y, glyph, style);
508 }
509 }
510 }
511}
512
513#[derive(Clone, Debug)]
515pub struct NebulaGauge<'a> {
516 ratio: f64,
517 tick: u64,
518 label: Option<Cow<'a, str>>,
519 palette: AislingPalette,
520 block: Option<Block<'a>>,
521}
522
523impl PartialEq for NebulaGauge<'_> {
524 fn eq(&self, other: &Self) -> bool {
525 self.ratio == other.ratio
526 && self.tick == other.tick
527 && self.label == other.label
528 && self.palette == other.palette
529 && option_block_eq(self.block.as_ref(), other.block.as_ref())
530 }
531}
532
533impl<'a> NebulaGauge<'a> {
534 #[must_use]
536 pub fn new(ratio: f64) -> Self {
537 Self {
538 ratio: ratio.clamp(0.0, 1.0),
539 tick: 0,
540 label: None,
541 palette: AislingPalette::dream(),
542 block: None,
543 }
544 }
545
546 #[must_use]
548 pub fn ratio(&self) -> f64 {
549 self.ratio
550 }
551
552 #[must_use]
554 pub fn tick(mut self, tick: u64) -> Self {
555 self.tick = tick;
556 self
557 }
558
559 #[must_use]
561 pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
562 self.label = Some(label.into());
563 self
564 }
565
566 #[must_use]
568 pub fn palette(mut self, palette: AislingPalette) -> Self {
569 self.palette = palette;
570 self
571 }
572
573 #[must_use]
575 pub fn block(mut self, block: Block<'a>) -> Self {
576 self.block = Some(block);
577 self
578 }
579}
580
581impl Widget for NebulaGauge<'_> {
582 fn render(&self, buf: &mut Buffer, area: Rect) {
583 let inner = self
584 .block
585 .as_ref()
586 .map_or(area, |block| block_content_area(block, area));
587 if let Some(block) = &self.block {
588 block.render(buf, area);
589 }
590 if is_empty(inner) {
591 return;
592 }
593
594 let right = inner.x.saturating_add(inner.width);
595 let bottom = inner.y.saturating_add(inner.height);
596 let filled = (f64::from(inner.width) * self.ratio).round() as u16;
597
598 for y in inner.y..bottom {
599 for x in inner.x..right {
600 let offset = x.saturating_sub(inner.x);
601 let flow = u64::from(offset) + u64::from(y) * 2 + self.tick;
602 if offset < filled {
603 set_styled_char(
604 buf,
605 x,
606 y,
607 '█',
608 Style::default()
609 .fg(self.palette.lane(flow))
610 .bg(self.palette.shadow)
611 .add_modifier(Modifier::BOLD),
612 );
613 } else {
614 set_styled_char(buf, x, y, '░', Style::default().fg(self.palette.shadow));
615 }
616 }
617 }
618
619 if let Some(label) = &self.label {
620 let row = inner.y + inner.height / 2;
621 let label_width = label.chars().count().min(usize::from(inner.width)) as u16;
622 let start = inner.x + inner.width.saturating_sub(label_width) / 2;
623 paint_text(
624 Rect::new(start, row, label_width, 1),
625 buf,
626 label.as_ref(),
627 Style::default()
628 .fg(self.palette.high)
629 .add_modifier(Modifier::BOLD),
630 );
631 }
632 }
633}
634
635#[derive(Clone, Debug, Eq, PartialEq)]
637pub struct SignalPanel<'a> {
638 title: Cow<'a, str>,
639 lines: Vec<Cow<'a, str>>,
640 tick: u64,
641 palette: AislingPalette,
642}
643
644impl<'a> SignalPanel<'a> {
645 #[must_use]
647 pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
648 Self {
649 title: title.into(),
650 lines: Vec::new(),
651 tick: 0,
652 palette: AislingPalette::flare(),
653 }
654 }
655
656 #[must_use]
658 pub fn line(mut self, line: impl Into<Cow<'a, str>>) -> Self {
659 self.lines.push(line.into());
660 self
661 }
662
663 #[must_use]
665 pub fn lines<I, S>(mut self, lines: I) -> Self
666 where
667 I: IntoIterator<Item = S>,
668 S: Into<Cow<'a, str>>,
669 {
670 self.lines = lines.into_iter().map(Into::into).collect();
671 self
672 }
673
674 #[must_use]
676 pub fn tick(mut self, tick: u64) -> Self {
677 self.tick = tick;
678 self
679 }
680
681 #[must_use]
683 pub fn palette(mut self, palette: AislingPalette) -> Self {
684 self.palette = palette;
685 self
686 }
687}
688
689impl Widget for SignalPanel<'_> {
690 fn render(&self, buf: &mut Buffer, area: Rect) {
691 if is_empty(area) {
692 return;
693 }
694
695 let block = Block::new(self.title.as_ref())
696 .with_borders(BorderStyle::Plain)
697 .with_border_color(self.palette.mid)
698 .with_inner_margin(Rect::ZERO);
699 let inner = block_content_area(&block, area);
700 block.render(buf, area);
701 if is_empty(inner) {
702 return;
703 }
704
705 let bars_width = inner.width.min(12);
706 let text_width = inner.width.saturating_sub(bars_width.saturating_add(1));
707 let max_lines = usize::from(inner.height);
708
709 for (index, line) in self.lines.iter().take(max_lines).enumerate() {
710 paint_text(
711 Rect::new(inner.x, inner.y + index as u16, text_width, 1),
712 buf,
713 line.as_ref(),
714 Style::default().fg(self.palette.high),
715 );
716 }
717
718 if bars_width == 0 {
719 return;
720 }
721
722 let bars_x = inner.x + inner.width.saturating_sub(bars_width);
723 for row in 0..inner.height {
724 for column in 0..bars_width {
725 let x = bars_x + column;
726 let y = inner.y + row;
727 let noise = field_noise(x, y, self.tick / 2);
728 let active = (noise + self.tick + u64::from(column)) % 7 <= 3;
729 let symbol = if active { '╱' } else { '·' };
730 let style = if active {
731 Style::default()
732 .fg(self.palette.lane(noise))
733 .add_modifier(Modifier::BOLD)
734 } else {
735 Style::default().fg(self.palette.shadow)
736 };
737 set_styled_char(buf, x, y, symbol, style);
738 }
739 }
740 }
741}
742
743#[derive(Clone, Debug)]
745pub struct FlickerPanel<'a> {
746 text: Cow<'a, str>,
747 tick: u64,
748 intensity: u16,
749 palette: AislingPalette,
750 block: Option<Block<'a>>,
751}
752
753impl PartialEq for FlickerPanel<'_> {
754 fn eq(&self, other: &Self) -> bool {
755 self.text == other.text
756 && self.tick == other.tick
757 && self.intensity == other.intensity
758 && self.palette == other.palette
759 && option_block_eq(self.block.as_ref(), other.block.as_ref())
760 }
761}
762
763impl Eq for FlickerPanel<'_> {}
764
765impl<'a> FlickerPanel<'a> {
766 #[must_use]
768 pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
769 Self {
770 text: text.into(),
771 tick: 0,
772 intensity: 5,
773 palette: AislingPalette::dream(),
774 block: None,
775 }
776 }
777
778 #[must_use]
780 pub fn tick(mut self, tick: u64) -> Self {
781 self.tick = tick;
782 self
783 }
784
785 #[must_use]
787 pub fn intensity(mut self, intensity: u16) -> Self {
788 self.intensity = intensity.min(10);
789 self
790 }
791
792 #[must_use]
794 pub fn palette(mut self, palette: AislingPalette) -> Self {
795 self.palette = palette;
796 self
797 }
798
799 #[must_use]
801 pub fn block(mut self, block: Block<'a>) -> Self {
802 self.block = Some(block);
803 self
804 }
805}
806
807impl Widget for FlickerPanel<'_> {
808 fn render(&self, buf: &mut Buffer, area: Rect) {
809 let inner = self
810 .block
811 .as_ref()
812 .map_or(area, |block| block_content_area(block, area));
813 if let Some(block) = &self.block {
814 block.render(buf, area);
815 }
816 if is_empty(inner) || self.intensity == 0 {
817 return;
818 }
819
820 let glitch_chars: Vec<char> = "░▒▓█▀▄▌▐│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬".chars().collect();
821 let text_chars: Vec<char> = self.text.chars().collect();
822 if text_chars.is_empty() {
823 return;
824 }
825
826 let right = inner.x.saturating_add(inner.width);
827 let bottom = inner.y.saturating_add(inner.height);
828 for y in inner.y..bottom {
829 for x in inner.x..right {
830 let col = usize::from(x.saturating_sub(inner.x));
831 let noise = field_noise(x, y, self.tick);
832 let glitch_gate = 11_u64.saturating_sub(u64::from(self.intensity));
833
834 let (ch, style) = if noise % 11 >= glitch_gate {
835 let g = glitch_chars[(noise as usize) % glitch_chars.len()];
836 (
837 g,
838 Style::default()
839 .fg(self.palette.pulse)
840 .add_modifier(Modifier::BOLD),
841 )
842 } else if col < text_chars.len() {
843 let c = text_chars[col];
844 let flicker = (noise + self.tick) % 9 == 0;
845 let style = if flicker {
846 Style::default()
847 .fg(self.palette.high)
848 .add_modifier(Modifier::BOLD)
849 } else {
850 Style::default().fg(self.palette.mid)
851 };
852 (c, style)
853 } else {
854 (' ', Style::default())
855 };
856 set_styled_char(buf, x, y, ch, style);
857 }
858 }
859 }
860}
861
862#[derive(Clone, Debug)]
864pub struct Waveform<'a> {
865 tick: u64,
866 frequency: f64,
867 amplitude: f64,
868 wave_type: WaveType,
869 palette: AislingPalette,
870 block: Option<Block<'a>>,
871}
872
873#[derive(Clone, Copy, Debug, Eq, PartialEq)]
874pub enum WaveType {
875 Sine,
876 Square,
877 Sawtooth,
878 Triangle,
879}
880
881impl PartialEq for Waveform<'_> {
882 fn eq(&self, other: &Self) -> bool {
883 self.tick == other.tick
884 && self.frequency == other.frequency
885 && self.amplitude == other.amplitude
886 && self.wave_type == other.wave_type
887 && self.palette == other.palette
888 && option_block_eq(self.block.as_ref(), other.block.as_ref())
889 }
890}
891
892impl Eq for Waveform<'_> {}
893
894impl<'a> Waveform<'a> {
895 #[must_use]
897 pub fn new(frequency: f64, amplitude: f64) -> Self {
898 Self {
899 tick: 0,
900 frequency,
901 amplitude: amplitude.clamp(0.0, 1.0),
902 wave_type: WaveType::Sine,
903 palette: AislingPalette::phosphor(),
904 block: None,
905 }
906 }
907
908 #[must_use]
910 pub fn tick(mut self, tick: u64) -> Self {
911 self.tick = tick;
912 self
913 }
914
915 #[must_use]
917 pub fn wave_type(mut self, wave_type: WaveType) -> Self {
918 self.wave_type = wave_type;
919 self
920 }
921
922 #[must_use]
924 pub fn palette(mut self, palette: AislingPalette) -> Self {
925 self.palette = palette;
926 self
927 }
928
929 #[must_use]
931 pub fn block(mut self, block: Block<'a>) -> Self {
932 self.block = Some(block);
933 self
934 }
935
936 fn sample(&self, phase: f64) -> f64 {
937 let t = phase.fract();
938 match self.wave_type {
939 WaveType::Sine => (std::f64::consts::TAU * t).sin(),
940 WaveType::Square => {
941 if t < 0.5 {
942 1.0
943 } else {
944 -1.0
945 }
946 }
947 WaveType::Sawtooth => 2.0 * t - 1.0,
948 WaveType::Triangle => {
949 if t < 0.5 {
950 4.0 * t - 1.0
951 } else {
952 3.0 - 4.0 * t
953 }
954 }
955 }
956 }
957}
958
959impl Widget for Waveform<'_> {
960 fn render(&self, buf: &mut Buffer, area: Rect) {
961 let inner = self
962 .block
963 .as_ref()
964 .map_or(area, |block| block_content_area(block, area));
965 if let Some(block) = &self.block {
966 block.render(buf, area);
967 }
968 if is_empty(inner) || inner.height < 3 {
969 return;
970 }
971
972 let mid_y = inner.y + inner.height / 2;
973 let half = (inner.height / 2) as f64;
974
975 for col in 0..inner.width {
976 let phase = f64::from(col) / f64::from(inner.width) * self.frequency
977 + f64::from(self.tick as u32) * 0.05;
978 let sample = self.sample(phase);
979 let offset = (sample * self.amplitude * half).round() as i16;
980
981 let y = mid_y as i16 + offset;
982 if y >= inner.y as i16 && y < (inner.y + inner.height) as i16 {
983 let noise = field_noise(inner.x + col, y as u16, self.tick);
984 set_styled_char(
985 buf,
986 inner.x + col,
987 y as u16,
988 '█',
989 Style::default()
990 .fg(self.palette.lane(noise))
991 .add_modifier(Modifier::BOLD),
992 );
993 }
994 }
995 }
996}
997
998#[derive(Clone, Debug, Eq, PartialEq)]
1000pub struct PulseRing {
1001 tick: u64,
1002 rings: u16,
1003 palette: AislingPalette,
1004}
1005
1006impl PulseRing {
1007 #[must_use]
1009 pub fn new(rings: u16) -> Self {
1010 Self {
1011 tick: 0,
1012 rings: rings.max(1),
1013 palette: AislingPalette::dream(),
1014 }
1015 }
1016
1017 #[must_use]
1019 pub fn tick(mut self, tick: u64) -> Self {
1020 self.tick = tick;
1021 self
1022 }
1023
1024 #[must_use]
1026 pub fn palette(mut self, palette: AislingPalette) -> Self {
1027 self.palette = palette;
1028 self
1029 }
1030}
1031
1032impl Widget for PulseRing {
1033 fn render(&self, buf: &mut Buffer, area: Rect) {
1034 if is_empty(area) || self.rings == 0 {
1035 return;
1036 }
1037
1038 let cx = area.x + area.width / 2;
1039 let cy = area.y + area.height / 2;
1040 let max_radius = (area.width.min(area.height) / 2) as f64;
1041 if max_radius < 1.0 {
1042 return;
1043 }
1044
1045 let right = area.x.saturating_add(area.width);
1046 let bottom = area.y.saturating_add(area.height);
1047
1048 for y in area.y..bottom {
1049 for x in area.x..right {
1050 let dx = x as f64 - cx as f64;
1051 let dy = y as f64 - cy as f64;
1052 let dist = (dx * dx + dy * dy).sqrt();
1053
1054 for ring in 0..self.rings {
1055 let ring_phase = (self.tick as f64 * 0.1 + ring as f64 * 3.0) % max_radius;
1056 let diff = (dist - ring_phase).abs();
1057 if diff < 1.5 {
1058 let noise = field_noise(x, y, self.tick + ring as u64);
1059 let style = if diff < 0.8 {
1060 Style::default()
1061 .fg(self.palette.high)
1062 .add_modifier(Modifier::BOLD)
1063 } else {
1064 Style::default().fg(self.palette.lane(noise))
1065 };
1066 set_styled_char(buf, x, y, '○', style);
1067 break;
1068 }
1069 }
1070 }
1071 }
1072 }
1073}
1074
1075#[derive(Clone, Debug, Eq, PartialEq)]
1077pub struct Radar {
1078 tick: u64,
1079 sweep_speed: u64,
1080 palette: AislingPalette,
1081}
1082
1083impl Radar {
1084 #[must_use]
1086 pub fn new(sweep_speed: u64) -> Self {
1087 Self {
1088 tick: 0,
1089 sweep_speed: sweep_speed.max(1),
1090 palette: AislingPalette::phosphor(),
1091 }
1092 }
1093
1094 #[must_use]
1096 pub fn tick(mut self, tick: u64) -> Self {
1097 self.tick = tick;
1098 self
1099 }
1100
1101 #[must_use]
1103 pub fn palette(mut self, palette: AislingPalette) -> Self {
1104 self.palette = palette;
1105 self
1106 }
1107}
1108
1109impl Widget for Radar {
1110 fn render(&self, buf: &mut Buffer, area: Rect) {
1111 if is_empty(area) {
1112 return;
1113 }
1114
1115 let cx = area.x + area.width / 2;
1116 let cy = area.y + area.height / 2;
1117 let max_r = (area.width.min(area.height) / 2) as f64;
1118 if max_r < 1.0 {
1119 return;
1120 }
1121
1122 let sweep_angle = (self.tick as f64 / self.sweep_speed as f64) % (std::f64::consts::TAU);
1123 let trail_len = std::f64::consts::PI * 0.6;
1124
1125 let right = area.x.saturating_add(area.width);
1126 let bottom = area.y.saturating_add(area.height);
1127
1128 for y in area.y..bottom {
1129 for x in area.x..right {
1130 let dx = x as f64 - cx as f64;
1131 let dy = y as f64 - cy as f64;
1132 let dist = (dx * dx + dy * dy).sqrt();
1133
1134 if dist > max_r {
1135 continue;
1136 }
1137
1138 let angle = dy.atan2(dx);
1139 let norm_angle = if angle < 0.0 {
1140 angle + std::f64::consts::TAU
1141 } else {
1142 angle
1143 };
1144
1145 let ring = dist as u16;
1146 if ring > 0 && dist % (max_r / 3.0) < 0.5 {
1147 set_styled_char(buf, x, y, '·', Style::default().fg(self.palette.shadow));
1148 continue;
1149 }
1150
1151 let diff = (norm_angle - sweep_angle).abs();
1152 let diff = if diff > std::f64::consts::PI {
1153 std::f64::consts::TAU - diff
1154 } else {
1155 diff
1156 };
1157
1158 if diff < trail_len {
1159 let fade = 1.0 - diff / trail_len;
1160 let noise = field_noise(x, y, self.tick);
1161 let color = if fade > 0.6 {
1162 self.palette.high
1163 } else {
1164 self.palette.lane(noise)
1165 };
1166 set_styled_char(
1167 buf,
1168 x,
1169 y,
1170 if dist < 1.0 { '●' } else { '·' },
1171 Style::default().fg(color).add_modifier(Modifier::BOLD),
1172 );
1173 } else if (dist - 1.0).abs() < 0.5
1174 || (dist - max_r * 0.5).abs() < 0.5
1175 || (dist - max_r * 0.9).abs() < 0.5
1176 {
1177 set_styled_char(buf, x, y, '·', Style::default().fg(self.palette.shadow));
1178 }
1179 }
1180 }
1181 }
1182}
1183
1184#[derive(Clone, Debug)]
1186pub struct OrbField<'a> {
1187 tick: u64,
1188 count: u16,
1189 palette: AislingPalette,
1190 block: Option<Block<'a>>,
1191}
1192
1193impl PartialEq for OrbField<'_> {
1194 fn eq(&self, other: &Self) -> bool {
1195 self.tick == other.tick
1196 && self.count == other.count
1197 && self.palette == other.palette
1198 && option_block_eq(self.block.as_ref(), other.block.as_ref())
1199 }
1200}
1201
1202impl Eq for OrbField<'_> {}
1203
1204impl<'a> OrbField<'a> {
1205 #[must_use]
1207 pub fn new(count: u16) -> Self {
1208 Self {
1209 tick: 0,
1210 count,
1211 palette: AislingPalette::dream(),
1212 block: None,
1213 }
1214 }
1215
1216 #[must_use]
1218 pub fn tick(mut self, tick: u64) -> Self {
1219 self.tick = tick;
1220 self
1221 }
1222
1223 #[must_use]
1225 pub fn palette(mut self, palette: AislingPalette) -> Self {
1226 self.palette = palette;
1227 self
1228 }
1229
1230 #[must_use]
1232 pub fn block(mut self, block: Block<'a>) -> Self {
1233 self.block = Some(block);
1234 self
1235 }
1236}
1237
1238impl Widget for OrbField<'_> {
1239 fn render(&self, buf: &mut Buffer, area: Rect) {
1240 let inner = self
1241 .block
1242 .as_ref()
1243 .map_or(area, |block| block_content_area(block, area));
1244 if let Some(block) = &self.block {
1245 block.render(buf, area);
1246 }
1247 if is_empty(inner) || self.count == 0 {
1248 return;
1249 }
1250
1251 for i in 0..u64::from(self.count) {
1252 let seed = field_noise(i as u16, i as u16 / 7, i);
1253 let speed_x = (seed % 7) as f64 * 0.3 + 0.2;
1254 let speed_y = ((seed >> 3) % 5) as f64 * 0.2 + 0.1;
1255 let phase_x = seed as f64 * 0.1;
1256 let phase_y = (seed >> 5) as f64 * 0.13;
1257
1258 let base_x = (inner.x as f64)
1259 + ((self.tick as f64 * speed_x * 0.02 + phase_x).sin() * 0.5 + 0.5)
1260 * inner.width as f64;
1261 let base_y = (inner.y as f64)
1262 + ((self.tick as f64 * speed_y * 0.02 + phase_y).cos() * 0.5 + 0.5)
1263 * inner.height as f64;
1264
1265 let px = base_x.round() as u16;
1266 let py = base_y.round() as u16;
1267
1268 if px >= inner.x
1269 && px < inner.x + inner.width
1270 && py >= inner.y
1271 && py < inner.y + inner.height
1272 {
1273 let noise = field_noise(px, py, self.tick + i);
1274 let glyph = if noise % 3 == 0 {
1275 '◆'
1276 } else if noise % 3 == 1 {
1277 '◇'
1278 } else {
1279 '•'
1280 };
1281 set_styled_char(
1282 buf,
1283 px,
1284 py,
1285 glyph,
1286 Style::default()
1287 .fg(self.palette.lane(i))
1288 .add_modifier(Modifier::BOLD),
1289 );
1290 }
1291 }
1292 }
1293}
1294
1295#[derive(Clone, Debug)]
1297pub struct NeonBorder<'a> {
1298 tick: u64,
1299 speed: u64,
1300 palette: AislingPalette,
1301 inner: Block<'a>,
1302}
1303
1304impl PartialEq for NeonBorder<'_> {
1305 fn eq(&self, other: &Self) -> bool {
1306 self.tick == other.tick
1307 && self.speed == other.speed
1308 && self.palette == other.palette
1309 && block_eq(&self.inner, &other.inner)
1310 }
1311}
1312
1313impl<'a> NeonBorder<'a> {
1314 #[must_use]
1316 pub fn new(inner: Block<'a>) -> Self {
1317 Self {
1318 tick: 0,
1319 speed: 3,
1320 palette: AislingPalette::dream(),
1321 inner,
1322 }
1323 }
1324
1325 #[must_use]
1327 pub fn tick(mut self, tick: u64) -> Self {
1328 self.tick = tick;
1329 self
1330 }
1331
1332 #[must_use]
1334 pub fn speed(mut self, speed: u64) -> Self {
1335 self.speed = speed.max(1);
1336 self
1337 }
1338
1339 #[must_use]
1341 pub fn palette(mut self, palette: AislingPalette) -> Self {
1342 self.palette = palette;
1343 self
1344 }
1345
1346 pub fn render_border(&self, buf: &mut Buffer, area: Rect) -> Rect {
1348 let inner = block_content_area(&self.inner, area);
1349
1350 let right = area.x.saturating_add(area.width);
1351 let bottom = area.y.saturating_add(area.height);
1352 let perimeter = 2 * (area.width + area.height) as u64;
1353
1354 for y in area.y..bottom {
1355 for x in area.x..right {
1356 if !is_edge(area, x, y) {
1357 continue;
1358 }
1359
1360 let pos = if y == area.y {
1361 u64::from(x - area.x)
1362 } else if x + 1 == right {
1363 u64::from(area.width) + u64::from(y - area.y)
1364 } else if y + 1 == bottom {
1365 u64::from(area.width + area.height) + u64::from(right - x - 1)
1366 } else {
1367 u64::from(2 * area.width + area.height) + u64::from(bottom - y - 1)
1368 };
1369
1370 let phase = (self.tick * self.speed + pos) % perimeter;
1371 let color_idx = (phase * 4 / perimeter) as usize;
1372 let color = match color_idx {
1373 0 => self.palette.low,
1374 1 => self.palette.mid,
1375 2 => self.palette.high,
1376 _ => self.palette.pulse,
1377 };
1378
1379 let ch = if y == area.y || y + 1 == bottom {
1380 '─'
1381 } else {
1382 '│'
1383 };
1384 set_styled_char(
1385 buf,
1386 x,
1387 y,
1388 ch,
1389 Style::default().fg(color).add_modifier(Modifier::BOLD),
1390 );
1391 }
1392 }
1393
1394 inner
1395 }
1396}
1397
1398impl Widget for NeonBorder<'_> {
1399 fn render(&self, buf: &mut Buffer, area: Rect) {
1400 self.render_border(buf, area);
1401 }
1402}
1403
1404#[derive(Clone, Debug)]
1413pub struct StreamPanel<'a> {
1414 lines: Vec<Cow<'a, str>>,
1415 scroll_offset: u16,
1416 follow_tail: bool,
1417 show_line_numbers: bool,
1418 tick: u64,
1419 palette: AislingPalette,
1420 block: Option<Block<'a>>,
1421}
1422
1423impl PartialEq for StreamPanel<'_> {
1424 fn eq(&self, other: &Self) -> bool {
1425 self.lines == other.lines
1426 && self.scroll_offset == other.scroll_offset
1427 && self.follow_tail == other.follow_tail
1428 && self.show_line_numbers == other.show_line_numbers
1429 && self.tick == other.tick
1430 && self.palette == other.palette
1431 && option_block_eq(self.block.as_ref(), other.block.as_ref())
1432 }
1433}
1434
1435impl<'a> StreamPanel<'a> {
1436 #[must_use]
1438 pub fn new() -> Self {
1439 Self {
1440 lines: Vec::new(),
1441 scroll_offset: 0,
1442 follow_tail: true,
1443 show_line_numbers: false,
1444 tick: 0,
1445 palette: AislingPalette::phosphor(),
1446 block: None,
1447 }
1448 }
1449
1450 #[must_use]
1452 pub fn lines<I, S>(mut self, lines: I) -> Self
1453 where
1454 I: IntoIterator<Item = S>,
1455 S: Into<Cow<'a, str>>,
1456 {
1457 self.lines = lines.into_iter().map(Into::into).collect();
1458 self
1459 }
1460
1461 #[must_use]
1463 pub fn push_line(mut self, line: impl Into<Cow<'a, str>>) -> Self {
1464 self.lines.push(line.into());
1465 self
1466 }
1467
1468 #[must_use]
1470 pub fn scroll_offset(mut self, offset: u16) -> Self {
1471 self.scroll_offset = offset;
1472 self
1473 }
1474
1475 #[must_use]
1477 pub fn follow_tail(mut self, follow: bool) -> Self {
1478 self.follow_tail = follow;
1479 self
1480 }
1481
1482 #[must_use]
1484 pub fn show_line_numbers(mut self, show: bool) -> Self {
1485 self.show_line_numbers = show;
1486 self
1487 }
1488
1489 #[must_use]
1491 pub fn tick(mut self, tick: u64) -> Self {
1492 self.tick = tick;
1493 self
1494 }
1495
1496 #[must_use]
1498 pub fn palette(mut self, palette: AislingPalette) -> Self {
1499 self.palette = palette;
1500 self
1501 }
1502
1503 #[must_use]
1505 pub fn block(mut self, block: Block<'a>) -> Self {
1506 self.block = Some(block);
1507 self
1508 }
1509
1510 pub fn render_with_interaction(
1512 &self,
1513 frame: &mut Frame<'_>,
1514 id: impl Into<WidgetId>,
1515 area: Rect,
1516 ) {
1517 self.render(frame.buffer(), area);
1518 for region in self.hit_regions(id, area) {
1519 frame.register_hit_region(region);
1520 }
1521 frame.mark_dirty(area);
1522 }
1523
1524 #[must_use]
1526 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
1527 let region_id = id.into();
1528 let inner = self
1529 .block
1530 .as_ref()
1531 .map_or(area, |block| block_content_area(block, area));
1532 if is_empty(area) || is_empty(inner) {
1533 return Vec::new();
1534 }
1535
1536 let mut regions = vec![
1537 HitRegion::new(region_id.clone(), area)
1538 .with_role(WidgetRole::Transcript)
1539 .with_label("stream")
1540 .with_value(WidgetValue::Count(self.lines.len())),
1541 ];
1542 let start = self.visible_start(inner.height);
1543
1544 for row in 0..inner.height {
1545 let line_idx = start + row as usize;
1546 if line_idx >= self.lines.len() {
1547 break;
1548 }
1549
1550 let row_area = Rect::new(inner.x, inner.y + row, inner.width, 1);
1551 regions.push(
1552 HitRegion::new(format!("{}:line:{line_idx}", region_id.as_ref()), row_area)
1553 .with_role(WidgetRole::TranscriptRow)
1554 .with_label(self.lines[line_idx].as_ref())
1555 .with_action(WidgetAction::Select)
1556 .with_cursor(MouseCursor::Text)
1557 .with_row(line_idx)
1558 .with_value(WidgetValue::LineNumber(line_idx + 1))
1559 .with_z_index(1),
1560 );
1561 }
1562
1563 regions
1564 }
1565
1566 #[must_use]
1568 pub fn line_count(&self) -> usize {
1569 self.lines.len()
1570 }
1571
1572 fn visible_start(&self, visible_height: u16) -> usize {
1574 let total = self.lines.len() as u16;
1575 if self.follow_tail || self.scroll_offset == 0 {
1576 let shown = visible_height.min(total);
1577 (total - shown) as usize
1578 } else {
1579 let max_top = total.saturating_sub(visible_height);
1580 (max_top.saturating_sub(self.scroll_offset)) as usize
1581 }
1582 }
1583}
1584
1585impl Default for StreamPanel<'_> {
1586 fn default() -> Self {
1587 Self::new()
1588 }
1589}
1590
1591impl Widget for StreamPanel<'_> {
1592 fn render(&self, buf: &mut Buffer, area: Rect) {
1593 let inner = self
1594 .block
1595 .as_ref()
1596 .map_or(area, |block| block_content_area(block, area));
1597 if let Some(block) = &self.block {
1598 block.render(buf, area);
1599 }
1600 if is_empty(inner) {
1601 return;
1602 }
1603
1604 let gutter_width = if self.show_line_numbers {
1605 let max_num = self.lines.len().max(1);
1606 let digits = format!("{max_num}").len() as u16;
1607 digits + 1
1608 } else {
1609 0
1610 };
1611
1612 let text_width = inner.width.saturating_sub(gutter_width);
1613 if text_width == 0 {
1614 return;
1615 }
1616
1617 let start = self.visible_start(inner.height);
1618 let right = inner.x.saturating_add(inner.width);
1619 let total = self.lines.len();
1620
1621 for row in 0..inner.height {
1622 let line_idx = start + row as usize;
1623 let y = inner.y + row;
1624
1625 if line_idx >= total {
1626 break;
1627 }
1628
1629 if self.show_line_numbers {
1630 let num_str = format!(
1631 "{:>width$}",
1632 line_idx + 1,
1633 width = (gutter_width - 1) as usize
1634 );
1635 paint_text(
1636 Rect::new(inner.x, y, gutter_width.saturating_sub(1), 1),
1637 buf,
1638 &num_str,
1639 Style::default().fg(self.palette.shadow),
1640 );
1641 set_styled_char(
1642 buf,
1643 inner.x + gutter_width - 1,
1644 y,
1645 '│',
1646 Style::default().fg(self.palette.shadow),
1647 );
1648 }
1649
1650 let line = &self.lines[line_idx];
1651 let text_chars: Vec<char> = line.chars().collect();
1652
1653 for col in 0..text_width {
1654 let x = inner.x + gutter_width + col;
1655 if x >= right {
1656 break;
1657 }
1658 let ch = text_chars.get(col as usize).copied().unwrap_or(' ');
1659 let noise = field_noise(x, y, self.tick);
1660 let style = if ch == ' ' {
1661 Style::default()
1662 } else if (noise + self.tick) % 31 == 0 {
1663 Style::default()
1664 .fg(self.palette.pulse)
1665 .add_modifier(Modifier::BOLD)
1666 } else {
1667 Style::default().fg(self.palette.high)
1668 };
1669 set_styled_char(buf, x, y, ch, style);
1670 }
1671 }
1672
1673 if !self.follow_tail && self.scroll_offset > 0 {
1674 let indicator_y = inner.y;
1675 let indicator_style = Style::default()
1676 .fg(self.palette.pulse)
1677 .add_modifier(Modifier::BOLD);
1678 if inner.width > 2 {
1679 set_styled_char(buf, right - 2, indicator_y, '▲', indicator_style);
1680 }
1681 }
1682 }
1683}
1684
1685#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1691pub enum SplitDirection {
1692 Horizontal,
1693 Vertical,
1694}
1695
1696pub struct SplitPane {
1701 ratio: f64,
1702 direction: SplitDirection,
1703 divider: Option<char>,
1704}
1705
1706impl SplitPane {
1707 #[must_use]
1709 pub fn horizontal() -> Self {
1710 Self {
1711 ratio: 0.5,
1712 direction: SplitDirection::Horizontal,
1713 divider: None,
1714 }
1715 }
1716
1717 #[must_use]
1719 pub fn vertical() -> Self {
1720 Self {
1721 ratio: 0.5,
1722 direction: SplitDirection::Vertical,
1723 divider: None,
1724 }
1725 }
1726
1727 #[must_use]
1729 pub fn ratio(mut self, ratio: f64) -> Self {
1730 self.ratio = ratio.clamp(0.0, 1.0);
1731 self
1732 }
1733
1734 #[must_use]
1736 pub fn divider(mut self, divider: char) -> Self {
1737 self.divider = Some(divider);
1738 self
1739 }
1740
1741 pub fn split(&self, area: Rect) -> (Rect, Rect, Rect) {
1744 if is_empty(area) {
1745 return (Rect::ZERO, Rect::ZERO, Rect::ZERO);
1746 }
1747
1748 match self.direction {
1749 SplitDirection::Vertical => {
1750 let has_divider = self.divider.is_some() && area.width > 1;
1751 let available = if has_divider {
1752 area.width.saturating_sub(1)
1753 } else {
1754 area.width
1755 };
1756 let first_width = (f64::from(available) * self.ratio).round() as u16;
1757 let second_width = available.saturating_sub(first_width);
1758
1759 let a = Rect::new(area.x, area.y, first_width, area.height);
1760 let div = if has_divider {
1761 Rect::new(area.x + first_width, area.y, 1, area.height)
1762 } else {
1763 Rect::ZERO
1764 };
1765 let b_x = area.x + first_width + if has_divider { 1 } else { 0 };
1766 let b = Rect::new(b_x, area.y, second_width, area.height);
1767 (a, b, div)
1768 }
1769 SplitDirection::Horizontal => {
1770 let has_divider = self.divider.is_some() && area.height > 1;
1771 let available = if has_divider {
1772 area.height.saturating_sub(1)
1773 } else {
1774 area.height
1775 };
1776 let first_height = (f64::from(available) * self.ratio).round() as u16;
1777 let second_height = available.saturating_sub(first_height);
1778
1779 let a = Rect::new(area.x, area.y, area.width, first_height);
1780 let div = if has_divider {
1781 Rect::new(area.x, area.y + first_height, area.width, 1)
1782 } else {
1783 Rect::ZERO
1784 };
1785 let b_y = area.y + first_height + if has_divider { 1 } else { 0 };
1786 let b = Rect::new(area.x, b_y, area.width, second_height);
1787 (a, b, div)
1788 }
1789 }
1790 }
1791
1792 pub fn render_divider(&self, buf: &mut Buffer, divider_area: Rect, palette: AislingPalette) {
1794 if is_empty(divider_area) {
1795 return;
1796 }
1797 let ch = self.divider.unwrap_or(' ');
1798 let style = Style::default().fg(palette.mid);
1799 for y in divider_area.y..divider_area.y.saturating_add(divider_area.height) {
1800 for x in divider_area.x..divider_area.x.saturating_add(divider_area.width) {
1801 set_styled_char(buf, x, y, ch, style);
1802 }
1803 }
1804 }
1805}
1806
1807#[derive(Clone, Debug)]
1813pub struct List<'a> {
1814 items: Vec<Cow<'a, str>>,
1815 selected: Option<usize>,
1816 scroll_offset: u16,
1817 tick: u64,
1818 palette: AislingPalette,
1819 block: Option<Block<'a>>,
1820}
1821
1822impl PartialEq for List<'_> {
1823 fn eq(&self, other: &Self) -> bool {
1824 self.items == other.items
1825 && self.selected == other.selected
1826 && self.scroll_offset == other.scroll_offset
1827 && self.tick == other.tick
1828 && self.palette == other.palette
1829 && option_block_eq(self.block.as_ref(), other.block.as_ref())
1830 }
1831}
1832
1833impl<'a> List<'a> {
1834 #[must_use]
1836 pub fn new() -> Self {
1837 Self {
1838 items: Vec::new(),
1839 selected: None,
1840 scroll_offset: 0,
1841 tick: 0,
1842 palette: AislingPalette::cypherpunk(),
1843 block: None,
1844 }
1845 }
1846
1847 #[must_use]
1849 pub fn item(mut self, item: impl Into<Cow<'a, str>>) -> Self {
1850 self.items.push(item.into());
1851 self
1852 }
1853
1854 #[must_use]
1856 pub fn items<I, S>(mut self, items: I) -> Self
1857 where
1858 I: IntoIterator<Item = S>,
1859 S: Into<Cow<'a, str>>,
1860 {
1861 self.items = items.into_iter().map(Into::into).collect();
1862 self
1863 }
1864
1865 #[must_use]
1867 pub fn selected(mut self, index: Option<usize>) -> Self {
1868 self.selected = index;
1869 self
1870 }
1871
1872 #[must_use]
1874 pub fn scroll_offset(mut self, offset: u16) -> Self {
1875 self.scroll_offset = offset;
1876 self
1877 }
1878
1879 #[must_use]
1881 pub fn tick(mut self, tick: u64) -> Self {
1882 self.tick = tick;
1883 self
1884 }
1885
1886 #[must_use]
1888 pub fn palette(mut self, palette: AislingPalette) -> Self {
1889 self.palette = palette;
1890 self
1891 }
1892
1893 #[must_use]
1895 pub fn block(mut self, block: Block<'a>) -> Self {
1896 self.block = Some(block);
1897 self
1898 }
1899
1900 pub fn render_with_interaction(
1902 &self,
1903 frame: &mut Frame<'_>,
1904 id: impl Into<WidgetId>,
1905 area: Rect,
1906 ) {
1907 self.render(frame.buffer(), area);
1908 for region in self.hit_regions(id, area) {
1909 frame.register_hit_region(region);
1910 }
1911 frame.mark_dirty(area);
1912 }
1913
1914 #[must_use]
1916 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
1917 let region_id = id.into();
1918 let inner = self
1919 .block
1920 .as_ref()
1921 .map_or(area, |block| block_content_area(block, area));
1922 if is_empty(area) || is_empty(inner) {
1923 return Vec::new();
1924 }
1925
1926 let mut regions = vec![
1927 HitRegion::new(region_id.clone(), area)
1928 .with_role(WidgetRole::Region)
1929 .with_label("list")
1930 .with_value(WidgetValue::Count(self.items.len())),
1931 ];
1932 let group = SelectionGroup::new(format!("{}:items", region_id.as_ref()));
1933 let start = self.visible_start(inner.height);
1934
1935 for row in 0..inner.height {
1936 let idx = start + row as usize;
1937 if idx >= self.items.len() {
1938 break;
1939 }
1940
1941 let selected = self.selected == Some(idx);
1942 let row_area = Rect::new(inner.x, inner.y + row, inner.width, 1);
1943 regions.push(
1944 HitRegion::new(format!("{}:item:{idx}", region_id.as_ref()), row_area)
1945 .with_role(WidgetRole::ListItem)
1946 .with_label(self.items[idx].as_ref())
1947 .with_action(WidgetAction::Focus)
1948 .with_cursor(MouseCursor::Pointer)
1949 .with_row(idx)
1950 .with_selection_group(group.clone())
1951 .with_state(WidgetState::default().selected(selected))
1952 .with_z_index(1),
1953 );
1954 }
1955
1956 regions
1957 }
1958
1959 #[must_use]
1961 pub fn item_count(&self) -> usize {
1962 self.items.len()
1963 }
1964
1965 fn visible_start(&self, visible_height: u16) -> usize {
1966 let total = self.items.len() as u16;
1967 if let Some(sel) = self.selected {
1968 let sel = sel as u16;
1969 if sel < self.scroll_offset {
1970 return sel as usize;
1971 }
1972 if sel >= self.scroll_offset + visible_height {
1973 return (sel + 1 - visible_height) as usize;
1974 }
1975 return self.scroll_offset as usize;
1976 }
1977 let max_top = total.saturating_sub(visible_height);
1978 (self.scroll_offset.min(max_top)) as usize
1979 }
1980}
1981
1982impl Default for List<'_> {
1983 fn default() -> Self {
1984 Self::new()
1985 }
1986}
1987
1988impl Widget for List<'_> {
1989 fn render(&self, buf: &mut Buffer, area: Rect) {
1990 let inner = self
1991 .block
1992 .as_ref()
1993 .map_or(area, |block| block_content_area(block, area));
1994 if let Some(block) = &self.block {
1995 block.render(buf, area);
1996 }
1997 if is_empty(inner) {
1998 return;
1999 }
2000
2001 let start = self.visible_start(inner.height);
2002 let indicator_width = 2u16;
2003 let text_width = inner.width.saturating_sub(indicator_width);
2004
2005 for row in 0..inner.height {
2006 let idx = start + row as usize;
2007 let y = inner.y + row;
2008
2009 if idx >= self.items.len() {
2010 break;
2011 }
2012
2013 let is_selected = self.selected == Some(idx);
2014
2015 let indicator = if is_selected { "▸ " } else { " " };
2016 let indicator_style = if is_selected {
2017 Style::default()
2018 .fg(self.palette.pulse)
2019 .add_modifier(Modifier::BOLD)
2020 } else {
2021 Style::default().fg(self.palette.shadow)
2022 };
2023 paint_text(
2024 Rect::new(inner.x, y, indicator_width, 1),
2025 buf,
2026 indicator,
2027 indicator_style,
2028 );
2029
2030 let item = &self.items[idx];
2031 let item_chars: Vec<char> = item.chars().collect();
2032
2033 for col in 0..text_width {
2034 let x = inner.x + indicator_width + col;
2035 let ch = item_chars.get(col as usize).copied().unwrap_or(' ');
2036 let style = if is_selected {
2037 if ch == ' ' {
2038 Style::default().bg(self.palette.shadow)
2039 } else {
2040 Style::default()
2041 .fg(self.palette.high)
2042 .bg(self.palette.shadow)
2043 .add_modifier(Modifier::BOLD)
2044 }
2045 } else if ch == ' ' {
2046 Style::default()
2047 } else {
2048 Style::default().fg(self.palette.high)
2049 };
2050 set_styled_char(buf, x, y, ch, style);
2051 }
2052 }
2053 }
2054}
2055
2056#[derive(Clone, Debug)]
2062pub struct TabBar<'a> {
2063 tabs: Vec<Cow<'a, str>>,
2064 selected: usize,
2065 tick: u64,
2066 palette: AislingPalette,
2067 block: Option<Block<'a>>,
2068}
2069
2070impl PartialEq for TabBar<'_> {
2071 fn eq(&self, other: &Self) -> bool {
2072 self.tabs == other.tabs
2073 && self.selected == other.selected
2074 && self.tick == other.tick
2075 && self.palette == other.palette
2076 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2077 }
2078}
2079
2080impl<'a> TabBar<'a> {
2081 #[must_use]
2083 pub fn new<I, S>(tabs: I) -> Self
2084 where
2085 I: IntoIterator<Item = S>,
2086 S: Into<Cow<'a, str>>,
2087 {
2088 Self {
2089 tabs: tabs.into_iter().map(Into::into).collect(),
2090 selected: 0,
2091 tick: 0,
2092 palette: AislingPalette::cypherpunk(),
2093 block: None,
2094 }
2095 }
2096
2097 #[must_use]
2099 pub fn selected(mut self, index: usize) -> Self {
2100 self.selected = index;
2101 self
2102 }
2103
2104 #[must_use]
2106 pub fn tick(mut self, tick: u64) -> Self {
2107 self.tick = tick;
2108 self
2109 }
2110
2111 #[must_use]
2113 pub fn palette(mut self, palette: AislingPalette) -> Self {
2114 self.palette = palette;
2115 self
2116 }
2117
2118 #[must_use]
2120 pub fn block(mut self, block: Block<'a>) -> Self {
2121 self.block = Some(block);
2122 self
2123 }
2124
2125 pub fn render_with_interaction(
2127 &self,
2128 frame: &mut Frame<'_>,
2129 id: impl Into<WidgetId>,
2130 area: Rect,
2131 ) {
2132 self.render(frame.buffer(), area);
2133 for region in self.hit_regions(id, area) {
2134 frame.register_hit_region(region);
2135 }
2136 frame.mark_dirty(area);
2137 }
2138
2139 #[must_use]
2141 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
2142 let region_id = id.into();
2143 let inner = self
2144 .block
2145 .as_ref()
2146 .map_or(area, |block| block_content_area(block, area));
2147 if is_empty(area) || is_empty(inner) {
2148 return Vec::new();
2149 }
2150
2151 let mut regions = vec![
2152 HitRegion::new(region_id.clone(), area)
2153 .with_role(WidgetRole::Region)
2154 .with_label("tabs")
2155 .with_value(WidgetValue::Count(self.tabs.len())),
2156 ];
2157 let mut x = inner.x;
2158 let right = inner.x.saturating_add(inner.width);
2159
2160 for (idx, tab) in self.tabs.iter().enumerate() {
2161 if x >= right {
2162 break;
2163 }
2164
2165 let label_width = tab.chars().count() as u16;
2166 let tab_width = label_width.saturating_add(4).min(right - x);
2167 if tab_width == 0 {
2168 break;
2169 }
2170
2171 regions.push(
2172 HitRegion::new(
2173 format!("{}:tab:{idx}", region_id.as_ref()),
2174 Rect::new(x, inner.y, tab_width, 1),
2175 )
2176 .with_role(WidgetRole::Tab)
2177 .with_label(tab.as_ref())
2178 .with_action(WidgetAction::Focus)
2179 .with_cursor(MouseCursor::Pointer)
2180 .with_row(idx)
2181 .with_shortcut(format!("{}", idx + 1))
2182 .with_state(WidgetState::default().selected(idx == self.selected))
2183 .with_z_index(1),
2184 );
2185
2186 x = x.saturating_add(tab_width);
2187 }
2188
2189 regions
2190 }
2191
2192 #[must_use]
2194 pub fn tab_count(&self) -> usize {
2195 self.tabs.len()
2196 }
2197}
2198
2199impl Widget for TabBar<'_> {
2200 fn render(&self, buf: &mut Buffer, area: Rect) {
2201 let inner = self
2202 .block
2203 .as_ref()
2204 .map_or(area, |block| block_content_area(block, area));
2205 if let Some(block) = &self.block {
2206 block.render(buf, area);
2207 }
2208 if is_empty(inner) {
2209 return;
2210 }
2211
2212 let mut x = inner.x;
2213 let right = inner.x.saturating_add(inner.width);
2214
2215 for (i, tab) in self.tabs.iter().enumerate() {
2216 if x >= right {
2217 break;
2218 }
2219
2220 let is_selected = i == self.selected;
2221 let label: Vec<char> = tab.chars().collect();
2222 let padding = 2u16;
2223 let tab_width = (label.len() as u16 + padding * 2).min(right - x);
2224
2225 if is_selected {
2226 set_styled_char(
2227 buf,
2228 x,
2229 inner.y,
2230 '㎍',
2231 Style::default()
2232 .fg(self.palette.pulse)
2233 .add_modifier(Modifier::BOLD),
2234 );
2235 } else {
2236 set_styled_char(buf, x, inner.y, ' ', Style::default());
2237 }
2238
2239 for col in 0..tab_width {
2240 let cx = x + col;
2241 if cx >= right {
2242 break;
2243 }
2244
2245 let char_idx = col.saturating_sub(padding) as usize;
2246 let ch = if col < padding || col >= tab_width - padding {
2247 ' '
2248 } else if char_idx < label.len() {
2249 label[char_idx]
2250 } else {
2251 ' '
2252 };
2253
2254 let style = if is_selected {
2255 Style::default()
2256 .fg(self.palette.high)
2257 .add_modifier(Modifier::BOLD)
2258 } else {
2259 Style::default().fg(self.palette.mid)
2260 };
2261 set_styled_char(buf, cx, inner.y, ch, style);
2262 }
2263
2264 if is_selected {
2265 let bottom = inner.y + inner.height.saturating_sub(1);
2266 for col in 0..tab_width {
2267 let cx = x + col;
2268 if cx >= right {
2269 break;
2270 }
2271 set_styled_char(
2272 buf,
2273 cx,
2274 bottom,
2275 '─',
2276 Style::default().fg(self.palette.pulse),
2277 );
2278 }
2279 }
2280
2281 x += tab_width;
2282 }
2283 }
2284}
2285
2286#[derive(Clone, Debug)]
2292pub struct Table<'a> {
2293 headers: Vec<Cow<'a, str>>,
2294 rows: Vec<Vec<Cow<'a, str>>>,
2295 widths: Option<Vec<u16>>,
2296 selected: Option<usize>,
2297 scroll_offset: u16,
2298 tick: u64,
2299 palette: AislingPalette,
2300 block: Option<Block<'a>>,
2301}
2302
2303impl PartialEq for Table<'_> {
2304 fn eq(&self, other: &Self) -> bool {
2305 self.headers == other.headers
2306 && self.rows == other.rows
2307 && self.widths == other.widths
2308 && self.selected == other.selected
2309 && self.scroll_offset == other.scroll_offset
2310 && self.tick == other.tick
2311 && self.palette == other.palette
2312 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2313 }
2314}
2315
2316impl<'a> Table<'a> {
2317 #[must_use]
2319 pub fn new<I, S>(headers: I) -> Self
2320 where
2321 I: IntoIterator<Item = S>,
2322 S: Into<Cow<'a, str>>,
2323 {
2324 Self {
2325 headers: headers.into_iter().map(Into::into).collect(),
2326 rows: Vec::new(),
2327 widths: None,
2328 selected: None,
2329 scroll_offset: 0,
2330 tick: 0,
2331 palette: AislingPalette::cypherpunk(),
2332 block: None,
2333 }
2334 }
2335
2336 #[must_use]
2338 pub fn row<I, S>(mut self, row: I) -> Self
2339 where
2340 I: IntoIterator<Item = S>,
2341 S: Into<Cow<'a, str>>,
2342 {
2343 self.rows.push(row.into_iter().map(Into::into).collect());
2344 self
2345 }
2346
2347 #[must_use]
2349 pub fn rows<I, R, S>(mut self, rows: I) -> Self
2350 where
2351 I: IntoIterator<Item = R>,
2352 R: IntoIterator<Item = S>,
2353 S: Into<Cow<'a, str>>,
2354 {
2355 self.rows = rows
2356 .into_iter()
2357 .map(|r| r.into_iter().map(Into::into).collect())
2358 .collect();
2359 self
2360 }
2361
2362 #[must_use]
2364 pub fn widths(mut self, widths: Vec<u16>) -> Self {
2365 self.widths = Some(widths);
2366 self
2367 }
2368
2369 #[must_use]
2371 pub fn selected(mut self, index: Option<usize>) -> Self {
2372 self.selected = index;
2373 self
2374 }
2375
2376 #[must_use]
2378 pub fn scroll_offset(mut self, offset: u16) -> Self {
2379 self.scroll_offset = offset;
2380 self
2381 }
2382
2383 #[must_use]
2385 pub fn tick(mut self, tick: u64) -> Self {
2386 self.tick = tick;
2387 self
2388 }
2389
2390 #[must_use]
2392 pub fn palette(mut self, palette: AislingPalette) -> Self {
2393 self.palette = palette;
2394 self
2395 }
2396
2397 #[must_use]
2399 pub fn block(mut self, block: Block<'a>) -> Self {
2400 self.block = Some(block);
2401 self
2402 }
2403
2404 pub fn render_with_interaction(
2406 &self,
2407 frame: &mut Frame<'_>,
2408 id: impl Into<WidgetId>,
2409 area: Rect,
2410 ) {
2411 self.render(frame.buffer(), area);
2412 for region in self.hit_regions(id, area) {
2413 frame.register_hit_region(region);
2414 }
2415 frame.mark_dirty(area);
2416 }
2417
2418 #[must_use]
2420 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
2421 let region_id = id.into();
2422 let inner = self
2423 .block
2424 .as_ref()
2425 .map_or(area, |block| block_content_area(block, area));
2426 if is_empty(area) || is_empty(inner) {
2427 return Vec::new();
2428 }
2429
2430 let mut regions = vec![
2431 HitRegion::new(region_id.clone(), area)
2432 .with_role(WidgetRole::Region)
2433 .with_label("table")
2434 .with_value(WidgetValue::Count(self.rows.len())),
2435 ];
2436 if self.headers.is_empty() || inner.height < 3 {
2437 return regions;
2438 }
2439
2440 let visible_rows = inner.height.saturating_sub(2);
2441 let start = self
2442 .scroll_offset
2443 .min((self.rows.len() as u16).saturating_sub(visible_rows.min(self.rows.len() as u16)));
2444
2445 for row_offset in 0..visible_rows {
2446 let row_idx = start as usize + row_offset as usize;
2447 if row_idx >= self.rows.len() {
2448 break;
2449 }
2450
2451 let y = inner.y + 2 + row_offset;
2452 let selected = self.selected == Some(row_idx);
2453 let label = self.rows[row_idx]
2454 .iter()
2455 .map(Cow::as_ref)
2456 .collect::<Vec<_>>()
2457 .join(" | ");
2458 regions.push(
2459 HitRegion::new(
2460 format!("{}:row:{row_idx}", region_id.as_ref()),
2461 Rect::new(inner.x, y, inner.width, 1),
2462 )
2463 .with_role(WidgetRole::ModelRow)
2464 .with_label(label)
2465 .with_action(WidgetAction::Focus)
2466 .with_cursor(MouseCursor::Pointer)
2467 .with_row(row_idx)
2468 .with_state(WidgetState::default().selected(selected))
2469 .with_value(WidgetValue::Count(self.rows[row_idx].len()))
2470 .with_z_index(1),
2471 );
2472 }
2473
2474 regions
2475 }
2476
2477 #[must_use]
2479 pub fn row_count(&self) -> usize {
2480 self.rows.len()
2481 }
2482
2483 fn compute_widths(&self, total_width: u16) -> Vec<u16> {
2484 if let Some(ref w) = self.widths {
2485 return w.clone();
2486 }
2487 let cols = self.headers.len().max(1) as u16;
2488 let per_col = total_width / cols;
2489 let mut widths = vec![per_col; cols as usize];
2490 let remainder = total_width.saturating_sub(per_col * cols);
2491 for w in widths.iter_mut().take(remainder as usize) {
2492 *w += 1;
2493 }
2494 widths
2495 }
2496}
2497
2498impl Widget for Table<'_> {
2499 fn render(&self, buf: &mut Buffer, area: Rect) {
2500 let inner = self
2501 .block
2502 .as_ref()
2503 .map_or(area, |block| block_content_area(block, area));
2504 if let Some(block) = &self.block {
2505 block.render(buf, area);
2506 }
2507 if is_empty(inner) || self.headers.is_empty() {
2508 return;
2509 }
2510
2511 let col_widths = self.compute_widths(inner.width);
2512 let header_height = 1u16;
2513 let divider_height = 1u16;
2514 let data_start_y = inner.y + header_height + divider_height;
2515 let visible_rows = inner.height.saturating_sub(header_height + divider_height);
2516
2517 for (col_idx, header) in self.headers.iter().enumerate() {
2518 if col_idx >= col_widths.len() {
2519 break;
2520 }
2521 let col_x = inner.x + col_widths[..col_idx].iter().sum::<u16>();
2522 let w = col_widths[col_idx];
2523 paint_text(
2524 Rect::new(col_x, inner.y, w, 1),
2525 buf,
2526 header.as_ref(),
2527 Style::default()
2528 .fg(self.palette.pulse)
2529 .add_modifier(Modifier::BOLD),
2530 );
2531 }
2532
2533 let div_y = inner.y + header_height;
2534 for col_idx in 0..self.headers.len().min(col_widths.len()) {
2535 let col_x = inner.x + col_widths[..col_idx].iter().sum::<u16>();
2536 let w = col_widths[col_idx];
2537 for dx in 0..w {
2538 set_styled_char(
2539 buf,
2540 col_x + dx,
2541 div_y,
2542 '─',
2543 Style::default().fg(self.palette.shadow),
2544 );
2545 }
2546 }
2547
2548 let total = self.rows.len() as u16;
2549 let start = self
2550 .scroll_offset
2551 .min(total.saturating_sub(visible_rows.min(total)));
2552
2553 for row_offset in 0..visible_rows {
2554 let row_idx = start as usize + row_offset as usize;
2555 let y = data_start_y + row_offset;
2556
2557 if row_idx >= self.rows.len() {
2558 break;
2559 }
2560
2561 let is_selected = self.selected == Some(row_idx);
2562 let row = &self.rows[row_idx];
2563
2564 for (col_idx, cell) in row.iter().enumerate() {
2565 if col_idx >= col_widths.len() {
2566 break;
2567 }
2568 let col_x = inner.x + col_widths[..col_idx].iter().sum::<u16>();
2569 let w = col_widths[col_idx];
2570
2571 let cell_chars: Vec<char> = cell.chars().collect();
2572 for dx in 0..w {
2573 let ch = cell_chars.get(dx as usize).copied().unwrap_or(' ');
2574 let style = if is_selected {
2575 Style::default()
2576 .fg(self.palette.high)
2577 .bg(self.palette.shadow)
2578 .add_modifier(Modifier::BOLD)
2579 } else {
2580 Style::default().fg(self.palette.high)
2581 };
2582 set_styled_char(buf, col_x + dx, y, ch, style);
2583 }
2584 }
2585 }
2586 }
2587}
2588
2589#[derive(Clone, Debug)]
2595pub struct Sparkline<'a> {
2596 data: Vec<u16>,
2597 max_value: Option<u16>,
2598 palette: AislingPalette,
2599 block: Option<Block<'a>>,
2600}
2601
2602impl PartialEq for Sparkline<'_> {
2603 fn eq(&self, other: &Self) -> bool {
2604 self.data == other.data
2605 && self.max_value == other.max_value
2606 && self.palette == other.palette
2607 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2608 }
2609}
2610
2611impl<'a> Sparkline<'a> {
2612 #[must_use]
2614 pub fn new(data: Vec<u16>) -> Self {
2615 Self {
2616 data,
2617 max_value: None,
2618 palette: AislingPalette::phosphor(),
2619 block: None,
2620 }
2621 }
2622
2623 #[must_use]
2625 pub fn max_value(mut self, max: u16) -> Self {
2626 self.max_value = Some(max);
2627 self
2628 }
2629
2630 #[must_use]
2632 pub fn palette(mut self, palette: AislingPalette) -> Self {
2633 self.palette = palette;
2634 self
2635 }
2636
2637 #[must_use]
2639 pub fn block(mut self, block: Block<'a>) -> Self {
2640 self.block = Some(block);
2641 self
2642 }
2643}
2644
2645impl Widget for Sparkline<'_> {
2646 fn render(&self, buf: &mut Buffer, area: Rect) {
2647 let inner = self
2648 .block
2649 .as_ref()
2650 .map_or(area, |block| block_content_area(block, area));
2651 if let Some(block) = &self.block {
2652 block.render(buf, area);
2653 }
2654 if is_empty(inner) || self.data.is_empty() {
2655 return;
2656 }
2657
2658 let max = self
2659 .max_value
2660 .unwrap_or_else(|| self.data.iter().copied().max().unwrap_or(1))
2661 .max(1);
2662 let bottom = inner.y.saturating_add(inner.height);
2663
2664 for col in 0..inner.width {
2665 let data_idx = (col as usize * self.data.len()) / usize::from(inner.width);
2666 let value = self.data.get(data_idx).copied().unwrap_or(0);
2667 let bar_height =
2668 ((f64::from(value) / f64::from(max)) * f64::from(inner.height)).round() as u16;
2669 let bar_y = bottom.saturating_sub(bar_height);
2670
2671 for y in bar_y..bottom {
2672 let noise = field_noise(inner.x + col, y, 0);
2673 let style = Style::default()
2674 .fg(self.palette.lane(noise))
2675 .add_modifier(Modifier::BOLD);
2676 set_styled_char(buf, inner.x + col, y, '█', style);
2677 }
2678
2679 for y in inner.y..bar_y {
2680 set_styled_char(
2681 buf,
2682 inner.x + col,
2683 y,
2684 '·',
2685 Style::default().fg(self.palette.shadow),
2686 );
2687 }
2688 }
2689 }
2690}
2691
2692#[derive(Clone, Debug)]
2698pub struct Gauge<'a> {
2699 ratio: f64,
2700 label: Option<Cow<'a, str>>,
2701 palette: AislingPalette,
2702 block: Option<Block<'a>>,
2703}
2704
2705impl PartialEq for Gauge<'_> {
2706 fn eq(&self, other: &Self) -> bool {
2707 self.ratio == other.ratio
2708 && self.label == other.label
2709 && self.palette == other.palette
2710 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2711 }
2712}
2713
2714impl<'a> Gauge<'a> {
2715 #[must_use]
2717 pub fn new(ratio: f64) -> Self {
2718 Self {
2719 ratio: ratio.clamp(0.0, 1.0),
2720 label: None,
2721 palette: AislingPalette::cypherpunk(),
2722 block: None,
2723 }
2724 }
2725
2726 #[must_use]
2728 pub fn ratio(&self) -> f64 {
2729 self.ratio
2730 }
2731
2732 #[must_use]
2734 pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
2735 self.label = Some(label.into());
2736 self
2737 }
2738
2739 #[must_use]
2741 pub fn palette(mut self, palette: AislingPalette) -> Self {
2742 self.palette = palette;
2743 self
2744 }
2745
2746 #[must_use]
2748 pub fn block(mut self, block: Block<'a>) -> Self {
2749 self.block = Some(block);
2750 self
2751 }
2752}
2753
2754impl Widget for Gauge<'_> {
2755 fn render(&self, buf: &mut Buffer, area: Rect) {
2756 let inner = self
2757 .block
2758 .as_ref()
2759 .map_or(area, |block| block_content_area(block, area));
2760 if let Some(block) = &self.block {
2761 block.render(buf, area);
2762 }
2763 if is_empty(inner) {
2764 return;
2765 }
2766
2767 let right = inner.x.saturating_add(inner.width);
2768 let bottom = inner.y.saturating_add(inner.height);
2769 let filled = (f64::from(inner.width) * self.ratio).round() as u16;
2770
2771 for y in inner.y..bottom {
2772 for x in inner.x..right {
2773 let offset = x.saturating_sub(inner.x);
2774 if offset < filled {
2775 set_styled_char(
2776 buf,
2777 x,
2778 y,
2779 '█',
2780 Style::default()
2781 .fg(self.palette.mid)
2782 .add_modifier(Modifier::BOLD),
2783 );
2784 } else {
2785 set_styled_char(buf, x, y, '░', Style::default().fg(self.palette.shadow));
2786 }
2787 }
2788 }
2789
2790 if let Some(label) = &self.label {
2791 let row = inner.y + inner.height / 2;
2792 let label_width = label.chars().count().min(usize::from(inner.width)) as u16;
2793 let start = inner.x + inner.width.saturating_sub(label_width) / 2;
2794 paint_text(
2795 Rect::new(start, row, label_width, 1),
2796 buf,
2797 label.as_ref(),
2798 Style::default()
2799 .fg(self.palette.high)
2800 .add_modifier(Modifier::BOLD),
2801 );
2802 }
2803 }
2804}
2805
2806#[derive(Clone, Debug)]
2812pub struct Paragraph<'a> {
2813 text: Cow<'a, str>,
2814 scroll_offset: u16,
2815 palette: AislingPalette,
2816 block: Option<Block<'a>>,
2817}
2818
2819impl PartialEq for Paragraph<'_> {
2820 fn eq(&self, other: &Self) -> bool {
2821 self.text == other.text
2822 && self.scroll_offset == other.scroll_offset
2823 && self.palette == other.palette
2824 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2825 }
2826}
2827
2828impl<'a> Paragraph<'a> {
2829 #[must_use]
2831 pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
2832 Self {
2833 text: text.into(),
2834 scroll_offset: 0,
2835 palette: AislingPalette::cypherpunk(),
2836 block: None,
2837 }
2838 }
2839
2840 #[must_use]
2842 pub fn scroll_offset(mut self, offset: u16) -> Self {
2843 self.scroll_offset = offset;
2844 self
2845 }
2846
2847 #[must_use]
2849 pub fn palette(mut self, palette: AislingPalette) -> Self {
2850 self.palette = palette;
2851 self
2852 }
2853
2854 #[must_use]
2856 pub fn block(mut self, block: Block<'a>) -> Self {
2857 self.block = Some(block);
2858 self
2859 }
2860
2861 fn wrap_lines(&self, width: u16) -> Vec<Cow<'_, str>> {
2862 if width == 0 {
2863 return Vec::new();
2864 }
2865 let mut result = Vec::new();
2866 for raw_line in self.text.lines() {
2867 if raw_line.is_empty() {
2868 result.push(Cow::Borrowed(""));
2869 continue;
2870 }
2871 let mut remaining = raw_line;
2872 while !remaining.is_empty() {
2873 let w = usize::from(width);
2874 if remaining.len() <= w {
2875 result.push(Cow::Borrowed(remaining));
2876 break;
2877 }
2878 let break_at = remaining[..w].rfind(' ').map(|p| p + 1).unwrap_or(w);
2879 result.push(Cow::Borrowed(&remaining[..break_at]));
2880 remaining = &remaining[break_at..];
2881 }
2882 }
2883 result
2884 }
2885}
2886
2887impl Widget for Paragraph<'_> {
2888 fn render(&self, buf: &mut Buffer, area: Rect) {
2889 let inner = self
2890 .block
2891 .as_ref()
2892 .map_or(area, |block| block_content_area(block, area));
2893 if let Some(block) = &self.block {
2894 block.render(buf, area);
2895 }
2896 if is_empty(inner) {
2897 return;
2898 }
2899
2900 let wrapped = self.wrap_lines(inner.width);
2901 let total = wrapped.len() as u16;
2902 let start = self
2903 .scroll_offset
2904 .min(total.saturating_sub(inner.height.min(total)));
2905
2906 for row in 0..inner.height {
2907 let line_idx = start as usize + row as usize;
2908 let y = inner.y + row;
2909
2910 if line_idx >= wrapped.len() {
2911 break;
2912 }
2913
2914 let line = &wrapped[line_idx];
2915 let chars: Vec<char> = line.chars().collect();
2916
2917 for col in 0..inner.width {
2918 let ch = chars.get(col as usize).copied().unwrap_or(' ');
2919 let style = if ch == ' ' {
2920 Style::default()
2921 } else {
2922 Style::default().fg(self.palette.high)
2923 };
2924 set_styled_char(buf, inner.x + col, y, ch, style);
2925 }
2926 }
2927 }
2928}
2929
2930#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2936pub enum Align {
2937 Left,
2938 Center,
2939 Right,
2940}
2941
2942#[derive(Clone, Debug)]
2944pub struct StatusSection<'a> {
2945 text: Cow<'a, str>,
2946 align: Align,
2947}
2948
2949#[derive(Clone, Debug)]
2951pub struct StatusBar<'a> {
2952 sections: Vec<StatusSection<'a>>,
2953 palette: AislingPalette,
2954}
2955
2956impl PartialEq for StatusBar<'_> {
2957 fn eq(&self, other: &Self) -> bool {
2958 self.sections.len() == other.sections.len()
2959 && self
2960 .sections
2961 .iter()
2962 .zip(other.sections.iter())
2963 .all(|(a, b)| a.text == b.text && a.align == b.align)
2964 && self.palette == other.palette
2965 }
2966}
2967
2968impl<'a> StatusBar<'a> {
2969 #[must_use]
2971 pub fn new() -> Self {
2972 Self {
2973 sections: Vec::new(),
2974 palette: AislingPalette::cypherpunk(),
2975 }
2976 }
2977
2978 #[must_use]
2980 pub fn left(mut self, text: impl Into<Cow<'a, str>>) -> Self {
2981 self.sections.push(StatusSection {
2982 text: text.into(),
2983 align: Align::Left,
2984 });
2985 self
2986 }
2987
2988 #[must_use]
2990 pub fn center(mut self, text: impl Into<Cow<'a, str>>) -> Self {
2991 self.sections.push(StatusSection {
2992 text: text.into(),
2993 align: Align::Center,
2994 });
2995 self
2996 }
2997
2998 #[must_use]
3000 pub fn right(mut self, text: impl Into<Cow<'a, str>>) -> Self {
3001 self.sections.push(StatusSection {
3002 text: text.into(),
3003 align: Align::Right,
3004 });
3005 self
3006 }
3007
3008 #[must_use]
3010 pub fn palette(mut self, palette: AislingPalette) -> Self {
3011 self.palette = palette;
3012 self
3013 }
3014}
3015
3016impl Default for StatusBar<'_> {
3017 fn default() -> Self {
3018 Self::new()
3019 }
3020}
3021
3022impl Widget for StatusBar<'_> {
3023 fn render(&self, buf: &mut Buffer, area: Rect) {
3024 if is_empty(area) || self.sections.is_empty() {
3025 return;
3026 }
3027
3028 let bg_style = Style::default()
3029 .fg(self.palette.high)
3030 .bg(self.palette.shadow);
3031
3032 for x in area.x..area.x.saturating_add(area.width) {
3033 for y in area.y..area.y.saturating_add(area.height) {
3034 set_styled_char(buf, x, y, ' ', bg_style);
3035 }
3036 }
3037
3038 let left_sections: Vec<_> = self
3039 .sections
3040 .iter()
3041 .filter(|s| s.align == Align::Left)
3042 .collect();
3043 let center_sections: Vec<_> = self
3044 .sections
3045 .iter()
3046 .filter(|s| s.align == Align::Center)
3047 .collect();
3048 let right_sections: Vec<_> = self
3049 .sections
3050 .iter()
3051 .filter(|s| s.align == Align::Right)
3052 .collect();
3053
3054 let mut x = area.x;
3055
3056 for section in &left_sections {
3057 let text: Vec<char> = section.text.chars().collect();
3058 let max_len = text
3059 .len()
3060 .min(usize::from(area.width.saturating_sub(x - area.x)));
3061 for (i, &ch) in text.iter().take(max_len).enumerate() {
3062 set_styled_char(
3063 buf,
3064 x + i as u16,
3065 area.y,
3066 ch,
3067 Style::default()
3068 .fg(self.palette.high)
3069 .add_modifier(Modifier::BOLD),
3070 );
3071 }
3072 x += max_len as u16;
3073 }
3074
3075 for section in ¢er_sections {
3076 let text: Vec<char> = section.text.chars().collect();
3077 let available = area.width.saturating_sub(x - area.x);
3078 let start_offset = available.saturating_sub(text.len() as u16) / 2;
3079 x += start_offset;
3080 for (i, &ch) in text.iter().take(usize::from(available)).enumerate() {
3081 set_styled_char(
3082 buf,
3083 x + i as u16,
3084 area.y,
3085 ch,
3086 Style::default()
3087 .fg(self.palette.high)
3088 .add_modifier(Modifier::BOLD),
3089 );
3090 }
3091 x += text.len() as u16;
3092 }
3093
3094 let right_x = area.x + area.width;
3095 let mut render_x = right_x;
3096 for section in right_sections.iter().rev() {
3097 let text: Vec<char> = section.text.chars().collect();
3098 render_x = render_x.saturating_sub(text.len() as u16);
3099 for (i, &ch) in text.iter().enumerate() {
3100 if render_x + (i as u16) >= area.x && render_x + (i as u16) < right_x {
3101 set_styled_char(
3102 buf,
3103 render_x + i as u16,
3104 area.y,
3105 ch,
3106 Style::default()
3107 .fg(self.palette.high)
3108 .add_modifier(Modifier::BOLD),
3109 );
3110 }
3111 }
3112 }
3113 }
3114}
3115
3116#[derive(Clone, Debug)]
3123pub struct Bordered<'a> {
3124 title: Cow<'a, str>,
3125 palette: AislingPalette,
3126}
3127
3128impl PartialEq for Bordered<'_> {
3129 fn eq(&self, other: &Self) -> bool {
3130 self.title == other.title && self.palette == other.palette
3131 }
3132}
3133
3134impl<'a> Bordered<'a> {
3135 #[must_use]
3137 pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
3138 Self {
3139 title: title.into(),
3140 palette: AislingPalette::cypherpunk(),
3141 }
3142 }
3143
3144 #[must_use]
3146 pub fn palette(mut self, palette: AislingPalette) -> Self {
3147 self.palette = palette;
3148 self
3149 }
3150
3151 pub fn render_inner(&self, buf: &mut Buffer, area: Rect) -> Rect {
3153 let block = Block::new(self.title.as_ref())
3154 .with_borders(BorderStyle::Plain)
3155 .with_border_color(self.palette.mid);
3156 let inner = block_content_area(&block, area);
3157 block.render(buf, area);
3158 inner
3159 }
3160}
3161
3162impl Widget for Bordered<'_> {
3163 fn render(&self, buf: &mut Buffer, area: Rect) {
3164 self.render_inner(buf, area);
3165 }
3166}
3167
3168fn is_empty(area: Rect) -> bool {
3173 area.width == 0 || area.height == 0
3174}
3175
3176fn block_content_area(block: &Block<'_>, area: Rect) -> Rect {
3177 match block.borders {
3178 BorderStyle::None => area,
3179 _ => Rect::new(
3180 area.x.saturating_add(1),
3181 area.y.saturating_add(1),
3182 area.width.saturating_sub(2),
3183 area.height.saturating_sub(2),
3184 ),
3185 }
3186}
3187
3188fn option_block_eq(left: Option<&Block<'_>>, right: Option<&Block<'_>>) -> bool {
3189 match (left, right) {
3190 (Some(left), Some(right)) => block_eq(left, right),
3191 (None, None) => true,
3192 _ => false,
3193 }
3194}
3195
3196fn block_eq(left: &Block<'_>, right: &Block<'_>) -> bool {
3197 left.title == right.title
3198 && left.title_right == right.title_right
3199 && left.borders == right.borders
3200 && left.border_color == right.border_color
3201 && left.bg == right.bg
3202 && left.style == right.style
3203 && left.inner_margin == right.inner_margin
3204}
3205
3206fn is_edge(area: Rect, x: u16, y: u16) -> bool {
3207 x == area.x
3208 || y == area.y
3209 || x + 1 == area.x.saturating_add(area.width)
3210 || y + 1 == area.y.saturating_add(area.height)
3211}
3212
3213fn field_noise(x: u16, y: u16, tick: u64) -> u64 {
3214 let mut value = u64::from(x).wrapping_mul(0x9e37_79b9_7f4a_7c15)
3215 ^ u64::from(y).wrapping_mul(0xbf58_476d_1ce4_e5b9)
3216 ^ tick.wrapping_mul(0x94d0_49bb_1331_11eb);
3217 value ^= value >> 30;
3218 value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
3219 value ^= value >> 27;
3220 value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
3221 value ^ (value >> 31)
3222}
3223
3224fn paint_text(area: Rect, buf: &mut Buffer, text: &str, style: Style) {
3225 if is_empty(area) {
3226 return;
3227 }
3228
3229 let right = area.x.saturating_add(area.width);
3230 for (offset, glyph) in text.chars().take(usize::from(area.width)).enumerate() {
3231 let x = area.x + offset as u16;
3232 if x >= right {
3233 break;
3234 }
3235 set_styled_char(buf, x, area.y, glyph, style);
3236 }
3237}
3238
3239fn set_cell_bg(buf: &mut Buffer, x: u16, y: u16, bg: Color) {
3240 let Some(mut cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
3241 return;
3242 };
3243 cell.bg = Some(bg);
3244 buf.set(usize::from(x), usize::from(y), cell);
3245}
3246
3247fn set_cell_style(buf: &mut Buffer, x: u16, y: u16, style: Style) {
3248 let Some(cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
3249 return;
3250 };
3251 buf.set(usize::from(x), usize::from(y), replace_style(cell, style));
3252}
3253
3254fn set_styled_char(buf: &mut Buffer, x: u16, y: u16, ch: char, style: Style) {
3255 buf.set(
3256 usize::from(x),
3257 usize::from(y),
3258 replace_style(
3259 Cell::new(ch, style.fg.unwrap_or(Color::WHITE), style.bg),
3260 style,
3261 ),
3262 );
3263}
3264
3265fn replace_style(mut cell: Cell, style: Style) -> Cell {
3266 cell.fg = style.fg.unwrap_or(Color::WHITE);
3267 cell.bg = style.bg;
3268 cell.bold = (style.bold || style.add_modifier.contains(Modifier::BOLD))
3269 && !style.sub_modifier.contains(Modifier::BOLD);
3270 cell.italic = (style.italic || style.add_modifier.contains(Modifier::ITALIC))
3271 && !style.sub_modifier.contains(Modifier::ITALIC);
3272 cell.underlined = (style.underlined || style.add_modifier.contains(Modifier::UNDERLINED))
3273 && !style.sub_modifier.contains(Modifier::UNDERLINED);
3274 cell
3275}
3276
3277#[cfg(test)]
3278mod tests {
3279 use super::*;
3280
3281 #[test]
3282 fn gauge_ratio_is_clamped() {
3283 assert_eq!(NebulaGauge::new(1.5).ratio(), 1.0);
3284 assert_eq!(NebulaGauge::new(-1.0).ratio(), 0.0);
3285 }
3286
3287 #[test]
3288 fn effect_can_be_applied_to_a_buffer() {
3289 let area = Rect::new(0, 0, 12, 4);
3290 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3291
3292 AislingEffect::new(8).intensity(7).apply(area, &mut buf);
3293 }
3294
3295 #[test]
3296 fn flicker_panel_renders_without_panic() {
3297 let area = Rect::new(0, 0, 20, 3);
3298 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3299 FlickerPanel::new("test")
3300 .tick(5)
3301 .intensity(3)
3302 .render(&mut buf, area);
3303 }
3304
3305 #[test]
3306 fn waveform_renders_without_panic() {
3307 let area = Rect::new(0, 0, 40, 10);
3308 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3309 Waveform::new(4.0, 0.6).tick(12).render(&mut buf, area);
3310 }
3311
3312 #[test]
3313 fn waveform_short_height_is_noop() {
3314 let area = Rect::new(0, 0, 20, 2);
3315 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3316 Waveform::new(4.0, 0.6).render(&mut buf, area);
3317 }
3318
3319 #[test]
3320 fn pulse_ring_renders_without_panic() {
3321 let area = Rect::new(0, 0, 30, 15);
3322 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3323 PulseRing::new(3).tick(7).render(&mut buf, area);
3324 }
3325
3326 #[test]
3327 fn pulse_ring_zero_area_is_noop() {
3328 let area = Rect::new(0, 0, 0, 0);
3329 let mut buf = Buffer::new(1, 1);
3330 PulseRing::new(5).render(&mut buf, area);
3331 }
3332
3333 #[test]
3334 fn radar_renders_without_panic() {
3335 let area = Rect::new(0, 0, 20, 20);
3336 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3337 Radar::new(5).tick(10).render(&mut buf, area);
3338 }
3339
3340 #[test]
3341 fn radar_small_area_is_noop() {
3342 let area = Rect::new(0, 0, 1, 1);
3343 let mut buf = Buffer::new(1, 1);
3344 Radar::new(5).render(&mut buf, area);
3345 }
3346
3347 #[test]
3348 fn orb_field_renders_without_panic() {
3349 let area = Rect::new(0, 0, 30, 10);
3350 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3351 OrbField::new(8).tick(5).render(&mut buf, area);
3352 }
3353
3354 #[test]
3355 fn neon_border_renders_without_panic() {
3356 let area = Rect::new(0, 0, 20, 10);
3357 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3358 NeonBorder::new(Block::new("test"))
3359 .tick(12)
3360 .render(&mut buf, area);
3361 }
3362
3363 #[test]
3364 fn stream_panel_renders_without_panic() {
3365 let area = Rect::new(0, 0, 40, 10);
3366 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3367 StreamPanel::new()
3368 .push_line("fn main() {")
3369 .push_line(" println!(\"hello\");")
3370 .push_line("}")
3371 .show_line_numbers(true)
3372 .tick(5)
3373 .render(&mut buf, area);
3374 }
3375
3376 #[test]
3377 fn stream_panel_empty_is_noop() {
3378 let area = Rect::new(0, 0, 10, 5);
3379 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3380 StreamPanel::new().render(&mut buf, area);
3381 }
3382
3383 #[test]
3384 fn stream_panel_follow_tail() {
3385 let lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
3386 let area = Rect::new(0, 0, 30, 5);
3387 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3388 let panel = StreamPanel::new()
3389 .lines(lines)
3390 .follow_tail(true)
3391 .show_line_numbers(true);
3392 panel.render(&mut buf, area);
3393 assert_eq!(panel.line_count(), 50);
3394 }
3395
3396 #[test]
3397 fn split_pane_vertical() {
3398 let area = Rect::new(0, 0, 80, 24);
3399 let (a, b, div) = SplitPane::vertical().ratio(0.6).split(area);
3400 assert_eq!(a.width, 48);
3401 assert_eq!(b.width, 32);
3402 assert_eq!(div.width, 0);
3403 }
3404
3405 #[test]
3406 fn split_pane_vertical_with_divider() {
3407 let area = Rect::new(0, 0, 80, 24);
3408 let (a, b, div) = SplitPane::vertical().ratio(0.5).divider('│').split(area);
3409 assert_eq!(a.width + b.width + div.width, 80);
3410 assert_eq!(div.width, 1);
3411 }
3412
3413 #[test]
3414 fn split_pane_horizontal() {
3415 let area = Rect::new(0, 0, 80, 24);
3416 let (a, b, _div) = SplitPane::horizontal().ratio(0.75).split(area);
3417 assert_eq!(a.height, 18);
3418 assert_eq!(b.height, 6);
3419 }
3420
3421 #[test]
3422 fn split_pane_empty_area() {
3423 let (a, b, div) = SplitPane::vertical().split(Rect::ZERO);
3424 assert_eq!(a, Rect::ZERO);
3425 assert_eq!(b, Rect::ZERO);
3426 assert_eq!(div, Rect::ZERO);
3427 }
3428
3429 #[test]
3430 fn list_renders_without_panic() {
3431 let area = Rect::new(0, 0, 30, 8);
3432 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3433 List::new()
3434 .item("Apple")
3435 .item("Banana")
3436 .item("Cherry")
3437 .selected(Some(1))
3438 .render(&mut buf, area);
3439 }
3440
3441 #[test]
3442 fn list_scrolls_to_selected() {
3443 let items: Vec<String> = (0..30).map(|i| format!("Item {i}")).collect();
3444 let area = Rect::new(0, 0, 20, 5);
3445 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3446 List::new()
3447 .items(items)
3448 .selected(Some(25))
3449 .render(&mut buf, area);
3450 }
3451
3452 #[test]
3453 fn tab_bar_renders_without_panic() {
3454 let area = Rect::new(0, 0, 60, 3);
3455 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3456 TabBar::new(["Tab 1", "Tab 2", "Tab 3"])
3457 .selected(1)
3458 .tick(3)
3459 .render(&mut buf, area);
3460 }
3461
3462 #[test]
3463 fn tab_bar_many_tabs() {
3464 let area = Rect::new(0, 0, 20, 1);
3465 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3466 TabBar::new(["A", "B", "C", "D", "E", "F", "G", "H"]).render(&mut buf, area);
3467 }
3468
3469 #[test]
3470 fn table_renders_without_panic() {
3471 let area = Rect::new(0, 0, 60, 10);
3472 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3473 Table::new(["Name", "Age", "City"])
3474 .row(["Alice", "30", "NYC"])
3475 .row(["Bob", "25", "LA"])
3476 .row(["Carol", "35", "Chicago"])
3477 .selected(Some(1))
3478 .render(&mut buf, area);
3479 }
3480
3481 #[test]
3482 fn table_with_explicit_widths() {
3483 let area = Rect::new(0, 0, 40, 5);
3484 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3485 Table::new(["A", "B"])
3486 .row(["x", "y"])
3487 .widths(vec![20, 20])
3488 .render(&mut buf, area);
3489 }
3490
3491 #[test]
3492 fn sparkline_renders_without_panic() {
3493 let area = Rect::new(0, 0, 30, 5);
3494 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3495 Sparkline::new(vec![1, 3, 5, 2, 8, 4, 6, 3, 7, 9]).render(&mut buf, area);
3496 }
3497
3498 #[test]
3499 fn sparkline_with_max_value() {
3500 let area = Rect::new(0, 0, 20, 3);
3501 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3502 Sparkline::new(vec![5, 10, 15])
3503 .max_value(20)
3504 .render(&mut buf, area);
3505 }
3506
3507 #[test]
3508 fn sparkline_empty_data_is_noop() {
3509 let area = Rect::new(0, 0, 20, 3);
3510 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3511 Sparkline::new(vec![]).render(&mut buf, area);
3512 }
3513
3514 #[test]
3515 fn gauge_simple_renders_without_panic() {
3516 let area = Rect::new(0, 0, 30, 3);
3517 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3518 Gauge::new(0.65).label("65%").render(&mut buf, area);
3519 }
3520
3521 #[test]
3522 fn simple_gauge_ratio_is_clamped() {
3523 assert_eq!(Gauge::new(2.0).ratio(), 1.0);
3524 assert_eq!(Gauge::new(-1.0).ratio(), 0.0);
3525 }
3526
3527 #[test]
3528 fn paragraph_renders_without_panic() {
3529 let area = Rect::new(0, 0, 30, 8);
3530 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3531 Paragraph::new(
3532 "Hello world. This is a longer paragraph that should wrap across multiple lines.",
3533 )
3534 .render(&mut buf, area);
3535 }
3536
3537 #[test]
3538 fn paragraph_scrolls() {
3539 let text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8";
3540 let area = Rect::new(0, 0, 20, 3);
3541 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3542 Paragraph::new(text).scroll_offset(3).render(&mut buf, area);
3543 }
3544
3545 #[test]
3546 fn status_bar_renders_without_panic() {
3547 let area = Rect::new(0, 0, 60, 1);
3548 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3549 StatusBar::new()
3550 .left("Left")
3551 .center("Center")
3552 .right("Right")
3553 .render(&mut buf, area);
3554 }
3555
3556 #[test]
3557 fn status_bar_only_left() {
3558 let area = Rect::new(0, 0, 20, 1);
3559 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3560 StatusBar::new().left("Hello").render(&mut buf, area);
3561 }
3562
3563 #[test]
3564 fn bordered_renders_without_panic() {
3565 let area = Rect::new(0, 0, 30, 10);
3566 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3567 Bordered::new("Container").render(&mut buf, area);
3568 }
3569
3570 #[test]
3571 fn bordered_returns_inner_area() {
3572 let area = Rect::new(0, 0, 30, 10);
3573 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3574 let inner = Bordered::new("Title").render_inner(&mut buf, area);
3575 assert_eq!(inner.x, 1);
3576 assert_eq!(inner.y, 1);
3577 assert_eq!(inner.width, 28);
3578 assert_eq!(inner.height, 8);
3579 }
3580}