#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
use std::borrow::Cow;
use scrin::{
Color, Rect,
core::buffer::{Buffer, Cell},
style::{Modifier, Style},
widgets::{
Widget,
block::{Block, BorderStyle},
},
};
pub use scrin;
pub mod prelude {
pub use crate::{
Aisling, AislingEffect, AislingExt, AislingPalette, FlickerPanel, GlyphRain, NebulaGauge,
PulseRing, SignalPanel, WaveType, Waveform, scrin,
};
pub use scrin::widgets::Widget;
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AislingPalette {
pub low: Color,
pub mid: Color,
pub high: Color,
pub pulse: Color,
pub shadow: Color,
}
impl AislingPalette {
#[must_use]
pub const fn dream() -> Self {
Self {
low: Color::rgb(58, 192, 255),
mid: Color::rgb(176, 92, 255),
high: Color::rgb(255, 219, 125),
pulse: Color::rgb(255, 118, 205),
shadow: Color::rgb(17, 18, 35),
}
}
#[must_use]
pub const fn phosphor() -> Self {
Self {
low: Color::rgb(61, 255, 142),
mid: Color::rgb(19, 189, 112),
high: Color::rgb(210, 255, 181),
pulse: Color::rgb(135, 255, 221),
shadow: Color::rgb(7, 22, 16),
}
}
#[must_use]
pub const fn flare() -> Self {
Self {
low: Color::rgb(255, 107, 107),
mid: Color::rgb(255, 168, 76),
high: Color::rgb(255, 236, 153),
pulse: Color::rgb(255, 75, 145),
shadow: Color::rgb(35, 14, 24),
}
}
fn lane(self, value: u64) -> Color {
match value % 4 {
0 => self.low,
1 => self.mid,
2 => self.high,
_ => self.pulse,
}
}
}
impl Default for AislingPalette {
fn default() -> Self {
Self::dream()
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct AislingEffect {
tick: u64,
intensity: u16,
palette: AislingPalette,
shimmer: bool,
scanlines: bool,
glow: bool,
}
impl AislingEffect {
#[must_use]
pub fn new(tick: u64) -> Self {
Self {
tick,
..Self::default()
}
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.tick = tick;
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.palette = palette;
self
}
#[must_use]
pub fn intensity(mut self, intensity: u16) -> Self {
self.intensity = intensity.min(10);
self
}
#[must_use]
pub fn shimmer(mut self, enabled: bool) -> Self {
self.shimmer = enabled;
self
}
#[must_use]
pub fn scanlines(mut self, enabled: bool) -> Self {
self.scanlines = enabled;
self
}
#[must_use]
pub fn glow(mut self, enabled: bool) -> Self {
self.glow = enabled;
self
}
pub fn apply(self, area: Rect, buf: &mut Buffer) {
if is_empty(area) || self.intensity == 0 {
return;
}
let right = area.x.saturating_add(area.width);
let bottom = area.y.saturating_add(area.height);
let edge_phase = self.tick / 2;
let shimmer_gate = 11_u64.saturating_sub(u64::from(self.intensity.min(10)));
for y in area.y..bottom {
for x in area.x..right {
if self.scanlines && (u64::from(y) + edge_phase).is_multiple_of(3) {
set_cell_bg(buf, x, y, self.palette.shadow);
}
if self.shimmer {
let phase = u64::from(x) * 3 + u64::from(y) * 5 + self.tick;
if phase % 11 >= shimmer_gate {
set_cell_style(
buf,
x,
y,
Style::default()
.fg(self.palette.lane(phase))
.add_modifier(Modifier::BOLD),
);
}
}
if self.glow
&& is_edge(area, x, y)
&& (u64::from(x) + u64::from(y) + edge_phase) % 5 == 0
{
set_cell_style(
buf,
x,
y,
Style::default()
.fg(self.palette.pulse)
.add_modifier(Modifier::BOLD),
);
}
}
}
}
}
impl Default for AislingEffect {
fn default() -> Self {
Self {
tick: 0,
intensity: 5,
palette: AislingPalette::default(),
shimmer: true,
scanlines: true,
glow: true,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Aisling<W> {
inner: W,
effect: AislingEffect,
}
impl<W> Aisling<W> {
#[must_use]
pub fn new(inner: W) -> Self {
Self {
inner,
effect: AislingEffect::default(),
}
}
#[must_use]
pub fn effect(mut self, effect: AislingEffect) -> Self {
self.effect = effect;
self
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.effect = self.effect.tick(tick);
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.effect = self.effect.palette(palette);
self
}
#[must_use]
pub fn intensity(mut self, intensity: u16) -> Self {
self.effect = self.effect.intensity(intensity);
self
}
}
impl<W: Widget> Widget for Aisling<W> {
fn render(&self, buf: &mut Buffer, area: Rect) {
self.inner.render(buf, area);
self.effect.apply(area, buf);
}
}
pub trait AislingExt: Widget + Sized {
#[must_use]
fn aisling(self) -> Aisling<Self> {
Aisling::new(self)
}
}
impl<W: Widget> AislingExt for W {}
#[derive(Clone, Debug)]
pub struct GlyphRain<'a> {
tick: u64,
density: u16,
glyphs: Cow<'a, str>,
palette: AislingPalette,
block: Option<Block<'a>>,
}
impl PartialEq for GlyphRain<'_> {
fn eq(&self, other: &Self) -> bool {
self.tick == other.tick
&& self.density == other.density
&& self.glyphs == other.glyphs
&& self.palette == other.palette
&& option_block_eq(self.block.as_ref(), other.block.as_ref())
}
}
impl Eq for GlyphRain<'_> {}
impl<'a> GlyphRain<'a> {
#[must_use]
pub fn new(tick: u64) -> Self {
Self {
tick,
density: 34,
glyphs: Cow::Borrowed("01#$*+<>[]{}"),
palette: AislingPalette::phosphor(),
block: None,
}
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.tick = tick;
self
}
#[must_use]
pub fn density(mut self, density: u16) -> Self {
self.density = density.min(100);
self
}
#[must_use]
pub fn glyphs(mut self, glyphs: impl Into<Cow<'a, str>>) -> Self {
self.glyphs = glyphs.into();
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.palette = palette;
self
}
#[must_use]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
}
impl Widget for GlyphRain<'_> {
fn render(&self, buf: &mut Buffer, area: Rect) {
let inner = self
.block
.as_ref()
.map_or(area, |block| block_content_area(block, area));
if let Some(block) = &self.block {
block.render(buf, area);
}
if is_empty(inner) || self.density == 0 {
return;
}
let glyphs: Vec<char> = self.glyphs.chars().collect();
if glyphs.is_empty() {
return;
}
let right = inner.x.saturating_add(inner.width);
let bottom = inner.y.saturating_add(inner.height);
for y in inner.y..bottom {
for x in inner.x..right {
let noise = field_noise(x, y, self.tick);
if noise % 100 >= u64::from(self.density) {
continue;
}
let glyph = glyphs[(noise as usize + usize::from(y)) % glyphs.len()];
let head = (noise + self.tick) % 9 == 0;
let style = if head {
Style::default()
.fg(self.palette.high)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.palette.lane(noise + self.tick))
};
set_styled_char(buf, x, y, glyph, style);
}
}
}
}
#[derive(Clone, Debug)]
pub struct NebulaGauge<'a> {
ratio: f64,
tick: u64,
label: Option<Cow<'a, str>>,
palette: AislingPalette,
block: Option<Block<'a>>,
}
impl PartialEq for NebulaGauge<'_> {
fn eq(&self, other: &Self) -> bool {
self.ratio == other.ratio
&& self.tick == other.tick
&& self.label == other.label
&& self.palette == other.palette
&& option_block_eq(self.block.as_ref(), other.block.as_ref())
}
}
impl<'a> NebulaGauge<'a> {
#[must_use]
pub fn new(ratio: f64) -> Self {
Self {
ratio: ratio.clamp(0.0, 1.0),
tick: 0,
label: None,
palette: AislingPalette::dream(),
block: None,
}
}
#[must_use]
pub fn ratio(&self) -> f64 {
self.ratio
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.tick = tick;
self
}
#[must_use]
pub fn label(mut self, label: impl Into<Cow<'a, str>>) -> Self {
self.label = Some(label.into());
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.palette = palette;
self
}
#[must_use]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
}
impl Widget for NebulaGauge<'_> {
fn render(&self, buf: &mut Buffer, area: Rect) {
let inner = self
.block
.as_ref()
.map_or(area, |block| block_content_area(block, area));
if let Some(block) = &self.block {
block.render(buf, area);
}
if is_empty(inner) {
return;
}
let right = inner.x.saturating_add(inner.width);
let bottom = inner.y.saturating_add(inner.height);
let filled = (f64::from(inner.width) * self.ratio).round() as u16;
for y in inner.y..bottom {
for x in inner.x..right {
let offset = x.saturating_sub(inner.x);
let flow = u64::from(offset) + u64::from(y) * 2 + self.tick;
if offset < filled {
set_styled_char(
buf,
x,
y,
'█',
Style::default()
.fg(self.palette.lane(flow))
.bg(self.palette.shadow)
.add_modifier(Modifier::BOLD),
);
} else {
set_styled_char(buf, x, y, '░', Style::default().fg(self.palette.shadow));
}
}
}
if let Some(label) = &self.label {
let row = inner.y + inner.height / 2;
let label_width = label.chars().count().min(usize::from(inner.width)) as u16;
let start = inner.x + inner.width.saturating_sub(label_width) / 2;
paint_text(
Rect::new(start, row, label_width, 1),
buf,
label.as_ref(),
Style::default()
.fg(self.palette.high)
.add_modifier(Modifier::BOLD),
);
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct SignalPanel<'a> {
title: Cow<'a, str>,
lines: Vec<Cow<'a, str>>,
tick: u64,
palette: AislingPalette,
}
impl<'a> SignalPanel<'a> {
#[must_use]
pub fn new(title: impl Into<Cow<'a, str>>) -> Self {
Self {
title: title.into(),
lines: Vec::new(),
tick: 0,
palette: AislingPalette::flare(),
}
}
#[must_use]
pub fn line(mut self, line: impl Into<Cow<'a, str>>) -> Self {
self.lines.push(line.into());
self
}
#[must_use]
pub fn lines<I, S>(mut self, lines: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<Cow<'a, str>>,
{
self.lines = lines.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.tick = tick;
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.palette = palette;
self
}
}
impl Widget for SignalPanel<'_> {
fn render(&self, buf: &mut Buffer, area: Rect) {
if is_empty(area) {
return;
}
let block = Block::new(self.title.as_ref())
.with_borders(BorderStyle::Plain)
.with_border_color(self.palette.mid)
.with_inner_margin(Rect::ZERO);
let inner = block_content_area(&block, area);
block.render(buf, area);
if is_empty(inner) {
return;
}
let bars_width = inner.width.min(12);
let text_width = inner.width.saturating_sub(bars_width.saturating_add(1));
let max_lines = usize::from(inner.height);
for (index, line) in self.lines.iter().take(max_lines).enumerate() {
paint_text(
Rect::new(inner.x, inner.y + index as u16, text_width, 1),
buf,
line.as_ref(),
Style::default().fg(self.palette.high),
);
}
if bars_width == 0 {
return;
}
let bars_x = inner.x + inner.width.saturating_sub(bars_width);
for row in 0..inner.height {
for column in 0..bars_width {
let x = bars_x + column;
let y = inner.y + row;
let noise = field_noise(x, y, self.tick / 2);
let active = (noise + self.tick + u64::from(column)) % 7 <= 3;
let symbol = if active { '╱' } else { '·' };
let style = if active {
Style::default()
.fg(self.palette.lane(noise))
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.palette.shadow)
};
set_styled_char(buf, x, y, symbol, style);
}
}
}
}
#[derive(Clone, Debug)]
pub struct FlickerPanel<'a> {
text: Cow<'a, str>,
tick: u64,
intensity: u16,
palette: AislingPalette,
block: Option<Block<'a>>,
}
impl PartialEq for FlickerPanel<'_> {
fn eq(&self, other: &Self) -> bool {
self.text == other.text
&& self.tick == other.tick
&& self.intensity == other.intensity
&& self.palette == other.palette
&& option_block_eq(self.block.as_ref(), other.block.as_ref())
}
}
impl Eq for FlickerPanel<'_> {}
impl<'a> FlickerPanel<'a> {
#[must_use]
pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
Self {
text: text.into(),
tick: 0,
intensity: 5,
palette: AislingPalette::dream(),
block: None,
}
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.tick = tick;
self
}
#[must_use]
pub fn intensity(mut self, intensity: u16) -> Self {
self.intensity = intensity.min(10);
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.palette = palette;
self
}
#[must_use]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
}
impl Widget for FlickerPanel<'_> {
fn render(&self, buf: &mut Buffer, area: Rect) {
let inner = self
.block
.as_ref()
.map_or(area, |block| block_content_area(block, area));
if let Some(block) = &self.block {
block.render(buf, area);
}
if is_empty(inner) || self.intensity == 0 {
return;
}
let glitch_chars: Vec<char> = "░▒▓█▀▄▌▐│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬".chars().collect();
let text_chars: Vec<char> = self.text.chars().collect();
if text_chars.is_empty() {
return;
}
let right = inner.x.saturating_add(inner.width);
let bottom = inner.y.saturating_add(inner.height);
for y in inner.y..bottom {
for x in inner.x..right {
let col = usize::from(x.saturating_sub(inner.x));
let noise = field_noise(x, y, self.tick);
let glitch_gate = 11_u64.saturating_sub(u64::from(self.intensity));
let (ch, style) = if noise % 11 >= glitch_gate {
let g = glitch_chars[(noise as usize) % glitch_chars.len()];
(g, Style::default().fg(self.palette.pulse).add_modifier(Modifier::BOLD))
} else if col < text_chars.len() {
let c = text_chars[col];
let flicker = (noise + self.tick) % 9 == 0;
let style = if flicker {
Style::default()
.fg(self.palette.high)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.palette.mid)
};
(c, style)
} else {
(' ', Style::default())
};
set_styled_char(buf, x, y, ch, style);
}
}
}
}
#[derive(Clone, Debug)]
pub struct Waveform<'a> {
tick: u64,
frequency: f64,
amplitude: f64,
wave_type: WaveType,
palette: AislingPalette,
block: Option<Block<'a>>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WaveType {
Sine,
Square,
Sawtooth,
Triangle,
}
impl PartialEq for Waveform<'_> {
fn eq(&self, other: &Self) -> bool {
self.tick == other.tick
&& self.frequency == other.frequency
&& self.amplitude == other.amplitude
&& self.wave_type == other.wave_type
&& self.palette == other.palette
&& option_block_eq(self.block.as_ref(), other.block.as_ref())
}
}
impl Eq for Waveform<'_> {}
impl<'a> Waveform<'a> {
#[must_use]
pub fn new(frequency: f64, amplitude: f64) -> Self {
Self {
tick: 0,
frequency,
amplitude: amplitude.clamp(0.0, 1.0),
wave_type: WaveType::Sine,
palette: AislingPalette::phosphor(),
block: None,
}
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.tick = tick;
self
}
#[must_use]
pub fn wave_type(mut self, wave_type: WaveType) -> Self {
self.wave_type = wave_type;
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.palette = palette;
self
}
#[must_use]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
fn sample(&self, phase: f64) -> f64 {
let t = phase.fract();
match self.wave_type {
WaveType::Sine => (std::f64::consts::TAU * t).sin(),
WaveType::Square => {
if t < 0.5 {
1.0
} else {
-1.0
}
}
WaveType::Sawtooth => 2.0 * t - 1.0,
WaveType::Triangle => {
if t < 0.5 {
4.0 * t - 1.0
} else {
3.0 - 4.0 * t
}
}
}
}
}
impl Widget for Waveform<'_> {
fn render(&self, buf: &mut Buffer, area: Rect) {
let inner = self
.block
.as_ref()
.map_or(area, |block| block_content_area(block, area));
if let Some(block) = &self.block {
block.render(buf, area);
}
if is_empty(inner) || inner.height < 3 {
return;
}
let mid_y = inner.y + inner.height / 2;
let half = (inner.height / 2) as f64;
for col in 0..inner.width {
let phase = f64::from(col) / f64::from(inner.width) * self.frequency
+ f64::from(self.tick as u32) * 0.05;
let sample = self.sample(phase);
let offset = (sample * self.amplitude * half).round() as i16;
let y = mid_y as i16 + offset;
if y >= inner.y as i16 && y < (inner.y + inner.height) as i16 {
let noise = field_noise(inner.x + col, y as u16, self.tick);
set_styled_char(
buf,
inner.x + col,
y as u16,
'█',
Style::default()
.fg(self.palette.lane(noise))
.add_modifier(Modifier::BOLD),
);
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PulseRing {
tick: u64,
rings: u16,
palette: AislingPalette,
}
impl PulseRing {
#[must_use]
pub fn new(rings: u16) -> Self {
Self {
tick: 0,
rings: rings.max(1),
palette: AislingPalette::dream(),
}
}
#[must_use]
pub fn tick(mut self, tick: u64) -> Self {
self.tick = tick;
self
}
#[must_use]
pub fn palette(mut self, palette: AislingPalette) -> Self {
self.palette = palette;
self
}
}
impl Widget for PulseRing {
fn render(&self, buf: &mut Buffer, area: Rect) {
if is_empty(area) || self.rings == 0 {
return;
}
let cx = area.x + area.width / 2;
let cy = area.y + area.height / 2;
let max_radius = (area.width.min(area.height) / 2) as f64;
if max_radius < 1.0 {
return;
}
let right = area.x.saturating_add(area.width);
let bottom = area.y.saturating_add(area.height);
for y in area.y..bottom {
for x in area.x..right {
let dx = x as f64 - cx as f64;
let dy = y as f64 - cy as f64;
let dist = (dx * dx + dy * dy).sqrt();
for ring in 0..self.rings {
let ring_phase = (self.tick as f64 * 0.1 + ring as f64 * 3.0) % max_radius;
let diff = (dist - ring_phase).abs();
if diff < 1.5 {
let noise = field_noise(x, y, self.tick + ring as u64);
let style = if diff < 0.8 {
Style::default()
.fg(self.palette.high)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(self.palette.lane(noise))
};
set_styled_char(buf, x, y, '○', style);
break;
}
}
}
}
}
}
fn is_empty(area: Rect) -> bool {
area.width == 0 || area.height == 0
}
fn block_content_area(block: &Block<'_>, area: Rect) -> Rect {
match block.borders {
BorderStyle::None => area,
_ => Rect::new(
area.x.saturating_add(1),
area.y.saturating_add(1),
area.width.saturating_sub(2),
area.height.saturating_sub(2),
),
}
}
fn option_block_eq(left: Option<&Block<'_>>, right: Option<&Block<'_>>) -> bool {
match (left, right) {
(Some(left), Some(right)) => block_eq(left, right),
(None, None) => true,
_ => false,
}
}
fn block_eq(left: &Block<'_>, right: &Block<'_>) -> bool {
left.title == right.title
&& left.title_right == right.title_right
&& left.borders == right.borders
&& left.border_color == right.border_color
&& left.bg == right.bg
&& left.style == right.style
&& left.inner_margin == right.inner_margin
}
fn is_edge(area: Rect, x: u16, y: u16) -> bool {
x == area.x
|| y == area.y
|| x + 1 == area.x.saturating_add(area.width)
|| y + 1 == area.y.saturating_add(area.height)
}
fn field_noise(x: u16, y: u16, tick: u64) -> u64 {
let mut value = u64::from(x).wrapping_mul(0x9e37_79b9_7f4a_7c15)
^ u64::from(y).wrapping_mul(0xbf58_476d_1ce4_e5b9)
^ tick.wrapping_mul(0x94d0_49bb_1331_11eb);
value ^= value >> 30;
value = value.wrapping_mul(0xbf58_476d_1ce4_e5b9);
value ^= value >> 27;
value = value.wrapping_mul(0x94d0_49bb_1331_11eb);
value ^ (value >> 31)
}
fn paint_text(area: Rect, buf: &mut Buffer, text: &str, style: Style) {
if is_empty(area) {
return;
}
let right = area.x.saturating_add(area.width);
for (offset, glyph) in text.chars().take(usize::from(area.width)).enumerate() {
let x = area.x + offset as u16;
if x >= right {
break;
}
set_styled_char(buf, x, area.y, glyph, style);
}
}
fn set_cell_bg(buf: &mut Buffer, x: u16, y: u16, bg: Color) {
let Some(mut cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
return;
};
cell.bg = Some(bg);
buf.set(usize::from(x), usize::from(y), cell);
}
fn set_cell_style(buf: &mut Buffer, x: u16, y: u16, style: Style) {
let Some(cell) = buf.get(usize::from(x), usize::from(y)).copied() else {
return;
};
buf.set(usize::from(x), usize::from(y), replace_style(cell, style));
}
fn set_styled_char(buf: &mut Buffer, x: u16, y: u16, ch: char, style: Style) {
buf.set(
usize::from(x),
usize::from(y),
replace_style(
Cell::new(ch, style.fg.unwrap_or(Color::WHITE), style.bg),
style,
),
);
}
fn replace_style(mut cell: Cell, style: Style) -> Cell {
cell.fg = style.fg.unwrap_or(Color::WHITE);
cell.bg = style.bg;
cell.bold = (style.bold || style.add_modifier.contains(Modifier::BOLD))
&& !style.sub_modifier.contains(Modifier::BOLD);
cell.italic = (style.italic || style.add_modifier.contains(Modifier::ITALIC))
&& !style.sub_modifier.contains(Modifier::ITALIC);
cell.underlined = (style.underlined || style.add_modifier.contains(Modifier::UNDERLINED))
&& !style.sub_modifier.contains(Modifier::UNDERLINED);
cell
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gauge_ratio_is_clamped() {
assert_eq!(NebulaGauge::new(1.5).ratio(), 1.0);
assert_eq!(NebulaGauge::new(-1.0).ratio(), 0.0);
}
#[test]
fn effect_can_be_applied_to_a_buffer() {
let area = Rect::new(0, 0, 12, 4);
let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
AislingEffect::new(8).intensity(7).apply(area, &mut buf);
}
#[test]
fn flicker_panel_renders_without_panic() {
let area = Rect::new(0, 0, 20, 3);
let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
FlickerPanel::new("test").tick(5).intensity(3).render(&mut buf, area);
}
#[test]
fn waveform_renders_without_panic() {
let area = Rect::new(0, 0, 40, 10);
let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
Waveform::new(4.0, 0.6).tick(12).render(&mut buf, area);
}
#[test]
fn waveform_short_height_is_noop() {
let area = Rect::new(0, 0, 20, 2);
let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
Waveform::new(4.0, 0.6).render(&mut buf, area);
}
#[test]
fn pulse_ring_renders_without_panic() {
let area = Rect::new(0, 0, 30, 15);
let mut buf = Buffer::new(usize::from(area.width), usize::from(area.height));
PulseRing::new(3).tick(7).render(&mut buf, area);
}
#[test]
fn pulse_ring_zero_area_is_noop() {
let area = Rect::new(0, 0, 0, 0);
let mut buf = Buffer::new(1, 1);
PulseRing::new(5).render(&mut buf, area);
}
}