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