Skip to main content

oxide_gui_core/
canvas.rs

1//! `Canvas<B>` — high-level drawing helper built on top of a `Backend`.
2//!
3//! `Canvas` owns a mutable reference to a `Backend` and adds convenience
4//! methods (rounded rects, progress bars, shadow boxes) that are composed
5//! from the primitives the backend already provides.  Nothing here touches
6//! the framebuffer directly — every call goes through `Backend::fill_rect`.
7
8use crate::backend::Backend;
9use crate::color::{Color, lerp_color, palette, rgb};
10use crate::event::Event;
11use crate::font;
12
13pub struct Canvas<'b, B: Backend> {
14    backend: &'b mut B,
15}
16
17impl<'b, B: Backend> Canvas<'b, B> {
18    pub fn new(backend: &'b mut B) -> Self { Self { backend } }
19
20    // ── Delegation ────────────────────────────────────────────────────────
21
22    pub fn width(&self)  -> u32 { self.backend.width() }
23    pub fn height(&self) -> u32 { self.backend.height() }
24    pub fn size(&self)   -> (u32, u32) { self.backend.size() }
25
26    pub fn fill_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
27        self.backend.fill_rect(x, y, w, h, color);
28    }
29
30    pub fn draw_text(&mut self, x: u32, y: u32, text: &str, color: Color) {
31        self.backend.draw_text(x, y, text, color);
32    }
33
34    pub fn hline(&mut self, x: u32, y: u32, w: u32, color: Color) {
35        self.backend.hline(x, y, w, color);
36    }
37
38    pub fn vline(&mut self, x: u32, y: u32, h: u32, color: Color) {
39        self.backend.vline(x, y, h, color);
40    }
41
42    pub fn draw_rect(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
43        self.backend.draw_rect(x, y, w, h, color);
44    }
45
46    pub fn clear(&mut self, color: Color) {
47        self.backend.clear(color);
48    }
49
50    pub fn present(&mut self) {
51        self.backend.present();
52    }
53
54    pub fn poll_event(&mut self) -> Option<Event> {
55        self.backend.poll_event()
56    }
57
58    // ── Composition helpers ───────────────────────────────────────────────
59
60    /// Draw a filled rectangle with a 1-pixel border.
61    pub fn panel(&mut self, x: u32, y: u32, w: u32, h: u32, fill: Color, border: Color) {
62        self.fill_rect(x, y, w, h, fill);
63        self.draw_rect(x, y, w, h, border);
64    }
65
66    /// Draw a title bar: filled strip + white text.
67    pub fn title_bar(&mut self, x: u32, y: u32, w: u32, h: u32, title: &str, bg: Color) {
68        self.fill_rect(x, y, w, h, bg);
69        let pad = 8u32;
70        self.draw_text(x + pad, y + (h - font::CHAR_H) / 2, title, palette::WHITE);
71    }
72
73    /// Draw a simple button with border, fill, and centred label.
74    pub fn button(
75        &mut self,
76        x: u32, y: u32, w: u32, h: u32,
77        label: &str,
78        fill: Color, border: Color, text_color: Color,
79    ) {
80        self.panel(x, y, w, h, fill, border);
81        let lw  = font::text_width(label);
82        let tx  = x + w.saturating_sub(lw) / 2;
83        let ty  = y + h.saturating_sub(font::CHAR_H) / 2;
84        self.draw_text(tx, ty, label, text_color);
85    }
86
87    /// Draw a horizontal progress bar.  `percent` is clamped to 0–100.
88    pub fn progress_bar(
89        &mut self,
90        x: u32, y: u32, w: u32, h: u32,
91        percent: u32,
92        track: Color, fill: Color, border: Color,
93    ) {
94        let pct  = percent.min(100);
95        let fill_w = w * pct / 100;
96        self.fill_rect(x, y, w, h, track);
97        if fill_w > 0 { self.fill_rect(x, y, fill_w, h, fill); }
98        self.draw_rect(x, y, w, h, border);
99    }
100
101    /// Draw a vertical divider line.
102    pub fn divider_v(&mut self, x: u32, y: u32, h: u32) {
103        self.vline(x, y, h, palette::DIVIDER);
104    }
105
106    /// Draw a horizontal divider line.
107    pub fn divider_h(&mut self, x: u32, y: u32, w: u32) {
108        self.hline(x, y, w, palette::DIVIDER);
109    }
110
111    /// Draw text centered horizontally in a `w`-wide strip starting at `x`.
112    pub fn centered_text(&mut self, x: u32, y: u32, w: u32, text: &str, color: Color) {
113        let tw = font::text_width(text);
114        let tx = x + w.saturating_sub(tw) / 2;
115        self.draw_text(tx, y, text, color);
116    }
117
118    /// Draw right-aligned text ending at `x + w`.
119    pub fn right_text(&mut self, x: u32, y: u32, w: u32, text: &str, color: Color) {
120        let tw = font::text_width(text);
121        let tx = x + w.saturating_sub(tw);
122        self.draw_text(tx, y, text, color);
123    }
124
125    /// Fill a rounded-corner rectangle (corners are clipped by 1 pixel).
126    pub fn fill_rounded(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
127        if w < 2 || h < 2 { self.fill_rect(x, y, w, h, color); return; }
128        self.fill_rect(x + 1, y,         w - 2, h,     color); // main body
129        self.fill_rect(x,     y + 1,     1,     h - 2, color); // left strip
130        self.fill_rect(x + w - 1, y + 1, 1,     h - 2, color); // right strip
131    }
132
133    /// Fill a horizontal gradient from `left` to `right` color across `w` pixels.
134    pub fn gradient_h(&mut self, x: u32, y: u32, w: u32, h: u32, left: Color, right: Color) {
135        if w == 0 { return; }
136        let steps = (w - 1).max(1);
137        for i in 0..w {
138            let c = lerp_color(left, right, i, steps);
139            self.fill_rect(x + i, y, 1, h, c);
140        }
141    }
142
143    /// Fill a vertical gradient from `top` to `bottom` color across `h` pixels.
144    pub fn gradient_v(&mut self, x: u32, y: u32, w: u32, h: u32, top: Color, bottom: Color) {
145        if h == 0 { return; }
146        let steps = (h - 1).max(1);
147        for i in 0..h {
148            let c = lerp_color(top, bottom, i, steps);
149            self.fill_rect(x, y + i, w, 1, c);
150        }
151    }
152
153    /// Panel with a 4-pixel dark drop shadow behind it.
154    pub fn shadow_panel(&mut self, x: u32, y: u32, w: u32, h: u32, fill: Color, border: Color) {
155        self.fill_rect(x + 4, y + 4, w, h, palette::BLACK);
156        self.panel(x, y, w, h, fill, border);
157    }
158
159    /// 3-pixel vertical accent bar — used for selection indicators.
160    pub fn accent_bar(&mut self, x: u32, y: u32, h: u32, color: Color) {
161        self.fill_rect(x, y, 3, h, color);
162    }
163
164    /// Tiny 4×4 filled status dot.
165    pub fn dot(&mut self, x: u32, y: u32, color: Color) {
166        self.fill_rect(x, y, 4, 4, color);
167    }
168
169    /// Colored app icon tile: rounded background + centered label.
170    pub fn icon_tile(&mut self, x: u32, y: u32, size: u32, bg: Color, label: &str) {
171        self.fill_rounded(x, y, size, size, bg);
172        self.centered_text(x, y + size.saturating_sub(font::CHAR_H) / 2, size, label, palette::WHITE);
173    }
174
175    /// Progress bar with a two-stop horizontal gradient fill.
176    pub fn gradient_progress(
177        &mut self,
178        x: u32, y: u32, w: u32, h: u32,
179        percent: u32,
180        track: Color, fill_l: Color, fill_r: Color, border: Color,
181    ) {
182        let pct    = percent.min(100);
183        let fill_w = w * pct / 100;
184        self.fill_rect(x, y, w, h, track);
185        if fill_w > 0 {
186            let steps = fill_w.saturating_sub(1).max(1);
187            for i in 0..fill_w {
188                let c = lerp_color(fill_l, fill_r, i, steps);
189                self.fill_rect(x + i, y, 1, h, c);
190            }
191        }
192        self.draw_rect(x, y, w, h, border);
193    }
194
195    // ── GNOME / libadwaita-style widgets ─────────────────────────────────
196
197    /// Filled rectangle with 4-pixel rounded corners (AdwPreferencesGroup / card style).
198    pub fn fill_round4(&mut self, x: u32, y: u32, w: u32, h: u32, color: Color) {
199        if w < 8 || h < 8 { self.fill_rect(x, y, w, h, color); return; }
200        // Body between rounded rows
201        self.fill_rect(x, y + 4, w, h.saturating_sub(8), color);
202        // Top 4 rows (staircase approximating 4 px radius)
203        self.fill_rect(x + 4, y,     w.saturating_sub(8), 1, color);
204        self.fill_rect(x + 2, y + 1, w.saturating_sub(4), 1, color);
205        self.fill_rect(x + 1, y + 2, w.saturating_sub(2), 2, color);
206        // Bottom 4 rows
207        self.fill_rect(x + 1, y + h - 4, w.saturating_sub(2), 2, color);
208        self.fill_rect(x + 2, y + h - 2, w.saturating_sub(4), 1, color);
209        self.fill_rect(x + 4, y + h - 1, w.saturating_sub(8), 1, color);
210    }
211
212    /// GNOME AdwToggleSwitch (52×26 px). Knob slides left=off, right=on.
213    pub fn toggle_switch(&mut self, x: u32, y: u32, on: bool, accent: Color) {
214        let (tw, th) = (52u32, 26u32);
215        let bg = if on { accent } else { rgb(0x4A, 0x4A, 0x4A) };
216        // Pill: left cap + body + right cap (each cap is th × th rounded)
217        self.fill_rect(x + th / 2, y, tw.saturating_sub(th), th, bg);
218        self.fill_round4(x, y, th, th, bg);
219        self.fill_round4(x + tw.saturating_sub(th), y, th, th, bg);
220        // White knob (22×22), 2 px inset
221        let kx = if on { x + tw.saturating_sub(th) + 2 } else { x + 2 };
222        self.fill_round4(kx, y + 2, th - 4, th - 4, palette::WHITE);
223    }
224
225    /// GNOME AdwActionRow — title + optional subtitle on left, text value on right.
226    /// Set `last = true` on the final row of a group to skip the separator.
227    /// Returns the row height (52).
228    pub fn action_row(
229        &mut self, x: u32, y: u32, w: u32,
230        title: &str, subtitle: &str, value: &str,
231        hovered: bool, last: bool,
232    ) -> u32 {
233        let h = 52u32;
234        let bg = if hovered { rgb(0x24, 0x24, 0x3C) } else { palette::CARD_BG };
235        self.fill_rect(x, y, w, h, bg);
236        self.draw_text(x + 16, y + 10, title, palette::TEXT);
237        if !subtitle.is_empty() {
238            self.draw_text(x + 16, y + 30, subtitle, palette::TEXT_DIM);
239        }
240        if !value.is_empty() {
241            self.right_text(x, y + 18, w.saturating_sub(16), value, palette::TEXT_DIM);
242        }
243        if !last {
244            self.hline(x + 16, y + h - 1, w.saturating_sub(16), palette::CARD_BORDER);
245        }
246        h
247    }
248
249    /// GNOME AdwSwitchRow — action row with a toggle switch on the right.
250    /// Returns the row height (52).
251    pub fn action_row_toggle(
252        &mut self, x: u32, y: u32, w: u32,
253        title: &str, subtitle: &str,
254        on: bool, accent: Color,
255        hovered: bool, last: bool,
256    ) -> u32 {
257        let h = 52u32;
258        let bg = if hovered { rgb(0x24, 0x24, 0x3C) } else { palette::CARD_BG };
259        self.fill_rect(x, y, w, h, bg);
260        self.draw_text(x + 16, y + 10, title, palette::TEXT);
261        if !subtitle.is_empty() {
262            self.draw_text(x + 16, y + 30, subtitle, palette::TEXT_DIM);
263        }
264        self.toggle_switch(x + w.saturating_sub(68), y + (h - 26) / 2, on, accent);
265        if !last {
266            self.hline(x + 16, y + h - 1, w.saturating_sub(16), palette::CARD_BORDER);
267        }
268        h
269    }
270
271    /// Navigation action row — chevron on right, no value text.
272    /// Returns the row height (52).
273    pub fn action_row_nav(
274        &mut self, x: u32, y: u32, w: u32,
275        title: &str, subtitle: &str,
276        hovered: bool, last: bool,
277    ) -> u32 {
278        let h = 52u32;
279        let bg = if hovered { rgb(0x24, 0x24, 0x3C) } else { palette::CARD_BG };
280        self.fill_rect(x, y, w, h, bg);
281        self.draw_text(x + 16, y + 10, title, palette::TEXT);
282        if !subtitle.is_empty() {
283            self.draw_text(x + 16, y + 30, subtitle, palette::TEXT_DIM);
284        }
285        self.draw_text(x + w.saturating_sub(24), y + 18, ">", palette::TEXT_DIM);
286        if !last {
287            self.hline(x + 16, y + h - 1, w.saturating_sub(16), palette::CARD_BORDER);
288        }
289        h
290    }
291
292    /// Pill-shaped search entry (height 36). Draws placeholder or query text.
293    pub fn search_bar(
294        &mut self, x: u32, y: u32, w: u32,
295        text: &str, focused: bool, accent: Color,
296    ) {
297        let h = 36u32;
298        self.fill_round4(x, y, w, h, rgb(0x1E, 0x1E, 0x30));
299        let border = if focused { accent } else { palette::CARD_BORDER };
300        self.draw_rect(x, y, w, h, border);
301        if focused {
302            self.draw_rect(x + 1, y + 1, w - 2, h - 2,
303                           lerp_color(border, palette::CARD_BG, 1, 2));
304        }
305        // Magnifier dot (symbolic circle using the 'O' glyph)
306        self.draw_text(x + 8, y + 10, "O", palette::TEXT_DIM);
307        let qx = x + 28;
308        if text.is_empty() {
309            self.draw_text(qx, y + 10, "Search...", palette::TEXT_DIM);
310        } else {
311            self.draw_text(qx, y + 10, text, palette::TEXT);
312        }
313    }
314
315    /// GNOME AdwHeaderBar (48 px tall) — centered title, back button, end menu button.
316    pub fn gnome_headerbar(
317        &mut self, x: u32, y: u32, w: u32,
318        title: &str, has_back: bool, bg: Color,
319    ) {
320        let h = 48u32;
321        self.fill_rect(x, y, w, h, bg);
322        self.hline(x, y + h - 1, w, palette::CARD_BORDER);
323        if has_back {
324            self.fill_round4(x + 8, y + 7, 34, 34, rgb(0x30, 0x30, 0x4A));
325            self.draw_text(x + 18, y + 16, "<", palette::TEXT);
326        }
327        self.centered_text(x, y + (h - 16) / 2, w, title, palette::TEXT);
328        // Three-dot menu button at end
329        let mx = x + w.saturating_sub(50);
330        self.fill_round4(mx, y + 7, 34, 34, rgb(0x30, 0x30, 0x4A));
331        self.fill_rect(mx +  7, y + 21, 4, 4, palette::TEXT_DIM);
332        self.fill_rect(mx + 15, y + 21, 4, 4, palette::TEXT_DIM);
333        self.fill_rect(mx + 23, y + 21, 4, 4, palette::TEXT_DIM);
334    }
335
336    /// 8-dot animated spinner. `cx`/`cy` is the center. Frame drives rotation.
337    pub fn spinner(&mut self, cx: u32, cy: u32, frame: u32, color: Color) {
338        const DOTS: &[(i32, i32)] = &[
339            (0, -10), (7, -7), (10, 0), (7, 7),
340            (0, 10),  (-7, 7), (-10, 0), (-7, -7),
341        ];
342        let head = (frame / 4) as usize % 8;
343        for (i, &(dx, dy)) in DOTS.iter().enumerate() {
344            let age = (i + 8 - head) % 8;
345            let c   = lerp_color(color, palette::SURFACE, age as u32, 7);
346            let px  = cx as i32 + dx - 1;
347            let py  = cy as i32 + dy - 1;
348            if px >= 0 && py >= 0 {
349                self.fill_rect(px as u32, py as u32, 3, 3, c);
350            }
351        }
352    }
353
354    /// Pill-shaped in-app notification toast (44 px tall).
355    pub fn toast(&mut self, x: u32, y: u32, w: u32, message: &str, accent: Color) {
356        let h = 44u32;
357        // Drop shadow
358        self.fill_rect(x + 4, y + 4, w, h, rgb(0, 0, 0));
359        // Background
360        self.fill_round4(x, y, w, h, rgb(0x2E, 0x2E, 0x46));
361        self.draw_rect(x, y, w, h, rgb(0x44, 0x44, 0x66));
362        // Colored accent square
363        self.fill_rect(x + 12, y + (h - 8) / 2, 8, 8, accent);
364        // Message
365        self.draw_text(x + 28, y + (h - 16) / 2, message, palette::TEXT);
366        // Dismiss 'x'
367        self.draw_text(x + w.saturating_sub(22), y + (h - 16) / 2, "x", palette::TEXT_DIM);
368    }
369
370    /// Circular avatar tile with initials (uses fill_round4 as circle approximation).
371    pub fn avatar(&mut self, x: u32, y: u32, size: u32, initials: &str, color: Color) {
372        self.fill_round4(x, y, size, size, color);
373        let tw = font::text_width(initials);
374        let tx = x + size.saturating_sub(tw) / 2;
375        let ty = y + size.saturating_sub(font::CHAR_H) / 2;
376        self.draw_text(tx, ty, initials, palette::WHITE);
377    }
378
379    /// Small pill-shaped chip / badge label.
380    pub fn chip(&mut self, x: u32, y: u32, text: &str, bg: Color, fg: Color) {
381        let tw = font::text_width(text);
382        let w  = tw + 16;
383        let h  = 22u32;
384        self.fill_round4(x, y, w, h, bg);
385        self.draw_text(x + 8, y + 3, text, fg);
386    }
387
388    /// Access the underlying backend directly for operations not in Canvas.
389    pub fn backend_mut(&mut self) -> &mut B { self.backend }
390}