• Stars
    star
    229
  • Rank 174,009 (Top 4 %)
  • Language
    CSS
  • Created about 7 years ago
  • Updated over 6 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

An example of how to embed Servo

Tutorial: Embedding Servo

This document describes how to use Servo in a third party Rust project. It comes with the implementation of standalone minimal browser.

This repository paulrouget/servo-embedding-example is regularly updated to match the must recent Servo version.

Servo documentation: doc.servo.org.

Servo type entry point: doc.servo.org/servo/struct.Servo.html.

Warning

The Servo embedding API is still work-in-progress. It is functional but not polished. Some cosmetic changes are planned. See this issue.

This tutorial does not cover how to build on Windows as it is far from being straight forward for now as it requires importing and tweaking multiple python script from Servo. See servo/servo#18343 and paulrouget/servoshell#33.

It doesn't not cover all the features we would find in a full-featured browser. A more complete example of a Servo-based browser can be found here: paulrouget/servoshell.

Overview

To embed Servo, we need to:

  1. compile Servo as a dependency
  2. provide OpenGL context with its OpenGL methods
  3. implement a set callbacks (servo-to-embedder communication) by implementing the WindowMethods trait:
    1. to provide coordinates of the OpenGL buffer,
    2. to wake-up the event loop,
    3. to update the OpenGL buffer,
    4. to be notified of page navigations,
  4. send events to servo (embedder-to-servo communication) by sending WindowEvents to servo:
    1. to create and select browsers,
    2. to enable mouse and keyboard interactions,
    3. to control page navigation,

In this tutorial we will go through each steps.

Create Rust project

Install rustup and make sure to install all the Servo dependencies as described here.

Initialize your project:

  • cargo init --bin --name servo-embedding-example

We recommend cloning the servo repository as we will need to copy some files to our project. In a parallel directory:

  • git clone https://github.com/servo/servo.git

Installing the proper Rust toolchain

As for now, Servo requires Rust Nightly.

Import the rust-toolchain file from the servo directory to make sure we use the same rustc version:

  • cp ../servo/rust-toolchain .
  • rustup install `cat rust-toolchain`

Note: this will change. See this issue.

libservo

Add Servo as a dependency to your Cargo.toml:

[dependencies]
libservo = { git = "https://github.com/servo/servo", rev = "XXXXXXXXXX" }

Replace XXXXXXXXXX with the commit hash of Servo HEAD. For example: 75a4dcf.

Because we are importing some files from the servo repository directly (rust-toolchain, and as we will see later, Cargo.lock and resources) and because Servo is not versionned yet, it is necessary to specify servo's revision hash (with rev = …).

If you'd rather use a local copy of Servo, do not point to the root of the servo directory but to components/servo:

[dependencies]
libservo = { path = "../servo/components/servo" }

Servo requires some specific version of its dependencies. It is highly recommended to import Servo's Cargo.lock:

  • cp ../servo/Cargo.lock .

Note: this will change. See this issue.

Servo also requires some "resources" files to run properly:

  • cp -r ../servo/resources .

Note: this will change. See this issue.

Update servo

In this example, we depend on a specific version of Servo. When you want to update Servo to a more recent version, do:

  1. change the revision in Cargo.toml (the rev field)
  2. Copy rust-toolchain, Cargo.lock and resources from Servo

OpenGL

The first thing Servo requires is access to OpenGL functions, as described by the gleam::gl::Gl trait.

It's up to the embedder to create and update the OpenGL buffer.

In this tutorial, we will use the Glutin crate to create the OpenGL context. Glutin is a library which provides cross-platform methods to open native windows with a full-window OpenGL buffer.

# Cargo.toml
glutin = "0.9"

Building

At this point, we should be able to build our project.

Minimal code:

extern crate servo;

fn main() {
    println!("Servo version: {}", servo::config::servo_version());
}

Run the project:

cargo build --release
cargo run --release

Notice that Cargo.lock has been updated.

Glutin's event loop

Now let's open an empty window and run the Glutin event loop (this code has nothing to do with Servo just yet). See Glutin documentation for more details.

extern crate servo;
extern crate glutin;

fn main() {
    println!("Servo version: {}", servo::config::servo_version());

    let mut event_loop = glutin::EventsLoop::new();

    let builder = glutin::WindowBuilder::new().with_dimensions(800, 600);
    let gl_version = glutin::GlRequest::Specific(glutin::Api::OpenGl, (3, 2));
    let context = glutin::ContextBuilder::new().with_gl(gl_version).with_vsync(true);
    let window = glutin::GlWindow::new(builder, context, &event_loop).unwrap();
  
    window.show();

    event_loop.run_forever(|_event| {
        glutin::ControlFlow::Continue
    });
}

EventLoopWaker: mechanism to wake up the event loop

Servo internals and the embedder code will run in different threads. Usually, the embedder will have an event loop that will block when no user events happen. Servo will need a way to wake up the event loop from a different thread when needed (to update the OpenGL buffer for example, or notify the embedder of any navigation changes). It's up to the embedder to provide such a mechanism, by providing a thread-safe object that implements the EventLoopWaker trait.

In our example, we will use Glutin's EventLoopProxy to wake up the event loop.

use std::sync::Arc;
use servo::compositing::compositor_thread::EventLoopWaker;

pub struct GlutinEventLoopWaker {
    pub proxy: Arc<glutin::EventsLoopProxy>
}

impl EventLoopWaker for GlutinEventLoopWaker {
    // Use by servo to share the "event loop waker" across threads
    fn clone(&self) -> Box<EventLoopWaker + Send> {
        Box::new(GlutinEventLoopWaker { proxy: self.proxy.clone() })
    }
    // Called by servo when the main thread needs to wake up
    fn wake(&self) {
        self.proxy.wakeup().expect("wakeup eventloop failed");
    }
}


fn main() {
    // […]
    let mut event_loop = glutin::EventsLoop::new();
    // Will be use used by Servo later
    let event_loop_waker = Box::new(GlutinEventLoopWaker {
        proxy: Arc::new(event_loop.create_proxy())
    });
    // […]
}

Retrieving OpenGL context

To provide access to the OpenGL context methods, we will use Glutin's get_proc_address to initialize the OpenGL binding:

use servo::gl;
use glutin::GlContext;

// […]
let gl = unsafe {
	window.context().make_current().expect("Couldn't make window current");
	gl::GlFns::load_with(|s| window.context().get_proc_address(s) as *const _)
};
// […]

At this point, we managed to:

  • compile our project with libservo
  • open an empty window
  • have a mechanism (GlutinEventLoopWaker) to wake the event loop
  • have an object (gl) that implements gl::Gl

The next step is to initialize Servo and implement its callbacks.

Configure Servo

http://doc.servo.org/servo_config/opts/struct.Opts.html

Servo can be configured with different options. It is also necessary to set the path of the resources directory we copied earlier.

use servo::servo_config::resource_files::set_resources_path;
use servo::servo_config::opts;

let path = env::current_dir().unwrap().join("resources");
let path = path.to_str().unwrap().to_string();
set_resources_path(Some(path));

let mut opts = opts::default_opts();
opts::set_defaults(opts);

the Servo type

Servo is the entry point to create browsers and send events to Servo's internals.

Initiliazing Servo (via Servo::new) requires an object that implements the WindowMethods.

Implement WindowMethods

http://doc.servo.org/compositing/windowing/trait.WindowMethods.html

The WindowMethods trait describes a set of methods the embedder must implement.

use servo::compositing::windowing::WindowMethods;
use std::rc::Rc;

struct Window {
    // All these fields will be used in WindowMethods implementations
    glutin_window: glutin::GlWindow,
    waker: Box<EventLoopWaker>,
    gl: Rc<gl::Gl>,
}

// […]

let window = Rc::new(Window {
    glutin_window: window,
    waker: event_loop_waker,
    gl: gl,
});

let mut servo = servo::Servo::new(window.clone());

// […]

impl WindowMethods for Window {
    // […]
}

These callbacks are used by Servo, to:

retrieve the event loop waker

via create_event_loop_waker().

get and set drawing area coordinates

via framebuffer_size() window_rect() size() …

These methods are used by Servo to get the size, position and pixel density of the window. For example when a website use window.outerWidth. Also used to calculate the layout of the page.

set_inner_size() set_position()…

Used to resize and move the native window. When a website use window.moveTo() for example.

OpenGL operations

These methods are used to update the pixels on the screen. present() is called when the OpenGL buffer is ready to be painted. Usually, by swapping the OpenGL buffers. prepare_for_composite() is called before compositing. It's useful when multiple buffers are present. Usually, it's when the OpenGL needs to be made current.

Navigation callbacks

load_start load_end head_parsed history_changed…

These methods are used to inform the embedder of the status of the current web page. For example, these are usuful to update a spinner when the page is loading or update the title and the url of the tab, when the user clicks on a link.

Minimal implementation:

use servo::euclid::{Point2D, ScaleFactor, Size2D, TypedPoint2D, TypedRect, TypedSize2D};
use servo::ipc_channel::ipc;
use servo::net_traits::net_error_list::NetError;
use servo::script_traits::LoadData;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::servo_url::ServoUrl;
use servo::style_traits::DevicePixel;
use servo::style_traits::cursor::Cursor;
use servo::msg::constellation_msg::{Key, KeyModifiers};

// […]

impl WindowMethods for Window {
    fn prepare_for_composite(&self, _width: usize, _height: usize) -> bool {
        true
    }

    fn present(&self) {
        self.glutin_window.swap_buffers().unwrap();
    }

    fn supports_clipboard(&self) -> bool {
        false
    }

    fn create_event_loop_waker(&self) -> Box<EventLoopWaker> {
        self.waker.clone()
    }

    fn gl(&self) -> Rc<gl::Gl> {
        self.gl.clone()
    }

    fn hidpi_factor(&self) -> ScaleFactor<f32, DeviceIndependentPixel, DevicePixel> {
        ScaleFactor::new(self.glutin_window.hidpi_factor())
    }

    fn framebuffer_size(&self) -> TypedSize2D<u32, DevicePixel> {
        let (width, height) = self.glutin_window.get_inner_size().unwrap();
        let scale_factor = self.glutin_window.hidpi_factor() as u32;
        TypedSize2D::new(scale_factor * width, scale_factor * height)
    }

    fn window_rect(&self) -> TypedRect<u32, DevicePixel> {
        TypedRect::new(TypedPoint2D::new(0, 0), self.framebuffer_size())
    }

    fn size(&self) -> TypedSize2D<f32, DeviceIndependentPixel> {
        let (width, height) = self.glutin_window.get_inner_size().unwrap();
        TypedSize2D::new(width as f32, height as f32)
    }

    fn client_window(&self, _id: BrowserId) -> (Size2D<u32>, Point2D<i32>) {
        let (width, height) = self.glutin_window.get_inner_size().unwrap();
        let (x, y) = self.glutin_window.get_position().unwrap();
        (Size2D::new(width, height), Point2D::new(x as i32, y as i32))
    }

    fn set_page_title(&self, _id: BrowserId, _title: Option<String>) {
        self.glutin_window.set_title(match title {
            Some(ref title) => title,
            None => "",
        });
    }

    fn allow_navigation(&self, _id: BrowserId, _url: ServoUrl, chan: ipc::IpcSender<bool>) {
        chan.send(true).ok();
    }

    fn set_inner_size(&self, _id: BrowserId, _size: Size2D<u32>) {
    }

    fn set_position(&self, _id: BrowserId, _point: Point2D<i32>) {
    }

    fn set_fullscreen_state(&self, _id: BrowserId, _state: bool) {
    }

    fn status(&self, _id: BrowserId, _status: Option<String>) {
    }

    fn load_start(&self, _id: BrowserId) {
    }

    fn load_end(&self, _id: BrowserId) {
    }

    fn load_error(&self, _id: BrowserId, _: NetError, _url: String) {
    }

    fn head_parsed(&self, _id: BrowserId) {
    }

    fn history_changed(&self, _id: BrowserId, _entries: Vec<LoadData>, _current: usize) {
    }

    fn set_cursor(&self, _cursor: Cursor) {
    }

    fn set_favicon(&self, _id: BrowserId, _url: ServoUrl) {
    }

    fn handle_key(&self, _id: Option<BrowserId>, _ch: Option<char>, _key: Key, _mods: KeyModifiers) {
    }
}

Note: this will change. See this issue.

Creating a browser

Now we have the callbacks in place, it's time to create the first browser. A browser, also known as a top level browsing context, can be seen as a "tab". It is identified by a BrowserId, which is an alias to TopLevelBrowsingContextId. Multiple browsers can be instanciated. Only one can be displayed at a time, unless multiple compositor are created (see this issue).

So far, we've seen how servo talks back to the embedder. To communicate to servo, we use WindowEvents. The two events we are interested in now are CreateBrowserand SelectBrowser.

CreateBrowser will build a new browser and, once ready, send back its BrowserId via a ipc channel. Once received, we use SelectBrowser to ask servo to paint that specific browser.

let url = ServoUrl::parse("https://servo.org").unwrap();
let (sender, receiver) = ipc::channel().unwrap();
servo.handle_events(vec![WindowEvent::NewBrowser(url, sender)]);
let browser_id = receiver.recv().unwrap();
servo.handle_events(vec![WindowEvent::SelectBrowser(browser_id)]);

Drawing the browser

At this point, almost everything is in place.

If we run the current code, we will only see a white screen. At the moment, servo needs to be notified when to process events from its internal components, and from the embedder.

Note: this will change. See this issue.

An empty call to handle_events will do.

event_loop.run_forever(|event| {
  // Blocked until user event or until servo unblocks it
  match event {
    // This is the event triggered by GlutinEventLoopWaker
    glutin::Event::Awakened => {
      servo.handle_events(vec![]);
    },
    _ => {
    }
  }
  glutin::ControlFlow::Continue
});

At this point, the web page should be painted.

embedding1

Notice that the window title says "Servo, the parallel browser engine". The title has been update via the WindowMethod::set_page_title callback we implemented earlier.

Sending events to servo

As you can notice, user inputs have no effects on the page. Scrolling, clicks or keyboard inputs are not sent to servo yet. In our example, user events come from Glutin, and need to be sent to Servo via handle_events(Vec<WindowEvent>). WindowEvents are use for embedder-to-servo communication. All the events are described in windowing.rs:

https://doc.servo.org/compositing/windowing/enum.WindowEvent.html

We already used two WindowEvent: CreateBrowser and SelectBrowser.

In this tutorial, we will implement mousemove events, cursor update, and scrolling.

Mouse coordinates and cursor update

This is a good way to show the two-ways communication mechanism between servo and the embedder. As the mouse moves, the embedder needs to send the pointer coordinates to servo, and as the pointer moves, servo might request the embedder to change its cursor (when hovering a link for example).

We catch the Glutin event, translate it into a Servo event, and send it to Servo. We save the pointer position as we will need it later for scrolling.

event_loop.run_forever(|event| {
  // Blocked until user event or until servo unblocks it
  match event {
    glutin::Event::WindowEvent {event: glutin::WindowEvent::MouseMoved{position: (x, y), ..} , ..} => {
      pointer = (x, y);
      let event = WindowEvent::MouseWindowMoveEventClass(TypedPoint2D::new(x as f32, y as f32));
      servo.handle_events(vec![event]);
    }

Now, as we can see, Servo paints an underline when the pointer hovers a link:

embedding2

Now, we expect the cursor to change from a regular arrow to the hand pointer. This is done by implementing the associated WindowMethod:

impl WindowMethods for Window {
  // […]
  fn set_cursor(&self, cursor: Cursor) {
    let cursor = match cursor {
      Cursor::Pointer => glutin::MouseCursor::Hand,
      _ => glutin::MouseCursor::Default,
    };
    self.glutin_window.set_cursor(cursor);
  }
}

embedding3

Now the cursor is properly updated as the pointer hovers the link.

Scrolling

Scrolling is pretty straight forward. Again, all we need is to translate Glutin events to Servo events:

glutin::Event::WindowEvent {event: glutin::WindowEvent::MouseWheel{delta, phase, ..} , ..} => {
  let (dx, dy) = match delta {
    glutin::MouseScrollDelta::LineDelta(dx, dy) => (dx, dy * 38.0 /*line height*/),
    glutin::MouseScrollDelta::PixelDelta(dx, dy) => (dx, dy),
  };
  let scroll_location = servo::webrender_api::ScrollLocation::Delta(TypedVector2D::new(dx, dy));
  let phase = match phase {
    glutin::TouchPhase::Started => TouchEventType::Down,
    glutin::TouchPhase::Moved => TouchEventType::Move,
    glutin::TouchPhase::Ended => TouchEventType::Up,
    glutin::TouchPhase::Cancelled => TouchEventType::Up,
  };
  let pointer = TypedPoint2D::new(pointer.0 as i32, pointer.1 as i32);
  let event = WindowEvent::Scroll(scroll_location, pointer, phase);
  servo.handle_events(vec![event]);
}

At this point, scrolling throught the web page should work.

Navigation control

Now let's say we want to reload the web page when the user presses the R key. We need to match the glutin key event, check the key, and then send a Reload event. Navigation events usually require a BrowserId.

// reload when R is pressed
glutin::Event::WindowEvent {
  event: glutin::WindowEvent::KeyboardInput{ input: glutin::KeyboardInput {
    state: glutin::ElementState::Pressed,
    virtual_keycode: Some(glutin::VirtualKeyCode::R),
    ..
    }, ..}, ..} => {
      let event = WindowEvent::Reload(browser_id);
      servo.handle_events(vec![event]);
}

Tab support

For tab support, all that needs to be done is creating a new browser via CreateBrowser, and select which one to paint via SelectBrowser. WindowMethods come with a BrowserId field which will tell us which tab is targetted.

Full support for key and mouse events and navigation control

The same way mouse move and mouse wheel events are sent to servo, it's also necessary to forward key events and mouse inputs. When it comes to navigation control, like reload, going back/forward, loading a new url, etc; it's up to the embedder to send to appropriate WindowEvent. This is not covered by this tutorial to not make it longer that it's necessary.

More

We recommend looking at Servo's Glutin ports or the ServoShell's Glutin port (includes tabs support) for a fully functional browser that uses all the WindowEvent and implements all the WindowMethod.

More Repositories

1

firefox.html

Firefox.html is an experiment: trying to re-implement the Firefox UI in HTML.
1,128
star
2

dzslides

DZSlides is a one-file HTML template to build slides in HTML5 and CSS3.
HTML
932
star
3

servoshell

A work-in-progress user interface for Servo, built in Rust.
Python
281
star
4

firefox-jsterm

JavaScript Terminal For Firefox
JavaScript
174
star
5

html5dashboard

HTML5 Dashboard
JavaScript
172
star
6

miniuploader

Upload to imgur.com with a couple of lines of code (JavaScript)
55
star
7

riverscreen

Alternative homescreen for Firefox OS
CSS
40
star
8

concatenator

Concatenator is a one-file HTML app that concatenate several images into a single image.
27
star
9

servo-embedding-api

Work in progress - Servo Embedding API proposol
Rust
26
star
10

mwc-demos

mwc demos
JavaScript
25
star
11

firefox-remote-styleEditors

I suck at python. Don't look at this, just proof of concept.
Python
14
star
12

mozBrowserAPI

Mozilla Browser API
11
star
13

AndroidServoView

ServoView is a drop-in replacement for GeckoView on Android.
Java
10
star
14

paulrouget.github.com

my website
HTML
10
star
15

vianaic

Vianaic script: Rules, references, history and examples
HTML
10
star
16

motivational

A Canvas Motivational Poster Editor
JavaScript
8
star
17

gtk

A temporary fork of Gtk, experimenting with better Macos integration.
C
8
star
18

flagit

A Vim plugin that displays icons next to marks.
Vim Script
7
star
19

hacks.mozilla.archives

JavaScript
7
star
20

htmlmediakeys

Bring media events to Web content
JavaScript
7
star
21

libsimpleservo

CSS
5
star
22

servofocus

Experimenting with a Xamarin.Forms + Servo
C#
5
star
23

firefox-animation-clock

Let you pause/step/speed-up the animation clock of a web page
JavaScript
5
star
24

font-inspector

JavaScript
5
star
25

immersivevideo

Panoramic Video with WebGL and HTML5
JavaScript
4
star
26

firefox-ui-template

A HTML mockup of the Firefox UI
3
star
27

dirplayer-fxos

Directory-based music player (Firefox OS player)
JavaScript
3
star
28

himalaya-gui

Just some experiments with Himalaya
Rust
3
star
29

html5dashboard_mobile

HTML5 Dasboard for Mobile
JavaScript
3
star
30

vianaic-font

Rust
3
star
31

bugzilla-light

A light UI for bugzilla
JavaScript
2
star
32

guicss

[WIP] A simple CSS system, based on Servo's parser
Rust
2
star
33

xul.css

<vbox>, <hbox>, <spacer>, ... xul, the good stuff, in HTML and CSS.
CSS
2
star
34

dzbugs

JavaScript
2
star
35

gtk-todo-demo

Just a simple gtk app to test my gtk patches
Rust
2
star
36

imgur-fxos

A simple Firefox OS app. Take a picture and share it online.
JavaScript
2
star
37

libui-servo-example

Just experimenting for now
C
2
star
38

slides

Where I publish my slides
2
star
39

servoapi

This is a temporary layer on top of Servo. We will eventually merge this API into Servo itself.
Rust
2
star
40

TabSorter

JavaScript
2
star
41

readability-fxos

A readbility client for Firefox OS
JavaScript
2
star
42

mockup-app-manager

JavaScript
2
star
43

mjs-sys

Rust mJS bindings
Rust
2
star
44

london

1
star
45

hn-fxos

Hackernews client for Firefox OS
CSS
1
star
46

appmanager-launcher

Firefox Addon: add a button to the toolbar to launch webide
JavaScript
1
star
47

pauls-firefox-menu

JavaScript
1
star
48

hologram

A CSS3 Hologram illusion (for Firefox 4 Mobile)
JavaScript
1
star
49

dci2

HTML5 Dynamic Content Injection
JavaScript
1
star
50

githubapi-experiments

JavaScript
1
star
51

firefoxos-hotfix

Script that updates `omni.ja` content (from the phone)
Shell
1
star
52

pushover-firefox-share-button

Pushover via Firefox Social API
1
star
53

mockups-devtools

1
star
54

servoglwindows

A work-in-progress cross platform window library for Synchro.
Rust
1
star
55

berry.vim

Vim syntax plugin for the Berry language.
Vim Script
1
star
56

simple-webcam-viewer

Simple HTML5 WebCam Viewer
1
star
57

privatebrowser

A privacy-focused browser for Firefox OS
JavaScript
1
star
58

bgvideo

1
star
59

devtools-stress-test

A tricky webpage by mihaisucan
1
star
60

bob-spoon

Lua
1
star
61

ms-example-direct-im-with-xaml

An example of an UWP App being launched to immersive mode while keeping a XAML view
C++
1
star
62

vorbis

Clone of https://gitlab.xiph.org/xiph/vorbis
C
1
star
63

cargo-issue-4604

1
star