From 2db828f0c7e6704d3d7374ba8688c491ef7fe3e9 Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Thu, 23 Jan 2025 20:15:53 +0800 Subject: [PATCH] Add minimal libservo example using winit (#35118) * Add minimal libservo example using winit Signed-off-by: Delan Azabani Co-authored-by: Martin Robinson * CI: include examples in libservo compile test Signed-off-by: Delan Azabani Co-authored-by: Martin Robinson * CI: build libservo with `continue-on-error` Signed-off-by: Delan Azabani Co-authored-by: Martin Robinson --------- Signed-off-by: Delan Azabani Co-authored-by: Martin Robinson --- .github/workflows/linux.yml | 5 +- .github/workflows/mac.yml | 5 +- .github/workflows/windows.yml | 5 +- Cargo.lock | 3 + components/servo/Cargo.toml | 6 + components/servo/examples/winit_minimal.rs | 265 +++++++++++++++++++++ shell.nix | 4 + 7 files changed, 287 insertions(+), 6 deletions(-) create mode 100644 components/servo/examples/winit_minimal.rs diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 119789b1843..6ee1303edd3 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -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: diff --git a/.github/workflows/mac.yml b/.github/workflows/mac.yml index 0b1296c77cf..51a25a7a411 100644 --- a/.github/workflows/mac.yml +++ b/.github/workflows/mac.yml @@ -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 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 03813ea321c..5cf1e50b853 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -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: diff --git a/Cargo.lock b/Cargo.lock index e1b96d2056c..c498419eb09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/components/servo/Cargo.toml b/components/servo/Cargo.toml index b7d300afe60..2afbe3a0b63 100644 --- a/components/servo/Cargo.toml +++ b/components/servo/Cargo.toml @@ -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" diff --git a/components/servo/examples/winit_minimal.rs b/components/servo/examples/winit_minimal.rs new file mode 100644 index 00000000000..56a42ac4cc3 --- /dev/null +++ b/components/servo/examples/winit_minimal.rs @@ -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> { + 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, + servo: Servo, + }, + Exiting, +} + +impl App { + fn new(event_loop: &EventLoop) -> Self { + Self::Initial(Waker::new(event_loop)) + } +} + +impl ApplicationHandler 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 { + Box::new(self.waker.clone()) + } +} + +#[derive(Clone)] +struct Waker(Arc>>); +#[derive(Debug)] +struct WakerEvent; + +impl Waker { + fn new(event_loop: &EventLoop) -> Self { + Self(Arc::new(Mutex::new(event_loop.create_proxy()))) + } +} + +impl embedder_traits::EventLoopWaker for Waker { + fn clone_box(&self) -> Box { + 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, +} + +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::::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(size: PhysicalSize) -> Size2D { + Size2D::new(size.width, size.height) +} + +pub fn winit_position_to_euclid_point(position: PhysicalPosition) -> Point2D { + Point2D::new(position.x, position.y) +} diff --git a/shell.nix b/shell.nix index 5850584b437..46f6e9db0ca 100644 --- a/shell.nix +++ b/shell.nix @@ -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 = ''