use crate::{fill_rect, font};
pub const DOWNLOAD_NOTICE_HEIGHT: u32 = 28;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DownloadNoticeStrip {
pub kind: DownloadNoticeKind,
pub filename: String,
pub path: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DownloadNoticeKind {
Started,
Completed,
Failed,
}
impl DownloadNoticeStrip {
pub fn paint(&self, buffer: &mut [u32], width: usize, height: usize, top_y: u32) {
let strip_h = DOWNLOAD_NOTICE_HEIGHT as usize;
if width == 0 || height == 0 || strip_h == 0 {
return;
}
if buffer.len() < width * height {
return;
}
let top = top_y as i32;
if top >= height as i32 {
return;
}
let (bg, accent) = palette(self.kind);
fill_rect(buffer, width, height, 0, top, width, strip_h, bg);
fill_rect(buffer, width, height, 0, top, width, 2, accent);
let text_x: i32 = 8;
let text_y = top + (strip_h as i32 - font::glyph_h() as i32) / 2;
let line = format_line(self.kind, &self.filename, &self.path);
let right_pad: i32 = 8;
let max_px = (width as i32 - text_x - right_pad).max(0) as usize;
let line_trunc = truncate_to_width(&line, max_px);
font::draw_text(buffer, width, height, text_x, text_y, line_trunc, COLOUR_FG);
}
}
fn format_line(kind: DownloadNoticeKind, filename: &str, path: &str) -> String {
match kind {
DownloadNoticeKind::Started => {
if path.is_empty() {
format!("v {filename}")
} else {
format!("v {filename} -> {path}")
}
}
DownloadNoticeKind::Completed => {
if path.is_empty() {
format!("OK {filename}")
} else {
format!("OK {filename} -> {path}")
}
}
DownloadNoticeKind::Failed => format!("X {filename}"),
}
}
fn palette(kind: DownloadNoticeKind) -> (u32, u32) {
match kind {
DownloadNoticeKind::Started => (COLOUR_BG_STARTED, COLOUR_ACCENT_STARTED),
DownloadNoticeKind::Completed => (COLOUR_BG_COMPLETED, COLOUR_ACCENT_COMPLETED),
DownloadNoticeKind::Failed => (COLOUR_BG_FAILED, COLOUR_ACCENT_FAILED),
}
}
fn truncate_to_width(s: &str, max_px: usize) -> &str {
if font::text_width(s) <= max_px {
return s;
}
if max_px < font::text_width("..") {
return "";
}
let mut end = s.len();
while end > 0 {
if !s.is_char_boundary(end) {
end -= 1;
continue;
}
let prefix = &s[..end];
if font::text_width(prefix) + font::text_width("..") <= max_px {
return prefix;
}
end -= 1;
}
""
}
const COLOUR_FG: u32 = 0xFF_F0_E8_D8;
const COLOUR_BG_STARTED: u32 = 0xFF_0E_12_18;
const COLOUR_ACCENT_STARTED: u32 = 0xFF_55_88_FF;
const COLOUR_BG_COMPLETED: u32 = 0xFF_0A_14_0E;
const COLOUR_ACCENT_COMPLETED: u32 = 0xFF_4A_C9_5C;
const COLOUR_BG_FAILED: u32 = 0xFF_16_0E_0E;
const COLOUR_ACCENT_FAILED: u32 = 0xFF_E0_5A_5A;
#[cfg(test)]
mod tests {
use super::*;
fn make_buf(w: usize, h: usize) -> Vec<u32> {
vec![0u32; w * h]
}
#[test]
fn paint_fills_strip_with_bg_started() {
let w = 800;
let h = DOWNLOAD_NOTICE_HEIGHT as usize;
let mut buf = make_buf(w, h);
let s = DownloadNoticeStrip {
kind: DownloadNoticeKind::Started,
filename: "file.zip".into(),
path: "/tmp/file.zip".into(),
};
s.paint(&mut buf, w, h, 0);
let idx = (h - 1) * w + (w - 1);
assert_eq!(buf[idx], COLOUR_BG_STARTED);
}
#[test]
fn paint_draws_accent_border_at_top() {
let w = 800;
let h = DOWNLOAD_NOTICE_HEIGHT as usize;
let mut buf = make_buf(w, h);
let s = DownloadNoticeStrip {
kind: DownloadNoticeKind::Completed,
filename: "file.zip".into(),
path: "/tmp/file.zip".into(),
};
s.paint(&mut buf, w, h, 0);
assert_eq!(buf[w / 2], COLOUR_ACCENT_COMPLETED);
}
#[test]
fn paint_failed_uses_red_accent() {
let w = 800;
let h = DOWNLOAD_NOTICE_HEIGHT as usize;
let mut buf = make_buf(w, h);
let s = DownloadNoticeStrip {
kind: DownloadNoticeKind::Failed,
filename: "bad.zip".into(),
path: String::new(),
};
s.paint(&mut buf, w, h, 0);
assert_eq!(buf[w / 2], COLOUR_ACCENT_FAILED);
}
#[test]
fn paint_strip_rows_are_nonzero() {
let w = 400;
let h = DOWNLOAD_NOTICE_HEIGHT as usize + 20;
let mut buf = make_buf(w, h);
let s = DownloadNoticeStrip {
kind: DownloadNoticeKind::Completed,
filename: "x.tar.gz".into(),
path: "/tmp/x.tar.gz".into(),
};
s.paint(&mut buf, w, h, 0);
let strip_has_paint = buf[..DOWNLOAD_NOTICE_HEIGHT as usize * w]
.iter()
.any(|&px| px != 0);
assert!(strip_has_paint, "strip rows should not all be zero");
let tail_clean = buf[DOWNLOAD_NOTICE_HEIGHT as usize * w..]
.iter()
.all(|&px| px == 0);
assert!(tail_clean, "rows after the strip must remain zero");
}
#[test]
fn paint_skips_when_top_y_off_screen() {
let w = 200;
let h = 28;
let mut buf = make_buf(w, h);
let s = DownloadNoticeStrip {
kind: DownloadNoticeKind::Started,
filename: "x".into(),
path: String::new(),
};
s.paint(&mut buf, w, h, 1000);
assert!(buf.iter().all(|&px| px == 0));
}
#[test]
fn kind_variants_produce_distinct_accent_colours() {
let (_, a_started) = palette(DownloadNoticeKind::Started);
let (_, a_completed) = palette(DownloadNoticeKind::Completed);
let (_, a_failed) = palette(DownloadNoticeKind::Failed);
assert_ne!(a_started, a_completed);
assert_ne!(a_started, a_failed);
assert_ne!(a_completed, a_failed);
}
#[test]
fn format_line_started_with_path() {
let line = format_line(DownloadNoticeKind::Started, "file.zip", "/tmp/file.zip");
assert!(line.starts_with("v "));
assert!(line.contains("file.zip"));
assert!(line.contains("/tmp/file.zip"));
}
#[test]
fn format_line_completed() {
let line = format_line(DownloadNoticeKind::Completed, "file.zip", "/dl/file.zip");
assert!(line.starts_with("OK "));
}
#[test]
fn format_line_failed_omits_path() {
let line = format_line(DownloadNoticeKind::Failed, "bad.zip", "");
assert!(line.starts_with("X "));
assert!(!line.contains("->"));
}
}