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

veridian_kernel/browser/
browser_main.rs

1//! Browser Main Module
2//!
3//! Top-level browser struct that ties together the tab manager, process
4//! isolation, rendering, navigation, and shell command integration.
5//! Provides the public API for creating, driving, and rendering the
6//! browser from the VeridianOS desktop environment.
7
8#![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// ---------------------------------------------------------------------------
23// Browser configuration
24// ---------------------------------------------------------------------------
25
26/// Browser configuration
27#[derive(Debug, Clone)]
28pub struct BrowserConfig {
29    /// Default home page URL
30    pub home_page: String,
31    /// Viewport width in pixels
32    pub viewport_width: u32,
33    /// Viewport height in pixels
34    pub viewport_height: u32,
35    /// Tab bar height in pixels
36    pub tab_bar_height: u32,
37    /// Address bar height in pixels
38    pub address_bar_height: u32,
39    /// Whether to show the address bar
40    pub show_address_bar: bool,
41    /// Whether to show navigation buttons
42    pub show_nav_buttons: bool,
43    /// Maximum tabs
44    pub max_tabs: usize,
45    /// Enable JavaScript
46    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// ---------------------------------------------------------------------------
66// Address bar state
67// ---------------------------------------------------------------------------
68
69/// Address bar editing state
70#[derive(Debug, Clone)]
71pub struct AddressBar {
72    /// Current text content
73    pub text: String,
74    /// Cursor position (byte offset)
75    pub cursor: usize,
76    /// Whether the address bar is focused (editing)
77    pub focused: bool,
78    /// Selection start (if any)
79    pub selection_start: Option<usize>,
80    /// Autocomplete suggestions
81    pub suggestions: Vec<String>,
82    /// Whether suggestions are visible
83    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    /// Set the URL text without triggering navigation
105    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    /// Focus the address bar and select all text
113    pub fn focus(&mut self) {
114        self.focused = true;
115        self.selection_start = Some(0);
116        self.cursor = self.text.len();
117    }
118
119    /// Unfocus the address bar
120    pub fn unfocus(&mut self) {
121        self.focused = false;
122        self.selection_start = None;
123        self.showing_suggestions = false;
124    }
125
126    /// Insert a character at cursor position
127    pub fn insert_char(&mut self, ch: char) {
128        // If there's a selection, delete it first
129        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    /// Delete character before cursor (backspace)
144    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    /// Delete character after cursor
158    pub fn delete(&mut self) {
159        if self.cursor < self.text.len() {
160            self.text.remove(self.cursor);
161        }
162    }
163
164    /// Move cursor left
165    pub fn move_left(&mut self) {
166        if self.cursor > 0 {
167            self.cursor -= 1;
168        }
169        self.selection_start = None;
170    }
171
172    /// Move cursor right
173    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    /// Move cursor to start
181    pub fn home(&mut self) {
182        self.cursor = 0;
183        self.selection_start = None;
184    }
185
186    /// Move cursor to end
187    pub fn end(&mut self) {
188        self.cursor = self.text.len();
189        self.selection_start = None;
190    }
191
192    /// Get the current text, potentially normalizing it as a URL
193    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 it looks like a URL, use it directly
199        if trimmed.starts_with("http://")
200            || trimmed.starts_with("https://")
201            || trimmed.starts_with("veridian://")
202        {
203            return trimmed.to_string();
204        }
205        // If it looks like a domain (contains a dot), add https://
206        if trimmed.contains('.') && !trimmed.contains(' ') {
207            return format!("https://{}", trimmed);
208        }
209        // Otherwise, treat as a search query (placeholder URL)
210        format!("veridian://search?q={}", url_encode(trimmed))
211    }
212}
213
214// ---------------------------------------------------------------------------
215// Navigation button state
216// ---------------------------------------------------------------------------
217
218/// Navigation button identifiers
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
220pub enum NavButton {
221    Back,
222    Forward,
223    Reload,
224    Home,
225    Stop,
226}
227
228/// Navigation bar with buttons
229pub struct NavigationBar {
230    /// Button width in pixels
231    pub button_width: u32,
232    /// Button height in pixels
233    pub button_height: u32,
234    /// Spacing between buttons
235    pub spacing: u32,
236    /// Which button is hovered
237    pub hovered: Option<NavButton>,
238    /// Which button is pressed
239    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    /// Total width of the navigation buttons area
260    pub fn total_width(&self) -> u32 {
261        // 4 buttons (back, forward, reload, home) + spacing
262        4 * self.button_width + 3 * self.spacing
263    }
264
265    /// Hit test: which button is at this x position?
266    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; // in spacing gap
272        }
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    /// Render navigation buttons into a pixel buffer row
283    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 // disabled
318            } 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            // Draw button background
333            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            // Draw label (centered, single char)
344            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            // Tiny glyph approximation
350            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; // used above symbolically
360        }
361    }
362}
363
364// ---------------------------------------------------------------------------
365// Browser
366// ---------------------------------------------------------------------------
367
368/// The main browser struct, integrating tabs, isolation, and rendering
369pub struct Browser {
370    /// Configuration
371    pub config: BrowserConfig,
372    /// Tab manager
373    pub tabs: TabManager,
374    /// Process isolation
375    pub isolation: ProcessIsolation,
376    /// Address bar state
377    pub address_bar: AddressBar,
378    /// Navigation bar
379    pub nav_bar: NavigationBar,
380    /// Pixel buffer for the entire browser window (BGRA u32)
381    pub framebuffer: Vec<u32>,
382    /// Whether the browser needs a repaint
383    pub needs_repaint: bool,
384    /// History of visited URLs (global, for autocomplete)
385    pub history: Vec<String>,
386    /// Maximum history entries
387    pub max_history: usize,
388    /// Whether the browser is running
389    pub running: bool,
390    /// Status bar text
391    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    /// Create a new browser with the given configuration
402    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    /// Initialize the browser: open the home page in a new tab
420    pub fn init(&mut self) {
421        let url = self.config.home_page.clone();
422        self.open_url(&url);
423    }
424
425    /// Open a URL in a new tab (or the current tab if it's blank)
426    pub fn open_url(&mut self, url: &str) -> Option<TabId> {
427        // If current tab is blank "New Tab", navigate it instead of creating new
428        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        // Create new tab
437        let tab_id = self.tabs.new_tab(url)?;
438
439        // Spawn isolated process
440        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        // Set origin
448        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    /// Navigate the active tab to a URL
460    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    /// Navigate a specific tab to a URL
467    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        // Update origin for process
473        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        // Add to history
480        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        // Load the page content (simplified: handle veridian:// pages inline)
488        self.load_page(tab_id, url);
489        self.update_address_bar();
490        self.needs_repaint = true;
491    }
492
493    /// Handle the "load" of a page (in a real browser, this would be async)
494    fn load_page(&mut self, tab_id: TabId, url: &str) {
495        if url.starts_with("veridian://") {
496            // Internal pages
497            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            // Execute any scripts in the page
504            if let Some(proc) = self.isolation.get_process_mut(tab_id) {
505                // Parse and set up DOM from HTML (simplified)
506                let _doc = proc.dom_api.create_element("html");
507                // Process script tags
508                use super::js_integration::ScriptEngine;
509                let mut engine = ScriptEngine::new();
510                engine.process_script_tags(&page_html);
511            }
512        } else {
513            // External pages would be fetched via network (stub)
514            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    /// Handle keyboard input
522    pub fn handle_key(&mut self, scancode: u8, ctrl: bool, shift: bool, alt: bool) {
523        // Address bar focused: route to address bar
524        if self.address_bar.focused {
525            match scancode {
526                0x0D => {
527                    // Enter: navigate
528                    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                    // Escape: cancel editing
536                    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(), // left arrow
542                0x81 => self.address_bar.move_right(), // right arrow
543                _ => {
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        // Tab shortcuts (Ctrl+key)
554        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                    // Ctrl+L: focus address bar
584                    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        // Alt+Left/Right for back/forward
594        if alt {
595            match scancode {
596                0x80 => self.go_back(),    // Alt+Left
597                0x81 => self.go_forward(), // Alt+Right
598                _ => {}
599            }
600            self.needs_repaint = true;
601            return;
602        }
603
604        // Pass key to active tab's page
605        // (In a real browser, this goes to the focused element)
606        let _ = (scancode, shift);
607        self.needs_repaint = true;
608    }
609
610    /// Handle mouse click at (x, y) relative to browser window
611    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        // Tab bar area
620        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        // Address bar area
636        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                // Navigation button click
645                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                // Address bar click
656                self.address_bar.focus();
657            }
658            self.needs_repaint = true;
659            return;
660        }
661
662        // Content area: forward click to active tab's page
663        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                // Forward to event system
667                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    /// Go back in the active tab
677    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    /// Go forward in the active tab
689    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    /// Reload the active tab
701    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    /// Navigate to home page
712    pub fn go_home(&mut self) {
713        let home = self.config.home_page.clone();
714        self.navigate_active(&home);
715    }
716
717    /// Stop loading the active tab
718    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    /// Close a tab
726    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    /// Update the address bar to reflect the active tab's URL
734    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    /// Render the entire browser to the framebuffer
745    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        // Ensure framebuffer size
755        if self.framebuffer.len() != fb_len {
756            self.framebuffer.resize(fb_len, 0xFF2D2D30);
757        }
758
759        // Clear
760        for pixel in self.framebuffer.iter_mut() {
761            *pixel = 0xFF2D2D30;
762        }
763
764        // 1. Render tab bar
765        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        // 2. Render address bar
773        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            // Background
779            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            // Navigation buttons
789            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            // Address bar text field
807            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            // Text field background
816            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            // Render URL text (simplified)
832            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            // Cursor
847            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        // 3. Render content area (placeholder: show tab info)
859        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; // white background for content
868            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            // Show page title and URL
878            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    /// Tick the browser (advance timers, GC, etc.)
905    pub fn tick(&mut self) {
906        self.tabs.tick();
907        self.isolation.tick_all();
908    }
909
910    /// Get the framebuffer for display
911    pub fn framebuffer(&self) -> &[u32] {
912        &self.framebuffer
913    }
914
915    /// Get browser dimensions
916    pub fn dimensions(&self) -> (u32, u32) {
917        (self.config.viewport_width, self.config.viewport_height)
918    }
919
920    /// Number of open tabs
921    pub fn tab_count(&self) -> usize {
922        self.tabs.tab_count()
923    }
924
925    /// Whether the browser is still running
926    pub fn is_running(&self) -> bool {
927        self.running
928    }
929
930    /// Quit the browser
931    pub fn quit(&mut self) {
932        // Close all tabs
933        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
941// ---------------------------------------------------------------------------
942// Shell command integration
943// ---------------------------------------------------------------------------
944
945/// Execute a browser command from the shell (e.g., `browser open https://...`)
946pub 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
1010// ---------------------------------------------------------------------------
1011// Internal pages
1012// ---------------------------------------------------------------------------
1013
1014/// Generate HTML for internal veridian:// pages
1015fn 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
1041/// Get the title for an internal page
1042fn 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
1053// ---------------------------------------------------------------------------
1054// Helpers
1055// ---------------------------------------------------------------------------
1056
1057/// Extract the origin (scheme + host) from a URL
1058fn extract_origin(url: &str) -> Option<String> {
1059    // Find scheme
1060    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
1078/// Simple URL encoding for search queries
1079fn 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
1105/// Simplified text rendering (placeholder for real font system)
1106fn 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            // Draw a simple dot pattern for each character
1118            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// ---------------------------------------------------------------------------
1132// Tests
1133// ---------------------------------------------------------------------------
1134
1135#[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        // Should not crash
1234    }
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); // 124
1310    }
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}