orinium_browser/browser/core/webview/
mod.rs1use 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
22pub enum FetchKind {
28 Html,
29 Css,
30}
31
32#[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
75pub struct DocumentInfo {
83 document_url: Url,
84 base_url: Url,
85 title: String,
86 pub dom: DomTree,
87}
88
89struct 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 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 self.ensure_text_measurer();
164 self.update_layout();
165
166 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 self.try_apply_css_results();
185 }
186
187 PagePhase::CssProcessing => {
188 self.try_apply_batch_result();
190 }
191
192 PagePhase::CssApplied => {
193 }
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 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 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 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 let mut parser = HtmlParser::new(html);
388 let dom = parser.parse();
389
390 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 let title = dom
404 .collect_text_by_tag("title")
405 .first()
406 .cloned()
407 .unwrap_or("".into());
408
409 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 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 if let Ok(url) = Url::parse(path) {
451 return Ok(url);
452 }
453
454 base_url.join(path)
456}