1use anyhow::Result;
33use std::collections::{HashMap, hash_map::DefaultHasher};
34use std::env;
35use std::hash::{Hash, Hasher};
36use std::rc::Rc;
37use std::time::{SystemTime, UNIX_EPOCH};
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())
169 }
170}
171
172impl BrowserApp {
173 pub fn run(self) -> Result<()> {
175 run_with_winit_backend(self)
176 }
177
178 pub fn new(default_window_size: (u32, u32), default_window_title: String) -> Self {
181 let network = BrowserResourceLoader::new(Some(Rc::new(NetworkCore::new())));
182
183 Self {
184 tabs: vec![],
185 active_tab: 0,
186 renders: HashMap::new(),
187 inputs: HashMap::new(),
188 window_tabs: HashMap::new(),
189 default_window_size,
190 default_window_title,
191 network,
192 pending_fetches: PendingFetches::new(),
193 }
194 }
195
196 pub fn open_window(
198 &mut self,
199 window_id: WindowId,
200 window_size: (u32, u32),
201 window_title: String,
202 scale_factor: f64,
203 tab_id: usize,
204 ) {
205 self.renders.insert(
206 window_id,
207 RenderState {
208 draw_commands: vec![],
209 window_size,
210 scale_factor,
211 window_title,
212 },
213 );
214 self.inputs.insert(window_id, InputState::default());
215 self.window_tabs.insert(window_id, tab_id);
216 }
217
218 pub fn close_window(&mut self, window_id: WindowId) {
220 self.renders.remove(&window_id);
221 self.inputs.remove(&window_id);
222 self.window_tabs.remove(&window_id);
223 }
224
225 pub fn default_window_size(&self) -> (f32, f32) {
227 (
228 self.default_window_size.0 as f32,
229 self.default_window_size.1 as f32,
230 )
231 }
232
233 pub fn default_window_title(&self) -> String {
235 self.default_window_title.clone()
236 }
237
238 pub fn tick(&mut self) -> BrowserCommand {
239 self.handle_network_messages();
240
241 let mut needs_redraw = false;
243 let tab_count = self.tabs.len();
244 for tab_id in 0..tab_count {
245 let Some(tab) = self.tabs.get_mut(tab_id) else {
246 continue;
247 };
248 for task in tab.tick() {
249 match task {
250 TabTask::Fetch { url, kind } => {
251 log::info!("Fetch requested in App: url={}", url);
252 let id = self.pending_fetches.insert(tab_id, kind, url.clone());
253 self.network.fetch_async(url, id);
254 }
255 TabTask::NeedsRedraw => {
256 needs_redraw = true;
257 }
258 }
259 }
260 }
261
262 if needs_redraw {
263 BrowserCommand::RequestRedraw
264 } else {
265 BrowserCommand::None
266 }
267 }
268
269 fn handle_network_messages(&mut self) {
270 let messages = self.network.try_receive();
271
272 for msg in messages {
273 log::info!("Network message received in App for fetch_id={}", msg.id);
274
275 let Some((tab_id, kind, url)) = self.pending_fetches.remove(msg.id) else {
277 log::warn!("No pending fetch found for fetch_id={}", msg.id);
278 continue;
279 };
280
281 let Some(tab) = self.tabs.get_mut(tab_id) else {
283 log::warn!("There is no Tab called id={}", tab_id);
284 continue;
285 };
286
287 match msg.response {
288 Ok(resp) => {
289 log::info!("Fetch Done in App for tab_id={}", tab_id);
290
291 match kind {
292 FetchKind::Html => {
293 let html = String::from_utf8_lossy(&resp.body).to_string();
294 tab.on_fetch_succeeded_html(html);
295 }
296 FetchKind::Css => {
297 let css = String::from_utf8_lossy(&resp.body).to_string();
298 tab.on_fetch_succeeded_css(css);
299 }
300 }
301 }
302 Err(err) => {
303 log::error!("NetworkError: {}", err);
304 tab.on_fetch_failed(err, url);
305 }
306 }
307 }
308 }
309
310 #[allow(dead_code)]
311 fn active_tab_mut(&mut self) -> Option<&mut Tab> {
313 self.tabs.get_mut(self.active_tab)
314 }
315
316 fn tab_id_for_window(&self, window_id: WindowId) -> usize {
318 *self.window_tabs.get(&window_id).unwrap_or(&self.active_tab)
319 }
320
321 fn rebuild_render_tree(&mut self, window_id: WindowId) {
323 let Some(render) = self.renders.get(&window_id) else {
324 return;
325 };
326 let sf = render.scale_factor as f32;
327 let viewport = (
328 render.window_size.0 as f32 / sf,
329 render.window_size.1 as f32 / sf,
330 );
331
332 let tab_id = self.tab_id_for_window(window_id);
333
334 let (title, draw_commands) = {
335 let Some(tab) = self.tabs.get_mut(tab_id) else {
336 return;
337 };
338
339 tab.relayout(viewport);
340
341 let Some((layout, info)) = tab.layout_and_info() else {
342 log::debug!("No layout/info available for tab {}", tab_id);
343 return;
344 };
345
346 let title = tab.title();
347 let draw_commands = renderer_model::generate_draw_commands(layout, info);
348
349 (title, draw_commands)
350 };
351
352 let Some(render) = self.renders.get_mut(&window_id) else {
353 return;
354 };
355 render.draw_commands = draw_commands;
356
357 if let Some(title) = title {
358 render.window_title = title;
359 }
360 }
361
362 pub fn handle_window_event(
364 &mut self,
365 window_id: WindowId,
366 event: WindowEvent,
367 gpu: &mut GpuRenderer,
368 ) -> BrowserCommand {
369 let browser_cmd = match event {
370 WindowEvent::CloseRequested => BrowserCommand::Exit,
371
372 WindowEvent::RedrawRequested => {
373 self.redraw(window_id, gpu);
374 BrowserCommand::RenameWindowTitle
375 }
376
377 WindowEvent::Resized(size) => {
378 if let Some(render) = self.renders.get_mut(&window_id) {
379 render.window_size = (size.width, size.height);
380 }
381 gpu.resize(size);
382 self.redraw(window_id, gpu);
383 BrowserCommand::RequestRedraw
384 }
385
386 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
387 gpu.set_scale_factor(scale_factor);
388 if let Some(render) = self.renders.get_mut(&window_id) {
389 render.scale_factor = scale_factor;
390 }
391 self.redraw(window_id, gpu);
392 BrowserCommand::RequestRedraw
393 }
394
395 WindowEvent::MouseWheel { delta, .. } => {
396 self.handle_scroll(window_id, delta);
397 BrowserCommand::RequestRedraw
398 }
399
400 WindowEvent::CursorMoved { position, .. } => {
401 if let Some(input) = self.inputs.get_mut(&window_id) {
402 input.mouse_position = (position.x, position.y);
403 }
404 BrowserCommand::None
405 }
406
407 WindowEvent::MouseInput { button, .. } => self.handle_mouse_input(window_id, button),
408
409 WindowEvent::ModifiersChanged(modifiers) => {
410 if let Some(input) = self.inputs.get_mut(&window_id) {
411 input.modifiers = modifiers.state();
412 }
413 BrowserCommand::None
414 }
415
416 WindowEvent::KeyboardInput { event, .. } => {
417 self.handle_keyboard_input(window_id, event)
418 }
419
420 _ => BrowserCommand::None,
421 };
422 let cmd_from_tick = self.tick();
423 match browser_cmd {
424 BrowserCommand::None => {
425 if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
426 self.redraw(window_id, gpu);
427 }
428 cmd_from_tick
429 }
430 _ => browser_cmd,
431 }
432 }
433
434 fn handle_keyboard_input(
436 &mut self,
437 window_id: WindowId,
438 event: winit::event::KeyEvent,
439 ) -> BrowserCommand {
440 const KEY_NEW_WINDOW: &str = "n";
442
443 if event.state != winit::event::ElementState::Pressed {
444 return BrowserCommand::None;
445 }
446
447 let ctrl = self
448 .inputs
449 .get(&window_id)
450 .map(|i| i.modifiers.control_key())
451 .unwrap_or(false);
452
453 if ctrl
454 && let winit::keyboard::Key::Character(ch) = &event.logical_key
455 && ch.as_str().eq_ignore_ascii_case(KEY_NEW_WINDOW)
456 {
457 let tab_id = self.new_empty_tab();
458 return BrowserCommand::OpenNewWindow { tab_id };
459 }
460
461 BrowserCommand::None
462 }
463
464 pub fn new_empty_tab(&mut self) -> usize {
466 self.tabs.push(Tab::new());
467 self.tabs.len() - 1
468 }
469
470 fn handle_mouse_input(
472 &mut self,
473 window_id: WindowId,
474 button: winit::event::MouseButton,
475 ) -> BrowserCommand {
476 if button != winit::event::MouseButton::Left {
477 return BrowserCommand::None;
478 }
479
480 let (x, y, sf) = match (self.inputs.get(&window_id), self.renders.get(&window_id)) {
481 (Some(input), Some(render)) => (
482 input.mouse_position.0,
483 input.mouse_position.1,
484 render.scale_factor,
485 ),
486 _ => return BrowserCommand::None,
487 };
488
489 let tab_id = self.tab_id_for_window(window_id);
490 if let Some(tab) = self.tabs.get_mut(tab_id) {
491 Self::handle_mouse_click(tab, (x / sf) as f32, (y / sf) as f32);
492 BrowserCommand::RequestRedraw
493 } else {
494 BrowserCommand::None
495 }
496 }
497
498 fn handle_scroll(&mut self, window_id: WindowId, delta: winit::event::MouseScrollDelta) {
500 let scroll_amount = match delta {
501 winit::event::MouseScrollDelta::LineDelta(_, y) => -y * 60.0,
502 winit::event::MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
503 };
504
505 let (window_height, sf) = match self.renders.get(&window_id) {
506 Some(render) => (render.window_size.1 as f32, render.scale_factor as f32),
507 None => return,
508 };
509
510 let tab_id = self.tab_id_for_window(window_id);
511 if let Some(tab) = self.tabs.get_mut(tab_id)
512 && let Some((layout, info)) = tab.layout_and_info_mut()
513 && let layouter::types::NodeKind::Container {
514 scroll_offset_y, ..
515 } = &mut info.kind
516 {
517 *scroll_offset_y = (*scroll_offset_y + scroll_amount).clamp(
518 0.0,
519 (layout
520 .layout_boxes
521 .iter()
522 .map(|l| l.children_box.height)
523 .sum::<f32>()
524 - (window_height / sf))
525 .max(0.0),
526 );
527 }
528 }
529
530 pub fn handle_mouse_click(tab: &mut Tab, x: f32, y: f32) {
532 let hit_path = match tab.layout_and_info() {
533 Some((layout, info)) => crate::engine::input::hit_test(layout, info, x, y),
534 None => return,
535 };
536
537 let href_opt = {
538 if let Some(hit) = hit_path.iter().find(|e| {
539 matches!(
540 e.info.kind,
541 layouter::types::NodeKind::Container { ref role, .. }
542 if matches!(role, layouter::types::ContainerRole::Link { .. })
543 )
544 }) {
545 if let layouter::types::NodeKind::Container { role, .. } = &hit.info.kind
546 && let layouter::types::ContainerRole::Link { href } = role
547 {
548 Some(href.clone())
549 } else {
550 None
551 }
552 } else {
553 None
554 }
555 };
556
557 if let Some(href) = href_opt {
558 tab.move_to(&href)
559 }
560 }
561
562 pub fn redraw(&mut self, window_id: WindowId, gpu: &mut GpuRenderer) {
564 self.rebuild_render_tree(window_id);
565 self.apply_draw_commands(window_id, gpu);
566 if let Err(e) = gpu.render() {
567 log::error!(target: "BrowserApp::redraw", "Render error occurred: {}", e);
568 }
569 }
570
571 pub fn apply_draw_commands(&self, window_id: WindowId, gpu: &mut GpuRenderer) {
573 if let Some(render) = self.renders.get(&window_id) {
574 gpu.parse_draw_commands(&render.draw_commands);
575 }
576 }
577
578 pub fn add_tab(&mut self, tab: Tab) {
580 self.tabs.push(tab);
581 }
582
583 pub fn window_size(&self, window_id: WindowId) -> (f32, f32) {
585 match self.renders.get(&window_id) {
586 Some(render) => (render.window_size.0 as f32, render.window_size.1 as f32),
587 None => (
588 self.default_window_size.0 as f32,
589 self.default_window_size.1 as f32,
590 ),
591 }
592 }
593
594 pub fn window_title(&self, window_id: WindowId) -> String {
596 match self.renders.get(&window_id) {
597 Some(render) => render.window_title.clone(),
598 None => self.default_window_title.clone(),
599 }
600 }
601}
602
603fn run_with_winit_backend(app: BrowserApp) -> Result<()> {
604 configure_winit_backend_for_wslg();
605 if env::var_os("ORINIUM_FORCE_X11").is_some() {
606 configure_winit_backend_forced_x11();
607 }
608
609 run_event_loop(app)
610}
611
612fn run_event_loop(app: BrowserApp) -> Result<()> {
613 let event_loop = winit::event_loop::EventLoop::new()?;
614 event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
615 let mut app = App::new(app);
616 event_loop.run_app(&mut app)?;
617 Ok(())
618}
619
620fn configure_winit_backend_forced_x11() {
621 let current = env::var("WINIT_UNIX_BACKEND").ok();
622 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
623
624 if should_force_x11 {
625 unsafe {
626 env::set_var("WINIT_UNIX_BACKEND", "x11");
627 env::remove_var("WAYLAND_DISPLAY");
628 }
629 log::info!("Forcing X11 (WINIT_UNIX_BACKEND=x11, WAYLAND_DISPLAY cleared)");
630 }
631}
632
633fn configure_winit_backend_for_wslg() {
634 let is_wsl = env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some();
635 if !is_wsl {
636 return;
637 }
638
639 if env::var_os("ORINIUM_PREFER_WAYLAND").is_some() {
641 return;
642 }
643
644 let current = env::var("WINIT_UNIX_BACKEND").ok();
645 let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
646
647 if should_force_x11 {
648 unsafe {
649 env::set_var("WINIT_UNIX_BACKEND", "x11");
650 env::remove_var("WAYLAND_DISPLAY");
651 }
652 log::info!("WSLg detected: defaulting to X11 backend for stability");
653 }
654}