servoshell/desktop/
gui.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashMap;
6use std::rc::Rc;
7use std::sync::Arc;
8
9use dpi::PhysicalSize;
10use egui::text::{CCursor, CCursorRange};
11use egui::text_edit::TextEditState;
12use egui::{
13    Button, Key, Label, LayerId, Modifiers, PaintCallback, TopBottomPanel, Vec2, WidgetInfo,
14    WidgetType, pos2,
15};
16use egui_glow::{CallbackFn, EguiGlow};
17use egui_winit::EventResponse;
18use euclid::{Box2D, Length, Point2D, Rect, Scale, Size2D};
19use log::warn;
20use servo::{
21    DeviceIndependentPixel, DevicePixel, Image, LoadStatus, OffscreenRenderingContext, PixelFormat,
22    RenderingContext, WebView, WebViewId,
23};
24use url::Url;
25use winit::event::{ElementState, MouseButton, WindowEvent};
26use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
27use winit::window::Window;
28
29use super::geometry::winit_position_to_euclid_point;
30use crate::desktop::event_loop::AppEvent;
31use crate::desktop::headed_window;
32use crate::running_app_state::{RunningAppState, UserInterfaceCommand};
33use crate::window::ServoShellWindow;
34
35/// The user interface of a headed servoshell. Currently this is implemented via
36/// egui.
37pub struct Gui {
38    rendering_context: Rc<OffscreenRenderingContext>,
39    context: EguiGlow,
40    event_queue: Vec<UserInterfaceCommand>,
41    toolbar_height: Length<f32, DeviceIndependentPixel>,
42
43    last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
44    location: String,
45
46    /// Whether the location has been edited by the user without clicking Go.
47    location_dirty: bool,
48
49    /// The [`LoadStatus`] of the active `WebView`.
50    load_status: LoadStatus,
51
52    /// The text to display in the status bar on the bottom of the window.
53    status_text: Option<String>,
54
55    /// Whether or not the current `WebView` can navigate backward.
56    can_go_back: bool,
57
58    /// Whether or not the current `WebView` can navigate forward.
59    can_go_forward: bool,
60
61    /// Handle to the GPU texture of the favicon.
62    ///
63    /// These need to be cached across egui draw calls.
64    favicon_textures: HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
65}
66
67fn truncate_with_ellipsis(input: &str, max_length: usize) -> String {
68    if input.chars().count() > max_length {
69        let truncated: String = input.chars().take(max_length.saturating_sub(1)).collect();
70        format!("{}…", truncated)
71    } else {
72        input.to_string()
73    }
74}
75
76impl Drop for Gui {
77    fn drop(&mut self) {
78        self.rendering_context
79            .make_current()
80            .expect("Could not make window RenderingContext current");
81        self.context.destroy();
82    }
83}
84
85impl Gui {
86    pub(crate) fn new(
87        winit_window: &Window,
88        event_loop: &ActiveEventLoop,
89        event_loop_proxy: EventLoopProxy<AppEvent>,
90        rendering_context: Rc<OffscreenRenderingContext>,
91        initial_url: Url,
92    ) -> Self {
93        rendering_context
94            .make_current()
95            .expect("Could not make window RenderingContext current");
96        let mut context = EguiGlow::new(
97            event_loop,
98            rendering_context.glow_gl_api(),
99            None,
100            None,
101            false,
102        );
103
104        context
105            .egui_winit
106            .init_accesskit(event_loop, winit_window, event_loop_proxy);
107        winit_window.set_visible(true);
108
109        context.egui_ctx.options_mut(|options| {
110            // Disable the builtin egui handlers for the Ctrl+Plus, Ctrl+Minus and Ctrl+0
111            // shortcuts as they don't work well with servoshell's `device-pixel-ratio` CLI argument.
112            options.zoom_with_keyboard = false;
113
114            // On platforms where winit fails to obtain a system theme, fall back to a light theme
115            // since it is the more common default.
116            options.fallback_theme = egui::Theme::Light;
117        });
118
119        Self {
120            rendering_context,
121            context,
122            event_queue: vec![],
123            toolbar_height: Default::default(),
124            last_mouse_position: None,
125            location: initial_url.to_string(),
126            location_dirty: false,
127            load_status: LoadStatus::Complete,
128            status_text: None,
129            can_go_back: false,
130            can_go_forward: false,
131            favicon_textures: Default::default(),
132        }
133    }
134
135    pub(crate) fn take_user_interface_commands(&mut self) -> Vec<UserInterfaceCommand> {
136        std::mem::take(&mut self.event_queue)
137    }
138
139    pub(crate) fn on_window_event(
140        &mut self,
141        winit_window: &Window,
142        event: &WindowEvent,
143    ) -> EventResponse {
144        let mut result = self.context.on_window_event(winit_window, event);
145        result.consumed &= match event {
146            WindowEvent::CursorMoved { position, .. } => {
147                let scale = Scale::<_, DeviceIndependentPixel, _>::new(
148                    self.context.egui_ctx.pixels_per_point(),
149                );
150                self.last_mouse_position =
151                    Some(winit_position_to_euclid_point(*position).to_f32() / scale);
152                self.last_mouse_position
153                    .is_some_and(|p| self.is_in_egui_toolbar_rect(p))
154            },
155            WindowEvent::MouseInput {
156                state: ElementState::Pressed,
157                button: MouseButton::Forward,
158                ..
159            } => {
160                self.event_queue.push(UserInterfaceCommand::Forward);
161                true
162            },
163            WindowEvent::MouseInput {
164                state: ElementState::Pressed,
165                button: MouseButton::Back,
166                ..
167            } => {
168                self.event_queue.push(UserInterfaceCommand::Back);
169                true
170            },
171            WindowEvent::MouseWheel { .. } | WindowEvent::MouseInput { .. } => self
172                .last_mouse_position
173                .is_some_and(|p| self.is_in_egui_toolbar_rect(p)),
174            _ => true,
175        };
176        result
177    }
178
179    /// The height of the top toolbar of this user inteface ie the distance from the top of the
180    /// window to the position of the `WebView`.
181    pub(crate) fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel> {
182        self.toolbar_height
183    }
184
185    /// Return true iff the given position is over the egui toolbar.
186    fn is_in_egui_toolbar_rect(&self, position: Point2D<f32, DeviceIndependentPixel>) -> bool {
187        position.y < self.toolbar_height.get()
188    }
189
190    /// Create a frameless button with square sizing, as used in the toolbar.
191    fn toolbar_button(text: &str) -> egui::Button<'_> {
192        egui::Button::new(text)
193            .frame(false)
194            .min_size(Vec2 { x: 20.0, y: 20.0 })
195    }
196
197    /// Draws a browser tab, checking for clicks and queues appropriate [`UserInterfaceCommand`]s.
198    /// Using a custom widget here would've been nice, but it doesn't seem as though egui
199    /// supports that, so we arrange multiple Widgets in a way that they look connected.
200    fn browser_tab(
201        ui: &mut egui::Ui,
202        window: &ServoShellWindow,
203        webview: WebView,
204        event_queue: &mut Vec<UserInterfaceCommand>,
205        favicon_texture: Option<egui::load::SizedTexture>,
206    ) {
207        let label = match (webview.page_title(), webview.url()) {
208            (Some(title), _) if !title.is_empty() => title,
209            (_, Some(url)) => url.to_string(),
210            _ => "New Tab".into(),
211        };
212
213        let inactive_bg_color = ui.visuals().window_fill;
214        let active_bg_color = ui.visuals().widgets.active.weak_bg_fill;
215        let active = window.active_webview().map(|webview| webview.id()) == Some(webview.id());
216
217        // Setup a tab frame that will contain the favicon, title and close button
218        let mut tab_frame = egui::Frame::NONE.corner_radius(4).begin(ui);
219        {
220            tab_frame.content_ui.add_space(5.0);
221
222            let visuals = tab_frame.content_ui.visuals_mut();
223            // Remove the stroke so we don't see the border between the close button and the label
224            visuals.widgets.active.bg_stroke.width = 0.0;
225            visuals.widgets.hovered.bg_stroke.width = 0.0;
226            // Now we make sure the fill color is always the same, irrespective of state, that way
227            // we can make sure that both the label and close button have the same background color
228            visuals.widgets.noninteractive.weak_bg_fill = inactive_bg_color;
229            visuals.widgets.inactive.weak_bg_fill = inactive_bg_color;
230            visuals.widgets.hovered.weak_bg_fill = active_bg_color;
231            visuals.widgets.active.weak_bg_fill = active_bg_color;
232            visuals.selection.bg_fill = active_bg_color;
233            visuals.selection.stroke.color = visuals.widgets.active.fg_stroke.color;
234            visuals.widgets.hovered.fg_stroke.color = visuals.widgets.active.fg_stroke.color;
235
236            // Expansion would also show that they are 2 separate widgets
237            visuals.widgets.active.expansion = 0.0;
238            visuals.widgets.hovered.expansion = 0.0;
239
240            if let Some(favicon) = favicon_texture {
241                tab_frame.content_ui.add(
242                    egui::Image::from_texture(favicon)
243                        .fit_to_exact_size(egui::vec2(16.0, 16.0))
244                        .bg_fill(egui::Color32::TRANSPARENT),
245                );
246            }
247
248            let tab = tab_frame
249                .content_ui
250                .add(Button::selectable(
251                    active,
252                    truncate_with_ellipsis(&label, 20),
253                ))
254                .on_hover_ui(|ui| {
255                    ui.label(&label);
256                });
257
258            let close_button = tab_frame
259                .content_ui
260                .add(egui::Button::new("X").fill(egui::Color32::TRANSPARENT));
261            close_button.widget_info(|| {
262                let mut info = WidgetInfo::new(WidgetType::Button);
263                info.label = Some("Close".into());
264                info
265            });
266            if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
267                event_queue.push(UserInterfaceCommand::CloseWebView(webview.id()))
268            } else if !active && tab.clicked() {
269                window.activate_webview(webview.id());
270            }
271        }
272
273        let response = tab_frame.allocate_space(ui);
274        let fill_color = if active || response.hovered() {
275            active_bg_color
276        } else {
277            inactive_bg_color
278        };
279        tab_frame.frame.fill = fill_color;
280        tab_frame.end(ui);
281    }
282
283    /// Update the user interface, but do not paint the updated state.
284    pub(crate) fn update(
285        &mut self,
286        state: &RunningAppState,
287        window: &ServoShellWindow,
288        headed_window: &headed_window::Window,
289    ) {
290        self.rendering_context
291            .make_current()
292            .expect("Could not make RenderingContext current");
293        let Self {
294            rendering_context,
295            context,
296            event_queue,
297            toolbar_height,
298            location,
299            location_dirty,
300            favicon_textures,
301            ..
302        } = self;
303
304        let winit_window = headed_window.winit_window();
305        context.run(winit_window, |ctx| {
306            load_pending_favicons(ctx, window, favicon_textures);
307
308            // TODO: While in fullscreen add some way to mitigate the increased phishing risk
309            // when not displaying the URL bar: https://github.com/servo/servo/issues/32443
310            if winit_window.fullscreen().is_none() {
311                let frame = egui::Frame::default()
312                    .fill(ctx.style().visuals.window_fill)
313                    .inner_margin(4.0);
314                TopBottomPanel::top("toolbar").frame(frame).show(ctx, |ui| {
315                    ui.allocate_ui_with_layout(
316                        ui.available_size(),
317                        egui::Layout::left_to_right(egui::Align::Center),
318                        |ui| {
319                            let back_button =
320                                ui.add_enabled(self.can_go_back, Gui::toolbar_button("⏴"));
321                            back_button.widget_info(|| {
322                                let mut info = WidgetInfo::new(WidgetType::Button);
323                                info.label = Some("Back".into());
324                                info
325                            });
326                            if back_button.clicked() {
327                                *location_dirty = false;
328                                event_queue.push(UserInterfaceCommand::Back);
329                            }
330
331                            let forward_button =
332                                ui.add_enabled(self.can_go_forward, Gui::toolbar_button("⏵"));
333                            forward_button.widget_info(|| {
334                                let mut info = WidgetInfo::new(WidgetType::Button);
335                                info.label = Some("Forward".into());
336                                info
337                            });
338                            if forward_button.clicked() {
339                                *location_dirty = false;
340                                event_queue.push(UserInterfaceCommand::Forward);
341                            }
342
343                            match self.load_status {
344                                LoadStatus::Started | LoadStatus::HeadParsed => {
345                                    let stop_button = ui.add(Gui::toolbar_button("X"));
346                                    stop_button.widget_info(|| {
347                                        let mut info = WidgetInfo::new(WidgetType::Button);
348                                        info.label = Some("Stop".into());
349                                        info
350                                    });
351                                    if stop_button.clicked() {
352                                        warn!("Do not support stop yet.");
353                                    }
354                                },
355                                LoadStatus::Complete => {
356                                    let reload_button = ui.add(Gui::toolbar_button("↻"));
357                                    reload_button.widget_info(|| {
358                                        let mut info = WidgetInfo::new(WidgetType::Button);
359                                        info.label = Some("Reload".into());
360                                        info
361                                    });
362                                    if reload_button.clicked() {
363                                        *location_dirty = false;
364                                        event_queue.push(UserInterfaceCommand::Reload);
365                                    }
366                                },
367                            }
368                            ui.add_space(2.0);
369
370                            ui.allocate_ui_with_layout(
371                                ui.available_size(),
372                                egui::Layout::right_to_left(egui::Align::Center),
373                                |ui| {
374                                    let mut experimental_preferences_enabled =
375                                        state.experimental_preferences_enabled();
376                                    let prefs_toggle = ui
377                                        .toggle_value(&mut experimental_preferences_enabled, "☢")
378                                        .on_hover_text("Enable experimental prefs");
379                                    prefs_toggle.widget_info(|| {
380                                        let mut info = WidgetInfo::new(WidgetType::Button);
381                                        info.label = Some("Enable experimental preferences".into());
382                                        info.selected = Some(experimental_preferences_enabled);
383                                        info
384                                    });
385                                    if prefs_toggle.clicked() {
386                                        state.set_experimental_preferences_enabled(
387                                            experimental_preferences_enabled,
388                                        );
389                                        *location_dirty = false;
390                                        event_queue.push(UserInterfaceCommand::ReloadAll);
391                                    }
392
393                                    let location_id = egui::Id::new("location_input");
394                                    let location_field = ui.add_sized(
395                                        ui.available_size(),
396                                        egui::TextEdit::singleline(location)
397                                            .id(location_id)
398                                            .hint_text("Search or enter address"),
399                                    );
400
401                                    if location_field.changed() {
402                                        *location_dirty = true;
403                                    }
404                                    // Handle adddress bar shortcut.
405                                    if ui.input(|i| {
406                                        if cfg!(target_os = "macos") {
407                                            i.clone().consume_key(Modifiers::COMMAND, Key::L)
408                                        } else {
409                                            i.clone().consume_key(Modifiers::COMMAND, Key::L) ||
410                                                i.clone().consume_key(Modifiers::ALT, Key::D)
411                                        }
412                                    }) {
413                                        // The focus request immediately makes gained_focus return true.
414                                        location_field.request_focus();
415                                    }
416                                    // Select address bar text when it's focused (click or shortcut).
417                                    if location_field.gained_focus() {
418                                        if let Some(mut state) =
419                                            TextEditState::load(ui.ctx(), location_id)
420                                        {
421                                            // Select the whole input.
422                                            state.cursor.set_char_range(Some(CCursorRange::two(
423                                                CCursor::new(0),
424                                                CCursor::new(location.len()),
425                                            )));
426                                            state.store(ui.ctx(), location_id);
427                                        }
428                                    }
429                                    // Navigate to address when enter is pressed in the address bar.
430                                    if location_field.lost_focus() &&
431                                        ui.input(|i| i.clone().key_pressed(Key::Enter))
432                                    {
433                                        event_queue
434                                            .push(UserInterfaceCommand::Go(location.clone()));
435                                    }
436                                },
437                            );
438                        },
439                    );
440                });
441
442                // A simple Tab header strip
443                TopBottomPanel::top("tabs").show(ctx, |ui| {
444                    ui.allocate_ui_with_layout(
445                        ui.available_size(),
446                        egui::Layout::left_to_right(egui::Align::Center),
447                        |ui| {
448                            for (id, webview) in window.webviews().into_iter() {
449                                let favicon = favicon_textures
450                                    .get(&id)
451                                    .map(|(_, favicon)| favicon)
452                                    .copied();
453                                Self::browser_tab(ui, window, webview, event_queue, favicon);
454                            }
455
456                            let new_tab_button = ui.add(Gui::toolbar_button("+"));
457                            new_tab_button.widget_info(|| {
458                                let mut info = WidgetInfo::new(WidgetType::Button);
459                                info.label = Some("New tab".into());
460                                info
461                            });
462                            if new_tab_button.clicked() {
463                                event_queue.push(UserInterfaceCommand::NewWebView);
464                            }
465
466                            let new_window_button = ui.add(Gui::toolbar_button("⊞"));
467                            new_window_button.widget_info(|| {
468                                let mut info = WidgetInfo::new(WidgetType::Button);
469                                info.label = Some("New window".into());
470                                info
471                            });
472                            if new_window_button.clicked() {
473                                event_queue.push(UserInterfaceCommand::NewWindow);
474                            }
475                        },
476                    );
477                });
478            };
479
480            // The toolbar height is where the Context’s available rect starts.
481            // For reasons that are unclear, the TopBottomPanel’s ui cursor exceeds this by one egui
482            // point, but the Context is correct and the TopBottomPanel is wrong.
483            *toolbar_height = Length::new(ctx.available_rect().min.y);
484
485            let scale =
486                Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
487
488            headed_window.for_each_active_dialog(window, |dialog| dialog.update(ctx));
489
490            // If the top parts of the GUI changed size, then update the size of the WebView and also
491            // the size of its RenderingContext.
492            let rect = ctx.available_rect();
493            let size = Size2D::new(rect.width(), rect.height()) * scale;
494            let rect = Box2D::from_origin_and_size(Point2D::origin(), size);
495            if let Some(webview) = window.active_webview() &&
496                rect != webview.rect()
497            {
498                // `rect` is sized to just the WebView viewport, which is required by
499                // `OffscreenRenderingContext` See:
500                // <https://github.com/servo/servo/issues/38369#issuecomment-3138378527>
501                webview.resize(PhysicalSize::new(size.width as u32, size.height as u32))
502            }
503
504            if let Some(status_text) = &self.status_text {
505                egui::Tooltip::always_open(
506                    ctx.clone(),
507                    LayerId::background(),
508                    "tooltip layer".into(),
509                    pos2(0.0, ctx.available_rect().max.y),
510                )
511                .show(|ui| ui.add(Label::new(status_text.clone()).extend()));
512            }
513
514            window.repaint_webviews();
515
516            if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
517                ctx.layer_painter(LayerId::background()).add(PaintCallback {
518                    rect: ctx.available_rect(),
519                    callback: Arc::new(CallbackFn::new(move |info, painter| {
520                        let clip = info.viewport_in_pixels();
521                        let rect_in_parent = Rect::new(
522                            Point2D::new(clip.left_px, clip.from_bottom_px),
523                            Size2D::new(clip.width_px, clip.height_px),
524                        );
525                        render_to_parent(painter.gl(), rect_in_parent)
526                    })),
527                });
528            }
529        });
530    }
531
532    /// Paint the GUI, as of the last update.
533    pub(crate) fn paint(&mut self, window: &Window) {
534        self.rendering_context
535            .make_current()
536            .expect("Could not make RenderingContext current");
537        self.rendering_context
538            .parent_context()
539            .prepare_for_rendering();
540        self.context.paint(window);
541        self.rendering_context.parent_context().present();
542    }
543
544    /// Updates the location field from the given [`RunningAppState`], unless the user has started
545    /// editing it without clicking Go, returning true iff it has changed (needing an egui update).
546    fn update_location_in_toolbar(&mut self, window: &ServoShellWindow) -> bool {
547        // User edited without clicking Go?
548        if self.location_dirty {
549            return false;
550        }
551
552        let current_url_string = window
553            .active_webview()
554            .and_then(|webview| Some(webview.url()?.to_string()));
555        match current_url_string {
556            Some(location) if location != self.location => {
557                self.location = location.to_owned();
558                true
559            },
560            _ => false,
561        }
562    }
563
564    fn update_load_status(&mut self, window: &ServoShellWindow) -> bool {
565        let state_status = window
566            .active_webview()
567            .map(|webview| webview.load_status())
568            .unwrap_or(LoadStatus::Complete);
569        let old_status = std::mem::replace(&mut self.load_status, state_status);
570        let status_changed = old_status != self.load_status;
571
572        // When the load status changes, we want the new changes to the URL to start
573        // being reflected in the location bar.
574        if status_changed {
575            self.location_dirty = false;
576        }
577
578        status_changed
579    }
580
581    fn update_status_text(&mut self, window: &ServoShellWindow) -> bool {
582        let state_status = window
583            .active_webview()
584            .and_then(|webview| webview.status_text());
585        let old_status = std::mem::replace(&mut self.status_text, state_status);
586        old_status != self.status_text
587    }
588
589    fn update_can_go_back_and_forward(&mut self, window: &ServoShellWindow) -> bool {
590        let (can_go_back, can_go_forward) = window
591            .active_webview()
592            .map(|webview| (webview.can_go_back(), webview.can_go_forward()))
593            .unwrap_or((false, false));
594        let old_can_go_back = std::mem::replace(&mut self.can_go_back, can_go_back);
595        let old_can_go_forward = std::mem::replace(&mut self.can_go_forward, can_go_forward);
596        old_can_go_back != self.can_go_back || old_can_go_forward != self.can_go_forward
597    }
598
599    /// Updates all fields taken from the given [`ServoShellWindow`], such as the location field.
600    /// Returns true iff the egui needs an update.
601    pub(crate) fn update_webview_data(&mut self, window: &ServoShellWindow) -> bool {
602        // Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
603        //       because logical OR would short-circuit if any of the functions return true.
604        //       We want to ensure that all functions are called. The "bitwise OR" operator
605        //       does not short-circuit.
606        self.update_load_status(window) |
607            self.update_location_in_toolbar(window) |
608            self.update_status_text(window) |
609            self.update_can_go_back_and_forward(window)
610    }
611
612    /// Returns true if a redraw is required after handling the provided event.
613    pub(crate) fn handle_accesskit_event(
614        &mut self,
615        event: &egui_winit::accesskit_winit::WindowEvent,
616    ) -> bool {
617        match event {
618            egui_winit::accesskit_winit::WindowEvent::InitialTreeRequested => {
619                self.context.egui_ctx.enable_accesskit();
620                true
621            },
622            egui_winit::accesskit_winit::WindowEvent::ActionRequested(req) => {
623                self.context
624                    .egui_winit
625                    .on_accesskit_action_request(req.clone());
626                true
627            },
628            egui_winit::accesskit_winit::WindowEvent::AccessibilityDeactivated => {
629                self.context.egui_ctx.disable_accesskit();
630                false
631            },
632        }
633    }
634
635    pub(crate) fn set_zoom_factor(&self, factor: f32) {
636        self.context.egui_ctx.set_zoom_factor(factor);
637    }
638}
639
640fn embedder_image_to_egui_image(image: &Image) -> egui::ColorImage {
641    let width = image.width as usize;
642    let height = image.height as usize;
643
644    match image.format {
645        PixelFormat::K8 => egui::ColorImage::from_gray([width, height], image.data()),
646        PixelFormat::KA8 => {
647            // Convert to rgba
648            let data: Vec<u8> = image
649                .data()
650                .chunks_exact(2)
651                .flat_map(|pixel| [pixel[0], pixel[0], pixel[0], pixel[1]])
652                .collect();
653            egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
654        },
655        PixelFormat::RGB8 => egui::ColorImage::from_rgb([width, height], image.data()),
656        PixelFormat::RGBA8 => {
657            egui::ColorImage::from_rgba_unmultiplied([width, height], image.data())
658        },
659        PixelFormat::BGRA8 => {
660            // Convert from BGRA to RGBA
661            let data: Vec<u8> = image
662                .data()
663                .chunks_exact(4)
664                .flat_map(|chunk| [chunk[2], chunk[1], chunk[0], chunk[3]])
665                .collect();
666            egui::ColorImage::from_rgba_unmultiplied([width, height], &data)
667        },
668    }
669}
670
671/// Uploads all favicons that have not yet been processed to the GPU.
672fn load_pending_favicons(
673    ctx: &egui::Context,
674    window: &ServoShellWindow,
675    texture_cache: &mut HashMap<WebViewId, (egui::TextureHandle, egui::load::SizedTexture)>,
676) {
677    for id in window.take_pending_favicon_loads() {
678        let Some(webview) = window.webview_by_id(id) else {
679            continue;
680        };
681        let Some(favicon) = webview.favicon() else {
682            continue;
683        };
684
685        let egui_image = embedder_image_to_egui_image(&favicon);
686        let handle = ctx.load_texture(format!("favicon-{id:?}"), egui_image, Default::default());
687        let texture = egui::load::SizedTexture::new(
688            handle.id(),
689            egui::vec2(favicon.width as f32, favicon.height as f32),
690        );
691
692        // We don't need the handle anymore but we can't drop it either since that would cause
693        // the texture to be freed.
694        texture_cache.insert(id, (handle, texture));
695    }
696}