orinium_browser/browser/core/webview/
mod.rs

1//! ブラウザのwebview機能。タスクとレンダリング情報の管理を行う。
2
3use crate::engine::{
4    css::parser::Parser as CssParser,
5    html::parser::{DomTree, Parser as HtmlParser},
6    layouter::{
7        self,
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#[derive(Debug, PartialEq)]
33enum PagePhase {
34    Init,
35    BeforeHtmlParsing,
36    HtmlParsed,
37    CssPending,
38    CssApplied,
39}
40
41pub struct WebView {
42    phase: PagePhase,
43
44    docment_info: Option<DocumentInfo>,
45
46    pending_css_urls: Vec<Url>,
47    loaded_css: Vec<String>,
48
49    resolved_styles: layouter::css_resolver::ResolvedStyles,
50    layout_and_info: Option<(LayoutNode, InfoNode)>,
51
52    needs_redraw: bool,
53}
54
55/// DocumentInfo holds basic information about the HTML document.
56/// It includes the document URL, base URL, title, and DOM tree.
57///
58/// - document_url: The URL of the document.
59/// - base_url: The base URL for resolving relative URLs.
60/// - title: The title of the document.
61/// - dom: The DOM tree of the document.
62pub struct DocumentInfo {
63    document_url: Url,
64    base_url: Url,
65    title: String,
66    pub dom: DomTree,
67}
68
69/// ParsedDocument holds the result of parsing an HTML document.
70/// It includes the document URL, base URL, DOM tree, title, style links, and inline styles.
71///
72/// - document_url: The URL of the document.
73/// - base_url: The base URL for resolving relative URLs.
74/// - dom: The DOM tree of the document.
75/// - title: The title of the document.
76/// - style_links: A list of URLs for linked stylesheets.
77/// - inline_styles: A list of inline CSS styles.
78struct ParsedDocument {
79    document_url: Url,
80    base_url: Url,
81    dom: DomTree,
82    title: String,
83    style_links: Vec<Url>,
84    inline_styles: Vec<String>,
85}
86
87impl Default for WebView {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl WebView {
94    pub fn new() -> Self {
95        Self {
96            phase: PagePhase::Init,
97
98            docment_info: None,
99
100            pending_css_urls: Vec::new(),
101            loaded_css: Vec::new(),
102
103            resolved_styles: layouter::css_resolver::ResolvedStyles::default(),
104            layout_and_info: None,
105
106            needs_redraw: false,
107        }
108    }
109
110    pub fn tick(&mut self) -> Vec<WebViewTask> {
111        let mut tasks = Vec::new();
112
113        match self.phase {
114            PagePhase::Init => {
115                self.resolved_styles
116                    .extend(layouter::css_resolver::CssResolver::resolve(
117                        &CssParser::new(USER_AGENT_CSS).parse().unwrap(),
118                    ));
119
120                tasks.push(WebViewTask::AskTabHtml);
121
122                self.phase = PagePhase::BeforeHtmlParsing;
123            }
124
125            PagePhase::BeforeHtmlParsing => {}
126
127            PagePhase::HtmlParsed => {
128                // Phase 1: UA.css only layout
129                let measurer = PlatformTextMeasurer::new().unwrap();
130
131                self.update_layout_and_info(measurer);
132
133                // CSS fetch を要求
134                for url in &self.pending_css_urls {
135                    log::info!("Fetch requested in WebView: url={}", url);
136                    tasks.push(WebViewTask::Fetch {
137                        url: url.clone(),
138                        kind: FetchKind::Css,
139                    });
140                }
141
142                self.phase = PagePhase::CssPending;
143            }
144
145            PagePhase::CssPending => {
146                // CSS が揃うまで待つ
147            }
148
149            PagePhase::CssApplied => {
150                // 安定状態
151            }
152        }
153
154        tasks
155    }
156
157    pub fn on_html_fetched(&mut self, html: String, document_url: Url) {
158        log::info!("Fetched HTML: {}", document_url);
159        let parsed = parse_html(&html, document_url);
160
161        self.pending_css_urls = parsed.style_links;
162
163        let docment_info = DocumentInfo {
164            document_url: parsed.document_url,
165            base_url: parsed.base_url,
166            dom: parsed.dom,
167            title: parsed.title,
168        };
169        self.docment_info = Some(docment_info);
170
171        self.resolved_styles
172            .extend(resolve_all_css(&parsed.inline_styles));
173
174        self.phase = PagePhase::HtmlParsed;
175    }
176
177    pub fn on_css_fetched(&mut self, css: String) {
178        self.loaded_css.push(css);
179
180        if self.loaded_css.len() == self.pending_css_urls.len() {
181            print!("Apply");
182            self.apply_css_and_relayout();
183            self.phase = PagePhase::CssApplied;
184            self.needs_redraw = true;
185        }
186    }
187
188    /// Update page (e.g. DOM changed)
189    ///
190    /// This is a stub method for now.
191    pub fn update_page(&mut self) {
192        let measurer = PlatformTextMeasurer::new().unwrap();
193
194        self.update_layout_and_info(measurer);
195    }
196
197    fn apply_css_and_relayout(&mut self) {
198        self.resolved_styles
199            .extend(resolve_all_css(&self.loaded_css));
200
201        let measurer = PlatformTextMeasurer::new().unwrap();
202
203        self.update_layout_and_info(measurer);
204    }
205
206    fn update_layout_and_info(&mut self, measurer: PlatformTextMeasurer) {
207        self.layout_and_info = Some(layouter::build_layout_and_info(
208            &self.docment_info.as_ref().unwrap().dom.root,
209            &self.resolved_styles,
210            &measurer,
211            TextStyle {
212                font_size: 16.0,
213                ..Default::default()
214            },
215            Vec::new(),
216        ));
217        self.needs_redraw = true;
218    }
219
220    pub fn navigate(&mut self) {
221        self.reset_for_navigation();
222    }
223
224    fn reset_for_navigation(&mut self) {
225        if self.phase != PagePhase::Init {
226            self.phase = PagePhase::BeforeHtmlParsing;
227        }
228
229        self.docment_info = None;
230        self.pending_css_urls.clear();
231        self.loaded_css.clear();
232        self.resolved_styles.clear();
233        self.layout_and_info = None;
234
235        self.needs_redraw = false;
236    }
237
238    pub fn title(&self) -> Option<&String> {
239        self.docment_info.as_ref().map(|d| &d.title)
240    }
241
242    pub fn relayout(&mut self, viewport: (f32, f32)) {
243        let Some((layout, _info)) = self.layout_and_info.as_mut() else {
244            return;
245        };
246
247        ui_layout::LayoutEngine::layout(layout, viewport.0, viewport.1);
248    }
249
250    /// 現在描画可能な Layout / Info を返す(なければ None)
251    pub fn layout_and_info(&self) -> Option<(&LayoutNode, &InfoNode)> {
252        self.layout_and_info.as_ref().map(|(l, i)| (l, i))
253    }
254
255    pub fn layout_and_info_mut(&mut self) -> Option<(&LayoutNode, &mut InfoNode)> {
256        self.layout_and_info.as_mut().map(|(l, i)| (&*l, i))
257    }
258
259    /// Returns document info
260    pub fn document_info(&self) -> Option<&DocumentInfo> {
261        self.docment_info.as_ref()
262    }
263
264    pub fn document_url(&self) -> Option<&Url> {
265        self.docment_info.as_ref().map(|info| &info.document_url)
266    }
267
268    pub fn base_url(&self) -> Option<&Url> {
269        self.docment_info.as_ref().map(|info| &info.base_url)
270    }
271
272    pub fn needs_redraw(&self) -> bool {
273        self.needs_redraw
274    }
275
276    pub fn clear_redraw_flag(&mut self) {
277        self.needs_redraw = false;
278    }
279}
280
281fn parse_html(html: &str, document_url: Url) -> ParsedDocument {
282    // --- DOM パース ---
283    let mut parser = HtmlParser::new(html);
284    let dom = parser.parse();
285
286    // --- base_url ---
287    let base_url = dom
288        .find_all(|n| n.tag_name() == Some("base"))
289        .iter()
290        .filter_map(|node_ref| {
291            let html_node = &node_ref.borrow().value;
292            let href = html_node.get_attr("href")?;
293            document_url.join(href).ok()
294        })
295        .next()
296        .unwrap_or_else(|| document_url.clone());
297
298    // --- title 抽出 ---
299    let title = dom
300        .collect_text_by_tag("title")
301        .first()
302        .cloned()
303        .unwrap_or("".into());
304
305    // --- Style links ---
306    // <link rel="stylesheet" href="...">
307    let link_nodes = dom.find_all(|n| n.tag_name() == Some("link"));
308    let mut style_links = Vec::new();
309
310    for node in link_nodes {
311        let (rel, href) = {
312            let node_ref = node.borrow();
313            let html_node = &node_ref.value;
314
315            let rel = html_node.get_attr("rel").map(|s| s.to_string());
316            let href = html_node.get_attr("href").map(|s| s.to_string());
317            (rel, href)
318        };
319
320        if let (Some(rel), Some(href)) = (rel, href)
321            && rel == "stylesheet"
322        {
323            let css_url = match resolve_url(&base_url, &href) {
324                Ok(url) => url,
325                Err(_) => continue,
326            };
327            style_links.push(css_url);
328        }
329    }
330
331    // --- Inline styles ---
332    let inline_styles = dom.collect_text_by_tag("style");
333
334    ParsedDocument {
335        document_url,
336        base_url,
337        dom,
338        title,
339        style_links,
340        inline_styles,
341    }
342}
343
344fn resolve_all_css(css_sources: &[String]) -> layouter::css_resolver::ResolvedStyles {
345    let mut resolved = layouter::css_resolver::ResolvedStyles::default();
346
347    for css in css_sources {
348        let sheet = match CssParser::new(css).parse() {
349            Ok(sheet) => sheet,
350            Err(err) => {
351                log::error!("Failed to parse CSS: {}", err);
352                continue;
353            }
354        };
355
356        resolved.extend(layouter::css_resolver::CssResolver::resolve(&sheet));
357    }
358
359    resolved
360}
361
362pub fn resolve_url(base_url: &Url, path: &str) -> Result<Url, url::ParseError> {
363    // absolute URL(scheme を持つ)
364    if let Ok(url) = Url::parse(path) {
365        return Ok(url);
366    }
367
368    // relative URL
369    base_url.join(path)
370}