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

veridian_kernel/browser/
window.rs

1//! Browser Window
2//!
3//! Top-level browser window that ties together URL navigation,
4//! HTML parsing, CSS styling, layout, and painting. Manages the
5//! address bar, viewport, scroll position, and history stacks.
6
7#![allow(dead_code)]
8
9use alloc::{
10    string::{String, ToString},
11    vec::Vec,
12};
13
14use super::{
15    css_parser::{CssParser, Stylesheet},
16    dom::Document,
17    layout::{build_layout_tree, LayoutBox},
18    paint::{build_display_list, DisplayList, Painter},
19    style::StyleResolver,
20    tree_builder::TreeBuilder,
21};
22
23/// Browser window state
24pub struct BrowserWindow {
25    /// Text in the address bar
26    pub address_bar: String,
27    /// Currently loaded URL
28    pub current_url: String,
29    /// Parsed DOM document
30    pub document: Option<Document>,
31    /// Author stylesheet
32    pub stylesheet: Option<Stylesheet>,
33    /// Layout tree root
34    pub layout_root: Option<LayoutBox>,
35    /// Current display list
36    pub display_list: Option<DisplayList>,
37    /// Vertical scroll offset (pixels)
38    pub scroll_y: i32,
39    /// Viewport width (pixels)
40    pub viewport_width: i32,
41    /// Viewport height (pixels)
42    pub viewport_height: i32,
43    /// Back navigation history
44    pub back_history: Vec<String>,
45    /// Forward navigation history
46    pub forward_history: Vec<String>,
47    /// Style resolver
48    resolver: StyleResolver,
49    /// Painter for rendering
50    painter: Option<Painter>,
51}
52
53impl Default for BrowserWindow {
54    fn default() -> Self {
55        Self::new(800, 600)
56    }
57}
58
59impl BrowserWindow {
60    /// Create a new browser window with given viewport dimensions
61    pub fn new(width: i32, height: i32) -> Self {
62        Self {
63            address_bar: String::new(),
64            current_url: String::new(),
65            document: None,
66            stylesheet: None,
67            layout_root: None,
68            display_list: None,
69            scroll_y: 0,
70            viewport_width: width,
71            viewport_height: height,
72            back_history: Vec::new(),
73            forward_history: Vec::new(),
74            resolver: StyleResolver::new(),
75            painter: None,
76        }
77    }
78
79    /// Navigate to a URL string
80    pub fn navigate(&mut self, url: &str) {
81        // Save current URL in history
82        if !self.current_url.is_empty() {
83            self.back_history.push(self.current_url.clone());
84        }
85        self.forward_history.clear();
86
87        self.current_url = url.to_string();
88        self.address_bar = url.to_string();
89        self.scroll_y = 0;
90
91        // Fetch content (placeholder - would use net::http in real implementation)
92        let html = self.fetch_url(url);
93        self.load_html(&html);
94    }
95
96    /// Load raw HTML content directly
97    pub fn load_html(&mut self, html: &str) {
98        // Parse HTML
99        let doc = TreeBuilder::build(html);
100
101        // Extract inline styles from <style> elements
102        let css = Self::extract_styles(html);
103
104        // Parse CSS
105        let stylesheet = CssParser::parse(&css);
106
107        // Set up resolver
108        self.resolver = StyleResolver::new();
109        self.resolver.add_stylesheet(stylesheet.clone());
110
111        // Build layout tree
112        let layout = build_layout_tree(
113            &doc,
114            &self.resolver,
115            self.viewport_width,
116            self.viewport_height,
117        );
118
119        // Build display list
120        let display_list = build_display_list(&layout, self.scroll_y);
121
122        self.document = Some(doc);
123        self.stylesheet = Some(stylesheet);
124        self.layout_root = Some(layout);
125        self.display_list = Some(display_list);
126    }
127
128    /// Load HTML with a separate CSS string
129    pub fn load_html_with_css(&mut self, html: &str, css: &str) {
130        let doc = TreeBuilder::build(html);
131        let stylesheet = CssParser::parse(css);
132
133        self.resolver = StyleResolver::new();
134        self.resolver.add_stylesheet(stylesheet.clone());
135
136        let layout = build_layout_tree(
137            &doc,
138            &self.resolver,
139            self.viewport_width,
140            self.viewport_height,
141        );
142
143        let display_list = build_display_list(&layout, self.scroll_y);
144
145        self.document = Some(doc);
146        self.stylesheet = Some(stylesheet);
147        self.layout_root = Some(layout);
148        self.display_list = Some(display_list);
149    }
150
151    /// Render the current page to a pixel buffer
152    pub fn render(&mut self) -> &[u32] {
153        let mut painter = Painter::new(self.viewport_width as usize, self.viewport_height as usize);
154
155        if let Some(ref dl) = self.display_list {
156            painter.paint(dl);
157        }
158
159        self.painter = Some(painter);
160        self.painter.as_ref().unwrap().as_bytes()
161    }
162
163    /// Handle scrolling
164    pub fn handle_scroll(&mut self, delta_y: i32) {
165        self.scroll_y += delta_y;
166        if self.scroll_y < 0 {
167            self.scroll_y = 0;
168        }
169
170        // Cap scroll at content height minus viewport
171        if let Some(ref layout) = self.layout_root {
172            let content_height = super::css_parser::fp_to_px(
173                layout.dimensions.content.height
174                    + layout.dimensions.padding.top
175                    + layout.dimensions.padding.bottom,
176            );
177            let max_scroll = content_height - self.viewport_height;
178            if max_scroll > 0 && self.scroll_y > max_scroll {
179                self.scroll_y = max_scroll;
180            }
181        }
182
183        // Rebuild display list with new scroll
184        self.rebuild_display_list();
185    }
186
187    /// Handle viewport resize
188    pub fn handle_resize(&mut self, width: i32, height: i32) {
189        self.viewport_width = width;
190        self.viewport_height = height;
191        self.relayout();
192    }
193
194    /// Go back in history
195    pub fn go_back(&mut self) -> bool {
196        if let Some(url) = self.back_history.pop() {
197            self.forward_history.push(self.current_url.clone());
198            let url_clone = url.clone();
199            self.current_url = url;
200            self.address_bar = url_clone.clone();
201            let html = self.fetch_url(&url_clone);
202            self.load_html(&html);
203            true
204        } else {
205            false
206        }
207    }
208
209    /// Go forward in history
210    pub fn go_forward(&mut self) -> bool {
211        if let Some(url) = self.forward_history.pop() {
212            self.back_history.push(self.current_url.clone());
213            let url_clone = url.clone();
214            self.current_url = url;
215            self.address_bar = url_clone.clone();
216            let html = self.fetch_url(&url_clone);
217            self.load_html(&html);
218            true
219        } else {
220            false
221        }
222    }
223
224    /// Reload the current page
225    pub fn reload(&mut self) {
226        if !self.current_url.is_empty() {
227            let url = self.current_url.clone();
228            let html = self.fetch_url(&url);
229            self.load_html(&html);
230        }
231    }
232
233    /// Set the address bar text
234    pub fn set_url(&mut self, url: &str) {
235        self.address_bar = url.to_string();
236    }
237
238    /// Get the current URL
239    pub fn get_url(&self) -> &str {
240        &self.current_url
241    }
242
243    /// Get the address bar text
244    pub fn get_address_bar(&self) -> &str {
245        &self.address_bar
246    }
247
248    /// Handle text input in address bar
249    pub fn address_bar_input(&mut self, ch: char) {
250        self.address_bar.push(ch);
251    }
252
253    /// Handle backspace in address bar
254    pub fn address_bar_backspace(&mut self) {
255        self.address_bar.pop();
256    }
257
258    /// Submit the address bar (navigate to typed URL)
259    pub fn address_bar_submit(&mut self) {
260        let url = self.address_bar.clone();
261        self.navigate(&url);
262    }
263
264    /// Check if we can go back
265    pub fn can_go_back(&self) -> bool {
266        !self.back_history.is_empty()
267    }
268
269    /// Check if we can go forward
270    pub fn can_go_forward(&self) -> bool {
271        !self.forward_history.is_empty()
272    }
273
274    /// Get the page title (from <title> element)
275    pub fn get_title(&self) -> String {
276        if let Some(ref doc) = self.document {
277            let titles = doc.get_elements_by_tag_name("title");
278            if let Some(&title_id) = titles.first() {
279                return doc.inner_text(title_id);
280            }
281        }
282        String::from("Untitled")
283    }
284
285    /// Get the document node count
286    pub fn node_count(&self) -> usize {
287        self.document.as_ref().map(|d| d.node_count()).unwrap_or(0)
288    }
289
290    /// Fetch URL content (placeholder for network integration)
291    fn fetch_url(&self, url: &str) -> String {
292        // In a real implementation, this would use crate::net::http
293        // For now, return a placeholder page
294        if url.starts_with("about:blank") {
295            return String::new();
296        }
297
298        if url.starts_with("about:") {
299            return alloc::format!(
300                "<!DOCTYPE html><html><head><title>{0}</title></head><body><h1>{0}</\
301                 h1><p>Internal page</p></body></html>",
302                url
303            );
304        }
305
306        // Default: show a "page not found" since we can't actually fetch
307        alloc::format!(
308            "<!DOCTYPE html><html><head><title>Error</title></head><body><h1>Page Not \
309             Found</h1><p>Could not load: {}</p></body></html>",
310            url
311        )
312    }
313
314    /// Extract CSS from <style> tags in HTML
315    fn extract_styles(html: &str) -> String {
316        let mut css = String::new();
317        let mut pos = 0;
318        let bytes = html.as_bytes();
319
320        while pos < bytes.len() {
321            // Find <style
322            if let Some(start) = find_tag_start(bytes, pos, b"<style") {
323                // Find >
324                let mut tag_end = start + 6;
325                while tag_end < bytes.len() && bytes[tag_end] != b'>' {
326                    tag_end += 1;
327                }
328                tag_end += 1; // skip '>'
329
330                // Find </style>
331                if let Some(end) = find_tag_start(bytes, tag_end, b"</style") {
332                    let style_content = &html[tag_end..end];
333                    css.push_str(style_content);
334                    css.push('\n');
335                    pos = end + 8;
336                } else {
337                    pos = tag_end;
338                }
339            } else {
340                break;
341            }
342        }
343
344        css
345    }
346
347    /// Re-layout the current document
348    fn relayout(&mut self) {
349        if let Some(ref doc) = self.document {
350            let layout = build_layout_tree(
351                doc,
352                &self.resolver,
353                self.viewport_width,
354                self.viewport_height,
355            );
356            let display_list = build_display_list(&layout, self.scroll_y);
357            self.layout_root = Some(layout);
358            self.display_list = Some(display_list);
359        }
360    }
361
362    /// Rebuild the display list (e.g., after scroll)
363    fn rebuild_display_list(&mut self) {
364        if let Some(ref layout) = self.layout_root {
365            let display_list = build_display_list(layout, self.scroll_y);
366            self.display_list = Some(display_list);
367        }
368    }
369}
370
371/// Find a tag in a byte slice (case-insensitive)
372fn find_tag_start(bytes: &[u8], start: usize, tag: &[u8]) -> Option<usize> {
373    if tag.is_empty() || start + tag.len() > bytes.len() {
374        return None;
375    }
376
377    for i in start..=(bytes.len() - tag.len()) {
378        let mut matched = true;
379        for (j, &t) in tag.iter().enumerate() {
380            if !bytes[i + j].eq_ignore_ascii_case(&t) {
381                matched = false;
382                break;
383            }
384        }
385        if matched {
386            return Some(i);
387        }
388    }
389    None
390}
391
392#[cfg(test)]
393mod tests {
394    #[allow(unused_imports)]
395    use alloc::vec;
396
397    use super::*;
398
399    #[test]
400    fn test_new_window() {
401        let w = BrowserWindow::new(800, 600);
402        assert_eq!(w.viewport_width, 800);
403        assert_eq!(w.viewport_height, 600);
404        assert_eq!(w.scroll_y, 0);
405    }
406
407    #[test]
408    fn test_default_window() {
409        let w = BrowserWindow::default();
410        assert_eq!(w.viewport_width, 800);
411    }
412
413    #[test]
414    fn test_load_html() {
415        let mut w = BrowserWindow::new(800, 600);
416        w.load_html("<p>Hello</p>");
417        assert!(w.document.is_some());
418        assert!(w.layout_root.is_some());
419    }
420
421    #[test]
422    fn test_load_html_with_css() {
423        let mut w = BrowserWindow::new(800, 600);
424        w.load_html_with_css("<p>Hello</p>", "p { color: red; }");
425        assert!(w.document.is_some());
426        assert!(w.stylesheet.is_some());
427    }
428
429    #[test]
430    fn test_navigate() {
431        let mut w = BrowserWindow::new(800, 600);
432        w.navigate("about:test");
433        assert_eq!(w.current_url, "about:test");
434        assert!(w.document.is_some());
435    }
436
437    #[test]
438    fn test_navigate_about_blank() {
439        let mut w = BrowserWindow::new(800, 600);
440        w.navigate("about:blank");
441        assert_eq!(w.current_url, "about:blank");
442    }
443
444    #[test]
445    fn test_history_back_forward() {
446        let mut w = BrowserWindow::new(800, 600);
447        w.navigate("about:page1");
448        w.navigate("about:page2");
449        assert!(w.can_go_back());
450        assert!(!w.can_go_forward());
451
452        w.go_back();
453        assert_eq!(w.current_url, "about:page1");
454        assert!(w.can_go_forward());
455
456        w.go_forward();
457        assert_eq!(w.current_url, "about:page2");
458    }
459
460    #[test]
461    fn test_no_back_when_empty() {
462        let mut w = BrowserWindow::new(800, 600);
463        assert!(!w.can_go_back());
464        assert!(!w.go_back());
465    }
466
467    #[test]
468    fn test_reload() {
469        let mut w = BrowserWindow::new(800, 600);
470        w.navigate("about:test");
471        w.reload();
472        assert_eq!(w.current_url, "about:test");
473    }
474
475    #[test]
476    fn test_scroll() {
477        let mut w = BrowserWindow::new(800, 600);
478        w.load_html("<p>Hello</p>");
479        w.handle_scroll(100);
480        assert!(w.scroll_y >= 0);
481    }
482
483    #[test]
484    fn test_scroll_negative_clamped() {
485        let mut w = BrowserWindow::new(800, 600);
486        w.load_html("<p>Hello</p>");
487        w.handle_scroll(-100);
488        assert_eq!(w.scroll_y, 0);
489    }
490
491    #[test]
492    fn test_resize() {
493        let mut w = BrowserWindow::new(800, 600);
494        w.load_html("<p>Hello</p>");
495        w.handle_resize(1024, 768);
496        assert_eq!(w.viewport_width, 1024);
497        assert_eq!(w.viewport_height, 768);
498    }
499
500    #[test]
501    fn test_get_title() {
502        let mut w = BrowserWindow::new(800, 600);
503        w.load_html("<html><head><title>My Page</title></head><body></body></html>");
504        assert_eq!(w.get_title(), "My Page");
505    }
506
507    #[test]
508    fn test_get_title_default() {
509        let w = BrowserWindow::new(800, 600);
510        assert_eq!(w.get_title(), "Untitled");
511    }
512
513    #[test]
514    fn test_address_bar() {
515        let mut w = BrowserWindow::new(800, 600);
516        w.set_url("https://example.com");
517        assert_eq!(w.get_address_bar(), "https://example.com");
518    }
519
520    #[test]
521    fn test_address_bar_input() {
522        let mut w = BrowserWindow::new(800, 600);
523        w.address_bar_input('h');
524        w.address_bar_input('i');
525        assert_eq!(w.get_address_bar(), "hi");
526    }
527
528    #[test]
529    fn test_address_bar_backspace() {
530        let mut w = BrowserWindow::new(800, 600);
531        w.set_url("abc");
532        w.address_bar_backspace();
533        assert_eq!(w.get_address_bar(), "ab");
534    }
535
536    #[test]
537    fn test_node_count() {
538        let mut w = BrowserWindow::new(800, 600);
539        assert_eq!(w.node_count(), 0);
540        w.load_html("<p>Hello</p>");
541        assert!(w.node_count() > 0);
542    }
543
544    #[test]
545    fn test_extract_styles() {
546        let css = BrowserWindow::extract_styles(
547            "<html><head><style>p { color: red; }</style></head><body></body></html>",
548        );
549        assert!(css.contains("color: red"));
550    }
551
552    #[test]
553    fn test_extract_no_styles() {
554        let css = BrowserWindow::extract_styles("<html><body></body></html>");
555        assert!(css.is_empty());
556    }
557
558    #[test]
559    fn test_find_tag_start() {
560        let bytes = b"hello <style>content</style>";
561        let result = find_tag_start(bytes, 0, b"<style");
562        assert_eq!(result, Some(6));
563    }
564
565    #[test]
566    fn test_find_tag_not_found() {
567        let bytes = b"hello world";
568        assert!(find_tag_start(bytes, 0, b"<style").is_none());
569    }
570}