1use std::collections::HashMap;
6#[cfg(any(target_os = "windows", target_os = "linux", target_os = "freebsd"))]
7use std::fs;
8#[cfg(any(target_os = "windows", target_os = "linux", target_os = "freebsd"))]
9use std::path::Path;
10use std::rc::Rc;
11use std::sync::Arc;
12
13use dpi::PhysicalSize;
14use egui::text::{CCursor, CCursorRange};
15use egui::text_edit::TextEditState;
16use egui::{
17 Button, FontDefinitions, Id, Key, Label, LayerId, Modifiers, Order, PaintCallback, Panel, Vec2,
18 WidgetInfo, WidgetType, pos2,
19};
20#[cfg(any(target_os = "windows", target_os = "linux", target_os = "freebsd"))]
21use egui::{FontData, FontFamily};
22use egui_glow::{CallbackFn, EguiGlow};
23use egui_winit::EventResponse;
24use euclid::{Length, Point2D, Rect, Scale, Size2D};
25#[cfg(any(target_os = "windows", target_os = "linux", target_os = "freebsd"))]
26use log::info;
27use log::warn;
28use servo::{
29 DeviceIndependentPixel, DevicePixel, Image, LoadStatus, OffscreenRenderingContext, PixelFormat,
30 RenderingContext, WebView, WebViewId,
31};
32use url::Url;
33use winit::event::WindowEvent;
34use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
35use winit::window::Window;
36
37use crate::desktop::event_loop::AppEvent;
38use crate::desktop::headed_window;
39use crate::running_app_state::{RunningAppState, UserInterfaceCommand};
40use crate::window::ServoShellWindow;
41
42pub struct Gui {
45 rendering_context: Rc<OffscreenRenderingContext>,
46 context: EguiGlow,
47 toolbar_height: Length<f32, DeviceIndependentPixel>,
48
49 location: String,
50
51 location_dirty: bool,
53
54 load_status: LoadStatus,
56
57 status_text: Option<String>,
59
60 can_go_back: bool,
62
63 can_go_forward: bool,
65
66 favicon_textures: HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
70
71 pending_accesskit_updates: Vec<accesskit::TreeUpdate>,
74}
75
76fn truncate_with_ellipsis(input: &str, max_length: usize) -> String {
77 if input.chars().count() > max_length {
78 let truncated: String = input.chars().take(max_length.saturating_sub(1)).collect();
79 format!("{}…", truncated)
80 } else {
81 input.to_string()
82 }
83}
84
85#[cfg(any(target_os = "windows", target_os = "linux", target_os = "freebsd"))]
86fn load_cjk_fonts(font_candidates: &[(&str, &str)]) -> FontDefinitions {
87 let mut fonts = FontDefinitions::default();
88 let mut loaded_font_names = Vec::new();
89
90 for (path_str, font_name) in font_candidates.iter() {
91 let font_path = Path::new(path_str);
92 if font_path.exists() {
93 match fs::read(font_path) {
94 Ok(bytes) => {
95 if !fonts.font_data.contains_key(*font_name) {
96 fonts
97 .font_data
98 .insert(font_name.to_string(), Arc::new(FontData::from_owned(bytes)));
99 loaded_font_names.push(font_name.to_string());
100 info!("Loaded font: {}", font_name);
101 }
102 },
103 Err(error) => {
104 info!("Failed to read font {}: {}", font_name, error);
105 },
106 }
107 }
108 }
109
110 if !loaded_font_names.is_empty() {
111 let proportional = fonts.families.get_mut(&FontFamily::Proportional).unwrap();
112 for font_name in loaded_font_names.iter() {
113 proportional.insert(0, font_name.clone());
114 }
115 }
116
117 fonts
118}
119
120#[cfg(target_os = "windows")]
121fn configure_fonts() -> FontDefinitions {
122 load_cjk_fonts(&[
123 (r"C:\Windows\Fonts\malgun.ttf", "Malgun Gothic"), (r"C:\Windows\Fonts\msyh.ttc", "Microsoft YaHei"), ])
126}
127
128#[cfg(any(target_os = "linux", target_os = "freebsd"))]
129fn configure_fonts() -> FontDefinitions {
130 load_cjk_fonts(&[
131 (
132 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
133 "Noto Sans CJK",
134 ), (
136 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
137 "Noto Sans CJK",
138 ), (
141 "/usr/local/share/fonts/noto/NotoSansCJKhk-Regular.otf",
142 "Noto Sans CJK HK",
143 ),
144 (
145 "/usr/local/share/fonts/noto/NotoSansCJKjp-Regular.otf",
146 "Noto Sans CJK JP",
147 ),
148 (
149 "/usr/local/share/fonts/noto/NotoSansCJKkr-Regular.otf",
150 "Noto Sans CJK KR",
151 ),
152 (
153 "/usr/local/share/fonts/noto/NotoSansCJKsc-Regular.otf",
154 "Noto Sans CJK SC",
155 ),
156 (
157 "/usr/local/share/fonts/noto/NotoSansCJKtc-Regular.otf",
158 "Noto Sans CJK TC",
159 ),
160 (
161 "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
162 "WenQuanYi Micro Hei",
163 ), (
165 "/usr/local/share/fonts/wqy/wqy-microhei.ttc",
166 "WenQuanYi Micro Hei",
167 ), ])
169}
170
171#[cfg(target_os = "macos")]
172fn configure_fonts() -> FontDefinitions {
173 FontDefinitions::default()
176}
177
178impl Drop for Gui {
179 fn drop(&mut self) {
180 self.rendering_context
181 .make_current()
182 .expect("Could not make window RenderingContext current");
183 self.context.destroy();
184 }
185}
186
187impl Gui {
188 pub(crate) fn new(
189 winit_window: &Window,
190 event_loop: &ActiveEventLoop,
191 event_loop_proxy: EventLoopProxy<AppEvent>,
192 rendering_context: Rc<OffscreenRenderingContext>,
193 initial_url: Url,
194 ) -> Self {
195 rendering_context
196 .make_current()
197 .expect("Could not make window RenderingContext current");
198 let mut context = EguiGlow::new(
199 event_loop,
200 rendering_context.glow_gl_api(),
201 None,
202 None,
203 false,
204 );
205
206 let font_definitions = configure_fonts();
207 context.egui_ctx.set_fonts(font_definitions);
208
209 context
210 .egui_winit
211 .init_accesskit(event_loop, winit_window, event_loop_proxy);
212 winit_window.set_visible(true);
213
214 context.egui_ctx.options_mut(|options| {
215 options.zoom_with_keyboard = false;
218
219 options.fallback_theme = egui::Theme::Light;
222 });
223
224 Self {
225 rendering_context,
226 context,
227 toolbar_height: Default::default(),
228 location: initial_url.to_string(),
229 location_dirty: false,
230 load_status: LoadStatus::Complete,
231 status_text: None,
232 can_go_back: false,
233 can_go_forward: false,
234 favicon_textures: Default::default(),
235 pending_accesskit_updates: vec![],
236 }
237 }
238
239 pub(crate) fn has_keyboard_focus(&self) -> bool {
240 self.context
241 .egui_ctx
242 .memory(|memory| memory.focused().is_some())
243 }
244
245 pub(crate) fn surrender_focus(&self) {
246 self.context.egui_ctx.memory_mut(|memory| {
247 if let Some(focused) = memory.focused() {
248 memory.surrender_focus(focused);
249 }
250 });
251 }
252
253 pub(crate) fn on_window_event(
254 &mut self,
255 winit_window: &Window,
256 event: &WindowEvent,
257 ) -> EventResponse {
258 self.context.on_window_event(winit_window, event)
259 }
260
261 pub(crate) fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
264 self.toolbar_height
265 }
266
267 pub(crate) fn is_in_egui_toolbar_rect(
269 &self,
270 position: Point2D<f32, DeviceIndependentPixel>,
271 ) -> bool {
272 position.y < self.toolbar_height.get()
273 }
274
275 fn toolbar_button(text: &str) -> egui::Button<'_> {
277 egui::Button::new(text)
278 .frame(false)
279 .min_size(Vec2 { x: 20.0, y: 20.0 })
280 }
281
282 fn browser_tab(
286 ui: &mut egui::Ui,
287 window: &ServoShellWindow,
288 webview: WebView,
289 favicon_texture: Option<egui::load::SizedTexture>,
290 ) {
291 let label = match (webview.page_title(), webview.url()) {
292 (Some(title), _) if !title.is_empty() => title,
293 (_, Some(url)) => url.to_string(),
294 _ => "New Tab".into(),
295 };
296
297 let inactive_bg_color = ui.visuals().window_fill;
298 let active_bg_color = ui.visuals().widgets.active.weak_bg_fill;
299 let active = window.active_webview().map(|webview| webview.id()) == Some(webview.id());
300
301 let mut tab_frame = egui::Frame::NONE.corner_radius(4).begin(ui);
303 {
304 tab_frame.content_ui.add_space(5.0);
305
306 let visuals = tab_frame.content_ui.visuals_mut();
307 visuals.widgets.active.bg_stroke.width = 0.0;
309 visuals.widgets.hovered.bg_stroke.width = 0.0;
310 visuals.widgets.noninteractive.weak_bg_fill = inactive_bg_color;
313 visuals.widgets.inactive.weak_bg_fill = inactive_bg_color;
314 visuals.widgets.hovered.weak_bg_fill = active_bg_color;
315 visuals.widgets.active.weak_bg_fill = active_bg_color;
316 visuals.selection.bg_fill = active_bg_color;
317 visuals.selection.stroke.color = visuals.widgets.active.fg_stroke.color;
318 visuals.widgets.hovered.fg_stroke.color = visuals.widgets.active.fg_stroke.color;
319
320 visuals.widgets.active.expansion = 0.0;
322 visuals.widgets.hovered.expansion = 0.0;
323
324 if let Some(favicon) = favicon_texture {
325 tab_frame.content_ui.add(
326 egui::Image::from_texture(favicon)
327 .fit_to_exact_size(egui::vec2(16.0, 16.0))
328 .bg_fill(egui::Color32::TRANSPARENT),
329 );
330 }
331
332 let tab = tab_frame
333 .content_ui
334 .add(Button::selectable(
335 active,
336 truncate_with_ellipsis(&label, 20),
337 ))
338 .on_hover_ui(|ui| {
339 ui.label(&label);
340 });
341
342 let close_button = tab_frame
343 .content_ui
344 .add(egui::Button::new("X").fill(egui::Color32::TRANSPARENT));
345 close_button.widget_info(|| {
346 let mut info = WidgetInfo::new(WidgetType::Button);
347 info.label = Some("Close".into());
348 info
349 });
350 if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
351 window
352 .queue_user_interface_command(UserInterfaceCommand::CloseWebView(webview.id()));
353 } else if !active && tab.clicked() {
354 window.activate_webview(webview.id());
355 }
356 }
357
358 let response = tab_frame.allocate_space(ui);
359 let fill_color = if active || response.hovered() {
360 active_bg_color
361 } else {
362 inactive_bg_color
363 };
364 tab_frame.frame.fill = fill_color;
365 tab_frame.end(ui);
366 }
367
368 pub(crate) fn update(
370 &mut self,
371 state: &RunningAppState,
372 window: &ServoShellWindow,
373 headed_window: &headed_window::HeadedWindow,
374 ) {
375 self.rendering_context
376 .make_current()
377 .expect("Could not make RenderingContext current");
378 let Self {
379 rendering_context,
380 context,
381 toolbar_height,
382 location,
383 location_dirty,
384 favicon_textures,
385 ..
386 } = self;
387
388 let winit_window = headed_window.winit_window();
389 context.run(winit_window, |ctx| {
390 load_pending_favicons(ctx, window, favicon_textures);
391
392 if !headed_window.is_fullscreen_from_document() {
396 let frame = egui::Frame::default()
397 .fill(ctx.style().visuals.window_fill)
398 .inner_margin(4.0);
399 Panel::top("toolbar").frame(frame).show_inside(ctx, |ui| {
400 ui.allocate_ui_with_layout(
401 ui.available_size(),
402 egui::Layout::left_to_right(egui::Align::Center),
403 |ui| {
404 let back_button =
405 ui.add_enabled(self.can_go_back, Gui::toolbar_button("⏴"));
406 back_button.widget_info(|| {
407 let mut info = WidgetInfo::new(WidgetType::Button);
408 info.label = Some("Back".into());
409 info
410 });
411 if back_button.clicked() {
412 *location_dirty = false;
413 window.queue_user_interface_command(UserInterfaceCommand::Back);
414 }
415
416 let forward_button =
417 ui.add_enabled(self.can_go_forward, Gui::toolbar_button("⏵"));
418 forward_button.widget_info(|| {
419 let mut info = WidgetInfo::new(WidgetType::Button);
420 info.label = Some("Forward".into());
421 info
422 });
423 if forward_button.clicked() {
424 *location_dirty = false;
425 window.queue_user_interface_command(UserInterfaceCommand::Forward);
426 }
427
428 match self.load_status {
429 LoadStatus::Started | LoadStatus::HeadParsed => {
430 let stop_button = ui.add(Gui::toolbar_button("X"));
431 stop_button.widget_info(|| {
432 let mut info = WidgetInfo::new(WidgetType::Button);
433 info.label = Some("Stop".into());
434 info
435 });
436 if stop_button.clicked() {
437 warn!("Do not support stop yet.");
438 }
439 },
440 LoadStatus::Complete => {
441 let reload_button = ui.add(Gui::toolbar_button("↻"));
442 reload_button.widget_info(|| {
443 let mut info = WidgetInfo::new(WidgetType::Button);
444 info.label = Some("Reload".into());
445 info
446 });
447 if reload_button.clicked() {
448 *location_dirty = false;
449 window.queue_user_interface_command(
450 UserInterfaceCommand::Reload,
451 );
452 }
453 },
454 }
455 ui.add_space(2.0);
456
457 ui.allocate_ui_with_layout(
458 ui.available_size(),
459 egui::Layout::right_to_left(egui::Align::Center),
460 |ui| {
461 let mut experimental_preferences_enabled =
462 state.experimental_preferences_enabled();
463 let prefs_toggle = ui
464 .toggle_value(&mut experimental_preferences_enabled, "☢")
465 .on_hover_text("Enable experimental prefs");
466 prefs_toggle.widget_info(|| {
467 let mut info = WidgetInfo::new(WidgetType::Button);
468 info.label = Some("Enable experimental preferences".into());
469 info.selected = Some(experimental_preferences_enabled);
470 info
471 });
472 if prefs_toggle.clicked() {
473 state.set_experimental_preferences_enabled(
474 experimental_preferences_enabled,
475 );
476 *location_dirty = false;
477 window.queue_user_interface_command(
478 UserInterfaceCommand::ReloadAll,
479 );
480 }
481
482 let location_id = egui::Id::new("location_input");
483 let location_field = ui.add_sized(
484 ui.available_size(),
485 egui::TextEdit::singleline(location)
486 .id(location_id)
487 .hint_text("Search or enter address"),
488 );
489
490 if location_field.changed() {
491 *location_dirty = true;
492 }
493 if ui.input(|i| {
495 if cfg!(target_os = "macos") {
496 i.clone().consume_key(Modifiers::COMMAND, Key::L)
497 } else {
498 i.clone().consume_key(Modifiers::COMMAND, Key::L) ||
499 i.clone().consume_key(Modifiers::ALT, Key::D)
500 }
501 }) {
502 location_field.request_focus();
504 }
505 if location_field.gained_focus() &&
507 let Some(mut state) =
508 TextEditState::load(ui.ctx(), location_id)
509 {
510 state.cursor.set_char_range(Some(CCursorRange::two(
512 CCursor::new(0),
513 CCursor::new(location.len()),
514 )));
515 state.store(ui.ctx(), location_id);
516 }
517 if location_field.lost_focus() &&
519 ui.input(|i| i.clone().key_pressed(Key::Enter))
520 {
521 window.queue_user_interface_command(
522 UserInterfaceCommand::Go(location.clone()),
523 );
524 }
525 },
526 );
527 },
528 );
529 });
530
531 let outer = Panel::top("tabs").show_inside(ctx, |ui| {
533 egui::ScrollArea::horizontal()
535 .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
536 .show(ui, |ui| {
537 ui.allocate_ui_with_layout(
538 ui.available_size(),
539 egui::Layout::left_to_right(egui::Align::Center),
540 |ui| {
541 for (id, webview) in window.webviews().into_iter() {
542 let favicon = favicon_textures
543 .get(&id)
544 .map(|(_, favicon)| favicon)
545 .copied();
546 Self::browser_tab(ui, window, webview, favicon);
547 }
548
549 let new_tab_button = ui.add(Gui::toolbar_button("+"));
550 new_tab_button.widget_info(|| {
551 let mut info = WidgetInfo::new(WidgetType::Button);
552 info.label = Some("New tab".into());
553 info
554 });
555 if new_tab_button.clicked() {
556 window.queue_user_interface_command(
557 UserInterfaceCommand::NewWebView,
558 );
559 }
560
561 let new_window_button = ui.add(Gui::toolbar_button("⊞"));
562 new_window_button.widget_info(|| {
563 let mut info = WidgetInfo::new(WidgetType::Button);
564 info.label = Some("New window".into());
565 info
566 });
567 if new_window_button.clicked() {
568 window.queue_user_interface_command(
569 UserInterfaceCommand::NewWindow,
570 );
571 }
572 },
573 );
574 })
575 });
576
577 *toolbar_height = Length::new(outer.response.rect.max.y);
578 } else {
579 *toolbar_height = Length::default();
580 }
581
582 let scale =
583 Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
584
585 headed_window.for_each_active_dialog(window, |dialog| dialog.update(ctx));
586
587 let available_rect = ctx.available_rect_before_wrap();
590
591 for (webview_id, webview) in window.webviews() {
593 if let Some(tree_id) = webview.accesskit_tree_id() {
594 let id = egui::Id::new(webview_id);
595 ctx.accesskit_node_builder(id, |node| {
596 node.set_tree_id(tree_id);
597 });
598 }
599 }
600 let size = Size2D::new(available_rect.width(), available_rect.height()) * scale;
601 if let Some(webview) = window.active_webview() &&
602 size != webview.size()
603 {
604 webview.resize(PhysicalSize::new(size.width as u32, size.height as u32))
608 }
609
610 if let Some(status_text) = &self.status_text {
611 egui::Tooltip::always_open(
612 ctx.clone(),
613 LayerId::new(Order::Tooltip, Id::new("tooltip")),
614 "tooltip layer".into(),
615 pos2(0.0, available_rect.max.y),
616 )
617 .show(|ui| ui.add(Label::new(status_text.clone()).extend()));
618 }
619
620 window.repaint_webviews();
621
622 if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
623 ctx.layer_painter(LayerId::background()).add(PaintCallback {
624 rect: available_rect,
625 callback: Arc::new(CallbackFn::new(move |info, painter| {
626 let clip = info.viewport_in_pixels();
627 let rect_in_parent = Rect::new(
628 Point2D::new(clip.left_px, clip.from_bottom_px),
629 Size2D::new(clip.width_px, clip.height_px),
630 );
631 render_to_parent(painter.gl(), rect_in_parent)
632 })),
633 });
634 }
635 });
636
637 if self.context.egui_ctx.has_requested_repaint() {
640 window.set_needs_repaint();
641 }
642
643 let adapter = self
644 .context
645 .egui_winit
646 .accesskit
647 .as_mut()
648 .expect("guaranteed by Gui::new()");
649 for tree_update in self.pending_accesskit_updates.drain(..) {
650 adapter.update_if_active(|| tree_update);
651 }
652 }
653
654 pub(crate) fn paint(&mut self, window: &Window) {
656 self.rendering_context
657 .make_current()
658 .expect("Could not make RenderingContext current");
659 self.rendering_context
660 .parent_context()
661 .prepare_for_rendering();
662 self.context.paint(window);
663 self.rendering_context.parent_context().present();
664 }
665
666 fn update_location_in_toolbar(&mut self, window: &ServoShellWindow) -> bool {
669 if self.location_dirty {
671 return false;
672 }
673
674 let current_url_string = window
675 .active_webview()
676 .and_then(|webview| Some(webview.url()?.to_string()));
677 match current_url_string {
678 Some(location) if location != self.location => {
679 self.location = location;
680 true
681 },
682 _ => false,
683 }
684 }
685
686 fn update_load_status(&mut self, window: &ServoShellWindow) -> bool {
687 let state_status = window
688 .active_webview()
689 .map(|webview| webview.load_status())
690 .unwrap_or(LoadStatus::Complete);
691 let old_status = std::mem::replace(&mut self.load_status, state_status);
692 let status_changed = old_status != self.load_status;
693
694 if status_changed {
697 self.location_dirty = false;
698 }
699
700 status_changed
701 }
702
703 fn update_status_text(&mut self, window: &ServoShellWindow) -> bool {
704 let state_status = window
705 .active_webview()
706 .and_then(|webview| webview.status_text());
707 let old_status = std::mem::replace(&mut self.status_text, state_status);
708 old_status != self.status_text
709 }
710
711 fn update_can_go_back_and_forward(&mut self, window: &ServoShellWindow) -> bool {
712 let (can_go_back, can_go_forward) = window
713 .active_webview()
714 .map(|webview| (webview.can_go_back(), webview.can_go_forward()))
715 .unwrap_or((false, false));
716 let old_can_go_back = std::mem::replace(&mut self.can_go_back, can_go_back);
717 let old_can_go_forward = std::mem::replace(&mut self.can_go_forward, can_go_forward);
718 old_can_go_back != self.can_go_back || old_can_go_forward != self.can_go_forward
719 }
720
721 pub(crate) fn update_webview_data(&mut self, window: &ServoShellWindow) -> bool {
724 self.update_load_status(window) |
729 self.update_location_in_toolbar(window) |
730 self.update_status_text(window) |
731 self.update_can_go_back_and_forward(window)
732 }
733
734 pub(crate) fn handle_accesskit_event(
736 &mut self,
737 event: &egui_winit::accesskit_winit::WindowEvent,
738 ) -> bool {
739 match event {
740 egui_winit::accesskit_winit::WindowEvent::InitialTreeRequested => {
741 self.context.egui_ctx.enable_accesskit();
742 true
743 },
744 egui_winit::accesskit_winit::WindowEvent::ActionRequested(req) => {
745 self.context
746 .egui_winit
747 .on_accesskit_action_request(req.clone());
748 true
749 },
750 egui_winit::accesskit_winit::WindowEvent::AccessibilityDeactivated => {
751 self.context.egui_ctx.disable_accesskit();
752 false
753 },
754 }
755 }
756
757 pub(crate) fn set_zoom_factor(&self, factor: f32) {
758 self.context.egui_ctx.set_zoom_factor(factor);
759 }
760
761 pub(crate) fn notify_accessibility_tree_update(&mut self, tree_update: accesskit::TreeUpdate) {
762 self.pending_accesskit_updates.push(tree_update);
763 }
764}
765
766fn embedder_image_to_egui_image(image: &Image) -> egui::ColorImage {
767 let width = image.width as usize;
768 let height = image.height as usize;
769
770 match image.format {
771 PixelFormat::K8 => egui::ColorImage::from_gray([width, height], image.data()),
772 PixelFormat::KA8 => {
773 let data: Vec<u8> = image
775 .data()
776 .chunks_exact(2)
777 .flat_map(|pixel| [pixel[0], pixel[0], pixel[0], pixel[1]])
778 .collect();
779 egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
780 },
781 PixelFormat::RGB8 => egui::ColorImage::from_rgb([width, height], image.data()),
782 PixelFormat::RGBA8 => {
783 egui::ColorImage::from_rgba_unmultiplied([width, height], image.data())
784 },
785 PixelFormat::BGRA8 => {
786 let data: Vec<u8> = image
788 .data()
789 .chunks_exact(4)
790 .flat_map(|chunk| [chunk[2], chunk[1], chunk[0], chunk[3]])
791 .collect();
792 egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
793 },
794 }
795}
796
797fn load_pending_favicons(
799 ctx: &egui::Context,
800 window: &ServoShellWindow,
801 texture_cache: &mut HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
802) {
803 for id in window.take_pending_favicon_loads() {
804 let Some(webview) = window.webview_by_id(id) else {
805 continue;
806 };
807 let Some(favicon) = webview.favicon() else {
808 continue;
809 };
810
811 let egui_image = embedder_image_to_egui_image(&favicon);
812 let handle = ctx.load_texture(format!("favicon-{id:?}"), egui_image, Default::default());
813 let texture = egui::load::SizedTexture::new(
814 handle.id(),
815 egui::vec2(favicon.width as f32, favicon.height as f32),
816 );
817
818 texture_cache.insert(id, (handle, texture));
821 }
822}