orinium_browser/browser/core/
app.rs

1//! Browser core: application entry and lifecycle manager.
2//!
3//! Responsibilities:
4//! - Manage Browser lifetime, window, and tab collection.
5//! - Coordinate network/resource loading and map responses to tabs.
6//! - Drive the engine pipeline: schedule layout, collect draw commands, and hand them to the platform renderer.
7//! - Handle input and window events and dispatch them to tabs/UI.
8//!
9//! Processing flow (high-level):
10//! 1. Initialize platform components (system window, GPU renderer, network core).
11//! 2. Create and register `Tab` instances and navigate to initial URLs.
12//! 3. Enter event loop: handle events -> update state -> request layout -> generate draw commands -> render.
13//! 4. Manage asynchronous fetches and inject resources into the engine when they arrive.
14//!
15//! Example (for contributors / local testing):
16//! ```no_run
17//! use orinium_browser::browser::BrowserApp;
18//! use orinium_browser::browser::Tab;
19//!
20//! let mut app = BrowserApp::default();
21//! let mut tab = Tab::new();
22//! tab.navigate("resource:///test/compatibility_test.html".parse().unwrap());
23//! app.add_tab(tab);
24//! app.run().unwrap();
25//! ```
26//!
27//! Developer notes:
28//! - For parsing and layout details see `engine::html`, `engine::css`, and `engine::layouter`.
29//! - For platform integration see `platform::{network, renderer, system}`.
30//! - Keep public API small and document invariants for Tab lifecycle and fetch handling.
31
32use 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    /// List of draw commands generated from the layout engine.
52    pub draw_commands: Vec<DrawCommand>,
53    /// Current window size in pixels (width, height).
54    pub window_size: (u32, u32),
55    /// Current scale factor (for HiDPI displays).
56    pub scale_factor: f64,
57    /// Current window title.
58    pub window_title: String,
59}
60
61/// Stores input-related state for a single browser window.
62#[derive(Default)]
63pub struct InputState {
64    /// Current mouse position in window coordinates.
65    pub mouse_position: (f64, f64),
66    /// Current keyboard modifier state (Ctrl, Shift, Alt, etc.).
67    pub modifiers: winit::keyboard::ModifiersState,
68}
69
70pub struct PendingFetches {
71    /// Maps (id) to (tab_id, FetchKind)
72    /// Id is used to track pending fetch requests.
73    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    /// URLとFetchKindを受け取り、一意IDを生成して登録
86    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        // URLをハッシュ化
97        let mut hasher = DefaultHasher::new();
98        url.hash(&mut hasher);
99        let url_hash = hasher.finish() as usize;
100
101        // 現在時刻ナノ秒
102        let now = SystemTime::now()
103            .duration_since(UNIX_EPOCH)
104            .expect("Time went backwards")
105            .as_nanos() as usize;
106
107        // ナノ秒 XOR カウンタ XOR URLハッシュ
108        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
116/// Main browser application struct.
117///
118/// Responsibilities:
119/// - Manage collection of `Tab` instances and the active tab index.
120/// - Coordinate resource loading and pending fetch lifecycle.
121/// - Orchestrate engine work (layout/draw-command generation) and submit commands to the renderer.
122/// - Process input and window events and propagate them to tabs/UI.
123///
124/// Typical lifecycle:
125/// 1. Construct `BrowserApp::new(...)`, which wires platform components (network, renderer, system).
126/// 2. Create `Tab` objects and call `add_tab` / `navigate` as needed.
127/// 3. Call `run()` to start the event loop. Each loop iteration:
128///    - Poll platform events (keyboard/mouse/window).
129///    - Update input state and dispatch to the active tab.
130///    - If DOM/CSS changes occurred, request layout and regenerate draw commands.
131///    - Submit draw commands to the platform-specific renderer.
132/// 4. Manage asynchronous resource fetches: match responses to pending fetch IDs and notify tabs.
133///
134/// Example usage:
135/// ```no_run
136/// use orinium_browser::browser::BrowserApp;
137/// use orinium_browser::browser::Tab;
138///
139/// let mut app = BrowserApp::default();
140/// let mut tab = Tab::new();
141/// tab.navigate("resource:///test/compatibility_test.html".parse().unwrap());
142/// app.add_tab(tab);
143/// app.run().unwrap();
144/// ```
145///
146/// Contributor guidance:
147/// - Add small unit tests to validate tab lifecycle, fetch handling, and draw-command generation.
148/// - Prefer adding examples under `examples/` to demonstrate end-to-end behavior.
149pub struct BrowserApp {
150    tabs: Vec<Tab>,
151    active_tab: usize,
152    /// Per-window render state, keyed by WindowId.
153    renders: HashMap<WindowId, RenderState>,
154    /// Per-window input state, keyed by WindowId.
155    inputs: HashMap<WindowId, InputState>,
156    /// Maps each window to the tab index it displays.
157    window_tabs: HashMap<WindowId, usize>,
158    /// Default window size used when opening a new window.
159    default_window_size: (u32, u32),
160    /// Default window title used when opening a new window.
161    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    /// Starts the main browser event loop asynchronously.
174    pub fn run(self) -> Result<()> {
175        run_with_winit_backend(self)
176    }
177
178    /// Creates a new browser instance with the given default window size and title.
179    /// Windows are registered later via `open_window`.
180    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    /// Registers a new window with the given id, size, title, scale factor, and associated tab.
197    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    /// Removes a window's state when the window is closed.
219    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    /// Returns the default window size for opening new windows.
226    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    /// Returns the default window title for opening new windows.
234    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        // tick all tabs and collect redraw requests
242        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            // pending_fetches から fetch 情報を取得
276            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            // Tab を取得
282            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    /// Returns a mutable reference to the currently active tab, if any.
312    fn active_tab_mut(&mut self) -> Option<&mut Tab> {
313        self.tabs.get_mut(self.active_tab)
314    }
315
316    /// Returns the tab index associated with the given window (falls back to `active_tab`).
317    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    /// Rebuilds the render tree for the window's assigned tab and generates draw commands.
322    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    /// Handles a `winit` window event for the given window and returns a `BrowserCommand`.
363    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    /// Handles keyboard input events and returns a `BrowserCommand`.
435    fn handle_keyboard_input(
436        &mut self,
437        window_id: WindowId,
438        event: winit::event::KeyEvent,
439    ) -> BrowserCommand {
440        // TODO: あとで消す
441        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    /// Adds a new empty tab and returns its index.
465    pub fn new_empty_tab(&mut self) -> usize {
466        self.tabs.push(Tab::new());
467        self.tabs.len() - 1
468    }
469
470    /// Handles mouse input events, mainly left-clicks for the active tab.
471    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    /// Handles scrolling for the window's assigned tab, updating its layout container offsets.
499    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    /// Handles a mouse click in the given tab at the specified coordinates.
531    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    /// Rebuilds the render tree and sends draw commands to the GPU for the given window.
563    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    /// Applies the current draw commands for the given window to the GPU renderer.
572    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    /// Adds a new tab to the browser.
579    pub fn add_tab(&mut self, tab: Tab) {
580        self.tabs.push(tab);
581    }
582
583    /// Returns the current window size for the given window as `(width, height)` in floating-point pixels.
584    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    /// Returns the window title for the given window.
595    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    // On WSLg, Wayland is often unstable; default to X11 unless explicitly requested.
640    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}