1#![allow(dead_code)]
10
11use alloc::{string::String, vec, vec::Vec};
12
13use crate::sync::once_lock::GlobalState;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AppCategory {
22 System,
24 Utility,
26 Development,
28 Graphics,
30 Network,
32 Multimedia,
34 Office,
36 Settings,
38 Other,
40}
41
42#[derive(Debug, Clone)]
44pub struct AppEntry {
45 pub name: String,
47 pub exec_path: String,
49 pub icon_name: String,
51 pub category: AppCategory,
53 pub description: String,
55}
56
57impl AppEntry {
58 pub fn new(
60 name: &str,
61 exec_path: &str,
62 icon_name: &str,
63 category: AppCategory,
64 description: &str,
65 ) -> Self {
66 Self {
67 name: String::from(name),
68 exec_path: String::from(exec_path),
69 icon_name: String::from(icon_name),
70 category,
71 description: String::from(description),
72 }
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum LauncherState {
79 Hidden,
81 Visible,
83 SearchActive,
85}
86
87#[derive(Debug, Clone)]
89pub enum LauncherAction {
90 Launch(String),
92 Hide,
94}
95
96pub struct AppLauncher {
102 entries: Vec<AppEntry>,
104 filtered: Vec<usize>,
106 search_query: String,
108 selected_index: usize,
110 state: LauncherState,
112 grid_columns: usize,
114 grid_rows: usize,
116 scroll_offset: usize,
118 overlay_x: usize,
120 overlay_y: usize,
122 overlay_width: usize,
124 overlay_height: usize,
126}
127
128impl AppLauncher {
129 pub fn new() -> Self {
132 let entries = default_applications();
133 let filtered: Vec<usize> = (0..entries.len()).collect();
134
135 Self {
136 entries,
137 filtered,
138 search_query: String::new(),
139 selected_index: 0,
140 state: LauncherState::Hidden,
141 grid_columns: 4,
142 grid_rows: 3,
143 scroll_offset: 0,
144 overlay_x: 0,
145 overlay_y: 0,
146 overlay_width: 640,
147 overlay_height: 480,
148 }
149 }
150
151 pub fn show(&mut self) {
153 self.state = LauncherState::Visible;
154 self.search_query.clear();
155 self.selected_index = 0;
156 self.scroll_offset = 0;
157 self.filtered = (0..self.entries.len()).collect();
159 }
160
161 pub fn hide(&mut self) {
163 self.state = LauncherState::Hidden;
164 }
165
166 pub fn toggle(&mut self) {
168 match self.state {
169 LauncherState::Hidden => self.show(),
170 LauncherState::Visible | LauncherState::SearchActive => self.hide(),
171 }
172 }
173
174 pub fn is_visible(&self) -> bool {
176 self.state != LauncherState::Hidden
177 }
178
179 pub fn set_overlay_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
182 self.overlay_x = x;
183 self.overlay_y = y;
184 self.overlay_width = width;
185 self.overlay_height = height;
186 }
187
188 pub fn register_app(&mut self, entry: AppEntry) {
190 self.entries.push(entry);
191 self.filter_entries();
193 }
194
195 pub fn unregister_app(&mut self, exec_path: &str) {
197 self.entries.retain(|e| e.exec_path.as_str() != exec_path);
198 self.filter_entries();
199 }
200
201 pub fn handle_key(&mut self, key: u8) -> Option<LauncherAction> {
216 if self.state == LauncherState::Hidden {
217 return None;
218 }
219
220 match key {
221 0x0A | 0x0D => {
223 if let Some(entry) = self.selected_entry() {
224 let path = entry.exec_path.clone();
225 self.hide();
226 return Some(LauncherAction::Launch(path));
227 }
228 None
229 }
230 0x1B => {
232 self.hide();
233 Some(LauncherAction::Hide)
234 }
235 0x08 => {
237 if !self.search_query.is_empty() {
238 self.search_query.pop();
239 self.filter_entries();
240 self.clamp_selection();
241 }
242 if self.search_query.is_empty() {
244 self.state = LauncherState::Visible;
245 }
246 None
247 }
248 0x80 => {
250 if self.selected_index >= self.grid_columns {
251 self.selected_index -= self.grid_columns;
252 self.ensure_visible();
253 }
254 None
255 }
256 0x81 => {
258 let new_idx = self.selected_index + self.grid_columns;
259 if new_idx < self.filtered.len() {
260 self.selected_index = new_idx;
261 self.ensure_visible();
262 }
263 None
264 }
265 0x82 => {
267 if self.selected_index > 0 {
268 self.selected_index -= 1;
269 self.ensure_visible();
270 }
271 None
272 }
273 0x83 => {
275 if self.selected_index + 1 < self.filtered.len() {
276 self.selected_index += 1;
277 self.ensure_visible();
278 }
279 None
280 }
281 0x20..=0x7E => {
283 self.state = LauncherState::SearchActive;
284 self.search_query.push(key as char);
285 self.filter_entries();
286 self.clamp_selection();
287 None
288 }
289 _ => None,
290 }
291 }
292
293 pub fn handle_click(&mut self, x: usize, y: usize) -> Option<LauncherAction> {
299 if self.state == LauncherState::Hidden {
300 return None;
301 }
302
303 let local_x = if x >= self.overlay_x {
305 x - self.overlay_x
306 } else {
307 return None;
308 };
309 let local_y = if y >= self.overlay_y {
310 y - self.overlay_y
311 } else {
312 return None;
313 };
314
315 if local_x >= self.overlay_width || local_y >= self.overlay_height {
317 return None;
318 }
319
320 let search_bar_height = SEARCH_BAR_HEIGHT;
322 let grid_top = search_bar_height + GRID_PADDING_TOP;
323 let cell_w = self.cell_width();
324 let cell_h = self.cell_height();
325
326 if local_y < grid_top {
327 self.state = LauncherState::SearchActive;
329 return None;
330 }
331
332 let grid_y = local_y - grid_top;
333 let grid_x = if local_x >= GRID_PADDING_LEFT {
334 local_x - GRID_PADDING_LEFT
335 } else {
336 return None;
337 };
338
339 let col = grid_x / cell_w;
341 let row = grid_y / cell_h;
342
343 if col >= self.grid_columns {
344 return None;
345 }
346
347 let entry_idx = self.scroll_offset + row * self.grid_columns + col;
348 if entry_idx < self.filtered.len() {
349 self.selected_index = entry_idx;
350 let real_idx = self.filtered[entry_idx];
351 if real_idx < self.entries.len() {
352 let path = self.entries[real_idx].exec_path.clone();
353 self.hide();
354 return Some(LauncherAction::Launch(path));
355 }
356 }
357
358 None
359 }
360
361 pub fn filter_entries(&mut self) {
366 self.filtered.clear();
367
368 if self.search_query.is_empty() {
369 for i in 0..self.entries.len() {
370 self.filtered.push(i);
371 }
372 return;
373 }
374
375 let query_lower: Vec<u8> = self.search_query.bytes().map(ascii_to_lower).collect();
378
379 for (i, entry) in self.entries.iter().enumerate() {
380 if ascii_contains_lower(entry.name.as_bytes(), &query_lower) {
381 self.filtered.push(i);
382 }
383 }
384 }
385
386 pub fn selected_entry(&self) -> Option<&AppEntry> {
388 let idx = self.filtered.get(self.selected_index)?;
389 self.entries.get(*idx)
390 }
391
392 pub fn visible_entries(&self) -> &[usize] {
394 &self.filtered
395 }
396
397 pub fn filtered_count(&self) -> usize {
399 self.filtered.len()
400 }
401
402 pub fn entries(&self) -> &[AppEntry] {
404 &self.entries
405 }
406
407 pub fn render_to_buffer(&self, buffer: &mut [u32], buf_width: usize, buf_height: usize) {
412 if self.state == LauncherState::Hidden {
413 return;
414 }
415
416 let ov_x = self.overlay_x;
417 let ov_y = self.overlay_y;
418 let ov_w = self.overlay_width;
419 let ov_h = self.overlay_height;
420
421 let bg_color: u32 = 0xCC222222; for row in ov_y..(ov_y + ov_h).min(buf_height) {
424 for col in ov_x..(ov_x + ov_w).min(buf_width) {
425 let idx = row * buf_width + col;
426 if idx < buffer.len() {
427 buffer[idx] = alpha_blend(buffer[idx], bg_color);
428 }
429 }
430 }
431
432 let border_color: u32 = 0xFF555555;
434 if ov_y < buf_height {
436 for col in ov_x..(ov_x + ov_w).min(buf_width) {
437 let idx = ov_y * buf_width + col;
438 if idx < buffer.len() {
439 buffer[idx] = border_color;
440 }
441 }
442 }
443 let bottom_y = ov_y + ov_h - 1;
445 if bottom_y < buf_height {
446 for col in ov_x..(ov_x + ov_w).min(buf_width) {
447 let idx = bottom_y * buf_width + col;
448 if idx < buffer.len() {
449 buffer[idx] = border_color;
450 }
451 }
452 }
453 for row in ov_y..(ov_y + ov_h).min(buf_height) {
455 let idx = row * buf_width + ov_x;
456 if idx < buffer.len() && ov_x < buf_width {
457 buffer[idx] = border_color;
458 }
459 }
460 let right_x = ov_x + ov_w - 1;
462 if right_x < buf_width {
463 for row in ov_y..(ov_y + ov_h).min(buf_height) {
464 let idx = row * buf_width + right_x;
465 if idx < buffer.len() {
466 buffer[idx] = border_color;
467 }
468 }
469 }
470
471 let search_y = ov_y + SEARCH_BAR_MARGIN_TOP;
473 let search_x = ov_x + SEARCH_BAR_MARGIN_LEFT;
474 let search_w = ov_w - SEARCH_BAR_MARGIN_LEFT * 2;
475 let search_h = SEARCH_BAR_INNER_HEIGHT;
476
477 let search_bg = if self.state == LauncherState::SearchActive {
479 0xFF3A3A3A
480 } else {
481 0xFF333333
482 };
483 for row in search_y..(search_y + search_h).min(buf_height) {
484 for col in search_x..(search_x + search_w).min(buf_width) {
485 let idx = row * buf_width + col;
486 if idx < buffer.len() {
487 buffer[idx] = search_bg;
488 }
489 }
490 }
491
492 let search_border = if self.state == LauncherState::SearchActive {
494 0xFF6688AA
495 } else {
496 0xFF555555
497 };
498 draw_rect_outline(
499 buffer,
500 buf_width,
501 buf_height,
502 search_x,
503 search_y,
504 search_w,
505 search_h,
506 search_border,
507 );
508
509 let text_y = search_y + (search_h.saturating_sub(FONT_HEIGHT)) / 2;
511 let text_x = search_x + 8;
512 if self.search_query.is_empty() {
513 draw_text_u32(
515 buffer,
516 buf_width,
517 buf_height,
518 b"Search applications...",
519 text_x,
520 text_y,
521 0xFF777777,
522 );
523 } else {
524 draw_text_u32(
525 buffer,
526 buf_width,
527 buf_height,
528 self.search_query.as_bytes(),
529 text_x,
530 text_y,
531 0xFFDDDDDD,
532 );
533 let cursor_x = text_x + self.search_query.len() * FONT_WIDTH;
535 for row in text_y..(text_y + FONT_HEIGHT).min(buf_height) {
536 let idx = row * buf_width + cursor_x;
537 if idx < buffer.len() && cursor_x < buf_width {
538 buffer[idx] = 0xFFCCCCCC;
539 }
540 }
541 }
542
543 let grid_top = ov_y + SEARCH_BAR_HEIGHT + GRID_PADDING_TOP;
545 let grid_left = ov_x + GRID_PADDING_LEFT;
546 let cell_w = self.cell_width();
547 let cell_h = self.cell_height();
548
549 let visible_count = self.grid_columns * self.grid_rows;
550 let start = self.scroll_offset;
551 let end = (start + visible_count).min(self.filtered.len());
552
553 for display_idx in start..end {
554 let local_idx = display_idx - start;
555 let col = local_idx % self.grid_columns;
556 let row = local_idx / self.grid_columns;
557
558 let cell_x = grid_left + col * cell_w;
559 let cell_y = grid_top + row * cell_h;
560
561 let real_idx = self.filtered[display_idx];
562 if real_idx >= self.entries.len() {
563 continue;
564 }
565 let entry = &self.entries[real_idx];
566
567 let is_selected = display_idx == self.selected_index;
569 if is_selected {
570 let hl_color: u32 = 0xFF445566;
571 for ry in cell_y..(cell_y + cell_h).min(buf_height) {
572 for rx in cell_x..(cell_x + cell_w).min(buf_width) {
573 let idx = ry * buf_width + rx;
574 if idx < buffer.len() {
575 buffer[idx] = hl_color;
576 }
577 }
578 }
579 }
580
581 let icon_size = ICON_SIZE;
583 let icon_x = cell_x + (cell_w.saturating_sub(icon_size)) / 2;
584 let icon_y = cell_y + ICON_MARGIN_TOP;
585 let icon_color = category_color(&entry.category);
586
587 for ry in icon_y..(icon_y + icon_size).min(buf_height) {
588 for rx in icon_x..(icon_x + icon_size).min(buf_width) {
589 let idx = ry * buf_width + rx;
590 if idx < buffer.len() {
591 buffer[idx] = icon_color;
592 }
593 }
594 }
595
596 if !entry.name.is_empty() {
598 let first_char = entry.name.as_bytes()[0];
599 let char_x = icon_x + (icon_size.saturating_sub(FONT_WIDTH)) / 2;
600 let char_y = icon_y + (icon_size.saturating_sub(FONT_HEIGHT)) / 2;
601 draw_char_u32(
602 buffer, buf_width, buf_height, first_char, char_x, char_y, 0xFFFFFFFF,
603 );
604 }
605
606 let name_bytes = entry.name.as_bytes();
608 let max_name_chars = cell_w / FONT_WIDTH;
609 let name_len = name_bytes.len().min(max_name_chars);
610 let name_pixel_w = name_len * FONT_WIDTH;
611 let name_x = cell_x + (cell_w.saturating_sub(name_pixel_w)) / 2;
612 let name_y = icon_y + icon_size + NAME_MARGIN_TOP;
613 let name_color = if is_selected { 0xFFFFFFFF } else { 0xFFCCCCCC };
614 draw_text_u32(
615 buffer,
616 buf_width,
617 buf_height,
618 &name_bytes[..name_len],
619 name_x,
620 name_y,
621 name_color,
622 );
623
624 if !entry.description.is_empty() {
626 let desc_bytes = entry.description.as_bytes();
627 let max_desc_chars = cell_w / FONT_WIDTH;
628 let desc_len = desc_bytes.len().min(max_desc_chars);
629 let desc_pixel_w = desc_len * FONT_WIDTH;
630 let desc_x = cell_x + (cell_w.saturating_sub(desc_pixel_w)) / 2;
631 let desc_y = name_y + FONT_HEIGHT + 2;
632 draw_text_u32(
633 buffer,
634 buf_width,
635 buf_height,
636 &desc_bytes[..desc_len],
637 desc_x,
638 desc_y,
639 0xFF888888,
640 );
641 }
642 }
643
644 let total_pages = (self.filtered.len() + visible_count - 1) / visible_count.max(1);
646 if total_pages > 1 {
647 let current_page = self.scroll_offset / visible_count.max(1);
648 let dots_y = ov_y + ov_h - 16;
650 let dots_total_w = total_pages * 12;
651 let dots_x = ov_x + (ov_w.saturating_sub(dots_total_w)) / 2;
652
653 for page in 0..total_pages {
654 let dot_x = dots_x + page * 12 + 2;
655 let dot_color = if page == current_page {
656 0xFFDDDDDD
657 } else {
658 0xFF666666
659 };
660 for ry in dots_y..(dots_y + 6).min(buf_height) {
662 for rx in dot_x..(dot_x + 6).min(buf_width) {
663 let idx = ry * buf_width + rx;
664 if idx < buffer.len() {
665 buffer[idx] = dot_color;
666 }
667 }
668 }
669 }
670 }
671
672 let count_text = format_count(self.filtered.len(), self.entries.len());
674 let count_y = ov_y + ov_h - 16;
675 let count_x = ov_x + 8;
676 draw_text_u32(
677 buffer,
678 buf_width,
679 buf_height,
680 count_text.as_bytes(),
681 count_x,
682 count_y,
683 0xFF666666,
684 );
685 }
686
687 fn cell_width(&self) -> usize {
693 let usable = self.overlay_width - GRID_PADDING_LEFT * 2;
694 usable / self.grid_columns.max(1)
695 }
696
697 fn cell_height(&self) -> usize {
699 let grid_area_h =
700 self.overlay_height - SEARCH_BAR_HEIGHT - GRID_PADDING_TOP - GRID_PADDING_BOTTOM;
701 grid_area_h / self.grid_rows.max(1)
702 }
703
704 fn clamp_selection(&mut self) {
706 if self.filtered.is_empty() {
707 self.selected_index = 0;
708 } else if self.selected_index >= self.filtered.len() {
709 self.selected_index = self.filtered.len() - 1;
710 }
711 }
712
713 fn ensure_visible(&mut self) {
716 let page_size = self.grid_columns * self.grid_rows;
717 if page_size == 0 {
718 return;
719 }
720
721 while self.selected_index >= self.scroll_offset + page_size {
723 self.scroll_offset += self.grid_columns;
724 }
725 while self.selected_index < self.scroll_offset && self.scroll_offset > 0 {
727 self.scroll_offset = self.scroll_offset.saturating_sub(self.grid_columns);
728 }
729 }
730}
731
732impl Default for AppLauncher {
733 fn default() -> Self {
734 Self::new()
735 }
736}
737
738const FONT_WIDTH: usize = 8;
744const FONT_HEIGHT: usize = 16;
746
747const SEARCH_BAR_HEIGHT: usize = 48;
749const SEARCH_BAR_MARGIN_TOP: usize = 12;
751const SEARCH_BAR_MARGIN_LEFT: usize = 16;
753const SEARCH_BAR_INNER_HEIGHT: usize = 28;
755
756const GRID_PADDING_TOP: usize = 12;
758const GRID_PADDING_BOTTOM: usize = 24;
760const GRID_PADDING_LEFT: usize = 16;
762
763const ICON_SIZE: usize = 48;
765const ICON_MARGIN_TOP: usize = 8;
767const NAME_MARGIN_TOP: usize = 4;
769
770pub fn parse_desktop_file(content: &str) -> Option<AppEntry> {
786 let mut name: Option<&str> = None;
787 let mut exec: Option<&str> = None;
788 let mut icon: Option<&str> = None;
789 let mut comment: Option<&str> = None;
790 let mut categories_raw: Option<&str> = None;
791 let mut in_desktop_entry = false;
792
793 for line in content.lines() {
794 let trimmed = line.trim();
795
796 if trimmed.starts_with('[') {
798 in_desktop_entry = trimmed == "[Desktop Entry]";
799 continue;
800 }
801
802 if !in_desktop_entry {
803 continue;
804 }
805
806 if trimmed.starts_with('#') {
808 continue;
809 }
810
811 if let Some(val) = strip_key(trimmed, "Name=") {
812 name = Some(val);
813 } else if let Some(val) = strip_key(trimmed, "Exec=") {
814 exec = Some(val);
815 } else if let Some(val) = strip_key(trimmed, "Icon=") {
816 icon = Some(val);
817 } else if let Some(val) = strip_key(trimmed, "Comment=") {
818 comment = Some(val);
819 } else if let Some(val) = strip_key(trimmed, "Categories=") {
820 categories_raw = Some(val);
821 }
822 }
823
824 let name_str = name?;
825 let exec_str = exec?;
826
827 let exec_clean = strip_field_codes(exec_str);
829
830 let category = categories_raw
831 .and_then(parse_category_string)
832 .unwrap_or(AppCategory::Other);
833
834 Some(AppEntry {
835 name: String::from(name_str),
836 exec_path: exec_clean,
837 icon_name: String::from(icon.unwrap_or("")),
838 category,
839 description: String::from(comment.unwrap_or("")),
840 })
841}
842
843fn strip_key<'a>(line: &'a str, key: &str) -> Option<&'a str> {
845 line.strip_prefix(key).map(|s| s.trim())
846}
847
848fn strip_field_codes(exec: &str) -> String {
850 let mut result = String::with_capacity(exec.len());
851 let bytes = exec.as_bytes();
852 let mut i = 0;
853 while i < bytes.len() {
854 if bytes[i] == b'%' && i + 1 < bytes.len() {
855 i += 2;
857 if i < bytes.len() && bytes[i] == b' ' {
859 i += 1;
860 }
861 } else {
862 result.push(bytes[i] as char);
863 i += 1;
864 }
865 }
866 while result.ends_with(' ') {
868 result.pop();
869 }
870 result
871}
872
873fn parse_category_string(cats: &str) -> Option<AppCategory> {
876 for segment in cats.split(';') {
877 let cat = segment.trim();
878 if cat.is_empty() {
879 continue;
880 }
881 match cat {
882 "System" | "Monitor" | "PackageManager" => return Some(AppCategory::System),
883 "Utility" | "Accessibility" | "Calculator" | "Clock" => {
884 return Some(AppCategory::Utility)
885 }
886 "Development" | "IDE" | "TextEditor" | "Debugger" | "WebDevelopment" => {
887 return Some(AppCategory::Development)
888 }
889 "Graphics" | "2DGraphics" | "3DGraphics" | "RasterGraphics" | "VectorGraphics" => {
890 return Some(AppCategory::Graphics)
891 }
892 "Network" | "WebBrowser" | "Email" | "Chat" | "IRCClient" | "FileTransfer" => {
893 return Some(AppCategory::Network)
894 }
895 "AudioVideo" | "Audio" | "Video" | "Multimedia" | "Player" | "Recorder" => {
896 return Some(AppCategory::Multimedia)
897 }
898 "Office" | "WordProcessor" | "Spreadsheet" | "Presentation" => {
899 return Some(AppCategory::Office)
900 }
901 "Settings" | "Preferences" | "DesktopSettings" | "HardwareSettings" => {
902 return Some(AppCategory::Settings)
903 }
904 _ => {}
905 }
906 }
907 None
908}
909
910fn default_applications() -> Vec<AppEntry> {
916 vec![
917 AppEntry::new(
918 "Terminal",
919 "/usr/bin/terminal",
920 "utilities-terminal",
921 AppCategory::System,
922 "Terminal emulator",
923 ),
924 AppEntry::new(
925 "File Manager",
926 "/usr/bin/files",
927 "system-file-manager",
928 AppCategory::System,
929 "Browse files",
930 ),
931 AppEntry::new(
932 "Text Editor",
933 "/usr/bin/editor",
934 "accessories-text-editor",
935 AppCategory::Utility,
936 "Edit text files",
937 ),
938 AppEntry::new(
939 "Settings",
940 "/usr/bin/settings",
941 "preferences-system",
942 AppCategory::Settings,
943 "System settings",
944 ),
945 AppEntry::new(
946 "System Monitor",
947 "/usr/bin/sysmonitor",
948 "utilities-system-monitor",
949 AppCategory::System,
950 "Monitor CPU and memory",
951 ),
952 AppEntry::new(
953 "Image Viewer",
954 "/usr/bin/image-viewer",
955 "eog",
956 AppCategory::Graphics,
957 "View images",
958 ),
959 AppEntry::new(
960 "Media Player",
961 "/usr/bin/mediaplayer",
962 "multimedia-player",
963 AppCategory::Multimedia,
964 "Play audio and video",
965 ),
966 AppEntry::new(
967 "Web Browser",
968 "/usr/bin/browser",
969 "web-browser",
970 AppCategory::Network,
971 "Browse the web",
972 ),
973 AppEntry::new(
974 "PDF Viewer",
975 "/usr/bin/pdfviewer",
976 "pdf-viewer",
977 AppCategory::Utility,
978 "View PDF documents",
979 ),
980 ]
981}
982
983pub fn category_color(cat: &AppCategory) -> u32 {
988 match cat {
989 AppCategory::System => 0xFF4488AA, AppCategory::Utility => 0xFF66AA44, AppCategory::Development => 0xFF886644, AppCategory::Graphics => 0xFFAA6688, AppCategory::Network => 0xFF4466AA, AppCategory::Multimedia => 0xFFAA4466, AppCategory::Office => 0xFF666699, AppCategory::Settings => 0xFF888888, AppCategory::Other => 0xFF555555, }
999}
1000
1001fn draw_char_u32(
1007 buffer: &mut [u32],
1008 buf_width: usize,
1009 buf_height: usize,
1010 ch: u8,
1011 px: usize,
1012 py: usize,
1013 color: u32,
1014) {
1015 let glyph = crate::graphics::font8x16::glyph(ch);
1016 for (row, &bits) in glyph.iter().enumerate() {
1017 let y = py + row;
1018 if y >= buf_height {
1019 break;
1020 }
1021 for col in 0..8 {
1022 if (bits >> (7 - col)) & 1 != 0 {
1023 let x = px + col;
1024 if x >= buf_width {
1025 break;
1026 }
1027 let idx = y * buf_width + x;
1028 if idx < buffer.len() {
1029 buffer[idx] = color;
1030 }
1031 }
1032 }
1033 }
1034}
1035
1036fn draw_text_u32(
1038 buffer: &mut [u32],
1039 buf_width: usize,
1040 buf_height: usize,
1041 text: &[u8],
1042 px: usize,
1043 py: usize,
1044 color: u32,
1045) {
1046 for (i, &ch) in text.iter().enumerate() {
1047 draw_char_u32(
1048 buffer,
1049 buf_width,
1050 buf_height,
1051 ch,
1052 px + i * FONT_WIDTH,
1053 py,
1054 color,
1055 );
1056 }
1057}
1058
1059fn draw_rect_outline(
1061 buffer: &mut [u32],
1062 buf_width: usize,
1063 buf_height: usize,
1064 x: usize,
1065 y: usize,
1066 w: usize,
1067 h: usize,
1068 color: u32,
1069) {
1070 for col in x..(x + w).min(buf_width) {
1072 if y < buf_height {
1073 let idx = y * buf_width + col;
1074 if idx < buffer.len() {
1075 buffer[idx] = color;
1076 }
1077 }
1078 let bottom = y + h - 1;
1079 if bottom < buf_height {
1080 let idx = bottom * buf_width + col;
1081 if idx < buffer.len() {
1082 buffer[idx] = color;
1083 }
1084 }
1085 }
1086 for row in y..(y + h).min(buf_height) {
1088 if x < buf_width {
1089 let idx = row * buf_width + x;
1090 if idx < buffer.len() {
1091 buffer[idx] = color;
1092 }
1093 }
1094 let right = x + w - 1;
1095 if right < buf_width {
1096 let idx = row * buf_width + right;
1097 if idx < buffer.len() {
1098 buffer[idx] = color;
1099 }
1100 }
1101 }
1102}
1103
1104fn alpha_blend(bg: u32, fg: u32) -> u32 {
1108 let fg_a = (fg >> 24) & 0xFF;
1109 if fg_a == 0xFF {
1110 return fg;
1111 }
1112 if fg_a == 0 {
1113 return bg;
1114 }
1115
1116 let inv_a = 255 - fg_a;
1117
1118 let fg_r = (fg >> 16) & 0xFF;
1119 let fg_g = (fg >> 8) & 0xFF;
1120 let fg_b = fg & 0xFF;
1121
1122 let bg_r = (bg >> 16) & 0xFF;
1123 let bg_g = (bg >> 8) & 0xFF;
1124 let bg_b = bg & 0xFF;
1125
1126 let r = (fg_r * fg_a + bg_r * inv_a) / 255;
1128 let g = (fg_g * fg_a + bg_g * inv_a) / 255;
1129 let b = (fg_b * fg_a + bg_b * inv_a) / 255;
1130
1131 0xFF000000 | (r << 16) | (g << 8) | b
1132}
1133
1134fn ascii_to_lower(b: u8) -> u8 {
1140 if b.is_ascii_uppercase() {
1141 b + 32
1142 } else {
1143 b
1144 }
1145}
1146
1147fn ascii_contains_lower(haystack: &[u8], needle: &[u8]) -> bool {
1150 if needle.is_empty() {
1151 return true;
1152 }
1153 if needle.len() > haystack.len() {
1154 return false;
1155 }
1156 let limit = haystack.len() - needle.len();
1157 for start in 0..=limit {
1158 let mut matches = true;
1159 for (j, &nb) in needle.iter().enumerate() {
1160 if ascii_to_lower(haystack[start + j]) != nb {
1161 matches = false;
1162 break;
1163 }
1164 }
1165 if matches {
1166 return true;
1167 }
1168 }
1169 false
1170}
1171
1172fn format_count(filtered: usize, total: usize) -> String {
1174 let mut s = String::with_capacity(32);
1175 append_usize(&mut s, filtered);
1176 s.push_str(" of ");
1177 append_usize(&mut s, total);
1178 s.push_str(" apps");
1179 s
1180}
1181
1182fn append_usize(s: &mut String, n: usize) {
1184 if n == 0 {
1185 s.push('0');
1186 return;
1187 }
1188 let mut buf = [0u8; 20];
1190 let mut pos = buf.len();
1191 let mut val = n;
1192 while val > 0 {
1193 pos -= 1;
1194 buf[pos] = b'0' + (val % 10) as u8;
1195 val /= 10;
1196 }
1197 for &ch in &buf[pos..] {
1198 s.push(ch as char);
1199 }
1200}
1201
1202static LAUNCHER: GlobalState<spin::Mutex<AppLauncher>> = GlobalState::new();
1208
1209pub fn init() -> Result<(), crate::error::KernelError> {
1211 LAUNCHER
1212 .init(spin::Mutex::new(AppLauncher::new()))
1213 .map_err(|_| crate::error::KernelError::InvalidState {
1214 expected: "uninitialized",
1215 actual: "initialized",
1216 })?;
1217
1218 crate::println!("[LAUNCHER] Application launcher initialized ({} apps)", 7);
1219 Ok(())
1220}
1221
1222pub fn with_launcher<R, F: FnOnce(&mut AppLauncher) -> R>(f: F) -> Option<R> {
1224 LAUNCHER.with(|lock| {
1225 let mut launcher = lock.lock();
1226 f(&mut launcher)
1227 })
1228}
1229
1230pub fn with_launcher_ref<R, F: FnOnce(&AppLauncher) -> R>(f: F) -> Option<R> {
1232 LAUNCHER.with(|lock| {
1233 let launcher = lock.lock();
1234 f(&launcher)
1235 })
1236}