/ultralight

Ultralight— a lightweight, pure-GPU, HTML UI renderer for native apps.

Primary LanguageC++

Ultralight

Welcome to the Ultralight Universal SDK!

This package contains everything you need to start building cross-platform HTML apps.

What is Ultralight?

Ultralight is a lightweight, pure-GPU, HTML rendering engine for native apps. Our aim is to provide support for the majority of the HTML5/CSS/JavaScript spec while still being as lightweight (in binary size and memory usage) as possible.

The motivation for this project stemmed from the observation that Chromium and other layout engines have become suboptimal for use in desktop app UI due to a separate set of design goals (running untrusted code, favoring performance at the cost of memory, the need to support every web platform feature under the sun, etc.).

Most native apps also need finer control over low-level platform functionality (such as file system, rendering, clipboard, etc.). Ultralight aims to not just be lightweight, but to offer native app developers much deeper integration with the underlying HTML engine.

Ultralight is also pure-GPU, meaning that all rendering (text, shadows, images, CSS transforms), is done via the GPU. The renderer emits abstract GPU commands (see the GPUDriver interface) which can then be handled by whichever graphics API you prefer (we provide stock implementations for D3D11, OpenGL, and Metal as of this writing).

Cross-Platform AppCore Runtime

On top of Ultralight, we've built an additional layer called AppCore that handles window creation, native event loops, native graphics API drivers, and more.

AppCore is intended to be used by those intending to build a standalone HTML-based desktop app and will eventually offer an API similar to Electron.

Useful Links

Link URL
Slack Channel https://chat.ultralig.ht
Twitter https://twitter.com/ultralight_ux
API Docs https://ultralig.ht/api/1_0/

Table of Contents

Getting Started

Before you get started, you will need the following on each platform:

Build Requirements

Common requirements for all platforms

  • CMake 2.8.12 or later
  • Compiler with C++11 or later

Building Sample Projects

Building Samples with CMake (All Platforms)

To generate projects for your platform and build, run the following from the root of the SDK:

mkdir build
cd build
cmake ..
cmake --build . --config Release

Building 64-bit on Windows

If you run the command cmake .. without any generators on Windows, it will usually select the default 32-bit Visual Studio version you have installed. To generate projects for 64-bit on Windows you will need to explicitly tell CMake to use the x64 platform.

mkdir build64
cd build64
cmake .. -DCMAKE_GENERATOR_PLATFORM=x64
cmake --build . --config Release

Running the Samples

On macOS and Linux the projects will be built to:

/build/samples/Browser/

On Windows the projects will be built to:

/build/samples/Browser/$(Configuration)

Using the C++ API

Compiler / Linker Flags

Setting your Include Directories

To use Ultralight in your C++ code, simply add the following directory to your project's include directories (replace $(ULTRLIGHT_SDK_ROOT) with the actual path you've placed the SDK):

$(ULTRALIGHT_SDK_ROOT)/include/

Linking to the Library (Windows / MSVC)

In Visual Studio, go to Linker → General → Additional Library Directories in your project's properties and set one of the following:

For Win32 / x86 Platforms:

$(ULTRALIGHT_SDK_ROOT)/lib/win/x86/

OR

For x64 Platforms:

$(ULTRALIGHT_SDK_ROOT)/lib/win/x64/

Then, go to Linker → Input → Additional Dependencies and add the following:

Ultralight.lib
UltralightCore.lib
WebCore.lib
AppCore.lib

Note: AppCore.lib is optional, only link if you use the AppCore API headers..

Linking to the Library (Linux)

First, copy the shared libraries in $(ULTRALIGHT_SDK_ROOT)/bin/linux to your OS's standard library directory.

Then, add the following to your Makefile's LDFLAGS:

-lUltralight -lUltralightCore -lWebCore -lAppCore

Note: -lAppCore is optional, only link if you use the AppCore API headers..

Linking to the Library (macOS)

Within XCode, select your target and go to General → Linked Frameworks and Libraries and add the following:

libUltralightCore.dylib
libUltralight.dylib
libWebCore.dylib
libAppCore.dylib

Or alternatively, if you are building with a Makefile, add the following to your LDFLAGS:

-lUltralight -lUltralightCore -lWebCore -lAppCore

Note: AppCore is optional, only link if you use the AppCore API headers..

API Headers

Simply include <Ultralight/Ultralight.h> at the top of your code to import the API.

#include <Ultralight/Ultralight.h>

If you want to use the optional AppCore API (cross-platform windowing/drawing layer), you should also include <AppCore/AppCore.h> at the top of your code.

#include <AppCore/AppCore.h>

Ultralight also exposes the full JavaScriptCore API so that users can make native calls to/from the JavaScript VM. To include these headers simply add:

#include <JavaScriptCore/JavaScriptCore.h>

Platform Handlers

Most OS-specific tasks in Ultralight can be overridden by users via the Platform interface.

Default platform implementations have been provided for everything except the FileSystem interface.

Of special note, the default GPUDriver is an offscreen OpenGL implementation that renders each View to a bitmap (see View::bitmap). This isn't the most performant option so you should instead use a native driver for each platform (eg, Metal on macOS). Platform-native drivers are automatically used when creating a Window through the AppCore API.

auto& platform = Platform::instance();

platform.set_config(config_);
platform.set_gpu_driver(gpu_driver_);
platform.set_file_system(file_system_);
platform.set_font_loader(font_loader_);

Config

The Config class allows you to configure renderer behavior and runtime WebKit options.

The most common things to customize are face_winding for front-facing triangles and device_scale_hint for application DPI scaling (used for oversampling raster output).

You can also set the default font (instead of Times New Roman).

Config config_;
config.face_winding = kFaceWinding_Clockwise; // CW in D3D, CCW in OGL
config.device_scale_hint = 1.0;               // Set DPI to monitor DPI scale
config.font_family_standard = "Segoe UI";     // Default font family

Platform::instance().set_config(config_);

GPUDriver

The virtual GPUDriver interface is used to perform all rendering in Ultralight.

Reference implementations for Direct3D11, OpenGL 3.2, Metal 2, and others are provided in the AppCore code (see deps/AppCore/src/ in the SDK).

class UExport GPUDriver
{
public:
  virtual ~GPUDriver();

  /******************************
   * Synchronization            *
   ******************************/

  virtual void BeginSynchronize() = 0;
  virtual void EndSynchronize() = 0;

  /******************************
   * Textures                   *
   ******************************/

  virtual uint32_t NextTextureId() = 0;
  virtual void CreateTexture(uint32_t texture_id,
                             Ref<Bitmap> bitmap) = 0;
  virtual void UpdateTexture(uint32_t texture_id,
                             Ref<Bitmap> bitmap) = 0;
  virtual void BindTexture(uint8_t texture_unit,
                           uint32_t texture_id) = 0;
  virtual void DestroyTexture(uint32_t texture_id) = 0;

  /******************************
   * Render Buffers             *
   ******************************/

  virtual uint32_t NextRenderBufferId() = 0;
  virtual void CreateRenderBuffer(uint32_t render_buffer_id,
                                 const RenderBuffer& buffer) = 0;
  virtual void BindRenderBuffer(uint32_t render_buffer_id) = 0;
  virtual void SetRenderBufferViewport(uint32_t render_buffer_id,
                                       uint32_t width,
                                       uint32_t height) = 0;
  virtual void ClearRenderBuffer(uint32_t render_buffer_id) = 0;
  virtual void DestroyRenderBuffer(uint32_t render_buffer_id) = 0;

  /******************************
   * Geometry                   *
   ******************************/

  virtual uint32_t NextGeometryId() = 0;
  virtual void CreateGeometry(uint32_t geometry_id,
                              const VertexBuffer& vertices,
                              const IndexBuffer& indices) = 0;
  virtual void UpdateGeometry(uint32_t geometry_id,
                              const VertexBuffer& vertices,
                              const IndexBuffer& indices) = 0;
  virtual void DrawGeometry(uint32_t geometry_id,
                            uint32_t indices_count,
                            uint32_t indices_offset,
                            const GPUState& state) = 0;
  virtual void DestroyGeometry(uint32_t geometry_id) = 0;

  /******************************
   * Command List               *
   ******************************/

  virtual void UpdateCommandList(const CommandList& list) = 0;
  virtual bool HasCommandsPending() = 0;
  virtual void DrawCommandList() = 0;
};

FontLoader

The virtual FontLoader interface is used to load font files (TrueType and OpenType files, usually stored somewhere in the Operating System) by font family.

class UExport FontLoader
{
public:
  virtual ~FontLoader();

  virtual String16 fallback_font() const = 0;
  
  virtual Ref<Buffer> Load(const String16& family,
                           int weight,
                           bool italic,
                           float size) = 0;
};

FileSystem

The virtual FileSystem interface is used for loading File URLs (eg, file:///page.html) and the JavaScript FileSystem API.

This API should be used to load any HTML/JS/CSS assets you've bundled with your application.

Only a small subset needs to be implemented to support File URL loading, specifically the following:

class UExport FileSystem
{
public:
  virtual ~FileSystem();

  virtual bool FileExists(const String16& path) = 0;

  virtual bool GetFileSize(FileHandle handle,
                           int64_t& result) = 0;

  virtual bool GetFileMimeType(const String16& path,
                               String16& result) = 0;

  virtual FileHandle OpenFile(const String16& path,
                              bool open_for_writing) = 0;

  virtual void CloseFile(FileHandle& handle) = 0;

  virtual int64_t ReadFromFile(FileHandle handle,
                               char* data,
                               int64_t length) = 0;
};

Rendering

Creating the Renderer

The Renderer class is used to create Views and update them.

You should create only one instance per application lifetime:

Ref<Renderer> renderer = Renderer::Create();

Updating the Renderer

Once per frame, you should call Renderer::Update() and Renderer::Render(), here is brief outline of how your Update function should look:

void MyApplication::Update()
{
  // Update internal logic (timers, event callbacks, etc.)
  renderer->Update();

  driver->BeginSynchronize();

  // Render all active views to command lists and dispatch calls to GPUDriver
  renderer->Render();

  driver->EndSynchronize();

  // Draw any pending commands to screen
  if (driver->HasCommandsPending())
  {
    driver->DrawCommandList();
    
    // Perform any additional drawing (Overlays) here...
    DrawOverlays();

    // Flip buffers here.
  }
}

Drawing View Overlays

When using your own GPUDriver, Views are rendered to an offscreen texture and so it is the user's responsibility to draw this texture to the screen. To get the Texture ID for a View, please see View::render_target().

Managing Views

Views are used to display and interact with web content in Ultralight.

Creating Views

To create a View, simply call Renderer::CreateView() with your desired width and height:

Ref<View> view = renderer_->CreateView(800, 600, false);

Loading Content

You can load content into a View via either View::LoadHTML() or View::LoadURL():

view->LoadHTML("<h1>Hello World</h1>"); // HTML string

view->LoadURL("http://www.google.com"); // Remote URL

view->LoadURL("file:///asset.html");    // File URL

Note: To load local File URLs, make sure your FileSystem resolves file paths relative to your application's asset directory.

Passing Mouse / Keyboard Input

You can pass input events to a View via the following methods:

view->FireMouseEvent(mouse_event);

view->FireKeyEvent(key_event);

view->FireScrollEvent(scroll_event);

Look at MouseEvent.h, KeyEvent.h, and ScrollEvent.h for more information.

Handling View Events

You can set callbacks for various View-related events by implementing the ViewListener interface and/or the LoadListener interface.

ViewListener Interface

To listen for View-specific events, you should inherit from the virtual ViewListener interface and bind your instance to a View via View::set_view_listener().

class MyViewListener : public ViewListener
{
public:
  MyViewListener() {}
  virtual ~MyViewListener() {}

  virtual void OnChangeTitle(View* caller,
                             const String& title) {}
  virtual void OnChangeURL(View* caller,
                           const String& url) {}
  virtual void OnChangeTooltip(View* caller,
                               const String& tooltip) {}
  virtual void OnChangeCursor(View* caller,
                              Cursor cursor) {}
  virtual void OnAddConsoleMessage(View* caller,
                                   MessageSource source,
                                   MessageLevel level,
                                   const String& message,
                                   uint32_t line_number,
                                   uint32_t column_number,
                                   const String& source_id) {}
};

// ... <snip>

// Later, bind an instance of MyViewListener to your View
view->set_view_listener(new MyViewListener());

LoadListener Interface

To listen for page load events, you should inherit from the virtual LoadListener interface and bind your instance to a View via View::set_load_listener().

class MyLoadListener : public LoadListener
{
public:
  MyLoadListener() {}
  virtual ~MyLoadListener() {}

  virtual void OnBeginLoading(View* caller) {}
  virtual void OnFinishLoading(View* caller) {}
  virtual void OnUpdateHistory(View* caller) {}
  virtual void OnDOMReady(View* caller) {}
};

// ... <snip>

// Later, bind an instance of MyLoadListener to your View
view->set_load_listener(new MyLoadListener());

JavaScript Integration

JavaScriptCore API

Ultralight exposes the entire JavaScriptCore C API for maximum performance and flexibility. This allows you to make direct calls to and from the native JavaScriptCore VM.

To include this API in your code, simply include <JavaScriptCore/JavaScript.h>

#include <JavaScriptCore/JavaScript.h>

To simplify things, a C++ wrapper for JavaScriptCore is provided in the AppCore code (we'll be using this in subsequent code examples). Simply include <AppCore/JSHelpers.h> to use it:

#include <AppCore/JSHelpers.h>

Set the JSContext

Before you can make any calls to JavaScript code (including creating any JSValues, JSObjects, etc.), you should pass your View's JSContext to SetJSContext():

#include <AppCore/JSHelpers.h>
using namespace framework;

//...

// Get JSContext from a View
JSContextRef myContext = view->js_context();

// Set the JSContext for all subsequent JSHelper calls
SetJSContext(myContext);

// Now we can create JSValues/JSObjects and call JavaScript code
JSValue val = 42;

JSObject obj;
obj["myProperty"] = val;

JSValue result = JSEval("1 + 1");

Note: you can create JSStrings and JSFunctions without a context.

// Don't need a context to create JSStrings:
JSString str = "Hello!";

// Don't need a context to default construct JSFunctions:
JSFunction emptyFunc;
emptyFunc.IsValid(); // Evaluates to FALSE

DOMReady Event

You should perform all API calls that reference DOM elements or scripts on a page in the DOMReady Event. See the LoadListener Interface above for details on how to register your own listener for this event.

struct MyListener : public LoadListener
{
  virtual void OnDOMReady(View* caller) override
  {
    SetJSContext(caller->js_context());

    // Perform page-specific JavaScript here.
  }
};

Evaluating Scripts

You can evaluate JavaScript in the current context by calling JSEval().

JSValue result = JSEval("1 + 1");

result.IsNumber(); // TRUE

result.ToInteger(); // 2

Calling JS Functions from C++

Let's say you had the following JavaScript function defined on your page:

function addNumbers(a, b) { return a + b; }

To get the function in C++, you would simply call:

// Get the global object
JSObject global = JSGlobalObject();

// Get the "addNumbers" property as a JSFunction. (debug assert if invalid)
JSFunction addNumbers = global["addNumbers"];

Call the JSFunction object (takes an initializer list of zero or more JSValues):

// Check if function is valid first.
if (addNumbers.IsValid())
{
  // Call the JSFunction
  JSValue result = addNumbers({ 1, 1 }); // will equal 2
  
  /**
   * NOTE: You can also pass a JSObject as the first parameter of a
   * JSFunction invocation to specify the 'this' object in JavaScript.
   *
   * If you don't specify one like above, the Global Object is used.
   */
   addNumbers(myObject, { 1, 1 });
}

Calling C++ Functions from JS

First define your C++ callback function with the following signature:

void MyClass::MyCallback(const JSObject& thisObject, const JSArgs& args)
{
  // Handle callback here.
}

/**
 * NOTE: You can also bind callbacks with return values: simply use JSValue
 * in your function's return value. The rest of the code remains the same.
 */
JSValue MyClass::MyCallback(const JSObject& thisObject, const JSArgs& args)
{
  // Handle callback here.
  
  return JSValue();
}

Then simply bind it to a named property using the BindJSCallback macro:

// Get the global object
JSObject global = JSGlobalObject();

// Bind it to the "MyCallback" property, will be exposed to JS as a Function
global["MyCallback"] = BindJSCallback(&MyClass::MyCallback);

Now you can call it from JavaScript on the page:

MyCallback(1, 2, 3, "hello");

Using the AppCore Code

The AppCore code provides a cross-platform base for you to start writing applications with Ultralight-- the only thing you need to provide are HTML assets and application logic.

Take a look at the Browser sample code and Tutorials for an example of use.