Skip to main content

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::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    /// 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()).unwrap()
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(
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    /// Registers a new window with the given id, size, title, scale factor, and associated tab.
200    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    /// Removes a window's state when the window is closed.
222    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    /// Returns the default window size for opening new windows.
229    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    /// Returns the default window title for opening new windows.
237    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        // tick all tabs and collect redraw requests
245        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            // pending_fetches から fetch 情報を取得
279            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            // Tab を取得
285            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    /// Returns a mutable reference to the currently active tab, if any.
315    fn active_tab_mut(&mut self) -> Option<&mut Tab> {
316        self.tabs.get_mut(self.active_tab)
317    }
318
319    /// Returns the tab index associated with the given window (falls back to `active_tab`).
320    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    /// Rebuilds the render tree for the window's assigned tab and generates draw commands.
325    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            // Reuse allocation
341            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        // Return reused buffer
369        render.draw_commands = draw_commands;
370
371        if let Some(title) = title {
372            render.window_title = title;
373        }
374    }
375
376    /// Handles a `winit` window event for the given window and returns a `BrowserCommand`.
377    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                    // tick() が追加の処理を要求 → RequestRedraw に昇格させる。
447                    // RequestRedraw のハンドラはタイトル設定も行うので情報は失われない。
448                    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    /// Handles keyboard input events and returns a `BrowserCommand`.
464    fn handle_keyboard_input(
465        &mut self,
466        window_id: WindowId,
467        event: winit::event::KeyEvent,
468    ) -> BrowserCommand {
469        // TODO: あとで消す
470        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    /// Adds a new empty tab and returns its index.
493    pub fn new_empty_tab(&mut self) -> usize {
494        self.tabs.push(Tab::new());
495        self.tabs.len() - 1
496    }
497
498    /// Handles mouse input events, mainly left-clicks for the active tab.
499    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    /// Handles scrolling for the window's assigned tab, updating its layout container offsets.
527    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    /// Handles a mouse click in the given tab at the specified coordinates.
559    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    /// Rebuilds the render tree and sends draw commands to the GPU for the given window.
591    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    /// Applies the current draw commands for the given window to the GPU renderer.
600    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    /// Adds a new tab to the browser.
607    pub fn add_tab(&mut self, tab: Tab) {
608        self.tabs.push(tab);
609    }
610
611    /// Returns the current window size for the given window as `(width, height)` in floating-point pixels.
612    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    /// Returns the window title for the given window.
623    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    // On WSLg, Wayland is often unstable; default to X11 unless explicitly requested.
668    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}