1use alloc::{boxed::Box, vec::Vec};
2use core::{fmt::Debug, ops::Range};
3
4use bon::Builder;
5#[cfg(feature = "dsl")]
6use compact_str::ToCompactString;
7use ratatui_core::{
8 buffer::Buffer,
9 layout::{Position, Rect},
10};
11
12use crate::{
13 shader::Shader,
14 simple_rng::{RangeSampler, SimpleRng},
15 CellFilter, Duration, EffectTimer,
16};
17
18#[derive(Clone, Debug)]
20pub enum GlitchType {
21 ChangeCase,
23 ChangeCharByValue(i8),
24}
25
26#[derive(Builder, Clone, Debug)]
28pub struct GlitchCell {
29 cell_idx: usize,
30 glitch_remaining_ms: u32,
31 presleep_remaining_ms: u32,
32 glitch: GlitchType,
33}
34
35#[derive(Builder, Clone, Debug)]
37pub struct Glitch {
38 cell_glitch_ratio: f32,
39 action_start_delay_ms: Range<u32>,
40 action_ms: Range<u32>,
41 #[builder(default)]
42 rng: SimpleRng,
43 #[builder(default)]
44 selection: CellFilter,
45
46 #[builder(skip)]
47 glitch_cells: Vec<GlitchCell>,
48 area: Option<Rect>,
49}
50
51impl Glitch {
52 fn ensure_population(&mut self, screen: Rect) {
53 let total_cells =
54 crate::math::round(screen.width as f32 * screen.height as f32 * self.cell_glitch_ratio)
55 as u32;
56
57 let current_population = self.glitch_cells.len() as u32;
58 if current_population < total_cells {
59 for _ in 0..(total_cells - current_population) {
60 let cell = GlitchCell::builder()
61 .cell_idx(
62 self.rng
63 .gen_range(0..(screen.width * screen.height) as usize),
64 )
65 .glitch(self.glitch_type())
66 .glitch_remaining_ms(self.rng.gen_range(self.action_ms.clone()))
67 .presleep_remaining_ms(
68 self.rng
69 .gen_range(self.action_start_delay_ms.clone()),
70 )
71 .build();
72 self.glitch_cells.push(cell);
73 }
74 }
75 }
76
77 fn update_cell(cell: &mut GlitchCell, last_frame_ms: u32) {
78 let f = |v: u32, sub: u32| (v.saturating_sub(sub), sub.saturating_sub(v));
79
80 let (updated, remaining) = f(cell.presleep_remaining_ms, last_frame_ms);
81 cell.presleep_remaining_ms = updated;
82 cell.glitch_remaining_ms = cell.glitch_remaining_ms.saturating_sub(remaining);
83 }
84
85 fn is_running(cell: &GlitchCell) -> bool {
86 cell.glitch_remaining_ms > 0
87 }
88
89 fn glitch_type(&mut self) -> GlitchType {
90 let idx: u32 = self.rng.gen();
91 match idx % 2 {
92 0 => GlitchType::ChangeCase,
93 1 => GlitchType::ChangeCharByValue(-10 + self.rng.gen_range(0..20) as i8),
94 _ => unreachable!(),
95 }
96 }
97}
98
99impl Shader for Glitch {
100 fn name(&self) -> &'static str {
101 "glitch"
102 }
103
104 fn process(&mut self, duration: Duration, buf: &mut Buffer, area: Rect) -> Option<Duration> {
105 self.ensure_population(area);
107
108 let last_frame_ms = duration.as_millis();
110 self.glitch_cells
111 .iter_mut()
112 .for_each(|cell| Self::update_cell(cell, last_frame_ms as _));
113
114 self.glitch_cells
116 .retain(|cell| cell.cell_idx < buf.content.len());
117
118 let predicate = self.selection.predicate(area);
119
120 self.glitch_cells
122 .iter()
123 .filter(|c| c.presleep_remaining_ms == 0)
124 .for_each(|cell| {
125 let x = cell.cell_idx % area.width as usize;
126 let y = cell.cell_idx / area.width as usize;
127 let pos = Position::new(area.x + x as u16, area.y + y as u16);
128 let c = buf
129 .cell_mut(Position::new(area.x + x as u16, area.y + y as u16))
130 .unwrap();
131
132 if !predicate.is_valid(pos, c) {
133 return;
134 }
135
136 match cell.glitch {
137 GlitchType::ChangeCase if c.symbol().is_ascii() => {
138 let ch = c.symbol().chars().next().unwrap();
139 c.set_char(if ch.is_ascii_uppercase() {
140 ch.to_ascii_lowercase()
141 } else {
142 ch.to_ascii_uppercase()
143 });
144 },
145 GlitchType::ChangeCharByValue(v) if c.symbol().len() == 1 => {
146 if c.symbol()
147 .chars()
148 .next()
149 .is_some_and(|ch| ch == ' ')
150 {
151 return;
152 }
153
154 c.set_char(if v > 0 {
155 c.symbol().as_bytes()[0]
156 .saturating_add(v as u8)
157 .clamp(32, 255) as char
158 } else {
159 c.symbol().as_bytes()[0]
160 .saturating_sub(v.unsigned_abs())
161 .clamp(32, 255) as char
162 });
163 },
164 _ => {},
165 }
166 });
167
168 self.glitch_cells.retain(Self::is_running);
170
171 None
172 }
173
174 fn done(&self) -> bool {
175 false
176 }
177
178 fn clone_box(&self) -> Box<dyn Shader> {
179 Box::new(self.clone())
180 }
181
182 fn area(&self) -> Option<Rect> {
183 self.area
184 }
185
186 fn set_area(&mut self, area: Rect) {
187 self.area = Some(area);
188 }
189
190 fn filter(&mut self, strategy: CellFilter) {
191 self.selection = strategy;
192 }
193
194 fn timer_mut(&mut self) -> Option<&mut EffectTimer> {
195 None
196 }
197
198 fn cell_filter(&self) -> Option<&CellFilter> {
199 Some(&self.selection)
200 }
201
202 fn reset(&mut self) {
203 self.glitch_cells.clear();
204 }
205
206 fn set_rng(&mut self, rng: SimpleRng) {
207 self.rng = rng;
208 }
209
210 #[cfg(feature = "dsl")]
211 fn to_dsl(&self) -> Result<crate::dsl::EffectExpression, crate::dsl::DslError> {
212 use crate::dsl::DslError;
213
214 Err(DslError::UnsupportedEffect { name: self.name().to_compact_string() })
215 }
216}