Remove stale pnpm-workspace.yaml and ARCHITECTURE-NEXT.md
This commit is contained in:
parent
71fc74f93f
commit
2a69990b34
|
|
@ -1,647 +0,0 @@
|
|||
# OpenPencil Next: Browser-Native Design Tool
|
||||
|
||||
## Vision
|
||||
|
||||
A design tool where every element is real HTML/CSS rendered by a real browser engine.
|
||||
Not a Figma clone. Not a browser wrapper. A new kind of application built from
|
||||
WebKit's rendering primitives.
|
||||
|
||||
## Core Concept: Islands
|
||||
|
||||
An **island** is an independent rendering context — its own DOM, styles, layout,
|
||||
and paint output. Components within an island share CSS context, compose via
|
||||
Shadow DOM, and have real parent-child relationships. Components on the infinite
|
||||
canvas that aren't composed together live in separate islands.
|
||||
|
||||
```
|
||||
Infinite canvas
|
||||
│
|
||||
├── Island A ─ one Document, one render surface
|
||||
│ └── <nav-bar>
|
||||
│ ├── <logo>
|
||||
│ └── <auth-button>
|
||||
│
|
||||
├── Island B ─ separate Document, separate surface
|
||||
│ └── <hero-section>
|
||||
│ ├── <heading>
|
||||
│ └── <cta-button>
|
||||
│
|
||||
├── Island C ─ standalone component
|
||||
│ └── <auth-button> (same component class, isolated context)
|
||||
│
|
||||
├── Island D ─ full page composition
|
||||
│ └── <app-shell>
|
||||
│ ├── <nav-bar> (shares CSS with siblings here)
|
||||
│ ├── <hero-section>
|
||||
│ └── <footer>
|
||||
```
|
||||
|
||||
Drag a component OUT of an island → new island created (new rendering context).
|
||||
Drag a component INTO an island → island destroyed, node joins target DOM.
|
||||
The component definition (Web Component class + styles) is the same — only
|
||||
the rendering context changes.
|
||||
|
||||
## WebKit Building Blocks
|
||||
|
||||
### What exists in Source/
|
||||
|
||||
| Component | Location | Role |
|
||||
|-----------|----------|------|
|
||||
| `WebCore::Page` | `Source/WebCore/page/Page.h` | A rendering context: DOM + CSS + layout + paint. **One per island.** |
|
||||
| `WebCore::Document` | `Source/WebCore/dom/Document.h` | The DOM tree within a Page. |
|
||||
| `WebCore::ShadowRoot` | `Source/WebCore/dom/ShadowRoot.h` | Shadow DOM for component style isolation. |
|
||||
| `CustomElementRegistry` | `Source/WebCore/dom/CustomElementRegistry.h` | Web Component registration. |
|
||||
| `TextureMapper` | `Source/WebCore/platform/graphics/texmap/TextureMapper.h` | GPU compositor. Draws textures with transforms. |
|
||||
| `TextureMapperLayer` | `Source/WebCore/platform/graphics/texmap/TextureMapperLayer.h` | A compositing layer with position, size, transform, opacity, filters, children. |
|
||||
| `CoordinatedPlatformLayer` | `Source/WebCore/platform/graphics/texmap/coordinated/` | Coordinated layer management between main thread and compositor thread. |
|
||||
| `BitmapTexture` | `Source/WebCore/platform/graphics/texmap/BitmapTexture.h` | A GPU texture that TextureMapper can draw. |
|
||||
| `LayerTreeHost` | `Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/LayerTreeHost.h` | Orchestrates the compositor. Creates GraphicsLayers, manages the scene. |
|
||||
| `ThreadedCompositor` | `Source/WebKit/WebProcess/WebPage/CoordinatedGraphics/ThreadedCompositor.h` | Runs compositing on a dedicated thread with its own GL context. |
|
||||
| `PageOverlay` | `Source/WebCore/page/PageOverlay.h` | Drawable layer on top of a Page with mouse event interception. Two modes: View (fixed) and Document (scrolls). |
|
||||
| `PageOverlayController` | `Source/WebCore/page/PageOverlayController.h` | Manages overlays. Handles repaint, mouse events, scale changes. |
|
||||
| `InspectorOverlay` | `Source/WebCore/inspector/InspectorOverlay.h` | Draws selection rects, margin/padding guides, grid overlays — exactly like our meta-layer needs. |
|
||||
| `WPE Platform` | `Source/WebKit/WPEPlatform/` | Headless rendering abstraction. Display, View, Buffer, Toplevel. No window system needed. |
|
||||
| `WPEViewHeadless` | `Source/WebKit/WPEPlatform/wpe/headless/` | Renders to GPU buffers at 60fps without a display server. |
|
||||
| `AcceleratedBackingStore` | `Source/WebKit/UIProcess/wpe/AcceleratedBackingStore.h` | Gets DMA-BUF / SHM buffers from WebKit's renderer. Manages buffer lifecycle. |
|
||||
| `Skia` | `Source/ThirdParty/skia/` | WPE WebKit already uses Skia for painting (2024+). Same Skia we know. |
|
||||
| `JavaScriptCore` | `Source/JavaScriptCore/` | JS engine. Each Page gets its own JS global object. |
|
||||
|
||||
### How they connect
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ Our Application │
|
||||
│ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ Island Manager │ │
|
||||
│ └───┬────────────┘ │
|
||||
│ │ │
|
||||
┌────────────────────────────┼───────────────────────────────┐
|
||||
│ │ │ │ │
|
||||
Island A Island B │ Island C Meta-layer
|
||||
│ │ │ │ │
|
||||
┌─────────┴──────┐ ┌────────┴───┐ │ ┌─────────┴──────┐ ┌────┴──────┐
|
||||
│ WebCore::Page │ │ WebCore:: │ │ │ WebCore::Page │ │ PageOverlay│
|
||||
│ + Document │ │ Page │ │ │ + Document │ │ on master │
|
||||
│ + ShadowRoots │ │ + Document │ │ │ + ShadowRoots │ │ Page │
|
||||
│ + Layout │ │ + Layout │ │ │ + Layout │ └────┬──────┘
|
||||
│ + Paint→Skia │ │ + Paint │ │ │ + Paint→Skia │ │
|
||||
└───────┬────────┘ └──────┬─────┘ │ └───────┬────────┘ │
|
||||
│ │ │ │ │
|
||||
▼ ▼ │ ▼ ▼
|
||||
┌───────────────────────────────────────────────────────────────────────────┐
|
||||
│ TextureMapper (GPU Compositor) │
|
||||
│ │
|
||||
│ TextureMapperLayer A TextureMapperLayer B TextureMapperLayer C │
|
||||
│ ┌──────────┐ ┌──────────────┐ ┌──────────┐ │
|
||||
│ │ pos: 100,200 │ pos: 600,500 │ │ pos: 100,700 │
|
||||
│ │ size: 400×60│ │ size: 800×400│ │ size: 120×40│ │
|
||||
│ │ texture: A │ │ texture: B │ │ texture: C │ │
|
||||
│ └──────────┘ └──────────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ Master transform: scale(zoom) × translate(panX, panY) │
|
||||
│ │
|
||||
│ Overlay layer (meta): selection rects, handles, guides, rulers │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────────┬─────┘
|
||||
│
|
||||
Screen/Window
|
||||
```
|
||||
|
||||
## Architecture Layers
|
||||
|
||||
### Layer 1: Island Runtime
|
||||
|
||||
Each island = `WebCore::Page` + the minimum `PageConfiguration` clients.
|
||||
|
||||
We implement lightweight stubs for the required clients:
|
||||
|
||||
| Client | Our Implementation |
|
||||
|--------|--------------------|
|
||||
| `ChromeClient` | Minimal: reports viewport size, device scale. No browser chrome. |
|
||||
| `EditorClient` | Full: enables contenteditable for text editing in design nodes. |
|
||||
| `FrameLoaderClient` | Stub: we don't load URLs. We inject HTML directly into the Document. |
|
||||
| `BackForwardClient` | Stub: no navigation history. |
|
||||
| `ProgressTrackerClient` | Stub: no loading progress. |
|
||||
| `SocketProvider` | Stub or real: depends on whether we want fetch() inside islands. |
|
||||
| `CookieJar` | Stub: no cookies. |
|
||||
| `DragClient` | Intercepted: we handle drag at the meta-layer level. |
|
||||
|
||||
Island lifecycle:
|
||||
```
|
||||
createIsland(html: string, css: string, size: {w, h}) → Island
|
||||
1. Build PageConfiguration with our stub clients
|
||||
2. Create WebCore::Page
|
||||
3. Get the Page's Document
|
||||
4. Parse and inject the HTML into Document
|
||||
5. Inject scoped CSS (design tokens + island styles)
|
||||
6. Force layout
|
||||
7. Render to BitmapTexture via Skia painting
|
||||
8. Register the texture as a TextureMapperLayer
|
||||
9. Position the layer in world space
|
||||
|
||||
destroyIsland(island: Island) → void
|
||||
1. Remove TextureMapperLayer from compositor
|
||||
2. Destroy WebCore::Page (releases Document, layout tree, paint state)
|
||||
```
|
||||
|
||||
### Layer 2: Component System
|
||||
|
||||
Components are Web Components registered in every island's `CustomElementRegistry`.
|
||||
|
||||
```cpp
|
||||
// Shared component definitions
|
||||
struct ComponentDef {
|
||||
String tagName; // "my-button", "nav-bar"
|
||||
String html; // shadow root innerHTML
|
||||
String css; // shadow root styles
|
||||
String js; // optional: class definition with lifecycle callbacks
|
||||
Vector<String> slots; // named slots this component accepts
|
||||
Vector<Property> props; // observed attributes → CSS custom properties
|
||||
};
|
||||
|
||||
// When creating an island, register all known components:
|
||||
for (auto& def : componentLibrary) {
|
||||
island.page->document().customElementRegistry()
|
||||
.define(def.tagName, def.classConstructor);
|
||||
}
|
||||
```
|
||||
|
||||
Shadow DOM within an island provides:
|
||||
- **Style isolation**: each component's CSS doesn't leak
|
||||
- **CSS custom properties pierce through**: design tokens (`--brand-primary`, `--spacing-md`) inherit naturally
|
||||
- **Slots = children**: `<slot>` elements enable real DOM composition
|
||||
- **Events bubble**: click inside a button inside a card inside a frame → bubbles through all shadow boundaries
|
||||
|
||||
### Layer 3: Compositor (The Infinite Canvas)
|
||||
|
||||
Built on `TextureMapper` + `TextureMapperLayer`.
|
||||
|
||||
The compositor manages:
|
||||
- A root `TextureMapperLayer` with the **world transform** (pan + zoom matrix)
|
||||
- Child `TextureMapperLayer`s, one per island, positioned in world space
|
||||
- An overlay layer for the meta-layer (selection, handles, guides)
|
||||
|
||||
```cpp
|
||||
class DesignCanvasCompositor {
|
||||
TextureMapper m_textureMapper;
|
||||
TextureMapperLayer m_rootLayer; // world transform lives here
|
||||
TextureMapperLayer m_overlayLayer; // meta-layer on top
|
||||
|
||||
// World transform
|
||||
TransformationMatrix m_worldTransform; // scale(zoom) * translate(pan)
|
||||
|
||||
void addIsland(Island& island, FloatPoint worldPos, FloatSize size) {
|
||||
auto* layer = new TextureMapperLayer();
|
||||
layer->setPosition(worldPos);
|
||||
layer->setSize(size);
|
||||
layer->setContentsLayer(island.platformLayer());
|
||||
m_rootLayer.addChild(layer);
|
||||
}
|
||||
|
||||
void setZoom(float zoom, FloatPoint origin) {
|
||||
// Zoom around origin point
|
||||
m_worldTransform = TransformationMatrix()
|
||||
.translate(origin.x(), origin.y())
|
||||
.scale(zoom)
|
||||
.translate(-origin.x(), -origin.y())
|
||||
.translate(m_panOffset.x(), m_panOffset.y());
|
||||
m_rootLayer.setTransform(m_worldTransform);
|
||||
}
|
||||
|
||||
void paint() {
|
||||
m_textureMapper.beginPainting();
|
||||
m_rootLayer.prepareForPainting(m_textureMapper);
|
||||
m_rootLayer.paint(m_textureMapper);
|
||||
m_overlayLayer.paint(m_textureMapper); // always on top
|
||||
m_textureMapper.endPainting();
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Resolution management**: when zoom stabilizes, re-render island textures at
|
||||
the effective resolution (zoom × base size) for crisp text. During zoom animation,
|
||||
use the existing texture scaled — text looks slightly blurry for ~100ms, same as
|
||||
every map application.
|
||||
|
||||
### Layer 4: Meta-Layer
|
||||
|
||||
The meta-layer handles all design-tool interactions. It intercepts pointer events
|
||||
before they reach any island and draws selection UI on top of everything.
|
||||
|
||||
Built on `PageOverlay` (OverlayType::View — fixed to viewport, doesn't scroll).
|
||||
|
||||
```cpp
|
||||
class DesignMetaLayer : public PageOverlayClient {
|
||||
// Selection state
|
||||
Vector<IslandNodeRef> m_selectedNodes;
|
||||
std::optional<DragState> m_dragState;
|
||||
std::optional<ResizeState> m_resizeState;
|
||||
|
||||
void drawRect(PageOverlay&, GraphicsContext& gc, const IntRect&) override {
|
||||
// Draw selection rectangles
|
||||
for (auto& node : m_selectedNodes) {
|
||||
auto worldBounds = nodeWorldBounds(node);
|
||||
gc.setStrokeColor(Color::blue);
|
||||
gc.strokeRect(worldBounds, 1.0f / m_zoom); // constant screen-space width
|
||||
drawResizeHandles(gc, worldBounds);
|
||||
}
|
||||
drawGuides(gc);
|
||||
drawRulers(gc);
|
||||
drawHoverHighlight(gc);
|
||||
}
|
||||
|
||||
bool mouseEvent(PageOverlay&, const PlatformMouseEvent& event) override {
|
||||
auto worldPos = screenToWorld(event.position());
|
||||
|
||||
// Hit-test: which island, which node?
|
||||
auto hit = hitTest(worldPos);
|
||||
|
||||
if (event.type() == PlatformEvent::Type::MousePressed) {
|
||||
if (hit.resizeHandle) startResize(hit);
|
||||
else if (hit.node) startSelection(hit);
|
||||
else if (hit.empty) startMarquee(worldPos);
|
||||
return true; // consume event
|
||||
}
|
||||
// ... drag, release, hover
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Hit-testing**: transform world-space click → island local coords → call
|
||||
`Document::elementFromPoint()` on that island's Document. WebKit does the
|
||||
heavy lifting — it knows exactly which DOM element is at any coordinate,
|
||||
accounting for z-index, overflow, transforms, etc.
|
||||
|
||||
### Layer 5: Property Panel & Tools
|
||||
|
||||
The property panel reads and writes real CSS on real DOM elements.
|
||||
|
||||
```cpp
|
||||
// Reading properties (user selects a node)
|
||||
auto* element = selectedNode.element; // live DOM Element in island's Document
|
||||
auto computed = element->document().domWindow()->getComputedStyle(*element);
|
||||
|
||||
propertyPanel.display = computed->display(); // "flex"
|
||||
propertyPanel.gap = computed->gap(); // "16px"
|
||||
propertyPanel.color = computed->color(); // "rgb(51, 51, 51)"
|
||||
propertyPanel.fontSize = computed->fontSize(); // "14px"
|
||||
propertyPanel.borderRadius = computed->borderRadius(); // "12px"
|
||||
|
||||
// Writing properties (user changes a value)
|
||||
element->style()->setProperty("gap", "24px");
|
||||
// Layout automatically recalculates
|
||||
// Island texture automatically re-renders
|
||||
// Compositor shows updated result at next frame
|
||||
```
|
||||
|
||||
No translation layer. No property mapping. The design tool properties ARE CSS properties.
|
||||
|
||||
### Layer 6: Scene Graph (Persistence)
|
||||
|
||||
The scene graph is the serializable representation of the design.
|
||||
|
||||
```
|
||||
DesignFile
|
||||
├── components: ComponentDef[] // Web Component definitions
|
||||
├── designTokens: Record<string, string> // CSS custom properties
|
||||
├── islands: Island[]
|
||||
│ ├── id: string
|
||||
│ ├── position: {x, y} // world space
|
||||
│ ├── size: {width, height} // viewport size
|
||||
│ └── tree: NodeTree // serialized DOM
|
||||
│ ├── tagName: string
|
||||
│ ├── attributes: Record<string, string>
|
||||
│ ├── inlineStyles: Record<string, string>
|
||||
│ ├── shadowCSS?: string
|
||||
│ └── children: NodeTree[]
|
||||
└── canvasState
|
||||
├── zoom: number
|
||||
└── pan: {x, y}
|
||||
```
|
||||
|
||||
**Export to code** = serialize island DOM. The design IS the code.
|
||||
No translation, no "generate CSS" — it was CSS all along.
|
||||
|
||||
## Key Operations
|
||||
|
||||
### Drag Component Between Islands
|
||||
|
||||
```
|
||||
User drags <auth-button> from Island A to free canvas at position (500, 800):
|
||||
|
||||
1. Meta-layer detects drag from Island A
|
||||
2. Get the element's outerHTML + computed styles from Island A's Document
|
||||
3. Remove element from Island A's DOM
|
||||
4. Island A re-renders (layout recalculates without the removed node)
|
||||
5. Create new Island C at (500, 800) with the element's HTML
|
||||
6. Island C renders the standalone component
|
||||
7. Register Island C's texture in compositor
|
||||
```
|
||||
|
||||
### Drag Component Into Island
|
||||
|
||||
```
|
||||
User drags Island C's <auth-button> into Island D's <nav-bar>:
|
||||
|
||||
1. Meta-layer detects drop target = Island D, parent = <nav-bar> element
|
||||
2. Get the element's outerHTML from Island C
|
||||
3. Destroy Island C (remove Page, remove compositor layer)
|
||||
4. Parse HTML and insert into Island D's <nav-bar> DOM
|
||||
5. Component is now a child of <nav-bar> — shares CSS context
|
||||
6. Island D re-renders
|
||||
```
|
||||
|
||||
### Responsive Preview
|
||||
|
||||
```
|
||||
User resizes an island's viewport:
|
||||
|
||||
1. Update the Page's viewport size
|
||||
2. CSS media queries fire automatically (the browser does this!)
|
||||
3. Container queries fire automatically
|
||||
4. Layout recalculates
|
||||
5. Island texture re-renders at new size
|
||||
6. Compositor shows the result
|
||||
|
||||
No simulation. Real responsive behavior from the real CSS engine.
|
||||
```
|
||||
|
||||
### Text Editing
|
||||
|
||||
```
|
||||
User double-clicks a text node:
|
||||
|
||||
1. Meta-layer passes focus through to the island's Page
|
||||
2. The browser's native contenteditable activates
|
||||
3. Cursor, selection, IME — all handled by WebKit
|
||||
4. Text reflows in real-time with real CSS
|
||||
5. On blur, meta-layer recaptures input
|
||||
```
|
||||
|
||||
## Platform Strategy
|
||||
|
||||
### Phase 1: WPE WebKit (Linux first)
|
||||
|
||||
WPE is designed for exactly this: headless WebKit rendering to GPU buffers.
|
||||
|
||||
- `WPEDisplayHeadless` — no window system dependency
|
||||
- `WPEViewHeadless` — renders to DMA-BUF / SHM buffers
|
||||
- `AcceleratedBackingStore` — manages the buffer pipeline
|
||||
- Skia painting engine — already integrated in WPE WebKit
|
||||
- TextureMapper compositor — OpenGL ES, mature, handles transforms/opacity/filters
|
||||
|
||||
Build: custom WPE WebKit, compile as a library, link into our C++/Rust app.
|
||||
|
||||
The application window itself: either a single real WPE toplevel that we
|
||||
own (for the panels + canvas viewport), or a native window (GTK, SDL, GLFW)
|
||||
where we composite WebKit's output.
|
||||
|
||||
### Phase 2: macOS
|
||||
|
||||
Options:
|
||||
- **WKWebView per island** — macOS has WKWebView with native CoreAnimation compositing.
|
||||
Each WKWebView renders to a CALayer. We composite CALayers ourselves in a Metal view.
|
||||
Higher-level API, less control, but works with Apple's WebKit.
|
||||
- **Build WPE for macOS** — WPE's platform layer is abstractable. The headless backend
|
||||
works anywhere with EGL/OpenGL. Harder to build but gives us the same architecture.
|
||||
- **WebKit from source for macOS** — compile WebCore + TextureMapper directly, bypass
|
||||
the Cocoa API layer. Maximum control, maximum build complexity.
|
||||
|
||||
Recommended: start with WKWebView per island for quick macOS support, migrate
|
||||
to WPE-on-macOS for unified architecture later.
|
||||
|
||||
### Phase 3: Windows
|
||||
|
||||
- WebView2 (Chromium) per island — similar to macOS WKWebView approach
|
||||
- Or build WPE with ANGLE for OpenGL-on-DirectX
|
||||
- Or use CEF (Chromium Embedded Framework) off-screen rendering
|
||||
|
||||
### Phase 4: Cross-Platform Unified
|
||||
|
||||
Long-term: ship WPE WebKit on all platforms. One codebase, one compositor,
|
||||
one architecture. WPE's platform layer is designed to be ported.
|
||||
|
||||
## What Changes vs. Current OpenPencil
|
||||
|
||||
| Current | Next |
|
||||
|---------|------|
|
||||
| Skia CanvasKit (WASM) for all rendering | WebKit for content rendering, TextureMapper/Skia for compositing |
|
||||
| Yoga WASM for layout (flexbox subset) | Real CSS layout engine (flexbox, grid, container queries, everything) |
|
||||
| Custom text shaping | WebKit's text engine (ICU, HarfBuzz, CoreText) |
|
||||
| SceneNode flat map | DOM trees in isolated Pages |
|
||||
| Kiwi binary .fig format | HTML/CSS-based format (with optional .fig import) |
|
||||
| Custom component/override system | Web Components + Shadow DOM |
|
||||
| "Export to code" = generate | "Export to code" = serialize what's already there |
|
||||
| Vue 3 + TypeScript app | C++/Rust app with WebKit library |
|
||||
| Tauri desktop wrapper | Native application, IS the browser |
|
||||
|
||||
## Implementation Language
|
||||
|
||||
The core must be **C++** (WebKit is C++). Options for the application layer:
|
||||
|
||||
- **Pure C++** — maximum integration, no FFI overhead
|
||||
- **Rust + C++ FFI** — Rust for the app layer (scene graph, tools, file I/O),
|
||||
C++ for WebKit integration. rust-bindgen for the bridge.
|
||||
- **C++ core + TypeScript UI** — the property panel / layers panel / toolbar
|
||||
could be one island that's the "app shell", rendering our UI in HTML/CSS.
|
||||
Meta-layer stays in C++.
|
||||
|
||||
The third option is interesting: **the design tool's UI is itself an island**.
|
||||
Panels are Web Components. The infinite canvas + meta-layer is the native layer.
|
||||
We eat our own dog food.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Island granularity**: one Page per top-level frame? Per component? Per page?
|
||||
Memory/performance testing needed. Each Page has overhead (JS context, style
|
||||
system, layout state). Hundreds might be fine; thousands might not.
|
||||
|
||||
2. **Shared component registry**: when a component class changes, every island
|
||||
that uses it needs to re-register and re-render. Mechanism: custom element
|
||||
upgrade? Full re-injection?
|
||||
|
||||
3. **Undo/redo**: DOM mutations as the undo unit? Or snapshot the serialized
|
||||
tree? DOM MutationObserver could track changes for undo.
|
||||
|
||||
4. **Collaboration (Yjs)**: sync what — the serialized tree, or DOM mutations?
|
||||
Could use DOM MutationObserver → Yjs doc, or serialize islands and sync
|
||||
the serialized form.
|
||||
|
||||
5. **Animation timeline**: CSS animations are real, but a design tool needs
|
||||
a timeline editor. How to introspect/control `Animation` objects from
|
||||
the meta-layer?
|
||||
|
||||
6. **Build complexity**: compiling WebKit from source is a multi-hour endeavor
|
||||
with many dependencies. CI/CD for 3 platforms will be challenging.
|
||||
Mitigation: start with one platform (Linux/WPE), get the architecture right.
|
||||
|
||||
## Minimal UI
|
||||
|
||||
The demo has almost no custom UI. The power is the browser engine underneath.
|
||||
|
||||
### Toolbar (4 buttons + zoom)
|
||||
|
||||
```
|
||||
[+ Frame] [+ Text] [+ Code] [⚙ Inspect] zoom: 100%
|
||||
```
|
||||
|
||||
- **+ Frame** — create a new island with a `<div>`, set its size by dragging
|
||||
- **+ Text** — create a new island with `<p contenteditable>`
|
||||
- **+ Code** — create a new island, paste raw HTML/CSS into it
|
||||
- **Inspect** — toggle: click an island to attach DevTools
|
||||
|
||||
### Canvas
|
||||
|
||||
The infinite canvas. Islands rendered as GPU-composited surfaces.
|
||||
Click → select (blue border). Drag → move. Drag between islands → reparent.
|
||||
|
||||
### DevTools as Property Panel
|
||||
|
||||
When an island is focused, **WebKit's built-in inspector connects to it**.
|
||||
Not a custom property panel. The real DevTools: Elements, Styles, Computed,
|
||||
Layout, Animations, Console.
|
||||
|
||||
```
|
||||
┌───────────────────────────────────┐
|
||||
│ [+ Frame] [+ Text] [+Code] [⚙] │
|
||||
├───────────────────────────────────┤
|
||||
│ Infinite Canvas │
|
||||
│ │
|
||||
│ ┌────────┐ ┌──────────┐ │
|
||||
│ │Island A│ │ Island B │ │
|
||||
│ │ │ │ selected▐│ │
|
||||
│ └────────┘ └──────────┘ │
|
||||
│ │
|
||||
├───────────────────────────────────┤
|
||||
│ 🔍 Inspector (Island B) │
|
||||
│ Elements │ Styles │ Computed │
|
||||
│ ▼ <hero-section> │
|
||||
│ ▼ #shadow-root (open) │
|
||||
│ <h1>Welcome</h1> │
|
||||
│ <button>Get Started</button>│
|
||||
│ ───────────────────────────── │
|
||||
│ display: flex; │
|
||||
│ gap: 24px; │
|
||||
│ padding: 48px; │
|
||||
└───────────────────────────────────┘
|
||||
```
|
||||
|
||||
Switching selected island switches the inspector target. Edit CSS in the
|
||||
inspector → island re-renders live on the canvas.
|
||||
|
||||
This means **every CSS property is editable from day one**. Grid, container
|
||||
queries, `:has()`, custom properties, animations — whatever the inspector
|
||||
can show, the user can change.
|
||||
|
||||
### DevTools Connection (WebKit Inspector)
|
||||
|
||||
WebKit has a built-in remote inspector protocol:
|
||||
|
||||
- On macOS: `WKWebView` exposes `_inspector` (private API) or
|
||||
`WKWebViewConfiguration.preferences._developerExtrasEnabled`
|
||||
enables right-click → Inspect Element
|
||||
- On WPE/GTK: `RemoteInspectorServer` listens on a socket,
|
||||
`webkit_web_view_get_inspector()` returns a `WebKitWebInspector`
|
||||
- The inspector protocol (JSON-RPC over socket) has domains:
|
||||
DOM, CSS, Page, Console, Network, Animation, LayerTree, etc.
|
||||
|
||||
For the PoC on macOS: enable the inspector on each WKWebView.
|
||||
When the user selects an island, programmatically show that island's
|
||||
inspector in a split panel.
|
||||
|
||||
## First Milestone: Proof of Concept (macOS)
|
||||
|
||||
Platform: macOS with native WebKit (WKWebView) + Core Animation/Metal.
|
||||
Language: Swift. This is the fastest path to validate the architecture.
|
||||
WPE (Linux/cross-platform) comes after the concept is proven.
|
||||
|
||||
### What to build
|
||||
|
||||
A macOS app (no Xcode, Swift Package Manager) that demonstrates:
|
||||
|
||||
1. **Metal viewport** — a single Metal view that composites island textures
|
||||
with a pan/zoom transform matrix
|
||||
2. **Islands as WKWebViews** — each island is a `WKWebView` rendered
|
||||
off-screen (hidden from the window, but its `CALayer` is captured
|
||||
or snapshotted for compositing)
|
||||
3. **HTML injection** — `loadHTMLString()` to put content into each island
|
||||
4. **Pan/zoom** — scroll wheel / trackpad to zoom and pan the canvas,
|
||||
applied as a transform on the compositor level
|
||||
5. **Selection** — click on an island → blue selection border drawn by
|
||||
the meta-layer (Metal overlay)
|
||||
6. **DevTools** — selecting an island opens WebKit's inspector for that
|
||||
WKWebView in a split panel below the canvas
|
||||
7. **Drag** — move islands around the canvas
|
||||
8. **Toolbar** — the 4 buttons: + Frame, + Text, + Code, Inspect toggle
|
||||
|
||||
### Architecture for macOS PoC
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ NSWindow │
|
||||
│ ┌─────────────────────────────────────────┐ │
|
||||
│ │ Toolbar (NSView / SwiftUI) │ │
|
||||
│ │ [+ Frame] [+ Text] [+ Code] [⚙] │ │
|
||||
│ ├─────────────────────────────────────────┤ │
|
||||
│ │ NSSplitView │ │
|
||||
│ │ ┌───────────────────────────────────┐ │ │
|
||||
│ │ │ CanvasView (NSView) │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ Hidden WKWebViews (off-screen) │ │ │
|
||||
│ │ │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │
|
||||
│ │ │ │ WK1 │ │ WK2 │ │ WK3 │ │ │ │
|
||||
│ │ │ └─────┘ └─────┘ └─────┘ │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ │ Canvas layer (Core Animation) │ │ │
|
||||
│ │ │ - world transform (pan/zoom) │ │ │
|
||||
│ │ │ - child CALayers (island renders) │ │ │
|
||||
│ │ │ - overlay layer (selection/guides)│ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ├───────────────────────────────────┤ │ │
|
||||
│ │ │ Inspector panel (WKWebView) │ │ │
|
||||
│ │ │ Connected to selected island │ │ │
|
||||
│ │ └───────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Each island `WKWebView` is a real WebKit rendering context with its own
|
||||
DOM, CSS, JS. Its rendered output is captured (via `takeSnapshot`,
|
||||
layer mirroring, or `snapshotView`) and composited onto the canvas.
|
||||
|
||||
For live updates: `WKWebView`'s `CALayer` can be re-rendered via
|
||||
`CALayerHost` or by positioning the actual WKWebView within the canvas
|
||||
view and applying the world transform via Core Animation. This avoids
|
||||
constant snapshotting — the WKWebViews ARE the canvas content, just
|
||||
transformed by our pan/zoom matrix.
|
||||
|
||||
### File structure
|
||||
|
||||
```
|
||||
open-pencil-next/
|
||||
├── Package.swift
|
||||
├── Sources/
|
||||
│ ├── App/
|
||||
│ │ ├── main.swift
|
||||
│ │ └── AppDelegate.swift
|
||||
│ ├── Canvas/
|
||||
│ │ ├── CanvasView.swift — the infinite canvas (NSView + Core Animation)
|
||||
│ │ ├── IslandManager.swift — creates/destroys WKWebView islands
|
||||
│ │ ├── MetaLayer.swift — selection borders, handles, guides (CALayer)
|
||||
│ │ └── HitTesting.swift — world-space → island → DOM element
|
||||
│ ├── Islands/
|
||||
│ │ ├── Island.swift — island model (WKWebView + position + size)
|
||||
│ │ ├── IslandRenderer.swift — manages WKWebView lifecycle and content
|
||||
│ │ └── ComponentRegistry.swift — shared Web Component definitions
|
||||
│ ├── Inspector/
|
||||
│ │ ├── InspectorPanel.swift — split panel hosting WebKit inspector
|
||||
│ │ └── InspectorBridge.swift — connects inspector to selected island
|
||||
│ ├── Toolbar/
|
||||
│ │ └── ToolbarView.swift — the 4 buttons
|
||||
│ └── Model/
|
||||
│ ├── SceneGraph.swift — serializable scene (islands + nodes)
|
||||
│ └── DesignTokens.swift — CSS custom properties shared across islands
|
||||
└── Tests/
|
||||
└── ...
|
||||
```
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
packages:
|
||||
- "packages/*"
|
||||
Loading…
Reference in a new issue