1use std::sync::Arc;
4
5use crate::{
6 browser::core::{
7 resource_loader::{BrowserNetworkError, BrowserResponse},
8 webview::JsPolicy,
9 },
10 engine::{
11 html::HtmlNodeType,
12 input::HitItem,
13 js::JsFetchResponse,
14 layouter::{
15 self,
16 types::{ColorScheme, ContainerRole, CursorStyle, InfoNode, NodeKind},
17 },
18 origin::Origin,
19 permissions::Permissions,
20 renderer_model::{self, DrawCommand},
21 tree::TreeNode,
22 ui::{CustomNode, PointerEvent, input_text_types::InputTextEvent},
23 },
24};
25use ui_layout::LayoutNode;
26use url::Url;
27use winit::event::ElementState;
28
29pub use super::webview::{CssApplicationStrategy, FetchKind, WebView, WebViewTask};
30
31pub enum TabTask {
32 Fetch {
33 url: Url,
34 kind: FetchKind,
35 origin: Origin,
37 permissions: Permissions,
39 generation: u64,
41 },
42 NeedsRedraw,
43 DevToolsRequest {
45 id: u64,
46 method: String,
47 params: String,
48 },
49}
50
51#[derive(Debug)]
52enum TabError {
53 NetworkError(BrowserNetworkError),
54}
55
56#[derive(Debug)]
57enum TabState {
58 Loading,
59 Loaded,
60 Error(TabError, Option<Url>), }
62
63pub struct Tab {
72 title: Option<String>,
73 base_url: Option<Url>,
74 document_url: Option<Url>,
75 webview: Option<WebView>,
76
77 system_color_scheme: ColorScheme,
78
79 js_policy: JsPolicy,
82
83 privileged: bool,
86
87 text_measurer: Option<Arc<crate::platform::renderer::text_measurer::PlatformTextMeasurer>>,
93
94 state: TabState,
95 document_generation: u64,
97 history: Vec<Url>,
99
100 hovered: Option<Arc<dyn CustomNode>>,
102 hovered_dom_path: Vec<u32>,
104 active_dom_path: Vec<u32>,
106 pressed_dom_id: Option<u32>,
111}
112
113impl std::fmt::Debug for Tab {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 f.debug_struct("Tab")
116 .field("title", &self.title)
117 .field("base_url", &self.base_url)
118 .field("document_url", &self.document_url)
119 .field("system_color_scheme", &self.system_color_scheme)
120 .field("js_policy", &self.js_policy)
121 .field("state", &self.state)
122 .field("history", &self.history)
123 .finish()
124 }
125}
126
127impl Default for Tab {
128 fn default() -> Self {
129 Self::new(ColorScheme::default(), JsPolicy::default())
130 }
131}
132
133impl Tab {
134 pub fn new(system_color_scheme: ColorScheme, js_policy: JsPolicy) -> Self {
135 Self {
136 title: None,
137 base_url: None,
138 document_url: None,
139 webview: None,
140
141 system_color_scheme,
142
143 js_policy,
144
145 privileged: false,
146
147 text_measurer: None,
148
149 state: TabState::Loading,
150 document_generation: 0,
151 history: Vec::new(),
152
153 hovered: None,
154 hovered_dom_path: Vec::new(),
155 active_dom_path: Vec::new(),
156 pressed_dom_id: None,
157 }
158 }
159
160 pub fn set_privileged(&mut self, privileged: bool) {
163 self.privileged = privileged;
164 }
165
166 fn root_origin(&self) -> Origin {
171 if self.privileged {
172 return Origin::internal();
173 }
174 self.document_url
175 .as_ref()
176 .map(Origin::from_url)
177 .unwrap_or_else(Origin::opaque)
178 }
179
180 fn root_permissions(&self) -> Permissions {
182 if self.privileged {
183 return Permissions::chrome();
184 }
185 self.document_url
186 .as_ref()
187 .map(Permissions::content_for)
188 .unwrap_or_else(Permissions::none)
189 }
190
191 pub(crate) fn document_generation(&self) -> u64 {
192 self.document_generation
193 }
194
195 fn may_read_fetch_response(
206 &self,
207 initiator: &Origin,
208 permissions: Permissions,
209 url: &Url,
210 headers: &[(String, String)],
211 ) -> bool {
212 if permissions.is_chrome() {
213 return true;
214 }
215 match url.scheme() {
216 "http" | "https" => {
217 let response_origin = Origin::from_url_string(url.as_str());
218 initiator.same_origin(&response_origin) || headers_allow_cors(headers, initiator)
219 }
220 "data" => true,
221 _ => false,
222 }
223 }
224
225 pub(crate) fn set_animation_visible(&mut self, visible: bool) {
230 if let Some(wv) = self.webview.as_mut() {
231 wv.set_animation_visible(visible);
232 }
233 }
234
235 pub fn tick(&mut self) -> Vec<TabTask> {
236 let mut tasks = Vec::new();
237 let page_origin = self.root_origin();
238 let page_permissions = self.root_permissions();
239 let Some(wv) = self.webview.as_mut() else {
240 return tasks;
241 };
242
243 let mut navigation: Option<Url> = None;
246 for task in wv.tick() {
247 match task {
248 WebViewTask::Fetch { url, kind } => {
249 log::info!("Fetch requested in Tab: url={}", url);
250 tasks.push(TabTask::Fetch {
251 url,
252 kind,
253 origin: page_origin.clone(),
254 permissions: page_permissions,
255 generation: self.document_generation,
256 });
257 }
258 WebViewTask::Navigate { url } => {
259 log::info!("Navigating (script): {url}");
262 navigation = Some(url);
263 }
264 WebViewTask::AskTabHtml => {
265 tasks.push(TabTask::Fetch {
266 url: self.document_url.as_ref().unwrap().clone(),
267 kind: FetchKind::Html,
268 origin: page_origin.clone(),
269 permissions: page_permissions,
270 generation: self.document_generation,
271 });
272 }
273 WebViewTask::DevToolsRequest { id, method, params } => {
274 tasks.push(TabTask::DevToolsRequest { id, method, params });
275 }
276 }
277 }
278
279 if wv.needs_redraw() {
280 tasks.push(TabTask::NeedsRedraw);
281 }
282
283 if let Some(url) = navigation {
286 self.navigate(url);
287 return tasks;
288 }
289
290 tasks
291 }
292
293 pub(crate) fn deliver_fetch(
303 &mut self,
304 kind: FetchKind,
305 url: Url,
306 origin: Origin,
307 permissions: Permissions,
308 response: Result<BrowserResponse, BrowserNetworkError>,
309 generation: u64,
310 ) {
311 if generation != self.document_generation {
312 log::info!(
313 "Dropping stale fetch delivery for generation={} (current={}) url={}",
314 generation,
315 self.document_generation,
316 url
317 );
318 return;
319 }
320 match response {
321 Ok(resp) => match kind {
322 FetchKind::Html => {
323 let html = String::from_utf8_lossy(&resp.body).to_string();
324 self.on_fetch_succeeded_html(html);
325 }
326 FetchKind::Css => {
327 let css = String::from_utf8_lossy(&resp.body).to_string();
328 self.on_fetch_succeeded_css_from(css, &url);
329 }
330 FetchKind::Script { index } => {
331 let source = String::from_utf8_lossy(&resp.body).to_string();
332 self.on_fetch_succeeded_script(index, source);
333 }
334 FetchKind::DynamicScript { node_id } => {
335 let source = String::from_utf8_lossy(&resp.body).to_string();
336 self.on_fetch_succeeded_dynamic_script(node_id, source, &url);
337 }
338 FetchKind::DynamicCss { node_id } => {
339 let source = String::from_utf8_lossy(&resp.body).to_string();
340 self.on_fetch_succeeded_dynamic_style(node_id, source, &url);
341 }
342 FetchKind::CssImport { target } => {
343 let source = String::from_utf8_lossy(&resp.body).to_string();
344 self.on_fetch_succeeded_css_import(source, &target);
345 }
346 FetchKind::Image { source } => {
347 self.on_fetch_succeeded_image(source, &resp.body);
348 }
349 FetchKind::Audio { source } => {
350 self.on_fetch_succeeded_audio(source, &resp.body);
351 }
352 FetchKind::Iframe { dom_id } => {
353 let html = String::from_utf8_lossy(&resp.body).to_string();
354 match Url::parse(&resp.url) {
355 Ok(final_url) => self.on_fetch_succeeded_iframe(dom_id, html, final_url),
356 Err(error) => {
357 log::warn!(
358 "Iframe response has invalid final URL {}: {error}",
359 resp.url
360 );
361 self.on_fetch_failed_iframe(dom_id);
362 }
363 }
364 }
365 FetchKind::JavaScript { request_id, .. } => {
366 let response_url = Url::parse(&resp.url);
367 if response_url.as_ref().is_ok_and(|response_url| {
368 self.may_read_fetch_response(
369 &origin,
370 permissions,
371 response_url,
372 &resp.headers,
373 )
374 }) {
375 let redirected = resp.url != url.as_str();
376 self.on_fetch_succeeded_js(request_id, resp, redirected);
377 } else {
378 log::warn!(
379 "Blocked CORS read of {} from {}",
380 resp.url,
381 origin.ascii_serialization()
382 );
383 self.on_fetch_failed_js(
384 request_id,
385 "Cross-origin response blocked by CORS policy".to_string(),
386 );
387 }
388 }
389 },
390 Err(err) => match kind {
391 FetchKind::Image { .. } | FetchKind::Audio { .. } => {
392 log::warn!("Media fetch failed without aborting page load: {url}");
393 }
394 FetchKind::Iframe { dom_id } => {
395 log::warn!("Iframe fetch failed without aborting page load: {url}");
396 self.on_fetch_failed_iframe(dom_id);
397 }
398 FetchKind::Script { index } => {
399 log::warn!("Classic script fetch failed without aborting page load: {url}");
400 self.on_fetch_failed_script(index);
401 }
402 FetchKind::DynamicScript { node_id } => {
403 log::warn!("Dynamic script fetch failed without aborting page load: {url}");
404 self.on_fetch_failed_dynamic_script(node_id);
405 }
406 FetchKind::DynamicCss { node_id } => {
407 log::warn!("Dynamic stylesheet fetch failed without aborting page load: {url}");
408 self.on_fetch_failed_dynamic_style(node_id);
409 }
410 FetchKind::CssImport { target } => {
411 log::warn!("CSS import fetch failed without aborting page load: {url}");
412 self.on_fetch_failed_css_import(&target);
413 }
414 FetchKind::JavaScript { request_id, .. } => {
415 self.on_fetch_failed_js(request_id, err.to_string());
416 }
417 FetchKind::Html | FetchKind::Css => self.on_fetch_failed(err, url),
418 },
419 }
420 }
421
422 pub fn on_css_fetched(&mut self, css: String) {
424 log::info!("CSS fetched in Tab");
425 if let Some(webview) = self.webview.as_mut() {
426 webview.on_css_fetched(css);
427 }
428 }
429
430 pub fn on_fetch_succeeded_html(&mut self, html: String) {
432 let Some(wv) = self.webview.as_mut() else {
433 return;
434 };
435
436 wv.on_html_fetched(html, self.document_url.as_ref().unwrap().clone());
437 self.title = wv.title().cloned();
438 let base_url = wv.base_url().unwrap().clone();
439 log::info!("HTML fetched, base_url={}", base_url);
440 self.base_url = Some(base_url);
441
442 if let TabState::Error(TabError::NetworkError(err), url_opt) = &self.state {
443 let error_message = match url_opt {
444 Some(url) => format!("Failed to load {}: {}", url, err),
445 None => format!("Failed to load page: {}", err),
446 };
447
448 let error_message_element = wv
449 .document_info()
450 .unwrap()
451 .dom
452 .get_elements_by_class_name("error-message");
453 let error_message_element = error_message_element.first().unwrap();
454 let new_child = TreeNode::new(HtmlNodeType::Text(error_message));
455 TreeNode::replace_child(error_message_element, 0, new_child);
456
457 wv.update_page();
460 } else {
461 self.state = TabState::Loaded;
462 }
463 }
464
465 pub fn on_fetch_succeeded_css(&mut self, css: String) {
466 let Some(wv) = self.webview.as_mut() else {
467 return;
468 };
469
470 wv.on_css_fetched(css);
471 }
472
473 pub fn on_fetch_succeeded_css_from(&mut self, css: String, stylesheet_url: &Url) {
474 let Some(wv) = self.webview.as_mut() else {
475 return;
476 };
477 wv.on_css_fetched_from(css, stylesheet_url);
478 }
479
480 pub fn on_fetch_succeeded_image(&mut self, source: String, bytes: &[u8]) {
482 let Some(wv) = self.webview.as_mut() else {
483 return;
484 };
485 if let Err(error) = wv.on_image_fetched(source, bytes) {
486 log::warn!("Failed to decode fetched image: {error}");
487 }
488 }
489
490 pub fn on_fetch_succeeded_audio(&mut self, source: String, bytes: &[u8]) {
492 if let Some(webview) = self.webview.as_mut() {
493 webview.on_audio_fetched(source, bytes);
494 }
495 }
496
497 pub fn on_fetch_succeeded_script(&mut self, index: usize, source: String) {
499 if let Some(webview) = self.webview.as_mut() {
500 webview.on_script_fetched(index, source);
501 }
502 }
503
504 pub fn on_fetch_failed_script(&mut self, index: usize) {
506 if let Some(webview) = self.webview.as_mut() {
507 webview.on_script_fetch_failed(index);
508 }
509 }
510
511 pub fn on_fetch_succeeded_dynamic_script(&mut self, node_id: u64, source: String, url: &Url) {
512 if let Some(webview) = self.webview.as_mut() {
513 webview.on_dynamic_script_fetched(node_id, source, url);
514 }
515 }
516
517 pub fn on_fetch_failed_dynamic_script(&mut self, node_id: u64) {
518 if let Some(webview) = self.webview.as_mut() {
519 webview.on_dynamic_script_fetch_failed(node_id);
520 }
521 }
522
523 pub fn on_fetch_succeeded_dynamic_style(&mut self, node_id: u64, source: String, url: &Url) {
524 if let Some(webview) = self.webview.as_mut() {
525 webview.on_dynamic_style_fetched(node_id, source, url);
526 }
527 }
528
529 pub fn on_fetch_failed_dynamic_style(&mut self, node_id: u64) {
530 if let Some(webview) = self.webview.as_mut() {
531 webview.on_dynamic_style_fetch_failed(node_id);
532 }
533 }
534
535 pub fn on_fetch_succeeded_css_import(&mut self, source: String, url: &Url) {
537 if let Some(webview) = self.webview.as_mut() {
538 webview.on_css_import_fetched(source, url);
539 }
540 }
541
542 pub fn on_fetch_failed_css_import(&mut self, url: &Url) {
544 if let Some(webview) = self.webview.as_mut() {
545 webview.on_css_import_fetch_failed(url);
546 }
547 }
548
549 pub fn on_fetch_succeeded_iframe(&mut self, dom_id: u64, html: String, final_url: Url) {
551 if let Some(webview) = self.webview.as_mut() {
552 webview.on_iframe_fetched(dom_id, html, final_url);
553 }
554 }
555
556 pub fn on_fetch_failed_iframe(&mut self, dom_id: u64) {
559 if let Some(webview) = self.webview.as_mut() {
560 webview.on_iframe_fetch_failed(dom_id);
561 }
562 }
563
564 pub fn on_fetch_succeeded_js(
566 &mut self,
567 request_id: u64,
568 response: BrowserResponse,
569 redirected: bool,
570 ) {
571 if let Some(webview) = self.webview.as_mut() {
572 webview.on_js_fetch_succeeded(
573 request_id,
574 JsFetchResponse {
575 url: response.url,
576 status: response.status.as_u16(),
577 status_text: response.status_text,
578 redirected,
579 body: response.body,
580 headers: response.headers,
581 },
582 );
583 }
584 }
585
586 pub fn on_fetch_failed_js(&mut self, request_id: u64, reason: String) {
588 if let Some(webview) = self.webview.as_mut() {
589 webview.on_js_fetch_failed(request_id, reason);
590 }
591 }
592
593 pub fn inspect(&mut self, method: &str, params: &str) -> Result<serde_json::Value, String> {
595 match self.webview.as_mut() {
596 Some(webview) => webview.inspect(method, params),
597 None => Err("no page".to_string()),
598 }
599 }
600
601 pub fn draw(&mut self, cmd_buf: &mut Vec<DrawCommand>, width: f32, height: f32) {
602 self.relayout((width, height));
603
604 if let Some((layout, info)) = self.layout_and_info() {
605 renderer_model::generate_draw_commands(cmd_buf, layout, info, (width, height));
606 self.clear_redraw_flag();
607 } else {
608 log::debug!(target: "Tab", "No layout/info available for tab");
609 }
610 }
611
612 pub fn handle_mouse_input(&mut self, px: f32, py: f32, state: ElementState) -> (bool, bool) {
613 let Some((_, info)) = self.layout_and_info() else {
616 return (false, false);
617 };
618
619 let path = self.hit_test(px, py);
620 let dom_id = crate::engine::input::hit_dom_id(&path);
621
622 let mut repaint = false;
623
624 match state {
625 ElementState::Pressed => {
626 let dismiss_path = path.clone();
627 let pointer_consumed = crate::engine::input::dispatch_pointer(
628 &path,
629 PointerEvent::Down { x: px, y: py },
630 );
631 if !pointer_consumed {
635 repaint |= crate::engine::input::dismiss_open_popups(info, &dismiss_path, px, py);
636 }
637
638 let input_target = path.iter().find_map(|hit| {
639 if let layouter::types::NodeKind::Custom { node, .. } = &hit.info.kind
640 && node.accepts_text_input()
641 {
642 Some(Arc::clone(node))
643 } else {
644 None
645 }
646 });
647
648 let input_focused =
649 crate::engine::input::focus_text_input(info, input_target.as_ref());
650
651 let href = path.iter().find_map(|hit| {
652 if let layouter::types::NodeKind::Container { role, .. } = &hit.info.kind
653 && let layouter::types::ContainerRole::Link { href } = role
654 {
655 Some(href.clone())
656 } else {
657 None
658 }
659 });
660 let active_dom_path = crate::engine::input::hit_dom_path(&path);
661
662 repaint |= input_focused | pointer_consumed;
663
664 if let Some(href) = href {
665 self.move_to(&href);
666 }
667
668 self.pressed_dom_id = dom_id;
669 self.active_dom_path = active_dom_path;
670 if let Some(wv) = self.webview.as_mut() {
671 repaint |= wv.set_interaction_paths(
672 self.hovered_dom_path.clone(),
673 self.active_dom_path.clone(),
674 );
675 }
676 }
677
678 ElementState::Released => {
679 repaint |= crate::engine::input::dispatch_pointer(
680 &path,
681 PointerEvent::Up { x: px, y: py },
682 );
683
684 let pressed = self.pressed_dom_id.take();
685 if let (Some(pressed), Some(released)) = (pressed, dom_id)
686 && pressed == released
687 {
688 repaint |= self.on_js_click(released);
689 }
690 if !self.active_dom_path.is_empty() {
691 self.active_dom_path.clear();
692 if let Some(wv) = self.webview.as_mut() {
693 repaint |= wv.clear_active_interaction();
694 }
695 }
696 }
697 }
698
699 let (move_repaint, move_focused) = self.handle_pointer_move(px, py);
700 repaint |= move_repaint;
701
702 (repaint, move_focused)
703 }
704
705 pub fn handle_pointer_move(&mut self, px: f32, py: f32) -> (bool, bool) {
710 let move_path = self.hit_test(px, py);
711
712 let input_focused =
713 crate::engine::input::dispatch_pointer(&move_path, PointerEvent::Move { x: px, y: py });
714
715 let previous = self.hovered.as_ref();
716 let hover_changed = crate::engine::input::update_hover(&move_path, previous);
717 let next_hovered = crate::engine::input::hit_custom_node(&move_path).cloned();
718 let hovered_dom_path = crate::engine::input::hit_dom_path(&move_path);
719
720 let mut repaint = false;
721 if hover_changed {
722 repaint = true;
723 }
724 self.hovered = next_hovered;
725
726 if hovered_dom_path != self.hovered_dom_path {
727 self.hovered_dom_path = hovered_dom_path;
728 if let Some(wv) = self.webview.as_mut() {
729 repaint |= wv.set_interaction_paths(
730 self.hovered_dom_path.clone(),
731 self.active_dom_path.clone(),
732 );
733 }
734 }
735
736 (repaint, input_focused)
737 }
738
739 pub fn handle_pointer_leave(&mut self) -> bool {
741 let mut repaint = false;
742 if let Some(prev) = self.hovered.take() {
743 prev.set_hovered(false);
744 repaint = true;
745 }
746 if !self.hovered_dom_path.is_empty() || !self.active_dom_path.is_empty() {
747 self.hovered_dom_path.clear();
748 self.active_dom_path.clear();
749 if let Some(wv) = self.webview.as_mut() {
750 repaint |= wv.set_interaction_paths(Vec::new(), Vec::new());
751 }
752 }
753 self.pressed_dom_id = None;
754 repaint
755 }
756
757 #[cfg(test)]
759 pub(crate) fn has_pending_press(&self) -> bool {
760 self.pressed_dom_id.is_some()
761 }
762
763 pub fn dispatch_text_input(&self, event: InputTextEvent) -> bool {
766 let Some((_, info)) = self.layout_and_info() else {
767 return false;
768 };
769 crate::engine::input::dispatch_text_input(info, event)
770 }
771
772 pub fn defocus_text_input(&self) -> bool {
774 let Some((_, info)) = self.layout_and_info() else {
775 return false;
776 };
777 crate::engine::input::focus_text_input(info, None)
778 }
779
780 pub fn link_at(&self, px: f32, py: f32) -> Option<String> {
782 let (layout, info) = self.layout_and_info()?;
783 let path = crate::engine::input::hit_test(layout, info, px, py);
784 path.iter().find_map(|hit| {
785 if let NodeKind::Container {
786 role: ContainerRole::Link { href },
787 ..
788 } = &hit.info.kind
789 {
790 Some(href.clone())
791 } else {
792 None
793 }
794 })
795 }
796
797 pub fn scroll_at(&mut self, px: f32, py: f32, dx: f32, dy: f32, viewport: (f32, f32)) {
799 let Some(scrolled_id) = self.layout_and_info_mut().and_then(|(layout, info)| {
800 crate::engine::input::scroll_at(layout, info, viewport, px, py, dx, dy)
801 }) else {
802 return;
803 };
804 if scrolled_id != crate::engine::input::NO_SCROLL_DOM_ID
805 && let Some(wv) = self.webview.as_mut()
806 {
807 wv.on_js_scroll(scrolled_id);
808 }
809 }
810
811 pub fn has_layout(&self) -> bool {
813 self.layout_and_info().is_some()
814 }
815
816 pub fn is_text_input_composing(&self) -> bool {
819 let Some((_, info)) = self.layout_and_info() else {
820 return false;
821 };
822 crate::engine::input::focused_text_input_is_composing(info)
823 }
824
825 fn hit_test<'a>(&'a self, px: f32, py: f32) -> Vec<HitItem<'a>> {
826 let Some((layout, info)) = self.layout_and_info() else {
827 return vec![];
828 };
829
830 crate::engine::input::hit_test(layout, info, px, py)
831 }
832
833 pub fn on_devtools_response(&mut self, id: u64, result: String) {
835 if let Some(webview) = self.webview.as_mut() {
836 webview.on_devtools_response(id, result);
837 }
838 }
839
840 pub fn on_fetch_failed(&mut self, err: BrowserNetworkError, failed_url: Url) {
842 self.navigate("resource:///error.html".parse().unwrap());
843 self.state = TabState::Error(TabError::NetworkError(err), Some(failed_url));
844 }
845
846 pub fn cursor_at(&self, x: f32, y: f32) -> Option<CursorStyle> {
850 self.webview.as_ref().and_then(|wv| wv.cursor_at(x, y))
851 }
852
853 pub fn navigate(&mut self, url: Url) {
854 self.navigate_internal(url, true);
855 }
856
857 pub fn go_back(&mut self) -> bool {
861 let Some(previous) = self.history.pop() else {
862 return false;
863 };
864 self.navigate_internal(previous, false);
865 true
866 }
867
868 pub fn reload(&mut self) {
870 if let Some(url) = self.document_url.clone() {
871 self.navigate_internal(url, false);
872 }
873 }
874
875 pub fn can_go_back(&self) -> bool {
877 !self.history.is_empty()
878 }
879
880 fn ensure_text_measurer(&mut self) {
885 if self.text_measurer.is_none() {
886 self.text_measurer =
887 crate::platform::renderer::text_measurer::PlatformTextMeasurer::new()
888 .ok()
889 .map(Arc::new);
890 }
891 }
892
893 fn navigate_internal(&mut self, url: Url, record_history: bool) {
894 if record_history
895 && self.document_url.as_ref() != Some(&url)
896 && let Some(previous) = self.document_url.clone()
897 {
898 self.history.push(previous);
899 }
900 self.document_generation = self.document_generation.saturating_add(1);
901 self.document_url = Some(url);
902 self.ensure_text_measurer();
903 let mut webview = WebView::new(self.system_color_scheme, self.js_policy);
904 webview.set_privileged(self.privileged);
905 webview.set_text_measurer(self.text_measurer.clone());
906 webview.navigate();
907 self.webview = Some(webview);
908 self.state = TabState::Loading;
909 self.title = None;
910 self.base_url = None;
911 self.hovered = None;
912 self.hovered_dom_path.clear();
913 self.active_dom_path.clear();
914 self.pressed_dom_id = None;
915 }
916
917 pub fn move_to(&mut self, href: &str) {
918 let Some(base_url) = self.base_url.clone() else {
919 return;
920 };
921
922 let url = super::webview::resolve_url(&base_url, href).unwrap();
923
924 if same_document_navigation(self.document_url.as_ref(), &url) {
928 if self.document_url.as_ref() != Some(&url)
929 && let Some(previous) = self.document_url.clone()
930 {
931 self.history.push(previous);
932 }
933 self.document_url = Some(url.clone());
934 if let Some(webview) = self.webview.as_mut() {
935 webview.scroll_to_fragment(&url);
936 }
937 return;
938 }
939
940 self.navigate(url)
942 }
943
944 pub fn relayout(&mut self, viewport: (f32, f32)) {
945 if let Some(wv) = self.webview.as_mut() {
946 wv.relayout(viewport);
947 }
948 }
949
950 pub(crate) fn layout_and_info_mut(&mut self) -> Option<(&LayoutNode, &mut InfoNode)> {
953 self.webview
954 .as_mut()
955 .and_then(|wv| wv.layout_and_info_mut())
956 }
957
958 pub fn on_js_click(&mut self, dom_id: u32) -> bool {
962 self.webview
963 .as_mut()
964 .is_some_and(|wv| wv.on_js_click(dom_id))
965 }
966
967 pub fn set_system_color_scheme(&mut self, scheme: ColorScheme) {
968 self.system_color_scheme = scheme;
969 if let Some(wv) = self.webview.as_mut() {
970 wv.set_system_color_scheme(scheme)
971 }
972 }
973
974 pub fn title(&self) -> Option<String> {
976 self.title.clone()
977 }
978
979 pub fn document_url(&self) -> Option<Url> {
981 self.document_url.clone()
982 }
983
984 pub(crate) fn layout_and_info(&self) -> Option<(&LayoutNode, &InfoNode)> {
985 self.webview.as_ref().and_then(|wv| wv.layout_and_info())
986 }
987
988 pub fn needs_redraw(&self) -> bool {
989 self.webview.as_ref().is_some_and(|wv| wv.needs_redraw())
990 }
991
992 pub fn clear_redraw_flag(&mut self) {
993 if let Some(wv) = self.webview.as_mut() {
994 wv.clear_redraw_flag();
995 }
996 }
997
998 pub fn set_js_policy(&mut self, policy: JsPolicy) {
999 self.js_policy = policy;
1000 if let Some(wv) = self.webview.as_mut() {
1001 wv.set_js_policy(policy);
1002 }
1003 }
1004}
1005
1006fn headers_allow_cors(headers: &[(String, String)], initiator: &Origin) -> bool {
1011 let serialized = initiator.ascii_serialization();
1012 headers
1013 .iter()
1014 .find(|(name, _)| name.eq_ignore_ascii_case("access-control-allow-origin"))
1015 .is_some_and(|(_, value)| value == "*" || value == &serialized)
1016}
1017
1018fn same_document_navigation(current: Option<&Url>, next: &Url) -> bool {
1021 let Some(current) = current else {
1022 return false;
1023 };
1024 fn identity(url: &Url) -> (String, Option<String>, Option<u16>, String, Option<String>) {
1025 (
1026 url.scheme().to_string(),
1027 url.host_str().map(str::to_string),
1028 url.port(),
1029 url.path().to_string(),
1030 url.query().map(str::to_string),
1031 )
1032 }
1033 identity(next) == identity(current)
1034}
1035
1036#[cfg(test)]
1037mod tests {
1038 use super::*;
1039 use crate::test_support::parse_url as url;
1040
1041 #[test]
1042 fn navigate_records_history() {
1043 let mut tab = Tab::default();
1044 tab.navigate(url("https://example.test/a"));
1045 tab.navigate(url("https://example.test/b"));
1046
1047 assert!(tab.can_go_back());
1048 assert_eq!(
1049 tab.document_url().as_ref().map(Url::as_str),
1050 Some("https://example.test/b")
1051 );
1052 }
1053
1054 #[test]
1055 fn navigating_to_same_url_does_not_duplicate_history() {
1056 let mut tab = Tab::default();
1057 tab.navigate(url("https://example.test/a"));
1058 tab.navigate(url("https://example.test/a"));
1059 assert!(!tab.can_go_back());
1060 }
1061
1062 #[test]
1063 fn go_back_restores_previous_url() {
1064 let mut tab = Tab::default();
1065 tab.navigate(url("https://example.test/a"));
1066 tab.navigate(url("https://example.test/b"));
1067
1068 assert!(tab.go_back());
1069 assert_eq!(
1070 tab.document_url().as_ref().map(Url::as_str),
1071 Some("https://example.test/a")
1072 );
1073 assert!(!tab.can_go_back());
1074 assert!(!tab.go_back());
1076 }
1077
1078 #[test]
1079 fn reload_keeps_url_without_recording_history() {
1080 let mut tab = Tab::default();
1081 tab.navigate(url("https://example.test/a"));
1082 tab.reload();
1083 assert_eq!(
1084 tab.document_url().as_ref().map(Url::as_str),
1085 Some("https://example.test/a")
1086 );
1087 assert!(!tab.can_go_back());
1088 }
1089
1090 #[test]
1091 fn fragment_only_url_change_is_a_same_document_navigation() {
1092 assert!(same_document_navigation(
1093 Some(&url("https://example.test/a#one")),
1094 &url("https://example.test/a#two"),
1095 ));
1096 assert!(same_document_navigation(
1097 Some(&url("https://example.test/a?q=1#one")),
1098 &url("https://example.test/a?q=1#two"),
1099 ));
1100 assert!(same_document_navigation(
1101 Some(&url("https://example.test/a")),
1102 &url("https://example.test/a"),
1103 ));
1104 assert!(!same_document_navigation(
1105 Some(&url("https://example.test/a")),
1106 &url("https://example.test/b"),
1107 ));
1108 assert!(!same_document_navigation(
1109 Some(&url("https://www.example.test/a#one")),
1110 &url("https://example.test/a#one"),
1111 ));
1112 assert!(!same_document_navigation(
1113 Some(&url("https://example.test/a?q=1")),
1114 &url("https://example.test/a?q=2#x"),
1115 ));
1116 assert!(!same_document_navigation(
1117 None,
1118 &url("https://example.test/a#x")
1119 ));
1120 }
1121
1122 #[test]
1123 fn pointer_move_between_press_and_release_keeps_click_pending() {
1124 let mut tab = Tab::default();
1125 tab.navigate(url("https://example.test/a"));
1126 tab.on_fetch_succeeded_html("<html><body><p>click me</p></body></html>".to_string());
1127
1128 let mut buf = Vec::new();
1131 tab.draw(&mut buf, 800.0, 600.0);
1132 for _ in 0..500 {
1133 for _ in tab.tick() {}
1134 if tab.layout_and_info().is_some() {
1135 break;
1136 }
1137 std::thread::sleep(std::time::Duration::from_millis(2));
1138 }
1139 tab.draw(&mut buf, 800.0, 600.0);
1140
1141 tab.handle_mouse_input(400.0, 12.0, ElementState::Pressed);
1144 assert!(tab.has_pending_press(), "press should land on an element");
1145
1146 tab.handle_pointer_move(401.0, 13.0);
1147 assert!(
1148 tab.has_pending_press(),
1149 "a pointer move must not cancel an in-flight press"
1150 );
1151
1152 tab.handle_mouse_input(401.0, 13.0, ElementState::Released);
1154 assert!(!tab.has_pending_press());
1155 }
1156
1157 #[test]
1158 fn select_click_lands_on_custom_node() {
1159 use crate::engine::layouter::types::{InfoNode, NodeKind, TextFlowStyle, TextStyle};
1160
1161 let mut tab = Tab::default();
1162 tab.navigate(url("https://example.test/a"));
1163 tab.on_fetch_succeeded_html(
1164 "<html><body><select id=\"sel\"><option>One</option><option>Two</option></select></body></html>"
1165 .to_string(),
1166 );
1167
1168 let mut buf = Vec::new();
1171 tab.draw(&mut buf, 800.0, 600.0);
1172 for _ in 0..500 {
1173 for _ in tab.tick() {}
1174 if tab.layout_and_info().is_some() {
1175 break;
1176 }
1177 std::thread::sleep(std::time::Duration::from_millis(2));
1178 }
1179 tab.draw(&mut buf, 800.0, 600.0);
1180
1181 let (cx_base, cy_base, width, height, select_node) = {
1182 let (layout, info) = tab.layout_and_info().unwrap();
1183
1184 fn find_custom_box<'a>(
1186 layout: &'a ui_layout::LayoutNode,
1187 info: &'a InfoNode,
1188 ) -> Option<(f32, f32, f32, f32, &'a Arc<dyn CustomNode>)> {
1189 if let NodeKind::Custom { node, .. } = &info.kind {
1190 if let Some(bm) = layout.layout_box.iter().next() {
1191 let (x, y, w, h) = (
1192 bm.content_box.x,
1193 bm.content_box.y,
1194 bm.border_box.width,
1195 bm.border_box.height,
1196 );
1197 return Some((x, y, w, h, &node));
1198 }
1199 }
1200 for (child_layout, child_info) in layout.children.iter().zip(&info.children) {
1201 if let Some(child_node) = child_layout.node() {
1202 if let Some(result) = find_custom_box(child_node, child_info) {
1203 return Some(result);
1204 }
1205 }
1206 }
1207 None
1208 }
1209
1210 let Some((cx_base, cy_base, width, height, select_node)) =
1211 find_custom_box(layout, info)
1212 else {
1213 panic!("select custom node not found in layout tree");
1214 };
1215 (cx_base, cy_base, width, height, Arc::clone(select_node))
1216 };
1217
1218 assert!(
1220 select_node
1221 .popup(&TextStyle::default(), &TextFlowStyle::default())
1222 .is_none(),
1223 "popup should start closed"
1224 );
1225
1226 let cx = cx_base + width / 2.0;
1228 let cy = cy_base + height / 2.0;
1229 tab.handle_mouse_input(cx, cy, ElementState::Pressed);
1230 assert!(
1231 tab.has_pending_press(),
1232 "press should land on the select element"
1233 );
1234 tab.handle_mouse_input(cx, cy, ElementState::Released);
1235 assert!(!tab.has_pending_press());
1236
1237 assert!(
1239 select_node
1240 .popup(&TextStyle::default(), &TextFlowStyle::default())
1241 .is_some(),
1242 "popup should be open after clicking select box"
1243 );
1244
1245 assert!(
1247 select_node
1248 .popup(&TextStyle::default(), &TextFlowStyle::default())
1249 .is_some(),
1250 "popup should be open after clicking select box"
1251 );
1252
1253 tab.handle_mouse_input(cx, cy, ElementState::Pressed);
1255 assert!(tab.has_pending_press());
1256 tab.handle_mouse_input(cx, cy, ElementState::Released);
1257 assert!(!tab.has_pending_press());
1258 assert!(
1259 select_node
1260 .popup(&TextStyle::default(), &TextFlowStyle::default())
1261 .is_none(),
1262 "popup should close when clicking select box again (toggle)"
1263 );
1264 }
1265
1266 #[test]
1267 fn cross_origin_fetch_requires_matching_cors_header() {
1268 let tab = Tab::default();
1269 let web_origin = Origin::from_url_string("https://example.test/index.html");
1270 let permissions = Permissions::content_for_str("https://example.test/index.html");
1271 let url = Url::parse("https://other.test/data.json").unwrap();
1272
1273 assert!(!tab.may_read_fetch_response(&web_origin, permissions, &url, &[]));
1274
1275 let wildcard = vec![("Access-Control-Allow-Origin".to_string(), "*".to_string())];
1276 assert!(tab.may_read_fetch_response(&web_origin, permissions, &url, &wildcard));
1277
1278 let matching = vec![(
1279 "Access-Control-Allow-Origin".to_string(),
1280 "https://example.test".to_string(),
1281 )];
1282 assert!(tab.may_read_fetch_response(&web_origin, permissions, &url, &matching));
1283
1284 let other = vec![(
1285 "Access-Control-Allow-Origin".to_string(),
1286 "https://other.test".to_string(),
1287 )];
1288 assert!(!tab.may_read_fetch_response(&web_origin, permissions, &url, &other));
1289 }
1290
1291 #[test]
1292 fn same_origin_fetch_is_readable_without_cors_header() {
1293 let tab = Tab::default();
1294 let web_origin = Origin::from_url_string("https://example.test/");
1295 let permissions = Permissions::content_for_str("https://example.test/");
1296 let url = Url::parse("https://example.test:443/api").unwrap();
1297 assert!(tab.may_read_fetch_response(&web_origin, permissions, &url, &[]));
1298 }
1299
1300 #[test]
1301 fn internal_page_reads_any_response_without_cors() {
1302 let tab = Tab::default();
1303 let internal = Origin::internal();
1304 let url = Url::parse("https://other.test/api").unwrap();
1305 assert!(tab.may_read_fetch_response(&internal, Permissions::chrome(), &url, &[]));
1306 }
1307
1308 #[test]
1309 fn data_page_cannot_read_internal_scheme_responses() {
1310 let tab = Tab::default();
1311 let data_origin = Origin::from_url(&Url::parse("data:text/html,test").unwrap());
1312 let permissions = Permissions::content_for_str("data:text/html,test");
1313 let resource_url = Url::parse("resource:///devtools/index.html").unwrap();
1314 assert!(!tab.may_read_fetch_response(&data_origin, permissions, &resource_url, &[]));
1315 let file_url = Url::parse("file:///etc/passwd").unwrap();
1316 assert!(!tab.may_read_fetch_response(&data_origin, permissions, &file_url, &[]));
1317 }
1318
1319 #[test]
1320 fn opaque_origin_cannot_read_internal_scheme_responses() {
1321 let tab = Tab::default();
1322 let opaque = Origin::opaque();
1323 let resource_url = Url::parse("resource:///devtools/index.html").unwrap();
1324 assert!(!tab.may_read_fetch_response(&opaque, Permissions::none(), &resource_url, &[]));
1325 let file_url = Url::parse("file:///etc/passwd").unwrap();
1326 assert!(!tab.may_read_fetch_response(&opaque, Permissions::none(), &file_url, &[]));
1327 }
1328
1329 #[test]
1330 fn web_page_reads_internal_scheme_responses_without_cors() {
1331 let tab = Tab::default();
1334 let web_origin = Origin::from_url_string("https://example.test/");
1335 let permissions = Permissions::content_for_str("https://example.test/");
1336 let url = Url::parse("data:text/plain,hello").unwrap();
1337 assert!(tab.may_read_fetch_response(&web_origin, permissions, &url, &[]));
1338 }
1339
1340 #[test]
1341 fn privileged_tab_holds_chrome_permissions() {
1342 let mut tab = Tab::default();
1343 tab.set_privileged(true);
1344 tab.navigate("resource:///devtools/index.html".parse().unwrap());
1345 assert_eq!(tab.root_permissions(), Permissions::chrome());
1346 assert!(tab.root_origin().is_internal());
1347 }
1348
1349 #[test]
1350 fn ordinary_resource_page_gets_no_chrome_permissions() {
1351 let mut tab = Tab::default();
1352 tab.navigate("resource:///test/index.html".parse().unwrap());
1353 let permissions = tab.root_permissions();
1354 assert!(!permissions.is_chrome());
1355 assert!(!permissions.file);
1356 assert!(permissions.resource);
1357 assert!(!tab.root_origin().is_internal());
1358 }
1359
1360 #[test]
1361 fn navigating_bumps_document_generation() {
1362 let mut tab = Tab::default();
1365 let before = tab.document_generation();
1366 tab.navigate("resource:///test/a.html".parse().unwrap());
1367 assert_eq!(tab.document_generation(), before + 1);
1368 tab.navigate("resource:///test/b.html".parse().unwrap());
1369 assert_eq!(tab.document_generation(), before + 2);
1370 }
1371
1372 #[test]
1373 fn navigation_reuses_the_same_text_measurer() {
1374 let mut tab = Tab::default();
1378 tab.navigate("resource:///test/a.html".parse().unwrap());
1379 let Some(first) = tab.text_measurer.clone() else {
1380 return;
1381 };
1382 tab.navigate("resource:///test/b.html".parse().unwrap());
1383 let Some(second) = tab.text_measurer.clone() else {
1384 panic!("the measurer must outlive a navigation");
1385 };
1386 assert!(Arc::ptr_eq(&first, &second));
1387 assert!(Arc::ptr_eq(
1389 &first,
1390 tab.webview.as_ref().unwrap().text_measurer().unwrap()
1391 ));
1392 }
1393
1394 #[test]
1395 fn headers_allow_cors_requires_exact_origin_value() {
1396 let initiator = Origin::from_url_string("https://example.test/");
1397 assert!(headers_allow_cors(
1398 &[("Access-Control-Allow-Origin".to_string(), "*".to_string())],
1399 &initiator
1400 ));
1401 assert!(headers_allow_cors(
1402 &[(
1403 "ACCEss-cOntRoL-aLLow-orIgIn".to_string(),
1404 "https://example.test".to_string()
1405 )],
1406 &initiator
1407 ));
1408 assert!(!headers_allow_cors(
1409 &[(
1410 "Access-Control-Allow-Origin".to_string(),
1411 "https://other.test".to_string()
1412 )],
1413 &initiator
1414 ));
1415 }
1416
1417 #[test]
1418 fn scheme_capabilities_do_not_bypass_cors() {
1419 let tab = Tab::default();
1422 let initiator = Origin::from_url_string("https://a.test/");
1423 let permissions = Permissions {
1424 network: true,
1425 resource: true,
1426 file: true,
1427 privileged: false,
1428 };
1429 let url = Url::parse("https://b.test/x").unwrap();
1430
1431 assert!(!tab.may_read_fetch_response(&initiator, permissions, &url, &[]));
1432 assert!(tab.may_read_fetch_response(
1433 &initiator,
1434 permissions,
1435 &url,
1436 &[(
1437 "Access-Control-Allow-Origin".to_string(),
1438 "https://a.test".to_string()
1439 )],
1440 ));
1441 }
1442
1443 #[test]
1444 fn privilege_is_the_only_cors_bypass() {
1445 let tab = Tab::default();
1446 let initiator = Origin::from_url_string("https://a.test/");
1447 let url = Url::parse("https://b.test/x").unwrap();
1448
1449 let privileged = Permissions {
1450 network: true,
1451 resource: true,
1452 file: true,
1453 privileged: true,
1454 };
1455 assert!(tab.may_read_fetch_response(&initiator, privileged, &url, &[]));
1456
1457 let unprivileged = Permissions {
1458 privileged: false,
1459 ..privileged
1460 };
1461 assert!(!tab.may_read_fetch_response(&initiator, unprivileged, &url, &[]));
1462 }
1463
1464 #[test]
1465 fn cors_headers_do_not_grant_local_file_read() {
1466 let tab = Tab::default();
1468 let initiator = Origin::from_url_string("https://a.test/");
1469 let permissions = Permissions::content_for_str("https://a.test/");
1470 let url = Url::parse("file:///etc/passwd").unwrap();
1471
1472 assert!(!tab.may_read_fetch_response(
1473 &initiator,
1474 permissions,
1475 &url,
1476 &[("Access-Control-Allow-Origin".to_string(), "*".to_string())],
1477 ));
1478 }
1479}