1use anyhow::Result;
33use std::collections::{HashMap, hash_map::DefaultHasher};
34use std::hash::{Hash, Hasher};
35use std::rc::Rc;
36use std::time::{SystemTime, UNIX_EPOCH};
37use std::{env, io};
38use url::Url;
39use winit::event::WindowEvent;
40use winit::window::WindowId;
41
42use super::tab::{FetchKind, Tab, TabTask};
43use super::{BrowserCommand, resource_loader::BrowserResourceLoader};
44use crate::engine::layouter;
45use crate::engine::renderer_model::{self, DrawCommand};
46use crate::platform::network::NetworkCore;
47use crate::platform::renderer::gpu::GpuRenderer;
48use crate::platform::system::App;
49
50pub struct RenderState {
51 pub draw_commands: Vec<DrawCommand>,
53 pub window_size: (u32, u32),
55 pub scale_factor: f64,
57 pub window_title: String,
59}
60
61#[derive(Default)]
63pub struct InputState {
64 pub mouse_position: (f64, f64),
66 pub modifiers: winit::keyboard::ModifiersState,
68}
69
70pub struct PendingFetches {
71 map: HashMap<usize, (usize, FetchKind, Url)>,
74 counter: usize,
75}
76
77impl PendingFetches {
78 pub fn new() -> Self {
79 Self {
80 map: HashMap::new(),
81 counter: 0,
82 }
83 }
84
85 pub fn insert(&mut self, tab_id: usize, kind: FetchKind, url: Url) -> usize {
87 self.counter += 1;
88
89 let id = self.generate_id(&url);
90
91 self.map.insert(id, (tab_id, kind, url));
92 dbg!(id)
93 }
94
95 fn generate_id(&self, url: &Url) -> usize {
96 let mut hasher = DefaultHasher::new();
98 url.hash(&mut hasher);
99 let url_hash = hasher.finish() as usize;
100
101 let now = SystemTime::now()
103 .duration_since(UNIX_EPOCH)
104 .expect("Time went backwards")
105 .as_nanos() as usize;
106
107 now ^ self.counter ^ url_hash
109 }
110
111 pub fn remove(&mut self, id: usize) -> Option<(usize, FetchKind, Url)> {
112 self.map.remove(&id)
113 }
114}
115
116pub struct BrowserApp {
150 tabs: Vec<Tab>,
151 active_tab: usize,
152 renders: HashMap<WindowId, RenderState>,
154 inputs: HashMap<WindowId, InputState>,
156 window_tabs: HashMap<WindowId, usize>,
158 default_window_size: (u32, u32),
160 default_window_title: String,
162 network: BrowserResourceLoader,
163 pending_fetches: PendingFetches,
164}
165
166impl Default for BrowserApp {
167 fn default() -> Self {
168 Self::new((800, 600), "Orinium Browser".to_string()).unwrap()
169 }
170}
171
172impl BrowserApp {
173 pub fn run(self) -> Result<()> {
175 run_with_winit_backend(self)
176 }
177
178 pub fn new(
181 default_window_size: (u32, u32),
182 default_window_title: String,
183 ) -> Result<Self, io::Error> {
184 let network = BrowserResourceLoader::new(Some(Rc::new(NetworkCore::new()?)));
185
186 Ok(Self {
187 tabs: vec![],
188 active_tab: 0,
189 renders: HashMap::new(),
190 inputs: HashMap::new(),
191 window_tabs: HashMap::new(),
192 default_window_size,
193 default_window_title,
194 network,
195 pending_fetches: PendingFetches::new(),
196 })
197 }
198
199 pub fn open_window(
201 &mut self,
202 window_id: WindowId,
203 window_size: (u32, u32),
204 window_title: String,
205 scale_factor: f64,
206 tab_id: usize,
207 ) {
208 self.renders.insert(
209 window_id,
210 RenderState {
211 draw_commands: vec![],
212 window_size,
213 scale_factor,
214 window_title,
215 },
216 );
217 self.inputs.insert(window_id, InputState::default());
218 self.window_tabs.insert(window_id, tab_id);
219 }
220
221 pub fn close_window(&mut self, window_id: WindowId) {
223 self.renders.remove(&window_id);
224 self.inputs.remove(&window_id);
225 self.window_tabs.remove(&window_id);
226 }
227
228 pub fn default_window_size(&self) -> (f32, f32) {
230 (
231 self.default_window_size.0 as f32,
232 self.default_window_size.1 as f32,
233 )
234 }
235
236 pub fn default_window_title(&self) -> String {
238 self.default_window_title.clone()
239 }
240
241 pub fn tick(&mut self) -> BrowserCommand {
242 self.handle_network_messages();
243
244 let mut needs_redraw = false;
246 let tab_count = self.tabs.len();
247 for tab_id in 0..tab_count {
248 let Some(tab) = self.tabs.get_mut(tab_id) else {
249 continue;
250 };
251 for task in tab.tick() {
252 match task {
253 TabTask::Fetch { url, kind } => {
254 log::info!("Fetch requested in App: url={}", url);
255 let id = self.pending_fetches.insert(tab_id, kind, url.clone());
256 self.network.fetch_async(url, id);
257 }
258 TabTask::NeedsRedraw => {
259 needs_redraw = true;
260 }
261 }
262 }
263 }
264
265 if needs_redraw {
266 BrowserCommand::RequestRedraw
267 } else {
268 BrowserCommand::None
269 }
270 }
271
272 fn handle_network_messages(&mut self) {
273 let messages = self.network.try_receive();
274
275 for msg in messages {
276 log::info!("Network message received in App for fetch_id={}", msg.id);
277
278 let Some((tab_id, kind, url)) = self.pending_fetches.remove(msg.id) else {
280 log::warn!("No pending fetch found for fetch_id={}", msg.id);
281 continue;
282 };
283
284 let Some(tab) = self.tabs.get_mut(tab_id) else {
286 log::warn!("There is no Tab called id={}", tab_id);
287 continue;
288 };
289
290 match msg.response {
291 Ok(resp) => {
292 log::info!("Fetch Done in App for tab_id={}", tab_id);
293
294 match kind {
295 FetchKind::Html => {
296 let html = String::from_utf8_lossy(&resp.body).to_string();
297 tab.on_fetch_succeeded_html(html);
298 }
299 FetchKind::Css => {
300 let css = String::from_utf8_lossy(&resp.body).to_string();
301 tab.on_fetch_succeeded_css(css);
302 }
303 }
304 }
305 Err(err) => {
306 log::error!("NetworkError: {}", err);
307 tab.on_fetch_failed(err, url);
308 }
309 }
310 }
311 }
312
313 #[allow(dead_code)]
314 fn active_tab_mut(&mut self) -> Option<&mut Tab> {
316 self.tabs.get_mut(self.active_tab)
317 }
318
319 fn tab_id_for_window(&self, window_id: WindowId) -> usize {
321 *self.window_tabs.get(&window_id).unwrap_or(&self.active_tab)
322 }
323
324 fn rebuild_render_tree(&mut self, window_id: WindowId) {
326 let tab_id = self.tab_id_for_window(window_id);
327
328 let (viewport, mut draw_commands) = {
329 let Some(render) = self.renders.get_mut(&window_id) else {
330 return;
331 };
332
333 let sf = render.scale_factor as f32;
334
335 let viewport = (
336 render.window_size.0 as f32 / sf,
337 render.window_size.1 as f32 / sf,
338 );
339
340 let mut draw_commands = std::mem::take(&mut render.draw_commands);
342 draw_commands.clear();
343
344 (viewport, draw_commands)
345 };
346
347 let title = {
348 let Some(tab) = self.tabs.get_mut(tab_id) else {
349 return;
350 };
351
352 tab.relayout(viewport);
353
354 let Some((layout, info)) = tab.layout_and_info() else {
355 log::debug!("No layout/info available for tab {}", tab_id);
356 return;
357 };
358
359 renderer_model::generate_draw_commands(&mut draw_commands, layout, info);
360
361 tab.title()
362 };
363
364 let Some(render) = self.renders.get_mut(&window_id) else {
365 return;
366 };
367
368 render.draw_commands = draw_commands;
370
371 if let Some(title) = title {
372 render.window_title = title;
373 }
374 }
375
376 pub fn handle_window_event(
378 &mut self,
379 window_id: WindowId,
380 event: WindowEvent,
381 gpu: &mut GpuRenderer,
382 ) -> BrowserCommand {
383 let browser_cmd = match event {
384 WindowEvent::CloseRequested => BrowserCommand::Exit,
385
386 WindowEvent::RedrawRequested => {
387 self.redraw(window_id, gpu);
388 BrowserCommand::RenameWindowTitle
389 }
390
391 WindowEvent::Resized(size) => {
392 if let Some(render) = self.renders.get_mut(&window_id) {
393 render.window_size = (size.width, size.height);
394 }
395 gpu.resize(size);
396 self.redraw(window_id, gpu);
397 BrowserCommand::RequestRedraw
398 }
399
400 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
401 gpu.set_scale_factor(scale_factor);
402 if let Some(render) = self.renders.get_mut(&window_id) {
403 render.scale_factor = scale_factor;
404 }
405 self.redraw(window_id, gpu);
406 BrowserCommand::RequestRedraw
407 }
408
409 WindowEvent::MouseWheel { delta, .. } => {
410 self.handle_scroll(window_id, delta);
411 BrowserCommand::RequestRedraw
412 }
413
414 WindowEvent::CursorMoved { position, .. } => {
415 if let Some(input) = self.inputs.get_mut(&window_id) {
416 input.mouse_position = (position.x, position.y);
417 }
418 BrowserCommand::None
419 }
420
421 WindowEvent::MouseInput { button, .. } => self.handle_mouse_input(window_id, button),
422
423 WindowEvent::ModifiersChanged(modifiers) => {
424 if let Some(input) = self.inputs.get_mut(&window_id) {
425 input.modifiers = modifiers.state();
426 }
427 BrowserCommand::None
428 }
429
430 WindowEvent::KeyboardInput { event, .. } => {
431 self.handle_keyboard_input(window_id, event)
432 }
433
434 _ => BrowserCommand::None,
435 };
436 let cmd_from_tick = self.tick();
437 match browser_cmd {
438 BrowserCommand::None => {
439 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
440 self.redraw(window_id, gpu);
441 }
442 cmd_from_tick
443 }
444 BrowserCommand::RenameWindowTitle => {
445 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
446 self.redraw(window_id, gpu);
449 BrowserCommand::RequestRedraw
450 } else {
451 browser_cmd
452 }
453 }
454 _ => {
455 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
456 self.redraw(window_id, gpu);
457 }
458 browser_cmd
459 }
460 }
461 }
462
463 fn handle_keyboard_input(
465 &mut self,
466 window_id: WindowId,
467 event: winit::event::KeyEvent,
468 ) -> BrowserCommand {
469 const KEY_NEW_WINDOW: &str = "n";
471
472 if event.state != winit::event::ElementState::Pressed {
473 return BrowserCommand::None;
474 }
475
476 let ctrl = self
477 .inputs
478 .get(&window_id)
479 .is_some_and(|i| i.modifiers.control_key());
480
481 if ctrl
482 && let winit::keyboard::Key::Character(ch) = &event.logical_key
483 && ch.as_str().eq_ignore_ascii_case(KEY_NEW_WINDOW)
484 {
485 let tab_id = self.new_empty_tab();
486 return BrowserCommand::OpenNewWindow { tab_id };
487 }
488
489 BrowserCommand::None
490 }
491
492 pub fn new_empty_tab(&mut self) -> usize {
494 self.tabs.push(Tab::new());
495 self.tabs.len() - 1
496 }
497
498 fn handle_mouse_input(
500 &mut self,
501 window_id: WindowId,
502 button: winit::event::MouseButton,
503 ) -> BrowserCommand {
504 if button != winit::event::MouseButton::Left {
505 return BrowserCommand::None;
506 }
507
508 let (x, y, sf) = match (self.inputs.get(&window_id), self.renders.get(&window_id)) {
509 (Some(input), Some(render)) => (
510 input.mouse_position.0,
511 input.mouse_position.1,
512 render.scale_factor,
513 ),
514 _ => return BrowserCommand::None,
515 };
516
517 let tab_id = self.tab_id_for_window(window_id);
518 if let Some(tab) = self.tabs.get_mut(tab_id) {
519 Self::handle_mouse_click(tab, (x / sf) as f32, (y / sf) as f32);
520 BrowserCommand::RequestRedraw
521 } else {
522 BrowserCommand::None
523 }
524 }
525
526 fn handle_scroll(&mut self, window_id: WindowId, delta: winit::event::MouseScrollDelta) {
528 let scroll_amount = match delta {
529 winit::event::MouseScrollDelta::LineDelta(_, y) => -y * 60.0,
530 winit::event::MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
531 };
532
533 let (window_height, sf) = match self.renders.get(&window_id) {
534 Some(render) => (render.window_size.1 as f32, render.scale_factor as f32),
535 None => return,
536 };
537
538 let tab_id = self.tab_id_for_window(window_id);
539 if let Some(tab) = self.tabs.get_mut(tab_id)
540 && let Some((layout, info)) = tab.layout_and_info_mut()
541 && let layouter::types::NodeKind::Container {
542 scroll_offset_y, ..
543 } = &mut info.kind
544 {
545 *scroll_offset_y = (*scroll_offset_y + scroll_amount).clamp(
546 0.0,
547 (layout
548 .layout_box
549 .iter()
550 .map(|l| l.children_box.height)
551 .sum::<f32>()
552 - (window_height / sf))
553 .max(0.0),
554 );
555 }
556 }
557
558 pub fn handle_mouse_click(tab: &mut Tab, x: f32, y: f32) {
560 let hit_path = match tab.layout_and_info() {
561 Some((layout, info)) => crate::engine::input::hit_test(layout, info, x, y),
562 None => return,
563 };
564
565 let href_opt = {
566 if let Some(hit) = hit_path.iter().find(|e| {
567 matches!(
568 e.info.kind,
569 layouter::types::NodeKind::Container { ref role, .. }
570 if matches!(role, layouter::types::ContainerRole::Link { .. })
571 )
572 }) {
573 if let layouter::types::NodeKind::Container { role, .. } = &hit.info.kind
574 && let layouter::types::ContainerRole::Link { href } = role
575 {
576 Some(href.clone())
577 } else {
578 None
579 }
580 } else {
581 None
582 }
583 };
584
585 if let Some(href) = href_opt {
586 tab.move_to(&href)
587 }
588 }
589
590 pub fn redraw(&mut self, window_id: WindowId, gpu: &mut GpuRenderer) {
592 self.rebuild_render_tree(window_id);
593 self.apply_draw_commands(window_id, gpu);
594 if let Err(e) = gpu.render() {
595 log::error!(target: "BrowserApp::redraw", "Render error occurred: {}", e);
596 }
597 }
598
599 pub fn apply_draw_commands(&self, window_id: WindowId, gpu: &mut GpuRenderer) {
601 if let Some(render) = self.renders.get(&window_id) {
602 gpu.parse_draw_commands(&render.draw_commands);
603 }
604 }
605
606 pub fn add_tab(&mut self, tab: Tab) {
608 self.tabs.push(tab);
609 }
610
611 pub fn window_size(&self, window_id: WindowId) -> (f32, f32) {
613 match self.renders.get(&window_id) {
614 Some(render) => (render.window_size.0 as f32, render.window_size.1 as f32),
615 None => (
616 self.default_window_size.0 as f32,
617 self.default_window_size.1 as f32,
618 ),
619 }
620 }
621
622 pub fn window_title(&self, window_id: WindowId) -> String {
624 match self.renders.get(&window_id) {
625 Some(render) => render.window_title.clone(),
626 None => self.default_window_title.clone(),
627 }
628 }
629}
630
631fn run_with_winit_backend(app: BrowserApp) -> Result<()> {
632 configure_winit_backend_for_wslg();
633 if env::var_os("ORINIUM_FORCE_X11").is_some() {
634 configure_winit_backend_forced_x11();
635 }
636
637 run_event_loop(app)
638}
639
640fn run_event_loop(app: BrowserApp) -> Result<()> {
641 let event_loop = winit::event_loop::EventLoop::new()?;
642 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
643 let mut app = App::new(app);
644 event_loop.run_app(&mut app)?;
645 Ok(())
646}
647
648fn configure_winit_backend_forced_x11() {
649 let current = env::var("WINIT_UNIX_BACKEND").ok();
650 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
651
652 if should_force_x11 {
653 unsafe {
654 env::set_var("WINIT_UNIX_BACKEND", "x11");
655 env::remove_var("WAYLAND_DISPLAY");
656 }
657 log::info!("Forcing X11 (WINIT_UNIX_BACKEND=x11, WAYLAND_DISPLAY cleared)");
658 }
659}
660
661fn configure_winit_backend_for_wslg() {
662 let is_wsl = env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some();
663 if !is_wsl {
664 return;
665 }
666
667 if env::var_os("ORINIUM_PREFER_WAYLAND").is_some() {
669 return;
670 }
671
672 let current = env::var("WINIT_UNIX_BACKEND").ok();
673 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
674
675 if should_force_x11 {
676 unsafe {
677 env::set_var("WINIT_UNIX_BACKEND", "x11");
678 env::remove_var("WAYLAND_DISPLAY");
679 }
680 log::info!("WSLg detected: defaulting to X11 backend for stability");
681 }
682}