use super::rave::{hsv, snap};
use super::*;
use crossterm::event::KeyCode;
use ratatui::buffer::{Buffer, Cell, CellDiffOption};
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use unicode_width::UnicodeWidthStr;
pub(super) const CODE: [KeyCode; 10] = [
KeyCode::Down,
KeyCode::Down,
KeyCode::Up,
KeyCode::Up,
KeyCode::Right,
KeyCode::Left,
KeyCode::Right,
KeyCode::Left,
KeyCode::Char('a'),
KeyCode::Char('b'),
];
const HICCUP: f32 = 6.5;
const JOLT: f32 = 0.18;
#[derive(Debug, Default)]
pub struct Drunk {
heard: usize,
since: Option<Instant>,
}
impl Drunk {
pub fn hear(&mut self, code: KeyCode) -> rave::Heard {
if code != CODE[self.heard] {
self.heard = match code {
KeyCode::Down if self.heard == 2 => 2,
KeyCode::Down => 1,
_ => 0,
};
return rave::Heard::Pass;
}
self.heard += 1;
match self.heard {
n if n == CODE.len() => {
self.heard = 0;
self.since = match self.since {
Some(_) => None,
None => Some(Instant::now()),
};
rave::Heard::Toggle
}
n if n > 8 => rave::Heard::Swallow,
_ => rave::Heard::Pass,
}
}
pub fn elapsed(&self) -> Option<Duration> {
self.since.map(|since| since.elapsed())
}
}
impl App {
pub(super) fn hear_drunk(&mut self, key: crossterm::event::KeyEvent) -> bool {
let code = match key.modifiers.is_empty() {
true => key.code,
false => KeyCode::Null,
};
match self.drunk.hear(code) {
rave::Heard::Pass => false,
rave::Heard::Swallow => true,
rave::Heard::Toggle => {
match self.drunk.since {
Some(_) => self.set_status("🍺 Drunk mode. The same code again to sober up"),
None => self.set_status("Water, and bed. Back to work"),
}
true
}
}
}
pub fn reeling(&self) -> bool {
self.drunk.since.is_some()
}
}
pub fn paint(buf: &mut Buffer, footer: Rect, t: Duration, colour: Option<bool>, label: bool) {
let secs = t.as_secs_f32();
let hiccup = hiccuping(secs);
let area = buf.area;
for y in area.top()..area.bottom() {
sway(buf, y, lean(secs, y) + i32::from(hiccup) * 2);
}
if (secs * 0.5).sin() > -0.3 {
ghost(buf, 2, colour);
}
if let Some(truecolor) = colour {
haze(buf, secs, truecolor);
}
if label {
sign(buf, footer.intersection(area), hiccup, colour);
}
}
fn lean(secs: f32, y: u16) -> i32 {
let fy = f32::from(y);
let tipsy = (secs / 2.0).min(1.0);
let lean = 2.2 * (secs * 0.9 + fy * 0.35).sin() + 0.8 * (secs * 0.37 - fy * 0.21).sin();
(tipsy * lean).round() as i32
}
fn hiccuping(secs: f32) -> bool {
secs >= HICCUP && secs.rem_euclid(HICCUP) < JOLT
}
fn sway(buf: &mut Buffer, y: u16, dx: i32) {
let area = buf.area;
if dx == 0 {
return;
}
let row: Vec<Cell> = (area.left()..area.right())
.map(|x| buf[(x, y)].clone())
.collect();
if row.iter().any(|c| c.diff_option != CellDiffOption::None) {
return;
}
let width = row.len() as i32;
for (i, was) in row.iter().enumerate() {
let from = i as i32 - dx;
let mut cell = match (0..width).contains(&from) {
true => row[from as usize].clone(),
false => {
let mut blank = Cell::default();
blank.bg = was.bg;
blank
}
};
if i as i32 == width - 1 && cell.symbol().width() > 1 {
cell.set_symbol(" ");
}
buf[(area.left() + i as u16, y)] = cell;
}
}
fn ghost(buf: &mut Buffer, offset: u16, colour: Option<bool>) {
let area = buf.area;
let faint = match colour {
Some(truecolor) => Style::default().fg(snap((95, 95, 125), truecolor)),
None => Style::default().add_modifier(Modifier::DIM),
};
for y in area.top()..area.bottom() {
let row: Vec<(String, bool)> = (area.left()..area.right())
.map(|x| {
let cell = &buf[(x, y)];
let plain = cell.diff_option == CellDiffOption::None;
(cell.symbol().to_string(), plain)
})
.collect();
let blank = |i: usize| row.get(i).is_none_or(|(s, plain)| s == " " && *plain);
let off = usize::from(offset);
for i in off..row.len() {
let (glyph, plain) = &row[i - off];
let covered = row[i - 1].0.width() > 1;
let open = (i..=i + off).all(blank) && !covered;
if open && *plain && glyph != " " && glyph.width() == 1 {
let cell = &mut buf[(area.left() + i as u16, y)];
cell.set_symbol(glyph);
cell.set_style(faint);
}
}
}
}
fn haze(buf: &mut Buffer, secs: f32, truecolor: bool) {
let area = buf.area;
for y in area.top()..area.bottom() {
let hue = (secs * 20.0 + f32::from(y) * 6.0).rem_euclid(360.0);
let tint = snap(hsv(hue, 0.55, 0.14), truecolor);
for x in area.left()..area.right() {
let cell = &mut buf[(x, y)];
if cell.bg == Color::Reset && cell.diff_option == CellDiffOption::None {
cell.bg = tint;
}
}
}
}
fn sign(buf: &mut Buffer, footer: Rect, hiccup: bool, colour: Option<bool>) {
let label = match hiccup {
true => " ~ *hic* ~ ",
false => " ~ ↓↓↑↑→←→←ab to sober up ~ ",
};
let width = label.width() as u16;
if footer.height == 0 || footer.width <= width {
return;
}
let style = match colour {
Some(truecolor) => Style::default()
.fg(Color::Indexed(16))
.bg(snap((230, 170, 60), truecolor)),
None => Style::default().add_modifier(Modifier::REVERSED),
};
let x = footer.x + (footer.width - width) / 2;
buf.set_string(x, footer.y, label, style.add_modifier(Modifier::BOLD));
}
#[cfg(test)]
mod tests {
use super::*;
fn type_code(drunk: &mut Drunk) -> Vec<rave::Heard> {
CODE.iter().map(|&key| drunk.hear(key)).collect()
}
fn row(buf: &Buffer, y: u16) -> String {
(buf.area.left()..buf.area.right())
.map(|x| buf[(x, y)].symbol().to_string())
.collect()
}
#[test]
fn the_code_starts_the_night_and_ends_it() {
let mut drunk = Drunk::default();
let heard = type_code(&mut drunk);
assert!(
heard[..8].iter().all(|h| *h == rave::Heard::Pass),
"{heard:?}"
);
assert_eq!(heard[8], rave::Heard::Swallow);
assert_eq!(heard[9], rave::Heard::Toggle);
assert!(drunk.elapsed().is_some());
type_code(&mut drunk);
assert!(drunk.elapsed().is_none());
}
#[test]
fn a_third_down_is_forgiven_and_a_slip_is_not() {
let mut drunk = Drunk::default();
for _ in 0..4 {
drunk.hear(KeyCode::Down);
}
assert_eq!(drunk.heard, 2);
drunk.hear(KeyCode::Char('x'));
assert_eq!(drunk.heard, 0);
}
#[test]
fn both_codes_are_heard_side_by_side() {
let mut app = crate::ui::tests::test_app();
for code in CODE {
app.on_key(crate::ui::tests::key(code));
}
assert!(app.reeling() && !app.raving());
for code in rave::CODE {
app.on_key(crate::ui::tests::key(code));
}
assert!(app.reeling() && app.raving());
}
#[test]
fn a_row_sways_without_losing_a_letter() {
let mut buf = Buffer::empty(Rect::new(0, 0, 30, 2));
buf.set_string(10, 0, "$12.34 spent", Style::default());
buf.set_string(10, 1, "linked", Style::default());
buf[(10, 1)].set_diff_option(CellDiffOption::Skip);
for dx in [-3, -1, 2, 3] {
let mut moved = buf.clone();
sway(&mut moved, 0, dx);
sway(&mut moved, 1, dx);
let text = row(&moved, 0);
let at = text.find("$12.34 spent").expect(&text);
assert_eq!(at as i32, 10 + dx);
assert_eq!(row(&moved, 1), row(&buf, 1));
}
}
#[test]
fn the_echo_lands_only_on_blanks() {
let mut buf = Buffer::empty(Rect::new(0, 0, 12, 1));
buf.set_string(0, 0, "ab cd", Style::default());
ghost(&mut buf, 2, None);
assert_eq!(row(&buf, 0), "ab cdcd ");
assert!(buf[(5, 0)].modifier.contains(Modifier::DIM));
assert!(!buf[(3, 0)].modifier.contains(Modifier::DIM));
}
#[test]
fn a_night_out_keeps_the_numbers_and_the_palette() {
let area = Rect::new(0, 0, 40, 4);
let footer = Rect::new(0, 3, 40, 1);
for tenth in 0..600 {
let mut buf = Buffer::empty(area);
buf.set_string(8, 0, "$12.34 spent", Style::default());
let t = Duration::from_millis(tenth * 100);
paint(&mut buf, footer, t, Some(false), true);
assert!(
row(&buf, 0).contains("$12.34 spent"),
"{t:?}: {}",
row(&buf, 0)
);
for cell in buf.content() {
assert!(
!matches!(cell.fg, Color::Rgb(..)) && !matches!(cell.bg, Color::Rgb(..)),
"{cell:?}"
);
}
}
}
#[test]
fn hiccups_come_later_and_briefly() {
assert!(!hiccuping(0.05));
assert!(hiccuping(HICCUP + 0.05));
assert!(!hiccuping(HICCUP + JOLT + 0.01));
assert!(hiccuping(2.0 * HICCUP));
}
}