1#![allow(dead_code)]
9
10use alloc::{
11 format,
12 string::{String, ToString},
13 vec,
14 vec::Vec,
15};
16
17use super::{
18 tab_isolation::{ProcessIsolation, TabCapabilities},
19 tabs::{TabAction, TabId, TabManager},
20};
21
22#[derive(Debug, Clone)]
28pub struct BrowserConfig {
29 pub home_page: String,
31 pub viewport_width: u32,
33 pub viewport_height: u32,
35 pub tab_bar_height: u32,
37 pub address_bar_height: u32,
39 pub show_address_bar: bool,
41 pub show_nav_buttons: bool,
43 pub max_tabs: usize,
45 pub enable_js: bool,
47}
48
49impl Default for BrowserConfig {
50 fn default() -> Self {
51 Self {
52 home_page: "veridian://newtab".to_string(),
53 viewport_width: 1024,
54 viewport_height: 768,
55 tab_bar_height: 32,
56 address_bar_height: 36,
57 show_address_bar: true,
58 show_nav_buttons: true,
59 max_tabs: 32,
60 enable_js: true,
61 }
62 }
63}
64
65#[derive(Debug, Clone)]
71pub struct AddressBar {
72 pub text: String,
74 pub cursor: usize,
76 pub focused: bool,
78 pub selection_start: Option<usize>,
80 pub suggestions: Vec<String>,
82 pub showing_suggestions: bool,
84}
85
86impl Default for AddressBar {
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92impl AddressBar {
93 pub fn new() -> Self {
94 Self {
95 text: String::new(),
96 cursor: 0,
97 focused: false,
98 selection_start: None,
99 suggestions: Vec::new(),
100 showing_suggestions: false,
101 }
102 }
103
104 pub fn set_url(&mut self, url: &str) {
106 self.text = url.to_string();
107 self.cursor = self.text.len();
108 self.selection_start = None;
109 self.showing_suggestions = false;
110 }
111
112 pub fn focus(&mut self) {
114 self.focused = true;
115 self.selection_start = Some(0);
116 self.cursor = self.text.len();
117 }
118
119 pub fn unfocus(&mut self) {
121 self.focused = false;
122 self.selection_start = None;
123 self.showing_suggestions = false;
124 }
125
126 pub fn insert_char(&mut self, ch: char) {
128 if let Some(sel_start) = self.selection_start {
130 let start = sel_start.min(self.cursor);
131 let end = sel_start.max(self.cursor);
132 self.text.drain(start..end);
133 self.cursor = start;
134 self.selection_start = None;
135 }
136
137 if self.cursor <= self.text.len() {
138 self.text.insert(self.cursor, ch);
139 self.cursor += ch.len_utf8();
140 }
141 }
142
143 pub fn backspace(&mut self) {
145 if let Some(sel_start) = self.selection_start {
146 let start = sel_start.min(self.cursor);
147 let end = sel_start.max(self.cursor);
148 self.text.drain(start..end);
149 self.cursor = start;
150 self.selection_start = None;
151 } else if self.cursor > 0 {
152 self.cursor -= 1;
153 self.text.remove(self.cursor);
154 }
155 }
156
157 pub fn delete(&mut self) {
159 if self.cursor < self.text.len() {
160 self.text.remove(self.cursor);
161 }
162 }
163
164 pub fn move_left(&mut self) {
166 if self.cursor > 0 {
167 self.cursor -= 1;
168 }
169 self.selection_start = None;
170 }
171
172 pub fn move_right(&mut self) {
174 if self.cursor < self.text.len() {
175 self.cursor += 1;
176 }
177 self.selection_start = None;
178 }
179
180 pub fn home(&mut self) {
182 self.cursor = 0;
183 self.selection_start = None;
184 }
185
186 pub fn end(&mut self) {
188 self.cursor = self.text.len();
189 self.selection_start = None;
190 }
191
192 pub fn get_navigation_url(&self) -> String {
194 let trimmed = self.text.trim();
195 if trimmed.is_empty() {
196 return String::new();
197 }
198 if trimmed.starts_with("http://")
200 || trimmed.starts_with("https://")
201 || trimmed.starts_with("veridian://")
202 {
203 return trimmed.to_string();
204 }
205 if trimmed.contains('.') && !trimmed.contains(' ') {
207 return format!("https://{}", trimmed);
208 }
209 format!("veridian://search?q={}", url_encode(trimmed))
211 }
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum NavButton {
221 Back,
222 Forward,
223 Reload,
224 Home,
225 Stop,
226}
227
228pub struct NavigationBar {
230 pub button_width: u32,
232 pub button_height: u32,
234 pub spacing: u32,
236 pub hovered: Option<NavButton>,
238 pub pressed: Option<NavButton>,
240}
241
242impl Default for NavigationBar {
243 fn default() -> Self {
244 Self::new()
245 }
246}
247
248impl NavigationBar {
249 pub fn new() -> Self {
250 Self {
251 button_width: 28,
252 button_height: 28,
253 spacing: 4,
254 hovered: None,
255 pressed: None,
256 }
257 }
258
259 pub fn total_width(&self) -> u32 {
261 4 * self.button_width + 3 * self.spacing
263 }
264
265 pub fn button_at(&self, x: u32) -> Option<NavButton> {
267 let stride = self.button_width + self.spacing;
268 let index = x / stride;
269 let offset = x % stride;
270 if offset > self.button_width {
271 return None; }
273 match index {
274 0 => Some(NavButton::Back),
275 1 => Some(NavButton::Forward),
276 2 => Some(NavButton::Reload),
277 3 => Some(NavButton::Home),
278 _ => None,
279 }
280 }
281
282 pub fn render(
284 &self,
285 buf: &mut [u32],
286 buf_width: usize,
287 y_offset: usize,
288 can_back: bool,
289 can_forward: bool,
290 is_loading: bool,
291 ) {
292 let buttons = [
293 (NavButton::Back, "<", can_back),
294 (NavButton::Forward, ">", can_forward),
295 (
296 if is_loading {
297 NavButton::Stop
298 } else {
299 NavButton::Reload
300 },
301 if is_loading { "X" } else { "R" },
302 true,
303 ),
304 (NavButton::Home, "H", true),
305 ];
306
307 let stride = (self.button_width + self.spacing) as usize;
308 let bw = self.button_width as usize;
309 let bh = self.button_height as usize;
310
311 for (i, (btn, label, enabled)) in buttons.iter().enumerate() {
312 let bx = i * stride;
313 let is_hovered = self.hovered == Some(*btn);
314 let is_pressed = self.pressed == Some(*btn);
315
316 let bg = if !enabled {
317 0xFF3C3C3C_u32 } else if is_pressed {
319 0xFF555555_u32
320 } else if is_hovered {
321 0xFF4A4A4A_u32
322 } else {
323 0xFF3C3C3C_u32
324 };
325
326 let fg = if *enabled {
327 0xFFFFFFFF_u32
328 } else {
329 0xFF606060_u32
330 };
331
332 for dy in 0..bh {
334 let py = y_offset + dy;
335 for dx in 0..bw {
336 let px = bx + dx;
337 if py * buf_width + px < buf.len() {
338 buf[py * buf_width + px] = bg;
339 }
340 }
341 }
342
343 let lx = bx + bw / 2;
345 let ly = y_offset + bh / 2;
346 if ly * buf_width + lx < buf.len() {
347 buf[ly * buf_width + lx] = fg;
348 }
349 for d in 1..3_usize {
351 if ly * buf_width + lx + d < buf.len() {
352 buf[ly * buf_width + lx + d] = fg;
353 }
354 if lx >= d && ly * buf_width + lx - d < buf.len() {
355 buf[ly * buf_width + lx - d] = fg;
356 }
357 }
358
359 let _ = label; }
361 }
362}
363
364pub struct Browser {
370 pub config: BrowserConfig,
372 pub tabs: TabManager,
374 pub isolation: ProcessIsolation,
376 pub address_bar: AddressBar,
378 pub nav_bar: NavigationBar,
380 pub framebuffer: Vec<u32>,
382 pub needs_repaint: bool,
384 pub history: Vec<String>,
386 pub max_history: usize,
388 pub running: bool,
390 pub status_text: String,
392}
393
394impl Default for Browser {
395 fn default() -> Self {
396 Self::new(BrowserConfig::default())
397 }
398}
399
400impl Browser {
401 pub fn new(config: BrowserConfig) -> Self {
403 let fb_size = (config.viewport_width * config.viewport_height) as usize;
404 Self {
405 tabs: TabManager::new(),
406 isolation: ProcessIsolation::new(),
407 address_bar: AddressBar::new(),
408 nav_bar: NavigationBar::new(),
409 framebuffer: vec![0xFF2D2D30; fb_size],
410 needs_repaint: true,
411 history: Vec::new(),
412 max_history: 1000,
413 running: true,
414 status_text: String::new(),
415 config,
416 }
417 }
418
419 pub fn init(&mut self) {
421 let url = self.config.home_page.clone();
422 self.open_url(&url);
423 }
424
425 pub fn open_url(&mut self, url: &str) -> Option<TabId> {
427 if let Some(active) = self.tabs.active_tab() {
429 if active.url.is_empty() {
430 let id = active.id;
431 self.navigate_tab(id, url);
432 return Some(id);
433 }
434 }
435
436 let tab_id = self.tabs.new_tab(url)?;
438
439 let caps = if url.starts_with("veridian://") {
441 TabCapabilities::trusted()
442 } else {
443 TabCapabilities::default_web()
444 };
445 let _ = self.isolation.spawn_with_capabilities(tab_id, caps);
446
447 if let Some(origin) = extract_origin(url) {
449 if let Some(proc) = self.isolation.get_process_mut(tab_id) {
450 proc.set_origin(&origin);
451 }
452 }
453
454 self.update_address_bar();
455 self.needs_repaint = true;
456 Some(tab_id)
457 }
458
459 pub fn navigate_active(&mut self, url: &str) {
461 if let Some(id) = self.tabs.active_tab_id() {
462 self.navigate_tab(id, url);
463 }
464 }
465
466 pub fn navigate_tab(&mut self, tab_id: TabId, url: &str) {
468 if let Some(tab) = self.tabs.get_tab_mut(tab_id) {
469 tab.navigate(url);
470 }
471
472 if let Some(origin) = extract_origin(url) {
474 if let Some(proc) = self.isolation.get_process_mut(tab_id) {
475 proc.set_origin(&origin);
476 }
477 }
478
479 if !url.is_empty() && !url.starts_with("veridian://newtab") {
481 self.history.push(url.to_string());
482 if self.history.len() > self.max_history {
483 self.history.remove(0);
484 }
485 }
486
487 self.load_page(tab_id, url);
489 self.update_address_bar();
490 self.needs_repaint = true;
491 }
492
493 fn load_page(&mut self, tab_id: TabId, url: &str) {
495 if url.starts_with("veridian://") {
496 let page_html = generate_internal_page(url);
498 if let Some(tab) = self.tabs.get_tab_mut(tab_id) {
499 tab.set_title(&internal_page_title(url));
500 tab.finish_loading();
501 }
502
503 if let Some(proc) = self.isolation.get_process_mut(tab_id) {
505 let _doc = proc.dom_api.create_element("html");
507 use super::js_integration::ScriptEngine;
509 let mut engine = ScriptEngine::new();
510 engine.process_script_tags(&page_html);
511 }
512 } else {
513 if let Some(tab) = self.tabs.get_tab_mut(tab_id) {
515 tab.finish_loading();
516 self.status_text = format!("Loaded: {}", url);
517 }
518 }
519 }
520
521 pub fn handle_key(&mut self, scancode: u8, ctrl: bool, shift: bool, alt: bool) {
523 if self.address_bar.focused {
525 match scancode {
526 0x0D => {
527 let url = self.address_bar.get_navigation_url();
529 if !url.is_empty() {
530 self.address_bar.unfocus();
531 self.navigate_active(&url);
532 }
533 }
534 0x1B => {
535 self.address_bar.unfocus();
537 self.update_address_bar();
538 }
539 0x08 => self.address_bar.backspace(),
540 0x7F => self.address_bar.delete(),
541 0x80 => self.address_bar.move_left(), 0x81 => self.address_bar.move_right(), _ => {
544 if (0x20..0x7F).contains(&scancode) {
545 self.address_bar.insert_char(scancode as char);
546 }
547 }
548 }
549 self.needs_repaint = true;
550 return;
551 }
552
553 if ctrl {
555 let action = TabManager::decode_shortcut(ctrl, shift, scancode);
556 match action {
557 TabAction::NewTab => {
558 self.open_url("");
559 self.address_bar.focus();
560 }
561 TabAction::CloseTab => {
562 if let Some(id) = self.tabs.active_tab_id() {
563 self.close_tab(id);
564 }
565 }
566 TabAction::NextTab => {
567 self.tabs.next_tab();
568 self.update_address_bar();
569 }
570 TabAction::PrevTab => {
571 self.tabs.prev_tab();
572 self.update_address_bar();
573 }
574 TabAction::SwitchToIndex(idx) => {
575 self.tabs.switch_to_index(idx);
576 self.update_address_bar();
577 }
578 TabAction::ReopenClosed => {
579 self.tabs.reopen_closed_tab();
580 self.update_address_bar();
581 }
582 TabAction::None => {
583 if scancode == b'l' || scancode == b'L' {
585 self.address_bar.focus();
586 }
587 }
588 }
589 self.needs_repaint = true;
590 return;
591 }
592
593 if alt {
595 match scancode {
596 0x80 => self.go_back(), 0x81 => self.go_forward(), _ => {}
599 }
600 self.needs_repaint = true;
601 return;
602 }
603
604 let _ = (scancode, shift);
607 self.needs_repaint = true;
608 }
609
610 pub fn handle_click(&mut self, x: i32, y: i32) {
612 let tab_bar_h = self.config.tab_bar_height as i32;
613 let addr_bar_h = if self.config.show_address_bar {
614 self.config.address_bar_height as i32
615 } else {
616 0
617 };
618
619 if y < tab_bar_h {
621 let action = self.tabs.handle_tab_bar_click(x, y);
622 match action {
623 TabAction::NewTab => {
624 self.open_url("");
625 self.address_bar.focus();
626 }
627 _ => {
628 self.update_address_bar();
629 }
630 }
631 self.needs_repaint = true;
632 return;
633 }
634
635 if y < tab_bar_h + addr_bar_h {
637 let nav_width = if self.config.show_nav_buttons {
638 self.nav_bar.total_width() as i32 + 8
639 } else {
640 0
641 };
642
643 if x < nav_width {
644 if let Some(btn) = self.nav_bar.button_at(x as u32) {
646 match btn {
647 NavButton::Back => self.go_back(),
648 NavButton::Forward => self.go_forward(),
649 NavButton::Reload => self.reload_active(),
650 NavButton::Home => self.go_home(),
651 NavButton::Stop => self.stop_loading(),
652 }
653 }
654 } else {
655 self.address_bar.focus();
657 }
658 self.needs_repaint = true;
659 return;
660 }
661
662 let content_y = y - tab_bar_h - addr_bar_h;
664 if let Some(tab_id) = self.tabs.active_tab_id() {
665 if let Some(proc) = self.isolation.get_process_mut(tab_id) {
666 let _target = proc
668 .dom_api
669 .event_dispatcher
670 .dispatch_click(x, content_y, 0);
671 }
672 }
673 self.needs_repaint = true;
674 }
675
676 pub fn go_back(&mut self) {
678 if let Some(tab) = self.tabs.active_tab_mut() {
679 if let Some(url) = tab.go_back() {
680 let id = tab.id;
681 self.load_page(id, &url);
682 }
683 }
684 self.update_address_bar();
685 self.needs_repaint = true;
686 }
687
688 pub fn go_forward(&mut self) {
690 if let Some(tab) = self.tabs.active_tab_mut() {
691 if let Some(url) = tab.go_forward() {
692 let id = tab.id;
693 self.load_page(id, &url);
694 }
695 }
696 self.update_address_bar();
697 self.needs_repaint = true;
698 }
699
700 pub fn reload_active(&mut self) {
702 if let Some(tab) = self.tabs.active_tab_mut() {
703 tab.reload();
704 let id = tab.id;
705 let url = tab.url.clone();
706 self.load_page(id, &url);
707 }
708 self.needs_repaint = true;
709 }
710
711 pub fn go_home(&mut self) {
713 let home = self.config.home_page.clone();
714 self.navigate_active(&home);
715 }
716
717 pub fn stop_loading(&mut self) {
719 if let Some(tab) = self.tabs.active_tab_mut() {
720 tab.finish_loading();
721 }
722 self.needs_repaint = true;
723 }
724
725 pub fn close_tab(&mut self, tab_id: TabId) {
727 self.isolation.kill_tab_process(tab_id);
728 self.tabs.close_tab(tab_id);
729 self.update_address_bar();
730 self.needs_repaint = true;
731 }
732
733 fn update_address_bar(&mut self) {
735 if !self.address_bar.focused {
736 if let Some(tab) = self.tabs.active_tab() {
737 self.address_bar.set_url(&tab.url);
738 } else {
739 self.address_bar.set_url("");
740 }
741 }
742 }
743
744 pub fn render(&mut self) {
746 if !self.needs_repaint {
747 return;
748 }
749
750 let w = self.config.viewport_width as usize;
751 let h = self.config.viewport_height as usize;
752 let fb_len = w * h;
753
754 if self.framebuffer.len() != fb_len {
756 self.framebuffer.resize(fb_len, 0xFF2D2D30);
757 }
758
759 for pixel in self.framebuffer.iter_mut() {
761 *pixel = 0xFF2D2D30;
762 }
763
764 let tab_bar_h = self.config.tab_bar_height as usize;
766 let tabs_ordered = self.tabs.tabs_in_order();
767 let tab_refs: Vec<&super::tabs::Tab> = tabs_ordered;
768 self.tabs
769 .tab_bar
770 .render_tab_bar(&tab_refs, &mut self.framebuffer, w);
771
772 if self.config.show_address_bar {
774 let addr_y = tab_bar_h;
775 let addr_h = self.config.address_bar_height as usize;
776 let addr_bg: u32 = 0xFF252526;
777
778 for dy in 0..addr_h {
780 let py = addr_y + dy;
781 for px in 0..w {
782 if py * w + px < fb_len {
783 self.framebuffer[py * w + px] = addr_bg;
784 }
785 }
786 }
787
788 if self.config.show_nav_buttons {
790 let can_back = self.tabs.active_tab().is_some_and(|t| t.can_go_back());
791 let can_fwd = self.tabs.active_tab().is_some_and(|t| t.can_go_forward());
792 let is_loading = self
793 .tabs
794 .active_tab()
795 .is_some_and(|t| t.load_state == super::tabs::TabLoadState::Loading);
796 self.nav_bar.render(
797 &mut self.framebuffer,
798 w,
799 addr_y + 4,
800 can_back,
801 can_fwd,
802 is_loading,
803 );
804 }
805
806 let text_x = if self.config.show_nav_buttons {
808 self.nav_bar.total_width() as usize + 12
809 } else {
810 8
811 };
812 let text_y = addr_y + addr_h / 2;
813 let text_w = w.saturating_sub(text_x).saturating_sub(8);
814
815 let field_bg: u32 = if self.address_bar.focused {
817 0xFF3C3C3C
818 } else {
819 0xFF333333
820 };
821 for dy in 4..addr_h.saturating_sub(4) {
822 let py = addr_y + dy;
823 for dx in 0..text_w {
824 let px = text_x + dx;
825 if py * w + px < fb_len {
826 self.framebuffer[py * w + px] = field_bg;
827 }
828 }
829 }
830
831 let display_text = if self.address_bar.text.len() > text_w / 8 {
833 &self.address_bar.text[..text_w / 8]
834 } else {
835 &self.address_bar.text
836 };
837 render_simple_text(
838 &mut self.framebuffer,
839 w,
840 text_x + 4,
841 text_y,
842 display_text,
843 0xFFD4D4D4,
844 );
845
846 if self.address_bar.focused {
848 let cursor_x = text_x + 4 + self.address_bar.cursor * 8;
849 for dy in 0..12_usize {
850 let py = text_y.saturating_sub(6) + dy;
851 if py * w + cursor_x < fb_len {
852 self.framebuffer[py * w + cursor_x] = 0xFFFFFFFF;
853 }
854 }
855 }
856 }
857
858 let content_y = tab_bar_h
860 + if self.config.show_address_bar {
861 self.config.address_bar_height as usize
862 } else {
863 0
864 };
865
866 if let Some(tab) = self.tabs.active_tab() {
867 let content_bg: u32 = 0xFFFFFFFF; for dy in 0..h.saturating_sub(content_y) {
869 let py = content_y + dy;
870 for px in 0..w {
871 if py * w + px < fb_len {
872 self.framebuffer[py * w + px] = content_bg;
873 }
874 }
875 }
876
877 render_simple_text(
879 &mut self.framebuffer,
880 w,
881 20,
882 content_y + 30,
883 &tab.title,
884 0xFF000000,
885 );
886
887 if tab.load_state == super::tabs::TabLoadState::Error {
888 if let Some(err) = &tab.error_message {
889 render_simple_text(
890 &mut self.framebuffer,
891 w,
892 20,
893 content_y + 60,
894 err,
895 0xFFCC0000,
896 );
897 }
898 }
899 }
900
901 self.needs_repaint = false;
902 }
903
904 pub fn tick(&mut self) {
906 self.tabs.tick();
907 self.isolation.tick_all();
908 }
909
910 pub fn framebuffer(&self) -> &[u32] {
912 &self.framebuffer
913 }
914
915 pub fn dimensions(&self) -> (u32, u32) {
917 (self.config.viewport_width, self.config.viewport_height)
918 }
919
920 pub fn tab_count(&self) -> usize {
922 self.tabs.tab_count()
923 }
924
925 pub fn is_running(&self) -> bool {
927 self.running
928 }
929
930 pub fn quit(&mut self) {
932 let ids: Vec<TabId> = self.tabs.tab_order().to_vec();
934 for id in ids {
935 self.isolation.kill_tab_process(id);
936 }
937 self.running = false;
938 }
939}
940
941pub fn handle_shell_command(browser: &mut Browser, args: &[&str]) -> String {
947 if args.is_empty() {
948 return "Usage: browser <open|back|forward|reload|tabs|close|quit> [args]".to_string();
949 }
950
951 match args[0] {
952 "open" => {
953 let url = if args.len() > 1 { args[1] } else { "" };
954 match browser.open_url(url) {
955 Some(id) => format!("Opened tab {} with URL: {}", id, url),
956 None => "Failed to open tab (max tabs reached?)".to_string(),
957 }
958 }
959 "back" => {
960 browser.go_back();
961 "Navigated back".to_string()
962 }
963 "forward" => {
964 browser.go_forward();
965 "Navigated forward".to_string()
966 }
967 "reload" => {
968 browser.reload_active();
969 "Reloading".to_string()
970 }
971 "home" => {
972 browser.go_home();
973 "Navigated to home page".to_string()
974 }
975 "tabs" => {
976 let tabs = browser.tabs.tabs_in_order();
977 let mut out = format!("{} tab(s) open:\n", tabs.len());
978 for tab in tabs {
979 let marker = if tab.active { "* " } else { " " };
980 out.push_str(&format!(
981 "{}[{}] {} - {}\n",
982 marker, tab.id, tab.title, tab.url
983 ));
984 }
985 out
986 }
987 "close" => {
988 if args.len() > 1 {
989 if let Ok(id) = args[1].parse::<u64>() {
990 browser.close_tab(id);
991 format!("Closed tab {}", id)
992 } else {
993 "Invalid tab ID".to_string()
994 }
995 } else if let Some(id) = browser.tabs.active_tab_id() {
996 browser.close_tab(id);
997 format!("Closed active tab {}", id)
998 } else {
999 "No active tab".to_string()
1000 }
1001 }
1002 "quit" => {
1003 browser.quit();
1004 "Browser closed".to_string()
1005 }
1006 _ => format!("Unknown browser command: {}", args[0]),
1007 }
1008}
1009
1010fn generate_internal_page(url: &str) -> String {
1016 let path = url.strip_prefix("veridian://").unwrap_or("");
1017 match path {
1018 "newtab" => "<html><head><title>New Tab</title></head><body><h1>VeridianOS \
1019 Browser</h1><p>Welcome to the VeridianOS built-in browser.</p></body></html>"
1020 .to_string(),
1021 "settings" => "<html><head><title>Settings</title></head><body><h1>Browser \
1022 Settings</h1><p>Configuration options will appear here.</p></body></html>"
1023 .to_string(),
1024 "about" => "<html><head><title>About</title></head><body><h1>About VeridianOS \
1025 Browser</h1><p>Built-in web browser for VeridianOS.</p><p>Kernel-space \
1026 rendering with per-tab isolation.</p></body></html>"
1027 .to_string(),
1028 "history" => "<html><head><title>History</title></head><body><h1>Browsing \
1029 History</h1><p>History entries will appear here.</p></body></html>"
1030 .to_string(),
1031 _ => {
1032 format!(
1033 "<html><head><title>Not Found</title></head><body><h1>Page Not Found</h1><p>The \
1034 page veridian://{} does not exist.</p></body></html>",
1035 path
1036 )
1037 }
1038 }
1039}
1040
1041fn internal_page_title(url: &str) -> String {
1043 let path = url.strip_prefix("veridian://").unwrap_or("");
1044 match path {
1045 "newtab" => "New Tab".to_string(),
1046 "settings" => "Settings".to_string(),
1047 "about" => "About".to_string(),
1048 "history" => "History".to_string(),
1049 _ => "Not Found".to_string(),
1050 }
1051}
1052
1053fn extract_origin(url: &str) -> Option<String> {
1059 let after_scheme = if let Some(rest) = url.strip_prefix("https://") {
1061 ("https://", rest)
1062 } else if let Some(rest) = url.strip_prefix("http://") {
1063 ("http://", rest)
1064 } else if let Some(rest) = url.strip_prefix("veridian://") {
1065 ("veridian://", rest)
1066 } else {
1067 return None;
1068 };
1069
1070 let host = after_scheme.1.split('/').next().unwrap_or("");
1071 if host.is_empty() {
1072 None
1073 } else {
1074 Some(format!("{}{}", after_scheme.0, host))
1075 }
1076}
1077
1078fn url_encode(input: &str) -> String {
1080 let mut result = String::with_capacity(input.len() * 3);
1081 for b in input.bytes() {
1082 match b {
1083 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
1084 result.push(b as char);
1085 }
1086 b' ' => result.push('+'),
1087 _ => {
1088 result.push('%');
1089 result.push(hex_char(b >> 4));
1090 result.push(hex_char(b & 0x0F));
1091 }
1092 }
1093 }
1094 result
1095}
1096
1097fn hex_char(nibble: u8) -> char {
1098 match nibble {
1099 0..=9 => (b'0' + nibble) as char,
1100 10..=15 => (b'A' + nibble - 10) as char,
1101 _ => '0',
1102 }
1103}
1104
1105fn render_simple_text(
1107 buf: &mut [u32],
1108 buf_width: usize,
1109 x: usize,
1110 y: usize,
1111 text: &str,
1112 color: u32,
1113) {
1114 for (i, _ch) in text.chars().enumerate() {
1115 let px = x + i * 8;
1116 if y > 0 && y * buf_width + px < buf.len() {
1117 for dy in 0..5_usize {
1119 for dx in 0..5_usize {
1120 let ppx = px + dx;
1121 let ppy = y.wrapping_sub(2) + dy;
1122 if ppy * buf_width + ppx < buf.len() && (dy + dx) % 2 == 0 {
1123 buf[ppy * buf_width + ppx] = color;
1124 }
1125 }
1126 }
1127 }
1128 }
1129}
1130
1131#[cfg(test)]
1136mod tests {
1137 use super::*;
1138
1139 #[test]
1140 fn test_browser_config_default() {
1141 let cfg = BrowserConfig::default();
1142 assert_eq!(cfg.viewport_width, 1024);
1143 assert_eq!(cfg.viewport_height, 768);
1144 assert!(cfg.enable_js);
1145 }
1146
1147 #[test]
1148 fn test_browser_new() {
1149 let browser = Browser::new(BrowserConfig::default());
1150 assert_eq!(browser.tab_count(), 0);
1151 assert!(browser.is_running());
1152 }
1153
1154 #[test]
1155 fn test_browser_init() {
1156 let mut browser = Browser::new(BrowserConfig::default());
1157 browser.init();
1158 assert_eq!(browser.tab_count(), 1);
1159 assert!(browser.tabs.active_tab().is_some());
1160 }
1161
1162 #[test]
1163 fn test_browser_open_url() {
1164 let mut browser = Browser::default();
1165 let id = browser.open_url("https://example.com").unwrap();
1166 assert_eq!(browser.tab_count(), 1);
1167 let tab = browser.tabs.get_tab(id).unwrap();
1168 assert_eq!(tab.url, "https://example.com");
1169 }
1170
1171 #[test]
1172 fn test_browser_open_multiple_tabs() {
1173 let mut browser = Browser::default();
1174 browser.open_url("https://a.com");
1175 browser.open_url("https://b.com");
1176 assert_eq!(browser.tab_count(), 2);
1177 }
1178
1179 #[test]
1180 fn test_browser_close_tab() {
1181 let mut browser = Browser::default();
1182 let id = browser.open_url("https://a.com").unwrap();
1183 browser.open_url("https://b.com");
1184 browser.close_tab(id);
1185 assert_eq!(browser.tab_count(), 1);
1186 }
1187
1188 #[test]
1189 fn test_browser_navigate() {
1190 let mut browser = Browser::default();
1191 browser.open_url("https://a.com");
1192 browser.navigate_active("https://b.com");
1193 assert_eq!(browser.tabs.active_tab().unwrap().url, "https://b.com");
1194 }
1195
1196 #[test]
1197 fn test_browser_back_forward() {
1198 let mut browser = Browser::default();
1199 browser.open_url("https://a.com");
1200 browser.navigate_active("https://b.com");
1201 browser.go_back();
1202 assert_eq!(browser.tabs.active_tab().unwrap().url, "https://a.com");
1203 browser.go_forward();
1204 assert_eq!(browser.tabs.active_tab().unwrap().url, "https://b.com");
1205 }
1206
1207 #[test]
1208 fn test_browser_go_home() {
1209 let mut browser = Browser::default();
1210 browser.open_url("https://example.com");
1211 browser.go_home();
1212 assert_eq!(browser.tabs.active_tab().unwrap().url, "veridian://newtab");
1213 }
1214
1215 #[test]
1216 fn test_browser_render() {
1217 let mut browser = Browser::new(BrowserConfig {
1218 viewport_width: 100,
1219 viewport_height: 100,
1220 ..BrowserConfig::default()
1221 });
1222 browser.open_url("veridian://newtab");
1223 browser.render();
1224 assert!(!browser.needs_repaint);
1225 assert_eq!(browser.framebuffer.len(), 10000);
1226 }
1227
1228 #[test]
1229 fn test_browser_tick() {
1230 let mut browser = Browser::default();
1231 browser.open_url("https://example.com");
1232 browser.tick();
1233 }
1235
1236 #[test]
1237 fn test_browser_quit() {
1238 let mut browser = Browser::default();
1239 browser.open_url("https://example.com");
1240 browser.quit();
1241 assert!(!browser.is_running());
1242 }
1243
1244 #[test]
1245 fn test_address_bar_new() {
1246 let bar = AddressBar::new();
1247 assert_eq!(bar.text, "");
1248 assert!(!bar.focused);
1249 }
1250
1251 #[test]
1252 fn test_address_bar_set_url() {
1253 let mut bar = AddressBar::new();
1254 bar.set_url("https://example.com");
1255 assert_eq!(bar.text, "https://example.com");
1256 assert_eq!(bar.cursor, bar.text.len());
1257 }
1258
1259 #[test]
1260 fn test_address_bar_editing() {
1261 let mut bar = AddressBar::new();
1262 bar.focus();
1263 bar.insert_char('h');
1264 bar.insert_char('i');
1265 assert_eq!(bar.text, "hi");
1266 bar.backspace();
1267 assert_eq!(bar.text, "h");
1268 bar.move_left();
1269 bar.insert_char('x');
1270 assert_eq!(bar.text, "xh");
1271 }
1272
1273 #[test]
1274 fn test_address_bar_navigation_url() {
1275 let mut bar = AddressBar::new();
1276 bar.text = "https://example.com".to_string();
1277 assert_eq!(bar.get_navigation_url(), "https://example.com");
1278
1279 bar.text = "example.com".to_string();
1280 assert_eq!(bar.get_navigation_url(), "https://example.com");
1281
1282 bar.text = "search terms".to_string();
1283 assert!(bar.get_navigation_url().starts_with("veridian://search?q="));
1284 }
1285
1286 #[test]
1287 fn test_address_bar_home_end() {
1288 let mut bar = AddressBar::new();
1289 bar.text = "hello".to_string();
1290 bar.cursor = 3;
1291 bar.home();
1292 assert_eq!(bar.cursor, 0);
1293 bar.end();
1294 assert_eq!(bar.cursor, 5);
1295 }
1296
1297 #[test]
1298 fn test_nav_bar_button_at() {
1299 let bar = NavigationBar::new();
1300 assert_eq!(bar.button_at(0), Some(NavButton::Back));
1301 assert_eq!(bar.button_at(32), Some(NavButton::Forward));
1302 assert_eq!(bar.button_at(64), Some(NavButton::Reload));
1303 assert_eq!(bar.button_at(96), Some(NavButton::Home));
1304 }
1305
1306 #[test]
1307 fn test_nav_bar_total_width() {
1308 let bar = NavigationBar::new();
1309 assert_eq!(bar.total_width(), 4 * 28 + 3 * 4); }
1311
1312 #[test]
1313 fn test_extract_origin() {
1314 assert_eq!(
1315 extract_origin("https://example.com/path"),
1316 Some("https://example.com".to_string())
1317 );
1318 assert_eq!(
1319 extract_origin("http://test.org"),
1320 Some("http://test.org".to_string())
1321 );
1322 assert_eq!(
1323 extract_origin("veridian://newtab"),
1324 Some("veridian://newtab".to_string())
1325 );
1326 assert_eq!(extract_origin("ftp://nope"), None);
1327 }
1328
1329 #[test]
1330 fn test_url_encode() {
1331 assert_eq!(url_encode("hello world"), "hello+world");
1332 assert_eq!(url_encode("a&b"), "a%26b");
1333 assert_eq!(url_encode("test"), "test");
1334 }
1335
1336 #[test]
1337 fn test_internal_page_generation() {
1338 let html = generate_internal_page("veridian://newtab");
1339 assert!(html.contains("VeridianOS Browser"));
1340 let html = generate_internal_page("veridian://about");
1341 assert!(html.contains("About"));
1342 let html = generate_internal_page("veridian://unknown");
1343 assert!(html.contains("Not Found"));
1344 }
1345
1346 #[test]
1347 fn test_internal_page_title() {
1348 assert_eq!(internal_page_title("veridian://newtab"), "New Tab");
1349 assert_eq!(internal_page_title("veridian://settings"), "Settings");
1350 assert_eq!(internal_page_title("veridian://xyz"), "Not Found");
1351 }
1352
1353 #[test]
1354 fn test_shell_command_open() {
1355 let mut browser = Browser::default();
1356 let result = handle_shell_command(&mut browser, &["open", "https://test.com"]);
1357 assert!(result.contains("Opened tab"));
1358 assert_eq!(browser.tab_count(), 1);
1359 }
1360
1361 #[test]
1362 fn test_shell_command_tabs() {
1363 let mut browser = Browser::default();
1364 browser.open_url("https://a.com");
1365 browser.open_url("https://b.com");
1366 let result = handle_shell_command(&mut browser, &["tabs"]);
1367 assert!(result.contains("2 tab(s)"));
1368 }
1369
1370 #[test]
1371 fn test_shell_command_close() {
1372 let mut browser = Browser::default();
1373 let id = browser.open_url("https://a.com").unwrap();
1374 browser.open_url("https://b.com");
1375 let result = handle_shell_command(&mut browser, &["close", &format!("{}", id)]);
1376 assert!(result.contains("Closed"));
1377 assert_eq!(browser.tab_count(), 1);
1378 }
1379
1380 #[test]
1381 fn test_shell_command_quit() {
1382 let mut browser = Browser::default();
1383 browser.open_url("https://a.com");
1384 let result = handle_shell_command(&mut browser, &["quit"]);
1385 assert_eq!(result, "Browser closed");
1386 assert!(!browser.is_running());
1387 }
1388
1389 #[test]
1390 fn test_shell_command_unknown() {
1391 let mut browser = Browser::default();
1392 let result = handle_shell_command(&mut browser, &["xyz"]);
1393 assert!(result.contains("Unknown"));
1394 }
1395
1396 #[test]
1397 fn test_shell_command_empty() {
1398 let mut browser = Browser::default();
1399 let result = handle_shell_command(&mut browser, &[]);
1400 assert!(result.contains("Usage"));
1401 }
1402
1403 #[test]
1404 fn test_browser_dimensions() {
1405 let browser = Browser::new(BrowserConfig {
1406 viewport_width: 800,
1407 viewport_height: 600,
1408 ..BrowserConfig::default()
1409 });
1410 assert_eq!(browser.dimensions(), (800, 600));
1411 }
1412
1413 #[test]
1414 fn test_address_bar_delete() {
1415 let mut bar = AddressBar::new();
1416 bar.text = "abc".to_string();
1417 bar.cursor = 1;
1418 bar.delete();
1419 assert_eq!(bar.text, "ac");
1420 }
1421}