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

veridian_kernel/desktop/
desktop_icons.rs

1//! Desktop Icons
2//!
3//! Provides desktop icon rendering, interaction (click, double-click, drag),
4//! and `.desktop`-file parsing. Icons are arranged on a grid and can be
5//! dragged to new positions with snap-to-grid alignment.
6
7#![allow(dead_code)]
8
9use alloc::{string::String, vec, vec::Vec};
10
11// ---------------------------------------------------------------------------
12// Desktop file parser
13// ---------------------------------------------------------------------------
14
15/// Parsed `.desktop` file (freedesktop-style).
16#[derive(Debug, Clone)]
17pub struct DesktopFile {
18    /// Display name (from `Name=`).
19    pub name: String,
20    /// Command to execute (from `Exec=`).
21    pub exec_command: String,
22    /// Icon identifier (from `Icon=`).
23    pub icon: String,
24    /// Categories list (from `Categories=`, semicolon-separated).
25    pub categories: Vec<String>,
26    /// Supported MIME types (from `MimeType=`, semicolon-separated).
27    pub mime_types: Vec<String>,
28}
29
30impl DesktopFile {
31    /// Parse a `.desktop` file from its textual content.
32    pub fn parse(content: &str) -> Option<Self> {
33        let mut name = String::new();
34        let mut exec_command = String::new();
35        let mut icon = String::new();
36        let mut categories = Vec::new();
37        let mut mime_types = Vec::new();
38
39        for line in content.lines() {
40            let line = line.trim();
41            if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
42                continue;
43            }
44
45            if let Some(val) = line.strip_prefix("Name=") {
46                name = String::from(val);
47            } else if let Some(val) = line.strip_prefix("Exec=") {
48                exec_command = String::from(val);
49            } else if let Some(val) = line.strip_prefix("Icon=") {
50                icon = String::from(val);
51            } else if let Some(val) = line.strip_prefix("Categories=") {
52                categories = val
53                    .split(';')
54                    .filter(|s| !s.is_empty())
55                    .map(String::from)
56                    .collect();
57            } else if let Some(val) = line.strip_prefix("MimeType=") {
58                mime_types = val
59                    .split(';')
60                    .filter(|s| !s.is_empty())
61                    .map(String::from)
62                    .collect();
63            }
64        }
65
66        if name.is_empty() {
67            return None;
68        }
69
70        Some(Self {
71            name,
72            exec_command,
73            icon,
74            categories,
75            mime_types,
76        })
77    }
78}
79
80// ---------------------------------------------------------------------------
81// Desktop icon
82// ---------------------------------------------------------------------------
83
84/// Icon size in pixels (square).
85pub const ICON_SIZE: u32 = 16;
86
87/// A single desktop icon.
88#[derive(Debug, Clone)]
89pub struct DesktopIcon {
90    /// Display name (rendered below the icon).
91    pub name: String,
92    /// 16x16 pixel data (ARGB8888, row-major).
93    pub icon_data: Vec<u32>,
94    /// X position on the desktop (pixels).
95    pub x: i32,
96    /// Y position on the desktop (pixels).
97    pub y: i32,
98    /// Whether this icon is currently selected.
99    pub selected: bool,
100    /// Path to the associated `.desktop` file.
101    pub desktop_file_path: String,
102    /// Parsed desktop file data.
103    pub desktop_file: Option<DesktopFile>,
104}
105
106impl DesktopIcon {
107    /// Create a new icon with a default solid colour.
108    pub fn new(name: &str, x: i32, y: i32) -> Self {
109        Self {
110            name: String::from(name),
111            icon_data: vec![0xFF4488CC; (ICON_SIZE * ICON_SIZE) as usize],
112            x,
113            y,
114            selected: false,
115            desktop_file_path: String::new(),
116            desktop_file: None,
117        }
118    }
119
120    /// Set custom icon pixel data (must be ICON_SIZE x ICON_SIZE).
121    pub fn set_icon_data(&mut self, data: &[u32]) {
122        let expected = (ICON_SIZE * ICON_SIZE) as usize;
123        if data.len() == expected {
124            self.icon_data.clear();
125            self.icon_data.extend_from_slice(data);
126        }
127    }
128
129    /// Total bounding box height (icon + label gap + label line).
130    pub fn total_height(&self) -> u32 {
131        // Icon (16) + gap (4) + label line (16)
132        ICON_SIZE + 4 + 16
133    }
134
135    /// Hit test: does the point lie within this icon's bounding box?
136    pub fn hit_test(&self, px: i32, py: i32) -> bool {
137        let w = ICON_SIZE as i32;
138        let h = self.total_height() as i32;
139        px >= self.x && px < self.x + w && py >= self.y && py < self.y + h
140    }
141}
142
143// ---------------------------------------------------------------------------
144// Icon grid
145// ---------------------------------------------------------------------------
146
147/// Grid-based layout manager for desktop icons.
148#[derive(Debug)]
149pub struct IconGrid {
150    /// All desktop icons.
151    pub icons: Vec<DesktopIcon>,
152    /// Horizontal spacing between grid cells (pixels).
153    pub grid_spacing_x: u32,
154    /// Vertical spacing between grid cells (pixels).
155    pub grid_spacing_y: u32,
156    /// Icon cell size (width = height = ICON_SIZE + padding).
157    pub cell_size: u32,
158    /// Desktop width for grid calculations.
159    desktop_width: u32,
160    /// Desktop height for grid calculations.
161    desktop_height: u32,
162}
163
164impl IconGrid {
165    /// Create a new icon grid for a desktop of the given dimensions.
166    pub fn new(desktop_width: u32, desktop_height: u32) -> Self {
167        Self {
168            icons: Vec::new(),
169            grid_spacing_x: 80,
170            grid_spacing_y: 80,
171            cell_size: 64,
172            desktop_width,
173            desktop_height,
174        }
175    }
176
177    /// Add an icon to the grid.
178    pub fn add_icon(&mut self, icon: DesktopIcon) {
179        self.icons.push(icon);
180    }
181
182    /// Remove an icon by index.
183    pub fn remove_icon(&mut self, index: usize) -> Option<DesktopIcon> {
184        if index < self.icons.len() {
185            Some(self.icons.remove(index))
186        } else {
187            None
188        }
189    }
190
191    /// Auto-arrange all icons in a grid layout (top-left to bottom-right).
192    pub fn arrange(&mut self) {
193        let margin_x: i32 = 20;
194        let margin_y: i32 = 40;
195        let cols = if self.grid_spacing_x == 0 {
196            1
197        } else {
198            ((self.desktop_width as i32 - margin_x * 2) / self.grid_spacing_x as i32).max(1)
199        };
200
201        for (i, icon) in self.icons.iter_mut().enumerate() {
202            let col = (i as i32) % cols;
203            let row = (i as i32) / cols;
204            icon.x = margin_x + col * self.grid_spacing_x as i32;
205            icon.y = margin_y + row * self.grid_spacing_y as i32;
206        }
207    }
208
209    /// Snap a position to the nearest grid cell.
210    pub fn snap_to_grid(&self, x: i32, y: i32) -> (i32, i32) {
211        let gx = self.grid_spacing_x as i32;
212        let gy = self.grid_spacing_y as i32;
213        if gx == 0 || gy == 0 {
214            return (x, y);
215        }
216        let sx = ((x + gx / 2) / gx) * gx;
217        let sy = ((y + gy / 2) / gy) * gy;
218        (sx.max(0), sy.max(0))
219    }
220
221    /// Render a single icon into a pixel buffer.
222    ///
223    /// `buf` is `buf_width x buf_height`, ARGB8888 row-major.
224    pub fn render_icon(icon: &DesktopIcon, buf: &mut [u32], buf_width: u32, buf_height: u32) {
225        let bw = buf_width as i32;
226        let bh = buf_height as i32;
227        let iw = ICON_SIZE as i32;
228
229        // Draw icon bitmap
230        for row in 0..ICON_SIZE as i32 {
231            let dy = icon.y + row;
232            if dy < 0 || dy >= bh {
233                continue;
234            }
235            for col in 0..iw {
236                let dx = icon.x + col;
237                if dx < 0 || dx >= bw {
238                    continue;
239                }
240                let src = icon.icon_data[(row * iw + col) as usize];
241                buf[(dy * bw + dx) as usize] = src;
242            }
243        }
244
245        // Draw selection highlight (1px border around icon)
246        if icon.selected {
247            let color = 0xFF44AAFF; // light blue
248            let x0 = icon.x - 1;
249            let y0 = icon.y - 1;
250            let x1 = icon.x + iw;
251            let y1 = icon.y + ICON_SIZE as i32;
252            for dx in x0..=x1 {
253                if dx >= 0 && dx < bw {
254                    if y0 >= 0 && y0 < bh {
255                        buf[(y0 * bw + dx) as usize] = color;
256                    }
257                    if y1 >= 0 && y1 < bh {
258                        buf[(y1 * bw + dx) as usize] = color;
259                    }
260                }
261            }
262            for dy in y0..=y1 {
263                if dy >= 0 && dy < bh {
264                    if x0 >= 0 && x0 < bw {
265                        buf[(dy * bw + x0) as usize] = color;
266                    }
267                    if x1 >= 0 && x1 < bw {
268                        buf[(dy * bw + x1) as usize] = color;
269                    }
270                }
271            }
272        }
273    }
274
275    /// Handle a click at `(px, py)`.
276    ///
277    /// Returns the index of the clicked icon (if any). Deselects all others.
278    pub fn handle_click(&mut self, px: i32, py: i32) -> Option<usize> {
279        let mut clicked = None;
280
281        for (i, icon) in self.icons.iter_mut().enumerate() {
282            if icon.hit_test(px, py) {
283                icon.selected = true;
284                clicked = Some(i);
285            } else {
286                icon.selected = false;
287            }
288        }
289
290        clicked
291    }
292
293    /// Handle a double-click at `(px, py)`.
294    ///
295    /// Returns the exec command of the double-clicked icon, if any.
296    pub fn handle_double_click(&mut self, px: i32, py: i32) -> Option<String> {
297        for icon in &self.icons {
298            if icon.hit_test(px, py) {
299                if let Some(ref df) = icon.desktop_file {
300                    if !df.exec_command.is_empty() {
301                        return Some(df.exec_command.clone());
302                    }
303                }
304            }
305        }
306        None
307    }
308
309    /// Handle drag: move the selected icon to `(px, py)` with grid snapping.
310    pub fn handle_drag(&mut self, px: i32, py: i32) {
311        let (sx, sy) = self.snap_to_grid(px, py);
312        for icon in &mut self.icons {
313            if icon.selected {
314                icon.x = sx;
315                icon.y = sy;
316                break;
317            }
318        }
319    }
320
321    /// Deselect all icons.
322    pub fn deselect_all(&mut self) {
323        for icon in &mut self.icons {
324            icon.selected = false;
325        }
326    }
327
328    /// Number of icons.
329    pub fn icon_count(&self) -> usize {
330        self.icons.len()
331    }
332}
333
334// ---------------------------------------------------------------------------
335// Tests
336// ---------------------------------------------------------------------------
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_desktop_file_parse() {
344        let content = concat!(
345            "[Desktop Entry]\n",
346            "Name=Terminal\n",
347            "Exec=vterm\n",
348            "Icon=terminal\n",
349            "Categories=System;Utility;\n",
350            "MimeType=text/plain;\n",
351        );
352        let df = DesktopFile::parse(content).unwrap();
353        assert_eq!(df.name, "Terminal");
354        assert_eq!(df.exec_command, "vterm");
355        assert_eq!(df.categories.len(), 2);
356        assert_eq!(df.mime_types.len(), 1);
357    }
358
359    #[test]
360    fn test_desktop_file_missing_name() {
361        let content = "Exec=foo\n";
362        assert!(DesktopFile::parse(content).is_none());
363    }
364
365    #[test]
366    fn test_icon_hit_test() {
367        let icon = DesktopIcon::new("Test", 100, 200);
368        assert!(icon.hit_test(108, 210));
369        assert!(!icon.hit_test(50, 50));
370    }
371
372    #[test]
373    fn test_icon_grid_arrange() {
374        let mut grid = IconGrid::new(800, 600);
375        for i in 0..5 {
376            grid.add_icon(DesktopIcon::new(&alloc::format!("Icon{}", i), 0, 0));
377        }
378        grid.arrange();
379        // First icon should be near top-left
380        assert!(grid.icons[0].x >= 20);
381        assert!(grid.icons[0].y >= 40);
382    }
383
384    #[test]
385    fn test_icon_grid_click() {
386        let mut grid = IconGrid::new(800, 600);
387        grid.add_icon(DesktopIcon::new("A", 100, 100));
388        grid.add_icon(DesktopIcon::new("B", 200, 100));
389        let clicked = grid.handle_click(108, 110);
390        assert_eq!(clicked, Some(0));
391        assert!(grid.icons[0].selected);
392        assert!(!grid.icons[1].selected);
393    }
394
395    #[test]
396    fn test_snap_to_grid() {
397        let grid = IconGrid::new(800, 600);
398        let (sx, sy) = grid.snap_to_grid(45, 95);
399        assert_eq!(sx, 80);
400        assert_eq!(sy, 80);
401    }
402
403    #[test]
404    fn test_icon_drag() {
405        let mut grid = IconGrid::new(800, 600);
406        grid.add_icon(DesktopIcon::new("A", 0, 0));
407        grid.icons[0].selected = true;
408        grid.handle_drag(90, 90);
409        // Should snap to grid
410        assert_eq!(grid.icons[0].x, 80);
411        assert_eq!(grid.icons[0].y, 80);
412    }
413
414    #[test]
415    fn test_remove_icon() {
416        let mut grid = IconGrid::new(800, 600);
417        grid.add_icon(DesktopIcon::new("A", 0, 0));
418        grid.add_icon(DesktopIcon::new("B", 80, 0));
419        let removed = grid.remove_icon(0);
420        assert!(removed.is_some());
421        assert_eq!(grid.icon_count(), 1);
422    }
423
424    #[test]
425    fn test_deselect_all() {
426        let mut grid = IconGrid::new(800, 600);
427        grid.add_icon(DesktopIcon::new("A", 0, 0));
428        grid.icons[0].selected = true;
429        grid.deselect_all();
430        assert!(!grid.icons[0].selected);
431    }
432
433    #[test]
434    fn test_render_icon_no_panic() {
435        let icon = DesktopIcon::new("Test", 0, 0);
436        let mut buf = vec![0u32; 64 * 64];
437        IconGrid::render_icon(&icon, &mut buf, 64, 64);
438        // Just verify it doesn't panic
439    }
440}