Add minimal libservo example using winit (#35118)

* Add minimal libservo example using winit

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>

* CI: include examples in libservo compile test

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>

* CI: build libservo with `continue-on-error`

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>

---------

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Delan Azabani 2025-01-23 20:15:53 +08:00 committed by GitHub
parent 8740c03682
commit 2db828f0c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 287 additions and 6 deletions

View file

@ -173,9 +173,10 @@ jobs:
timeout_minutes: 20
max_attempts: 2 # https://github.com/servo/servo/issues/30683
command: ./mach test-unit --${{ inputs.profile }}
- name: Build libservo
- name: Build libservo with examples
if: ${{ inputs.build-libservo }}
run: cargo build -p libservo
continue-on-error: true
run: cargo build -p libservo --all-targets
- name: Archive build timing
uses: actions/upload-artifact@v4
with:

View file

@ -160,9 +160,10 @@ jobs:
timeout_minutes: 40 # https://github.com/servo/servo/issues/30275
max_attempts: 3 # https://github.com/servo/servo/issues/30683
command: ./mach test-unit --${{ inputs.profile }}
- name: Build libservo
- name: Build libservo with examples
if: ${{ inputs.build-libservo }}
run: cargo build -p libservo
continue-on-error: true
run: cargo build -p libservo --all-targets
- name: Build mach package
run: ./mach package --${{ inputs.profile }}
- name: Run DMG smoketest

View file

@ -176,9 +176,10 @@ jobs:
timeout_minutes: 30
max_attempts: 3 # https://github.com/servo/servo/issues/30683
command: .\mach test-unit --${{ inputs.profile }} -- -- --test-threads=1
- name: Build libservo
- name: Build libservo with examples
if: ${{ inputs.build-libservo }}
run: cargo build -p libservo
continue-on-error: true
run: cargo build -p libservo --all-targets
- name: Archive build timing
uses: actions/upload-artifact@v4
with:

3
Cargo.lock generated
View file

@ -4210,6 +4210,7 @@ dependencies = [
"keyboard-types",
"layout_thread_2013",
"layout_thread_2020",
"libservo",
"log",
"media",
"mozangle",
@ -4218,6 +4219,7 @@ dependencies = [
"profile",
"profile_traits",
"rayon",
"rustls",
"script",
"script_layout_interface",
"script_traits",
@ -4238,6 +4240,7 @@ dependencies = [
"webrender_traits",
"webxr",
"webxr-api",
"winit",
]
[[package]]

View file

@ -105,3 +105,9 @@ surfman = { workspace = true, features = ["sm-x11", "sm-raw-window-handle-06"] }
[target.'cfg(all(not(target_os = "windows"), not(target_os = "ios"), not(target_os = "android"), not(target_env = "ohos"), not(target_arch = "arm"), not(target_arch = "aarch64")))'.dependencies]
gaol = "0.2.1"
[dev-dependencies]
libservo = { path = ".", features = ["tracing"] }
rustls = { version = "0.23", default-features = false, features = ["ring"] }
tracing = { workspace = true }
winit = "0.30.8"

View file

@ -0,0 +1,265 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::error::Error;
use std::mem::replace;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use base::id::WebViewId;
use compositing::windowing::{AnimationState, EmbedderEvent, EmbedderMethods, WindowMethods};
use embedder_traits::EmbedderMsg;
use euclid::{Point2D, Scale, Size2D};
use servo::Servo;
use servo_geometry::DeviceIndependentPixel;
use servo_url::ServoUrl;
use surfman::{Connection, SurfaceType};
use tracing::warn;
use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DevicePixel};
use webrender_traits::RenderingContext;
use winit::application::ApplicationHandler;
use winit::dpi::{PhysicalPosition, PhysicalSize};
use winit::event::WindowEvent;
use winit::event_loop::EventLoop;
use winit::raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use winit::window::Window;
fn main() -> Result<(), Box<dyn Error>> {
rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to install crypto provider");
let event_loop = EventLoop::with_user_event()
.build()
.expect("Failed to create EventLoop");
let mut app = App::new(&event_loop);
event_loop.run_app(&mut app)?;
Ok(())
}
enum App {
Initial(Waker),
Running {
window_delegate: Rc<WindowDelegate>,
servo: Servo<WindowDelegate>,
},
Exiting,
}
impl App {
fn new(event_loop: &EventLoop<WakerEvent>) -> Self {
Self::Initial(Waker::new(event_loop))
}
}
impl ApplicationHandler<WakerEvent> for App {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
if let Self::Initial(waker) = self {
let window = event_loop
.create_window(Window::default_attributes())
.expect("Failed to create winit Window");
let display_handle = event_loop
.display_handle()
.expect("Failed to get display handle");
let connection = Connection::from_display_handle(display_handle)
.expect("Failed to create connection");
let adapter = connection
.create_adapter()
.expect("Failed to create adapter");
let rendering_context = RenderingContext::create(&connection, &adapter, None)
.expect("Failed to create rendering context");
let native_widget = rendering_context
.connection()
.create_native_widget_from_window_handle(
window.window_handle().expect("Failed to get window handle"),
winit_size_to_euclid_size(window.inner_size())
.to_i32()
.to_untyped(),
)
.expect("Failed to create native widget");
let surface = rendering_context
.create_surface(SurfaceType::Widget { native_widget })
.expect("Failed to create surface");
rendering_context
.bind_surface(surface)
.expect("Failed to bind surface");
rendering_context
.make_gl_context_current()
.expect("Failed to make context current");
let window_delegate = Rc::new(WindowDelegate::new(window));
let mut servo = Servo::new(
Default::default(),
Default::default(),
rendering_context,
Box::new(EmbedderDelegate {
waker: waker.clone(),
}),
window_delegate.clone(),
Default::default(),
compositing::CompositeTarget::Window,
);
servo.setup_logging();
servo.handle_events([EmbedderEvent::NewWebView(
ServoUrl::parse("https://demo.servo.org/experiments/twgl-tunnel/")
.expect("Guaranteed by argument"),
WebViewId::new(),
)]);
*self = Self::Running {
window_delegate,
servo,
};
}
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
event: WindowEvent,
) {
if let Self::Running {
window_delegate,
servo,
} = self
{
let mut events_for_servo = vec![];
for (_webview_id, message) in servo.get_events() {
match message {
// FIXME: rust-analyzer autocompletes this as top_level_browsing_context_id
EmbedderMsg::WebViewOpened(webview_id) => {
let rect = window_delegate.get_coordinates().get_viewport().to_f32();
events_for_servo.extend([
EmbedderEvent::FocusWebView(webview_id),
EmbedderEvent::MoveResizeWebView(webview_id, rect),
EmbedderEvent::RaiseWebViewToTop(webview_id, true),
]);
},
_ => {},
}
}
servo.handle_events(events_for_servo);
}
match event {
WindowEvent::CloseRequested => {
if matches!(self, Self::Running { .. }) {
let Self::Running { servo, .. } = replace(self, Self::Exiting) else {
unreachable!()
};
// TODO: ask Servo to shut down and wait for EmbedderMsg::Shutdown?
servo.deinit();
}
event_loop.exit();
},
WindowEvent::RedrawRequested => {
if let Self::Running {
window_delegate,
servo,
} = self
{
servo.present();
window_delegate.window.request_redraw();
}
},
_ => (),
}
}
}
struct EmbedderDelegate {
waker: Waker,
}
impl EmbedderMethods for EmbedderDelegate {
// FIXME: rust-analyzer “Implement missing members” autocompletes this as
// webxr_api::MainThreadWaker, which is not available when building without
// libservo/webxr, and even if it was, it would fail to compile with E0053.
fn create_event_loop_waker(&mut self) -> Box<dyn embedder_traits::EventLoopWaker> {
Box::new(self.waker.clone())
}
}
#[derive(Clone)]
struct Waker(Arc<Mutex<winit::event_loop::EventLoopProxy<WakerEvent>>>);
#[derive(Debug)]
struct WakerEvent;
impl Waker {
fn new(event_loop: &EventLoop<WakerEvent>) -> Self {
Self(Arc::new(Mutex::new(event_loop.create_proxy())))
}
}
impl embedder_traits::EventLoopWaker for Waker {
fn clone_box(&self) -> Box<dyn embedder_traits::EventLoopWaker> {
Box::new(Self(self.0.clone()))
}
fn wake(&self) {
if let Err(error) = self
.0
.lock()
.expect("Failed to lock EventLoopProxy")
.send_event(WakerEvent)
{
warn!(?error, "Failed to wake event loop");
}
}
}
struct WindowDelegate {
window: Window,
animation_state: Cell<AnimationState>,
}
impl WindowDelegate {
fn new(window: Window) -> Self {
Self {
window,
animation_state: Cell::new(AnimationState::Idle),
}
}
}
impl WindowMethods for WindowDelegate {
fn get_coordinates(&self) -> compositing::windowing::EmbedderCoordinates {
let monitor = self
.window
.current_monitor()
.or_else(|| self.window.available_monitors().nth(0))
.expect("Failed to get winit monitor");
let scale =
Scale::<f64, DeviceIndependentPixel, DevicePixel>::new(self.window.scale_factor());
let window_size = winit_size_to_euclid_size(self.window.outer_size()).to_i32();
let window_origin = self.window.outer_position().unwrap_or_default();
let window_origin = winit_position_to_euclid_point(window_origin).to_i32();
let window_rect = DeviceIntRect::from_origin_and_size(window_origin, window_size);
let viewport_origin = DeviceIntPoint::zero(); // bottom left
let viewport_size = winit_size_to_euclid_size(self.window.inner_size()).to_f32();
let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32());
compositing::windowing::EmbedderCoordinates {
hidpi_factor: Scale::new(self.window.scale_factor() as f32),
screen_size: (winit_size_to_euclid_size(monitor.size()).to_f64() / scale).to_i32(),
available_screen_size: (winit_size_to_euclid_size(monitor.size()).to_f64() / scale)
.to_i32(),
window_rect: (window_rect.to_f64() / scale).to_i32(),
framebuffer: viewport.size(),
viewport,
}
}
fn set_animation_state(&self, state: compositing::windowing::AnimationState) {
self.animation_state.set(state);
}
}
pub fn winit_size_to_euclid_size<T>(size: PhysicalSize<T>) -> Size2D<T, DevicePixel> {
Size2D::new(size.width, size.height)
}
pub fn winit_position_to_euclid_point<T>(position: PhysicalPosition<T>) -> Point2D<T, DevicePixel> {
Point2D::new(position.x, position.y)
}

View file

@ -134,6 +134,10 @@ stdenv.mkDerivation (androidEnvironment // {
# [WARN script::dom::gpu] Could not get GPUAdapter ("NotFound")
# TLA Err: Error: Couldn't request WebGPU adapter.
vulkan-loader
# $ cargo run -p libservo --example winit_minimal
# Unable to load the libEGL shared object
libGL
];
shellHook = ''