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