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 winit_window.fullscreen().is_none() {
395 let frame = egui::Frame::default()
396 .fill(ctx.style().visuals.window_fill)
397 .inner_margin(4.0);
398 Panel::top("toolbar").frame(frame).show_inside(ctx, |ui| {
399 ui.allocate_ui_with_layout(
400 ui.available_size(),
401 egui::Layout::left_to_right(egui::Align::Center),
402 |ui| {
403 let back_button =
404 ui.add_enabled(self.can_go_back, Gui::toolbar_button("⏴"));
405 back_button.widget_info(|| {
406 let mut info = WidgetInfo::new(WidgetType::Button);
407 info.label = Some("Back".into());
408 info
409 });
410 if back_button.clicked() {
411 *location_dirty = false;
412 window.queue_user_interface_command(UserInterfaceCommand::Back);
413 }
414
415 let forward_button =
416 ui.add_enabled(self.can_go_forward, Gui::toolbar_button("⏵"));
417 forward_button.widget_info(|| {
418 let mut info = WidgetInfo::new(WidgetType::Button);
419 info.label = Some("Forward".into());
420 info
421 });
422 if forward_button.clicked() {
423 *location_dirty = false;
424 window.queue_user_interface_command(UserInterfaceCommand::Forward);
425 }
426
427 match self.load_status {
428 LoadStatus::Started | LoadStatus::HeadParsed => {
429 let stop_button = ui.add(Gui::toolbar_button("X"));
430 stop_button.widget_info(|| {
431 let mut info = WidgetInfo::new(WidgetType::Button);
432 info.label = Some("Stop".into());
433 info
434 });
435 if stop_button.clicked() {
436 warn!("Do not support stop yet.");
437 }
438 },
439 LoadStatus::Complete => {
440 let reload_button = ui.add(Gui::toolbar_button("↻"));
441 reload_button.widget_info(|| {
442 let mut info = WidgetInfo::new(WidgetType::Button);
443 info.label = Some("Reload".into());
444 info
445 });
446 if reload_button.clicked() {
447 *location_dirty = false;
448 window.queue_user_interface_command(
449 UserInterfaceCommand::Reload,
450 );
451 }
452 },
453 }
454 ui.add_space(2.0);
455
456 ui.allocate_ui_with_layout(
457 ui.available_size(),
458 egui::Layout::right_to_left(egui::Align::Center),
459 |ui| {
460 let mut experimental_preferences_enabled =
461 state.experimental_preferences_enabled();
462 let prefs_toggle = ui
463 .toggle_value(&mut experimental_preferences_enabled, "☢")
464 .on_hover_text("Enable experimental prefs");
465 prefs_toggle.widget_info(|| {
466 let mut info = WidgetInfo::new(WidgetType::Button);
467 info.label = Some("Enable experimental preferences".into());
468 info.selected = Some(experimental_preferences_enabled);
469 info
470 });
471 if prefs_toggle.clicked() {
472 state.set_experimental_preferences_enabled(
473 experimental_preferences_enabled,
474 );
475 *location_dirty = false;
476 window.queue_user_interface_command(
477 UserInterfaceCommand::ReloadAll,
478 );
479 }
480
481 let location_id = egui::Id::new("location_input");
482 let location_field = ui.add_sized(
483 ui.available_size(),
484 egui::TextEdit::singleline(location)
485 .id(location_id)
486 .hint_text("Search or enter address"),
487 );
488
489 if location_field.changed() {
490 *location_dirty = true;
491 }
492 if ui.input(|i| {
494 if cfg!(target_os = "macos") {
495 i.clone().consume_key(Modifiers::COMMAND, Key::L)
496 } else {
497 i.clone().consume_key(Modifiers::COMMAND, Key::L) ||
498 i.clone().consume_key(Modifiers::ALT, Key::D)
499 }
500 }) {
501 location_field.request_focus();
503 }
504 if location_field.gained_focus() &&
506 let Some(mut state) =
507 TextEditState::load(ui.ctx(), location_id)
508 {
509 state.cursor.set_char_range(Some(CCursorRange::two(
511 CCursor::new(0),
512 CCursor::new(location.len()),
513 )));
514 state.store(ui.ctx(), location_id);
515 }
516 if location_field.lost_focus() &&
518 ui.input(|i| i.clone().key_pressed(Key::Enter))
519 {
520 window.queue_user_interface_command(
521 UserInterfaceCommand::Go(location.clone()),
522 );
523 }
524 },
525 );
526 },
527 );
528 });
529
530 let outer = Panel::top("tabs").show_inside(ctx, |ui| {
532 ui.allocate_ui_with_layout(
533 ui.available_size(),
534 egui::Layout::left_to_right(egui::Align::Center),
535 |ui| {
536 for (id, webview) in window.webviews().into_iter() {
537 let favicon = favicon_textures
538 .get(&id)
539 .map(|(_, favicon)| favicon)
540 .copied();
541 Self::browser_tab(ui, window, webview, favicon);
542 }
543
544 let new_tab_button = ui.add(Gui::toolbar_button("+"));
545 new_tab_button.widget_info(|| {
546 let mut info = WidgetInfo::new(WidgetType::Button);
547 info.label = Some("New tab".into());
548 info
549 });
550 if new_tab_button.clicked() {
551 window
552 .queue_user_interface_command(UserInterfaceCommand::NewWebView);
553 }
554
555 let new_window_button = ui.add(Gui::toolbar_button("⊞"));
556 new_window_button.widget_info(|| {
557 let mut info = WidgetInfo::new(WidgetType::Button);
558 info.label = Some("New window".into());
559 info
560 });
561 if new_window_button.clicked() {
562 window
563 .queue_user_interface_command(UserInterfaceCommand::NewWindow);
564 }
565 },
566 );
567 });
568
569 *toolbar_height = Length::new(outer.response.rect.max.y);
570 } else {
571 *toolbar_height = Length::default();
572 }
573
574 let scale =
575 Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
576
577 headed_window.for_each_active_dialog(window, |dialog| dialog.update(ctx));
578
579 let available_rect = ctx.available_rect_before_wrap();
582
583 for (webview_id, webview) in window.webviews() {
585 if let Some(tree_id) = webview.accesskit_tree_id() {
586 let id = egui::Id::new(webview_id);
587 ctx.accesskit_node_builder(id, |node| {
588 node.set_tree_id(tree_id);
589 });
590 }
591 }
592 let size = Size2D::new(available_rect.width(), available_rect.height()) * scale;
593 if let Some(webview) = window.active_webview() &&
594 size != webview.size()
595 {
596 webview.resize(PhysicalSize::new(size.width as u32, size.height as u32))
600 }
601
602 if let Some(status_text) = &self.status_text {
603 egui::Tooltip::always_open(
604 ctx.clone(),
605 LayerId::new(Order::Tooltip, Id::new("tooltip")),
606 "tooltip layer".into(),
607 pos2(0.0, available_rect.max.y),
608 )
609 .show(|ui| ui.add(Label::new(status_text.clone()).extend()));
610 window.set_needs_repaint();
611 }
612
613 window.repaint_webviews();
614
615 if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
616 ctx.layer_painter(LayerId::background()).add(PaintCallback {
617 rect: available_rect,
618 callback: Arc::new(CallbackFn::new(move |info, painter| {
619 let clip = info.viewport_in_pixels();
620 let rect_in_parent = Rect::new(
621 Point2D::new(clip.left_px, clip.from_bottom_px),
622 Size2D::new(clip.width_px, clip.height_px),
623 );
624 render_to_parent(painter.gl(), rect_in_parent)
625 })),
626 });
627 }
628 });
629
630 let adapter = self
631 .context
632 .egui_winit
633 .accesskit
634 .as_mut()
635 .expect("guaranteed by Gui::new()");
636 for tree_update in self.pending_accesskit_updates.drain(..) {
637 adapter.update_if_active(|| tree_update);
638 }
639 }
640
641 pub(crate) fn paint(&mut self, window: &Window) {
643 self.rendering_context
644 .make_current()
645 .expect("Could not make RenderingContext current");
646 self.rendering_context
647 .parent_context()
648 .prepare_for_rendering();
649 self.context.paint(window);
650 self.rendering_context.parent_context().present();
651 }
652
653 fn update_location_in_toolbar(&mut self, window: &ServoShellWindow) -> bool {
656 if self.location_dirty {
658 return false;
659 }
660
661 let current_url_string = window
662 .active_webview()
663 .and_then(|webview| Some(webview.url()?.to_string()));
664 match current_url_string {
665 Some(location) if location != self.location => {
666 self.location = location;
667 true
668 },
669 _ => false,
670 }
671 }
672
673 fn update_load_status(&mut self, window: &ServoShellWindow) -> bool {
674 let state_status = window
675 .active_webview()
676 .map(|webview| webview.load_status())
677 .unwrap_or(LoadStatus::Complete);
678 let old_status = std::mem::replace(&mut self.load_status, state_status);
679 let status_changed = old_status != self.load_status;
680
681 if status_changed {
684 self.location_dirty = false;
685 }
686
687 status_changed
688 }
689
690 fn update_status_text(&mut self, window: &ServoShellWindow) -> bool {
691 let state_status = window
692 .active_webview()
693 .and_then(|webview| webview.status_text());
694 let old_status = std::mem::replace(&mut self.status_text, state_status);
695 old_status != self.status_text
696 }
697
698 fn update_can_go_back_and_forward(&mut self, window: &ServoShellWindow) -> bool {
699 let (can_go_back, can_go_forward) = window
700 .active_webview()
701 .map(|webview| (webview.can_go_back(), webview.can_go_forward()))
702 .unwrap_or((false, false));
703 let old_can_go_back = std::mem::replace(&mut self.can_go_back, can_go_back);
704 let old_can_go_forward = std::mem::replace(&mut self.can_go_forward, can_go_forward);
705 old_can_go_back != self.can_go_back || old_can_go_forward != self.can_go_forward
706 }
707
708 pub(crate) fn update_webview_data(&mut self, window: &ServoShellWindow) -> bool {
711 self.update_load_status(window) |
716 self.update_location_in_toolbar(window) |
717 self.update_status_text(window) |
718 self.update_can_go_back_and_forward(window)
719 }
720
721 pub(crate) fn handle_accesskit_event(
723 &mut self,
724 event: &egui_winit::accesskit_winit::WindowEvent,
725 ) -> bool {
726 match event {
727 egui_winit::accesskit_winit::WindowEvent::InitialTreeRequested => {
728 self.context.egui_ctx.enable_accesskit();
729 true
730 },
731 egui_winit::accesskit_winit::WindowEvent::ActionRequested(req) => {
732 self.context
733 .egui_winit
734 .on_accesskit_action_request(req.clone());
735 true
736 },
737 egui_winit::accesskit_winit::WindowEvent::AccessibilityDeactivated => {
738 self.context.egui_ctx.disable_accesskit();
739 false
740 },
741 }
742 }
743
744 pub(crate) fn set_zoom_factor(&self, factor: f32) {
745 self.context.egui_ctx.set_zoom_factor(factor);
746 }
747
748 pub(crate) fn notify_accessibility_tree_update(&mut self, tree_update: accesskit::TreeUpdate) {
749 self.pending_accesskit_updates.push(tree_update);
750 }
751}
752
753fn embedder_image_to_egui_image(image: &Image) -> egui::ColorImage {
754 let width = image.width as usize;
755 let height = image.height as usize;
756
757 match image.format {
758 PixelFormat::K8 => egui::ColorImage::from_gray([width, height], image.data()),
759 PixelFormat::KA8 => {
760 let data: Vec<u8> = image
762 .data()
763 .chunks_exact(2)
764 .flat_map(|pixel| [pixel[0], pixel[0], pixel[0], pixel[1]])
765 .collect();
766 egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
767 },
768 PixelFormat::RGB8 => egui::ColorImage::from_rgb([width, height], image.data()),
769 PixelFormat::RGBA8 => {
770 egui::ColorImage::from_rgba_unmultiplied([width, height], image.data())
771 },
772 PixelFormat::BGRA8 => {
773 let data: Vec<u8> = image
775 .data()
776 .chunks_exact(4)
777 .flat_map(|chunk| [chunk[2], chunk[1], chunk[0], chunk[3]])
778 .collect();
779 egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
780 },
781 }
782}
783
784fn load_pending_favicons(
786 ctx: &egui::Context,
787 window: &ServoShellWindow,
788 texture_cache: &mut HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
789) {
790 for id in window.take_pending_favicon_loads() {
791 let Some(webview) = window.webview_by_id(id) else {
792 continue;
793 };
794 let Some(favicon) = webview.favicon() else {
795 continue;
796 };
797
798 let egui_image = embedder_image_to_egui_image(&favicon);
799 let handle = ctx.load_texture(format!("favicon-{id:?}"), egui_image, Default::default());
800 let texture = egui::load::SizedTexture::new(
801 handle.id(),
802 egui::vec2(favicon.width as f32, favicon.height as f32),
803 );
804
805 texture_cache.insert(id, (handle, texture));
808 }
809}