libservo: Expose an OffscreenRenderingContext and use it for servoshell (#35465)

Create a new `RenderingContext` which is used to render to a
`SurfmanRenderingContext`-related offscreen buffer. This allows having a
temporary place to render Servo and then blitting the results to a
subsection of the parent `RenderingContext`.

The goal with this change is to remove the details of how servoshell
renders from the `Compositor` and prepare for the compositor-per-WebView
world.


Co-authred-by: Ngo Iok Ui (Wu Yu Wei) <yuweiwu@pm.me>

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
Martin Robinson 2025-02-17 09:35:05 +01:00 committed by GitHub
parent d466688526
commit 6dce329acc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 655 additions and 608 deletions

View file

@ -11,19 +11,16 @@ use std::time::Instant;
use std::{env, fs};
use log::{info, trace, warn};
use raw_window_handle::HasDisplayHandle;
use servo::compositing::windowing::{AnimationState, WindowMethods};
use servo::compositing::CompositeTarget;
use servo::config::opts::Opts;
use servo::config::prefs::Preferences;
use servo::servo_config::pref;
use servo::servo_url::ServoUrl;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::webxr::glwindow::GlWindowDiscovery;
#[cfg(target_os = "windows")]
use servo::webxr::openxr::{AppInfo, OpenXrDiscovery};
use servo::{EventLoopWaker, Servo};
use surfman::Connection;
use url::Url;
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
@ -55,18 +52,14 @@ pub struct App {
state: AppState,
}
pub(crate) enum Present {
Deferred,
None,
}
/// Action to be taken by the caller of [`App::handle_events`].
pub(crate) enum PumpResult {
/// The caller should shut down Servo and its related context.
Shutdown,
Continue {
update: bool,
present: Present,
need_update: bool,
new_servo_frame: bool,
need_window_redraw: bool,
},
}
@ -102,52 +95,25 @@ impl App {
/// Initialize Application once event loop start running.
pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) {
// Create rendering context
let initial_window_size = self.servoshell_preferences.initial_window_size;
let rendering_context = if self.servoshell_preferences.headless {
let connection = Connection::new().expect("Failed to create connection");
let adapter = connection
.create_software_adapter()
.expect("Failed to create adapter");
SurfmanRenderingContext::create(
&connection,
&adapter,
Some(initial_window_size.to_untyped().to_i32()),
)
.expect("Failed to create WR surfman")
} else {
let display_handle = event_loop
.unwrap()
.display_handle()
.expect("could not get display handle from window");
let connection = Connection::from_display_handle(display_handle)
.expect("Failed to create connection");
let adapter = connection
.create_adapter()
.expect("Failed to create adapter");
SurfmanRenderingContext::create(&connection, &adapter, None)
.expect("Failed to create WR surfman")
};
let headless = self.servoshell_preferences.headless;
let window = if headless {
headless_window::Window::new(&self.servoshell_preferences)
} else {
Rc::new(headed_window::Window::new(
&self.opts,
&self.servoshell_preferences,
&rendering_context,
event_loop.unwrap(),
))
};
if window.winit_window().is_some() {
self.minibrowser = Some(Minibrowser::new(
&rendering_context,
event_loop.unwrap(),
self.initial_url.clone(),
));
}
assert_eq!(headless, event_loop.is_none());
let window = match event_loop {
Some(event_loop) => {
let window = headed_window::Window::new(
&self.opts,
&self.servoshell_preferences,
event_loop,
);
self.minibrowser = Some(Minibrowser::new(
window.offscreen_rendering_context(),
event_loop,
self.initial_url.clone(),
));
Rc::new(window)
},
None => headless_window::Window::new(&self.servoshell_preferences),
};
self.windows.insert(window.id(), window);
@ -174,12 +140,6 @@ impl App {
// Implements embedder methods, used by libservo and constellation.
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
let composite_target = if self.minibrowser.is_some() {
CompositeTarget::OffscreenFbo
} else {
CompositeTarget::ContextFbo
};
// TODO: Remove this once dyn upcasting coercion stabilises
// <https://github.com/rust-lang/rust/issues/65991>
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
@ -195,11 +155,11 @@ impl App {
let servo = Servo::new(
self.opts.clone(),
self.preferences.clone(),
Rc::new(rendering_context),
window.rendering_context(),
embedder,
Rc::new(UpcastedWindow(window.clone())),
self.servoshell_preferences.user_agent.clone(),
composite_target,
CompositeTarget::ContextFbo,
);
servo.setup_logging();
@ -233,33 +193,28 @@ impl App {
state.shutdown();
self.state = AppState::ShuttingDown;
},
PumpResult::Continue { update, present } => {
if update {
if let Some(ref mut minibrowser) = self.minibrowser {
if minibrowser.update_webview_data(state) {
// Update the minibrowser immediately. While we could update by requesting a
// redraw, doing so would delay the location update by two frames.
minibrowser.update(
window.winit_window().unwrap(),
state,
"update_location_in_toolbar",
);
}
}
PumpResult::Continue {
need_update: update,
new_servo_frame,
need_window_redraw,
} => {
// A new Servo frame is ready, so swap the buffer on our `RenderingContext`. In headed mode
// this won't immediately update the widget surface, because we render to an offscreen
// `RenderingContext`.
if new_servo_frame {
state.servo().present();
}
match present {
Present::Deferred => {
// The compositor has painted to this frame.
trace!("PumpResult::Present::Deferred");
// Request a winit redraw event, so we can paint the minibrowser and present.
// Otherwise, it's in headless mode and we present directly.
if let Some(window) = window.winit_window() {
window.request_redraw();
} else {
state.servo().present();
}
},
Present::None => {},
let updated = match (update, &mut self.minibrowser) {
(true, Some(minibrowser)) => minibrowser.update_webview_data(state),
_ => false,
};
// If in headed mode, request a winit redraw event, so we can paint the minibrowser.
if updated || need_window_redraw || new_servo_frame {
if let Some(window) = window.winit_window() {
window.request_redraw();
}
}
},
}
@ -292,15 +247,12 @@ impl App {
state.shutdown();
self.state = AppState::ShuttingDown;
},
PumpResult::Continue { present, .. } => {
match present {
Present::Deferred => {
// The compositor has painted to this frame.
trace!("PumpResult::Present::Deferred");
// In headless mode, we present directly.
state.servo().present();
},
Present::None => {},
PumpResult::Continue {
new_servo_frame, ..
} => {
if new_servo_frame {
// In headless mode, we present directly.
state.servo().present();
}
},
}
@ -401,8 +353,6 @@ impl ApplicationHandler<WakerEvent> for App {
minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
minibrowser.paint(window.winit_window().unwrap());
}
state.servo().present();
}
// Handle the event

View file

@ -25,7 +25,7 @@ use servo::{
use tinyfiledialogs::MessageBoxIcon;
use url::Url;
use super::app::{Present, PumpResult};
use super::app::PumpResult;
use super::dialog::Dialog;
use super::gamepad::GamepadSupport;
use super::keyutils::CMD_OR_CONTROL;
@ -74,7 +74,7 @@ pub struct RunningAppStateInner {
need_update: bool,
/// Whether or not the application needs to be redrawn.
need_present: bool,
new_servo_frame_ready: bool,
}
impl Drop for RunningAppState {
@ -101,7 +101,7 @@ impl RunningAppState {
window,
gamepad_support: GamepadSupport::maybe_new(),
need_update: false,
need_present: false,
new_servo_frame_ready: false,
}),
}
}
@ -133,28 +133,21 @@ impl RunningAppState {
self.handle_gamepad_events();
}
let should_continue = self.servo().spin_event_loop();
// Delegate handlers may have asked us to present or update compositor contents.
let need_present = std::mem::replace(&mut self.inner_mut().need_present, false);
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
if !should_continue {
if !self.servo().spin_event_loop() {
return PumpResult::Shutdown;
}
// Currently, egui-file-dialog dialogs need to be constantly presented or animations aren't fluid.
let need_present = need_present || self.has_active_dialog();
// Delegate handlers may have asked us to present or update compositor contents.
let new_servo_frame = std::mem::replace(&mut self.inner_mut().new_servo_frame_ready, false);
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
let present = if need_present {
Present::Deferred
} else {
Present::None
};
// Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid.
let need_window_redraw = new_servo_frame || self.has_active_dialog();
PumpResult::Continue {
update: need_update,
present,
need_update,
new_servo_frame,
need_window_redraw,
}
}
@ -240,7 +233,6 @@ impl RunningAppState {
.or_default()
.push(dialog);
inner_mut.need_update = true;
inner_mut.need_present = true;
}
fn has_active_dialog(&self) -> bool {
@ -415,22 +407,17 @@ impl WebViewDelegate for RunningAppState {
}
fn notify_ready_to_show(&self, webview: servo::WebView) {
let scale = self.inner().window.hidpi_factor().get();
let toolbar = self.inner().window.toolbar_height().get();
// Adjust for our toolbar height.
// TODO: Adjust for egui window decorations if we end up using those
let mut rect = self
let rect = self
.inner()
.window
.get_coordinates()
.get_viewport()
.to_f32();
rect.min.y += toolbar * scale;
webview.focus();
webview.move_resize(rect);
webview.raise_to_top(true);
webview.notify_rendering_context_resized();
}
fn notify_closed(&self, webview: servo::WebView) {
@ -497,7 +484,7 @@ impl WebViewDelegate for RunningAppState {
}
fn notify_new_frame_ready(&self, _webview: servo::WebView) {
self.inner_mut().need_present = true;
self.inner_mut().new_servo_frame_ready = true;
}
fn play_gamepad_haptic_effect(

View file

@ -12,7 +12,7 @@ use std::time::Duration;
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
use keyboard_types::{Modifiers, ShortcutMatcher};
use log::{debug, info};
use log::{debug, info, warn};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use servo::compositing::windowing::{
AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods,
@ -22,13 +22,14 @@ use servo::servo_config::pref;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel};
use servo::webrender_api::ScrollLocation;
use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
use servo::webrender_traits::SurfmanRenderingContext;
use servo::{
Cursor, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Theme, TouchEvent, TouchEventAction,
TouchId, WebView, WheelDelta, WheelEvent, WheelMode,
};
use surfman::{Context, Device, SurfaceType};
use surfman::{Connection, Context, Device, SurfaceType};
use url::Url;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use winit::event::{
@ -52,9 +53,9 @@ pub struct Window {
inner_size: Cell<PhysicalSize<u32>>,
toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>,
mouse_down_button: Cell<Option<MouseButton>>,
mouse_down_point: Cell<Point2D<i32, DevicePixel>>,
webview_relative_mouse_down_point: Cell<Point2D<f32, DevicePixel>>,
monitor: winit::monitor::MonitorHandle,
mouse_pos: Cell<Point2D<i32, DevicePixel>>,
webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
last_pressed: Cell<Option<(KeyboardEvent, Option<LogicalKey>)>>,
/// A map of winit's key codes to key values that are interpreted from
/// winit's ReceivedChar events.
@ -64,13 +65,21 @@ pub struct Window {
device_pixel_ratio_override: Option<f32>,
xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
modifiers_state: Cell<ModifiersState>,
/// The RenderingContext that renders directly onto the Window. This is used as
/// the target of egui rendering and also where Servo rendering results are finally
/// blitted.
window_rendering_context: SurfmanRenderingContext,
/// The `RenderingContext` of Servo itself. This is used to render Servo results
/// temporarily until they can be blitted into the egui scene.
rendering_context: Rc<OffscreenRenderingContext>,
}
impl Window {
pub fn new(
opts: &Opts,
servoshell_preferences: &ServoShellPreferences,
rendering_context: &SurfmanRenderingContext,
event_loop: &ActiveEventLoop,
) -> Window {
// If there's no chrome, start off with the window invisible. It will be set to visible in
@ -111,37 +120,48 @@ impl Window {
let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> =
Scale::new(screen_scale);
let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32();
let inner_size = winit_window.inner_size();
// Initialize surfman
let display_handle = event_loop
.display_handle()
.expect("could not get display handle from window");
let connection =
Connection::from_display_handle(display_handle).expect("Failed to create connection");
let adapter = connection
.create_adapter()
.expect("Failed to create adapter");
let window_handle = winit_window
.window_handle()
.expect("could not get window handle from window");
let inner_size = winit_window.inner_size();
let native_widget = rendering_context
.connection()
let native_widget = connection
.create_native_widget_from_window_handle(
window_handle,
winit_size_to_euclid_size(inner_size).to_i32().to_untyped(),
)
.expect("Failed to create native widget");
let surface_type = SurfaceType::Widget { native_widget };
let surface = rendering_context
.create_surface(surface_type)
let window_rendering_context = SurfmanRenderingContext::create(&connection, &adapter, None)
.expect("Failed to create window RenderingContext");
let surface = window_rendering_context
.create_surface(SurfaceType::Widget { native_widget })
.expect("Failed to create surface");
rendering_context
window_rendering_context
.bind_surface(surface)
.expect("Failed to bind surface");
// Make sure the gl context is made current.
rendering_context.make_gl_context_current().unwrap();
window_rendering_context.make_gl_context_current().unwrap();
let rendering_context_size = Size2D::new(inner_size.width, inner_size.height);
let rendering_context =
Rc::new(window_rendering_context.offscreen_context(rendering_context_size));
debug!("Created window {:?}", winit_window.id());
Window {
winit_window,
mouse_down_button: Cell::new(None),
mouse_down_point: Cell::new(Point2D::zero()),
mouse_pos: Cell::new(Point2D::zero()),
webview_relative_mouse_down_point: Cell::new(Point2D::zero()),
webview_relative_mouse_point: Cell::new(Point2D::zero()),
last_pressed: Cell::new(None),
keys_down: RefCell::new(HashMap::new()),
animation_state: Cell::new(AnimationState::Idle),
@ -153,6 +173,8 @@ impl Window {
xr_window_poses: RefCell::new(vec![]),
modifiers_state: Cell::new(ModifiersState::empty()),
toolbar_height: Cell::new(Default::default()),
window_rendering_context,
rendering_context,
}
}
@ -244,13 +266,7 @@ impl Window {
}
/// Helper function to handle a click
fn handle_mouse(
&self,
webview: &WebView,
button: MouseButton,
action: ElementState,
coords: Point2D<i32, DevicePixel>,
) {
fn handle_mouse(&self, webview: &WebView, button: MouseButton, action: ElementState) {
let max_pixel_dist = 10.0 * self.hidpi_factor().get();
let mouse_button = match &button {
MouseButton::Left => ServoMouseButton::Left,
@ -261,9 +277,10 @@ impl Window {
MouseButton::Other(value) => ServoMouseButton::Other(*value),
};
let point = self.webview_relative_mouse_point.get();
let action = match action {
ElementState::Pressed => {
self.mouse_down_point.set(coords);
self.webview_relative_mouse_down_point.set(point);
self.mouse_down_button.set(Some(button));
MouseButtonAction::Down
},
@ -273,7 +290,7 @@ impl Window {
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
action,
button: mouse_button,
point: coords.to_f32(),
point,
}));
// Also send a 'click' event if this is release and the press was recorded
@ -285,14 +302,13 @@ impl Window {
}
if let Some(mouse_down_button) = self.mouse_down_button.get() {
let pixel_dist = self.mouse_down_point.get() - coords;
let pixel_dist =
((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32).sqrt();
let pixel_dist = self.webview_relative_mouse_down_point.get() - point;
let pixel_dist = (pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y).sqrt();
if mouse_down_button == button && pixel_dist < max_pixel_dist {
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
action: MouseButtonAction::Click,
button: mouse_button,
point: coords.to_f32(),
point,
}));
}
}
@ -418,6 +434,10 @@ impl Window {
.otherwise(|| handled = false);
handled
}
pub(crate) fn offscreen_rendering_context(&self) -> Rc<OffscreenRenderingContext> {
self.rendering_context.clone()
}
}
impl WindowPortsMethods for Window {
@ -545,15 +565,15 @@ impl WindowPortsMethods for Window {
WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()),
WindowEvent::MouseInput { state, button, .. } => {
if button == MouseButton::Left || button == MouseButton::Right {
self.handle_mouse(&webview, button, state, self.mouse_pos.get());
self.handle_mouse(&webview, button, state);
}
},
WindowEvent::CursorMoved { position, .. } => {
let position = winit_position_to_euclid_point(position);
self.mouse_pos.set(position.to_i32());
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent {
point: position.to_f32(),
}));
let mut point = winit_position_to_euclid_point(position).to_f32();
point.y -= (self.toolbar_height() * self.hidpi_factor()).0;
self.webview_relative_mouse_point.set(point);
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
},
WindowEvent::MouseWheel { delta, phase, .. } => {
let (mut dx, mut dy, mode) = match delta {
@ -574,8 +594,8 @@ impl WindowPortsMethods for Window {
z: 0.0,
mode,
};
let pos = self.mouse_pos.get();
let point = Point2D::new(pos.x as f32, pos.y as f32);
let pos = self.webview_relative_mouse_point.get();
let point = Point2D::new(pos.x, pos.y);
// Scroll events snap to the major axis of movement, with vertical
// preferred over horizontal.
@ -590,7 +610,11 @@ impl WindowPortsMethods for Window {
// Send events
webview.notify_input_event(InputEvent::Wheel(WheelEvent { delta, point }));
webview.notify_scroll_event(scroll_location, self.mouse_pos.get(), phase);
webview.notify_scroll_event(
scroll_location,
self.webview_relative_mouse_point.get().to_i32(),
phase,
);
},
WindowEvent::Touch(touch) => {
webview.notify_input_event(InputEvent::Touch(TouchEvent {
@ -607,6 +631,14 @@ impl WindowPortsMethods for Window {
},
WindowEvent::Resized(new_size) => {
if self.inner_size.get() != new_size {
let rendering_context_size = Size2D::new(new_size.width, new_size.height);
if let Err(error) = self
.window_rendering_context
.resize(rendering_context_size.to_i32())
{
warn!("Could not resize window RenderingContext: {error:?}");
}
self.inner_size.set(new_size);
webview.notify_rendering_context_resized();
}
@ -658,6 +690,10 @@ impl WindowPortsMethods for Window {
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) {
self.toolbar_height.set(height);
}
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
self.rendering_context.clone()
}
}
impl WindowMethods for Window {
@ -671,7 +707,9 @@ impl WindowMethods for Window {
let window_rect = (window_rect.to_f64() / window_scale).to_i32();
let viewport_origin = DeviceIntPoint::zero(); // bottom left
let viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32();
let mut viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32();
viewport_size.height -= (self.toolbar_height() * self.hidpi_factor()).0;
let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32());
let screen_size = self.screen_size.to_i32();

View file

@ -12,6 +12,9 @@ use euclid::{Box2D, Length, Point2D, Scale, Size2D};
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
use servo::webrender_traits::rendering_context::RenderingContext;
use servo::webrender_traits::SurfmanRenderingContext;
use surfman::Connection;
use super::app_state::RunningAppState;
use crate::desktop::window_trait::WindowPortsMethods;
@ -24,19 +27,31 @@ pub struct Window {
inner_size: Cell<DeviceIntSize>,
screen_size: Size2D<i32, DeviceIndependentPixel>,
window_rect: Box2D<i32, DeviceIndependentPixel>,
rendering_context: SurfmanRenderingContext,
}
impl Window {
#[allow(clippy::new_ret_no_self)]
pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc<dyn WindowPortsMethods> {
let size = servoshell_preferences.initial_window_size;
let connection = Connection::new().expect("Failed to create connection");
let adapter = connection
.create_software_adapter()
.expect("Failed to create adapter");
let rendering_context = SurfmanRenderingContext::create(
&connection,
&adapter,
Some(size.to_untyped().to_i32()),
)
.expect("Failed to create WR surfman");
let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override;
let device_pixel_ratio_override: Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> =
device_pixel_ratio_override.map(Scale::new);
let hidpi_factor = device_pixel_ratio_override.unwrap_or_else(Scale::identity);
let size = servoshell_preferences.initial_window_size.to_i32();
let inner_size = Cell::new((size.to_f32() * hidpi_factor).to_i32());
let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size);
let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size.to_i32());
let screen_size = servoshell_preferences.screen_size_override.map_or_else(
|| window_rect.size(),
@ -50,6 +65,7 @@ impl Window {
inner_size,
screen_size,
window_rect,
rendering_context,
};
Rc::new(window)
@ -132,6 +148,12 @@ impl WindowPortsMethods for Window {
fn set_toolbar_height(&self, _height: Length<f32, DeviceIndependentPixel>) {
unimplemented!("headless Window only")
}
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
// `SurfmanRenderingContext` uses shared ownership internally so cloning it here does
// not create a new one really.
Rc::new(self.rendering_context.clone())
}
}
impl WindowMethods for Window {

View file

@ -3,27 +3,25 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell};
use std::num::NonZeroU32;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use egui::text::{CCursor, CCursorRange};
use egui::text_edit::TextEditState;
use egui::{
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, Pos2, SelectableLabel,
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, SelectableLabel,
TopBottomPanel, Vec2,
};
use egui_glow::CallbackFn;
use egui_winit::EventResponse;
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
use gleam::gl;
use glow::NativeFramebuffer;
use euclid::{Box2D, Length, Point2D, Rect, Scale, Size2D};
use log::{trace, warn};
use servo::base::id::WebViewId;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::servo_url::ServoUrl;
use servo::webrender_api::units::DevicePixel;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
use servo::{LoadStatus, WebView};
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::event_loop::ActiveEventLoop;
@ -34,14 +32,11 @@ use super::egui_glue::EguiGlow;
use super::geometry::winit_position_to_euclid_point;
pub struct Minibrowser {
rendering_context: Rc<OffscreenRenderingContext>,
pub context: EguiGlow,
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
pub toolbar_height: Length<f32, DeviceIndependentPixel>,
/// The framebuffer object name for the widget surface we should draw to, or None if our widget
/// surface does not use a framebuffer object.
widget_surface_fbo: Option<NativeFramebuffer>,
last_update: Instant,
last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
location: RefCell<String>,
@ -81,12 +76,14 @@ impl Drop for Minibrowser {
impl Minibrowser {
pub fn new(
rendering_context: &SurfmanRenderingContext,
rendering_context: Rc<OffscreenRenderingContext>,
event_loop: &ActiveEventLoop,
initial_url: ServoUrl,
) -> Self {
let gl = unsafe {
glow::Context::from_loader_function(|s| rendering_context.get_proc_address(s))
glow::Context::from_loader_function(|s| {
rendering_context.parent_context().get_proc_address(s)
})
};
// Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs
@ -99,17 +96,11 @@ impl Minibrowser {
.egui_ctx
.options_mut(|options| options.zoom_with_keyboard = false);
let widget_surface_fbo = match rendering_context.context_surface_info() {
Ok(Some(info)) => info.framebuffer_object,
Ok(None) => panic!("Failed to get widget surface info from surfman!"),
Err(error) => panic!("Failed to get widget surface info from surfman! {error:?}"),
};
Self {
rendering_context,
context,
event_queue: RefCell::new(vec![]),
toolbar_height: Default::default(),
widget_surface_fbo,
last_update: Instant::now(),
last_mouse_position: None,
location: RefCell::new(initial_url.to_string()),
@ -268,17 +259,16 @@ impl Minibrowser {
reason
);
let Self {
rendering_context,
context,
event_queue,
toolbar_height,
widget_surface_fbo,
last_update,
location,
location_dirty,
..
} = self;
let widget_fbo = *widget_surface_fbo;
let servo_framebuffer_id = state.servo().offscreen_framebuffer_id();
let _duration = context.run(window, |ctx| {
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
@ -387,26 +377,23 @@ impl Minibrowser {
return;
};
CentralPanel::default().frame(Frame::NONE).show(ctx, |ui| {
let Pos2 { x, y } = ui.cursor().min;
let Vec2 {
x: width,
y: height,
} = ui.available_size();
let rect =
Box2D::from_origin_and_size(Point2D::new(x, y), Size2D::new(width, height)) *
scale;
// If the top parts of the GUI changed size, then update the size of the WebView and also
// the size of its RenderingContext.
let available_size = ui.available_size();
let rect = Box2D::from_origin_and_size(
Point2D::origin(),
Size2D::new(available_size.x, available_size.y),
) * scale;
if rect != webview.rect() {
webview.move_resize(rect);
rendering_context.resize(rect.size().to_i32().to_untyped());
}
let min = ui.cursor().min;
let size = ui.available_size();
let rect = egui::Rect::from_min_size(min, size);
ui.allocate_space(size);
let Some(servo_fbo) = servo_framebuffer_id else {
return;
};
if let Some(status_text) = &self.status_text {
egui::containers::popup::show_tooltip_at(
ctx,
@ -417,45 +404,19 @@ impl Minibrowser {
);
}
ui.painter().add(PaintCallback {
rect,
callback: Arc::new(CallbackFn::new(move |info, painter| {
use glow::HasContext as _;
let clip = info.viewport_in_pixels();
let x = clip.left_px as gl::GLint;
let y = clip.from_bottom_px as gl::GLint;
let width = clip.width_px as gl::GLsizei;
let height = clip.height_px as gl::GLsizei;
unsafe {
painter.gl().clear_color(0.0, 0.0, 0.0, 0.0);
painter.gl().scissor(x, y, width, height);
painter.gl().enable(gl::SCISSOR_TEST);
painter.gl().clear(gl::COLOR_BUFFER_BIT);
painter.gl().disable(gl::SCISSOR_TEST);
let servo_fbo = NonZeroU32::new(servo_fbo).map(NativeFramebuffer);
painter
.gl()
.bind_framebuffer(gl::READ_FRAMEBUFFER, servo_fbo);
painter
.gl()
.bind_framebuffer(gl::DRAW_FRAMEBUFFER, widget_fbo);
painter.gl().blit_framebuffer(
x,
y,
x + width,
y + height,
x,
y,
x + width,
y + height,
gl::COLOR_BUFFER_BIT,
gl::NEAREST,
if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
ui.painter().add(PaintCallback {
rect,
callback: Arc::new(CallbackFn::new(move |info, painter| {
let clip = info.viewport_in_pixels();
let rect_in_parent = Rect::new(
Point2D::new(clip.left_px, clip.from_bottom_px),
Size2D::new(clip.width_px, clip.height_px),
);
painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo);
}
})),
});
render_to_parent(painter.gl(), rect_in_parent)
})),
});
}
});
*last_update = now;
@ -464,14 +425,11 @@ impl Minibrowser {
/// Paint the minibrowser, as of the last update.
pub fn paint(&mut self, window: &Window) {
unsafe {
use glow::HasContext as _;
self.context
.painter
.gl()
.bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo);
}
self.rendering_context
.parent_context()
.prepare_for_rendering();
self.context.paint(window);
let _ = self.rendering_context.parent_context().present();
}
/// Updates the location field from the given [WebViewManager], unless the user has started

View file

@ -11,6 +11,7 @@ use euclid::{Length, Scale};
use servo::compositing::windowing::WindowMethods;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
use servo::webrender_traits::rendering_context::RenderingContext;
use servo::{Cursor, WebView};
use super::app_state::RunningAppState;
@ -46,4 +47,5 @@ pub trait WindowPortsMethods: WindowMethods {
fn winit_window(&self) -> Option<&winit::window::Window>;
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel>;
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>);
fn rendering_context(&self) -> Rc<dyn RenderingContext>;
}