Skip to content

Instantly share code, notes, and snippets.

@jaytaph
Last active August 1, 2025 19:12
Show Gist options
  • Save jaytaph/4a38539d288eb81b9719ec2ad029710b to your computer and use it in GitHub Desktop.
Save jaytaph/4a38539d288eb81b9719ec2ad029710b to your computer and use it in GitHub Desktop.
Gosub API setup

Gosub API Setup

This document describes the structure of the Gosub API.

GosubEngine

First, we have a GosubEngine. We instantiate this only once in our application/useragent and is capable of dealing with multiple pages that can be rendered and used. We can send a configuration to the GosubEngine to set up the engine and to define certain behaviors.

Zones

Next, a GosubEngine must have at least one Zone. A Zone is a container for multiple Tabs and all share certain structures, like a cookie jar, session storage, and local storage. A Zone can be thought of as a browser window where you can have multiple tabs open. A HTTP session (for instance, through cookies) in one tab, is also visible in another tab in that same zone.

The engine can contain multiple zones, and each zone is independent of each other. However, they can share underwater the same render processes and threads, but from the user's perspective, they are independent.

This means that you can have a single window, with multiple tabs, where some tabs are in zone 1, and others are in zone 2.

This basically mimics the behavior of container-like browsers.

Zones can also be created as persistent data structures, with titles, descriptions, colors and other metadata. They could also have its own set of history, bookmarks and such.

Tabs

Inside a Zone, we can have multiple Tabs. A Tab is a single page that can be rendered and interacted with. However, it doesn't mean that the user agent can only show a single tab at a time. It's possible for a user agent to have a page where two tabs are shown side by side, or even multiple tabs in a grid-like fashion. Such a tab-manager is not part of the Gosub API, but can be implemented on top of it.

Engine instances

Each tab holds an EngineInstance. This is the actual instance that is responsible for rendering the page, handling events, and managing the state of the tab. The EngineInstance is responsible for loading the page, rendering it. It is basically a "mini" gosub engine, but can share the same resources as the main GosubEngine, like the GPU renderer, parser etc.

Tab communication

There are two ways of communicating with a tab. First, we can send events to the tab, which will then be delegated to the engine instance of that tab. This could be a "mouse move" event, a "key press" event, or any other event that the user agent wants to send to the tab. The Tab will then handle the event and pass it on to the EngineInstance.

The second way of communicating with a tab is through the tick method. This method will return a TickResult, which contains the current state of the tab. Depending on the result, the user agent can see if the tab is still loading HTML content, if it's parsing, or if it is in the process of rendering the page onto a framebuffer.

If for instance the TickResult contains the "needs_redraw" flag being set to true, the user agent can then can fetch the surface / framebuffer of the tab, and rennder it to screen how it sees fit.

Tick system

The tick system allows the engine to do "work" during a call from the user-agent. Most often, you want to call the tick method of the engine once every 16 milliseconds, which is roughly 60 frames per second. This allows the engine to do be fluent and responsive, while also allowing the user agent to render the page to the screen.

It is user-agent / ui toolkit dependend how this is done. For instance, with a 16ms timer interval, or through a vsync system.

Note that the tick() method of the GosubEngine will return a BTreeMap<TabId, TickResult>, which contains the results of ALL tabs in the engine. Even though this seems a lot of work, it doesn't mean that the engine has to do a lot of work. Any tabs that are not active, will be run (or tick()ed) with a different interval. Background tabs that have css animations running for instance, can run at a 100ms or even 1 second interval, while active tabs (most often, only one) can run at a 16ms (full speed).

Tabs that do not require any work will not be ticked at all. Only when it receives an event, it will wake up and tick. This also means that an inactive tab can still receive events like a scroll-event, or a mouse move event (even though it's unknown how that would be accomplished from a user-agent perspective), but it will handle it.

Rendering a tab

If something has changed in the tab (an event triggers a change in scroll, or we hover over an element), then we need to see where in the render pipeline we must trigger. We often do not need to trigger the whole render pipeline, but could only activate part of it (for instance, scrolling a page does not require a full re-render, but only a composition of the rendered tiles).

Based on the tab state, we either do not render anything, or when active, we could render it, which in turn will set the "needs_redraw" flag in the next tickresult to true.

There is no point in trigger a render when a tab is not active, however, there might be good reasons to do the rendering nevertheless. Since we already are in a slower tick() rate (100ms, or even 1s), we can render on each of these ticks. I don't know if this is pointless though, because as soon as the tab becomes active, we trigger a render anyway before we can show something. Maybe we should not render anything unless a tab is active?

Class Diagram

classDiagram
    class GosubEngine {
        - config: GosubEngineConfig
        - runtime: Arc&lt;Runtime>
        - zones: HashMap&lt;ZoneId, Zone>
        + tick(): BTreeMap&lt;TabId, TickResult>
        + create_zone(): ZoneId
        + open_tab(zone: ZoneId): TabId
    }

    class Zone {
        - id: ZoneId
        - cookie_jar: CookieJar
        - session_storage: Storage
        - local_storage: Storage
        - tabs: HashMap&lt;TabId, Tab>
    }

    class Tab {
        - id: TabId
        - instance: EngineInstance
        - title: String
        - favicon: Vec&lt;u8>
        - mode: TabMode
        - last_tick: Instant
        - state: TabState
        + handle_event(event: EngineEvent)
        + tick(): TickResult
    }

    class EngineInstance {
        - dirty: DirtyFlags
        - current_url: Option&lt;String>
        - raw_html: String
        - loading_task: Option&lt;JoinHandle&lt;Result&lt;Response,Error>>>
        - surface: Option&lt;ImageSurface>
        - failed: bool
        + start_loading(url: String)
        + tick(): TickResult
        + render()
        + get_surface(): Option&lt;&ImageSurface>
    }

    GosubEngine "1" o-- "many" Zone : contains
    Zone "1" o-- "many" Tab      : contains
    Tab      "1" *-- "1" EngineInstance : owns
Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment