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

veridian_kernel/desktop/desktop_ext/
mod.rs

1//! Desktop Extension Modules
2//!
3//! Provides advanced desktop functionality for the VeridianOS desktop
4//! environment:
5//!
6//! 1. **Clipboard Protocol** -- Wayland wl_data_device compatible clipboard
7//!    with MIME type negotiation, primary selection, and history.
8//! 2. **Drag-and-Drop** -- wl_data_offer protocol with enter/leave/drop/motion
9//!    events.
10//! 3. **Global Keyboard Shortcuts** -- Configurable key bindings with modifier
11//!    masks.
12//! 4. **Theme Engine** -- Color schemes (light/dark/solarized/nord/dracula)
13//!    with runtime switching.
14//! 5. **Font Rendering** -- TrueType parser with integer Bezier rasterization
15//!    and glyph caching.
16//! 6. **CJK Unicode** -- Wide character detection, double-width cell rendering,
17//!    and IME framework.
18//!
19//! All math is integer-only (no floating point). Uses fixed-point 8.8 or 16.16
20//! where fractional precision is needed.
21
22#![allow(dead_code)]
23
24#[cfg(feature = "alloc")]
25extern crate alloc;
26
27pub mod cjk;
28pub mod clipboard;
29pub mod dnd;
30pub mod font_render;
31pub mod shortcuts;
32pub mod theme;
33
34// Re-export all public types and functions for API compatibility.
35
36// Clipboard
37// CJK
38pub use cjk::{char_width, is_cjk_wide, CellContent, ImeState};
39#[cfg(feature = "alloc")]
40pub use cjk::{string_width, truncate_to_width, ImeCandidate, InputMethodEditor};
41#[cfg(feature = "alloc")]
42pub use clipboard::ClipboardManager;
43pub use clipboard::{ClipboardEntry, ClipboardError, ClipboardMime, SelectionType};
44// Drag-and-Drop
45pub use dnd::{DndError, DndEvent, DndState};
46#[cfg(feature = "alloc")]
47pub use dnd::{DndManager, DragSource, DropTarget};
48#[cfg(feature = "alloc")]
49pub use font_render::{
50    apply_hinting, rasterize_outline, render_glyph, GlyphBitmap, GlyphCache, GlyphContour,
51    GlyphOutline,
52};
53// Font Rendering
54pub use font_render::{
55    FontError, HeadTable, HheaTable, HintingMode, MaxpTable, OutlinePoint, SubpixelMode,
56    TableEntry, TableTag, TtfParser,
57};
58#[cfg(feature = "alloc")]
59pub use shortcuts::ShortcutManager;
60// Shortcuts
61pub use shortcuts::{KeyBinding, KeyCode, ModifierMask, ShortcutAction, ShortcutPriority};
62// Theme
63pub use theme::{IconTheme, StyleProperty, ThemeColor, ThemeColors, ThemeManager, ThemePreset};
64
65// ============================================================================
66// Tests
67// ============================================================================
68
69#[cfg(test)]
70mod tests {
71    #[allow(unused_imports)]
72    use alloc::{string::String, vec, vec::Vec};
73
74    use super::{
75        clipboard::{CLIPBOARD_HISTORY_MAX, CLIPBOARD_MAX_DATA_SIZE},
76        font_render::{read_u16_be, read_u32_be, GLYPH_CACHE_SIZE},
77        *,
78    };
79
80    // --- Clipboard Tests ---
81
82    #[test]
83    fn test_clipboard_copy_paste() {
84        let mut mgr = ClipboardManager::new();
85        let data = vec![0x48, 0x65, 0x6C, 0x6C, 0x6F]; // "Hello"
86        mgr.copy(
87            SelectionType::Clipboard,
88            1,
89            ClipboardMime::TextPlain,
90            data.clone(),
91        )
92        .unwrap();
93        let result = mgr
94            .paste(SelectionType::Clipboard, ClipboardMime::TextPlain)
95            .unwrap();
96        assert_eq!(result, &data[..]);
97    }
98
99    #[test]
100    fn test_clipboard_paste_empty() {
101        let mgr = ClipboardManager::new();
102        assert_eq!(
103            mgr.paste(SelectionType::Clipboard, ClipboardMime::TextPlain),
104            Err(ClipboardError::Empty)
105        );
106    }
107
108    #[test]
109    fn test_clipboard_paste_wrong_mime() {
110        let mut mgr = ClipboardManager::new();
111        mgr.copy(
112            SelectionType::Clipboard,
113            1,
114            ClipboardMime::TextPlain,
115            vec![1, 2, 3],
116        )
117        .unwrap();
118        assert_eq!(
119            mgr.paste(SelectionType::Clipboard, ClipboardMime::ImagePng),
120            Err(ClipboardError::MimeNotFound)
121        );
122    }
123
124    #[test]
125    fn test_clipboard_primary_selection() {
126        let mut mgr = ClipboardManager::new();
127        mgr.copy(SelectionType::Primary, 1, ClipboardMime::TextPlain, vec![1])
128            .unwrap();
129        assert!(mgr.has_data(SelectionType::Primary));
130        assert!(!mgr.has_data(SelectionType::Clipboard));
131    }
132
133    #[test]
134    fn test_clipboard_history() {
135        let mut mgr = ClipboardManager::new();
136        for i in 0..10u8 {
137            mgr.copy(
138                SelectionType::Clipboard,
139                1,
140                ClipboardMime::TextPlain,
141                vec![i],
142            )
143            .unwrap();
144        }
145        // History should have at most CLIPBOARD_HISTORY_MAX entries.
146        assert!(mgr.history().len() <= CLIPBOARD_HISTORY_MAX);
147    }
148
149    #[test]
150    fn test_clipboard_data_too_large() {
151        let mut mgr = ClipboardManager::new();
152        let big = vec![0u8; CLIPBOARD_MAX_DATA_SIZE + 1];
153        assert_eq!(
154            mgr.copy(SelectionType::Clipboard, 1, ClipboardMime::TextPlain, big),
155            Err(ClipboardError::DataTooLarge)
156        );
157    }
158
159    #[test]
160    fn test_clipboard_negotiate_mime() {
161        let mut mgr = ClipboardManager::new();
162        mgr.copy(
163            SelectionType::Clipboard,
164            1,
165            ClipboardMime::TextHtml,
166            vec![1],
167        )
168        .unwrap();
169        let result = mgr.negotiate_mime(
170            SelectionType::Clipboard,
171            &[ClipboardMime::TextPlain, ClipboardMime::TextHtml],
172        );
173        assert_eq!(result, Some(ClipboardMime::TextHtml));
174    }
175
176    #[test]
177    fn test_clipboard_clear() {
178        let mut mgr = ClipboardManager::new();
179        mgr.copy(
180            SelectionType::Clipboard,
181            1,
182            ClipboardMime::TextPlain,
183            vec![1],
184        )
185        .unwrap();
186        mgr.clear(SelectionType::Clipboard);
187        assert!(!mgr.has_data(SelectionType::Clipboard));
188    }
189
190    #[test]
191    fn test_clipboard_restore_history() {
192        let mut mgr = ClipboardManager::new();
193        mgr.copy(
194            SelectionType::Clipboard,
195            1,
196            ClipboardMime::TextPlain,
197            vec![1],
198        )
199        .unwrap();
200        mgr.copy(
201            SelectionType::Clipboard,
202            1,
203            ClipboardMime::TextPlain,
204            vec![2],
205        )
206        .unwrap();
207        // History has the first entry (vec![1]).
208        mgr.restore_from_history(0).unwrap();
209        let result = mgr
210            .paste(SelectionType::Clipboard, ClipboardMime::TextPlain)
211            .unwrap();
212        assert_eq!(result, &[1]);
213    }
214
215    // --- Drag-and-Drop Tests ---
216
217    #[test]
218    fn test_dnd_start_drag() {
219        let mut dnd = DndManager::new();
220        dnd.start_drag(1, vec![ClipboardMime::TextPlain], 10, 20, 32, 32)
221            .unwrap();
222        assert_eq!(dnd.state(), DndState::Dragging);
223    }
224
225    #[test]
226    fn test_dnd_double_drag_error() {
227        let mut dnd = DndManager::new();
228        dnd.start_drag(1, vec![ClipboardMime::TextPlain], 0, 0, 32, 32)
229            .unwrap();
230        assert_eq!(
231            dnd.start_drag(2, vec![], 0, 0, 32, 32),
232            Err(DndError::AlreadyDragging)
233        );
234    }
235
236    #[test]
237    fn test_dnd_motion_no_drag() {
238        let mut dnd = DndManager::new();
239        assert_eq!(dnd.motion(10, 10), Err(DndError::NotDragging));
240    }
241
242    #[test]
243    fn test_dnd_enter_leave_events() {
244        let mut dnd = DndManager::new();
245        dnd.register_target(DropTarget {
246            surface_id: 42,
247            accepted_mimes: vec![ClipboardMime::TextPlain],
248            x: 100,
249            y: 100,
250            width: 200,
251            height: 200,
252        });
253        dnd.start_drag(1, vec![ClipboardMime::TextPlain], 0, 0, 32, 32)
254            .unwrap();
255        dnd.drain_events(); // Clear start events.
256
257        // Move into target.
258        dnd.motion(150, 150).unwrap();
259        let events = dnd.drain_events();
260        assert!(events
261            .iter()
262            .any(|e| matches!(e, DndEvent::Enter { surface_id: 42, .. })));
263
264        // Move out of target.
265        dnd.motion(0, 0).unwrap();
266        let events = dnd.drain_events();
267        assert!(events
268            .iter()
269            .any(|e| matches!(e, DndEvent::Leave { surface_id: 42 })));
270    }
271
272    #[test]
273    fn test_dnd_drop_action() {
274        let mut dnd = DndManager::new();
275        dnd.register_target(DropTarget {
276            surface_id: 5,
277            accepted_mimes: vec![ClipboardMime::TextPlain],
278            x: 0,
279            y: 0,
280            width: 100,
281            height: 100,
282        });
283        dnd.start_drag(1, vec![ClipboardMime::TextPlain], 50, 50, 16, 16)
284            .unwrap();
285        dnd.motion(50, 50).unwrap();
286        let result = dnd.drop_action();
287        assert!(result.is_ok());
288    }
289
290    #[test]
291    fn test_dnd_cancel() {
292        let mut dnd = DndManager::new();
293        dnd.start_drag(1, vec![], 0, 0, 10, 10).unwrap();
294        dnd.cancel();
295        assert_eq!(dnd.state(), DndState::Idle);
296    }
297
298    #[test]
299    fn test_drop_target_contains() {
300        let t = DropTarget {
301            surface_id: 1,
302            accepted_mimes: vec![],
303            x: 10,
304            y: 20,
305            width: 100,
306            height: 50,
307        };
308        assert!(t.contains(10, 20));
309        assert!(t.contains(109, 69));
310        assert!(!t.contains(110, 20));
311        assert!(!t.contains(5, 20));
312    }
313
314    // --- Shortcut Tests ---
315
316    #[test]
317    fn test_shortcut_manager_defaults() {
318        let mgr = ShortcutManager::new();
319        // Should have default bindings registered.
320        assert!(mgr.binding_count() > 0);
321    }
322
323    #[test]
324    fn test_shortcut_process_key() {
325        let mgr = ShortcutManager::new();
326        // Alt+Tab should match SwitchNextWindow.
327        let result = mgr.process_key(ModifierMask::ALT, 0x0F);
328        assert_eq!(result, Some(ShortcutAction::SwitchNextWindow));
329    }
330
331    #[test]
332    fn test_shortcut_no_match() {
333        let mgr = ShortcutManager::new();
334        let result = mgr.process_key(ModifierMask::NONE, 0x99);
335        assert_eq!(result, None);
336    }
337
338    #[test]
339    fn test_shortcut_register_unregister() {
340        let mut mgr = ShortcutManager::new();
341        let count = mgr.binding_count();
342        let id = mgr.register(KeyBinding::new(
343            ModifierMask::CTRL,
344            0x1E,
345            ShortcutAction::Custom(42),
346        ));
347        assert_eq!(mgr.binding_count(), count + 1);
348        mgr.unregister(id);
349        assert_eq!(mgr.binding_count(), count);
350    }
351
352    #[test]
353    fn test_shortcut_disabled() {
354        let mut mgr = ShortcutManager::new();
355        mgr.set_enabled(false);
356        let result = mgr.process_key(ModifierMask::ALT, 0x0F);
357        assert_eq!(result, None);
358    }
359
360    #[test]
361    fn test_modifier_mask_combine() {
362        let m = ModifierMask::CTRL.combine(ModifierMask::ALT);
363        assert!(m.has(ModifierMask::CTRL));
364        assert!(m.has(ModifierMask::ALT));
365        assert!(!m.has(ModifierMask::SHIFT));
366    }
367
368    // --- Theme Tests ---
369
370    #[test]
371    fn test_theme_default_dark() {
372        let mgr = ThemeManager::new();
373        assert_eq!(mgr.current_preset(), ThemePreset::Dark);
374    }
375
376    #[test]
377    fn test_theme_switch() {
378        let mut mgr = ThemeManager::new();
379        mgr.set_theme(ThemePreset::Nord);
380        assert_eq!(mgr.current_preset(), ThemePreset::Nord);
381        // Verify a characteristic Nord color.
382        assert_eq!(mgr.colors().accent, ThemeColor::from_rgb(0x88, 0xC0, 0xD0));
383    }
384
385    #[test]
386    fn test_theme_all_presets_load() {
387        let mut mgr = ThemeManager::new();
388        let presets = [
389            ThemePreset::Light,
390            ThemePreset::Dark,
391            ThemePreset::SolarizedDark,
392            ThemePreset::SolarizedLight,
393            ThemePreset::Nord,
394            ThemePreset::Dracula,
395        ];
396        for preset in &presets {
397            mgr.set_theme(*preset);
398            assert_eq!(mgr.current_preset(), *preset);
399        }
400    }
401
402    #[test]
403    fn test_theme_color_components() {
404        let c = ThemeColor::from_argb(0x80, 0xFF, 0x00, 0xAA);
405        assert_eq!(c.alpha(), 0x80);
406        assert_eq!(c.red(), 0xFF);
407        assert_eq!(c.green(), 0x00);
408        assert_eq!(c.blue(), 0xAA);
409    }
410
411    #[test]
412    fn test_theme_color_darken() {
413        let c = ThemeColor::from_rgb(100, 200, 50);
414        let d = c.darken(50);
415        assert_eq!(d.red(), 50);
416        assert_eq!(d.green(), 100);
417        assert_eq!(d.blue(), 25);
418    }
419
420    #[test]
421    fn test_theme_style_property() {
422        let mgr = ThemeManager::new();
423        assert_eq!(mgr.map_style_property(StyleProperty::FontSize), 14);
424        assert_eq!(mgr.map_style_property(StyleProperty::BorderWidth), 1);
425    }
426
427    #[test]
428    fn test_theme_gtk_name() {
429        let mut mgr = ThemeManager::new();
430        mgr.set_theme(ThemePreset::Dracula);
431        assert_eq!(mgr.gtk_theme_name(), "Dracula");
432    }
433
434    // --- Font Rendering Tests ---
435
436    #[test]
437    fn test_ttf_parser_invalid_data() {
438        let result = TtfParser::new(&[0, 1, 2, 3]);
439        assert!(result.is_err());
440    }
441
442    #[test]
443    fn test_ttf_parser_empty() {
444        let result = TtfParser::new(&[]);
445        assert!(matches!(result, Err(FontError::InvalidFont)));
446    }
447
448    #[test]
449    fn test_read_u16_be() {
450        assert_eq!(read_u16_be(&[0x01, 0x02], 0), Some(0x0102));
451        assert_eq!(read_u16_be(&[0xFF, 0x00], 0), Some(0xFF00));
452        assert_eq!(read_u16_be(&[0x01], 0), None);
453    }
454
455    #[test]
456    fn test_read_u32_be() {
457        assert_eq!(read_u32_be(&[0x00, 0x01, 0x00, 0x00], 0), Some(0x00010000));
458        assert_eq!(read_u32_be(&[0x01, 0x02], 0), None);
459    }
460
461    #[test]
462    fn test_glyph_cache_insert_lookup() {
463        let mut cache = GlyphCache::new();
464        let bmp = GlyphBitmap {
465            data: vec![128; 16],
466            width: 4,
467            height: 4,
468            bearing_x: 0,
469            bearing_y: 4,
470            advance: 5,
471        };
472        cache.insert(65, 16, bmp.clone());
473        assert_eq!(cache.len(), 1);
474        let result = cache.get(65, 16);
475        assert!(result.is_some());
476        assert_eq!(result.unwrap().width, 4);
477    }
478
479    #[test]
480    fn test_glyph_cache_miss() {
481        let mut cache = GlyphCache::new();
482        assert!(cache.get(65, 16).is_none());
483    }
484
485    #[test]
486    fn test_glyph_cache_hit_rate() {
487        let mut cache = GlyphCache::new();
488        cache.insert(
489            65,
490            16,
491            GlyphBitmap {
492                data: vec![0; 4],
493                width: 2,
494                height: 2,
495                bearing_x: 0,
496                bearing_y: 2,
497                advance: 3,
498            },
499        );
500        cache.get(65, 16); // hit
501        cache.get(66, 16); // miss
502        assert_eq!(cache.hit_rate_percent(), 50);
503    }
504
505    #[test]
506    fn test_glyph_cache_eviction() {
507        let mut cache = GlyphCache::new();
508        for i in 0..GLYPH_CACHE_SIZE + 10 {
509            cache.insert(
510                i as u32,
511                12,
512                GlyphBitmap {
513                    data: vec![0; 1],
514                    width: 1,
515                    height: 1,
516                    bearing_x: 0,
517                    bearing_y: 1,
518                    advance: 1,
519                },
520            );
521        }
522        assert!(cache.len() <= GLYPH_CACHE_SIZE);
523    }
524
525    #[test]
526    fn test_rasterize_empty_outline() {
527        let outline = GlyphOutline {
528            contours: Vec::new(),
529            x_min: 0,
530            y_min: 0,
531            x_max: 0,
532            y_max: 0,
533            advance_width: 0,
534            lsb: 0,
535        };
536        let bmp = rasterize_outline(&outline, 16, 2048);
537        assert_eq!(bmp.width, 0);
538        assert_eq!(bmp.height, 0);
539    }
540
541    #[test]
542    fn test_table_tag_constants() {
543        assert_eq!(TableTag::CMAP.0, *b"cmap");
544        assert_eq!(TableTag::HEAD.0, *b"head");
545        assert_eq!(TableTag::GLYF.0, *b"glyf");
546    }
547
548    // --- CJK / Unicode Tests ---
549
550    #[test]
551    fn test_is_cjk_wide_basic() {
552        assert!(is_cjk_wide('\u{4E00}')); // CJK Unified start
553        assert!(is_cjk_wide('\u{9FFF}')); // CJK Unified end
554        assert!(is_cjk_wide('\u{AC00}')); // Hangul start
555        assert!(is_cjk_wide('\u{3042}')); // Hiragana 'a'
556        assert!(is_cjk_wide('\u{30A2}')); // Katakana 'a'
557        assert!(is_cjk_wide('\u{FF01}')); // Fullwidth '!'
558    }
559
560    #[test]
561    fn test_is_cjk_wide_false() {
562        assert!(!is_cjk_wide('A'));
563        assert!(!is_cjk_wide('z'));
564        assert!(!is_cjk_wide(' '));
565        assert!(!is_cjk_wide('\u{00E9}')); // e-acute
566    }
567
568    #[test]
569    fn test_char_width() {
570        assert_eq!(char_width('A'), 1);
571        assert_eq!(char_width('\u{4E00}'), 2);
572        assert_eq!(char_width('\0'), 0);
573        assert_eq!(char_width('\u{0300}'), 0); // Combining
574        assert_eq!(char_width('\u{200B}'), 0); // Zero-width space
575    }
576
577    #[test]
578    fn test_string_width() {
579        assert_eq!(string_width("Hello"), 5);
580        assert_eq!(string_width("\u{4F60}\u{597D}"), 4); // Two CJK chars
581        assert_eq!(string_width("A\u{4E00}B"), 4); // Mixed
582    }
583
584    #[test]
585    fn test_truncate_to_width() {
586        let s = "Hello, World!";
587        let truncated = truncate_to_width(s, 10);
588        assert!(string_width(&truncated) <= 10);
589    }
590
591    #[test]
592    fn test_cell_content_default() {
593        assert_eq!(CellContent::default(), CellContent::Empty);
594    }
595
596    // --- IME Tests ---
597
598    #[test]
599    fn test_ime_disabled_passthrough() {
600        let mut ime = InputMethodEditor::new();
601        // IME is disabled by default.
602        ime.feed_char('a');
603        assert_eq!(ime.state(), ImeState::Committed);
604        assert_eq!(ime.take_committed(), "a");
605    }
606
607    #[test]
608    fn test_ime_composing() {
609        let mut ime = InputMethodEditor::new();
610        ime.set_enabled(true);
611        ime.feed_char('n');
612        ime.feed_char('i');
613        assert_eq!(ime.state(), ImeState::Composing);
614        assert_eq!(ime.preedit(), "ni");
615        assert!(!ime.candidates().is_empty());
616    }
617
618    #[test]
619    fn test_ime_select_candidate() {
620        let mut ime = InputMethodEditor::new();
621        ime.set_enabled(true);
622        ime.feed_char('n');
623        ime.feed_char('i');
624        ime.feed_char('1'); // Select first candidate.
625        assert_eq!(ime.state(), ImeState::Committed);
626        let committed = ime.take_committed();
627        assert_eq!(committed, "\u{4F60}"); // ni -> U+4F60
628    }
629
630    #[test]
631    fn test_ime_backspace() {
632        let mut ime = InputMethodEditor::new();
633        ime.set_enabled(true);
634        ime.feed_char('h');
635        ime.feed_char('a');
636        ime.feed_backspace();
637        assert_eq!(ime.preedit(), "h");
638        ime.feed_backspace();
639        assert_eq!(ime.state(), ImeState::Inactive);
640    }
641
642    #[test]
643    fn test_ime_escape_cancels() {
644        let mut ime = InputMethodEditor::new();
645        ime.set_enabled(true);
646        ime.feed_char('s');
647        ime.feed_char('h');
648        ime.feed_char('i');
649        ime.feed_escape();
650        assert_eq!(ime.state(), ImeState::Inactive);
651        assert!(ime.preedit().is_empty());
652    }
653
654    #[test]
655    fn test_ime_space_commits_first() {
656        let mut ime = InputMethodEditor::new();
657        ime.set_enabled(true);
658        ime.feed_char('w');
659        ime.feed_char('o');
660        ime.feed_char(' '); // Commit first candidate.
661        assert_eq!(ime.state(), ImeState::Committed);
662        let committed = ime.take_committed();
663        assert_eq!(committed, "\u{6211}"); // wo -> U+6211
664    }
665}