Skip to main content

orinium_browser/browser/core/
app.rs

1//! Browser core: application entry and lifecycle manager.
2//!
3//! Responsibilities:
4//! - Manage the window collection and each window's [`BrowserUi`].
5//! - Forward winit window events to the owning window's UI.
6//! - Coordinate network/resource loading and route responses to the owning window.
7//!
8//! Tab state, input handling, and rendering are delegated to [`BrowserUi`] /
9//! [`BrowserRenderer`]; this type stays a thin orchestrator.
10//!
11//! Processing flow (high-level):
12//! 1. Initialize platform components (system window, GPU renderer, network core).
13//! 2. Create and register `BrowserUi` instances and navigate to initial URLs.
14//! 3. Enter event loop: forward events -> delegate to `BrowserUi` -> route fetches.
15//!
16//! Example (for contributors / local testing):
17//! ```no_run
18//! use orinium_browser::browser::{BrowserApp, BrowserUi, Tab};
19//!
20//! let mut tab = Tab::default();
21//! tab.navigate("resource:///test/test.html".parse().unwrap());
22//! let mut app = BrowserApp::default();
23//! app.set_default_ui(BrowserUi::with_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;
43use super::ui::BrowserUi;
44use super::{BrowserCommand, resource_loader::BrowserResourceLoader};
45use crate::browser::core::ui::TabId;
46use crate::engine::origin::Origin;
47use crate::platform::network::{NetworkCore, NetworkRequest};
48use crate::platform::renderer::gpu::GpuRenderer;
49use crate::platform::system::App;
50
51pub struct PendingFetches {
52    /// Maps (id) to (window_id, tab_id, FetchKind, Url)
53    /// Id is used to track pending fetch requests.
54    map: HashMap<usize, (WindowId, TabId, FetchKind, Url)>,
55    counter: usize,
56}
57
58impl PendingFetches {
59    pub fn new() -> Self {
60        Self {
61            map: HashMap::new(),
62            counter: 0,
63        }
64    }
65
66    /// URLとFetchKindを受け取り、一意IDを生成して登録
67    pub fn insert(
68        &mut self,
69        window_id: WindowId,
70        tab_id: TabId,
71        kind: FetchKind,
72        url: Url,
73    ) -> usize {
74        self.counter += 1;
75
76        let id = self.generate_id(&url);
77
78        self.map.insert(id, (window_id, tab_id, kind, url));
79        id
80    }
81
82    fn generate_id(&self, url: &Url) -> usize {
83        // URLをハッシュ化
84        let mut hasher = DefaultHasher::new();
85        url.hash(&mut hasher);
86        let url_hash = hasher.finish() as usize;
87
88        // 現在時刻ナノ秒
89        let now = SystemTime::now()
90            .duration_since(UNIX_EPOCH)
91            .expect("Time went backwards")
92            .as_nanos() as usize;
93
94        // ナノ秒 XOR カウンタ XOR URLハッシュ
95        now ^ self.counter ^ url_hash
96    }
97
98    pub fn remove(&mut self, id: usize) -> Option<(WindowId, TabId, FetchKind, Url)> {
99        self.map.remove(&id)
100    }
101}
102
103/// Main browser application struct.
104///
105/// Responsibilities:
106/// - Manage the window collection and per-window [`BrowserUi`] instances.
107/// - Forward winit window events to each window's UI.
108/// - Coordinate resource loading and route fetched results to the owning window.
109///
110/// Tab state, input handling, and rendering are delegated to [`BrowserUi`] /
111/// [`BrowserRenderer`].
112///
113/// Typical lifecycle:
114/// 1. Construct `BrowserApp::new(...)`, which wires platform components (network, system).
115/// 2. Create `Tab` objects, wrap them in a `BrowserUi`, and call `set_default_ui`.
116/// 3. Call `run()` to start the event loop. Each loop iteration:
117///    - Forward winit events to the owning window's `BrowserUi`.
118///    - Tick the UI, forward fetch requests to the network, and route responses back.
119///
120/// Example usage:
121/// ```no_run
122/// use orinium_browser::browser::{BrowserApp, BrowserUi, Tab};
123///
124/// let mut tab = Tab::default();
125/// tab.navigate("resource:///test/test.html".parse().unwrap());
126/// let mut app = BrowserApp::default();
127/// app.set_default_ui(BrowserUi::with_tab(tab));
128/// app.run().unwrap();
129/// ```
130pub struct BrowserApp {
131    /// Maps each window to its UI (tabs, input state, renderer).
132    windows: HashMap<WindowId, BrowserUi>,
133    /// Default window size used when opening a new window.
134    default_window_size: (u32, u32),
135    /// Default window title used when opening a new window.
136    default_window_title: String,
137    network: BrowserResourceLoader,
138    pending_fetches: PendingFetches,
139    /// UI used when the first window opens (if set before `run()`).
140    default_ui: Option<BrowserUi>,
141}
142
143impl Default for BrowserApp {
144    fn default() -> Self {
145        Self::new((1280, 800), "Orinium Browser".to_string()).unwrap()
146    }
147}
148
149impl BrowserApp {
150    /// Starts the main browser event loop asynchronously.
151    /// Returns an error if no default UI was set via `set_default_ui`.
152    pub fn run(self) -> Result<()> {
153        if self.default_ui.is_none() {
154            anyhow::bail!("set_default_ui must be called before run()");
155        }
156        run_with_winit_backend(self)
157    }
158
159    /// Creates a new browser instance with the given default window size and title.
160    /// Windows are registered later via `open_window`.
161    pub fn new(
162        default_window_size: (u32, u32),
163        default_window_title: String,
164    ) -> Result<Self, io::Error> {
165        let network = BrowserResourceLoader::new(Some(Rc::new(NetworkCore::new()?)));
166
167        Ok(Self {
168            windows: HashMap::new(),
169            default_window_size,
170            default_window_title,
171            network,
172            pending_fetches: PendingFetches::new(),
173            default_ui: None,
174        })
175    }
176
177    /// Registers a new window with the given id, size, title, scale factor, and associated UI.
178    pub fn open_window(
179        &mut self,
180        window_id: WindowId,
181        window_size: (u32, u32),
182        window_title: String,
183        scale_factor: f64,
184        mut root_ui: BrowserUi,
185    ) {
186        root_ui.set_window(window_size, scale_factor, window_title);
187        self.windows.insert(window_id, root_ui);
188    }
189
190    /// Removes a window's state when the window is closed.
191    pub fn close_window(&mut self, window_id: WindowId) {
192        self.windows.remove(&window_id);
193    }
194
195    /// Returns the default window size for opening new windows.
196    pub fn default_window_size(&self) -> (f32, f32) {
197        (
198            self.default_window_size.0 as f32,
199            self.default_window_size.1 as f32,
200        )
201    }
202
203    /// Returns the default window title for opening new windows.
204    pub fn default_window_title(&self) -> String {
205        self.default_window_title.clone()
206    }
207
208    /// Sets the UI to use when the first window opens.
209    /// Must be called before `run()`.
210    pub fn set_default_ui(&mut self, ui: BrowserUi) {
211        self.default_ui = Some(ui);
212    }
213
214    /// Takes the default UI, or returns `None` if not set.
215    pub fn take_default_ui(&mut self) -> Option<BrowserUi> {
216        self.default_ui.take()
217    }
218
219    /// Handles a `winit` window event for the given window and returns a `BrowserCommand`.
220    pub fn handle_window_event(
221        &mut self,
222        window_id: WindowId,
223        event: WindowEvent,
224        gpu: &mut GpuRenderer,
225    ) -> BrowserCommand {
226        let browser_cmd = match self.windows.get_mut(&window_id) {
227            Some(ui) => ui.handle_window_event(event, gpu),
228            None => BrowserCommand::None,
229        };
230        let cmd_from_tick = self.tick(window_id);
231        match browser_cmd {
232            BrowserCommand::None => {
233                if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
234                    self.redraw(window_id, gpu);
235                }
236                cmd_from_tick
237            }
238            BrowserCommand::RenameWindowTitle => {
239                if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
240                    // tick() が追加の処理を要求 → RequestRedraw に昇格させる。
241                    // RequestRedraw のハンドラはタイトル設定も行うので情報は失われない。
242                    self.redraw(window_id, gpu);
243                    BrowserCommand::RequestRedraw
244                } else {
245                    browser_cmd
246                }
247            }
248            _ => {
249                if matches!(cmd_from_tick, BrowserCommand::RequestRedraw) {
250                    self.redraw(window_id, gpu);
251                }
252                browser_cmd
253            }
254        }
255    }
256
257    /// Rebuilds the render tree and sends draw commands to the GPU for the given window.
258    pub fn redraw(&mut self, window_id: WindowId, gpu: &mut GpuRenderer) {
259        let Some(ui) = self.windows.get_mut(&window_id) else {
260            return;
261        };
262        ui.redraw(gpu);
263    }
264
265    /// Applies the current draw commands for the given window to the GPU renderer.
266    pub fn apply_draw_commands(&self, window_id: WindowId, gpu: &mut GpuRenderer) {
267        if let Some(ui) = self.windows.get(&window_id) {
268            ui.apply_draw_commands(gpu);
269        }
270    }
271
272    /// Advances background page work for a window between OS events.
273    ///
274    /// This keeps animated custom controls, such as an active audio timer,
275    /// repainting even when the user is not moving the pointer.
276    pub(crate) fn poll_window(&mut self, window_id: WindowId) -> bool {
277        matches!(self.tick(window_id), BrowserCommand::RequestRedraw)
278    }
279
280    /// Returns the current window size for the given window as `(width, height)` in floating-point pixels.
281    pub fn window_size(&self, window_id: WindowId) -> (f32, f32) {
282        match self.windows.get(&window_id) {
283            Some(ui) => {
284                let (width, height) = ui.window_size();
285                (width as f32, height as f32)
286            }
287            None => (
288                self.default_window_size.0 as f32,
289                self.default_window_size.1 as f32,
290            ),
291        }
292    }
293
294    /// Returns the window title for the given window.
295    pub fn window_title(&self, window_id: WindowId) -> String {
296        match self.windows.get(&window_id) {
297            Some(ui) => ui.window_title(),
298            None => self.default_window_title.clone(),
299        }
300    }
301
302    /// Ticks the given window's UI and forwards its fetch requests to the network.
303    fn tick(&mut self, window_id: WindowId) -> BrowserCommand {
304        self.handle_network_messages();
305
306        let Some(ui) = self.windows.get_mut(&window_id) else {
307            return BrowserCommand::None;
308        };
309        let outcome = ui.tick();
310
311        for fetch in outcome.fetches {
312            let url = fetch.request.url;
313            let kind = fetch.request.kind;
314            let initiator = fetch.request.origin;
315            log::info!("Fetch requested in App: url={}", url);
316            let mut request = match &kind {
317                FetchKind::JavaScript {
318                    method,
319                    headers,
320                    body,
321                    ..
322                } => NetworkRequest {
323                    url: url.to_string(),
324                    method: method.clone(),
325                    headers: headers.clone(),
326                    body: body.clone(),
327                },
328                _ => NetworkRequest::get(url.to_string()),
329            };
330            // Add browser-controlled Origin / Referer headers and strip any the
331            // page script supplied, so internal resource URLs never leak to
332            // external servers.
333            apply_fetch_headers(&mut request, &initiator, &kind);
334            let id = self
335                .pending_fetches
336                .insert(window_id, fetch.tab_id, kind, url);
337            // The initiator gates access to internal schemes inside the loader.
338            self.network.fetch_request_async(request, id, &initiator);
339        }
340
341        if outcome.needs_redraw {
342            BrowserCommand::RequestRedraw
343        } else {
344            BrowserCommand::None
345        }
346    }
347
348    fn handle_network_messages(&mut self) {
349        let messages = self.network.try_receive();
350
351        for msg in messages {
352            log::info!("Network message received in App for fetch_id={}", msg.id);
353
354            // pending_fetches から fetch 情報を取得
355            let Some((window_id, tab_id, kind, url)) = self.pending_fetches.remove(msg.id) else {
356                log::warn!("No pending fetch found for fetch_id={}", msg.id);
357                continue;
358            };
359
360            // 該当ウィンドウの UI へ配送
361            let Some(ui) = self.windows.get_mut(&window_id) else {
362                log::warn!("There is no window called id={:?}", window_id);
363                continue;
364            };
365            ui.deliver_fetch(&tab_id, kind, url, msg.response);
366        }
367    }
368}
369
370/// Applies the browser-controlled `Origin` / `Referer` request headers.
371///
372/// - Any `Origin` / `Referer` supplied by page scripts is stripped.
373/// - An `Origin` header is sent for CORS-mode requests (`fetch`/`XMLHttpRequest`)
374///   and for non-GET requests; the value is the initiator's serialized origin
375///   (`"null"` for opaque/internal pages), as required by the Fetch standard.
376/// - A `Referer` origin-only header is sent for network origins only; opaque
377///   pages never send one, so bundled `resource:` URLs cannot be leaked.
378fn apply_fetch_headers(request: &mut NetworkRequest, initiator: &Origin, kind: &FetchKind) {
379    request.headers.retain(|(name, _)| {
380        let name = name.to_ascii_lowercase();
381        name != "origin" && name != "referer"
382    });
383
384    if !request.url.starts_with("http://") && !request.url.starts_with("https://") {
385        return;
386    }
387
388    let is_cors_request = matches!(kind, FetchKind::JavaScript { .. }) || request.method != "GET";
389    if is_cors_request {
390        request
391            .headers
392            .push(("Origin".to_string(), initiator.ascii_serialization()));
393    }
394    if initiator.is_network() {
395        request
396            .headers
397            .push(("Referer".to_string(), initiator.ascii_serialization()));
398    }
399}
400
401fn run_with_winit_backend(app: BrowserApp) -> Result<()> {
402    configure_winit_backend_for_wslg();
403    if env::var_os("ORINIUM_FORCE_X11").is_some() {
404        configure_winit_backend_forced_x11();
405    }
406
407    run_event_loop(app)
408}
409
410fn run_event_loop(app: BrowserApp) -> Result<()> {
411    let event_loop = winit::event_loop::EventLoop::new()?;
412    event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
413    let mut app = App::new(app);
414    event_loop.run_app(&mut app)?;
415    Ok(())
416}
417
418fn configure_winit_backend_forced_x11() {
419    let current = env::var("WINIT_UNIX_BACKEND").ok();
420    let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
421
422    if should_force_x11 {
423        unsafe {
424            env::set_var("WINIT_UNIX_BACKEND", "x11");
425            env::remove_var("WAYLAND_DISPLAY");
426        }
427        log::info!("Forcing X11 (WINIT_UNIX_BACKEND=x11, WAYLAND_DISPLAY cleared)");
428    }
429}
430
431fn configure_winit_backend_for_wslg() {
432    let is_wsl = env::var_os("WSL_DISTRO_NAME").is_some() || env::var_os("WSL_INTEROP").is_some();
433    if !is_wsl {
434        return;
435    }
436
437    // On WSLg, Wayland is often unstable; default to X11 unless explicitly requested.
438    if env::var_os("ORINIUM_PREFER_WAYLAND").is_some() {
439        return;
440    }
441
442    let current = env::var("WINIT_UNIX_BACKEND").ok();
443    let should_force_x11 = !matches!(current.as_deref(), Some("x11"));
444
445    if should_force_x11 {
446        unsafe {
447            env::set_var("WINIT_UNIX_BACKEND", "x11");
448            env::remove_var("WAYLAND_DISPLAY");
449        }
450        log::info!("WSLg detected: defaulting to X11 backend for stability");
451    }
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457
458    fn js_fetch(method: &str) -> FetchKind {
459        FetchKind::JavaScript {
460            request_id: 1,
461            method: method.to_string(),
462            headers: Vec::new(),
463            body: Vec::new(),
464        }
465    }
466
467    fn header<'a>(request: &'a NetworkRequest, name: &str) -> Option<&'a str> {
468        request
469            .headers
470            .iter()
471            .find(|(n, _)| n.eq_ignore_ascii_case(name))
472            .map(|(_, v)| v.as_str())
473    }
474
475    #[test]
476    fn web_fetch_gets_browser_origin_and_origin_only_referer() {
477        let initiator = Origin::from_url_string("https://example.test/index.html");
478        let mut request = NetworkRequest::get("https://api.example.test/data".to_string());
479        request
480            .headers
481            .push(("Origin".to_string(), "https://evil.test".to_string()));
482        request
483            .headers
484            .push(("Referer".to_string(), "https://evil.test/leak".to_string()));
485
486        apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
487
488        assert_eq!(header(&request, "Origin"), Some("https://example.test"));
489        assert_eq!(header(&request, "Referer"), Some("https://example.test"));
490    }
491
492    #[test]
493    fn non_get_requests_carry_origin_without_requiring_fetch_kind() {
494        let initiator = Origin::from_url_string("https://example.test/");
495        let mut request = NetworkRequest {
496            url: "https://api.example.test/items".to_string(),
497            method: "POST".to_string(),
498            headers: Vec::new(),
499            body: b"{}".to_vec(),
500        };
501
502        apply_fetch_headers(
503            &mut request,
504            &initiator,
505            &FetchKind::Image {
506                source: "irrelevant".to_string(),
507            },
508        );
509
510        assert_eq!(header(&request, "Origin"), Some("https://example.test"));
511        assert_eq!(header(&request, "Referer"), Some("https://example.test"));
512    }
513
514    #[test]
515    fn plain_subresources_skip_origin_header() {
516        let initiator = Origin::from_url_string("https://example.test/");
517        let mut request = NetworkRequest::get("https://cdn.example.test/pic.png".to_string());
518
519        apply_fetch_headers(
520            &mut request,
521            &initiator,
522            &FetchKind::Image {
523                source: "pic".to_string(),
524            },
525        );
526
527        assert_eq!(header(&request, "Origin"), None);
528        assert_eq!(header(&request, "Referer"), Some("https://example.test"));
529    }
530
531    #[test]
532    fn opaque_pages_never_leak_referer_or_internal_scheme() {
533        let initiator = Origin::opaque();
534        let mut request = NetworkRequest::get("resource:///devtools/index.html".to_string());
535        request
536            .headers
537            .push(("Referer".to_string(), "resource:///secret".to_string()));
538
539        apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
540
541        assert!(
542            request.headers.is_empty(),
543            "headers = {:?}",
544            request.headers
545        );
546    }
547
548    #[test]
549    fn opaque_fetch_sends_null_origin_but_no_referer() {
550        let initiator = Origin::opaque();
551        let mut request = NetworkRequest::get("https://api.example.test/data".to_string());
552
553        apply_fetch_headers(&mut request, &initiator, &js_fetch("GET"));
554
555        assert_eq!(header(&request, "Origin"), Some("null"));
556        assert_eq!(header(&request, "Referer"), None);
557    }
558}