mirror of
https://github.com/Hopiu/bowser.git
synced 2026-03-16 19:10:24 +00:00
Initial bowser project scaffold
This commit is contained in:
commit
b0e693e50c
31 changed files with 481 additions and 0 deletions
158
.github/prompts/plan-bowserBrowser.prompt.md
vendored
Normal file
158
.github/prompts/plan-bowserBrowser.prompt.md
vendored
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
## Plan: Bowser — Custom Web Browser from Scratch
|
||||||
|
|
||||||
|
Build a complete web browser following the [browser.engineering](https://browser.engineering/) curriculum, implementing all major components without relying on WebView wrappers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Language Choice
|
||||||
|
|
||||||
|
- Default: **Python** (matches browser.engineering; fastest to build). Use Skia (`skia-python`) + GTK (`PyGObject`).
|
||||||
|
- Optional ports: **Go** (networking/concurrency, single binary) or **Zig** (C interop, fine-grained control) once the Python version is feature-complete. Keep Python parity as the spec.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### GUI Toolkit: GTK via PyGObject
|
||||||
|
|
||||||
|
- Cross-platform; mature Python bindings.
|
||||||
|
- Paint with Skia (preferred) or Cairo; full control of pixels, no WebView wrappers.
|
||||||
|
- Alternative: Skia + SDL2/GLFW if you want lower-level window/input handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Architecture Overview (from browser.engineering)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ Browser │
|
||||||
|
│ ┌─────────┐ ┌─────────────────────────────────────────┐ │
|
||||||
|
│ │ Chrome │ │ Tab │ │
|
||||||
|
│ │ (GTK) │ │ ┌───────┐ ┌────────┐ ┌───────────┐ │ │
|
||||||
|
│ │ │ │ │ Frame │ │ Frame │ │ Frame │ │ │
|
||||||
|
│ │ - tabs │ │ │(main) │ │(iframe)│ │ (iframe) │ │ │
|
||||||
|
│ │ - addr │ │ └───────┘ └────────┘ └───────────┘ │ │
|
||||||
|
│ │ - back │ └─────────────────────────────────────────┘ │
|
||||||
|
│ └─────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Implementation Phases
|
||||||
|
|
||||||
|
#### Part 1: Loading Pages
|
||||||
|
| Chapter | Component | Key Classes/Functions |
|
||||||
|
|---------|-----------|----------------------|
|
||||||
|
| 1 | URL & HTTP | `URL`, `request()`, `resolve()`, `COOKIE_JAR` |
|
||||||
|
| 2 | Drawing | `Browser`, `Chrome`, Skia/Cairo canvas |
|
||||||
|
| 3 | Text Formatting | `get_font()`, `linespace()`, word wrapping |
|
||||||
|
|
||||||
|
#### Part 2: Viewing Documents
|
||||||
|
| Chapter | Component | Key Classes/Functions |
|
||||||
|
|---------|-----------|----------------------|
|
||||||
|
| 4 | HTML Parsing | `HTMLParser`, `Text`, `Element`, `print_tree()` |
|
||||||
|
| 5 | Layout | `DocumentLayout`, `BlockLayout`, `LineLayout`, `TextLayout` |
|
||||||
|
| 6 | CSS | `CSSParser`, `TagSelector`, `DescendantSelector`, `style()` |
|
||||||
|
| 7 | Interaction | `Chrome`, hyperlinks, `click()`, `focus_element()` |
|
||||||
|
|
||||||
|
#### Part 3: Running Applications
|
||||||
|
| Chapter | Component | Key Classes/Functions |
|
||||||
|
|---------|-----------|----------------------|
|
||||||
|
| 8 | Forms | `submit_form()`, POST requests |
|
||||||
|
| 9 | JavaScript | `JSContext`, embed external engine (see below) |
|
||||||
|
| 10 | Security | Cookies, same-origin policy, XSS/CSRF protection |
|
||||||
|
|
||||||
|
#### Part 4: Modern Browsers
|
||||||
|
| Chapter | Component | Key Classes/Functions |
|
||||||
|
|---------|-----------|----------------------|
|
||||||
|
| 11 | Visual Effects | `Blend`, `Transform`, `CompositedLayer` |
|
||||||
|
| 12 | Threading | `Task`, `TaskRunner`, event loop |
|
||||||
|
| 13 | Animations | `NumericAnimation`, GPU compositing |
|
||||||
|
| 14 | Accessibility | `AccessibilityNode`, `speak_text()` |
|
||||||
|
| 15 | Embeds | `ImageLayout`, `IframeLayout`, `Frame` |
|
||||||
|
| 16 | Invalidation | `ProtectedField`, `dirty_style()`, incremental layout |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### JavaScript Engine Strategy
|
||||||
|
|
||||||
|
**Primary options (pick one to start):**
|
||||||
|
- **QuickJS / QuickJS-NG** — Small, fast, ES2020; approachable embedding.
|
||||||
|
- **Duktape** — Very small, forgiving; good starter.
|
||||||
|
- **LibJS** — Much larger; useful for reference, heavier to embed.
|
||||||
|
|
||||||
|
**Path:** start by embedding QuickJS (or QuickJS-NG). Add bindings for DOM APIs used in chapters. Optionally experiment with Duktape for simplicity. Custom interpreter later only for learning.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
bowser/
|
||||||
|
├── src/
|
||||||
|
│ ├── network/
|
||||||
|
│ │ ├── url.py # URL parsing, resolution
|
||||||
|
│ │ ├── http.py # HTTP/HTTPS requests
|
||||||
|
│ │ └── cookies.py # Cookie jar management
|
||||||
|
│ ├── parser/
|
||||||
|
│ │ ├── html.py # HTMLParser, Text, Element
|
||||||
|
│ │ └── css.py # CSSParser, selectors
|
||||||
|
│ ├── layout/
|
||||||
|
│ │ ├── document.py # DocumentLayout
|
||||||
|
│ │ ├── block.py # BlockLayout, LineLayout
|
||||||
|
│ │ ├── inline.py # TextLayout, InputLayout
|
||||||
|
│ │ └── embed.py # ImageLayout, IframeLayout
|
||||||
|
│ ├── render/
|
||||||
|
│ │ ├── paint.py # PaintCommand, Draw* classes
|
||||||
|
│ │ ├── composite.py # CompositedLayer, visual effects
|
||||||
|
│ │ └── fonts.py # Font management, text shaping
|
||||||
|
│ ├── script/
|
||||||
|
│ │ ├── context.py # JSContext
|
||||||
|
│ │ ├── bindings.py # DOM bindings for JS engine
|
||||||
|
│ │ └── runtime.js # JS runtime helpers
|
||||||
|
│ ├── browser/
|
||||||
|
│ │ ├── tab.py # Tab, Frame
|
||||||
|
│ │ ├── chrome.py # Chrome (UI)
|
||||||
|
│ │ └── browser.py # Main Browser class
|
||||||
|
│ └── accessibility/
|
||||||
|
│ └── a11y.py # AccessibilityNode, screen reader
|
||||||
|
├── tests/
|
||||||
|
├── assets/
|
||||||
|
│ └── default.css # User-agent stylesheet
|
||||||
|
└── main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Development Milestones
|
||||||
|
|
||||||
|
- [ ] **M1**: Display "Hello World" in window (URL → HTTP → canvas)
|
||||||
|
- [ ] **M2**: Render plain HTML with text wrapping
|
||||||
|
- [ ] **M3**: Parse and apply basic CSS (colors, fonts, margins)
|
||||||
|
- [ ] **M4**: Clickable links and navigation
|
||||||
|
- [ ] **M5**: Form input and submission
|
||||||
|
- [ ] **M6**: JavaScript execution (console.log, DOM queries)
|
||||||
|
- [ ] **M7**: Event handling (onclick, onsubmit)
|
||||||
|
- [ ] **M8**: Images and iframes
|
||||||
|
- [ ] **M9**: Smooth scrolling and animations
|
||||||
|
- [ ] **M10**: Accessibility tree and keyboard navigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Key Dependencies (Python)
|
||||||
|
|
||||||
|
```
|
||||||
|
skia-python # 2D graphics (or cairocffi)
|
||||||
|
PyGObject # GTK bindings
|
||||||
|
pyduktape2 # JavaScript engine (or quickjs)
|
||||||
|
harfbuzz # Text shaping (via uharfbuzz)
|
||||||
|
Pillow # Image decoding
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Resources
|
||||||
|
|
||||||
|
- 📖 [browser.engineering](https://browser.engineering/) — Primary reference
|
||||||
|
- 📖 [Let's Build a Browser Engine](https://limpet.net/mbrubeck/2014/08/08/toy-layout-engine-1.html) — Matt Brubeck's Rust tutorial
|
||||||
|
- 🔧 [Skia Graphics Library](https://skia.org/)
|
||||||
|
- 🔧 [QuickJS](https://bellard.org/quickjs/) — Embeddable JS engine
|
||||||
6
assets/default.css
Normal file
6
assets/default.css
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
/* Default user-agent stylesheet placeholder. */
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 8px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
13
main.py
Normal file
13
main.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
"""Entry point for Bowser browser (stub)."""
|
||||||
|
|
||||||
|
from src.browser.browser import Browser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
browser = Browser()
|
||||||
|
browser.new_tab("https://example.com")
|
||||||
|
browser.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
1
src/__init__.py
Normal file
1
src/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Bowser browser engine packages."""
|
||||||
1
src/accessibility/__init__.py
Normal file
1
src/accessibility/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Accessibility tree and helpers."""
|
||||||
10
src/accessibility/a11y.py
Normal file
10
src/accessibility/a11y.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Accessibility stubs."""
|
||||||
|
|
||||||
|
|
||||||
|
class AccessibilityNode:
|
||||||
|
def __init__(self, node, parent=None):
|
||||||
|
self.node = node
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
return self
|
||||||
1
src/browser/__init__.py
Normal file
1
src/browser/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Browser chrome and tab orchestration."""
|
||||||
21
src/browser/browser.py
Normal file
21
src/browser/browser.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
"""Browser entry and orchestration."""
|
||||||
|
|
||||||
|
from ..network.url import URL
|
||||||
|
from .tab import Tab
|
||||||
|
|
||||||
|
|
||||||
|
class Browser:
|
||||||
|
def __init__(self):
|
||||||
|
self.tabs = []
|
||||||
|
self.active_tab: Tab | None = None
|
||||||
|
|
||||||
|
def new_tab(self, url: str):
|
||||||
|
tab = Tab(self)
|
||||||
|
tab.load(URL(url))
|
||||||
|
self.tabs.append(tab)
|
||||||
|
self.active_tab = tab
|
||||||
|
return tab
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# Placeholder: mainloop hooks into GTK
|
||||||
|
pass
|
||||||
10
src/browser/chrome.py
Normal file
10
src/browser/chrome.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Browser chrome (GTK UI) stub."""
|
||||||
|
|
||||||
|
|
||||||
|
class Chrome:
|
||||||
|
def __init__(self, browser):
|
||||||
|
self.browser = browser
|
||||||
|
|
||||||
|
def paint(self):
|
||||||
|
# Placeholder: draw tabs/address bar via GTK + Skia/Cairo
|
||||||
|
pass
|
||||||
27
src/browser/tab.py
Normal file
27
src/browser/tab.py
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
"""Tab and frame orchestration stubs."""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from ..network.url import URL
|
||||||
|
|
||||||
|
|
||||||
|
class Frame:
|
||||||
|
def __init__(self, tab: "Tab", parent_frame=None, frame_element=None):
|
||||||
|
self.tab = tab
|
||||||
|
self.parent_frame = parent_frame
|
||||||
|
self.frame_element = frame_element
|
||||||
|
|
||||||
|
def load(self, url: URL, payload: Optional[bytes] = None):
|
||||||
|
# TODO: integrate network + parsing + layout + render pipeline
|
||||||
|
self.tab.current_url = url
|
||||||
|
|
||||||
|
|
||||||
|
class Tab:
|
||||||
|
def __init__(self, browser: "Browser", tab_height: int = 40):
|
||||||
|
self.browser = browser
|
||||||
|
self.tab_height = tab_height
|
||||||
|
self.current_url: Optional[URL] = None
|
||||||
|
self.main_frame = Frame(self)
|
||||||
|
|
||||||
|
def load(self, url: URL, payload: Optional[bytes] = None):
|
||||||
|
self.main_frame.load(url, payload)
|
||||||
1
src/layout/__init__.py
Normal file
1
src/layout/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Layout: block/inline layout objects."""
|
||||||
23
src/layout/block.py
Normal file
23
src/layout/block.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""Block and line layout stubs."""
|
||||||
|
|
||||||
|
|
||||||
|
class BlockLayout:
|
||||||
|
def __init__(self, node, parent=None, previous=None, frame=None):
|
||||||
|
self.node = node
|
||||||
|
self.parent = parent
|
||||||
|
self.previous = previous
|
||||||
|
self.frame = frame
|
||||||
|
self.children = []
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class LineLayout:
|
||||||
|
def __init__(self, node, parent=None, previous=None):
|
||||||
|
self.node = node
|
||||||
|
self.parent = parent
|
||||||
|
self.previous = previous
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
return 0
|
||||||
12
src/layout/document.py
Normal file
12
src/layout/document.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""Document-level layout stub."""
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentLayout:
|
||||||
|
def __init__(self, node, frame=None):
|
||||||
|
self.node = node
|
||||||
|
self.frame = frame
|
||||||
|
self.children = []
|
||||||
|
|
||||||
|
def layout(self, width: int, zoom: float = 1.0):
|
||||||
|
# Placeholder layout logic
|
||||||
|
return width * zoom
|
||||||
23
src/layout/embed.py
Normal file
23
src/layout/embed.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"""Embedded content layout stubs (images, iframes)."""
|
||||||
|
|
||||||
|
|
||||||
|
class ImageLayout:
|
||||||
|
def __init__(self, node, parent=None, previous=None, frame=None):
|
||||||
|
self.node = node
|
||||||
|
self.parent = parent
|
||||||
|
self.previous = previous
|
||||||
|
self.frame = frame
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class IframeLayout:
|
||||||
|
def __init__(self, node, parent=None, previous=None, parent_frame=None):
|
||||||
|
self.node = node
|
||||||
|
self.parent = parent
|
||||||
|
self.previous = previous
|
||||||
|
self.parent_frame = parent_frame
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
return 0
|
||||||
12
src/layout/inline.py
Normal file
12
src/layout/inline.py
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
"""Inline and text layout stubs."""
|
||||||
|
|
||||||
|
|
||||||
|
class TextLayout:
|
||||||
|
def __init__(self, node, word, parent=None, previous=None):
|
||||||
|
self.node = node
|
||||||
|
self.word = word
|
||||||
|
self.parent = parent
|
||||||
|
self.previous = previous
|
||||||
|
|
||||||
|
def layout(self):
|
||||||
|
return len(self.word)
|
||||||
1
src/network/__init__.py
Normal file
1
src/network/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Networking: URL parsing, HTTP, cookies."""
|
||||||
17
src/network/cookies.py
Normal file
17
src/network/cookies.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
"""In-memory cookie jar (placeholder)."""
|
||||||
|
|
||||||
|
from http.cookies import SimpleCookie
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class CookieJar:
|
||||||
|
def __init__(self):
|
||||||
|
self._cookies: Dict[str, SimpleCookie] = {}
|
||||||
|
|
||||||
|
def set_cookies(self, origin: str, cookie_header: str) -> None:
|
||||||
|
jar = self._cookies.setdefault(origin, SimpleCookie())
|
||||||
|
jar.load(cookie_header)
|
||||||
|
|
||||||
|
def get_cookie_header(self, origin: str) -> str:
|
||||||
|
jar = self._cookies.get(origin)
|
||||||
|
return jar.output(header="", sep="; ").strip() if jar else ""
|
||||||
18
src/network/http.py
Normal file
18
src/network/http.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""HTTP requests and response handling."""
|
||||||
|
|
||||||
|
import http.client
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from .url import URL
|
||||||
|
|
||||||
|
|
||||||
|
def request(url: URL, payload: Optional[bytes] = None, method: str = "GET"):
|
||||||
|
parsed = url._parsed
|
||||||
|
conn_class = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
|
||||||
|
conn = conn_class(parsed.hostname, parsed.port or (443 if parsed.scheme == "https" else 80))
|
||||||
|
path = parsed.path or "/"
|
||||||
|
if parsed.query:
|
||||||
|
path = f"{path}?{parsed.query}"
|
||||||
|
headers = {}
|
||||||
|
conn.request(method, path, body=payload, headers=headers)
|
||||||
|
return conn.getresponse()
|
||||||
20
src/network/url.py
Normal file
20
src/network/url.py
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
"""URL parsing and resolution."""
|
||||||
|
|
||||||
|
from urllib.parse import urlparse, urljoin
|
||||||
|
|
||||||
|
|
||||||
|
class URL:
|
||||||
|
def __init__(self, url: str):
|
||||||
|
self._parsed = urlparse(url)
|
||||||
|
|
||||||
|
def resolve(self, relative: str) -> "URL":
|
||||||
|
return URL(urljoin(self._parsed.geturl(), relative))
|
||||||
|
|
||||||
|
def origin(self) -> str:
|
||||||
|
scheme = self._parsed.scheme
|
||||||
|
host = self._parsed.hostname or ""
|
||||||
|
port = f":{self._parsed.port}" if self._parsed.port else ""
|
||||||
|
return f"{scheme}://{host}{port}"
|
||||||
|
|
||||||
|
def __str__(self) -> str: # pragma: no cover - convenience
|
||||||
|
return self._parsed.geturl()
|
||||||
1
src/parser/__init__.py
Normal file
1
src/parser/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Parsing: HTML and CSS parsers."""
|
||||||
16
src/parser/css.py
Normal file
16
src/parser/css.py
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
"""CSS parser stubs."""
|
||||||
|
|
||||||
|
|
||||||
|
class CSSRule:
|
||||||
|
def __init__(self, selector: str, declarations: dict):
|
||||||
|
self.selector = selector
|
||||||
|
self.declarations = declarations
|
||||||
|
|
||||||
|
|
||||||
|
def parse(css_text: str):
|
||||||
|
# Placeholder: split on semicolons per line
|
||||||
|
rules = []
|
||||||
|
for line in css_text.splitlines():
|
||||||
|
if "{" not in line:
|
||||||
|
continue
|
||||||
|
return rules
|
||||||
29
src/parser/html.py
Normal file
29
src/parser/html.py
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
"""HTML parser stubs."""
|
||||||
|
|
||||||
|
|
||||||
|
class Text:
|
||||||
|
def __init__(self, text, parent=None):
|
||||||
|
self.text = text
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def __repr__(self): # pragma: no cover - debug helper
|
||||||
|
return f"Text({self.text!r})"
|
||||||
|
|
||||||
|
|
||||||
|
class Element:
|
||||||
|
def __init__(self, tag, attributes=None, parent=None):
|
||||||
|
self.tag = tag
|
||||||
|
self.attributes = attributes or {}
|
||||||
|
self.children = []
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def __repr__(self): # pragma: no cover - debug helper
|
||||||
|
return f"Element({self.tag!r}, {self.attributes!r})"
|
||||||
|
|
||||||
|
|
||||||
|
def print_tree(node, indent=0):
|
||||||
|
spacer = " " * indent
|
||||||
|
print(f"{spacer}{node}")
|
||||||
|
if hasattr(node, "children"):
|
||||||
|
for child in node.children:
|
||||||
|
print_tree(child, indent + 1)
|
||||||
1
src/render/__init__.py
Normal file
1
src/render/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""Rendering and painting primitives."""
|
||||||
11
src/render/composite.py
Normal file
11
src/render/composite.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""Compositing stubs."""
|
||||||
|
|
||||||
|
|
||||||
|
class CompositedLayer:
|
||||||
|
def __init__(self, display_item=None):
|
||||||
|
self.items = []
|
||||||
|
if display_item:
|
||||||
|
self.items.append(display_item)
|
||||||
|
|
||||||
|
def add(self, display_item):
|
||||||
|
self.items.append(display_item)
|
||||||
10
src/render/fonts.py
Normal file
10
src/render/fonts.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
"""Font management stubs."""
|
||||||
|
|
||||||
|
|
||||||
|
def get_font(size: int, weight: str = "normal", style: str = "normal"):
|
||||||
|
return (size, weight, style)
|
||||||
|
|
||||||
|
|
||||||
|
def linespace(font) -> int:
|
||||||
|
size, _, _ = font
|
||||||
|
return int(size * 1.2)
|
||||||
18
src/render/paint.py
Normal file
18
src/render/paint.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
"""Painting primitives (stubs)."""
|
||||||
|
|
||||||
|
|
||||||
|
class PaintCommand:
|
||||||
|
def __init__(self, rect):
|
||||||
|
self.rect = rect
|
||||||
|
|
||||||
|
|
||||||
|
class DrawText(PaintCommand):
|
||||||
|
def __init__(self, x1, y1, text, font, color):
|
||||||
|
super().__init__((x1, y1, x1, y1))
|
||||||
|
self.text = text
|
||||||
|
self.font = font
|
||||||
|
self.color = color
|
||||||
|
|
||||||
|
def execute(self, canvas):
|
||||||
|
# Placeholder: integrate with Skia/Cairo later
|
||||||
|
pass
|
||||||
1
src/script/__init__.py
Normal file
1
src/script/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
"""JavaScript integration and bindings."""
|
||||||
6
src/script/bindings.py
Normal file
6
src/script/bindings.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
"""DOM bindings to expose into the JS engine (stub)."""
|
||||||
|
|
||||||
|
|
||||||
|
def install_dom_bindings(js_context):
|
||||||
|
# TODO: expose document/window APIs
|
||||||
|
return js_context
|
||||||
11
src/script/context.py
Normal file
11
src/script/context.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
"""JavaScript context integration (stub)."""
|
||||||
|
|
||||||
|
|
||||||
|
class JSContext:
|
||||||
|
def __init__(self, tab, url_origin: str):
|
||||||
|
self.tab = tab
|
||||||
|
self.url_origin = url_origin
|
||||||
|
|
||||||
|
def run(self, script: str, code: str, window_id=0):
|
||||||
|
# Placeholder: wire to QuickJS/NG binding
|
||||||
|
return None
|
||||||
2
src/script/runtime.js
Normal file
2
src/script/runtime.js
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Runtime helpers injected into JS contexts.
|
||||||
|
// Placeholder: add DOM polyfills and event loop shims here.
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
Loading…
Reference in a new issue