⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/desktop/
app_switcher.rs

1//! Alt-Tab Application Switcher
2//!
3//! Provides an overlay for cycling between open windows with thumbnails
4//! and application icons. The switcher is shown while Alt is held and Tab
5//! is pressed to cycle; releasing Alt commits the selection.
6
7#![allow(dead_code)]
8
9use alloc::{string::String, vec::Vec};
10
11// ---------------------------------------------------------------------------
12// Types
13// ---------------------------------------------------------------------------
14
15/// Application icon (simple geometric shape for now)
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum AppIcon {
18    Terminal,
19    FileManager,
20    TextEditor,
21    Settings,
22    ImageViewer,
23    Generic,
24}
25
26/// Application switcher entry
27pub struct SwitcherEntry {
28    pub window_id: u32,
29    pub title: String,
30    pub app_icon: AppIcon,
31    /// Down-scaled window content (XRGB8888 pixels, row-major)
32    pub thumbnail: Option<Vec<u32>>,
33    pub thumbnail_width: u32,
34    pub thumbnail_height: u32,
35}
36
37// ---------------------------------------------------------------------------
38// AppSwitcher
39// ---------------------------------------------------------------------------
40
41/// Switcher overlay state.
42///
43/// The overlay is drawn centered on screen and shows one entry per open
44/// window. Entries are rendered as an icon + title, with the currently
45/// selected entry highlighted.
46pub struct AppSwitcher {
47    entries: Vec<SwitcherEntry>,
48    selected_index: usize,
49    visible: bool,
50    /// Top-left corner of the overlay (computed from screen size)
51    overlay_x: u32,
52    overlay_y: u32,
53    overlay_width: u32,
54    overlay_height: u32,
55    /// Dimensions of a single entry cell
56    entry_width: u32,
57    entry_height: u32,
58    padding: u32,
59}
60
61impl AppSwitcher {
62    /// Create a new (hidden) application switcher.
63    pub fn new() -> Self {
64        Self {
65            entries: Vec::new(),
66            selected_index: 0,
67            visible: false,
68            overlay_x: 0,
69            overlay_y: 0,
70            overlay_width: 0,
71            overlay_height: 0,
72            entry_width: 120,
73            entry_height: 100,
74            padding: 12,
75        }
76    }
77
78    /// Show the switcher overlay populated with the given windows.
79    ///
80    /// `windows` is a list of `(window_id, title)` pairs in Z-order (top
81    /// first). The first entry is pre-selected.
82    pub fn show(&mut self, windows: Vec<(u32, String)>) {
83        self.entries.clear();
84        self.selected_index = 0;
85
86        for (wid, title) in windows {
87            let icon = guess_icon(&title);
88            self.entries.push(SwitcherEntry {
89                window_id: wid,
90                title,
91                app_icon: icon,
92                thumbnail: None,
93                thumbnail_width: 0,
94                thumbnail_height: 0,
95            });
96        }
97
98        if !self.entries.is_empty() {
99            // Start with the second entry selected (Alt-Tab skips current)
100            if self.entries.len() > 1 {
101                self.selected_index = 1;
102            }
103            self.visible = true;
104        }
105    }
106
107    /// Hide the overlay and return the selected window ID (if any).
108    pub fn hide(&mut self) -> Option<u32> {
109        self.visible = false;
110        let wid = self.entries.get(self.selected_index).map(|e| e.window_id);
111        self.entries.clear();
112        wid
113    }
114
115    /// Advance selection to the next entry (wraps around).
116    pub fn next(&mut self) {
117        if self.entries.is_empty() {
118            return;
119        }
120        self.selected_index = (self.selected_index + 1) % self.entries.len();
121    }
122
123    /// Move selection to the previous entry (wraps around).
124    pub fn previous(&mut self) {
125        if self.entries.is_empty() {
126            return;
127        }
128        if self.selected_index == 0 {
129            self.selected_index = self.entries.len() - 1;
130        } else {
131            self.selected_index -= 1;
132        }
133    }
134
135    /// Returns `true` if the overlay is currently visible.
136    pub fn is_visible(&self) -> bool {
137        self.visible
138    }
139
140    /// Get the currently selected window ID (without hiding the overlay).
141    pub fn selected_window_id(&self) -> Option<u32> {
142        self.entries.get(self.selected_index).map(|e| e.window_id)
143    }
144
145    /// Get the number of entries.
146    pub fn entry_count(&self) -> usize {
147        self.entries.len()
148    }
149
150    /// Render the switcher overlay into a u32 XRGB8888 buffer.
151    ///
152    /// The overlay is drawn centered on the screen. `buf_width` and
153    /// `buf_height` are the full screen dimensions.
154    pub fn render(&self, buffer: &mut [u32], buf_width: u32, buf_height: u32) {
155        if !self.visible || self.entries.is_empty() {
156            return;
157        }
158
159        let count = self.entries.len() as u32;
160        let ew = self.entry_width;
161        let eh = self.entry_height;
162        let pad = self.padding;
163
164        // Compute overlay bounds
165        let total_width = count * ew + (count + 1) * pad;
166        let total_height = eh + pad * 2;
167
168        // Center on screen
169        let ox = if total_width < buf_width {
170            (buf_width - total_width) / 2
171        } else {
172            0
173        };
174        let oy = if total_height < buf_height {
175            (buf_height - total_height) / 2
176        } else {
177            0
178        };
179
180        let bw = buf_width as usize;
181
182        // Draw semi-transparent dark background
183        let bg_color: u32 = 0xE0202020; // ~88% opaque dark gray
184        for y in 0..total_height {
185            for x in 0..total_width {
186                let px = (ox + x) as usize;
187                let py = (oy + y) as usize;
188                if px < buf_width as usize && py < buf_height as usize {
189                    let idx = py * bw + px;
190                    if idx < buffer.len() {
191                        // Alpha blend with existing content
192                        let src_a = (bg_color >> 24) & 0xFF;
193                        let src_r = (bg_color >> 16) & 0xFF;
194                        let src_g = (bg_color >> 8) & 0xFF;
195                        let src_b = bg_color & 0xFF;
196
197                        let dst = buffer[idx];
198                        let dst_r = (dst >> 16) & 0xFF;
199                        let dst_g = (dst >> 8) & 0xFF;
200                        let dst_b = dst & 0xFF;
201
202                        let inv_a = 255 - src_a;
203                        let r = (src_r * src_a + dst_r * inv_a) / 255;
204                        let g = (src_g * src_a + dst_g * inv_a) / 255;
205                        let b = (src_b * src_a + dst_b * inv_a) / 255;
206
207                        buffer[idx] = 0xFF00_0000 | (r << 16) | (g << 8) | b;
208                    }
209                }
210            }
211        }
212
213        // Draw rounded border
214        let border_color: u32 = 0xFF5294E2; // Blue accent
215                                            // Top and bottom edges
216        for x in 0..total_width {
217            let px = (ox + x) as usize;
218            let py_top = oy as usize;
219            let py_bot = (oy + total_height).saturating_sub(1) as usize;
220            if px < buf_width as usize {
221                let idx_t = py_top * bw + px;
222                let idx_b = py_bot * bw + px;
223                if idx_t < buffer.len() {
224                    buffer[idx_t] = border_color;
225                }
226                if idx_b < buffer.len() {
227                    buffer[idx_b] = border_color;
228                }
229            }
230        }
231        // Left and right edges
232        for y in 0..total_height {
233            let py = (oy + y) as usize;
234            let px_left = ox as usize;
235            let px_right = (ox + total_width).saturating_sub(1) as usize;
236            if py < buf_height as usize {
237                let idx_l = py * bw + px_left;
238                let idx_r = py * bw + px_right;
239                if idx_l < buffer.len() {
240                    buffer[idx_l] = border_color;
241                }
242                if idx_r < buffer.len() {
243                    buffer[idx_r] = border_color;
244                }
245            }
246        }
247
248        // Draw each entry
249        for (i, entry) in self.entries.iter().enumerate() {
250            let entry_x = ox + pad + (i as u32) * (ew + pad);
251            let entry_y = oy + pad;
252            let selected = i == self.selected_index;
253
254            self.render_entry(buffer, buf_width, entry_x, entry_y, entry, selected);
255        }
256    }
257
258    /// Render a single switcher entry at position `(x, y)` in the buffer.
259    fn render_entry(
260        &self,
261        buffer: &mut [u32],
262        buf_width: u32,
263        x: u32,
264        y: u32,
265        entry: &SwitcherEntry,
266        selected: bool,
267    ) {
268        let bw = buf_width as usize;
269        let ew = self.entry_width as usize;
270        let eh = self.entry_height as usize;
271
272        // Selection highlight background
273        let entry_bg = if selected {
274            0xFF3A5F8A // Highlighted blue
275        } else {
276            0xFF2A2A2A // Dark background
277        };
278
279        for dy in 0..eh {
280            for dx in 0..ew {
281                let px = x as usize + dx;
282                let py = y as usize + dy;
283                if px < buf_width as usize {
284                    let idx = py * bw + px;
285                    if idx < buffer.len() {
286                        buffer[idx] = entry_bg;
287                    }
288                }
289            }
290        }
291
292        // Selection border
293        if selected {
294            let sel_border: u32 = 0xFF7AB4FF; // Light blue
295            for dx in 0..ew {
296                let px = x as usize + dx;
297                // Top
298                let idx_t = y as usize * bw + px;
299                if idx_t < buffer.len() {
300                    buffer[idx_t] = sel_border;
301                }
302                // Bottom
303                let idx_b = (y as usize + eh - 1) * bw + px;
304                if idx_b < buffer.len() {
305                    buffer[idx_b] = sel_border;
306                }
307            }
308            for dy in 0..eh {
309                let py = y as usize + dy;
310                // Left
311                let idx_l = py * bw + x as usize;
312                if idx_l < buffer.len() {
313                    buffer[idx_l] = sel_border;
314                }
315                // Right
316                let idx_r = py * bw + x as usize + ew - 1;
317                if idx_r < buffer.len() {
318                    buffer[idx_r] = sel_border;
319                }
320            }
321        }
322
323        // Draw icon centered in the upper portion
324        let icon_size: u32 = 32;
325        let icon_x = x + (self.entry_width.saturating_sub(icon_size)) / 2;
326        let icon_y = y + 8;
327        render_icon(buffer, buf_width, icon_x, icon_y, icon_size, entry.app_icon);
328
329        // Draw title text centered below the icon (truncated to fit)
330        let title_y = y + 8 + icon_size + 8;
331        let max_chars = (self.entry_width / 8) as usize;
332        let title_bytes = entry.title.as_bytes();
333        let display_len = title_bytes.len().min(max_chars);
334        let text_pixel_w = display_len * 8;
335        let title_x = x + (self.entry_width.saturating_sub(text_pixel_w as u32)) / 2;
336
337        let text_color: u32 = 0xFFFFFFFF;
338        let r = (text_color >> 16) & 0xFF;
339        let g = (text_color >> 8) & 0xFF;
340        let b = text_color & 0xFF;
341        let pixel = 0xFF00_0000 | (r << 16) | (g << 8) | b;
342
343        for (ci, &ch) in title_bytes[..display_len].iter().enumerate() {
344            let glyph = crate::graphics::font8x16::glyph(ch);
345            for (row, &bits) in glyph.iter().enumerate() {
346                for col in 0..8 {
347                    if (bits >> (7 - col)) & 1 != 0 {
348                        let px = title_x as usize + ci * 8 + col;
349                        let py = title_y as usize + row;
350                        if px < buf_width as usize {
351                            let idx = py * bw + px;
352                            if idx < buffer.len() {
353                                buffer[idx] = pixel;
354                            }
355                        }
356                    }
357                }
358            }
359        }
360    }
361}
362
363impl Default for AppSwitcher {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369// ---------------------------------------------------------------------------
370// Icon rendering
371// ---------------------------------------------------------------------------
372
373/// Render a simple geometric icon for an application type.
374///
375/// Uses basic line/shape drawing (no bitmaps) into a u32 XRGB8888 buffer.
376pub fn render_icon(buffer: &mut [u32], buf_width: u32, x: u32, y: u32, size: u32, icon: AppIcon) {
377    let bw = buf_width as usize;
378    let sz = size as usize;
379    let bx = x as usize;
380    let by = y as usize;
381
382    match icon {
383        AppIcon::Terminal => {
384            // Terminal: >_ prompt shape
385            let fg: u32 = 0xFF00CC66; // Green
386            let bg: u32 = 0xFF1A1A1A; // Dark background
387
388            // Fill background
389            fill_rect(buffer, bw, bx, by, sz, sz, bg);
390
391            // Draw ">" character (two diagonal lines forming an arrow)
392            let m = sz / 6; // margin
393            let mid_y = sz / 2;
394            // Top arm of >
395            for i in 0..(mid_y - m) {
396                let px = bx + m + i;
397                let py = by + m + i;
398                if px < buf_width as usize {
399                    let idx = py * bw + px;
400                    if idx < buffer.len() {
401                        buffer[idx] = fg;
402                    }
403                }
404            }
405            // Bottom arm of >
406            for i in 0..(mid_y - m) {
407                let px = bx + m + i;
408                let py = by + mid_y + (mid_y - m).saturating_sub(1 + i);
409                if px < buf_width as usize {
410                    let idx = py * bw + px;
411                    if idx < buffer.len() {
412                        buffer[idx] = fg;
413                    }
414                }
415            }
416            // Draw "_" underscore
417            let uy = by + mid_y + m;
418            let ux_start = bx + mid_y;
419            let ux_end = bx + sz - m;
420            for px in ux_start..ux_end {
421                if px < buf_width as usize {
422                    let idx = uy * bw + px;
423                    if idx < buffer.len() {
424                        buffer[idx] = fg;
425                    }
426                }
427            }
428        }
429        AppIcon::FileManager => {
430            // Folder shape
431            let fg: u32 = 0xFFDDAA22; // Golden yellow
432
433            // Fill main folder body
434            let tab_h = sz / 4;
435            let tab_w = sz / 2;
436            // Tab on top-left
437            fill_rect(buffer, bw, bx + 2, by + 2, tab_w, tab_h, fg);
438            // Main body
439            fill_rect(
440                buffer,
441                bw,
442                bx + 2,
443                by + 2 + tab_h,
444                sz - 4,
445                sz - tab_h - 4,
446                fg,
447            );
448        }
449        AppIcon::TextEditor => {
450            // Document shape with lines
451            let bg: u32 = 0xFFEEEEEE; // Light paper
452            let fg: u32 = 0xFF333333; // Dark text lines
453
454            // Paper background
455            fill_rect(buffer, bw, bx + 4, by + 2, sz - 8, sz - 4, bg);
456
457            // Horizontal text lines
458            let line_gap = sz / 6;
459            for i in 1..5 {
460                let ly = by + 4 + i * line_gap;
461                let lx_start = bx + 8;
462                let lx_end = bx + sz - 8;
463                for px in lx_start..lx_end.min(bx + sz) {
464                    if px < buf_width as usize && ly < by + sz {
465                        let idx = ly * bw + px;
466                        if idx < buffer.len() {
467                            buffer[idx] = fg;
468                        }
469                    }
470                }
471            }
472        }
473        AppIcon::Settings => {
474            // Gear shape (simplified as octagon)
475            let fg: u32 = 0xFF888888; // Gray
476
477            let center = sz / 2;
478            let outer_r = (sz / 2).saturating_sub(2);
479            let inner_r = outer_r / 2;
480            let outer_sq = (outer_r * outer_r) as i32;
481            let inner_sq = (inner_r * inner_r) as i32;
482
483            for dy in 0..sz {
484                for dx in 0..sz {
485                    let cx = dx as i32 - center as i32;
486                    let cy = dy as i32 - center as i32;
487                    let dist_sq = cx * cx + cy * cy;
488                    if dist_sq <= outer_sq && dist_sq >= inner_sq {
489                        let px = bx + dx;
490                        let py = by + dy;
491                        if px < buf_width as usize {
492                            let idx = py * bw + px;
493                            if idx < buffer.len() {
494                                buffer[idx] = fg;
495                            }
496                        }
497                    }
498                }
499            }
500        }
501        AppIcon::ImageViewer => {
502            // Mountain/landscape shape
503            let sky: u32 = 0xFF6699CC; // Light blue
504            let mtn: u32 = 0xFF336633; // Dark green
505
506            // Sky background
507            fill_rect(buffer, bw, bx + 2, by + 2, sz - 4, sz - 4, sky);
508
509            // Mountain triangle (centered)
510            let base_y = by + sz - 4;
511            let peak_x = bx + sz / 2;
512            let peak_y = by + sz / 4;
513            let half_base = sz / 3;
514
515            for row_y in peak_y..base_y {
516                let progress = row_y - peak_y;
517                let total = base_y - peak_y;
518                if total == 0 {
519                    continue;
520                }
521                let half_w = (progress * half_base) / total;
522                let start_x = peak_x.saturating_sub(half_w);
523                let end_x = peak_x + half_w;
524                for px in start_x..end_x.min(bx + sz - 2) {
525                    if px < buf_width as usize {
526                        let idx = row_y * bw + px;
527                        if idx < buffer.len() {
528                            buffer[idx] = mtn;
529                        }
530                    }
531                }
532            }
533        }
534        AppIcon::Generic => {
535            // Default: simple bordered square
536            let fg: u32 = 0xFF6688AA;
537            let bg: u32 = 0xFF334455;
538
539            fill_rect(buffer, bw, bx + 2, by + 2, sz - 4, sz - 4, bg);
540
541            // Border
542            for i in 0..sz {
543                // Top
544                set_pixel(buffer, bw, bx + i, by + 2, fg);
545                // Bottom
546                set_pixel(buffer, bw, bx + i, by + sz - 3, fg);
547                // Left
548                set_pixel(buffer, bw, bx + 2, by + i, fg);
549                // Right
550                set_pixel(buffer, bw, bx + sz - 3, by + i, fg);
551            }
552        }
553    }
554}
555
556// ---------------------------------------------------------------------------
557// Helper: guess icon from window title
558// ---------------------------------------------------------------------------
559
560/// Heuristic to guess an AppIcon from the window title.
561fn guess_icon(title: &str) -> AppIcon {
562    let lower: Vec<u8> = title.bytes().map(|b| b.to_ascii_lowercase()).collect();
563    let lower_str = core::str::from_utf8(&lower).unwrap_or("");
564
565    if lower_str.contains("terminal")
566        || lower_str.contains("shell")
567        || lower_str.contains("console")
568    {
569        AppIcon::Terminal
570    } else if lower_str.contains("file") || lower_str.contains("folder") {
571        AppIcon::FileManager
572    } else if lower_str.contains("editor")
573        || lower_str.contains("text")
574        || lower_str.contains("code")
575    {
576        AppIcon::TextEditor
577    } else if lower_str.contains("setting") || lower_str.contains("config") {
578        AppIcon::Settings
579    } else if lower_str.contains("image")
580        || lower_str.contains("photo")
581        || lower_str.contains("view")
582    {
583        AppIcon::ImageViewer
584    } else {
585        AppIcon::Generic
586    }
587}
588
589// ---------------------------------------------------------------------------
590// Pixel helpers
591// ---------------------------------------------------------------------------
592
593/// Fill a rectangle in a u32 pixel buffer.
594fn fill_rect(
595    buffer: &mut [u32],
596    buf_width: usize,
597    x: usize,
598    y: usize,
599    w: usize,
600    h: usize,
601    color: u32,
602) {
603    for dy in 0..h {
604        for dx in 0..w {
605            let idx = (y + dy) * buf_width + (x + dx);
606            if idx < buffer.len() {
607                buffer[idx] = color;
608            }
609        }
610    }
611}
612
613/// Set a single pixel in a u32 buffer (bounds-checked).
614fn set_pixel(buffer: &mut [u32], buf_width: usize, x: usize, y: usize, color: u32) {
615    let idx = y * buf_width + x;
616    if idx < buffer.len() {
617        buffer[idx] = color;
618    }
619}