2026 02 12_unified_architecture_plan_ARCHIVED - mark-ik/graphshell GitHub Wiki
β οΈ ARCHIVED: Alternative Approach Not PursuedStatus: This document represents an alternative architectural approach that was NOT PURSUED. It is archived for historical reference.
Why archived: This proposal was created before verifying the user's actual requirements. The user explicitly requested an egui_tiles-based tiling approach with separate viewports for graph and webviews, not a continuous zoom-based model.
Superseded by:
- 2026-02-12_architecture_reconciliation.md β Documents why this approach was not pursued
- 2026-02-12_servoshell_inheritance_analysis.md β Describes the egui_tiles approach that IS being pursued
Key differences from pursued approach:
- This doc: Continuous zoom canvas where webviews render as textures ON nodes
- Actual approach: egui_tiles tiling where webviews are separate panes alongside graph
Historical value: This document contains useful analysis of servoshell's constraints and Servo rendering capabilities, even though the proposed solution differs from what was requested.
Graphshell's UI was built by forking servoshell's Gui and grafting a
View::Graph / View::Detail enum on top. This approach inherited
servoshell's assumptions β a single window with a tab bar, where exactly
one webview renders full-screen below the toolbar β and then fought them
at every turn.
| Symptom | Root Cause |
|---|---|
| Webviews destroyed/recreated on every view toggle | Servoshell assumes the active tab's webview owns the entire viewport below the toolbar. We can't composite webview content into graph nodes within that model. |
| Two parallel identity systems (WebViewCollection tabs vs Graph nodes) bridged by a fragile bidirectional HashMap | Servoshell's tab model and our graph model are separate data structures that must be kept in sync. |
active_webview_nodes save/restore hack |
Direct consequence of destroying webviews on graph-view entry. |
| Phantom node creation on navigation |
sync_to_graph infers graph mutations from webview URL changes reactively instead of the graph being the authoritative source. |
| Tab bar hidden in graph view, shown in detail view | We toggle servoshell's UI elements rather than replacing them. |
| Force-consuming all input events in graph view | Prevents invisible webviews from receiving events, but is a band-aid. |
| Address bar has split codepath (graph vs detail) | Because the toolbar was designed for one purpose and we repurposed it for two. |
Servoshell is a reference browser shell β it demonstrates Servo's embedding API with a minimal traditional browser UI. Graphshell is a spatial browser where the primary interface IS the graph. By inheriting servoshell's frame loop and extending it with conditional branches, we've built a spatial browser that's embarrassed to show its graph β it has to tear down all the browser parts first.
- The graph IS the UI. There is no separate "graph view" vs "detail view." There is one continuous spatial canvas, and you can zoom into a node to see its web content.
-
A node IS a tab. No bidirectional mapping. The graph node is the
single source of truth. A
WebViewIdis a property of a node, not a parallel identity. - Webview lifecycle follows the viewport. Nodes near the camera get webviews created (warm up); nodes far away get webviews reclaimed (cool down). No bulk destroy/recreate.
- The graph owns navigation. When a link is clicked, the graph decides what happens β create a new node, follow an edge to an existing node, etc. Navigation is a graph operation, not a webview operation that we try to reverse-engineer.
-
Servo's embedding API is our foundation, not servoshell's Gui.
We use
WebView,WebViewBuilder,RenderingContextdirectly, not through layers of servoshell UI code.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β egui frame β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Spatial Canvas β β
β β β β
β β [node]ββββββ[node] [node] β β
β β β β β β
β β [node] [FOCUSED NODE] β β
β β ββββββββββββββββ β β
β β β webview β β β
β β β rendered β β β
β β β as texture β β β
β β ββββββββββββββββ β β
β β β β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Command Bar (universal, context-sensitive) β β
β βββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Instead of two exclusive rendering paths, there is one spatial canvas. The camera has a position and zoom level. At far zoom, you see the full graph with nodes as icons/thumbnails. As you zoom into a node, the node's web content becomes visible (rendered as a texture by Servo's offscreen rendering). At full zoom, the web content fills the viewport and you're effectively "inside" the node.
The viewport determines which nodes are "warm" (close enough to need a webview) vs "cold" (too far away, metadata only):
Zoom level / distance from camera center
βββββββββββββββββββββββββββββββββββββββββ
Active β node fills viewport, receives input
Warm β webview running, rendered as texture on node
Cold β thumbnail/favicon only, no webview process
This replaces the binary toggle between Graph and Detail views. Webviews are never bulk-destroyed β they're created and reclaimed based on proximity to the camera, with hysteresis to prevent thrashing.
pub struct Node {
// Identity
pub url: String,
pub title: String,
// Spatial
pub position: Point2D<f32>,
pub velocity: Vector2D<f32>,
// Webview (owned, not mapped)
pub webview: Option<WebView>, // None when cold
pub thumbnail: Option<Texture>, // Cached render for cold display
// Visual + interaction
pub is_selected: bool,
pub is_pinned: bool,
// Lifecycle managed by viewport proximity
pub thermal_state: ThermalState, // Active / Warm / Cold
}
pub enum ThermalState {
/// Web content fills viewport, node receives keyboard/mouse input
Active,
/// Webview running, content rendered as texture on graph node
Warm,
/// No webview process, display thumbnail/favicon/placeholder
Cold,
}Key change: Node owns its Option<WebView> directly. No
bidirectional HashMap. No WebViewCollection tab ordering. The graph is
the tab manager.
When a user clicks a link in a warm/active node's web content:
- Servo fires a navigation event on that node's
WebView. - Graphshell intercepts it via the embedder delegate.
- The graph decides: create a new child node with the target URL and an edge from the current node, OR navigate the existing node (user preference / link type).
- A new node is created in the graph at a position offset from the parent.
- Camera optionally animates to the new node.
This is the inverse of the current approach where we detect URL changes
after the fact in sync_to_graph.
The address bar, back/forward buttons, and view-toggle button are all artifacts of servoshell's traditional browser UI. Replace them with a single command bar (Ctrl+L or always-visible strip):
- Type a URL β create a new node or navigate the focused node
- Type a search query β search within the graph or web search
- Graph commands β pin, delete, connect, etc.
Back/forward become graph-level operations (move focus to parent/child node along history edges).
This is a significant refactor. Suggested phasing:
Goal: Prove the core concept works β a Servo WebView rendered as an egui texture on a graph node.
- Use Servo's offscreen rendering to capture webview content to a texture.
- Display that texture as the node's face in egui_graphs custom node
rendering (we already have
GraphNodeShapeinfrastructure from favicon work). - Keep the rest of the architecture as-is temporarily β just prove the rendering pipeline.
This is the critical path. If we can render a webview as a texture on a node, everything else follows.
- Replace
Viewenum with continuous camera model. - Implement thermal state transitions based on viewport proximity.
- Node creates/destroys its own webview based on thermal state.
- Remove
active_webview_nodes,webview_to_node/node_to_webviewmaps.
- Implement embedder delegate that intercepts navigation requests.
- Route link clicks through graph operations (new node + edge).
- Remove
sync_to_graphpolling mechanism. - Remove
webview_previous_urltracking.
- Replace toolbar with command bar.
- Unify input handling (no more graph-view event suppression).
- Implement graph-native back/forward (edge traversal).
Servo supports OffscreenRenderingContext. The question is whether we
can get the rendered output as an OpenGL texture that egui can composite.
Servoshell already uses render_to_parent_callback with a glow
PaintCallback. We need the equivalent but targeting a texture instead
of the parent window's framebuffer.
Options:
- Framebuffer Object (FBO): Render each webview to its own FBO, then use the color attachment texture in egui. This is the standard approach.
- ReadPixels path: Render β readback β upload as egui texture. Works but slow (GPUβCPUβGPU round-trip). Acceptable as first pass.
- Shared texture: If Servo and egui share the same GL context, we may be able to use the texture directly. Needs investigation.
Each webview has a script thread and compositor resources. We need to measure the practical limit. The thermal state system should keep the warm count small (3-5 nodes), with only 1 active at a time.
For the zoomed-in webview rendering, nodes need to be larger. We already
have custom DisplayNode via GraphNodeShape. The size can be
viewport-dependent β small circles when zoomed out, expanding to show
web content as you zoom in.
- petgraph StableGraph β data structure for the graph
- egui_graphs β rendering and interaction for the spatial canvas
- Physics engine β force-directed layout (already on worker thread)
- Persistence β fjall log + redb snapshots
- Graph, Node, Edge types β core data model (extended, not replaced)
-
Custom node rendering (
GraphNodeShape) β already supports favicon; extends naturally to thumbnails and webview textures
-
Viewenum (Graph vs Detail) β continuous zoom model -
webview_to_node/node_to_webviewHashMaps βNodeowns itsOption<WebView> -
active_webview_nodesβ thermal state manages lifecycle -
webview_controller.rs(manage_lifecycle,sync_to_graph) β viewport-based lifecycle manager -
webview_previous_urltracking β navigation interception - Servoshell tab bar β command bar or omit
-
on_window_eventinput suppression β unified spatial input -
browser_tab()widget β nodes are tabs - Toolbar back/forward/reload β graph-native navigation
| Risk | Mitigation |
|---|---|
| Servo offscreen rendering may not produce textures egui can use | Phase A is a focused spike to prove this works before committing to full refactor |
| Performance with multiple warm webviews | Thermal state limits concurrent webviews; measure early |
| Loss of traditional browser UX for users who expect tabs | The command bar and zoom interaction need to feel natural; consider an optional sidebar node list |
| Large refactor surface | Phased approach β each phase is functional on its own |
| Upstream servoshell changes become harder to merge | We're already divergent; this makes the divergence intentional and clean rather than accidental |
- This plan document
- Phase A spike proving webviewβtextureβnode rendering pipeline
- Incremental PRs for Phases A through D
- Updated CODEBASE_MAP and DEVELOPER_GUIDE after major milestones