1#![allow(dead_code)]
12
13use crate::kernel::draw;
14use crate::os_lib::web_engine::{draw_border, WebEngine};
15use alloc::string::String;
16use alloc::vec::Vec;
17
18const CONTROL_BAR_H: i32 = 40;
21
22struct TabSlot {
28 title: String,
29 engine: Option<WebEngine>,
30}
31
32static SVG_BACK: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M12.5 16.5L6 10L12.5 3.5\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>";
33
34static SVG_FORWARD: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M7.5 3.5L14 10L7.5 16.5\" stroke=\"currentColor\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>";
35
36static SVG_HOME: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10 2.5L3.5 8V16.5C3.5 17 4 17.5 4.5 17.5H7.5V12.5H12.5V17.5H15.5C16 17.5 16.5 17 16.5 16.5V8L10 2.5Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"/></svg>";
37
38static SVG_SHIELD_SECURE: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10 2.5C13 3.5 16.5 3.5 16.5 3.5V8.5C16.5 13 13 16.5 10 17.5C7 16.5 3.5 13 3.5 8.5V3.5C3.5 3.5 7 3.5 10 2.5Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"/><path d=\"M7 10L9 12L13 8\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>";
39
40static SVG_SHIELD_WARN: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M10 2.5C13 3.5 16.5 3.5 16.5 3.5V8.5C16.5 13 13 16.5 10 17.5C7 16.5 3.5 13 3.5 8.5V3.5C3.5 3.5 7 3.5 10 2.5Z\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"/><path d=\"M4.5 4.5L15.5 15.5\" stroke=\"currentColor\" stroke-width=\"2\"/></svg>";
41
42static SVG_RELOAD: &[u8] = b"<svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M17 10C17 13.866 13.866 17 10 17C6.13401 17 3 13.866 3 10C3 6.13401 6.13401 3 10 3C12.186 3 14.136 4.003 15.42 5.58M17 3V6H14\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg>";
43
44fn render_home(engine: &mut WebEngine, top: i32) {
46 engine.current_is_https = false;
47 engine.load_html(crate::os_lib::web_engine::HOME_HTML, "home", top);
48 engine.title = String::from("AtmOS Browser");
49}
50
51pub struct WebBrowser {
53 pub engine: WebEngine,
55 pub address_bar_focused: bool,
56 pub address_box: crate::kernel::text_box::TextBox,
57 pub active_tab_idx: usize,
58 tabs: Vec<TabSlot>,
59 pub omnibox: crate::os_lib::web_engine::omnibox::OmniboxEngine,
60 pub omnibox_items: Vec<crate::os_lib::web_engine::omnibox::OmniboxItem>,
61 pub selected_suggestion: Option<usize>,
62}
63
64static REDRAW_LOADING: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
67static REDRAW_DIRTY: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
68static REDRAW_VIDEO: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
69static REDRAW_IMG: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
70
71pub fn log_redraw_reasons() {
73}
74
75impl WebBrowser {
76 pub fn execute_search_or_navigate(&mut self, text: &str) {
77 let input = text.trim();
78 if input.is_empty() {
79 return;
80 }
81 let top = self.top_offset();
82 if let Some(rest) = input.strip_prefix('/') {
83 let loaded = {
84 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
85 crate::kernel::fs::get_fs().read_file(input)
86 };
87 if let Some((_meta, content)) = loaded {
88 if let Ok(html) = core::str::from_utf8(&content) {
89 self.engine.load_html(html, rest, top);
90 }
91 }
92 } else if input.starts_with("http://") || input.starts_with("https://") {
93 let is_https = input.starts_with("https://");
94 let clean = if is_https {
95 input.get(8..).unwrap_or("")
96 } else {
97 input.get(7..).unwrap_or("")
98 };
99 let parts: Vec<&str> = clean.splitn(2, '/').collect();
100 let host = parts[0];
101 let path = if parts.len() > 1 {
102 alloc::format!("/{}", parts[1])
103 } else {
104 String::from("/")
105 };
106 self.engine.load_page(is_https, host, &path, true, top);
107 } else if input.contains('.') && !input.contains(' ') {
108 let parts: Vec<&str> = input.splitn(2, '/').collect();
109 let host = parts[0];
110 let path = if parts.len() > 1 {
111 alloc::format!("/{}", parts[1])
112 } else {
113 String::from("/")
114 };
115 let is_https = !host.starts_with("localhost");
116 self.engine.load_page(is_https, host, &path, true, top);
117 } else {
118 let query = crate::os_lib::web_engine::duckduckgo::url_encode(input);
119 let path = alloc::format!("/?q={}", query);
120 self.engine
121 .load_page(true, "duckduckgo.com", &path, true, top);
122 }
123 self.address_bar_focused = false;
124 self.omnibox_items.clear();
125 self.selected_suggestion = None;
126 let final_url = self.engine.get_url();
127 self.omnibox.record_visit(&self.engine.title, &final_url);
128 self.address_box.set_text(&final_url);
129 self.engine.dirty = true;
130 self.engine.dirty_reason = "mod.rs:exec_nav";
131 }
132
133 pub fn top_offset(&self) -> i32 {
136 CONTROL_BAR_H
137 }
138
139 fn sync_active_title(&mut self) {
141 let title = if self.engine.title.is_empty() {
142 self.engine.current_host.clone()
143 } else {
144 self.engine.title.clone()
145 };
146 if let Some(slot) = self.tabs.get_mut(self.active_tab_idx) {
147 slot.title = title;
148 }
149 }
150
151 fn activate(&mut self, idx: usize) {
154 if idx == self.active_tab_idx || idx >= self.tabs.len() {
155 return;
156 }
157 self.sync_active_title();
158 if let Some(mut next) = self.tabs[idx].engine.take() {
160 core::mem::swap(&mut self.engine, &mut next);
162 self.tabs[self.active_tab_idx].engine = Some(next);
164 self.active_tab_idx = idx;
165 }
166 self.engine.active = true;
167 self.engine.dirty = true;
168 self.engine.dirty_reason = "mod.rs:105";
169 self.address_box.set_text(&self.engine.get_url());
170 self.address_bar_focused = false;
171 }
172}
173
174impl Default for WebBrowser {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180impl WebBrowser {
181 pub fn new() -> Self {
182 crate::os_lib::web_engine::clear_browser_cache();
184 let mut engine = WebEngine::new();
185 engine.win_w = 800;
186 engine.win_h = 600;
187 render_home(&mut engine, CONTROL_BAR_H);
189 let initial_url = engine.get_url();
190 let initial_slot = TabSlot {
192 title: String::from("AtmOS Browser"),
193 engine: None,
194 };
195 Self {
196 engine,
197 address_bar_focused: false,
198 address_box: crate::kernel::text_box::TextBox::with_text(&initial_url),
199 active_tab_idx: 0,
200 tabs: alloc::vec![initial_slot],
201 omnibox: crate::os_lib::web_engine::omnibox::OmniboxEngine::new(),
202 omnibox_items: Vec::new(),
203 selected_suggestion: None,
204 }
205 }
206}
207
208static mut GLOBAL_BROWSER: Option<WebBrowser> = None;
210
211pub fn init() {
213 unsafe {
214 GLOBAL_BROWSER = Some(WebBrowser::new());
215 }
216}
217
218pub fn get_instance() -> &'static mut WebBrowser {
219 unsafe { GLOBAL_BROWSER.get_or_insert_with(WebBrowser::new) }
221}
222
223pub fn is_active() -> bool {
224 unsafe {
225 match &GLOBAL_BROWSER {
226 Some(b) => b.engine.active,
227 None => false,
228 }
229 }
230}
231
232pub fn hovered_cursor_is_text() -> bool {
234 unsafe {
235 match &GLOBAL_BROWSER {
236 Some(b) => b.engine.active && b.engine.hovered_cursor_is_text,
237 None => false,
238 }
239 }
240}
241
242pub fn needs_render() -> bool {
243 unsafe {
244 match &GLOBAL_BROWSER {
245 Some(b) => {
246 let r = b.engine.active && (b.engine.loading || b.engine.dirty);
251 if r {
252 if b.engine.loading {
253 RENDER_LOADING.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
254 }
255 if b.engine.dirty {
256 RENDER_DIRTY.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
257 }
258 }
259 r
260 }
261 None => false,
262 }
263 }
264}
265
266static RENDER_LOADING: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
268static RENDER_DIRTY: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
269
270impl crate::kernel::window_mgr::App for WebBrowser {
272 fn name(&self) -> &str {
273 "browser"
274 }
275
276 fn instance_info(&self) -> alloc::string::String {
277 self.engine.title.clone()
278 }
279
280 fn on_resize(&mut self, width: u32, height: u32) {
281 let top = self.top_offset();
282 self.engine.win_w = width;
283 self.engine.win_h = height;
284 let html = self.engine.last_html.clone();
285 self.engine.parse_and_layout(&html, None, top, width, height);
286 let viewport_h = height.saturating_sub(top as u32);
287 self.engine.js_runtime.fire_resize(width as i32, viewport_h as i32);
288 self.engine.dirty = true;
289 self.engine.dirty_reason = "mod.rs:202";
290 }
291
292 fn needs_timer_redraw(&self) -> bool {
293 let loading = self.engine.loading;
299 let dirty = self.engine.dirty;
300 let video = self.engine.video_playing;
301 let img = crate::os_lib::web_engine::image_results_ready();
302 let canvas = crate::os_lib::canvas2d::canvas_dirty();
305 let font_wait = crate::os_lib::web_engine::font_pending() > 0;
319 if loading {
320 REDRAW_LOADING.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
321 }
322 if dirty {
323 REDRAW_DIRTY.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
324 }
326 if video {
327 REDRAW_VIDEO.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
328 }
329 if img {
330 REDRAW_IMG.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
331 }
332 let showing_placeholder = loading && self.engine.elements.is_empty();
346 let timers = self.engine.js_runtime.has_due_timers();
348 let frames = self.engine.frames_want_redraw() || self.engine.has_pending_dynamic_scripts();
350 let dom_pending = self.engine.js_runtime.dom.borrow().dirty;
352 showing_placeholder || dirty || video || img || canvas || font_wait || timers || frames || dom_pending
353 }
354
355 fn draw(
357 &mut self,
358 screen: &crate::kernel::draw::Screen,
359 win_x: u32,
360 win_y: u32,
361 win_w: u32,
362 win_h: u32,
363 ) {
364 crate::os_lib::web_engine::set_draw_stage(1);
365 let _ = crate::os_lib::canvas2d::take_canvas_dirty();
368 let top = self.top_offset();
369 if self.engine.win_w != win_w || self.engine.win_h != win_h || self.engine.top_offset != top
371 {
372 let size_changed = self.engine.win_w != win_w || self.engine.win_h != win_h;
374 self.engine.win_w = win_w;
375 self.engine.win_h = win_h;
376 let html = self.engine.last_html.clone();
377 self.engine.parse_and_layout(&html, None, top, win_w, win_h);
378 if size_changed {
380 let viewport_h = win_h.saturating_sub(top as u32);
381 self.engine
382 .js_runtime
383 .fire_resize(win_w as i32, viewport_h as i32);
384 }
385 }
386 let theme = crate::kernel::config::get_config().theme;
387 self.engine.draw(screen, win_x, win_y, win_w, win_h, top);
397
398 screen.boxfill(
400 win_x,
401 win_y,
402 win_x + win_w,
403 win_y + CONTROL_BAR_H as u32,
404 crate::kernel::draw::Color(theme.tab_area_bg),
405 );
406
407 let can_back = self.engine.can_go_back();
409 let back_fg = if can_back { theme.button_fg } else { theme.non_active_window_border };
410 screen.boxfill(
411 win_x + 10,
412 win_y + 8,
413 win_x + 42,
414 win_y + 32,
415 draw::Color(theme.button_bg),
416 );
417 draw_border(
418 screen,
419 (win_x + 10) as i32,
420 (win_y + 8) as i32,
421 (win_x + 42) as i32,
422 (win_y + 32) as i32,
423 theme.non_active_window_border,
424 );
425 screen.draw_svg_icon(win_x + 16, win_y + 10, SVG_BACK, 20, draw::Color(back_fg));
426
427 let can_fwd = self.engine.can_go_forward();
429 let fwd_fg = if can_fwd { theme.button_fg } else { theme.non_active_window_border };
430 screen.boxfill(
431 win_x + 48,
432 win_y + 8,
433 win_x + 80,
434 win_y + 32,
435 draw::Color(theme.button_bg),
436 );
437 draw_border(
438 screen,
439 (win_x + 48) as i32,
440 (win_y + 8) as i32,
441 (win_x + 80) as i32,
442 (win_y + 32) as i32,
443 theme.non_active_window_border,
444 );
445 screen.draw_svg_icon(win_x + 54, win_y + 10, SVG_FORWARD, 20, draw::Color(fwd_fg));
446
447 screen.boxfill(
449 win_x + 86,
450 win_y + 8,
451 win_x + 118,
452 win_y + 32,
453 draw::Color(theme.button_bg),
454 );
455 draw_border(
456 screen,
457 (win_x + 86) as i32,
458 (win_y + 8) as i32,
459 (win_x + 118) as i32,
460 (win_y + 32) as i32,
461 theme.non_active_window_border,
462 );
463 screen.draw_svg_icon(
464 win_x + 92,
465 win_y + 10,
466 SVG_HOME,
467 20,
468 draw::Color(theme.button_fg),
469 );
470
471 screen.boxfill(
473 win_x + 124,
474 win_y + 8,
475 win_x + 156,
476 win_y + 32,
477 draw::Color(theme.button_bg),
478 );
479 draw_border(
480 screen,
481 (win_x + 124) as i32,
482 (win_y + 8) as i32,
483 (win_x + 156) as i32,
484 (win_y + 32) as i32,
485 theme.non_active_window_border,
486 );
487 screen.draw_svg_icon(
488 win_x + 130,
489 win_y + 10,
490 SVG_RELOAD,
491 20,
492 draw::Color(theme.button_fg),
493 );
494
495 let is_https = self.engine.current_is_https;
497 let badge_w = if is_https { 95 } else { 125 };
498 let badge_bg = if is_https { 0xFF1B4D3E } else { 0xFF4C1D1D };
499 let badge_border = if is_https { 0xFF50FA7B } else { 0xFFFF5555 };
500 let badge_lbl = if is_https { "Secure" } else { "Not Secure" };
501 let badge_fg = if is_https { 0xFFECFDF5 } else { 0xFFFEE2E2 };
502 let badge_svg = if is_https {
503 SVG_SHIELD_SECURE
504 } else {
505 SVG_SHIELD_WARN
506 };
507
508 screen.boxfill(
509 win_x + 162,
510 win_y + 8,
511 win_x + 162 + badge_w,
512 win_y + 32,
513 draw::Color(badge_bg),
514 );
515 draw_border(
516 screen,
517 (win_x + 162) as i32,
518 (win_y + 8) as i32,
519 (win_x + 162 + badge_w) as i32,
520 (win_y + 32) as i32,
521 badge_border,
522 );
523 screen.draw_svg_icon(
524 win_x + 164,
525 win_y + 10,
526 badge_svg,
527 20,
528 draw::Color(badge_border),
529 );
530 screen.draw_string_vector(
531 win_x + 189,
532 win_y + 13,
533 badge_lbl,
534 draw::Color(badge_fg),
535 11,
536 );
537
538 let badge_end = 162 + badge_w;
539
540 let bar_right = (win_x + win_w).saturating_sub(10);
542 let bar_left = win_x + badge_end + 6;
543 if bar_right > bar_left {
544 let addr_bg = if self.address_bar_focused {
545 theme.terminal_bg
546 } else {
547 theme.tab_area_bg
548 };
549 let addr_border = if self.address_bar_focused {
550 theme.focus_highlight
551 } else {
552 theme.non_active_window_border
553 };
554 screen.boxfill(
555 bar_left,
556 win_y + 8,
557 bar_right,
558 win_y + 32,
559 draw::Color(addr_bg),
560 );
561 draw_border(
562 screen,
563 bar_left as i32,
564 (win_y + 8) as i32,
565 bar_right as i32,
566 (win_y + 32) as i32,
567 addr_border,
568 );
569
570 let is_placeholder = self.address_box.text.is_empty() && !self.address_bar_focused;
571 let display_text = if is_placeholder {
572 String::from("Search or enter address...")
573 } else {
574 self.address_box.text.clone()
575 };
576 let text_color = if is_placeholder {
577 theme.non_active_window_border
578 } else {
579 theme.terminal_fg
580 };
581 screen.draw_string_vector(
582 bar_left + 10,
583 win_y + 12,
584 &display_text,
585 draw::Color(text_color),
586 12,
587 );
588
589 if self.address_bar_focused {
591 let prefix: String = self
592 .address_box
593 .text
594 .chars()
595 .take(self.address_box.cursor_pos)
596 .collect();
597 let pw = crate::kernel::vector_font::get_vector_string_width(&prefix, 12);
598 let cursor_x = bar_left + 10 + pw;
599 if cursor_x < bar_right {
600 screen.boxfill(
601 cursor_x,
602 win_y + 11,
603 cursor_x + 1,
604 win_y + 28,
605 draw::Color(crate::kernel::ime::cursor_color()),
606 );
607 }
608 }
609 }
610
611 screen.boxfill(
612 win_x,
613 win_y + 39,
614 win_x + win_w,
615 win_y + 40,
616 crate::kernel::draw::Color(theme.tab_area_border),
617 );
618
619 if self.address_bar_focused && !self.omnibox_items.is_empty() {
621 let badge_w = if self.engine.current_is_https { 95 } else { 125 };
622 let bar_left = win_x + 162 + badge_w + 6;
623 let bar_right = (win_x + win_w).saturating_sub(10);
624 if bar_right > bar_left {
625 let drop_x = bar_left;
626 let drop_w = bar_right - bar_left;
627 let item_h = 24u32;
628 let drop_h = (self.omnibox_items.len() as u32) * item_h + 4;
629 let drop_y = win_y + 34;
630
631 screen.boxfill_alpha(
633 drop_x,
634 drop_y,
635 drop_x + drop_w,
636 drop_y + drop_h,
637 draw::Color(0xFF1E1E28),
638 248,
639 );
640 draw_border(
641 screen,
642 drop_x as i32,
643 drop_y as i32,
644 (drop_x + drop_w) as i32,
645 (drop_y + drop_h) as i32,
646 0xFF3D3D4E,
647 );
648
649 for (i, item) in self.omnibox_items.iter().enumerate() {
650 let iy = drop_y + 2 + (i as u32) * item_h;
651 let is_selected = self.selected_suggestion == Some(i);
652
653 if is_selected {
654 screen.boxfill(
655 drop_x + 1,
656 iy,
657 drop_x + drop_w - 1,
658 iy + item_h,
659 draw::Color(0xFF353548),
660 );
661 }
662
663 let (icon_sym, icon_col) = match item.kind {
665 crate::os_lib::web_engine::omnibox::OmniboxItemKind::Bookmark => ("*", 0xFFFFD700),
666 crate::os_lib::web_engine::omnibox::OmniboxItemKind::History => (">", 0xFF6BA4E8),
667 crate::os_lib::web_engine::omnibox::OmniboxItemKind::Search => ("?", 0xFFDE5833),
668 };
669
670 screen.draw_string_vector(
671 drop_x + 8,
672 iy + 5,
673 icon_sym,
674 draw::Color(icon_col),
675 11,
676 );
677
678 let text_col = if is_selected { 0xFFFFFFFF } else { 0xFFD0D0E0 };
680 screen.draw_string_vector(
681 drop_x + 22,
682 iy + 5,
683 &item.title,
684 draw::Color(text_col),
685 12,
686 );
687
688 let badge_text = item.kind.badge_label();
690 if drop_w > 140 {
691 screen.draw_string_vector(
692 drop_x + drop_w - 85,
693 iy + 6,
694 badge_text,
695 draw::Color(icon_col),
696 10,
697 );
698 }
699 }
700 }
701 }
702
703 if !self.address_bar_focused && self.engine.dirty {
705 self.address_box.set_text(&self.engine.get_url());
706 }
707 if !self.engine.title.is_empty() {
708 if let Some(slot) = self.tabs.get_mut(self.active_tab_idx) {
709 slot.title = self.engine.title.clone();
710 }
711 }
712 crate::os_lib::web_engine::set_draw_stage(0);
713 }
714
715 fn on_mouse(
716 &mut self,
717 local_x: i32,
718 local_y: i32,
719 btn_left: bool,
720 btn_right: bool,
721 wheel: i32,
722 ) {
723 let top = self.top_offset();
724
725 if btn_left && self.address_bar_focused && !self.omnibox_items.is_empty() {
726 let bar_left = 162 + if self.engine.current_is_https { 95 } else { 125 } + 6;
727 let bar_right = (self.engine.win_w as i32).saturating_sub(10).max(bar_left + 100);
728 let drop_y = 34;
729 let item_h = 24;
730 let drop_h = (self.omnibox_items.len() as i32) * item_h + 4;
731 if (bar_left..=bar_right).contains(&local_x) && (drop_y..=drop_y + drop_h).contains(&local_y) {
732 let clicked_idx = ((local_y - drop_y - 2) / item_h) as usize;
733 if let Some(item) = self.omnibox_items.get(clicked_idx).cloned() {
734 self.execute_search_or_navigate(&item.url);
735 return;
736 }
737 }
738 }
739
740 if btn_left && (0..CONTROL_BAR_H).contains(&local_y) {
741 if (8..=32).contains(&local_y) {
742 if (10..=42).contains(&local_x) {
743 self.engine.go_back(top);
744 self.address_bar_focused = false;
745 self.address_box.set_text(&self.engine.get_url());
746 self.engine.dirty = true;
747 self.engine.dirty_reason = "mod.rs:532";
748 return;
749 }
750 if (48..=80).contains(&local_x) {
751 self.engine.go_forward(top);
752 self.address_bar_focused = false;
753 self.address_box.set_text(&self.engine.get_url());
754 self.engine.dirty = true;
755 self.engine.dirty_reason = "mod.rs:539";
756 return;
757 }
758 if (86..=118).contains(&local_x) {
759 render_home(&mut self.engine, top);
761 self.address_bar_focused = false;
762 self.address_box.set_text(&self.engine.get_url());
763 self.engine.dirty = true;
764 self.engine.dirty_reason = "mod.rs:547";
765 return;
766 }
767 if (124..=156).contains(&local_x) {
768 if self.engine.current_host == "localhost" {
770 let path = self.engine.current_path.clone();
771 if path == "/home" {
772 render_home(&mut self.engine, top);
774 } else {
775 let loaded = {
779 let _fs_guard = crate::kernel::fs::FS_LOCK.lock();
780 crate::kernel::fs::get_fs().read_file(&path)
781 };
782 if let Some((_meta, content)) = loaded {
783 if let Ok(html) = core::str::from_utf8(&content) {
784 self.engine.load_html(
785 html,
786 path.strip_prefix('/').unwrap_or(&path),
787 top,
788 );
789 }
790 }
791 }
792 } else {
793 self.engine.load_page(
794 self.engine.current_is_https,
795 &self.engine.current_host.clone(),
796 &self.engine.current_path.clone(),
797 false,
798 top,
799 );
800 }
801 self.address_bar_focused = false;
802 self.address_box.set_text(&self.engine.get_url());
803 return;
804 }
805 let badge_end = 162
807 + if self.engine.current_is_https {
808 95
809 } else {
810 125
811 };
812 let bar_left = badge_end + 6;
813 if local_x >= bar_left {
814 self.address_bar_focused = true;
815 self.address_box.set_text(&self.engine.get_url());
816 self.engine.dirty = true;
817 self.engine.dirty_reason = "mod.rs:593";
818 return;
819 }
820 }
821 self.address_bar_focused = false;
822 self.engine.dirty = true;
823 self.engine.dirty_reason = "mod.rs:598";
824 return;
825 }
826
827 if btn_left {
828 self.address_bar_focused = false;
829 self.engine.dirty = true;
830 self.engine.dirty_reason = "mod.rs:604";
831 }
832
833 self.engine
834 .on_mouse(local_x, local_y, btn_left, btn_right, wheel, top);
835 }
836
837 fn on_key(&mut self, keycode: u8, pressed: bool, ascii: Option<char>) {
838 if !pressed {
839 return;
840 }
841
842 if self.address_bar_focused {
843 if keycode == 0x1C {
844 let chosen_url = if let Some(idx) = self.selected_suggestion {
846 self.omnibox_items.get(idx).map(|item| item.url.clone())
847 } else {
848 None
849 };
850 let text_to_nav = chosen_url.unwrap_or_else(|| self.address_box.text.clone());
851 self.execute_search_or_navigate(&text_to_nav);
852 } else if keycode == 0x50 {
853 if !self.omnibox_items.is_empty() {
855 let next = match self.selected_suggestion {
856 Some(i) => (i + 1) % self.omnibox_items.len(),
857 None => 0,
858 };
859 self.selected_suggestion = Some(next);
860 if let Some(item) = self.omnibox_items.get(next) {
861 self.address_box.set_text(&item.title);
862 }
863 self.engine.dirty = true;
864 self.engine.dirty_reason = "mod.rs:sugg_down";
865 }
866 } else if keycode == 0x48 {
867 if !self.omnibox_items.is_empty() {
869 let prev = match self.selected_suggestion {
870 Some(0) | None => self.omnibox_items.len().saturating_sub(1),
871 Some(i) => i - 1,
872 };
873 self.selected_suggestion = Some(prev);
874 if let Some(item) = self.omnibox_items.get(prev) {
875 self.address_box.set_text(&item.title);
876 }
877 self.engine.dirty = true;
878 self.engine.dirty_reason = "mod.rs:sugg_up";
879 }
880 } else if keycode == 0x01 {
881 self.omnibox_items.clear();
883 self.selected_suggestion = None;
884 self.engine.dirty = true;
885 self.engine.dirty_reason = "mod.rs:sugg_esc";
886 } else {
887 let changed = self.address_box.handle_key(keycode, ascii);
888 if changed {
889 self.selected_suggestion = None;
890 let text = self.address_box.text.trim();
891 if !text.is_empty() {
892 let search_cands = if !text.starts_with("http://") && !text.starts_with("https://") && !text.starts_with('/') {
893 crate::os_lib::web_engine::duckduckgo::get_mock_suggestions(text)
894 } else {
895 alloc::vec![]
896 };
897 self.omnibox_items = self.omnibox.get_suggestions(text, &search_cands, 6);
898 } else {
899 self.omnibox_items.clear();
900 }
901 self.engine.dirty = true;
902 self.engine.dirty_reason = "mod.rs:text_input";
903 }
904 }
905 return;
906 }
907
908 self.engine.on_key(keycode, pressed, ascii);
909
910 if (ascii == Some('\x03') || keycode == 0x2E) && self.engine.focused_id.is_none() && !self.address_bar_focused {
912 let selected = self.engine.get_selected_text();
913 if !selected.is_empty() {
914 crate::os_lib::clipboard::set_text(&selected);
915 crate::info!("[BROWSER] Copied {} bytes to clipboard: {:?}", selected.len(), selected);
916 }
917 }
918
919 if keycode == 0x01 && !self.address_bar_focused {
921 self.engine.clear_selection();
922 }
923
924 if keycode == 0x0E && self.engine.focused_id.is_none() {
925 let top = self.top_offset();
926 self.engine.go_back(top);
927 self.address_box.set_text(&self.engine.get_url());
928 }
929
930 if self.engine.focused_id.is_none() && !self.address_bar_focused {
940 let page = (self.engine.win_h as i32 - self.top_offset()).max(80);
942 let dy = match keycode {
943 0x48 => Some(-60), 0x50 => Some(60), 0x49 => Some(-page), 0x51 => Some(page), 0x47 => Some(-self.engine.max_scroll_y), 0x4F => Some(self.engine.max_scroll_y), _ => None,
950 };
951 if let Some(dy) = dy {
952 self.engine.scroll(dy);
953 self.engine.dirty = true;
954 self.engine.dirty_reason = "browser/mod.rs:key_scroll";
955 }
956 }
957 }
958
959 fn tabs(&self) -> alloc::vec::Vec<alloc::string::String> {
960 self.tabs
962 .iter()
963 .enumerate()
964 .map(|(i, t)| {
965 if i == self.active_tab_idx {
966 if self.engine.title.is_empty() {
967 self.engine.current_host.clone()
968 } else {
969 self.engine.title.clone()
970 }
971 } else {
972 t.title.clone()
973 }
974 })
975 .collect()
976 }
977
978 fn active_tab(&self) -> usize {
979 self.active_tab_idx
980 }
981
982 fn close_tab(&mut self, idx: usize) -> bool {
983 if idx >= self.tabs.len() {
984 return false;
985 }
986
987 let was_active = idx == self.active_tab_idx;
988 self.tabs.remove(idx);
989
990 if self.tabs.is_empty() {
991 self.engine.active = false;
992 crate::os_lib::web_engine::clear_browser_cache();
993 return true;
994 }
995
996 if was_active {
997 let new_idx = idx.min(self.tabs.len() - 1);
999 if let Some(mut next) = self.tabs[new_idx].engine.take() {
1000 core::mem::swap(&mut self.engine, &mut next);
1001 }
1003 self.active_tab_idx = new_idx;
1004 self.engine.active = true;
1005 self.engine.dirty = true;
1006 self.engine.dirty_reason = "mod.rs:733";
1007 self.address_box.set_text(&self.engine.get_url());
1008 self.address_bar_focused = false;
1009 } else if idx < self.active_tab_idx {
1010 self.active_tab_idx -= 1;
1012 }
1013 false
1014 }
1015
1016 fn switch_tab(&mut self, idx: usize) {
1017 self.activate(idx);
1018 }
1019
1020 fn add_tab(&mut self) {
1021 if self.tabs.len() >= 16 {
1022 return;
1023 }
1024 self.sync_active_title();
1026 let old_active = self.active_tab_idx;
1027 let w = self.engine.win_w;
1028 let h = self.engine.win_h;
1029
1030 let mut new_engine = WebEngine::new();
1032 new_engine.win_w = w;
1033 new_engine.win_h = h;
1034 new_engine.current_host = String::from("newtab");
1035 new_engine.current_path = String::from("/");
1036 new_engine.title = String::from("New Tab");
1037 new_engine.active = true;
1038 let top = self.top_offset();
1039 new_engine.parse_and_layout(
1040 "<h1>New Tab</h1><p>Enter URL in the address bar.</p>",
1041 None,
1042 top,
1043 w,
1044 h,
1045 );
1046
1047 let prev = core::mem::replace(&mut self.engine, new_engine);
1049 self.tabs[old_active].engine = Some(prev);
1050
1051 self.tabs.push(TabSlot {
1052 title: String::from("New Tab"),
1053 engine: None,
1054 });
1055 self.active_tab_idx = self.tabs.len() - 1;
1056
1057 self.address_box.set_text("");
1058 self.address_bar_focused = true;
1059 self.engine.dirty = true;
1060 self.engine.dirty_reason = "mod.rs:786";
1061 }
1062
1063 fn get_dom_value(&self, id: &str) -> Option<alloc::string::String> {
1064 self.engine.form_values.get(id).cloned()
1065 }
1066
1067 fn set_dom_value(&mut self, id: &str, val: &str) {
1068 self.engine.form_values.insert(
1069 alloc::string::String::from(id),
1070 alloc::string::String::from(val),
1071 );
1072 self.engine.dirty = true;
1073 self.engine.dirty_reason = "mod.rs:798";
1074 }
1075
1076 fn get_dom_content(&self, id: &str) -> Option<alloc::string::String> {
1077 self.engine.dom_contents.get(id).cloned()
1078 }
1079
1080 fn set_dom_content(&mut self, id: &str, val: &str) {
1081 self.engine.set_dom_content(id, val);
1082 }
1083}