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, GlyphRain, NebulaGauge, SignalPanel,
22 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
623fn is_empty(area: Rect) -> bool {
624 area.width == 0 || area.height == 0
625}
626
627fn block_content_area(block: &Block<'_>, area: Rect) -> Rect {
628 match block.borders {
629 BorderStyle::None => area,
630 _ => Rect::new(
631 area.x.saturating_add(1),
632 area.y.saturating_add(1),
633 area.width.saturating_sub(2),
634 area.height.saturating_sub(2),
635 ),
636 }
637}
638
639fn option_block_eq(left: Option<&Block<'_>>, right: Option<&Block<'_>>) -> bool {
640 match (left, right) {
641 (Some(left), Some(right)) => block_eq(left, right),
642 (None, None) => true,
643 _ => false,
644 }
645}
646
647fn block_eq(left: &Block<'_>, right: &Block<'_>) -> bool {
648 left.title == right.title
649 && left.title_right == right.title_right
650 && left.borders == right.borders
651 && left.border_color == right.border_color
652 && left.bg == right.bg
653 && left.style == right.style
654 && left.inner_margin == right.inner_margin
655}
656
657fn is_edge(area: Rect, x: u16, y: u16) -> bool {
658 x == area.x
659 || y == area.y
660 || x + 1 == area.x.saturating_add(area.width)
661 || y + 1 == area.y.saturating_add(area.height)
662}
663
664fn field_noise(x: u16, y: u16, tick: u64) -> u64 {
665 let mut value = u64::from(x).wrapping_mul(0x9e37_79b9_7f4a_7c15)
666 ^ u64::from(y).wrapping_mul(0xbf58_476d_1ce4_e5b9)
667 ^ tick.wrapping_mul(0x94d0_49bb_1331_11eb);
668 value ^= value >> 30;
669 value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
670 value ^= value >> 27;
671 value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
672 value ^ (value >> 31)
673}
674
675fn paint_text(area: Rect, buf: &mut Buffer, text: &str, style: Style) {
676 if is_empty(area) {
677 return;
678 }
679
680 let right = area.x.saturating_add(area.width);
681 for (offset, glyph) in text.chars().take(usize::from(area.width)).enumerate() {
682 let x = area.x + offset as u16;
683 if x >= right {
684 break;
685 }
686 set_styled_char(buf, x, area.y, glyph, style);
687 }
688}
689
690fn set_cell_bg(buf: &mut Buffer, x: u16, y: u16, bg: Color) {
691 let Some(mut cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
692 return;
693 };
694 cell.bg = Some(bg);
695 buf.set(usize::from(x), usize::from(y), cell);
696}
697
698fn set_cell_style(buf: &mut Buffer, x: u16, y: u16, style: Style) {
699 let Some(cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
700 return;
701 };
702 buf.set(usize::from(x), usize::from(y), replace_style(cell, style));
703}
704
705fn set_styled_char(buf: &mut Buffer, x: u16, y: u16, ch: char, style: Style) {
706 buf.set(
707 usize::from(x),
708 usize::from(y),
709 replace_style(
710 Cell::new(ch, style.fg.unwrap_or(Color::WHITE), style.bg),
711 style,
712 ),
713 );
714}
715
716fn replace_style(mut cell: Cell, style: Style) -> Cell {
717 cell.fg = style.fg.unwrap_or(Color::WHITE);
718 cell.bg = style.bg;
719 cell.bold = (style.bold || style.add_modifier.contains(Modifier::BOLD))
720 && !style.sub_modifier.contains(Modifier::BOLD);
721 cell.italic = (style.italic || style.add_modifier.contains(Modifier::ITALIC))
722 && !style.sub_modifier.contains(Modifier::ITALIC);
723 cell.underlined = (style.underlined || style.add_modifier.contains(Modifier::UNDERLINED))
724 && !style.sub_modifier.contains(Modifier::UNDERLINED);
725 cell
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 #[test]
733 fn gauge_ratio_is_clamped() {
734 assert_eq!(NebulaGauge::new(1.5).ratio(), 1.0);
735 assert_eq!(NebulaGauge::new(-1.0).ratio(), 0.0);
736 }
737
738 #[test]
739 fn effect_can_be_applied_to_a_buffer() {
740 let area = Rect::new(0, 0, 12, 4);
741 let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
742
743 AislingEffect::new(8).intensity(7).apply(area, &mut buf);
744 }
745}