1use std::cell::{Cell, Ref, RefCell};
8use std::collections::HashMap;
9use std::collections::hash_map::Entry;
10use std::rc::Rc;
11
12use crossbeam_channel::{Receiver, Sender, unbounded};
13use euclid::Rect;
14use image::{DynamicImage, ImageFormat, RgbaImage};
15use log::{error, info, warn};
16use servo::{
17 AllowOrDenyRequest, AuthenticationRequest, CSSPixel, DeviceIntPoint, DeviceIntSize,
18 EmbedderControl, EmbedderControlId, EventLoopWaker, GamepadHapticEffectType, GenericSender,
19 InputEvent, InputEventId, InputEventResult, IpcSender, JSValue, LoadStatus, MediaSessionEvent,
20 PermissionRequest, PrefValue, ScreenshotCaptureError, Servo, ServoDelegate, ServoError,
21 TraversalId, WebDriverCommandMsg, WebDriverJSResult, WebDriverLoadStatus,
22 WebDriverScriptCommand, WebDriverSenders, WebView, WebViewBuilder, WebViewDelegate, WebViewId,
23 pref,
24};
25use url::Url;
26
27use crate::GamepadSupport;
28use crate::prefs::{EXPERIMENTAL_PREFS, ServoShellPreferences};
29use crate::webdriver::WebDriverEmbedderControls;
30use crate::window::{PlatformWindow, ServoShellWindow, ServoShellWindowId};
31
32#[derive(Default)]
33pub struct WebViewCollection {
34 webviews: HashMap<WebViewId, WebView>,
38
39 pub(crate) creation_order: Vec<WebViewId>,
41
42 active_webview_id: Option<WebViewId>,
45}
46
47impl WebViewCollection {
48 pub fn add(&mut self, webview: WebView) {
49 let id = webview.id();
50 self.creation_order.push(id);
51 self.webviews.insert(id, webview);
52 }
53
54 pub fn remove(&mut self, id: WebViewId) -> Option<WebView> {
57 self.creation_order.retain(|&webview_id| webview_id != id);
58 let removed_webview = self.webviews.remove(&id);
59
60 if self.active_webview_id == Some(id) {
61 self.active_webview_id = None;
62 if let Some(newest) = self.creation_order.last() {
63 self.activate_webview(*newest);
64 }
65 }
66
67 removed_webview
68 }
69
70 pub fn get(&self, id: WebViewId) -> Option<&WebView> {
71 self.webviews.get(&id)
72 }
73
74 pub fn contains(&self, id: WebViewId) -> bool {
75 self.webviews.contains_key(&id)
76 }
77
78 pub fn active(&self) -> Option<&WebView> {
79 self.active_webview_id.and_then(|id| self.webviews.get(&id))
80 }
81
82 pub fn active_id(&self) -> Option<WebViewId> {
83 self.active_webview_id
84 }
85
86 pub fn newest(&self) -> Option<&WebView> {
88 self.creation_order
89 .last()
90 .and_then(|id| self.webviews.get(id))
91 }
92
93 pub fn all_in_creation_order(&self) -> impl Iterator<Item = (WebViewId, &WebView)> {
94 self.creation_order
95 .iter()
96 .filter_map(move |id| self.webviews.get(id).map(|webview| (*id, webview)))
97 }
98
99 pub fn values(&self) -> impl Iterator<Item = &WebView> {
101 self.webviews.values()
102 }
103
104 pub fn is_empty(&self) -> bool {
106 self.webviews.is_empty()
107 }
108
109 pub(crate) fn activate_webview(&mut self, id_to_activate: WebViewId) {
110 assert!(self.creation_order.contains(&id_to_activate));
111
112 self.active_webview_id = Some(id_to_activate);
113 for (webview_id, webview) in self.all_in_creation_order() {
114 if id_to_activate == webview_id {
115 webview.show();
116 webview.focus();
117 } else {
118 webview.hide();
119 webview.blur();
120 }
121 }
122 }
123
124 pub(crate) fn activate_webview_by_index(&mut self, index: usize) {
125 self.activate_webview(
126 *self
127 .creation_order
128 .get(index)
129 .expect("Tried to activate an unknown WebView"),
130 );
131 }
132}
133
134#[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
136pub(crate) enum UserInterfaceCommand {
137 Go(String),
138 Back,
139 Forward,
140 Reload,
141 ReloadAll,
142 NewWebView,
143 CloseWebView(WebViewId),
144 NewWindow,
145}
146
147pub(crate) struct RunningAppState {
148 gamepad_support: RefCell<Option<GamepadSupport>>,
150
151 pub(crate) webdriver_senders: RefCell<WebDriverSenders>,
153
154 pub(crate) webdriver_embedder_controls: WebDriverEmbedderControls,
158
159 pub(crate) pending_webdriver_events: RefCell<HashMap<InputEventId, Sender<()>>>,
163
164 pub(crate) webdriver_receiver: Option<Receiver<WebDriverCommandMsg>>,
167
168 pub(crate) servoshell_preferences: ServoShellPreferences,
170
171 pub(crate) servo: Servo,
173
174 pub(crate) achieved_stable_image: Rc<Cell<bool>>,
177
178 exit_scheduled: Cell<bool>,
181
182 experimental_preferences_enabled: Cell<bool>,
184
185 windows: RefCell<HashMap<ServoShellWindowId, Rc<ServoShellWindow>>>,
190}
191
192impl RunningAppState {
193 pub(crate) fn new(
194 servo: Servo,
195 servoshell_preferences: ServoShellPreferences,
196 event_loop_waker: Box<dyn EventLoopWaker>,
197 ) -> Self {
198 servo.set_delegate(Rc::new(ServoShellServoDelegate));
199
200 let gamepad_support = if pref!(dom_gamepad_enabled) {
201 GamepadSupport::maybe_new()
202 } else {
203 None
204 };
205
206 let webdriver_receiver = servoshell_preferences.webdriver_port.get().map(|port| {
207 let (embedder_sender, embedder_receiver) = unbounded();
208 webdriver_server::start_server(port, embedder_sender, event_loop_waker);
209 embedder_receiver
210 });
211
212 let experimental_preferences_enabled =
213 Cell::new(servoshell_preferences.experimental_preferences_enabled);
214
215 Self {
216 windows: Default::default(),
217 gamepad_support: RefCell::new(gamepad_support),
218 webdriver_senders: RefCell::default(),
219 webdriver_embedder_controls: Default::default(),
220 pending_webdriver_events: Default::default(),
221 webdriver_receiver,
222 servoshell_preferences,
223 servo,
224 achieved_stable_image: Default::default(),
225 exit_scheduled: Default::default(),
226 experimental_preferences_enabled,
227 }
228 }
229
230 pub(crate) fn open_window(
231 self: &Rc<Self>,
232 platform_window: Rc<dyn PlatformWindow>,
233 initial_url: Url,
234 ) {
235 let window = Rc::new(ServoShellWindow::new(platform_window));
236 window.create_and_activate_toplevel_webview(self.clone(), initial_url);
237 self.windows.borrow_mut().insert(window.id(), window);
238 }
239
240 pub(crate) fn windows<'a>(
241 &'a self,
242 ) -> Ref<'a, HashMap<ServoShellWindowId, Rc<ServoShellWindow>>> {
243 self.windows.borrow()
244 }
245
246 pub(crate) fn any_window(&self) -> Rc<ServoShellWindow> {
249 self.windows
250 .borrow()
251 .values()
252 .next()
253 .expect("Should always have at least one window open when running WebDriver")
254 .clone()
255 }
256
257 pub(crate) fn focused_window(&self) -> Option<Rc<ServoShellWindow>> {
258 self.windows
259 .borrow()
260 .values()
261 .find(|window| window.focused())
262 .cloned()
263 }
264
265 #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
266 pub(crate) fn window(&self, id: ServoShellWindowId) -> Option<Rc<ServoShellWindow>> {
267 self.windows.borrow().get(&id).cloned()
268 }
269
270 pub(crate) fn webview_by_id(&self, webview_id: WebViewId) -> Option<WebView> {
271 self.maybe_window_for_webview_id(webview_id)?
272 .webview_by_id(webview_id)
273 }
274
275 pub(crate) fn webdriver_receiver(&self) -> Option<&Receiver<WebDriverCommandMsg>> {
276 self.webdriver_receiver.as_ref()
277 }
278
279 pub(crate) fn servo(&self) -> &Servo {
280 &self.servo
281 }
282
283 pub(crate) fn schedule_exit(&self) {
284 self.servoshell_preferences.webdriver_port.set(None);
290 self.exit_scheduled.set(true);
291 }
292
293 #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
294 pub(crate) fn experimental_preferences_enabled(&self) -> bool {
295 self.experimental_preferences_enabled.get()
296 }
297
298 #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
299 pub(crate) fn set_experimental_preferences_enabled(&self, new_value: bool) {
300 let old_value = self.experimental_preferences_enabled.replace(new_value);
301 if old_value == new_value {
302 return;
303 }
304 for pref in EXPERIMENTAL_PREFS {
305 self.servo.set_preference(pref, PrefValue::Bool(new_value));
306 }
307 }
308
309 pub(crate) fn spin_event_loop(self: &Rc<Self>) -> bool {
316 self.handle_webdriver_messages();
317
318 if pref!(dom_gamepad_enabled) {
319 self.handle_gamepad_events();
320 }
321
322 self.servo.spin_event_loop();
323
324 for window in self.windows.borrow().values() {
325 window.update_and_request_repaint_if_necessary(self);
326 }
327
328 if self.servoshell_preferences.exit_after_stable_image && self.achieved_stable_image.get() {
329 self.schedule_exit();
330 }
331
332 if self.servoshell_preferences.webdriver_port.get().is_none() {
336 self.windows
337 .borrow_mut()
338 .retain(|_, window| !self.exit_scheduled.get() && !window.should_close());
339 if self.windows.borrow().is_empty() {
340 self.schedule_exit()
341 }
342 }
343
344 !self.exit_scheduled.get()
345 }
346
347 #[cfg_attr(any(target_os = "android", target_env = "ohos"), expect(dead_code))]
348 pub(crate) fn foreach_window_and_interface_commands(
349 self: &Rc<Self>,
350 callback: impl Fn(&ServoShellWindow, Vec<UserInterfaceCommand>),
351 ) {
352 let windows: Vec<_> = self.windows.borrow().values().cloned().collect();
354 for window in windows {
355 callback(
356 &window,
357 window.platform_window().take_user_interface_commands(),
358 )
359 }
360 }
361
362 pub(crate) fn maybe_window_for_webview_id(
363 &self,
364 webview_id: WebViewId,
365 ) -> Option<Rc<ServoShellWindow>> {
366 for window in self.windows.borrow().values() {
367 if window.contains_webview(webview_id) {
368 return Some(window.clone());
369 }
370 }
371 None
372 }
373
374 pub(crate) fn window_for_webview_id(&self, webview_id: WebViewId) -> Rc<ServoShellWindow> {
375 self.maybe_window_for_webview_id(webview_id)
376 .expect("Looking for unexpected WebView: {webview_id:?}")
377 }
378
379 pub(crate) fn platform_window_for_webview_id(
380 &self,
381 webview_id: WebViewId,
382 ) -> Rc<dyn PlatformWindow> {
383 self.window_for_webview_id(webview_id).platform_window()
384 }
385
386 fn maybe_request_screenshot(&self, webview: WebView) {
389 let output_path = self.servoshell_preferences.output_image_path.clone();
390 if !self.servoshell_preferences.exit_after_stable_image && output_path.is_none() {
391 return;
392 }
393
394 let achieved_stable_image = self.achieved_stable_image.clone();
396 if achieved_stable_image.get() {
397 return;
398 }
399
400 webview.take_screenshot(None, move |image| {
401 achieved_stable_image.set(true);
402
403 let Some(output_path) = output_path else {
404 return;
405 };
406
407 let image = match image {
408 Ok(image) => image,
409 Err(error) => {
410 error!("Could not take screenshot: {error:?}");
411 return;
412 },
413 };
414
415 let image_format = ImageFormat::from_path(&output_path).unwrap_or(ImageFormat::Png);
416 if let Err(error) =
417 DynamicImage::ImageRgba8(image).save_with_format(output_path, image_format)
418 {
419 error!("Failed to save screenshot: {error}.");
420 }
421 });
422 }
423
424 pub(crate) fn set_pending_traversal(
425 &self,
426 traversal_id: TraversalId,
427 sender: GenericSender<WebDriverLoadStatus>,
428 ) {
429 self.webdriver_senders
430 .borrow_mut()
431 .pending_traversals
432 .insert(traversal_id, sender);
433 }
434
435 pub(crate) fn set_load_status_sender(
436 &self,
437 webview_id: WebViewId,
438 sender: GenericSender<WebDriverLoadStatus>,
439 ) {
440 self.webdriver_senders
441 .borrow_mut()
442 .load_status_senders
443 .insert(webview_id, sender);
444 }
445
446 fn remove_load_status_sender(&self, webview_id: WebViewId) {
447 self.webdriver_senders
448 .borrow_mut()
449 .load_status_senders
450 .remove(&webview_id);
451 }
452
453 fn set_script_command_interrupt_sender(&self, sender: Option<IpcSender<WebDriverJSResult>>) {
454 self.webdriver_senders
455 .borrow_mut()
456 .script_evaluation_interrupt_sender = sender;
457 }
458
459 pub(crate) fn handle_webdriver_input_event(
460 &self,
461 webview_id: WebViewId,
462 input_event: InputEvent,
463 response_sender: Option<Sender<()>>,
464 ) {
465 if let Some(webview) = self.webview_by_id(webview_id) {
466 let event_id = webview.notify_input_event(input_event);
467 if let Some(response_sender) = response_sender {
468 self.pending_webdriver_events
469 .borrow_mut()
470 .insert(event_id, response_sender);
471 }
472 } else {
473 error!("Could not find WebView ({webview_id:?}) for WebDriver event: {input_event:?}");
474 };
475 }
476
477 pub(crate) fn handle_webdriver_screenshot(
478 &self,
479 webview_id: WebViewId,
480 rect: Option<Rect<f32, CSSPixel>>,
481 result_sender: Sender<Result<RgbaImage, ScreenshotCaptureError>>,
482 ) {
483 if let Some(webview) = self.webview_by_id(webview_id) {
484 let rect = rect.map(|rect| rect.to_box2d().into());
485 webview.take_screenshot(rect, move |result| {
486 if let Err(error) = result_sender.send(result) {
487 warn!("Failed to send response to TakeScreenshot: {error}");
488 }
489 });
490 } else if let Err(error) =
491 result_sender.send(Err(ScreenshotCaptureError::WebViewDoesNotExist))
492 {
493 error!("Failed to send response to TakeScreenshot: {error}");
494 }
495 }
496
497 pub(crate) fn handle_webdriver_script_command(&self, script_command: &WebDriverScriptCommand) {
498 match script_command {
499 WebDriverScriptCommand::ExecuteScript(_webview_id, response_sender) |
500 WebDriverScriptCommand::ExecuteAsyncScript(_webview_id, response_sender) => {
501 self.set_script_command_interrupt_sender(Some(response_sender.clone()));
505 },
506 WebDriverScriptCommand::AddLoadStatusSender(webview_id, load_status_sender) => {
507 self.set_load_status_sender(*webview_id, load_status_sender.clone());
508 },
509 WebDriverScriptCommand::RemoveLoadStatusSender(webview_id) => {
510 self.remove_load_status_sender(*webview_id);
511 },
512 _ => {
513 self.set_script_command_interrupt_sender(None);
514 },
515 }
516 }
517
518 pub(crate) fn handle_webdriver_load_url(
519 &self,
520 webview_id: WebViewId,
521 url: Url,
522 load_status_sender: GenericSender<WebDriverLoadStatus>,
523 ) {
524 let Some(webview) = self.webview_by_id(webview_id) else {
525 return;
526 };
527
528 self.platform_window_for_webview_id(webview_id)
529 .dismiss_embedder_controls_for_webview(webview_id);
530
531 info!("Loading URL in webview {}: {}", webview_id, url);
532 self.set_load_status_sender(webview_id, load_status_sender);
533 webview.load(url);
534 }
535
536 pub(crate) fn handle_gamepad_events(&self) {
537 let Some(active_webview) = self
538 .focused_window()
539 .and_then(|window| window.active_webview())
540 else {
541 return;
542 };
543 if let Some(gamepad_support) = self.gamepad_support.borrow_mut().as_mut() {
544 gamepad_support.handle_gamepad_events(active_webview);
545 }
546 }
547
548 fn interrupt_webdriver_script_evaluation(&self) {
558 if let Some(sender) = &self
559 .webdriver_senders
560 .borrow()
561 .script_evaluation_interrupt_sender
562 {
563 sender.send(Ok(JSValue::Null)).unwrap_or_else(|err| {
564 info!(
565 "Notify dialog appear failed. Maybe the channel to webdriver is closed: {err}"
566 );
567 });
568 }
569 }
570}
571
572impl WebViewDelegate for RunningAppState {
573 fn screen_geometry(&self, webview: WebView) -> Option<servo::ScreenGeometry> {
574 Some(
575 self.platform_window_for_webview_id(webview.id())
576 .screen_geometry(),
577 )
578 }
579
580 fn notify_status_text_changed(&self, webview: WebView, _status: Option<String>) {
581 self.window_for_webview_id(webview.id()).set_needs_update();
582 }
583
584 fn notify_history_changed(&self, webview: WebView, _entries: Vec<Url>, _current: usize) {
585 self.window_for_webview_id(webview.id()).set_needs_update();
586 }
587
588 fn notify_page_title_changed(&self, webview: WebView, _: Option<String>) {
589 self.window_for_webview_id(webview.id()).set_needs_update();
590 }
591
592 fn notify_traversal_complete(&self, _webview: WebView, traversal_id: TraversalId) {
593 let mut webdriver_state = self.webdriver_senders.borrow_mut();
594 if let Entry::Occupied(entry) = webdriver_state.pending_traversals.entry(traversal_id) {
595 let sender = entry.remove();
596 let _ = sender.send(WebDriverLoadStatus::Complete);
597 }
598 }
599
600 fn request_move_to(&self, webview: WebView, new_position: DeviceIntPoint) {
601 self.platform_window_for_webview_id(webview.id())
602 .set_position(new_position);
603 }
604
605 fn request_resize_to(&self, webview: WebView, requested_outer_size: DeviceIntSize) {
606 self.platform_window_for_webview_id(webview.id())
607 .request_resize(&webview, requested_outer_size);
608 }
609
610 fn request_authentication(
611 &self,
612 webview: WebView,
613 authentication_request: AuthenticationRequest,
614 ) {
615 self.platform_window_for_webview_id(webview.id())
616 .show_http_authentication_dialog(webview.id(), authentication_request);
617 }
618
619 fn request_open_auxiliary_webview(&self, parent_webview: WebView) -> Option<WebView> {
620 let window = self.window_for_webview_id(parent_webview.id());
621 let platform_window = window.platform_window();
622
623 let webview =
624 WebViewBuilder::new_auxiliary(&self.servo, platform_window.rendering_context())
625 .hidpi_scale_factor(platform_window.hidpi_scale_factor())
626 .delegate(parent_webview.delegate())
627 .build();
628
629 webview.notify_theme_change(platform_window.theme());
630
631 window.add_webview(webview.clone());
632
633 if self.servoshell_preferences.webdriver_port.get().is_none() {
637 window.activate_webview(webview.id());
638 } else {
639 webview.hide();
640 }
641
642 Some(webview)
643 }
644
645 fn notify_closed(&self, webview: WebView) {
646 self.window_for_webview_id(webview.id())
647 .close_webview(webview.id())
648 }
649
650 fn notify_input_event_handled(
651 &self,
652 webview: WebView,
653 id: InputEventId,
654 result: InputEventResult,
655 ) {
656 self.platform_window_for_webview_id(webview.id())
657 .notify_input_event_handled(&webview, id, result);
658 if let Some(response_sender) = self.pending_webdriver_events.borrow_mut().remove(&id) {
659 let _ = response_sender.send(());
660 }
661 }
662
663 fn notify_cursor_changed(&self, webview: WebView, cursor: servo::Cursor) {
664 self.platform_window_for_webview_id(webview.id())
665 .set_cursor(cursor);
666 }
667
668 fn notify_load_status_changed(&self, webview: WebView, status: LoadStatus) {
669 self.window_for_webview_id(webview.id()).set_needs_update();
670
671 if status == LoadStatus::Complete {
672 if let Some(sender) = self
673 .webdriver_senders
674 .borrow_mut()
675 .load_status_senders
676 .remove(&webview.id())
677 {
678 let _ = sender.send(WebDriverLoadStatus::Complete);
679 }
680 self.maybe_request_screenshot(webview);
681 }
682 }
683
684 fn notify_fullscreen_state_changed(&self, webview: WebView, fullscreen_state: bool) {
685 self.platform_window_for_webview_id(webview.id())
686 .set_fullscreen(fullscreen_state);
687 }
688
689 fn show_bluetooth_device_dialog(
690 &self,
691 webview: WebView,
692 devices: Vec<String>,
693 response_sender: GenericSender<Option<String>>,
694 ) {
695 self.platform_window_for_webview_id(webview.id())
696 .show_bluetooth_device_dialog(webview.id(), devices, response_sender);
697 }
698
699 fn request_permission(&self, webview: WebView, permission_request: PermissionRequest) {
700 self.platform_window_for_webview_id(webview.id())
701 .show_permission_dialog(webview.id(), permission_request);
702 }
703
704 fn notify_new_frame_ready(&self, webview: WebView) {
705 self.window_for_webview_id(webview.id()).set_needs_repaint();
706 }
707
708 fn play_gamepad_haptic_effect(
709 &self,
710 _webview: WebView,
711 index: usize,
712 effect_type: GamepadHapticEffectType,
713 effect_complete_sender: IpcSender<bool>,
714 ) {
715 match self.gamepad_support.borrow_mut().as_mut() {
716 Some(gamepad_support) => {
717 gamepad_support.play_haptic_effect(index, effect_type, effect_complete_sender);
718 },
719 None => {
720 let _ = effect_complete_sender.send(false);
721 },
722 }
723 }
724
725 fn stop_gamepad_haptic_effect(
726 &self,
727 _webview: WebView,
728 index: usize,
729 haptic_stop_sender: IpcSender<bool>,
730 ) {
731 let stopped = match self.gamepad_support.borrow_mut().as_mut() {
732 Some(gamepad_support) => gamepad_support.stop_haptic_effect(index),
733 None => false,
734 };
735 let _ = haptic_stop_sender.send(stopped);
736 }
737
738 fn show_embedder_control(&self, webview: WebView, embedder_control: EmbedderControl) {
739 if self.servoshell_preferences.webdriver_port.get().is_some() {
740 if matches!(&embedder_control, EmbedderControl::SimpleDialog(..)) {
741 self.interrupt_webdriver_script_evaluation();
742
743 if let Some(sender) = self
745 .webdriver_senders
746 .borrow_mut()
747 .load_status_senders
748 .get(&webview.id())
749 {
750 let _ = sender.send(WebDriverLoadStatus::Blocked);
751 };
752 }
753
754 self.webdriver_embedder_controls
755 .show_embedder_control(webview.id(), embedder_control);
756 return;
757 }
758
759 self.window_for_webview_id(webview.id())
760 .show_embedder_control(webview, embedder_control);
761 }
762
763 fn hide_embedder_control(&self, webview: WebView, embedder_control_id: EmbedderControlId) {
764 if self.servoshell_preferences.webdriver_port.get().is_some() {
765 self.webdriver_embedder_controls
766 .hide_embedder_control(webview.id(), embedder_control_id);
767 return;
768 }
769
770 self.window_for_webview_id(webview.id())
771 .hide_embedder_control(webview, embedder_control_id);
772 }
773
774 fn notify_favicon_changed(&self, webview: WebView) {
775 self.window_for_webview_id(webview.id())
776 .notify_favicon_changed(webview);
777 }
778
779 fn notify_media_session_event(&self, webview: WebView, event: MediaSessionEvent) {
780 self.platform_window_for_webview_id(webview.id())
781 .notify_media_session_event(event);
782 }
783
784 fn notify_crashed(&self, webview: WebView, reason: String, backtrace: Option<String>) {
785 self.platform_window_for_webview_id(webview.id())
786 .notify_crashed(webview, reason, backtrace);
787 }
788}
789
790struct ServoShellServoDelegate;
791impl ServoDelegate for ServoShellServoDelegate {
792 fn notify_devtools_server_started(&self, port: u16, _token: String) {
793 info!("Devtools Server running on port {port}");
794 }
795
796 fn request_devtools_connection(&self, request: AllowOrDenyRequest) {
797 request.allow();
798 }
799
800 fn notify_error(&self, error: ServoError) {
801 error!("Saw Servo error: {error:?}!");
802 }
803}