1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::borrow::Cow;
5
6use scrin::{
7 Color, Rect,
8 core::buffer::{Buffer, Cell},
9 style::{Modifier, Style},
10 widgets::{
11 Widget,
12 block::{Block, BorderStyle},
13 },
14};
15
16pub use scrin;
17
18pub mod prelude {
20 pub use crate::{
21 Aisling, AislingEffect, AislingExt, AislingPalette, FlickerPanel, GlyphRain, NebulaGauge,
22 PulseRing, SignalPanel, WaveType, Waveform, scrin,
23 };
24 pub use scrin::widgets::Widget;
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub struct AislingPalette {
30 pub low: Color,
31 pub mid: Color,
32 pub high: Color,
33 pub pulse: Color,
34 pub shadow: Color,
35}
36
37impl AislingPalette {
38 #[must_use]
40 pub const fn dream() -> Self {
41 Self {
42 low: Color::rgb(58, 192, 255),
43 mid: Color::rgb(176, 92, 255),
44 high: Color::rgb(255, 219, 125),
45 pulse: Color::rgb(255, 118, 205),
46 shadow: Color::rgb(17, 18, 35),
47 }
48 }
49
50 #[must_use]
52 pub const fn phosphor() -> Self {
53 Self {
54 low: Color::rgb(61, 255, 142),
55 mid: Color::rgb(19, 189, 112),
56 high: Color::rgb(210, 255, 181),
57 pulse: Color::rgb(135, 255, 221),
58 shadow: Color::rgb(7, 22, 16),
59 }
60 }
61
62 #[must_use]
64 pub const fn flare() -> Self {
65 Self {
66 low: Color::rgb(255, 107, 107),
67 mid: Color::rgb(255, 168, 76),
68 high: Color::rgb(255, 236, 153),
69 pulse: Color::rgb(255, 75, 145),
70 shadow: Color::rgb(35, 14, 24),
71 }
72 }
73
74 fn lane(self, value: u64) -> Color {
75 match value % 4 {
76 0 => self.low,
77 1 => self.mid,
78 2 => self.high,
79 _ => self.pulse,
80 }
81 }
82}
83
84impl Default for AislingPalette {
85 fn default() -> Self {
86 Self::dream()
87 }
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub struct AislingEffect {
93 tick: u64,
94 intensity: u16,
95 palette: AislingPalette,
96 shimmer: bool,
97 scanlines: bool,
98 glow: bool,
99}
100
101impl AislingEffect {
102 #[must_use]
104 pub fn new(tick: u64) -> Self {
105 Self {
106 tick,
107 ..Self::default()
108 }
109 }
110
111 #[must_use]
113 pub fn tick(mut self, tick: u64) -> Self {
114 self.tick = tick;
115 self
116 }
117
118 #[must_use]
120 pub fn palette(mut self, palette: AislingPalette) -> Self {
121 self.palette = palette;
122 self
123 }
124
125 #[must_use]
127 pub fn intensity(mut self, intensity: u16) -> Self {
128 self.intensity = intensity.min(10);
129 self
130 }
131
132 #[must_use]
134 pub fn shimmer(mut self, enabled: bool) -> Self {
135 self.shimmer = enabled;
136 self
137 }
138
139 #[must_use]
141 pub fn scanlines(mut self, enabled: bool) -> Self {
142 self.scanlines = enabled;
143 self
144 }
145
146 #[must_use]
148 pub fn glow(mut self, enabled: bool) -> Self {
149 self.glow = enabled;
150 self
151 }
152
153 pub fn apply(self, area: Rect, buf: &mut Buffer) {
155 if is_empty(area) || self.intensity == 0 {
156 return;
157 }
158
159 let right = area.x.saturating_add(area.width);
160 let bottom = area.y.saturating_add(area.height);
161 let edge_phase = self.tick / 2;
162 let shimmer_gate = 11_u64.saturating_sub(u64::from(self.intensity.min(10)));
163
164 for y in area.y..bottom {
165 for x in area.x..right {
166 if self.scanlines && (u64::from(y) + edge_phase).is_multiple_of(3) {
167 set_cell_bg(buf, x, y, self.palette.shadow);
168 }
169
170 if self.shimmer {
171 let phase = u64::from(x) * 3 + u64::from(y) * 5 + self.tick;
172 if phase % 11 >= shimmer_gate {
173 set_cell_style(
174 buf,
175 x,
176 y,
177 Style::default()
178 .fg(self.palette.lane(phase))
179 .add_modifier(Modifier::BOLD),
180 );
181 }
182 }
183
184 if self.glow
185 && is_edge(area, x, y)
186 && (u64::from(x) + u64::from(y) + edge_phase) % 5 == 0
187 {
188 set_cell_style(
189 buf,
190 x,
191 y,
192 Style::default()
193 .fg(self.palette.pulse)
194 .add_modifier(Modifier::BOLD),
195 );
196 }
197 }
198 }
199 }
200}
201
202impl Default for AislingEffect {
203 fn default() -> Self {
204 Self {
205 tick: 0,
206 intensity: 5,
207 palette: AislingPalette::default(),
208 shimmer: true,
209 scanlines: true,
210 glow: true,
211 }
212 }
213}
214
215#[derive(Clone, Debug, Eq, PartialEq)]
217pub struct Aisling<W> {
218 inner: W,
219 effect: AislingEffect,
220}
221
222impl<W> Aisling<W> {
223 #[must_use]
225 pub fn new(inner: W) -> Self {
226 Self {
227 inner,
228 effect: AislingEffect::default(),
229 }
230 }
231
232 #[must_use]
234 pub fn effect(mut self, effect: AislingEffect) -> Self {
235 self.effect = effect;
236 self
237 }
238
239 #[must_use]
241 pub fn tick(mut self, tick: u64) -> Self {
242 self.effect = self.effect.tick(tick);
243 self
244 }
245
246 #[must_use]
248 pub fn palette(mut self, palette: AislingPalette) -> Self {
249 self.effect = self.effect.palette(palette);
250 self
251 }
252
253 #[must_use]
255 pub fn intensity(mut self, intensity: u16) -> Self {
256 self.effect = self.effect.intensity(intensity);
257 self
258 }
259}
260
261impl<W: Widget> Widget for Aisling<W> {
262 fn render(&self, buf: &mut Buffer, area: Rect) {
263 self.inner.render(buf, area);
264 self.effect.apply(area, buf);
265 }
266}
267
268pub trait AislingExt: Widget + Sized {
270 #[must_use]
272 fn aisling(self) -> Aisling<Self> {
273 Aisling::new(self)
274 }
275}
276
277impl<W: Widget> AislingExt for W {}
278
279#[derive(Clone, Debug)]
281pub struct GlyphRain<'a> {
282 tick: u64,
283 density: u16,
284 glyphs: Cow<'a, str>,
285 palette: AislingPalette,
286 block: Option<Block<'a>>,
287}
288
289impl PartialEq for GlyphRain<'_> {
290 fn eq(&self, other: &Self) -> bool {
291 self.tick == other.tick
292 && self.density == other.density
293 && self.glyphs == other.glyphs
294 && self.palette == other.palette
295 && option_block_eq(self.block.as_ref(), other.block.as_ref())
296 }
297}
298
299impl Eq for GlyphRain<'_> {}
300
301impl<'a> GlyphRain<'a> {
302 #[must_use]
304 pub fn new(tick: u64) -> Self {
305 Self {
306 tick,
307 density: 34,
308 glyphs: Cow::Borrowed("01#$*+<>[]{}"),
309 palette: AislingPalette::phosphor(),
310 block: None,
311 }
312 }
313
314 #[must_use]
316 pub fn tick(mut self, tick: u64) -> Self {
317 self.tick = tick;
318 self
319 }
320
321 #[must_use]
323 pub fn density(mut self, density: u16) -> Self {
324 self.density = density.min(100);
325 self
326 }
327
328 #[must_use]
330 pub fn glyphs(mut self, glyphs: impl Into<Cow<'a, str>>) -> Self {
331 self.glyphs = glyphs.into();
332 self
333 }
334
335 #[must_use]
337 pub fn palette(mut self, palette: AislingPalette) -> Self {
338 self.palette = palette;
339 self
340 }
341
342 #[must_use]
344 pub fn block(mut self, block: Block<'a>) -> Self {
345 self.block = Some(block);
346 self
347 }
348}
349
350impl Widget for GlyphRain<'_> {
351 fn render(&self, buf: &mut Buffer, area: Rect) {
352 let inner = self
353 .block
354 .as_ref()
355 .map_or(area, |block| block_content_area(block, area));
356 if let Some(block) = &self.block {
357 block.render(buf, area);
358 }
359 if is_empty(inner) || self.density == 0 {
360 return;
361 }
362
363 let glyphs: Vec<char> = self.glyphs.chars().collect();
364 if glyphs.is_empty() {
365 return;
366 }
367
368 let right = inner.x.saturating_add(inner.width);
369 let bottom = inner.y.saturating_add(inner.height);
370 for y in inner.y..bottom {
371 for x in inner.x..right {
372 let noise = field_noise(x, y, self.tick);
373 if noise % 100 >= u64::from(self.density) {
374 continue;
375 }
376
377 let glyph = glyphs[(noise as usize + usize::from(y)) % glyphs.len()];
378 let head = (noise + self.tick) % 9 == 0;
379 let style = if head {
380 Style::default()
381 .fg(self.palette.high)
382 .add_modifier(Modifier::BOLD)
383 } else {
384 Style::default().fg(self.palette.lane(noise + self.tick))
385 };
386
387 set_styled_char(buf, x, y, glyph, style);
388 }
389 }
390 }
391}
392
393#[derive(Clone, Debug)]
395pub struct NebulaGauge<'a> {
396 ratio: f64,
397 tick: u64,
398 label: Option<Cow<'a, str>>,
399 palette: AislingPalette,
400 block: Option<Block<'a>>,
401}
402
403impl PartialEq for NebulaGauge<'_> {
404 fn eq(&self, other: &Self) -> bool {
405 self.ratio == other.ratio
406 && self.tick == other.tick
407 && self.label == other.label
408 && self.palette == other.palette
409 && option_block_eq(self.block.as_ref(), other.block.as_ref())
410 }
411}
412
413impl<'a> NebulaGauge<'a> {
414 #[must_use]
416 pub fn new(ratio: f64) -> Self {
417 Self {
418 ratio: ratio.clamp(0.0, 1.0),
419 tick: 0,
420 label: None,
421 palette: AislingPalette::dream(),
422 block: None,
423 }
424 }
425
426 #[must_use]
428 pub fn ratio(&self) -> f64 {
429 self.ratio
430 }
431
432 #[must_use]
434 pub fn tick(mut self, tick: u64) -> Self {
435 self.tick = tick;
436 self
437 }
438
439 #[must_use]
441 pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
442 self.label = Some(label.into());
443 self
444 }
445
446 #[must_use]
448 pub fn palette(mut self, palette: AislingPalette) -> Self {
449 self.palette = palette;
450 self
451 }
452
453 #[must_use]
455 pub fn block(mut self, block: Block<'a>) -> Self {
456 self.block = Some(block);
457 self
458 }
459}
460
461impl Widget for NebulaGauge<'_> {
462 fn render(&self, buf: &mut Buffer, area: Rect) {
463 let inner = self
464 .block
465 .as_ref()
466 .map_or(area, |block| block_content_area(block, area));
467 if let Some(block) = &self.block {
468 block.render(buf, area);
469 }
470 if is_empty(inner) {
471 return;
472 }
473
474 let right = inner.x.saturating_add(inner.width);
475 let bottom = inner.y.saturating_add(inner.height);
476 let filled = (f64::from(inner.width) * self.ratio).round() as u16;
477
478 for y in inner.y..bottom {
479 for x in inner.x..right {
480 let offset = x.saturating_sub(inner.x);
481 let flow = u64::from(offset) + u64::from(y) * 2 + self.tick;
482 if offset < filled {
483 set_styled_char(
484 buf,
485 x,
486 y,
487 '█',
488 Style::default()
489 .fg(self.palette.lane(flow))
490 .bg(self.palette.shadow)
491 .add_modifier(Modifier::BOLD),
492 );
493 } else {
494 set_styled_char(buf, x, y, '░', Style::default().fg(self.palette.shadow));
495 }
496 }
497 }
498
499 if let Some(label) = &self.label {
500 let row = inner.y + inner.height / 2;
501 let label_width = label.chars().count().min(usize::from(inner.width)) as u16;
502 let start = inner.x + inner.width.saturating_sub(label_width) / 2;
503 paint_text(
504 Rect::new(start, row, label_width, 1),
505 buf,
506 label.as_ref(),
507 Style::default()
508 .fg(self.palette.high)
509 .add_modifier(Modifier::BOLD),
510 );
511 }
512 }
513}
514
515#[derive(Clone, Debug, Eq, PartialEq)]
517pub struct SignalPanel<'a> {
518 title: Cow<'a, str>,
519 lines: Vec<Cow<'a, str>>,
520 tick: u64,
521 palette: AislingPalette,
522}
523
524impl<'a> SignalPanel<'a> {
525 #[must_use]
527 pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
528 Self {
529 title: title.into(),
530 lines: Vec::new(),
531 tick: 0,
532 palette: AislingPalette::flare(),
533 }
534 }
535
536 #[must_use]
538 pub fn line(mut self, line: impl Into<Cow<'a, str>>) -> Self {
539 self.lines.push(line.into());
540 self
541 }
542
543 #[must_use]
545 pub fn lines<I, S>(mut self, lines: I) -> Self
546 where
547 I: IntoIterator<Item = S>,
548 S: Into<Cow<'a, str>>,
549 {
550 self.lines = lines.into_iter().map(Into::into).collect();
551 self
552 }
553
554 #[must_use]
556 pub fn tick(mut self, tick: u64) -> Self {
557 self.tick = tick;
558 self
559 }
560
561 #[must_use]
563 pub fn palette(mut self, palette: AislingPalette) -> Self {
564 self.palette = palette;
565 self
566 }
567}
568
569impl Widget for SignalPanel<'_> {
570 fn render(&self, buf: &mut Buffer, area: Rect) {
571 if is_empty(area) {
572 return;
573 }
574
575 let block = Block::new(self.title.as_ref())
576 .with_borders(BorderStyle::Plain)
577 .with_border_color(self.palette.mid)
578 .with_inner_margin(Rect::ZERO);
579 let inner = block_content_area(&block, area);
580 block.render(buf, area);
581 if is_empty(inner) {
582 return;
583 }
584
585 let bars_width = inner.width.min(12);
586 let text_width = inner.width.saturating_sub(bars_width.saturating_add(1));
587 let max_lines = usize::from(inner.height);
588
589 for (index, line) in self.lines.iter().take(max_lines).enumerate() {
590 paint_text(
591 Rect::new(inner.x, inner.y + index as u16, text_width, 1),
592 buf,
593 line.as_ref(),
594 Style::default().fg(self.palette.high),
595 );
596 }
597
598 if bars_width == 0 {
599 return;
600 }
601
602 let bars_x = inner.x + inner.width.saturating_sub(bars_width);
603 for row in 0..inner.height {
604 for column in 0..bars_width {
605 let x = bars_x + column;
606 let y = inner.y + row;
607 let noise = field_noise(x, y, self.tick / 2);
608 let active = (noise + self.tick + u64::from(column)) % 7 <= 3;
609 let symbol = if active { '╱' } else { '·' };
610 let style = if active {
611 Style::default()
612 .fg(self.palette.lane(noise))
613 .add_modifier(Modifier::BOLD)
614 } else {
615 Style::default().fg(self.palette.shadow)
616 };
617 set_styled_char(buf, x, y, symbol, style);
618 }
619 }
620 }
621}
622
623#[derive(Clone, Debug)]
625pub struct FlickerPanel<'a> {
626 text: Cow<'a, str>,
627 tick: u64,
628 intensity: u16,
629 palette: AislingPalette,
630 block: Option<Block<'a>>,
631}
632
633impl PartialEq for FlickerPanel<'_> {
634 fn eq(&self, other: &Self) -> bool {
635 self.text == other.text
636 && self.tick == other.tick
637 && self.intensity == other.intensity
638 && self.palette == other.palette
639 && option_block_eq(self.block.as_ref(), other.block.as_ref())
640 }
641}
642
643impl Eq for FlickerPanel<'_> {}
644
645impl<'a> FlickerPanel<'a> {
646 #[must_use]
648 pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
649 Self {
650 text: text.into(),
651 tick: 0,
652 intensity: 5,
653 palette: AislingPalette::dream(),
654 block: None,
655 }
656 }
657
658 #[must_use]
660 pub fn tick(mut self, tick: u64) -> Self {
661 self.tick = tick;
662 self
663 }
664
665 #[must_use]
667 pub fn intensity(mut self, intensity: u16) -> Self {
668 self.intensity = intensity.min(10);
669 self
670 }
671
672 #[must_use]
674 pub fn palette(mut self, palette: AislingPalette) -> Self {
675 self.palette = palette;
676 self
677 }
678
679 #[must_use]
681 pub fn block(mut self, block: Block<'a>) -> Self {
682 self.block = Some(block);
683 self
684 }
685}
686
687impl Widget for FlickerPanel<'_> {
688 fn render(&self, buf: &mut Buffer, area: Rect) {
689 let inner = self
690 .block
691 .as_ref()
692 .map_or(area, |block| block_content_area(block, area));
693 if let Some(block) = &self.block {
694 block.render(buf, area);
695 }
696 if is_empty(inner) || self.intensity == 0 {
697 return;
698 }
699
700 let glitch_chars: Vec<char> = "░▒▓█▀▄▌▐│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬".chars().collect();
701 let text_chars: Vec<char> = self.text.chars().collect();
702 if text_chars.is_empty() {
703 return;
704 }
705
706 let right = inner.x.saturating_add(inner.width);
707 let bottom = inner.y.saturating_add(inner.height);
708 for y in inner.y..bottom {
709 for x in inner.x..right {
710 let col = usize::from(x.saturating_sub(inner.x));
711 let noise = field_noise(x, y, self.tick);
712 let glitch_gate = 11_u64.saturating_sub(u64::from(self.intensity));
713
714 let (ch, style) = if noise % 11 >= glitch_gate {
715 let g = glitch_chars[(noise as usize) % glitch_chars.len()];
716 (g, Style::default().fg(self.palette.pulse).add_modifier(Modifier::BOLD))
717 } else if col < text_chars.len() {
718 let c = text_chars[col];
719 let flicker = (noise + self.tick) % 9 == 0;
720 let style = if flicker {
721 Style::default()
722 .fg(self.palette.high)
723 .add_modifier(Modifier::BOLD)
724 } else {
725 Style::default().fg(self.palette.mid)
726 };
727 (c, style)
728 } else {
729 (' ', Style::default())
730 };
731 set_styled_char(buf, x, y, ch, style);
732 }
733 }
734 }
735}
736
737#[derive(Clone, Debug)]
739pub struct Waveform<'a> {
740 tick: u64,
741 frequency: f64,
742 amplitude: f64,
743 wave_type: WaveType,
744 palette: AislingPalette,
745 block: Option<Block<'a>>,
746}
747
748#[derive(Clone, Copy, Debug, Eq, PartialEq)]
749pub enum WaveType {
750 Sine,
751 Square,
752 Sawtooth,
753 Triangle,
754}
755
756impl PartialEq for Waveform<'_> {
757 fn eq(&self, other: &Self) -> bool {
758 self.tick == other.tick
759 && self.frequency == other.frequency
760 && self.amplitude == other.amplitude
761 && self.wave_type == other.wave_type
762 && self.palette == other.palette
763 && option_block_eq(self.block.as_ref(), other.block.as_ref())
764 }
765}
766
767impl Eq for Waveform<'_> {}
768
769impl<'a> Waveform<'a> {
770 #[must_use]
772 pub fn new(frequency: f64, amplitude: f64) -> Self {
773 Self {
774 tick: 0,
775 frequency,
776 amplitude: amplitude.clamp(0.0, 1.0),
777 wave_type: WaveType::Sine,
778 palette: AislingPalette::phosphor(),
779 block: None,
780 }
781 }
782
783 #[must_use]
785 pub fn tick(mut self, tick: u64) -> Self {
786 self.tick = tick;
787 self
788 }
789
790 #[must_use]
792 pub fn wave_type(mut self, wave_type: WaveType) -> Self {
793 self.wave_type = wave_type;
794 self
795 }
796
797 #[must_use]
799 pub fn palette(mut self, palette: AislingPalette) -> Self {
800 self.palette = palette;
801 self
802 }
803
804 #[must_use]
806 pub fn block(mut self, block: Block<'a>) -> Self {
807 self.block = Some(block);
808 self
809 }
810
811 fn sample(&self, phase: f64) -> f64 {
812 let t = phase.fract();
813 match self.wave_type {
814 WaveType::Sine => (std::f64::consts::TAU * t).sin(),
815 WaveType::Square => {
816 if t < 0.5 {
817 1.0
818 } else {
819 -1.0
820 }
821 }
822 WaveType::Sawtooth => 2.0 * t - 1.0,
823 WaveType::Triangle => {
824 if t < 0.5 {
825 4.0 * t - 1.0
826 } else {
827 3.0 - 4.0 * t
828 }
829 }
830 }
831 }
832}
833
834impl Widget for Waveform<'_> {
835 fn render(&self, buf: &mut Buffer, area: Rect) {
836 let inner = self
837 .block
838 .as_ref()
839 .map_or(area, |block| block_content_area(block, area));
840 if let Some(block) = &self.block {
841 block.render(buf, area);
842 }
843 if is_empty(inner) || inner.height < 3 {
844 return;
845 }
846
847 let mid_y = inner.y + inner.height / 2;
848 let half = (inner.height / 2) as f64;
849
850 for col in 0..inner.width {
851 let phase = f64::from(col) / f64::from(inner.width) * self.frequency
852 + f64::from(self.tick as u32) * 0.05;
853 let sample = self.sample(phase);
854 let offset = (sample * self.amplitude * half).round() as i16;
855
856 let y = mid_y as i16 + offset;
857 if y >= inner.y as i16 && y < (inner.y + inner.height) as i16 {
858 let noise = field_noise(inner.x + col, y as u16, self.tick);
859 set_styled_char(
860 buf,
861 inner.x + col,
862 y as u16,
863 '█',
864 Style::default()
865 .fg(self.palette.lane(noise))
866 .add_modifier(Modifier::BOLD),
867 );
868 }
869 }
870 }
871}
872
873#[derive(Clone, Debug, Eq, PartialEq)]
875pub struct PulseRing {
876 tick: u64,
877 rings: u16,
878 palette: AislingPalette,
879}
880
881impl PulseRing {
882 #[must_use]
884 pub fn new(rings: u16) -> Self {
885 Self {
886 tick: 0,
887 rings: rings.max(1),
888 palette: AislingPalette::dream(),
889 }
890 }
891
892 #[must_use]
894 pub fn tick(mut self, tick: u64) -> Self {
895 self.tick = tick;
896 self
897 }
898
899 #[must_use]
901 pub fn palette(mut self, palette: AislingPalette) -> Self {
902 self.palette = palette;
903 self
904 }
905}
906
907impl Widget for PulseRing {
908 fn render(&self, buf: &mut Buffer, area: Rect) {
909 if is_empty(area) || self.rings == 0 {
910 return;
911 }
912
913 let cx = area.x + area.width / 2;
914 let cy = area.y + area.height / 2;
915 let max_radius = (area.width.min(area.height) / 2) as f64;
916 if max_radius < 1.0 {
917 return;
918 }
919
920 let right = area.x.saturating_add(area.width);
921 let bottom = area.y.saturating_add(area.height);
922
923 for y in area.y..bottom {
924 for x in area.x..right {
925 let dx = x as f64 - cx as f64;
926 let dy = y as f64 - cy as f64;
927 let dist = (dx * dx + dy * dy).sqrt();
928
929 for ring in 0..self.rings {
930 let ring_phase = (self.tick as f64 * 0.1 + ring as f64 * 3.0) % max_radius;
931 let diff = (dist - ring_phase).abs();
932 if diff < 1.5 {
933 let noise = field_noise(x, y, self.tick + ring as u64);
934 let style = if diff < 0.8 {
935 Style::default()
936 .fg(self.palette.high)
937 .add_modifier(Modifier::BOLD)
938 } else {
939 Style::default().fg(self.palette.lane(noise))
940 };
941 set_styled_char(buf, x, y, '○', style);
942 break;
943 }
944 }
945 }
946 }
947 }
948}
949
950fn is_empty(area: Rect) -> bool {
951 area.width == 0 || area.height == 0
952}
953
954fn block_content_area(block: &Block<'_>, area: Rect) -> Rect {
955 match block.borders {
956 BorderStyle::None => area,
957 _ => Rect::new(
958 area.x.saturating_add(1),
959 area.y.saturating_add(1),
960 area.width.saturating_sub(2),
961 area.height.saturating_sub(2),
962 ),
963 }
964}
965
966fn option_block_eq(left: Option<&Block<'_>>, right: Option<&Block<'_>>) -> bool {
967 match (left, right) {
968 (Some(left), Some(right)) => block_eq(left, right),
969 (None, None) => true,
970 _ => false,
971 }
972}
973
974fn block_eq(left: &Block<'_>, right: &Block<'_>) -> bool {
975 left.title == right.title
976 && left.title_right == right.title_right
977 && left.borders == right.borders
978 && left.border_color == right.border_color
979 && left.bg == right.bg
980 && left.style == right.style
981 && left.inner_margin == right.inner_margin
982}
983
984fn is_edge(area: Rect, x: u16, y: u16) -> bool {
985 x == area.x
986 || y == area.y
987 || x + 1 == area.x.saturating_add(area.width)
988 || y + 1 == area.y.saturating_add(area.height)
989}
990
991fn field_noise(x: u16, y: u16, tick: u64) -> u64 {
992 let mut value = u64::from(x).wrapping_mul(0x9e37_79b9_7f4a_7c15)
993 ^ u64::from(y).wrapping_mul(0xbf58_476d_1ce4_e5b9)
994 ^ tick.wrapping_mul(0x94d0_49bb_1331_11eb);
995 value ^= value >> 30;
996 value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
997 value ^= value >> 27;
998 value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
999 value ^ (value >> 31)
1000}
1001
1002fn paint_text(area: Rect, buf: &mut Buffer, text: &str, style: Style) {
1003 if is_empty(area) {
1004 return;
1005 }
1006
1007 let right = area.x.saturating_add(area.width);
1008 for (offset, glyph) in text.chars().take(usize::from(area.width)).enumerate() {
1009 let x = area.x + offset as u16;
1010 if x >= right {
1011 break;
1012 }
1013 set_styled_char(buf, x, area.y, glyph, style);
1014 }
1015}
1016
1017fn set_cell_bg(buf: &mut Buffer, x: u16, y: u16, bg: Color) {
1018 let Some(mut cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
1019 return;
1020 };
1021 cell.bg = Some(bg);
1022 buf.set(usize::from(x), usize::from(y), cell);
1023}
1024
1025fn set_cell_style(buf: &mut Buffer, x: u16, y: u16, style: Style) {
1026 let Some(cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
1027 return;
1028 };
1029 buf.set(usize::from(x), usize::from(y), replace_style(cell, style));
1030}
1031
1032fn set_styled_char(buf: &mut Buffer, x: u16, y: u16, ch: char, style: Style) {
1033 buf.set(
1034 usize::from(x),
1035 usize::from(y),
1036 replace_style(
1037 Cell::new(ch, style.fg.unwrap_or(Color::WHITE), style.bg),
1038 style,
1039 ),
1040 );
1041}
1042
1043fn replace_style(mut cell: Cell, style: Style) -> Cell {
1044 cell.fg = style.fg.unwrap_or(Color::WHITE);
1045 cell.bg = style.bg;
1046 cell.bold = (style.bold || style.add_modifier.contains(Modifier::BOLD))
1047 && !style.sub_modifier.contains(Modifier::BOLD);
1048 cell.italic = (style.italic || style.add_modifier.contains(Modifier::ITALIC))
1049 && !style.sub_modifier.contains(Modifier::ITALIC);
1050 cell.underlined = (style.underlined || style.add_modifier.contains(Modifier::UNDERLINED))
1051 && !style.sub_modifier.contains(Modifier::UNDERLINED);
1052 cell
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057 use super::*;
1058
1059 #[test]
1060 fn gauge_ratio_is_clamped() {
1061 assert_eq!(NebulaGauge::new(1.5).ratio(), 1.0);
1062 assert_eq!(NebulaGauge::new(-1.0).ratio(), 0.0);
1063 }
1064
1065 #[test]
1066 fn effect_can_be_applied_to_a_buffer() {
1067 let area = Rect::new(0, 0, 12, 4);
1068 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
1069
1070 AislingEffect::new(8).intensity(7).apply(area, &mut buf);
1071 }
1072
1073 #[test]
1074 fn flicker_panel_renders_without_panic() {
1075 let area = Rect::new(0, 0, 20, 3);
1076 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
1077 FlickerPanel::new("test").tick(5).intensity(3).render(&mut buf, area);
1078 }
1079
1080 #[test]
1081 fn waveform_renders_without_panic() {
1082 let area = Rect::new(0, 0, 40, 10);
1083 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
1084 Waveform::new(4.0, 0.6).tick(12).render(&mut buf, area);
1085 }
1086
1087 #[test]
1088 fn waveform_short_height_is_noop() {
1089 let area = Rect::new(0, 0, 20, 2);
1090 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
1091 Waveform::new(4.0, 0.6).render(&mut buf, area);
1092 }
1093
1094 #[test]
1095 fn pulse_ring_renders_without_panic() {
1096 let area = Rect::new(0, 0, 30, 15);
1097 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
1098 PulseRing::new(3).tick(7).render(&mut buf, area);
1099 }
1100
1101 #[test]
1102 fn pulse_ring_zero_area_is_noop() {
1103 let area = Rect::new(0, 0, 0, 0);
1104 let mut buf = Buffer::new(1, 1);
1105 PulseRing::new(5).render(&mut buf, area);
1106 }
1107}