Skip to main content

orinium_browser/browser/core/webview/
mod.rs

1//! ブラウザのwebview機能。タスクとレンダリング情報の管理を行う。
2
3use crate::engine::{
4    css::{self, parser::Parser as CssParser},
5    html::parser::{DomTree, Parser as HtmlParser},
6    layouter::{
7        self, InheritedCss,
8        types::{InfoNode, TextStyle},
9    },
10};
11use crate::platform::renderer::text_measurer::PlatformTextMeasurer;
12use ui_layout::LayoutNode;
13use url::Url;
14
15const USER_AGENT_CSS: &str = include_str!("../../../../resource/user-agent.css");
16
17pub enum WebViewTask {
18    AskTabHtml,
19    Fetch { url: Url, kind: FetchKind },
20}
21
22/// TODO:
23/// - Root Document fetch
24/// - Image fetch
25/// - JS fetch
26/// - その他リソース fetch
27pub enum FetchKind {
28    Html,
29    Css,
30}
31
32/// CSS application strategy.
33///
34/// - `Batch`: wait for all external CSS to be fetched, then process everything
35///   at once on a background thread and apply the result.
36/// - `Incremental`: process each CSS file on a background thread as it arrives,
37///   applying results progressively.
38#[derive(Debug, Clone, Copy, PartialEq)]
39pub enum CssApplicationStrategy {
40    Batch,
41    Incremental,
42}
43
44#[derive(Debug, PartialEq)]
45enum PagePhase {
46    Init,
47    BeforeHtmlParsing,
48    HtmlParsed,
49    CssPending,
50    CssProcessing,
51    CssApplied,
52}
53
54pub struct WebView {
55    phase: PagePhase,
56
57    docment_info: Option<DocumentInfo>,
58
59    pending_css_urls: Vec<Url>,
60    loaded_css: Vec<String>,
61
62    resolved_styles: layouter::css_resolver::ResolvedStyles,
63    layout_and_info: Option<(LayoutNode, InfoNode)>,
64
65    needs_redraw: bool,
66
67    text_measurer: Option<PlatformTextMeasurer>,
68
69    css_processor: css::processor::CssProcessor,
70    css_strategy: CssApplicationStrategy,
71    css_results_expected: usize,
72    css_results_received: usize,
73}
74
75/// DocumentInfo holds basic information about the HTML document.
76/// It includes the document URL, base URL, title, and DOM tree.
77///
78/// - document_url: The URL of the document.
79/// - base_url: The base URL for resolving relative URLs.
80/// - title: The title of the document.
81/// - dom: The DOM tree of the document.
82pub struct DocumentInfo {
83    document_url: Url,
84    base_url: Url,
85    title: String,
86    pub dom: DomTree,
87}
88
89/// ParsedDocument holds the result of parsing an HTML document.
90/// It includes the document URL, base URL, DOM tree, title, style links, and inline styles.
91///
92/// - document_url: The URL of the document.
93/// - base_url: The base URL for resolving relative URLs.
94/// - dom: The DOM tree of the document.
95/// - title: The title of the document.
96/// - style_links: A list of URLs for linked stylesheets.
97/// - inline_styles: A list of inline CSS styles.
98struct ParsedDocument {
99    document_url: Url,
100    base_url: Url,
101    dom: DomTree,
102    title: String,
103    style_links: Vec<Url>,
104    inline_styles: Vec<String>,
105}
106
107impl Default for WebView {
108    fn default() -> Self {
109        Self::new()
110    }
111}
112
113impl WebView {
114    pub fn new() -> Self {
115        Self {
116            phase: PagePhase::Init,
117
118            docment_info: None,
119
120            pending_css_urls: Vec::new(),
121            loaded_css: Vec::new(),
122
123            resolved_styles: layouter::css_resolver::ResolvedStyles::default(),
124            layout_and_info: None,
125
126            needs_redraw: false,
127
128            text_measurer: None,
129
130            css_processor: css::processor::CssProcessor::new(),
131            css_strategy: CssApplicationStrategy::Incremental,
132            css_results_expected: 0,
133            css_results_received: 0,
134        }
135    }
136
137    /// Set the CSS application strategy.
138    ///
139    /// Default is `Incremental`.
140    pub fn set_css_strategy(&mut self, strategy: CssApplicationStrategy) {
141        self.css_strategy = strategy;
142    }
143
144    pub fn tick(&mut self) -> Vec<WebViewTask> {
145        let mut tasks = Vec::new();
146
147        match self.phase {
148            PagePhase::Init => {
149                self.resolved_styles
150                    .extend(layouter::css_resolver::CssResolver::resolve(
151                        &CssParser::new(USER_AGENT_CSS).parse().unwrap(),
152                    ));
153
154                tasks.push(WebViewTask::AskTabHtml);
155
156                self.phase = PagePhase::BeforeHtmlParsing;
157            }
158
159            PagePhase::BeforeHtmlParsing => {}
160
161            PagePhase::HtmlParsed => {
162                // Phase 1: UA.css only layout
163                self.ensure_text_measurer();
164                self.update_layout();
165
166                // CSS fetch を要求
167                if self.pending_css_urls.is_empty() {
168                    self.phase = PagePhase::CssApplied;
169                } else {
170                    for url in &self.pending_css_urls {
171                        log::info!("Fetch requested in WebView: url={}", url);
172                        tasks.push(WebViewTask::Fetch {
173                            url: url.clone(),
174                            kind: FetchKind::Css,
175                        });
176                    }
177
178                    self.phase = PagePhase::CssPending;
179                }
180            }
181
182            PagePhase::CssPending => {
183                // Poll for CSS processor results (Incremental strategy)
184                self.try_apply_css_results();
185            }
186
187            PagePhase::CssProcessing => {
188                // Poll for the single batch result (Batch strategy)
189                self.try_apply_batch_result();
190            }
191
192            PagePhase::CssApplied => {
193                // 安定状態
194            }
195        }
196
197        tasks
198    }
199
200    pub fn on_html_fetched(&mut self, html: String, document_url: Url) {
201        log::info!("Fetched HTML: {}", document_url);
202        let parsed = parse_html(&html, document_url);
203
204        self.pending_css_urls = parsed.style_links;
205        self.css_results_expected = self.pending_css_urls.len();
206
207        let docment_info = DocumentInfo {
208            document_url: parsed.document_url,
209            base_url: parsed.base_url,
210            dom: parsed.dom,
211            title: parsed.title,
212        };
213        self.docment_info = Some(docment_info);
214
215        for inline_css in &parsed.inline_styles {
216            if let Ok(sheet) = CssParser::new(inline_css).parse() {
217                self.resolved_styles
218                    .extend(layouter::css_resolver::CssResolver::resolve(&sheet));
219            }
220        }
221
222        self.phase = PagePhase::HtmlParsed;
223    }
224
225    pub fn on_css_fetched(&mut self, css: String) {
226        match self.css_strategy {
227            CssApplicationStrategy::Batch => {
228                self.loaded_css.push(css);
229
230                if self.loaded_css.len() == self.pending_css_urls.len() {
231                    let all_css = std::mem::take(&mut self.loaded_css);
232                    self.css_results_expected = 1;
233                    self.css_results_received = 0;
234                    self.css_processor.process(all_css);
235                    self.phase = PagePhase::CssProcessing;
236                }
237            }
238            CssApplicationStrategy::Incremental => {
239                self.css_processor.process(vec![css]);
240            }
241        }
242    }
243
244    /// Update page (e.g. DOM changed)
245    ///
246    /// This is a stub method for now.
247    pub fn update_page(&mut self) {
248        self.ensure_text_measurer();
249        self.update_layout();
250    }
251
252    fn apply_resolved_styles_and_relayout(
253        &mut self,
254        resolved: layouter::css_resolver::ResolvedStyles,
255    ) {
256        self.resolved_styles.extend(resolved);
257        self.update_layout();
258    }
259
260    fn try_apply_css_results(&mut self) {
261        while let Some(resolved) = self.css_processor.try_receive() {
262            self.css_results_received += 1;
263            self.apply_resolved_styles_and_relayout(resolved);
264            self.needs_redraw = true;
265
266            if self.css_results_received >= self.css_results_expected {
267                self.phase = PagePhase::CssApplied;
268            }
269        }
270    }
271
272    fn try_apply_batch_result(&mut self) {
273        if let Some(resolved) = self.css_processor.try_receive() {
274            self.css_results_received += 1;
275            self.apply_resolved_styles_and_relayout(resolved);
276            self.needs_redraw = true;
277            self.phase = PagePhase::CssApplied;
278        }
279    }
280
281    fn ensure_text_measurer(&mut self) {
282        if self.text_measurer.is_none() {
283            self.text_measurer = Some(PlatformTextMeasurer::new().unwrap());
284        }
285    }
286
287    fn build_layout(
288        docment_info: &DocumentInfo,
289        resolved_styles: &layouter::css_resolver::ResolvedStyles,
290        measurer: &PlatformTextMeasurer,
291    ) -> (LayoutNode, InfoNode) {
292        layouter::build_layout_and_info(
293            &docment_info.dom.root,
294            resolved_styles,
295            measurer,
296            InheritedCss {
297                text_style: TextStyle {
298                    font_size: 16.0,
299                    ..Default::default()
300                },
301            },
302            Vec::new(),
303        )
304    }
305
306    fn update_layout(&mut self) {
307        let doc_info = match self.docment_info.as_ref() {
308            Some(d) => d,
309            None => return,
310        };
311
312        self.layout_and_info = Some(Self::build_layout(
313            doc_info,
314            &self.resolved_styles,
315            self.text_measurer.as_ref().unwrap(),
316        ));
317        self.needs_redraw = true;
318    }
319
320    pub fn navigate(&mut self) {
321        self.reset_for_navigation();
322    }
323
324    fn reset_for_navigation(&mut self) {
325        if self.phase != PagePhase::Init {
326            self.phase = PagePhase::BeforeHtmlParsing;
327        }
328
329        self.docment_info = None;
330        self.pending_css_urls.clear();
331        self.loaded_css.clear();
332        self.resolved_styles.clear();
333        self.layout_and_info = None;
334
335        self.needs_redraw = false;
336
337        self.css_processor = css::processor::CssProcessor::new();
338        self.css_results_expected = 0;
339        self.css_results_received = 0;
340    }
341
342    pub fn title(&self) -> Option<&String> {
343        self.docment_info.as_ref().map(|d| &d.title)
344    }
345
346    pub fn relayout(&mut self, viewport: (f32, f32)) {
347        let Some((layout, _info)) = self.layout_and_info.as_mut() else {
348            return;
349        };
350
351        ui_layout::LayoutEngine::layout(layout, viewport.0, viewport.1);
352    }
353
354    /// 現在描画可能な Layout / Info を返す(なければ None)
355    pub fn layout_and_info(&self) -> Option<(&LayoutNode, &InfoNode)> {
356        self.layout_and_info.as_ref().map(|(l, i)| (l, i))
357    }
358
359    pub fn layout_and_info_mut(&mut self) -> Option<(&LayoutNode, &mut InfoNode)> {
360        self.layout_and_info.as_mut().map(|(l, i)| (&*l, i))
361    }
362
363    /// Returns document info
364    pub fn document_info(&self) -> Option<&DocumentInfo> {
365        self.docment_info.as_ref()
366    }
367
368    pub fn document_url(&self) -> Option<&Url> {
369        self.docment_info.as_ref().map(|info| &info.document_url)
370    }
371
372    pub fn base_url(&self) -> Option<&Url> {
373        self.docment_info.as_ref().map(|info| &info.base_url)
374    }
375
376    pub fn needs_redraw(&self) -> bool {
377        self.needs_redraw
378    }
379
380    pub fn clear_redraw_flag(&mut self) {
381        self.needs_redraw = false;
382    }
383}
384
385fn parse_html(html: &str, document_url: Url) -> ParsedDocument {
386    // --- DOM パース ---
387    let mut parser = HtmlParser::new(html);
388    let dom = parser.parse();
389
390    // --- base_url ---
391    let base_url = dom
392        .find_all(|n| n.tag_name() == Some("base"))
393        .iter()
394        .filter_map(|node_ref| {
395            let html_node = &node_ref.borrow().value;
396            let href = html_node.get_attr("href")?;
397            document_url.join(href).ok()
398        })
399        .next()
400        .unwrap_or_else(|| document_url.clone());
401
402    // --- title 抽出 ---
403    let title = dom
404        .collect_text_by_tag("title")
405        .first()
406        .cloned()
407        .unwrap_or("".into());
408
409    // --- Style links ---
410    // <link rel="stylesheet" href="...">
411    let link_nodes = dom.find_all(|n| n.tag_name() == Some("link"));
412    let mut style_links = Vec::new();
413
414    for node in link_nodes {
415        let (rel, href) = {
416            let node_ref = node.borrow();
417            let html_node = &node_ref.value;
418
419            let rel = html_node.get_attr("rel").map(|s| s.to_string());
420            let href = html_node.get_attr("href").map(|s| s.to_string());
421            (rel, href)
422        };
423
424        if let (Some(rel), Some(href)) = (rel, href)
425            && rel == "stylesheet"
426        {
427            let css_url = match resolve_url(&base_url, &href) {
428                Ok(url) => url,
429                Err(_) => continue,
430            };
431            style_links.push(css_url);
432        }
433    }
434
435    // --- Inline styles ---
436    let inline_styles = dom.collect_text_by_tag("style");
437
438    ParsedDocument {
439        document_url,
440        base_url,
441        dom,
442        title,
443        style_links,
444        inline_styles,
445    }
446}
447
448pub fn resolve_url(base_url: &Url, path: &str) -> Result<Url, url::ParseError> {
449    // absolute URL(scheme を持つ)
450    if let Ok(url) = Url::parse(path) {
451        return Ok(url);
452    }
453
454    // relative URL
455    base_url.join(path)
456}