use crate::backend::Backend;
use crate::color::{Color, lerp_color, palette, rgb};
use crate::event::Event;
use crate::font;
pub struct Canvas<'b, B: Backend> {
backend: &'b mut B,
}
impl<'b, B: Backend> Canvas<'b, B> {
pub fn new(backend: &'b mut B) -> Self { Self { backend } }
pub fn width(&self) -> u32 { self.backend.width() }
pub fn height(&self) -> u32 { self.backend.height() }
pub fn size(&self) -> (u32, u32) { self.backend.size() }
pub fn fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
self.backend.fill_rect(x, y, w, h, color);
}
pub fn draw_text(&mut self, x: u32, y: u32, text: &str, color: Color) {
self.backend.draw_text(x, y, text, color);
}
pub fn hline(&mut self, x: u32, y: u32, w: u32, color: Color) {
self.backend.hline(x, y, w, color);
}
pub fn vline(&mut self, x: u32, y: u32, h: u32, color: Color) {
self.backend.vline(x, y, h, color);
}
pub fn draw_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
self.backend.draw_rect(x, y, w, h, color);
}
pub fn clear(&mut self, color: Color) {
self.backend.clear(color);
}
pub fn present(&mut self) {
self.backend.present();
}
pub fn poll_event(&mut self) -> Option<Event> {
self.backend.poll_event()
}
pub fn panel(&mut self, x: u32, y: u32, w: u32, h: u32, fill: Color, border: Color) {
self.fill_rect(x, y, w, h, fill);
self.draw_rect(x, y, w, h, border);
}
pub fn title_bar(&mut self, x: u32, y: u32, w: u32, h: u32, title: &str, bg: Color) {
self.fill_rect(x, y, w, h, bg);
let pad = 8u32;
self.draw_text(x + pad, y + (h - font::CHAR_H) / 2, title, palette::WHITE);
}
pub fn button(
&mut self,
x: u32, y: u32, w: u32, h: u32,
label: &str,
fill: Color, border: Color, text_color: Color,
) {
self.panel(x, y, w, h, fill, border);
let lw = font::text_width(label);
let tx = x + w.saturating_sub(lw) / 2;
let ty = y + h.saturating_sub(font::CHAR_H) / 2;
self.draw_text(tx, ty, label, text_color);
}
pub fn progress_bar(
&mut self,
x: u32, y: u32, w: u32, h: u32,
percent: u32,
track: Color, fill: Color, border: Color,
) {
let pct = percent.min(100);
let fill_w = w * pct / 100;
self.fill_rect(x, y, w, h, track);
if fill_w > 0 { self.fill_rect(x, y, fill_w, h, fill); }
self.draw_rect(x, y, w, h, border);
}
pub fn divider_v(&mut self, x: u32, y: u32, h: u32) {
self.vline(x, y, h, palette::DIVIDER);
}
pub fn divider_h(&mut self, x: u32, y: u32, w: u32) {
self.hline(x, y, w, palette::DIVIDER);
}
pub fn centered_text(&mut self, x: u32, y: u32, w: u32, text: &str, color: Color) {
let tw = font::text_width(text);
let tx = x + w.saturating_sub(tw) / 2;
self.draw_text(tx, y, text, color);
}
pub fn right_text(&mut self, x: u32, y: u32, w: u32, text: &str, color: Color) {
let tw = font::text_width(text);
let tx = x + w.saturating_sub(tw);
self.draw_text(tx, y, text, color);
}
pub fn fill_rounded(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
if w < 2 || h < 2 { self.fill_rect(x, y, w, h, color); return; }
self.fill_rect(x + 1, y, w - 2, h, color); self.fill_rect(x, y + 1, 1, h - 2, color); self.fill_rect(x + w - 1, y + 1, 1, h - 2, color); }
pub fn gradient_h(&mut self, x: u32, y: u32, w: u32, h: u32, left: Color, right: Color) {
if w == 0 { return; }
let steps = (w - 1).max(1);
for i in 0..w {
let c = lerp_color(left, right, i, steps);
self.fill_rect(x + i, y, 1, h, c);
}
}
pub fn gradient_v(&mut self, x: u32, y: u32, w: u32, h: u32, top: Color, bottom: Color) {
if h == 0 { return; }
let steps = (h - 1).max(1);
for i in 0..h {
let c = lerp_color(top, bottom, i, steps);
self.fill_rect(x, y + i, w, 1, c);
}
}
pub fn shadow_panel(&mut self, x: u32, y: u32, w: u32, h: u32, fill: Color, border: Color) {
self.fill_rect(x + 4, y + 4, w, h, palette::BLACK);
self.panel(x, y, w, h, fill, border);
}
pub fn accent_bar(&mut self, x: u32, y: u32, h: u32, color: Color) {
self.fill_rect(x, y, 3, h, color);
}
pub fn dot(&mut self, x: u32, y: u32, color: Color) {
self.fill_rect(x, y, 4, 4, color);
}
pub fn icon_tile(&mut self, x: u32, y: u32, size: u32, bg: Color, label: &str) {
self.fill_rounded(x, y, size, size, bg);
self.centered_text(x, y + size.saturating_sub(font::CHAR_H) / 2, size, label, palette::WHITE);
}
pub fn gradient_progress(
&mut self,
x: u32, y: u32, w: u32, h: u32,
percent: u32,
track: Color, fill_l: Color, fill_r: Color, border: Color,
) {
let pct = percent.min(100);
let fill_w = w * pct / 100;
self.fill_rect(x, y, w, h, track);
if fill_w > 0 {
let steps = fill_w.saturating_sub(1).max(1);
for i in 0..fill_w {
let c = lerp_color(fill_l, fill_r, i, steps);
self.fill_rect(x + i, y, 1, h, c);
}
}
self.draw_rect(x, y, w, h, border);
}
pub fn fill_round4(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
if w < 8 || h < 8 { self.fill_rect(x, y, w, h, color); return; }
self.fill_rect(x, y + 4, w, h.saturating_sub(8), color);
self.fill_rect(x + 4, y, w.saturating_sub(8), 1, color);
self.fill_rect(x + 2, y + 1, w.saturating_sub(4), 1, color);
self.fill_rect(x + 1, y + 2, w.saturating_sub(2), 2, color);
self.fill_rect(x + 1, y + h - 4, w.saturating_sub(2), 2, color);
self.fill_rect(x + 2, y + h - 2, w.saturating_sub(4), 1, color);
self.fill_rect(x + 4, y + h - 1, w.saturating_sub(8), 1, color);
}
pub fn toggle_switch(&mut self, x: u32, y: u32, on: bool, accent: Color) {
let (tw, th) = (52u32, 26u32);
let bg = if on { accent } else { rgb(0x4A, 0x4A, 0x4A) };
self.fill_rect(x + th / 2, y, tw.saturating_sub(th), th, bg);
self.fill_round4(x, y, th, th, bg);
self.fill_round4(x + tw.saturating_sub(th), y, th, th, bg);
let kx = if on { x + tw.saturating_sub(th) + 2 } else { x + 2 };
self.fill_round4(kx, y + 2, th - 4, th - 4, palette::WHITE);
}
pub fn action_row(
&mut self, x: u32, y: u32, w: u32,
title: &str, subtitle: &str, value: &str,
hovered: bool, last: bool,
) -> u32 {
let h = 52u32;
let bg = if hovered { rgb(0x24, 0x24, 0x3C) } else { palette::CARD_BG };
self.fill_rect(x, y, w, h, bg);
self.draw_text(x + 16, y + 10, title, palette::TEXT);
if !subtitle.is_empty() {
self.draw_text(x + 16, y + 30, subtitle, palette::TEXT_DIM);
}
if !value.is_empty() {
self.right_text(x, y + 18, w.saturating_sub(16), value, palette::TEXT_DIM);
}
if !last {
self.hline(x + 16, y + h - 1, w.saturating_sub(16), palette::CARD_BORDER);
}
h
}
pub fn action_row_toggle(
&mut self, x: u32, y: u32, w: u32,
title: &str, subtitle: &str,
on: bool, accent: Color,
hovered: bool, last: bool,
) -> u32 {
let h = 52u32;
let bg = if hovered { rgb(0x24, 0x24, 0x3C) } else { palette::CARD_BG };
self.fill_rect(x, y, w, h, bg);
self.draw_text(x + 16, y + 10, title, palette::TEXT);
if !subtitle.is_empty() {
self.draw_text(x + 16, y + 30, subtitle, palette::TEXT_DIM);
}
self.toggle_switch(x + w.saturating_sub(68), y + (h - 26) / 2, on, accent);
if !last {
self.hline(x + 16, y + h - 1, w.saturating_sub(16), palette::CARD_BORDER);
}
h
}
pub fn action_row_nav(
&mut self, x: u32, y: u32, w: u32,
title: &str, subtitle: &str,
hovered: bool, last: bool,
) -> u32 {
let h = 52u32;
let bg = if hovered { rgb(0x24, 0x24, 0x3C) } else { palette::CARD_BG };
self.fill_rect(x, y, w, h, bg);
self.draw_text(x + 16, y + 10, title, palette::TEXT);
if !subtitle.is_empty() {
self.draw_text(x + 16, y + 30, subtitle, palette::TEXT_DIM);
}
self.draw_text(x + w.saturating_sub(24), y + 18, ">", palette::TEXT_DIM);
if !last {
self.hline(x + 16, y + h - 1, w.saturating_sub(16), palette::CARD_BORDER);
}
h
}
pub fn search_bar(
&mut self, x: u32, y: u32, w: u32,
text: &str, focused: bool, accent: Color,
) {
let h = 36u32;
self.fill_round4(x, y, w, h, rgb(0x1E, 0x1E, 0x30));
let border = if focused { accent } else { palette::CARD_BORDER };
self.draw_rect(x, y, w, h, border);
if focused {
self.draw_rect(x + 1, y + 1, w - 2, h - 2,
lerp_color(border, palette::CARD_BG, 1, 2));
}
self.draw_text(x + 8, y + 10, "O", palette::TEXT_DIM);
let qx = x + 28;
if text.is_empty() {
self.draw_text(qx, y + 10, "Search...", palette::TEXT_DIM);
} else {
self.draw_text(qx, y + 10, text, palette::TEXT);
}
}
pub fn gnome_headerbar(
&mut self, x: u32, y: u32, w: u32,
title: &str, has_back: bool, bg: Color,
) {
let h = 48u32;
self.fill_rect(x, y, w, h, bg);
self.hline(x, y + h - 1, w, palette::CARD_BORDER);
if has_back {
self.fill_round4(x + 8, y + 7, 34, 34, rgb(0x30, 0x30, 0x4A));
self.draw_text(x + 18, y + 16, "<", palette::TEXT);
}
self.centered_text(x, y + (h - 16) / 2, w, title, palette::TEXT);
let mx = x + w.saturating_sub(50);
self.fill_round4(mx, y + 7, 34, 34, rgb(0x30, 0x30, 0x4A));
self.fill_rect(mx + 7, y + 21, 4, 4, palette::TEXT_DIM);
self.fill_rect(mx + 15, y + 21, 4, 4, palette::TEXT_DIM);
self.fill_rect(mx + 23, y + 21, 4, 4, palette::TEXT_DIM);
}
pub fn spinner(&mut self, cx: u32, cy: u32, frame: u32, color: Color) {
const DOTS: &[(i32, i32)] = &[
(0, -10), (7, -7), (10, 0), (7, 7),
(0, 10), (-7, 7), (-10, 0), (-7, -7),
];
let head = (frame / 4) as usize % 8;
for (i, &(dx, dy)) in DOTS.iter().enumerate() {
let age = (i + 8 - head) % 8;
let c = lerp_color(color, palette::SURFACE, age as u32, 7);
let px = cx as i32 + dx - 1;
let py = cy as i32 + dy - 1;
if px >= 0 && py >= 0 {
self.fill_rect(px as u32, py as u32, 3, 3, c);
}
}
}
pub fn toast(&mut self, x: u32, y: u32, w: u32, message: &str, accent: Color) {
let h = 44u32;
self.fill_rect(x + 4, y + 4, w, h, rgb(0, 0, 0));
self.fill_round4(x, y, w, h, rgb(0x2E, 0x2E, 0x46));
self.draw_rect(x, y, w, h, rgb(0x44, 0x44, 0x66));
self.fill_rect(x + 12, y + (h - 8) / 2, 8, 8, accent);
self.draw_text(x + 28, y + (h - 16) / 2, message, palette::TEXT);
self.draw_text(x + w.saturating_sub(22), y + (h - 16) / 2, "x", palette::TEXT_DIM);
}
pub fn avatar(&mut self, x: u32, y: u32, size: u32, initials: &str, color: Color) {
self.fill_round4(x, y, size, size, color);
let tw = font::text_width(initials);
let tx = x + size.saturating_sub(tw) / 2;
let ty = y + size.saturating_sub(font::CHAR_H) / 2;
self.draw_text(tx, ty, initials, palette::WHITE);
}
pub fn chip(&mut self, x: u32, y: u32, text: &str, bg: Color, fg: Color) {
let tw = font::text_width(text);
let w = tw + 16;
let h = 22u32;
self.fill_round4(x, y, w, h, bg);
self.draw_text(x + 8, y + 3, text, fg);
}
pub fn backend_mut(&mut self) -> &mut B { self.backend }
}