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 effects::{EffectKind, EffectPlayer, LoaderKind, LoaderPlayer},
10 interaction::{
11 HitRegion, MouseCursor, ScrollRowHit, SelectableSpan, SelectionGroup, TextRange,
12 WidgetAction, WidgetId, WidgetRole, WidgetState, WidgetValue,
13 },
14 style::{Modifier, Style},
15 theme::{Theme, ThemeTokens},
16 widgets::{
17 Widget,
18 block::{Block, BorderStyle},
19 },
20};
21
22pub use scrin;
23
24pub mod prelude {
26 pub use crate::{
27 Aisling, AislingEffect, AislingExt, AislingPalette, Align, Bordered, FlickerPanel, Gauge,
28 GlyphRain, List, NebulaGauge, NeonBorder, OrbField, Paragraph, PulseRing, Radar,
29 ScrinEffect, ScrinLoader, SignalPanel, Sparkline, SplitDirection, SplitPane, StatusBar,
30 StreamPanel, TabBar, Table, WaveType, Waveform, scrin,
31 };
32 pub use scrin::effects::{EffectKind, LoaderKind};
33 pub use scrin::interaction::{HitRegion, WidgetId, WidgetRole};
34 pub use scrin::theme::{Theme, ThemeTokens};
35 pub use scrin::widgets::Widget;
36}
37
38impl Default for AislingPalette {
40 fn default() -> Self {
41 Self::cypherpunk()
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct AislingPalette {
48 pub low: Color,
49 pub mid: Color,
50 pub high: Color,
51 pub pulse: Color,
52 pub shadow: Color,
53}
54
55impl AislingPalette {
56 #[must_use]
58 pub const fn cypherpunk() -> Self {
59 Self {
60 low: Color::rgb(0, 230, 255),
61 mid: Color::rgb(0, 255, 136),
62 high: Color::rgb(240, 255, 255),
63 pulse: Color::rgb(255, 200, 0),
64 shadow: Color::rgb(8, 12, 20),
65 }
66 }
67
68 #[must_use]
70 pub const fn dream() -> Self {
71 Self {
72 low: Color::rgb(58, 160, 220),
73 mid: Color::rgb(120, 100, 180),
74 high: Color::rgb(220, 210, 170),
75 pulse: Color::rgb(0, 200, 200),
76 shadow: Color::rgb(12, 14, 24),
77 }
78 }
79
80 #[must_use]
82 pub const fn phosphor() -> Self {
83 Self {
84 low: Color::rgb(61, 255, 142),
85 mid: Color::rgb(19, 189, 112),
86 high: Color::rgb(210, 255, 181),
87 pulse: Color::rgb(135, 255, 221),
88 shadow: Color::rgb(7, 22, 16),
89 }
90 }
91
92 #[must_use]
94 pub const fn flare() -> Self {
95 Self {
96 low: Color::rgb(255, 170, 50),
97 mid: Color::rgb(255, 120, 30),
98 high: Color::rgb(255, 230, 140),
99 pulse: Color::rgb(255, 80, 60),
100 shadow: Color::rgb(24, 12, 8),
101 }
102 }
103
104 #[must_use]
106 pub const fn theme_tokens(self) -> ThemeTokens {
107 ThemeTokens::new(
108 self.shadow,
109 self.high,
110 self.low,
111 self.mid,
112 self.mid,
113 self.pulse,
114 self.pulse,
115 )
116 }
117
118 #[must_use]
120 pub const fn from_theme_tokens(tokens: ThemeTokens) -> Self {
121 Self {
122 low: tokens.dim,
123 mid: tokens.accent,
124 high: tokens.text,
125 pulse: tokens.warning,
126 shadow: tokens.panel,
127 }
128 }
129
130 #[must_use]
132 pub const fn theme(self) -> Theme {
133 Theme {
134 bg: self.shadow,
135 fg: self.high,
136 accent: self.mid,
137 accent_bright: self.low,
138 muted: self.low,
139 surface: self.shadow,
140 surface_bright: self.shadow,
141 error: self.pulse,
142 warning: self.pulse,
143 success: self.mid,
144 info: self.low,
145 border: self.low,
146 border_focus: self.pulse,
147 text_primary: self.high,
148 text_secondary: self.mid,
149 text_dim: self.low,
150 highlight_bg: self.mid,
151 highlight_fg: self.shadow,
152 glow: self.pulse,
153 }
154 }
155
156 #[must_use]
158 pub const fn from_theme(theme: Theme) -> Self {
159 Self {
160 low: theme.info,
161 mid: theme.accent,
162 high: theme.text_primary,
163 pulse: theme.glow,
164 shadow: theme.surface,
165 }
166 }
167
168 #[must_use]
170 pub fn block<'a>(self, title: &'a str) -> Block<'a> {
171 Block::new(title)
172 .with_borders(BorderStyle::Plain)
173 .with_theme_tokens(self.theme_tokens())
174 .with_border_color(self.low)
175 .with_inner_margin(Rect::ZERO)
176 }
177
178 fn lane(self, value: u64) -> Color {
179 match value % 4 {
180 0 => self.low,
181 1 => self.mid,
182 2 => self.high,
183 _ => self.pulse,
184 }
185 }
186}
187
188impl From<AislingPalette> for ThemeTokens {
189 fn from(value: AislingPalette) -> Self {
190 value.theme_tokens()
191 }
192}
193
194impl From<ThemeTokens> for AislingPalette {
195 fn from(value: ThemeTokens) -> Self {
196 Self::from_theme_tokens(value)
197 }
198}
199
200impl From<AislingPalette> for Theme {
201 fn from(value: AislingPalette) -> Self {
202 value.theme()
203 }
204}
205
206impl From<Theme> for AislingPalette {
207 fn from(value: Theme) -> Self {
208 Self::from_theme(value)
209 }
210}
211
212#[derive(Clone, Copy, Debug, Eq, PartialEq)]
214pub struct AislingEffect {
215 tick: u64,
216 intensity: u16,
217 palette: AislingPalette,
218 shimmer: bool,
219 scanlines: bool,
220 glow: bool,
221}
222
223impl AislingEffect {
224 #[must_use]
226 pub fn new(tick: u64) -> Self {
227 Self {
228 tick,
229 ..Self::default()
230 }
231 }
232
233 #[must_use]
235 pub fn tick(mut self, tick: u64) -> Self {
236 self.tick = tick;
237 self
238 }
239
240 #[must_use]
242 pub fn palette(mut self, palette: AislingPalette) -> Self {
243 self.palette = palette;
244 self
245 }
246
247 #[must_use]
249 pub fn intensity(mut self, intensity: u16) -> Self {
250 self.intensity = intensity.min(10);
251 self
252 }
253
254 #[must_use]
256 pub fn shimmer(mut self, enabled: bool) -> Self {
257 self.shimmer = enabled;
258 self
259 }
260
261 #[must_use]
263 pub fn scanlines(mut self, enabled: bool) -> Self {
264 self.scanlines = enabled;
265 self
266 }
267
268 #[must_use]
270 pub fn glow(mut self, enabled: bool) -> Self {
271 self.glow = enabled;
272 self
273 }
274
275 pub fn apply(self, area: Rect, buf: &mut Buffer) {
277 if is_empty(area) || self.intensity == 0 {
278 return;
279 }
280
281 let right = area.x.saturating_add(area.width);
282 let bottom = area.y.saturating_add(area.height);
283 let edge_phase = self.tick / 2;
284 let shimmer_gate = 11_u64.saturating_sub(u64::from(self.intensity.min(10)));
285
286 for y in area.y..bottom {
287 for x in area.x..right {
288 if self.scanlines && (u64::from(y) + edge_phase).is_multiple_of(3) {
289 set_cell_bg(buf, x, y, self.palette.shadow);
290 }
291
292 if self.shimmer {
293 let phase = u64::from(x) * 3 + u64::from(y) * 5 + self.tick;
294 if phase % 11 >= shimmer_gate {
295 set_cell_style(
296 buf,
297 x,
298 y,
299 Style::default()
300 .fg(self.palette.lane(phase))
301 .add_modifier(Modifier::BOLD),
302 );
303 }
304 }
305
306 if self.glow
307 && is_edge(area, x, y)
308 && (u64::from(x) + u64::from(y) + edge_phase) % 5 == 0
309 {
310 set_cell_style(
311 buf,
312 x,
313 y,
314 Style::default()
315 .fg(self.palette.pulse)
316 .add_modifier(Modifier::BOLD),
317 );
318 }
319 }
320 }
321 }
322}
323
324impl Default for AislingEffect {
325 fn default() -> Self {
326 Self {
327 tick: 0,
328 intensity: 5,
329 palette: AislingPalette::default(),
330 shimmer: true,
331 scanlines: true,
332 glow: true,
333 }
334 }
335}
336
337#[derive(Clone, Debug, Eq, PartialEq)]
339pub struct Aisling<W> {
340 inner: W,
341 effect: AislingEffect,
342}
343
344impl<W> Aisling<W> {
345 #[must_use]
347 pub fn new(inner: W) -> Self {
348 Self {
349 inner,
350 effect: AislingEffect::default(),
351 }
352 }
353
354 #[must_use]
356 pub fn effect(mut self, effect: AislingEffect) -> Self {
357 self.effect = effect;
358 self
359 }
360
361 #[must_use]
363 pub fn tick(mut self, tick: u64) -> Self {
364 self.effect = self.effect.tick(tick);
365 self
366 }
367
368 #[must_use]
370 pub fn palette(mut self, palette: AislingPalette) -> Self {
371 self.effect = self.effect.palette(palette);
372 self
373 }
374
375 #[must_use]
377 pub fn intensity(mut self, intensity: u16) -> Self {
378 self.effect = self.effect.intensity(intensity);
379 self
380 }
381}
382
383impl<W: Widget> Widget for Aisling<W> {
384 fn render(&self, buf: &mut Buffer, area: Rect) {
385 self.inner.render(buf, area);
386 self.effect.apply(area, buf);
387 }
388}
389
390pub trait AislingExt: Widget + Sized {
392 #[must_use]
394 fn aisling(self) -> Aisling<Self> {
395 Aisling::new(self)
396 }
397}
398
399impl<W: Widget> AislingExt for W {}
400
401#[derive(Clone, Debug)]
403pub struct ScrinEffect<'a> {
404 kind: EffectKind,
405 text: Cow<'a, str>,
406 tick: u64,
407 duration: Option<usize>,
408 seed: Option<u64>,
409 palette: AislingPalette,
410 block: Option<Block<'a>>,
411}
412
413impl PartialEq for ScrinEffect<'_> {
414 fn eq(&self, other: &Self) -> bool {
415 self.kind == other.kind
416 && self.text == other.text
417 && self.tick == other.tick
418 && self.duration == other.duration
419 && self.seed == other.seed
420 && self.palette == other.palette
421 && option_block_eq(self.block.as_ref(), other.block.as_ref())
422 }
423}
424
425impl<'a> ScrinEffect<'a> {
426 #[must_use]
428 pub fn new(kind: EffectKind, text: impl Into<Cow<'a, str>>) -> Self {
429 Self {
430 kind,
431 text: text.into(),
432 tick: 0,
433 duration: None,
434 seed: None,
435 palette: AislingPalette::cypherpunk(),
436 block: None,
437 }
438 }
439
440 #[must_use]
442 pub fn tick(mut self, tick: u64) -> Self {
443 self.tick = tick;
444 self
445 }
446
447 #[must_use]
449 pub fn duration(mut self, duration: usize) -> Self {
450 self.duration = Some(duration.max(1));
451 self
452 }
453
454 #[must_use]
456 pub fn seed(mut self, seed: u64) -> Self {
457 self.seed = Some(seed);
458 self
459 }
460
461 #[must_use]
463 pub fn palette(mut self, palette: AislingPalette) -> Self {
464 self.palette = palette;
465 self
466 }
467
468 #[must_use]
470 pub fn block(mut self, block: Block<'a>) -> Self {
471 self.block = Some(block);
472 self
473 }
474
475 pub fn render_with_interaction(
477 &self,
478 frame: &mut Frame<'_>,
479 id: impl Into<WidgetId>,
480 area: Rect,
481 ) {
482 let id = id.into();
483 self.render(frame.buffer(), area);
484 frame.register_hit_region(
485 HitRegion::new(id, area)
486 .with_role(WidgetRole::Effect)
487 .with_label(format!("{} effect", self.kind.name()))
488 .with_action(WidgetAction::Focus)
489 .with_value(WidgetValue::Status(self.kind.name().to_string())),
490 );
491 frame.mark_dirty(area);
492 }
493
494 fn player_for(&self, area: Rect) -> EffectPlayer {
495 let mut player = EffectPlayer::new(self.kind, self.text.as_ref())
496 .with_accent(self.palette.mid)
497 .with_size(
498 usize::from(area.width.max(1)),
499 usize::from(area.height.max(1)),
500 )
501 .with_gradient_colors(
502 vec![
503 self.palette.low,
504 self.palette.mid,
505 self.palette.high,
506 self.palette.pulse,
507 ],
508 45.0,
509 );
510 if let Some(duration) = self.duration {
511 player = player.with_duration(duration);
512 }
513 if let Some(seed) = self.seed {
514 player = player.with_seed(seed);
515 }
516 let total = player.total_frames().max(1);
517 player.set_frame((self.tick as usize) % total);
518 player
519 }
520}
521
522impl Widget for ScrinEffect<'_> {
523 fn render(&self, buf: &mut Buffer, area: Rect) {
524 let inner = self
525 .block
526 .as_ref()
527 .map_or(area, |block| block_content_area(block, area));
528 if let Some(block) = &self.block {
529 block.render(buf, area);
530 }
531 if is_empty(inner) {
532 return;
533 }
534 self.player_for(inner).render_to_buffer(buf, inner);
535 }
536}
537
538#[derive(Clone, Debug)]
540pub struct ScrinLoader<'a> {
541 kind: LoaderKind,
542 progress: f32,
543 tick: u64,
544 label: Option<Cow<'a, str>>,
545 unit: Option<Cow<'a, str>>,
546 fraction: bool,
547 palette: AislingPalette,
548 block: Option<Block<'a>>,
549}
550
551impl PartialEq for ScrinLoader<'_> {
552 fn eq(&self, other: &Self) -> bool {
553 self.kind == other.kind
554 && self.progress == other.progress
555 && self.tick == other.tick
556 && self.label == other.label
557 && self.unit == other.unit
558 && self.fraction == other.fraction
559 && self.palette == other.palette
560 && option_block_eq(self.block.as_ref(), other.block.as_ref())
561 }
562}
563
564impl<'a> ScrinLoader<'a> {
565 #[must_use]
567 pub fn new(kind: LoaderKind, progress: f32) -> Self {
568 Self {
569 kind,
570 progress: progress.clamp(0.0, 1.0),
571 tick: 0,
572 label: None,
573 unit: None,
574 fraction: false,
575 palette: AislingPalette::cypherpunk(),
576 block: None,
577 }
578 }
579
580 #[must_use]
582 pub fn progress(&self) -> f32 {
583 self.progress
584 }
585
586 #[must_use]
588 pub fn tick(mut self, tick: u64) -> Self {
589 self.tick = tick;
590 self
591 }
592
593 #[must_use]
595 pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
596 self.label = Some(label.into());
597 self
598 }
599
600 #[must_use]
602 pub fn unit(mut self, unit: impl Into<Cow<'a, str>>) -> Self {
603 self.unit = Some(unit.into());
604 self
605 }
606
607 #[must_use]
609 pub fn fraction(mut self, fraction: bool) -> Self {
610 self.fraction = fraction;
611 self
612 }
613
614 #[must_use]
616 pub fn palette(mut self, palette: AislingPalette) -> Self {
617 self.palette = palette;
618 self
619 }
620
621 #[must_use]
623 pub fn block(mut self, block: Block<'a>) -> Self {
624 self.block = Some(block);
625 self
626 }
627
628 pub fn render_with_interaction(
630 &self,
631 frame: &mut Frame<'_>,
632 id: impl Into<WidgetId>,
633 area: Rect,
634 ) {
635 let id = id.into();
636 self.render(frame.buffer(), area);
637 frame.register_hit_region(
638 HitRegion::new(id, area)
639 .with_role(WidgetRole::StatusIndicator)
640 .with_label(self.label.as_deref().unwrap_or_else(|| self.kind.name()))
641 .with_action(WidgetAction::Focus)
642 .with_value(WidgetValue::Percent((self.progress * 100.0).round() as u16)),
643 );
644 frame.mark_dirty(area);
645 }
646
647 fn player_for(&self, area: Rect) -> LoaderPlayer {
648 let mut player = LoaderPlayer::new(self.kind)
649 .with_accent(self.palette.mid)
650 .with_size(
651 usize::from(area.width.max(1)),
652 usize::from(area.height.max(1)),
653 )
654 .with_fraction(self.fraction)
655 .with_gradient_colors(
656 vec![
657 self.palette.low,
658 self.palette.mid,
659 self.palette.high,
660 self.palette.pulse,
661 ],
662 45.0,
663 );
664 if let Some(label) = &self.label {
665 player = player.with_label(label.to_string());
666 }
667 if let Some(unit) = &self.unit {
668 player = player.with_unit(unit.as_ref());
669 }
670 player
671 }
672}
673
674impl Widget for ScrinLoader<'_> {
675 fn render(&self, buf: &mut Buffer, area: Rect) {
676 let inner = self
677 .block
678 .as_ref()
679 .map_or(area, |block| block_content_area(block, area));
680 if let Some(block) = &self.block {
681 block.render(buf, area);
682 }
683 if is_empty(inner) {
684 return;
685 }
686 self.player_for(inner).render(
687 self.tick as usize,
688 LoaderPlayer::progress_from_fraction(self.progress),
689 buf,
690 inner,
691 );
692 }
693}
694
695#[derive(Clone, Debug)]
697pub struct GlyphRain<'a> {
698 tick: u64,
699 density: u16,
700 glyphs: Cow<'a, str>,
701 palette: AislingPalette,
702 block: Option<Block<'a>>,
703}
704
705impl PartialEq for GlyphRain<'_> {
706 fn eq(&self, other: &Self) -> bool {
707 self.tick == other.tick
708 && self.density == other.density
709 && self.glyphs == other.glyphs
710 && self.palette == other.palette
711 && option_block_eq(self.block.as_ref(), other.block.as_ref())
712 }
713}
714
715impl Eq for GlyphRain<'_> {}
716
717impl<'a> GlyphRain<'a> {
718 #[must_use]
720 pub fn new(tick: u64) -> Self {
721 Self {
722 tick,
723 density: 34,
724 glyphs: Cow::Borrowed("01#$*+<>[]{}"),
725 palette: AislingPalette::phosphor(),
726 block: None,
727 }
728 }
729
730 #[must_use]
732 pub fn tick(mut self, tick: u64) -> Self {
733 self.tick = tick;
734 self
735 }
736
737 #[must_use]
739 pub fn density(mut self, density: u16) -> Self {
740 self.density = density.min(100);
741 self
742 }
743
744 #[must_use]
746 pub fn glyphs(mut self, glyphs: impl Into<Cow<'a, str>>) -> Self {
747 self.glyphs = glyphs.into();
748 self
749 }
750
751 #[must_use]
753 pub fn palette(mut self, palette: AislingPalette) -> Self {
754 self.palette = palette;
755 self
756 }
757
758 #[must_use]
760 pub fn block(mut self, block: Block<'a>) -> Self {
761 self.block = Some(block);
762 self
763 }
764}
765
766impl Widget for GlyphRain<'_> {
767 fn render(&self, buf: &mut Buffer, area: Rect) {
768 let inner = self
769 .block
770 .as_ref()
771 .map_or(area, |block| block_content_area(block, area));
772 if let Some(block) = &self.block {
773 block.render(buf, area);
774 }
775 if is_empty(inner) || self.density == 0 {
776 return;
777 }
778
779 let glyphs: Vec<char> = self.glyphs.chars().collect();
780 if glyphs.is_empty() {
781 return;
782 }
783
784 let right = inner.x.saturating_add(inner.width);
785 let bottom = inner.y.saturating_add(inner.height);
786 for y in inner.y..bottom {
787 for x in inner.x..right {
788 let noise = field_noise(x, y, self.tick);
789 if noise % 100 >= u64::from(self.density) {
790 continue;
791 }
792
793 let glyph = glyphs[(noise as usize + usize::from(y)) % glyphs.len()];
794 let head = (noise + self.tick) % 9 == 0;
795 let style = if head {
796 Style::default()
797 .fg(self.palette.high)
798 .add_modifier(Modifier::BOLD)
799 } else {
800 Style::default().fg(self.palette.lane(noise + self.tick))
801 };
802
803 set_styled_char(buf, x, y, glyph, style);
804 }
805 }
806 }
807}
808
809#[derive(Clone, Debug)]
811pub struct NebulaGauge<'a> {
812 ratio: f64,
813 tick: u64,
814 label: Option<Cow<'a, str>>,
815 palette: AislingPalette,
816 block: Option<Block<'a>>,
817}
818
819impl PartialEq for NebulaGauge<'_> {
820 fn eq(&self, other: &Self) -> bool {
821 self.ratio == other.ratio
822 && self.tick == other.tick
823 && self.label == other.label
824 && self.palette == other.palette
825 && option_block_eq(self.block.as_ref(), other.block.as_ref())
826 }
827}
828
829impl<'a> NebulaGauge<'a> {
830 #[must_use]
832 pub fn new(ratio: f64) -> Self {
833 Self {
834 ratio: ratio.clamp(0.0, 1.0),
835 tick: 0,
836 label: None,
837 palette: AislingPalette::dream(),
838 block: None,
839 }
840 }
841
842 #[must_use]
844 pub fn ratio(&self) -> f64 {
845 self.ratio
846 }
847
848 #[must_use]
850 pub fn tick(mut self, tick: u64) -> Self {
851 self.tick = tick;
852 self
853 }
854
855 #[must_use]
857 pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
858 self.label = Some(label.into());
859 self
860 }
861
862 #[must_use]
864 pub fn palette(mut self, palette: AislingPalette) -> Self {
865 self.palette = palette;
866 self
867 }
868
869 #[must_use]
871 pub fn block(mut self, block: Block<'a>) -> Self {
872 self.block = Some(block);
873 self
874 }
875}
876
877impl Widget for NebulaGauge<'_> {
878 fn render(&self, buf: &mut Buffer, area: Rect) {
879 let inner = self
880 .block
881 .as_ref()
882 .map_or(area, |block| block_content_area(block, area));
883 if let Some(block) = &self.block {
884 block.render(buf, area);
885 }
886 if is_empty(inner) {
887 return;
888 }
889
890 let right = inner.x.saturating_add(inner.width);
891 let bottom = inner.y.saturating_add(inner.height);
892 let filled = (f64::from(inner.width) * self.ratio).round() as u16;
893
894 for y in inner.y..bottom {
895 for x in inner.x..right {
896 let offset = x.saturating_sub(inner.x);
897 let flow = u64::from(offset) + u64::from(y) * 2 + self.tick;
898 if offset < filled {
899 set_styled_char(
900 buf,
901 x,
902 y,
903 '█',
904 Style::default()
905 .fg(self.palette.lane(flow))
906 .bg(self.palette.shadow)
907 .add_modifier(Modifier::BOLD),
908 );
909 } else {
910 set_styled_char(buf, x, y, '░', Style::default().fg(self.palette.shadow));
911 }
912 }
913 }
914
915 if let Some(label) = &self.label {
916 let row = inner.y + inner.height / 2;
917 let label_width = label.chars().count().min(usize::from(inner.width)) as u16;
918 let start = inner.x + inner.width.saturating_sub(label_width) / 2;
919 paint_text(
920 Rect::new(start, row, label_width, 1),
921 buf,
922 label.as_ref(),
923 Style::default()
924 .fg(self.palette.high)
925 .add_modifier(Modifier::BOLD),
926 );
927 }
928 }
929}
930
931#[derive(Clone, Debug, Eq, PartialEq)]
933pub struct SignalPanel<'a> {
934 title: Cow<'a, str>,
935 lines: Vec<Cow<'a, str>>,
936 tick: u64,
937 palette: AislingPalette,
938}
939
940impl<'a> SignalPanel<'a> {
941 #[must_use]
943 pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
944 Self {
945 title: title.into(),
946 lines: Vec::new(),
947 tick: 0,
948 palette: AislingPalette::flare(),
949 }
950 }
951
952 #[must_use]
954 pub fn line(mut self, line: impl Into<Cow<'a, str>>) -> Self {
955 self.lines.push(line.into());
956 self
957 }
958
959 #[must_use]
961 pub fn lines<I, S>(mut self, lines: I) -> Self
962 where
963 I: IntoIterator<Item = S>,
964 S: Into<Cow<'a, str>>,
965 {
966 self.lines = lines.into_iter().map(Into::into).collect();
967 self
968 }
969
970 #[must_use]
972 pub fn tick(mut self, tick: u64) -> Self {
973 self.tick = tick;
974 self
975 }
976
977 #[must_use]
979 pub fn palette(mut self, palette: AislingPalette) -> Self {
980 self.palette = palette;
981 self
982 }
983}
984
985impl Widget for SignalPanel<'_> {
986 fn render(&self, buf: &mut Buffer, area: Rect) {
987 if is_empty(area) {
988 return;
989 }
990
991 let block = Block::new(self.title.as_ref())
992 .with_borders(BorderStyle::Plain)
993 .with_border_color(self.palette.mid)
994 .with_inner_margin(Rect::ZERO);
995 let inner = block_content_area(&block, area);
996 block.render(buf, area);
997 if is_empty(inner) {
998 return;
999 }
1000
1001 let bars_width = inner.width.min(12);
1002 let text_width = inner.width.saturating_sub(bars_width.saturating_add(1));
1003 let max_lines = usize::from(inner.height);
1004
1005 for (index, line) in self.lines.iter().take(max_lines).enumerate() {
1006 paint_text(
1007 Rect::new(inner.x, inner.y + index as u16, text_width, 1),
1008 buf,
1009 line.as_ref(),
1010 Style::default().fg(self.palette.high),
1011 );
1012 }
1013
1014 if bars_width == 0 {
1015 return;
1016 }
1017
1018 let bars_x = inner.x + inner.width.saturating_sub(bars_width);
1019 for row in 0..inner.height {
1020 for column in 0..bars_width {
1021 let x = bars_x + column;
1022 let y = inner.y + row;
1023 let noise = field_noise(x, y, self.tick / 2);
1024 let active = (noise + self.tick + u64::from(column)) % 7 <= 3;
1025 let symbol = if active { '╱' } else { '·' };
1026 let style = if active {
1027 Style::default()
1028 .fg(self.palette.lane(noise))
1029 .add_modifier(Modifier::BOLD)
1030 } else {
1031 Style::default().fg(self.palette.shadow)
1032 };
1033 set_styled_char(buf, x, y, symbol, style);
1034 }
1035 }
1036 }
1037}
1038
1039#[derive(Clone, Debug)]
1041pub struct FlickerPanel<'a> {
1042 text: Cow<'a, str>,
1043 tick: u64,
1044 intensity: u16,
1045 palette: AislingPalette,
1046 block: Option<Block<'a>>,
1047}
1048
1049impl PartialEq for FlickerPanel<'_> {
1050 fn eq(&self, other: &Self) -> bool {
1051 self.text == other.text
1052 && self.tick == other.tick
1053 && self.intensity == other.intensity
1054 && self.palette == other.palette
1055 && option_block_eq(self.block.as_ref(), other.block.as_ref())
1056 }
1057}
1058
1059impl Eq for FlickerPanel<'_> {}
1060
1061impl<'a> FlickerPanel<'a> {
1062 #[must_use]
1064 pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
1065 Self {
1066 text: text.into(),
1067 tick: 0,
1068 intensity: 5,
1069 palette: AislingPalette::dream(),
1070 block: None,
1071 }
1072 }
1073
1074 #[must_use]
1076 pub fn tick(mut self, tick: u64) -> Self {
1077 self.tick = tick;
1078 self
1079 }
1080
1081 #[must_use]
1083 pub fn intensity(mut self, intensity: u16) -> Self {
1084 self.intensity = intensity.min(10);
1085 self
1086 }
1087
1088 #[must_use]
1090 pub fn palette(mut self, palette: AislingPalette) -> Self {
1091 self.palette = palette;
1092 self
1093 }
1094
1095 #[must_use]
1097 pub fn block(mut self, block: Block<'a>) -> Self {
1098 self.block = Some(block);
1099 self
1100 }
1101}
1102
1103impl Widget for FlickerPanel<'_> {
1104 fn render(&self, buf: &mut Buffer, area: Rect) {
1105 let inner = self
1106 .block
1107 .as_ref()
1108 .map_or(area, |block| block_content_area(block, area));
1109 if let Some(block) = &self.block {
1110 block.render(buf, area);
1111 }
1112 if is_empty(inner) || self.intensity == 0 {
1113 return;
1114 }
1115
1116 let glitch_chars: Vec<char> = "░▒▓█▀▄▌▐│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬".chars().collect();
1117 let text_chars: Vec<char> = self.text.chars().collect();
1118 if text_chars.is_empty() {
1119 return;
1120 }
1121
1122 let right = inner.x.saturating_add(inner.width);
1123 let bottom = inner.y.saturating_add(inner.height);
1124 for y in inner.y..bottom {
1125 for x in inner.x..right {
1126 let col = usize::from(x.saturating_sub(inner.x));
1127 let noise = field_noise(x, y, self.tick);
1128 let glitch_gate = 11_u64.saturating_sub(u64::from(self.intensity));
1129
1130 let (ch, style) = if noise % 11 >= glitch_gate {
1131 let g = glitch_chars[(noise as usize) % glitch_chars.len()];
1132 (
1133 g,
1134 Style::default()
1135 .fg(self.palette.pulse)
1136 .add_modifier(Modifier::BOLD),
1137 )
1138 } else if col < text_chars.len() {
1139 let c = text_chars[col];
1140 let flicker = (noise + self.tick) % 9 == 0;
1141 let style = if flicker {
1142 Style::default()
1143 .fg(self.palette.high)
1144 .add_modifier(Modifier::BOLD)
1145 } else {
1146 Style::default().fg(self.palette.mid)
1147 };
1148 (c, style)
1149 } else {
1150 (' ', Style::default())
1151 };
1152 set_styled_char(buf, x, y, ch, style);
1153 }
1154 }
1155 }
1156}
1157
1158#[derive(Clone, Debug)]
1160pub struct Waveform<'a> {
1161 tick: u64,
1162 frequency: f64,
1163 amplitude: f64,
1164 wave_type: WaveType,
1165 palette: AislingPalette,
1166 block: Option<Block<'a>>,
1167}
1168
1169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1170pub enum WaveType {
1171 Sine,
1172 Square,
1173 Sawtooth,
1174 Triangle,
1175}
1176
1177impl PartialEq for Waveform<'_> {
1178 fn eq(&self, other: &Self) -> bool {
1179 self.tick == other.tick
1180 && self.frequency == other.frequency
1181 && self.amplitude == other.amplitude
1182 && self.wave_type == other.wave_type
1183 && self.palette == other.palette
1184 && option_block_eq(self.block.as_ref(), other.block.as_ref())
1185 }
1186}
1187
1188impl Eq for Waveform<'_> {}
1189
1190impl<'a> Waveform<'a> {
1191 #[must_use]
1193 pub fn new(frequency: f64, amplitude: f64) -> Self {
1194 Self {
1195 tick: 0,
1196 frequency,
1197 amplitude: amplitude.clamp(0.0, 1.0),
1198 wave_type: WaveType::Sine,
1199 palette: AislingPalette::phosphor(),
1200 block: None,
1201 }
1202 }
1203
1204 #[must_use]
1206 pub fn tick(mut self, tick: u64) -> Self {
1207 self.tick = tick;
1208 self
1209 }
1210
1211 #[must_use]
1213 pub fn wave_type(mut self, wave_type: WaveType) -> Self {
1214 self.wave_type = wave_type;
1215 self
1216 }
1217
1218 #[must_use]
1220 pub fn palette(mut self, palette: AislingPalette) -> Self {
1221 self.palette = palette;
1222 self
1223 }
1224
1225 #[must_use]
1227 pub fn block(mut self, block: Block<'a>) -> Self {
1228 self.block = Some(block);
1229 self
1230 }
1231
1232 fn sample(&self, phase: f64) -> f64 {
1233 let t = phase.fract();
1234 match self.wave_type {
1235 WaveType::Sine => (std::f64::consts::TAU * t).sin(),
1236 WaveType::Square => {
1237 if t < 0.5 {
1238 1.0
1239 } else {
1240 -1.0
1241 }
1242 }
1243 WaveType::Sawtooth => 2.0 * t - 1.0,
1244 WaveType::Triangle => {
1245 if t < 0.5 {
1246 4.0 * t - 1.0
1247 } else {
1248 3.0 - 4.0 * t
1249 }
1250 }
1251 }
1252 }
1253}
1254
1255impl Widget for Waveform<'_> {
1256 fn render(&self, buf: &mut Buffer, area: Rect) {
1257 let inner = self
1258 .block
1259 .as_ref()
1260 .map_or(area, |block| block_content_area(block, area));
1261 if let Some(block) = &self.block {
1262 block.render(buf, area);
1263 }
1264 if is_empty(inner) || inner.height < 3 {
1265 return;
1266 }
1267
1268 let mid_y = inner.y + inner.height / 2;
1269 let half = (inner.height / 2) as f64;
1270
1271 for col in 0..inner.width {
1272 let phase = f64::from(col) / f64::from(inner.width) * self.frequency
1273 + f64::from(self.tick as u32) * 0.05;
1274 let sample = self.sample(phase);
1275 let offset = (sample * self.amplitude * half).round() as i16;
1276
1277 let y = mid_y as i16 + offset;
1278 if y >= inner.y as i16 && y < (inner.y + inner.height) as i16 {
1279 let noise = field_noise(inner.x + col, y as u16, self.tick);
1280 set_styled_char(
1281 buf,
1282 inner.x + col,
1283 y as u16,
1284 '█',
1285 Style::default()
1286 .fg(self.palette.lane(noise))
1287 .add_modifier(Modifier::BOLD),
1288 );
1289 }
1290 }
1291 }
1292}
1293
1294#[derive(Clone, Debug, Eq, PartialEq)]
1296pub struct PulseRing {
1297 tick: u64,
1298 rings: u16,
1299 palette: AislingPalette,
1300}
1301
1302impl PulseRing {
1303 #[must_use]
1305 pub fn new(rings: u16) -> Self {
1306 Self {
1307 tick: 0,
1308 rings: rings.max(1),
1309 palette: AislingPalette::dream(),
1310 }
1311 }
1312
1313 #[must_use]
1315 pub fn tick(mut self, tick: u64) -> Self {
1316 self.tick = tick;
1317 self
1318 }
1319
1320 #[must_use]
1322 pub fn palette(mut self, palette: AislingPalette) -> Self {
1323 self.palette = palette;
1324 self
1325 }
1326}
1327
1328impl Widget for PulseRing {
1329 fn render(&self, buf: &mut Buffer, area: Rect) {
1330 if is_empty(area) || self.rings == 0 {
1331 return;
1332 }
1333
1334 let cx = area.x + area.width / 2;
1335 let cy = area.y + area.height / 2;
1336 let max_radius = (area.width.min(area.height) / 2) as f64;
1337 if max_radius < 1.0 {
1338 return;
1339 }
1340
1341 let right = area.x.saturating_add(area.width);
1342 let bottom = area.y.saturating_add(area.height);
1343
1344 for y in area.y..bottom {
1345 for x in area.x..right {
1346 let dx = x as f64 - cx as f64;
1347 let dy = y as f64 - cy as f64;
1348 let dist = (dx * dx + dy * dy).sqrt();
1349
1350 for ring in 0..self.rings {
1351 let ring_phase = (self.tick as f64 * 0.1 + ring as f64 * 3.0) % max_radius;
1352 let diff = (dist - ring_phase).abs();
1353 if diff < 1.5 {
1354 let noise = field_noise(x, y, self.tick + ring as u64);
1355 let style = if diff < 0.8 {
1356 Style::default()
1357 .fg(self.palette.high)
1358 .add_modifier(Modifier::BOLD)
1359 } else {
1360 Style::default().fg(self.palette.lane(noise))
1361 };
1362 set_styled_char(buf, x, y, '○', style);
1363 break;
1364 }
1365 }
1366 }
1367 }
1368 }
1369}
1370
1371#[derive(Clone, Debug, Eq, PartialEq)]
1373pub struct Radar {
1374 tick: u64,
1375 sweep_speed: u64,
1376 palette: AislingPalette,
1377}
1378
1379impl Radar {
1380 #[must_use]
1382 pub fn new(sweep_speed: u64) -> Self {
1383 Self {
1384 tick: 0,
1385 sweep_speed: sweep_speed.max(1),
1386 palette: AislingPalette::phosphor(),
1387 }
1388 }
1389
1390 #[must_use]
1392 pub fn tick(mut self, tick: u64) -> Self {
1393 self.tick = tick;
1394 self
1395 }
1396
1397 #[must_use]
1399 pub fn palette(mut self, palette: AislingPalette) -> Self {
1400 self.palette = palette;
1401 self
1402 }
1403}
1404
1405impl Widget for Radar {
1406 fn render(&self, buf: &mut Buffer, area: Rect) {
1407 if is_empty(area) {
1408 return;
1409 }
1410
1411 let cx = area.x + area.width / 2;
1412 let cy = area.y + area.height / 2;
1413 let max_r = (area.width.min(area.height) / 2) as f64;
1414 if max_r < 1.0 {
1415 return;
1416 }
1417
1418 let sweep_angle = (self.tick as f64 / self.sweep_speed as f64) % (std::f64::consts::TAU);
1419 let trail_len = std::f64::consts::PI * 0.6;
1420
1421 let right = area.x.saturating_add(area.width);
1422 let bottom = area.y.saturating_add(area.height);
1423
1424 for y in area.y..bottom {
1425 for x in area.x..right {
1426 let dx = x as f64 - cx as f64;
1427 let dy = y as f64 - cy as f64;
1428 let dist = (dx * dx + dy * dy).sqrt();
1429
1430 if dist > max_r {
1431 continue;
1432 }
1433
1434 let angle = dy.atan2(dx);
1435 let norm_angle = if angle < 0.0 {
1436 angle + std::f64::consts::TAU
1437 } else {
1438 angle
1439 };
1440
1441 let ring = dist as u16;
1442 if ring > 0 && dist % (max_r / 3.0) < 0.5 {
1443 set_styled_char(buf, x, y, '·', Style::default().fg(self.palette.shadow));
1444 continue;
1445 }
1446
1447 let diff = (norm_angle - sweep_angle).abs();
1448 let diff = if diff > std::f64::consts::PI {
1449 std::f64::consts::TAU - diff
1450 } else {
1451 diff
1452 };
1453
1454 if diff < trail_len {
1455 let fade = 1.0 - diff / trail_len;
1456 let noise = field_noise(x, y, self.tick);
1457 let color = if fade > 0.6 {
1458 self.palette.high
1459 } else {
1460 self.palette.lane(noise)
1461 };
1462 set_styled_char(
1463 buf,
1464 x,
1465 y,
1466 if dist < 1.0 { '●' } else { '·' },
1467 Style::default().fg(color).add_modifier(Modifier::BOLD),
1468 );
1469 } else if (dist - 1.0).abs() < 0.5
1470 || (dist - max_r * 0.5).abs() < 0.5
1471 || (dist - max_r * 0.9).abs() < 0.5
1472 {
1473 set_styled_char(buf, x, y, '·', Style::default().fg(self.palette.shadow));
1474 }
1475 }
1476 }
1477 }
1478}
1479
1480#[derive(Clone, Debug)]
1482pub struct OrbField<'a> {
1483 tick: u64,
1484 count: u16,
1485 palette: AislingPalette,
1486 block: Option<Block<'a>>,
1487}
1488
1489impl PartialEq for OrbField<'_> {
1490 fn eq(&self, other: &Self) -> bool {
1491 self.tick == other.tick
1492 && self.count == other.count
1493 && self.palette == other.palette
1494 && option_block_eq(self.block.as_ref(), other.block.as_ref())
1495 }
1496}
1497
1498impl Eq for OrbField<'_> {}
1499
1500impl<'a> OrbField<'a> {
1501 #[must_use]
1503 pub fn new(count: u16) -> Self {
1504 Self {
1505 tick: 0,
1506 count,
1507 palette: AislingPalette::dream(),
1508 block: None,
1509 }
1510 }
1511
1512 #[must_use]
1514 pub fn tick(mut self, tick: u64) -> Self {
1515 self.tick = tick;
1516 self
1517 }
1518
1519 #[must_use]
1521 pub fn palette(mut self, palette: AislingPalette) -> Self {
1522 self.palette = palette;
1523 self
1524 }
1525
1526 #[must_use]
1528 pub fn block(mut self, block: Block<'a>) -> Self {
1529 self.block = Some(block);
1530 self
1531 }
1532}
1533
1534impl Widget for OrbField<'_> {
1535 fn render(&self, buf: &mut Buffer, area: Rect) {
1536 let inner = self
1537 .block
1538 .as_ref()
1539 .map_or(area, |block| block_content_area(block, area));
1540 if let Some(block) = &self.block {
1541 block.render(buf, area);
1542 }
1543 if is_empty(inner) || self.count == 0 {
1544 return;
1545 }
1546
1547 for i in 0..u64::from(self.count) {
1548 let seed = field_noise(i as u16, i as u16 / 7, i);
1549 let speed_x = (seed % 7) as f64 * 0.3 + 0.2;
1550 let speed_y = ((seed >> 3) % 5) as f64 * 0.2 + 0.1;
1551 let phase_x = seed as f64 * 0.1;
1552 let phase_y = (seed >> 5) as f64 * 0.13;
1553
1554 let base_x = (inner.x as f64)
1555 + ((self.tick as f64 * speed_x * 0.02 + phase_x).sin() * 0.5 + 0.5)
1556 * inner.width as f64;
1557 let base_y = (inner.y as f64)
1558 + ((self.tick as f64 * speed_y * 0.02 + phase_y).cos() * 0.5 + 0.5)
1559 * inner.height as f64;
1560
1561 let px = base_x.round() as u16;
1562 let py = base_y.round() as u16;
1563
1564 if px >= inner.x
1565 && px < inner.x + inner.width
1566 && py >= inner.y
1567 && py < inner.y + inner.height
1568 {
1569 let noise = field_noise(px, py, self.tick + i);
1570 let glyph = if noise % 3 == 0 {
1571 '◆'
1572 } else if noise % 3 == 1 {
1573 '◇'
1574 } else {
1575 '•'
1576 };
1577 set_styled_char(
1578 buf,
1579 px,
1580 py,
1581 glyph,
1582 Style::default()
1583 .fg(self.palette.lane(i))
1584 .add_modifier(Modifier::BOLD),
1585 );
1586 }
1587 }
1588 }
1589}
1590
1591#[derive(Clone, Debug)]
1593pub struct NeonBorder<'a> {
1594 tick: u64,
1595 speed: u64,
1596 palette: AislingPalette,
1597 inner: Block<'a>,
1598}
1599
1600impl PartialEq for NeonBorder<'_> {
1601 fn eq(&self, other: &Self) -> bool {
1602 self.tick == other.tick
1603 && self.speed == other.speed
1604 && self.palette == other.palette
1605 && block_eq(&self.inner, &other.inner)
1606 }
1607}
1608
1609impl<'a> NeonBorder<'a> {
1610 #[must_use]
1612 pub fn new(inner: Block<'a>) -> Self {
1613 Self {
1614 tick: 0,
1615 speed: 3,
1616 palette: AislingPalette::dream(),
1617 inner,
1618 }
1619 }
1620
1621 #[must_use]
1623 pub fn tick(mut self, tick: u64) -> Self {
1624 self.tick = tick;
1625 self
1626 }
1627
1628 #[must_use]
1630 pub fn speed(mut self, speed: u64) -> Self {
1631 self.speed = speed.max(1);
1632 self
1633 }
1634
1635 #[must_use]
1637 pub fn palette(mut self, palette: AislingPalette) -> Self {
1638 self.palette = palette;
1639 self
1640 }
1641
1642 pub fn render_border(&self, buf: &mut Buffer, area: Rect) -> Rect {
1644 let inner = block_content_area(&self.inner, area);
1645
1646 let right = area.x.saturating_add(area.width);
1647 let bottom = area.y.saturating_add(area.height);
1648 let perimeter = 2 * (area.width + area.height) as u64;
1649
1650 for y in area.y..bottom {
1651 for x in area.x..right {
1652 if !is_edge(area, x, y) {
1653 continue;
1654 }
1655
1656 let pos = if y == area.y {
1657 u64::from(x - area.x)
1658 } else if x + 1 == right {
1659 u64::from(area.width) + u64::from(y - area.y)
1660 } else if y + 1 == bottom {
1661 u64::from(area.width + area.height) + u64::from(right - x - 1)
1662 } else {
1663 u64::from(2 * area.width + area.height) + u64::from(bottom - y - 1)
1664 };
1665
1666 let phase = (self.tick * self.speed + pos) % perimeter;
1667 let color_idx = (phase * 4 / perimeter) as usize;
1668 let color = match color_idx {
1669 0 => self.palette.low,
1670 1 => self.palette.mid,
1671 2 => self.palette.high,
1672 _ => self.palette.pulse,
1673 };
1674
1675 let ch = if y == area.y || y + 1 == bottom {
1676 '─'
1677 } else {
1678 '│'
1679 };
1680 set_styled_char(
1681 buf,
1682 x,
1683 y,
1684 ch,
1685 Style::default().fg(color).add_modifier(Modifier::BOLD),
1686 );
1687 }
1688 }
1689
1690 inner
1691 }
1692}
1693
1694impl Widget for NeonBorder<'_> {
1695 fn render(&self, buf: &mut Buffer, area: Rect) {
1696 self.render_border(buf, area);
1697 }
1698}
1699
1700#[derive(Clone, Debug)]
1709pub struct StreamPanel<'a> {
1710 lines: Vec<Cow<'a, str>>,
1711 scroll_offset: u16,
1712 follow_tail: bool,
1713 show_line_numbers: bool,
1714 tick: u64,
1715 palette: AislingPalette,
1716 block: Option<Block<'a>>,
1717}
1718
1719impl PartialEq for StreamPanel<'_> {
1720 fn eq(&self, other: &Self) -> bool {
1721 self.lines == other.lines
1722 && self.scroll_offset == other.scroll_offset
1723 && self.follow_tail == other.follow_tail
1724 && self.show_line_numbers == other.show_line_numbers
1725 && self.tick == other.tick
1726 && self.palette == other.palette
1727 && option_block_eq(self.block.as_ref(), other.block.as_ref())
1728 }
1729}
1730
1731impl<'a> StreamPanel<'a> {
1732 #[must_use]
1734 pub fn new() -> Self {
1735 Self {
1736 lines: Vec::new(),
1737 scroll_offset: 0,
1738 follow_tail: true,
1739 show_line_numbers: false,
1740 tick: 0,
1741 palette: AislingPalette::phosphor(),
1742 block: None,
1743 }
1744 }
1745
1746 #[must_use]
1748 pub fn lines<I, S>(mut self, lines: I) -> Self
1749 where
1750 I: IntoIterator<Item = S>,
1751 S: Into<Cow<'a, str>>,
1752 {
1753 self.lines = lines.into_iter().map(Into::into).collect();
1754 self
1755 }
1756
1757 #[must_use]
1759 pub fn push_line(mut self, line: impl Into<Cow<'a, str>>) -> Self {
1760 self.lines.push(line.into());
1761 self
1762 }
1763
1764 #[must_use]
1766 pub fn scroll_offset(mut self, offset: u16) -> Self {
1767 self.scroll_offset = offset;
1768 self
1769 }
1770
1771 #[must_use]
1773 pub fn follow_tail(mut self, follow: bool) -> Self {
1774 self.follow_tail = follow;
1775 self
1776 }
1777
1778 #[must_use]
1780 pub fn show_line_numbers(mut self, show: bool) -> Self {
1781 self.show_line_numbers = show;
1782 self
1783 }
1784
1785 #[must_use]
1787 pub fn tick(mut self, tick: u64) -> Self {
1788 self.tick = tick;
1789 self
1790 }
1791
1792 #[must_use]
1794 pub fn palette(mut self, palette: AislingPalette) -> Self {
1795 self.palette = palette;
1796 self
1797 }
1798
1799 #[must_use]
1801 pub fn block(mut self, block: Block<'a>) -> Self {
1802 self.block = Some(block);
1803 self
1804 }
1805
1806 pub fn render_with_interaction(
1808 &self,
1809 frame: &mut Frame<'_>,
1810 id: impl Into<WidgetId>,
1811 area: Rect,
1812 ) {
1813 let id = id.into();
1814 self.render(frame.buffer(), area);
1815 for region in self.hit_regions(id.clone(), area) {
1816 frame.register_hit_region(region);
1817 }
1818 for span in self.selectable_spans(id.clone(), area) {
1819 frame.register_selectable_span(span);
1820 }
1821 if let Some((viewport, start, rows)) = self.scroll_region(id.clone(), area) {
1822 frame.register_scroll_region(id, viewport, start, rows);
1823 }
1824 frame.mark_dirty(area);
1825 }
1826
1827 #[must_use]
1829 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
1830 let region_id = id.into();
1831 let inner = self
1832 .block
1833 .as_ref()
1834 .map_or(area, |block| block_content_area(block, area));
1835 if is_empty(area) || is_empty(inner) {
1836 return Vec::new();
1837 }
1838
1839 let mut regions = vec![
1840 HitRegion::new(region_id.clone(), area)
1841 .with_role(WidgetRole::Transcript)
1842 .with_label("stream")
1843 .with_value(WidgetValue::Count(self.lines.len())),
1844 ];
1845 let start = self.visible_start(inner.height);
1846
1847 for row in 0..inner.height {
1848 let line_idx = start + row as usize;
1849 if line_idx >= self.lines.len() {
1850 break;
1851 }
1852
1853 let row_area = Rect::new(inner.x, inner.y + row, inner.width, 1);
1854 regions.push(
1855 HitRegion::new(format!("{}:line:{line_idx}", region_id.as_ref()), row_area)
1856 .with_role(WidgetRole::TranscriptRow)
1857 .with_label(self.lines[line_idx].as_ref())
1858 .with_action(WidgetAction::Select)
1859 .with_cursor(MouseCursor::Text)
1860 .with_row(line_idx)
1861 .with_value(WidgetValue::LineNumber(line_idx + 1))
1862 .with_z_index(1),
1863 );
1864 }
1865
1866 regions
1867 }
1868
1869 #[must_use]
1871 pub fn selectable_spans(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<SelectableSpan> {
1872 let region_id = id.into();
1873 let inner = self
1874 .block
1875 .as_ref()
1876 .map_or(area, |block| block_content_area(block, area));
1877 if is_empty(inner) {
1878 return Vec::new();
1879 }
1880
1881 let gutter_width = self.gutter_width();
1882 let text_width = inner.width.saturating_sub(gutter_width);
1883 if text_width == 0 {
1884 return Vec::new();
1885 }
1886
1887 let group = SelectionGroup::new(format!("{}:lines", region_id.as_ref()));
1888 let start = self.visible_start(inner.height);
1889 let mut spans = Vec::new();
1890
1891 for row in 0..inner.height {
1892 let line_idx = start + row as usize;
1893 if line_idx >= self.lines.len() {
1894 break;
1895 }
1896
1897 let text = clipped_text(self.lines[line_idx].as_ref(), text_width);
1898 if text.is_empty() {
1899 continue;
1900 }
1901
1902 let width = text.chars().count().min(usize::from(text_width)) as u16;
1903 spans.push(
1904 SelectableSpan::from_logical(
1905 format!("{}:span:{line_idx}", region_id.as_ref()),
1906 region_id.clone(),
1907 Rect::new(inner.x + gutter_width, inner.y + row, width, 1),
1908 TextRange::new(line_idx, 0, width as usize),
1909 text,
1910 )
1911 .with_group(group.clone()),
1912 );
1913 }
1914
1915 spans
1916 }
1917
1918 #[must_use]
1920 pub fn scroll_region(
1921 &self,
1922 id: impl Into<WidgetId>,
1923 area: Rect,
1924 ) -> Option<(Rect, usize, Vec<ScrollRowHit>)> {
1925 let region_id = id.into();
1926 let inner = self
1927 .block
1928 .as_ref()
1929 .map_or(area, |block| block_content_area(block, area));
1930 if is_empty(inner) {
1931 return None;
1932 }
1933
1934 let start = self.visible_start(inner.height);
1935 let mut rows = Vec::new();
1936 for row in 0..inner.height {
1937 let line_idx = start + row as usize;
1938 if line_idx >= self.lines.len() {
1939 break;
1940 }
1941 let row_id = WidgetId::new(format!("{}:line:{line_idx}", region_id.as_ref()));
1942 rows.push(
1943 ScrollRowHit::new(row_id.clone(), line_idx)
1944 .with_source_line(line_idx)
1945 .with_span_id(format!("{}:span:{line_idx}", region_id.as_ref()))
1946 .with_item_id(row_id),
1947 );
1948 }
1949
1950 Some((inner, start, rows))
1951 }
1952
1953 #[must_use]
1955 pub fn line_count(&self) -> usize {
1956 self.lines.len()
1957 }
1958
1959 fn gutter_width(&self) -> u16 {
1960 if self.show_line_numbers {
1961 let max_num = self.lines.len().max(1);
1962 let digits = format!("{max_num}").len() as u16;
1963 digits + 1
1964 } else {
1965 0
1966 }
1967 }
1968
1969 fn visible_start(&self, visible_height: u16) -> usize {
1971 let total = self.lines.len() as u16;
1972 if self.follow_tail || self.scroll_offset == 0 {
1973 let shown = visible_height.min(total);
1974 (total - shown) as usize
1975 } else {
1976 let max_top = total.saturating_sub(visible_height);
1977 (max_top.saturating_sub(self.scroll_offset)) as usize
1978 }
1979 }
1980}
1981
1982impl Default for StreamPanel<'_> {
1983 fn default() -> Self {
1984 Self::new()
1985 }
1986}
1987
1988impl Widget for StreamPanel<'_> {
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 gutter_width = self.gutter_width();
2002
2003 let text_width = inner.width.saturating_sub(gutter_width);
2004 if text_width == 0 {
2005 return;
2006 }
2007
2008 let start = self.visible_start(inner.height);
2009 let right = inner.x.saturating_add(inner.width);
2010 let total = self.lines.len();
2011
2012 for row in 0..inner.height {
2013 let line_idx = start + row as usize;
2014 let y = inner.y + row;
2015
2016 if line_idx >= total {
2017 break;
2018 }
2019
2020 if self.show_line_numbers {
2021 let num_str = format!(
2022 "{:>width$}",
2023 line_idx + 1,
2024 width = (gutter_width - 1) as usize
2025 );
2026 paint_text(
2027 Rect::new(inner.x, y, gutter_width.saturating_sub(1), 1),
2028 buf,
2029 &num_str,
2030 Style::default().fg(self.palette.shadow),
2031 );
2032 set_styled_char(
2033 buf,
2034 inner.x + gutter_width - 1,
2035 y,
2036 '│',
2037 Style::default().fg(self.palette.shadow),
2038 );
2039 }
2040
2041 let line = &self.lines[line_idx];
2042 let text_chars: Vec<char> = line.chars().collect();
2043
2044 for col in 0..text_width {
2045 let x = inner.x + gutter_width + col;
2046 if x >= right {
2047 break;
2048 }
2049 let ch = text_chars.get(col as usize).copied().unwrap_or(' ');
2050 let noise = field_noise(x, y, self.tick);
2051 let style = if ch == ' ' {
2052 Style::default()
2053 } else if (noise + self.tick) % 31 == 0 {
2054 Style::default()
2055 .fg(self.palette.pulse)
2056 .add_modifier(Modifier::BOLD)
2057 } else {
2058 Style::default().fg(self.palette.high)
2059 };
2060 set_styled_char(buf, x, y, ch, style);
2061 }
2062 }
2063
2064 if !self.follow_tail && self.scroll_offset > 0 {
2065 let indicator_y = inner.y;
2066 let indicator_style = Style::default()
2067 .fg(self.palette.pulse)
2068 .add_modifier(Modifier::BOLD);
2069 if inner.width > 2 {
2070 set_styled_char(buf, right - 2, indicator_y, '▲', indicator_style);
2071 }
2072 }
2073 }
2074}
2075
2076#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2082pub enum SplitDirection {
2083 Horizontal,
2084 Vertical,
2085}
2086
2087pub struct SplitPane {
2092 ratio: f64,
2093 direction: SplitDirection,
2094 divider: Option<char>,
2095}
2096
2097impl SplitPane {
2098 #[must_use]
2100 pub fn horizontal() -> Self {
2101 Self {
2102 ratio: 0.5,
2103 direction: SplitDirection::Horizontal,
2104 divider: None,
2105 }
2106 }
2107
2108 #[must_use]
2110 pub fn vertical() -> Self {
2111 Self {
2112 ratio: 0.5,
2113 direction: SplitDirection::Vertical,
2114 divider: None,
2115 }
2116 }
2117
2118 #[must_use]
2120 pub fn ratio(mut self, ratio: f64) -> Self {
2121 self.ratio = ratio.clamp(0.0, 1.0);
2122 self
2123 }
2124
2125 #[must_use]
2127 pub fn divider(mut self, divider: char) -> Self {
2128 self.divider = Some(divider);
2129 self
2130 }
2131
2132 pub fn split(&self, area: Rect) -> (Rect, Rect, Rect) {
2135 if is_empty(area) {
2136 return (Rect::ZERO, Rect::ZERO, Rect::ZERO);
2137 }
2138
2139 match self.direction {
2140 SplitDirection::Vertical => {
2141 let has_divider = self.divider.is_some() && area.width > 1;
2142 let available = if has_divider {
2143 area.width.saturating_sub(1)
2144 } else {
2145 area.width
2146 };
2147 let first_width = (f64::from(available) * self.ratio).round() as u16;
2148 let second_width = available.saturating_sub(first_width);
2149
2150 let a = Rect::new(area.x, area.y, first_width, area.height);
2151 let div = if has_divider {
2152 Rect::new(area.x + first_width, area.y, 1, area.height)
2153 } else {
2154 Rect::ZERO
2155 };
2156 let b_x = area.x + first_width + if has_divider { 1 } else { 0 };
2157 let b = Rect::new(b_x, area.y, second_width, area.height);
2158 (a, b, div)
2159 }
2160 SplitDirection::Horizontal => {
2161 let has_divider = self.divider.is_some() && area.height > 1;
2162 let available = if has_divider {
2163 area.height.saturating_sub(1)
2164 } else {
2165 area.height
2166 };
2167 let first_height = (f64::from(available) * self.ratio).round() as u16;
2168 let second_height = available.saturating_sub(first_height);
2169
2170 let a = Rect::new(area.x, area.y, area.width, first_height);
2171 let div = if has_divider {
2172 Rect::new(area.x, area.y + first_height, area.width, 1)
2173 } else {
2174 Rect::ZERO
2175 };
2176 let b_y = area.y + first_height + if has_divider { 1 } else { 0 };
2177 let b = Rect::new(area.x, b_y, area.width, second_height);
2178 (a, b, div)
2179 }
2180 }
2181 }
2182
2183 pub fn render_divider(&self, buf: &mut Buffer, divider_area: Rect, palette: AislingPalette) {
2185 if is_empty(divider_area) {
2186 return;
2187 }
2188 let ch = self.divider.unwrap_or(' ');
2189 let style = Style::default().fg(palette.mid);
2190 for y in divider_area.y..divider_area.y.saturating_add(divider_area.height) {
2191 for x in divider_area.x..divider_area.x.saturating_add(divider_area.width) {
2192 set_styled_char(buf, x, y, ch, style);
2193 }
2194 }
2195 }
2196}
2197
2198#[derive(Clone, Debug)]
2204pub struct List<'a> {
2205 items: Vec<Cow<'a, str>>,
2206 selected: Option<usize>,
2207 scroll_offset: u16,
2208 tick: u64,
2209 palette: AislingPalette,
2210 block: Option<Block<'a>>,
2211}
2212
2213impl PartialEq for List<'_> {
2214 fn eq(&self, other: &Self) -> bool {
2215 self.items == other.items
2216 && self.selected == other.selected
2217 && self.scroll_offset == other.scroll_offset
2218 && self.tick == other.tick
2219 && self.palette == other.palette
2220 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2221 }
2222}
2223
2224impl<'a> List<'a> {
2225 #[must_use]
2227 pub fn new() -> Self {
2228 Self {
2229 items: Vec::new(),
2230 selected: None,
2231 scroll_offset: 0,
2232 tick: 0,
2233 palette: AislingPalette::cypherpunk(),
2234 block: None,
2235 }
2236 }
2237
2238 #[must_use]
2240 pub fn item(mut self, item: impl Into<Cow<'a, str>>) -> Self {
2241 self.items.push(item.into());
2242 self
2243 }
2244
2245 #[must_use]
2247 pub fn items<I, S>(mut self, items: I) -> Self
2248 where
2249 I: IntoIterator<Item = S>,
2250 S: Into<Cow<'a, str>>,
2251 {
2252 self.items = items.into_iter().map(Into::into).collect();
2253 self
2254 }
2255
2256 #[must_use]
2258 pub fn selected(mut self, index: Option<usize>) -> Self {
2259 self.selected = index;
2260 self
2261 }
2262
2263 #[must_use]
2265 pub fn scroll_offset(mut self, offset: u16) -> Self {
2266 self.scroll_offset = offset;
2267 self
2268 }
2269
2270 #[must_use]
2272 pub fn tick(mut self, tick: u64) -> Self {
2273 self.tick = tick;
2274 self
2275 }
2276
2277 #[must_use]
2279 pub fn palette(mut self, palette: AislingPalette) -> Self {
2280 self.palette = palette;
2281 self
2282 }
2283
2284 #[must_use]
2286 pub fn block(mut self, block: Block<'a>) -> Self {
2287 self.block = Some(block);
2288 self
2289 }
2290
2291 pub fn render_with_interaction(
2293 &self,
2294 frame: &mut Frame<'_>,
2295 id: impl Into<WidgetId>,
2296 area: Rect,
2297 ) {
2298 let id = id.into();
2299 self.render(frame.buffer(), area);
2300 for region in self.hit_regions(id.clone(), area) {
2301 frame.register_hit_region(region);
2302 }
2303 for span in self.selectable_spans(id.clone(), area) {
2304 frame.register_selectable_span(span);
2305 }
2306 if let Some((viewport, start, rows)) = self.scroll_region(id.clone(), area) {
2307 frame.register_scroll_region(id, viewport, start, rows);
2308 }
2309 frame.mark_dirty(area);
2310 }
2311
2312 #[must_use]
2314 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
2315 let region_id = id.into();
2316 let inner = self
2317 .block
2318 .as_ref()
2319 .map_or(area, |block| block_content_area(block, area));
2320 if is_empty(area) || is_empty(inner) {
2321 return Vec::new();
2322 }
2323
2324 let mut regions = vec![
2325 HitRegion::new(region_id.clone(), area)
2326 .with_role(WidgetRole::Region)
2327 .with_label("list")
2328 .with_value(WidgetValue::Count(self.items.len())),
2329 ];
2330 let group = SelectionGroup::new(format!("{}:items", region_id.as_ref()));
2331 let start = self.visible_start(inner.height);
2332
2333 for row in 0..inner.height {
2334 let idx = start + row as usize;
2335 if idx >= self.items.len() {
2336 break;
2337 }
2338
2339 let selected = self.selected == Some(idx);
2340 let row_area = Rect::new(inner.x, inner.y + row, inner.width, 1);
2341 regions.push(
2342 HitRegion::new(format!("{}:item:{idx}", region_id.as_ref()), row_area)
2343 .with_role(WidgetRole::ListItem)
2344 .with_label(self.items[idx].as_ref())
2345 .with_action(WidgetAction::Focus)
2346 .with_cursor(MouseCursor::Pointer)
2347 .with_row(idx)
2348 .with_selection_group(group.clone())
2349 .with_state(WidgetState::default().selected(selected))
2350 .with_z_index(1),
2351 );
2352 }
2353
2354 regions
2355 }
2356
2357 #[must_use]
2359 pub fn selectable_spans(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<SelectableSpan> {
2360 let region_id = id.into();
2361 let inner = self
2362 .block
2363 .as_ref()
2364 .map_or(area, |block| block_content_area(block, area));
2365 if is_empty(inner) {
2366 return Vec::new();
2367 }
2368
2369 let text_x = inner.x.saturating_add(2);
2370 let text_width = inner.width.saturating_sub(2);
2371 if text_width == 0 {
2372 return Vec::new();
2373 }
2374
2375 let group = SelectionGroup::new(format!("{}:items", region_id.as_ref()));
2376 let start = self.visible_start(inner.height);
2377 let mut spans = Vec::new();
2378
2379 for row in 0..inner.height {
2380 let item_idx = start + row as usize;
2381 if item_idx >= self.items.len() {
2382 break;
2383 }
2384
2385 let text = clipped_text(self.items[item_idx].as_ref(), text_width);
2386 if text.is_empty() {
2387 continue;
2388 }
2389
2390 let width = text.chars().count().min(usize::from(text_width)) as u16;
2391 spans.push(
2392 SelectableSpan::from_logical(
2393 format!("{}:span:{item_idx}", region_id.as_ref()),
2394 region_id.clone(),
2395 Rect::new(text_x, inner.y + row, width, 1),
2396 TextRange::new(item_idx, 0, width as usize),
2397 text,
2398 )
2399 .with_group(group.clone()),
2400 );
2401 }
2402
2403 spans
2404 }
2405
2406 #[must_use]
2408 pub fn scroll_region(
2409 &self,
2410 id: impl Into<WidgetId>,
2411 area: Rect,
2412 ) -> Option<(Rect, usize, Vec<ScrollRowHit>)> {
2413 let region_id = id.into();
2414 let inner = self
2415 .block
2416 .as_ref()
2417 .map_or(area, |block| block_content_area(block, area));
2418 if is_empty(inner) {
2419 return None;
2420 }
2421
2422 let start = self.visible_start(inner.height);
2423 let mut rows = Vec::new();
2424 for row in 0..inner.height {
2425 let item_idx = start + row as usize;
2426 if item_idx >= self.items.len() {
2427 break;
2428 }
2429 let row_id = WidgetId::new(format!("{}:item:{item_idx}", region_id.as_ref()));
2430 rows.push(
2431 ScrollRowHit::new(row_id.clone(), item_idx)
2432 .with_span_id(format!("{}:span:{item_idx}", region_id.as_ref()))
2433 .with_item_id(row_id),
2434 );
2435 }
2436
2437 Some((inner, start, rows))
2438 }
2439
2440 #[must_use]
2442 pub fn item_count(&self) -> usize {
2443 self.items.len()
2444 }
2445
2446 fn visible_start(&self, visible_height: u16) -> usize {
2447 let total = self.items.len() as u16;
2448 if let Some(sel) = self.selected {
2449 let sel = sel as u16;
2450 if sel < self.scroll_offset {
2451 return sel as usize;
2452 }
2453 if sel >= self.scroll_offset + visible_height {
2454 return (sel + 1 - visible_height) as usize;
2455 }
2456 return self.scroll_offset as usize;
2457 }
2458 let max_top = total.saturating_sub(visible_height);
2459 (self.scroll_offset.min(max_top)) as usize
2460 }
2461}
2462
2463impl Default for List<'_> {
2464 fn default() -> Self {
2465 Self::new()
2466 }
2467}
2468
2469impl Widget for List<'_> {
2470 fn render(&self, buf: &mut Buffer, area: Rect) {
2471 let inner = self
2472 .block
2473 .as_ref()
2474 .map_or(area, |block| block_content_area(block, area));
2475 if let Some(block) = &self.block {
2476 block.render(buf, area);
2477 }
2478 if is_empty(inner) {
2479 return;
2480 }
2481
2482 let start = self.visible_start(inner.height);
2483 let indicator_width = 2u16;
2484 let text_width = inner.width.saturating_sub(indicator_width);
2485
2486 for row in 0..inner.height {
2487 let idx = start + row as usize;
2488 let y = inner.y + row;
2489
2490 if idx >= self.items.len() {
2491 break;
2492 }
2493
2494 let is_selected = self.selected == Some(idx);
2495
2496 let indicator = if is_selected { "▸ " } else { " " };
2497 let indicator_style = if is_selected {
2498 Style::default()
2499 .fg(self.palette.pulse)
2500 .add_modifier(Modifier::BOLD)
2501 } else {
2502 Style::default().fg(self.palette.shadow)
2503 };
2504 paint_text(
2505 Rect::new(inner.x, y, indicator_width, 1),
2506 buf,
2507 indicator,
2508 indicator_style,
2509 );
2510
2511 let item = &self.items[idx];
2512 let item_chars: Vec<char> = item.chars().collect();
2513
2514 for col in 0..text_width {
2515 let x = inner.x + indicator_width + col;
2516 let ch = item_chars.get(col as usize).copied().unwrap_or(' ');
2517 let style = if is_selected {
2518 if ch == ' ' {
2519 Style::default().bg(self.palette.shadow)
2520 } else {
2521 Style::default()
2522 .fg(self.palette.high)
2523 .bg(self.palette.shadow)
2524 .add_modifier(Modifier::BOLD)
2525 }
2526 } else if ch == ' ' {
2527 Style::default()
2528 } else {
2529 Style::default().fg(self.palette.high)
2530 };
2531 set_styled_char(buf, x, y, ch, style);
2532 }
2533 }
2534 }
2535}
2536
2537#[derive(Clone, Debug)]
2543pub struct TabBar<'a> {
2544 tabs: Vec<Cow<'a, str>>,
2545 selected: usize,
2546 tick: u64,
2547 palette: AislingPalette,
2548 block: Option<Block<'a>>,
2549}
2550
2551impl PartialEq for TabBar<'_> {
2552 fn eq(&self, other: &Self) -> bool {
2553 self.tabs == other.tabs
2554 && self.selected == other.selected
2555 && self.tick == other.tick
2556 && self.palette == other.palette
2557 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2558 }
2559}
2560
2561impl<'a> TabBar<'a> {
2562 #[must_use]
2564 pub fn new<I, S>(tabs: I) -> Self
2565 where
2566 I: IntoIterator<Item = S>,
2567 S: Into<Cow<'a, str>>,
2568 {
2569 Self {
2570 tabs: tabs.into_iter().map(Into::into).collect(),
2571 selected: 0,
2572 tick: 0,
2573 palette: AislingPalette::cypherpunk(),
2574 block: None,
2575 }
2576 }
2577
2578 #[must_use]
2580 pub fn selected(mut self, index: usize) -> Self {
2581 self.selected = index;
2582 self
2583 }
2584
2585 #[must_use]
2587 pub fn tick(mut self, tick: u64) -> Self {
2588 self.tick = tick;
2589 self
2590 }
2591
2592 #[must_use]
2594 pub fn palette(mut self, palette: AislingPalette) -> Self {
2595 self.palette = palette;
2596 self
2597 }
2598
2599 #[must_use]
2601 pub fn block(mut self, block: Block<'a>) -> Self {
2602 self.block = Some(block);
2603 self
2604 }
2605
2606 pub fn render_with_interaction(
2608 &self,
2609 frame: &mut Frame<'_>,
2610 id: impl Into<WidgetId>,
2611 area: Rect,
2612 ) {
2613 let id = id.into();
2614 self.render(frame.buffer(), area);
2615 for region in self.hit_regions(id.clone(), area) {
2616 frame.register_hit_region(region);
2617 }
2618 frame.mark_dirty(area);
2619 }
2620
2621 #[must_use]
2623 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
2624 let region_id = id.into();
2625 let inner = self
2626 .block
2627 .as_ref()
2628 .map_or(area, |block| block_content_area(block, area));
2629 if is_empty(area) || is_empty(inner) {
2630 return Vec::new();
2631 }
2632
2633 let mut regions = vec![
2634 HitRegion::new(region_id.clone(), area)
2635 .with_role(WidgetRole::Region)
2636 .with_label("tabs")
2637 .with_value(WidgetValue::Count(self.tabs.len())),
2638 ];
2639 let mut x = inner.x;
2640 let right = inner.x.saturating_add(inner.width);
2641
2642 for (idx, tab) in self.tabs.iter().enumerate() {
2643 if x >= right {
2644 break;
2645 }
2646
2647 let label_width = tab.chars().count() as u16;
2648 let tab_width = label_width.saturating_add(4).min(right - x);
2649 if tab_width == 0 {
2650 break;
2651 }
2652
2653 regions.push(
2654 HitRegion::new(
2655 format!("{}:tab:{idx}", region_id.as_ref()),
2656 Rect::new(x, inner.y, tab_width, 1),
2657 )
2658 .with_role(WidgetRole::Tab)
2659 .with_label(tab.as_ref())
2660 .with_action(WidgetAction::Focus)
2661 .with_cursor(MouseCursor::Pointer)
2662 .with_row(idx)
2663 .with_shortcut(format!("{}", idx + 1))
2664 .with_state(WidgetState::default().selected(idx == self.selected))
2665 .with_z_index(1),
2666 );
2667
2668 x = x.saturating_add(tab_width);
2669 }
2670
2671 regions
2672 }
2673
2674 #[must_use]
2676 pub fn tab_count(&self) -> usize {
2677 self.tabs.len()
2678 }
2679}
2680
2681impl Widget for TabBar<'_> {
2682 fn render(&self, buf: &mut Buffer, area: Rect) {
2683 let inner = self
2684 .block
2685 .as_ref()
2686 .map_or(area, |block| block_content_area(block, area));
2687 if let Some(block) = &self.block {
2688 block.render(buf, area);
2689 }
2690 if is_empty(inner) {
2691 return;
2692 }
2693
2694 let mut x = inner.x;
2695 let right = inner.x.saturating_add(inner.width);
2696
2697 for (i, tab) in self.tabs.iter().enumerate() {
2698 if x >= right {
2699 break;
2700 }
2701
2702 let is_selected = i == self.selected;
2703 let label: Vec<char> = tab.chars().collect();
2704 let padding = 2u16;
2705 let tab_width = (label.len() as u16 + padding * 2).min(right - x);
2706
2707 if is_selected {
2708 set_styled_char(
2709 buf,
2710 x,
2711 inner.y,
2712 '㎍',
2713 Style::default()
2714 .fg(self.palette.pulse)
2715 .add_modifier(Modifier::BOLD),
2716 );
2717 } else {
2718 set_styled_char(buf, x, inner.y, ' ', Style::default());
2719 }
2720
2721 for col in 0..tab_width {
2722 let cx = x + col;
2723 if cx >= right {
2724 break;
2725 }
2726
2727 let char_idx = col.saturating_sub(padding) as usize;
2728 let ch = if col < padding || col >= tab_width - padding {
2729 ' '
2730 } else if char_idx < label.len() {
2731 label[char_idx]
2732 } else {
2733 ' '
2734 };
2735
2736 let style = if is_selected {
2737 Style::default()
2738 .fg(self.palette.high)
2739 .add_modifier(Modifier::BOLD)
2740 } else {
2741 Style::default().fg(self.palette.mid)
2742 };
2743 set_styled_char(buf, cx, inner.y, ch, style);
2744 }
2745
2746 if is_selected {
2747 let bottom = inner.y + inner.height.saturating_sub(1);
2748 for col in 0..tab_width {
2749 let cx = x + col;
2750 if cx >= right {
2751 break;
2752 }
2753 set_styled_char(
2754 buf,
2755 cx,
2756 bottom,
2757 '─',
2758 Style::default().fg(self.palette.pulse),
2759 );
2760 }
2761 }
2762
2763 x += tab_width;
2764 }
2765 }
2766}
2767
2768#[derive(Clone, Debug)]
2774pub struct Table<'a> {
2775 headers: Vec<Cow<'a, str>>,
2776 rows: Vec<Vec<Cow<'a, str>>>,
2777 widths: Option<Vec<u16>>,
2778 selected: Option<usize>,
2779 scroll_offset: u16,
2780 tick: u64,
2781 palette: AislingPalette,
2782 block: Option<Block<'a>>,
2783}
2784
2785impl PartialEq for Table<'_> {
2786 fn eq(&self, other: &Self) -> bool {
2787 self.headers == other.headers
2788 && self.rows == other.rows
2789 && self.widths == other.widths
2790 && self.selected == other.selected
2791 && self.scroll_offset == other.scroll_offset
2792 && self.tick == other.tick
2793 && self.palette == other.palette
2794 && option_block_eq(self.block.as_ref(), other.block.as_ref())
2795 }
2796}
2797
2798impl<'a> Table<'a> {
2799 #[must_use]
2801 pub fn new<I, S>(headers: I) -> Self
2802 where
2803 I: IntoIterator<Item = S>,
2804 S: Into<Cow<'a, str>>,
2805 {
2806 Self {
2807 headers: headers.into_iter().map(Into::into).collect(),
2808 rows: Vec::new(),
2809 widths: None,
2810 selected: None,
2811 scroll_offset: 0,
2812 tick: 0,
2813 palette: AislingPalette::cypherpunk(),
2814 block: None,
2815 }
2816 }
2817
2818 #[must_use]
2820 pub fn row<I, S>(mut self, row: I) -> Self
2821 where
2822 I: IntoIterator<Item = S>,
2823 S: Into<Cow<'a, str>>,
2824 {
2825 self.rows.push(row.into_iter().map(Into::into).collect());
2826 self
2827 }
2828
2829 #[must_use]
2831 pub fn rows<I, R, S>(mut self, rows: I) -> Self
2832 where
2833 I: IntoIterator<Item = R>,
2834 R: IntoIterator<Item = S>,
2835 S: Into<Cow<'a, str>>,
2836 {
2837 self.rows = rows
2838 .into_iter()
2839 .map(|r| r.into_iter().map(Into::into).collect())
2840 .collect();
2841 self
2842 }
2843
2844 #[must_use]
2846 pub fn widths(mut self, widths: Vec<u16>) -> Self {
2847 self.widths = Some(widths);
2848 self
2849 }
2850
2851 #[must_use]
2853 pub fn selected(mut self, index: Option<usize>) -> Self {
2854 self.selected = index;
2855 self
2856 }
2857
2858 #[must_use]
2860 pub fn scroll_offset(mut self, offset: u16) -> Self {
2861 self.scroll_offset = offset;
2862 self
2863 }
2864
2865 #[must_use]
2867 pub fn tick(mut self, tick: u64) -> Self {
2868 self.tick = tick;
2869 self
2870 }
2871
2872 #[must_use]
2874 pub fn palette(mut self, palette: AislingPalette) -> Self {
2875 self.palette = palette;
2876 self
2877 }
2878
2879 #[must_use]
2881 pub fn block(mut self, block: Block<'a>) -> Self {
2882 self.block = Some(block);
2883 self
2884 }
2885
2886 pub fn render_with_interaction(
2888 &self,
2889 frame: &mut Frame<'_>,
2890 id: impl Into<WidgetId>,
2891 area: Rect,
2892 ) {
2893 let id = id.into();
2894 self.render(frame.buffer(), area);
2895 for region in self.hit_regions(id.clone(), area) {
2896 frame.register_hit_region(region);
2897 }
2898 for span in self.selectable_spans(id.clone(), area) {
2899 frame.register_selectable_span(span);
2900 }
2901 if let Some((viewport, start, rows)) = self.scroll_region(id.clone(), area) {
2902 frame.register_scroll_region(id, viewport, start, rows);
2903 }
2904 frame.mark_dirty(area);
2905 }
2906
2907 #[must_use]
2909 pub fn hit_regions(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<HitRegion> {
2910 let region_id = id.into();
2911 let inner = self
2912 .block
2913 .as_ref()
2914 .map_or(area, |block| block_content_area(block, area));
2915 if is_empty(area) || is_empty(inner) {
2916 return Vec::new();
2917 }
2918
2919 let mut regions = vec![
2920 HitRegion::new(region_id.clone(), area)
2921 .with_role(WidgetRole::Region)
2922 .with_label("table")
2923 .with_value(WidgetValue::Count(self.rows.len())),
2924 ];
2925 if self.headers.is_empty() || inner.height < 3 {
2926 return regions;
2927 }
2928
2929 let visible_rows = inner.height.saturating_sub(2);
2930 let start = self
2931 .scroll_offset
2932 .min((self.rows.len() as u16).saturating_sub(visible_rows.min(self.rows.len() as u16)));
2933
2934 for row_offset in 0..visible_rows {
2935 let row_idx = start as usize + row_offset as usize;
2936 if row_idx >= self.rows.len() {
2937 break;
2938 }
2939
2940 let y = inner.y + 2 + row_offset;
2941 let selected = self.selected == Some(row_idx);
2942 let label = self.rows[row_idx]
2943 .iter()
2944 .map(Cow::as_ref)
2945 .collect::<Vec<_>>()
2946 .join(" | ");
2947 regions.push(
2948 HitRegion::new(
2949 format!("{}:row:{row_idx}", region_id.as_ref()),
2950 Rect::new(inner.x, y, inner.width, 1),
2951 )
2952 .with_role(WidgetRole::ModelRow)
2953 .with_label(label)
2954 .with_action(WidgetAction::Focus)
2955 .with_cursor(MouseCursor::Pointer)
2956 .with_row(row_idx)
2957 .with_state(WidgetState::default().selected(selected))
2958 .with_value(WidgetValue::Count(self.rows[row_idx].len()))
2959 .with_z_index(1),
2960 );
2961 }
2962
2963 regions
2964 }
2965
2966 #[must_use]
2968 pub fn selectable_spans(&self, id: impl Into<WidgetId>, area: Rect) -> Vec<SelectableSpan> {
2969 let region_id = id.into();
2970 let inner = self
2971 .block
2972 .as_ref()
2973 .map_or(area, |block| block_content_area(block, area));
2974 let Some((viewport, start, _)) = self.scroll_region(region_id.clone(), area) else {
2975 return Vec::new();
2976 };
2977 if self.headers.is_empty() || is_empty(inner) || is_empty(viewport) {
2978 return Vec::new();
2979 }
2980
2981 let group = SelectionGroup::new(format!("{}:rows", region_id.as_ref()));
2982 let mut spans = Vec::new();
2983 for row_offset in 0..viewport.height {
2984 let row_idx = start + row_offset as usize;
2985 if row_idx >= self.rows.len() {
2986 break;
2987 }
2988
2989 let text = clipped_text(&self.row_label(row_idx), viewport.width);
2990 if text.is_empty() {
2991 continue;
2992 }
2993
2994 let width = text.chars().count().min(usize::from(viewport.width)) as u16;
2995 spans.push(
2996 SelectableSpan::from_logical(
2997 format!("{}:span:{row_idx}", region_id.as_ref()),
2998 region_id.clone(),
2999 Rect::new(viewport.x, viewport.y + row_offset, width, 1),
3000 TextRange::new(row_idx, 0, width as usize),
3001 text,
3002 )
3003 .with_group(group.clone()),
3004 );
3005 }
3006
3007 spans
3008 }
3009
3010 #[must_use]
3012 pub fn scroll_region(
3013 &self,
3014 id: impl Into<WidgetId>,
3015 area: Rect,
3016 ) -> Option<(Rect, usize, Vec<ScrollRowHit>)> {
3017 let region_id = id.into();
3018 let inner = self
3019 .block
3020 .as_ref()
3021 .map_or(area, |block| block_content_area(block, area));
3022 if self.headers.is_empty() || inner.height < 3 || is_empty(inner) {
3023 return None;
3024 }
3025
3026 let visible_rows = inner.height.saturating_sub(2);
3027 let viewport = Rect::new(inner.x, inner.y + 2, inner.width, visible_rows);
3028 if is_empty(viewport) {
3029 return None;
3030 }
3031
3032 let total = self.rows.len() as u16;
3033 let start = self
3034 .scroll_offset
3035 .min(total.saturating_sub(visible_rows.min(total))) as usize;
3036 let mut rows = Vec::new();
3037 for row_offset in 0..visible_rows {
3038 let row_idx = start + row_offset as usize;
3039 if row_idx >= self.rows.len() {
3040 break;
3041 }
3042 let row_id = WidgetId::new(format!("{}:row:{row_idx}", region_id.as_ref()));
3043 rows.push(
3044 ScrollRowHit::new(row_id.clone(), row_idx)
3045 .with_span_id(format!("{}:span:{row_idx}", region_id.as_ref()))
3046 .with_item_id(row_id),
3047 );
3048 }
3049
3050 Some((viewport, start, rows))
3051 }
3052
3053 #[must_use]
3055 pub fn row_count(&self) -> usize {
3056 self.rows.len()
3057 }
3058
3059 fn row_label(&self, row_idx: usize) -> String {
3060 self.rows[row_idx]
3061 .iter()
3062 .map(Cow::as_ref)
3063 .collect::<Vec<_>>()
3064 .join(" | ")
3065 }
3066
3067 fn compute_widths(&self, total_width: u16) -> Vec<u16> {
3068 if let Some(ref w) = self.widths {
3069 return w.clone();
3070 }
3071 let cols = self.headers.len().max(1) as u16;
3072 let per_col = total_width / cols;
3073 let mut widths = vec![per_col; cols as usize];
3074 let remainder = total_width.saturating_sub(per_col * cols);
3075 for w in widths.iter_mut().take(remainder as usize) {
3076 *w += 1;
3077 }
3078 widths
3079 }
3080}
3081
3082impl Widget for Table<'_> {
3083 fn render(&self, buf: &mut Buffer, area: Rect) {
3084 let inner = self
3085 .block
3086 .as_ref()
3087 .map_or(area, |block| block_content_area(block, area));
3088 if let Some(block) = &self.block {
3089 block.render(buf, area);
3090 }
3091 if is_empty(inner) || self.headers.is_empty() {
3092 return;
3093 }
3094
3095 let col_widths = self.compute_widths(inner.width);
3096 let header_height = 1u16;
3097 let divider_height = 1u16;
3098 let data_start_y = inner.y + header_height + divider_height;
3099 let visible_rows = inner.height.saturating_sub(header_height + divider_height);
3100
3101 for (col_idx, header) in self.headers.iter().enumerate() {
3102 if col_idx >= col_widths.len() {
3103 break;
3104 }
3105 let col_x = inner.x + col_widths[..col_idx].iter().sum::<u16>();
3106 let w = col_widths[col_idx];
3107 paint_text(
3108 Rect::new(col_x, inner.y, w, 1),
3109 buf,
3110 header.as_ref(),
3111 Style::default()
3112 .fg(self.palette.pulse)
3113 .add_modifier(Modifier::BOLD),
3114 );
3115 }
3116
3117 let div_y = inner.y + header_height;
3118 for col_idx in 0..self.headers.len().min(col_widths.len()) {
3119 let col_x = inner.x + col_widths[..col_idx].iter().sum::<u16>();
3120 let w = col_widths[col_idx];
3121 for dx in 0..w {
3122 set_styled_char(
3123 buf,
3124 col_x + dx,
3125 div_y,
3126 '─',
3127 Style::default().fg(self.palette.shadow),
3128 );
3129 }
3130 }
3131
3132 let total = self.rows.len() as u16;
3133 let start = self
3134 .scroll_offset
3135 .min(total.saturating_sub(visible_rows.min(total)));
3136
3137 for row_offset in 0..visible_rows {
3138 let row_idx = start as usize + row_offset as usize;
3139 let y = data_start_y + row_offset;
3140
3141 if row_idx >= self.rows.len() {
3142 break;
3143 }
3144
3145 let is_selected = self.selected == Some(row_idx);
3146 let row = &self.rows[row_idx];
3147
3148 for (col_idx, cell) in row.iter().enumerate() {
3149 if col_idx >= col_widths.len() {
3150 break;
3151 }
3152 let col_x = inner.x + col_widths[..col_idx].iter().sum::<u16>();
3153 let w = col_widths[col_idx];
3154
3155 let cell_chars: Vec<char> = cell.chars().collect();
3156 for dx in 0..w {
3157 let ch = cell_chars.get(dx as usize).copied().unwrap_or(' ');
3158 let style = if is_selected {
3159 Style::default()
3160 .fg(self.palette.high)
3161 .bg(self.palette.shadow)
3162 .add_modifier(Modifier::BOLD)
3163 } else {
3164 Style::default().fg(self.palette.high)
3165 };
3166 set_styled_char(buf, col_x + dx, y, ch, style);
3167 }
3168 }
3169 }
3170 }
3171}
3172
3173#[derive(Clone, Debug)]
3179pub struct Sparkline<'a> {
3180 data: Vec<u16>,
3181 max_value: Option<u16>,
3182 palette: AislingPalette,
3183 block: Option<Block<'a>>,
3184}
3185
3186impl PartialEq for Sparkline<'_> {
3187 fn eq(&self, other: &Self) -> bool {
3188 self.data == other.data
3189 && self.max_value == other.max_value
3190 && self.palette == other.palette
3191 && option_block_eq(self.block.as_ref(), other.block.as_ref())
3192 }
3193}
3194
3195impl<'a> Sparkline<'a> {
3196 #[must_use]
3198 pub fn new(data: Vec<u16>) -> Self {
3199 Self {
3200 data,
3201 max_value: None,
3202 palette: AislingPalette::phosphor(),
3203 block: None,
3204 }
3205 }
3206
3207 #[must_use]
3209 pub fn max_value(mut self, max: u16) -> Self {
3210 self.max_value = Some(max);
3211 self
3212 }
3213
3214 #[must_use]
3216 pub fn palette(mut self, palette: AislingPalette) -> Self {
3217 self.palette = palette;
3218 self
3219 }
3220
3221 #[must_use]
3223 pub fn block(mut self, block: Block<'a>) -> Self {
3224 self.block = Some(block);
3225 self
3226 }
3227}
3228
3229impl Widget for Sparkline<'_> {
3230 fn render(&self, buf: &mut Buffer, area: Rect) {
3231 let inner = self
3232 .block
3233 .as_ref()
3234 .map_or(area, |block| block_content_area(block, area));
3235 if let Some(block) = &self.block {
3236 block.render(buf, area);
3237 }
3238 if is_empty(inner) || self.data.is_empty() {
3239 return;
3240 }
3241
3242 let max = self
3243 .max_value
3244 .unwrap_or_else(|| self.data.iter().copied().max().unwrap_or(1))
3245 .max(1);
3246 let bottom = inner.y.saturating_add(inner.height);
3247
3248 for col in 0..inner.width {
3249 let data_idx = (col as usize * self.data.len()) / usize::from(inner.width);
3250 let value = self.data.get(data_idx).copied().unwrap_or(0);
3251 let bar_height =
3252 ((f64::from(value) / f64::from(max)) * f64::from(inner.height)).round() as u16;
3253 let bar_y = bottom.saturating_sub(bar_height);
3254
3255 for y in bar_y..bottom {
3256 let noise = field_noise(inner.x + col, y, 0);
3257 let style = Style::default()
3258 .fg(self.palette.lane(noise))
3259 .add_modifier(Modifier::BOLD);
3260 set_styled_char(buf, inner.x + col, y, '█', style);
3261 }
3262
3263 for y in inner.y..bar_y {
3264 set_styled_char(
3265 buf,
3266 inner.x + col,
3267 y,
3268 '·',
3269 Style::default().fg(self.palette.shadow),
3270 );
3271 }
3272 }
3273 }
3274}
3275
3276#[derive(Clone, Debug)]
3282pub struct Gauge<'a> {
3283 ratio: f64,
3284 label: Option<Cow<'a, str>>,
3285 palette: AislingPalette,
3286 block: Option<Block<'a>>,
3287}
3288
3289impl PartialEq for Gauge<'_> {
3290 fn eq(&self, other: &Self) -> bool {
3291 self.ratio == other.ratio
3292 && self.label == other.label
3293 && self.palette == other.palette
3294 && option_block_eq(self.block.as_ref(), other.block.as_ref())
3295 }
3296}
3297
3298impl<'a> Gauge<'a> {
3299 #[must_use]
3301 pub fn new(ratio: f64) -> Self {
3302 Self {
3303 ratio: ratio.clamp(0.0, 1.0),
3304 label: None,
3305 palette: AislingPalette::cypherpunk(),
3306 block: None,
3307 }
3308 }
3309
3310 #[must_use]
3312 pub fn ratio(&self) -> f64 {
3313 self.ratio
3314 }
3315
3316 #[must_use]
3318 pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
3319 self.label = Some(label.into());
3320 self
3321 }
3322
3323 #[must_use]
3325 pub fn palette(mut self, palette: AislingPalette) -> Self {
3326 self.palette = palette;
3327 self
3328 }
3329
3330 #[must_use]
3332 pub fn block(mut self, block: Block<'a>) -> Self {
3333 self.block = Some(block);
3334 self
3335 }
3336}
3337
3338impl Widget for Gauge<'_> {
3339 fn render(&self, buf: &mut Buffer, area: Rect) {
3340 let inner = self
3341 .block
3342 .as_ref()
3343 .map_or(area, |block| block_content_area(block, area));
3344 if let Some(block) = &self.block {
3345 block.render(buf, area);
3346 }
3347 if is_empty(inner) {
3348 return;
3349 }
3350
3351 let right = inner.x.saturating_add(inner.width);
3352 let bottom = inner.y.saturating_add(inner.height);
3353 let filled = (f64::from(inner.width) * self.ratio).round() as u16;
3354
3355 for y in inner.y..bottom {
3356 for x in inner.x..right {
3357 let offset = x.saturating_sub(inner.x);
3358 if offset < filled {
3359 set_styled_char(
3360 buf,
3361 x,
3362 y,
3363 '█',
3364 Style::default()
3365 .fg(self.palette.mid)
3366 .add_modifier(Modifier::BOLD),
3367 );
3368 } else {
3369 set_styled_char(buf, x, y, '░', Style::default().fg(self.palette.shadow));
3370 }
3371 }
3372 }
3373
3374 if let Some(label) = &self.label {
3375 let row = inner.y + inner.height / 2;
3376 let label_width = label.chars().count().min(usize::from(inner.width)) as u16;
3377 let start = inner.x + inner.width.saturating_sub(label_width) / 2;
3378 paint_text(
3379 Rect::new(start, row, label_width, 1),
3380 buf,
3381 label.as_ref(),
3382 Style::default()
3383 .fg(self.palette.high)
3384 .add_modifier(Modifier::BOLD),
3385 );
3386 }
3387 }
3388}
3389
3390#[derive(Clone, Debug)]
3396pub struct Paragraph<'a> {
3397 text: Cow<'a, str>,
3398 scroll_offset: u16,
3399 palette: AislingPalette,
3400 block: Option<Block<'a>>,
3401}
3402
3403impl PartialEq for Paragraph<'_> {
3404 fn eq(&self, other: &Self) -> bool {
3405 self.text == other.text
3406 && self.scroll_offset == other.scroll_offset
3407 && self.palette == other.palette
3408 && option_block_eq(self.block.as_ref(), other.block.as_ref())
3409 }
3410}
3411
3412impl<'a> Paragraph<'a> {
3413 #[must_use]
3415 pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
3416 Self {
3417 text: text.into(),
3418 scroll_offset: 0,
3419 palette: AislingPalette::cypherpunk(),
3420 block: None,
3421 }
3422 }
3423
3424 #[must_use]
3426 pub fn scroll_offset(mut self, offset: u16) -> Self {
3427 self.scroll_offset = offset;
3428 self
3429 }
3430
3431 #[must_use]
3433 pub fn palette(mut self, palette: AislingPalette) -> Self {
3434 self.palette = palette;
3435 self
3436 }
3437
3438 #[must_use]
3440 pub fn block(mut self, block: Block<'a>) -> Self {
3441 self.block = Some(block);
3442 self
3443 }
3444
3445 fn wrap_lines(&self, width: u16) -> Vec<Cow<'_, str>> {
3446 if width == 0 {
3447 return Vec::new();
3448 }
3449 let mut result = Vec::new();
3450 for raw_line in self.text.lines() {
3451 if raw_line.is_empty() {
3452 result.push(Cow::Borrowed(""));
3453 continue;
3454 }
3455 let mut remaining = raw_line;
3456 while !remaining.is_empty() {
3457 let w = usize::from(width);
3458 if remaining.len() <= w {
3459 result.push(Cow::Borrowed(remaining));
3460 break;
3461 }
3462 let break_at = remaining[..w].rfind(' ').map(|p| p + 1).unwrap_or(w);
3463 result.push(Cow::Borrowed(&remaining[..break_at]));
3464 remaining = &remaining[break_at..];
3465 }
3466 }
3467 result
3468 }
3469}
3470
3471impl Widget for Paragraph<'_> {
3472 fn render(&self, buf: &mut Buffer, area: Rect) {
3473 let inner = self
3474 .block
3475 .as_ref()
3476 .map_or(area, |block| block_content_area(block, area));
3477 if let Some(block) = &self.block {
3478 block.render(buf, area);
3479 }
3480 if is_empty(inner) {
3481 return;
3482 }
3483
3484 let wrapped = self.wrap_lines(inner.width);
3485 let total = wrapped.len() as u16;
3486 let start = self
3487 .scroll_offset
3488 .min(total.saturating_sub(inner.height.min(total)));
3489
3490 for row in 0..inner.height {
3491 let line_idx = start as usize + row as usize;
3492 let y = inner.y + row;
3493
3494 if line_idx >= wrapped.len() {
3495 break;
3496 }
3497
3498 let line = &wrapped[line_idx];
3499 let chars: Vec<char> = line.chars().collect();
3500
3501 for col in 0..inner.width {
3502 let ch = chars.get(col as usize).copied().unwrap_or(' ');
3503 let style = if ch == ' ' {
3504 Style::default()
3505 } else {
3506 Style::default().fg(self.palette.high)
3507 };
3508 set_styled_char(buf, inner.x + col, y, ch, style);
3509 }
3510 }
3511 }
3512}
3513
3514#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3520pub enum Align {
3521 Left,
3522 Center,
3523 Right,
3524}
3525
3526#[derive(Clone, Debug)]
3528pub struct StatusSection<'a> {
3529 text: Cow<'a, str>,
3530 align: Align,
3531}
3532
3533#[derive(Clone, Debug)]
3535pub struct StatusBar<'a> {
3536 sections: Vec<StatusSection<'a>>,
3537 palette: AislingPalette,
3538}
3539
3540impl PartialEq for StatusBar<'_> {
3541 fn eq(&self, other: &Self) -> bool {
3542 self.sections.len() == other.sections.len()
3543 && self
3544 .sections
3545 .iter()
3546 .zip(other.sections.iter())
3547 .all(|(a, b)| a.text == b.text && a.align == b.align)
3548 && self.palette == other.palette
3549 }
3550}
3551
3552impl<'a> StatusBar<'a> {
3553 #[must_use]
3555 pub fn new() -> Self {
3556 Self {
3557 sections: Vec::new(),
3558 palette: AislingPalette::cypherpunk(),
3559 }
3560 }
3561
3562 #[must_use]
3564 pub fn left(mut self, text: impl Into<Cow<'a, str>>) -> Self {
3565 self.sections.push(StatusSection {
3566 text: text.into(),
3567 align: Align::Left,
3568 });
3569 self
3570 }
3571
3572 #[must_use]
3574 pub fn center(mut self, text: impl Into<Cow<'a, str>>) -> Self {
3575 self.sections.push(StatusSection {
3576 text: text.into(),
3577 align: Align::Center,
3578 });
3579 self
3580 }
3581
3582 #[must_use]
3584 pub fn right(mut self, text: impl Into<Cow<'a, str>>) -> Self {
3585 self.sections.push(StatusSection {
3586 text: text.into(),
3587 align: Align::Right,
3588 });
3589 self
3590 }
3591
3592 #[must_use]
3594 pub fn palette(mut self, palette: AislingPalette) -> Self {
3595 self.palette = palette;
3596 self
3597 }
3598}
3599
3600impl Default for StatusBar<'_> {
3601 fn default() -> Self {
3602 Self::new()
3603 }
3604}
3605
3606impl Widget for StatusBar<'_> {
3607 fn render(&self, buf: &mut Buffer, area: Rect) {
3608 if is_empty(area) || self.sections.is_empty() {
3609 return;
3610 }
3611
3612 let bg_style = Style::default()
3613 .fg(self.palette.high)
3614 .bg(self.palette.shadow);
3615
3616 for x in area.x..area.x.saturating_add(area.width) {
3617 for y in area.y..area.y.saturating_add(area.height) {
3618 set_styled_char(buf, x, y, ' ', bg_style);
3619 }
3620 }
3621
3622 let left_sections: Vec<_> = self
3623 .sections
3624 .iter()
3625 .filter(|s| s.align == Align::Left)
3626 .collect();
3627 let center_sections: Vec<_> = self
3628 .sections
3629 .iter()
3630 .filter(|s| s.align == Align::Center)
3631 .collect();
3632 let right_sections: Vec<_> = self
3633 .sections
3634 .iter()
3635 .filter(|s| s.align == Align::Right)
3636 .collect();
3637
3638 let mut x = area.x;
3639
3640 for section in &left_sections {
3641 let text: Vec<char> = section.text.chars().collect();
3642 let max_len = text
3643 .len()
3644 .min(usize::from(area.width.saturating_sub(x - area.x)));
3645 for (i, &ch) in text.iter().take(max_len).enumerate() {
3646 set_styled_char(
3647 buf,
3648 x + i as u16,
3649 area.y,
3650 ch,
3651 Style::default()
3652 .fg(self.palette.high)
3653 .add_modifier(Modifier::BOLD),
3654 );
3655 }
3656 x += max_len as u16;
3657 }
3658
3659 for section in ¢er_sections {
3660 let text: Vec<char> = section.text.chars().collect();
3661 let available = area.width.saturating_sub(x - area.x);
3662 let start_offset = available.saturating_sub(text.len() as u16) / 2;
3663 x += start_offset;
3664 for (i, &ch) in text.iter().take(usize::from(available)).enumerate() {
3665 set_styled_char(
3666 buf,
3667 x + i as u16,
3668 area.y,
3669 ch,
3670 Style::default()
3671 .fg(self.palette.high)
3672 .add_modifier(Modifier::BOLD),
3673 );
3674 }
3675 x += text.len() as u16;
3676 }
3677
3678 let right_x = area.x + area.width;
3679 let mut render_x = right_x;
3680 for section in right_sections.iter().rev() {
3681 let text: Vec<char> = section.text.chars().collect();
3682 render_x = render_x.saturating_sub(text.len() as u16);
3683 for (i, &ch) in text.iter().enumerate() {
3684 if render_x + (i as u16) >= area.x && render_x + (i as u16) < right_x {
3685 set_styled_char(
3686 buf,
3687 render_x + i as u16,
3688 area.y,
3689 ch,
3690 Style::default()
3691 .fg(self.palette.high)
3692 .add_modifier(Modifier::BOLD),
3693 );
3694 }
3695 }
3696 }
3697 }
3698}
3699
3700#[derive(Clone, Debug)]
3707pub struct Bordered<'a> {
3708 title: Cow<'a, str>,
3709 palette: AislingPalette,
3710}
3711
3712impl PartialEq for Bordered<'_> {
3713 fn eq(&self, other: &Self) -> bool {
3714 self.title == other.title && self.palette == other.palette
3715 }
3716}
3717
3718impl<'a> Bordered<'a> {
3719 #[must_use]
3721 pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
3722 Self {
3723 title: title.into(),
3724 palette: AislingPalette::cypherpunk(),
3725 }
3726 }
3727
3728 #[must_use]
3730 pub fn palette(mut self, palette: AislingPalette) -> Self {
3731 self.palette = palette;
3732 self
3733 }
3734
3735 pub fn render_inner(&self, buf: &mut Buffer, area: Rect) -> Rect {
3737 let block = Block::new(self.title.as_ref())
3738 .with_borders(BorderStyle::Plain)
3739 .with_border_color(self.palette.mid);
3740 let inner = block_content_area(&block, area);
3741 block.render(buf, area);
3742 inner
3743 }
3744}
3745
3746impl Widget for Bordered<'_> {
3747 fn render(&self, buf: &mut Buffer, area: Rect) {
3748 self.render_inner(buf, area);
3749 }
3750}
3751
3752fn is_empty(area: Rect) -> bool {
3757 area.width == 0 || area.height == 0
3758}
3759
3760fn block_content_area(block: &Block<'_>, area: Rect) -> Rect {
3761 match block.borders {
3762 BorderStyle::None => area,
3763 _ => Rect::new(
3764 area.x.saturating_add(1),
3765 area.y.saturating_add(1),
3766 area.width.saturating_sub(2),
3767 area.height.saturating_sub(2),
3768 ),
3769 }
3770}
3771
3772fn option_block_eq(left: Option<&Block<'_>>, right: Option<&Block<'_>>) -> bool {
3773 match (left, right) {
3774 (Some(left), Some(right)) => block_eq(left, right),
3775 (None, None) => true,
3776 _ => false,
3777 }
3778}
3779
3780fn block_eq(left: &Block<'_>, right: &Block<'_>) -> bool {
3781 left.title == right.title
3782 && left.title_right == right.title_right
3783 && left.borders == right.borders
3784 && left.border_color == right.border_color
3785 && left.bg == right.bg
3786 && left.style == right.style
3787 && left.inner_margin == right.inner_margin
3788}
3789
3790fn is_edge(area: Rect, x: u16, y: u16) -> bool {
3791 x == area.x
3792 || y == area.y
3793 || x + 1 == area.x.saturating_add(area.width)
3794 || y + 1 == area.y.saturating_add(area.height)
3795}
3796
3797fn field_noise(x: u16, y: u16, tick: u64) -> u64 {
3798 let mut value = u64::from(x).wrapping_mul(0x9e37_79b9_7f4a_7c15)
3799 ^ u64::from(y).wrapping_mul(0xbf58_476d_1ce4_e5b9)
3800 ^ tick.wrapping_mul(0x94d0_49bb_1331_11eb);
3801 value ^= value >> 30;
3802 value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
3803 value ^= value >> 27;
3804 value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
3805 value ^ (value >> 31)
3806}
3807
3808fn paint_text(area: Rect, buf: &mut Buffer, text: &str, style: Style) {
3809 if is_empty(area) {
3810 return;
3811 }
3812
3813 let right = area.x.saturating_add(area.width);
3814 for (offset, glyph) in text.chars().take(usize::from(area.width)).enumerate() {
3815 let x = area.x + offset as u16;
3816 if x >= right {
3817 break;
3818 }
3819 set_styled_char(buf, x, area.y, glyph, style);
3820 }
3821}
3822
3823fn clipped_text(text: &str, width: u16) -> String {
3824 text.chars().take(usize::from(width)).collect()
3825}
3826
3827fn set_cell_bg(buf: &mut Buffer, x: u16, y: u16, bg: Color) {
3828 let Some(mut cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
3829 return;
3830 };
3831 cell.bg = Some(bg);
3832 buf.set(usize::from(x), usize::from(y), cell);
3833}
3834
3835fn set_cell_style(buf: &mut Buffer, x: u16, y: u16, style: Style) {
3836 let Some(cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
3837 return;
3838 };
3839 buf.set(usize::from(x), usize::from(y), replace_style(cell, style));
3840}
3841
3842fn set_styled_char(buf: &mut Buffer, x: u16, y: u16, ch: char, style: Style) {
3843 buf.set(
3844 usize::from(x),
3845 usize::from(y),
3846 replace_style(
3847 Cell::new(ch, style.fg.unwrap_or(Color::WHITE), style.bg),
3848 style,
3849 ),
3850 );
3851}
3852
3853fn replace_style(mut cell: Cell, style: Style) -> Cell {
3854 cell.fg = style.fg.unwrap_or(Color::WHITE);
3855 cell.bg = style.bg;
3856 cell.bold = (style.bold || style.add_modifier.contains(Modifier::BOLD))
3857 && !style.sub_modifier.contains(Modifier::BOLD);
3858 cell.italic = (style.italic || style.add_modifier.contains(Modifier::ITALIC))
3859 && !style.sub_modifier.contains(Modifier::ITALIC);
3860 cell.underlined = (style.underlined || style.add_modifier.contains(Modifier::UNDERLINED))
3861 && !style.sub_modifier.contains(Modifier::UNDERLINED);
3862 cell
3863}
3864
3865#[cfg(test)]
3866mod tests {
3867 use super::*;
3868
3869 #[test]
3870 fn gauge_ratio_is_clamped() {
3871 assert_eq!(NebulaGauge::new(1.5).ratio(), 1.0);
3872 assert_eq!(NebulaGauge::new(-1.0).ratio(), 0.0);
3873 }
3874
3875 #[test]
3876 fn effect_can_be_applied_to_a_buffer() {
3877 let area = Rect::new(0, 0, 12, 4);
3878 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3879
3880 AislingEffect::new(8).intensity(7).apply(area, &mut buf);
3881 }
3882
3883 #[test]
3884 fn scrin_effect_renders_without_panic() {
3885 let area = Rect::new(0, 0, 32, 6);
3886 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3887 ScrinEffect::new(EffectKind::Matrix, "scrin")
3888 .tick(4)
3889 .duration(12)
3890 .seed(7)
3891 .render(&mut buf, area);
3892 }
3893
3894 #[test]
3895 fn scrin_loader_renders_without_panic() {
3896 let area = Rect::new(0, 0, 36, 4);
3897 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3898 ScrinLoader::new(LoaderKind::Bar, 0.42)
3899 .tick(3)
3900 .label("loading")
3901 .unit("items")
3902 .fraction(true)
3903 .render(&mut buf, area);
3904 }
3905
3906 #[test]
3907 fn scrin_loader_progress_is_clamped() {
3908 assert_eq!(ScrinLoader::new(LoaderKind::Bar, 2.0).progress(), 1.0);
3909 assert_eq!(ScrinLoader::new(LoaderKind::Bar, -1.0).progress(), 0.0);
3910 }
3911
3912 #[test]
3913 fn flicker_panel_renders_without_panic() {
3914 let area = Rect::new(0, 0, 20, 3);
3915 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3916 FlickerPanel::new("test")
3917 .tick(5)
3918 .intensity(3)
3919 .render(&mut buf, area);
3920 }
3921
3922 #[test]
3923 fn waveform_renders_without_panic() {
3924 let area = Rect::new(0, 0, 40, 10);
3925 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3926 Waveform::new(4.0, 0.6).tick(12).render(&mut buf, area);
3927 }
3928
3929 #[test]
3930 fn waveform_short_height_is_noop() {
3931 let area = Rect::new(0, 0, 20, 2);
3932 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3933 Waveform::new(4.0, 0.6).render(&mut buf, area);
3934 }
3935
3936 #[test]
3937 fn pulse_ring_renders_without_panic() {
3938 let area = Rect::new(0, 0, 30, 15);
3939 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3940 PulseRing::new(3).tick(7).render(&mut buf, area);
3941 }
3942
3943 #[test]
3944 fn pulse_ring_zero_area_is_noop() {
3945 let area = Rect::new(0, 0, 0, 0);
3946 let mut buf = Buffer::new(1, 1);
3947 PulseRing::new(5).render(&mut buf, area);
3948 }
3949
3950 #[test]
3951 fn radar_renders_without_panic() {
3952 let area = Rect::new(0, 0, 20, 20);
3953 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3954 Radar::new(5).tick(10).render(&mut buf, area);
3955 }
3956
3957 #[test]
3958 fn radar_small_area_is_noop() {
3959 let area = Rect::new(0, 0, 1, 1);
3960 let mut buf = Buffer::new(1, 1);
3961 Radar::new(5).render(&mut buf, area);
3962 }
3963
3964 #[test]
3965 fn orb_field_renders_without_panic() {
3966 let area = Rect::new(0, 0, 30, 10);
3967 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3968 OrbField::new(8).tick(5).render(&mut buf, area);
3969 }
3970
3971 #[test]
3972 fn neon_border_renders_without_panic() {
3973 let area = Rect::new(0, 0, 20, 10);
3974 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3975 NeonBorder::new(Block::new("test"))
3976 .tick(12)
3977 .render(&mut buf, area);
3978 }
3979
3980 #[test]
3981 fn stream_panel_renders_without_panic() {
3982 let area = Rect::new(0, 0, 40, 10);
3983 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3984 StreamPanel::new()
3985 .push_line("fn main() {")
3986 .push_line(" println!(\"hello\");")
3987 .push_line("}")
3988 .show_line_numbers(true)
3989 .tick(5)
3990 .render(&mut buf, area);
3991 }
3992
3993 #[test]
3994 fn stream_panel_empty_is_noop() {
3995 let area = Rect::new(0, 0, 10, 5);
3996 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
3997 StreamPanel::new().render(&mut buf, area);
3998 }
3999
4000 #[test]
4001 fn stream_panel_follow_tail() {
4002 let lines: Vec<String> = (0..50).map(|i| format!("line {i}")).collect();
4003 let area = Rect::new(0, 0, 30, 5);
4004 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4005 let panel = StreamPanel::new()
4006 .lines(lines)
4007 .follow_tail(true)
4008 .show_line_numbers(true);
4009 panel.render(&mut buf, area);
4010 assert_eq!(panel.line_count(), 50);
4011 }
4012
4013 #[test]
4014 fn stream_panel_exports_selectable_scroll_metadata() {
4015 let area = Rect::new(0, 0, 24, 3);
4016 let panel = StreamPanel::new()
4017 .lines(["alpha", "bravo", "charlie", "delta"])
4018 .show_line_numbers(true);
4019
4020 let spans = panel.selectable_spans("stream", area);
4021 let (_, start, rows) = panel.scroll_region("stream", area).unwrap();
4022
4023 assert_eq!(spans.len(), 3);
4024 assert_eq!(start, 1);
4025 assert_eq!(rows.len(), 3);
4026 assert_eq!(rows[0].logical_row, 1);
4027 }
4028
4029 #[test]
4030 fn split_pane_vertical() {
4031 let area = Rect::new(0, 0, 80, 24);
4032 let (a, b, div) = SplitPane::vertical().ratio(0.6).split(area);
4033 assert_eq!(a.width, 48);
4034 assert_eq!(b.width, 32);
4035 assert_eq!(div.width, 0);
4036 }
4037
4038 #[test]
4039 fn split_pane_vertical_with_divider() {
4040 let area = Rect::new(0, 0, 80, 24);
4041 let (a, b, div) = SplitPane::vertical().ratio(0.5).divider('│').split(area);
4042 assert_eq!(a.width + b.width + div.width, 80);
4043 assert_eq!(div.width, 1);
4044 }
4045
4046 #[test]
4047 fn split_pane_horizontal() {
4048 let area = Rect::new(0, 0, 80, 24);
4049 let (a, b, _div) = SplitPane::horizontal().ratio(0.75).split(area);
4050 assert_eq!(a.height, 18);
4051 assert_eq!(b.height, 6);
4052 }
4053
4054 #[test]
4055 fn split_pane_empty_area() {
4056 let (a, b, div) = SplitPane::vertical().split(Rect::ZERO);
4057 assert_eq!(a, Rect::ZERO);
4058 assert_eq!(b, Rect::ZERO);
4059 assert_eq!(div, Rect::ZERO);
4060 }
4061
4062 #[test]
4063 fn list_renders_without_panic() {
4064 let area = Rect::new(0, 0, 30, 8);
4065 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4066 List::new()
4067 .item("Apple")
4068 .item("Banana")
4069 .item("Cherry")
4070 .selected(Some(1))
4071 .render(&mut buf, area);
4072 }
4073
4074 #[test]
4075 fn list_scrolls_to_selected() {
4076 let items: Vec<String> = (0..30).map(|i| format!("Item {i}")).collect();
4077 let area = Rect::new(0, 0, 20, 5);
4078 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4079 List::new()
4080 .items(items)
4081 .selected(Some(25))
4082 .render(&mut buf, area);
4083 }
4084
4085 #[test]
4086 fn list_exports_selectable_scroll_metadata() {
4087 let area = Rect::new(0, 0, 20, 2);
4088 let list = List::new().items(["one", "two", "three"]).selected(Some(2));
4089
4090 let spans = list.selectable_spans("list", area);
4091 let regions = list.hit_regions("list", area);
4092 let (_, start, rows) = list.scroll_region("list", area).unwrap();
4093
4094 assert_eq!(spans.len(), 2);
4095 assert_eq!(regions.len(), 3);
4096 assert_eq!(start, 1);
4097 assert_eq!(rows[1].logical_row, 2);
4098 }
4099
4100 #[test]
4101 fn tab_bar_renders_without_panic() {
4102 let area = Rect::new(0, 0, 60, 3);
4103 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4104 TabBar::new(["Tab 1", "Tab 2", "Tab 3"])
4105 .selected(1)
4106 .tick(3)
4107 .render(&mut buf, area);
4108 }
4109
4110 #[test]
4111 fn tab_bar_many_tabs() {
4112 let area = Rect::new(0, 0, 20, 1);
4113 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4114 TabBar::new(["A", "B", "C", "D", "E", "F", "G", "H"]).render(&mut buf, area);
4115 }
4116
4117 #[test]
4118 fn table_renders_without_panic() {
4119 let area = Rect::new(0, 0, 60, 10);
4120 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4121 Table::new(["Name", "Age", "City"])
4122 .row(["Alice", "30", "NYC"])
4123 .row(["Bob", "25", "LA"])
4124 .row(["Carol", "35", "Chicago"])
4125 .selected(Some(1))
4126 .render(&mut buf, area);
4127 }
4128
4129 #[test]
4130 fn table_with_explicit_widths() {
4131 let area = Rect::new(0, 0, 40, 5);
4132 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4133 Table::new(["A", "B"])
4134 .row(["x", "y"])
4135 .widths(vec![20, 20])
4136 .render(&mut buf, area);
4137 }
4138
4139 #[test]
4140 fn table_exports_selectable_scroll_metadata() {
4141 let area = Rect::new(0, 0, 30, 5);
4142 let table = Table::new(["Name", "State"])
4143 .row(["alpha", "idle"])
4144 .row(["bravo", "run"])
4145 .row(["charlie", "done"]);
4146
4147 let spans = table.selectable_spans("table", area);
4148 let regions = table.hit_regions("table", area);
4149 let (viewport, start, rows) = table.scroll_region("table", area).unwrap();
4150
4151 assert_eq!(spans.len(), 3);
4152 assert_eq!(regions.len(), 4);
4153 assert_eq!(viewport.y, 2);
4154 assert_eq!(start, 0);
4155 assert_eq!(rows[2].logical_row, 2);
4156 }
4157
4158 #[test]
4159 fn sparkline_renders_without_panic() {
4160 let area = Rect::new(0, 0, 30, 5);
4161 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4162 Sparkline::new(vec![1, 3, 5, 2, 8, 4, 6, 3, 7, 9]).render(&mut buf, area);
4163 }
4164
4165 #[test]
4166 fn sparkline_with_max_value() {
4167 let area = Rect::new(0, 0, 20, 3);
4168 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4169 Sparkline::new(vec![5, 10, 15])
4170 .max_value(20)
4171 .render(&mut buf, area);
4172 }
4173
4174 #[test]
4175 fn sparkline_empty_data_is_noop() {
4176 let area = Rect::new(0, 0, 20, 3);
4177 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4178 Sparkline::new(vec![]).render(&mut buf, area);
4179 }
4180
4181 #[test]
4182 fn gauge_simple_renders_without_panic() {
4183 let area = Rect::new(0, 0, 30, 3);
4184 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4185 Gauge::new(0.65).label("65%").render(&mut buf, area);
4186 }
4187
4188 #[test]
4189 fn simple_gauge_ratio_is_clamped() {
4190 assert_eq!(Gauge::new(2.0).ratio(), 1.0);
4191 assert_eq!(Gauge::new(-1.0).ratio(), 0.0);
4192 }
4193
4194 #[test]
4195 fn paragraph_renders_without_panic() {
4196 let area = Rect::new(0, 0, 30, 8);
4197 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4198 Paragraph::new(
4199 "Hello world. This is a longer paragraph that should wrap across multiple lines.",
4200 )
4201 .render(&mut buf, area);
4202 }
4203
4204 #[test]
4205 fn paragraph_scrolls() {
4206 let text = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\nLine 6\nLine 7\nLine 8";
4207 let area = Rect::new(0, 0, 20, 3);
4208 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4209 Paragraph::new(text).scroll_offset(3).render(&mut buf, area);
4210 }
4211
4212 #[test]
4213 fn status_bar_renders_without_panic() {
4214 let area = Rect::new(0, 0, 60, 1);
4215 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4216 StatusBar::new()
4217 .left("Left")
4218 .center("Center")
4219 .right("Right")
4220 .render(&mut buf, area);
4221 }
4222
4223 #[test]
4224 fn status_bar_only_left() {
4225 let area = Rect::new(0, 0, 20, 1);
4226 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4227 StatusBar::new().left("Hello").render(&mut buf, area);
4228 }
4229
4230 #[test]
4231 fn bordered_renders_without_panic() {
4232 let area = Rect::new(0, 0, 30, 10);
4233 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4234 Bordered::new("Container").render(&mut buf, area);
4235 }
4236
4237 #[test]
4238 fn bordered_returns_inner_area() {
4239 let area = Rect::new(0, 0, 30, 10);
4240 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
4241 let inner = Bordered::new("Title").render_inner(&mut buf, area);
4242 assert_eq!(inner.x, 1);
4243 assert_eq!(inner.y, 1);
4244 assert_eq!(inner.width, 28);
4245 assert_eq!(inner.height, 8);
4246 }
4247}