@blender-ui/native 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +68 -2
- package/blenderui.win32-x64.node +0 -0
- package/index.d.ts +187 -0
- package/index.js +541 -0
- package/package.json +60 -4
- package/signals.js +628 -0
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @blender-ui/native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Blender's UI toolkit from JavaScript: native windows drawn on the CPU or with
|
|
4
|
+
Direct3D 11, HTML elements, CSS and Tailwind classes, and signals. It runs on
|
|
5
|
+
Node.js and Bun through a native addon (napi-rs), with no browser and no
|
|
6
|
+
Electron. For React, see
|
|
7
|
+
[`@blender-ui/react`](https://www.npmjs.com/package/@blender-ui/react).
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @blender-ui/native
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
const { App, signal } = require('@blender-ui/native')
|
|
15
|
+
|
|
16
|
+
const app = new App({ gpu: true })
|
|
17
|
+
const win = app.window({ title: 'Hello', width: 360, height: 200 })
|
|
18
|
+
win.body.className = 'm-0 p-6 flex flex-col gap-3 bg-[#2b2b2b] text-[#e6e6e6]'
|
|
19
|
+
|
|
20
|
+
const clicks = signal(0)
|
|
21
|
+
const label = win.createElement('span')
|
|
22
|
+
label.textContent = clicks // follows the signal
|
|
23
|
+
const button = win.createElement('button')
|
|
24
|
+
button.textContent = 'Add one'
|
|
25
|
+
button.addEventListener('click', () => clicks.value++)
|
|
26
|
+
win.body.appendChild(label)
|
|
27
|
+
win.body.appendChild(button)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## API
|
|
31
|
+
|
|
32
|
+
- **A DOM-like tree:** `createElement`, `createTextNode`, `appendChild`,
|
|
33
|
+
`insertBefore`, `removeChild`, `setAttribute`, `className` (with Tailwind
|
|
34
|
+
classes), `style` (a string or an object), `textContent`.
|
|
35
|
+
- **Events:** `addEventListener` for `click`, `mousedown`, `mouseup`,
|
|
36
|
+
`mousemove`, `mouseenter`, `mouseleave`, `wheel`, `keydown`, `keyup`, `input`
|
|
37
|
+
and `change`. They bubble through the tree, and `stopPropagation()` works.
|
|
38
|
+
They arrive after BUI has handled them, so `preventDefault()` only marks the
|
|
39
|
+
event.
|
|
40
|
+
- **Frames and measurement:** `win.requestAnimationFrame(cb)` is paced by the
|
|
41
|
+
display. `el.getBoundingClientRect()` and `win.screenshot()` return
|
|
42
|
+
promises.
|
|
43
|
+
- **Signals:** `signal`, `memo`, `effect`, `batch`, `untrack`,
|
|
44
|
+
`startTransition`. Pass a signal to `setAttribute`, `style`, `textContent`
|
|
45
|
+
or `createTextNode`, and its changes update that node alone. The signals are
|
|
46
|
+
also available without the addon: `require('@blender-ui/native/signals')`.
|
|
47
|
+
- **Tests:** `new App({ headless: true })` renders off-screen, and
|
|
48
|
+
`win.click(x, y)` simulates input.
|
|
49
|
+
|
|
50
|
+
## How it's fast
|
|
51
|
+
|
|
52
|
+
- **Its own UI thread:** BUI renders on a thread of its own, so it never
|
|
53
|
+
blocks Node and an idle window uses no CPU.
|
|
54
|
+
- **Only changes cross:** each tick, a window sends one batch of operations
|
|
55
|
+
(an `Int32Array` and a string table) to the UI thread.
|
|
56
|
+
- **Styles parsed once:** a `style` value is parsed when it's set, not every
|
|
57
|
+
frame.
|
|
58
|
+
|
|
59
|
+
## Requirements
|
|
60
|
+
|
|
61
|
+
- **Prebuilt for Windows x64.** On other platforms, build from a clone of the
|
|
62
|
+
[repository](https://github.com/ppmpreetham/BUI) with `npm run build` in
|
|
63
|
+
`bindings/node`. That needs Rust, CMake and a C compiler; native windows
|
|
64
|
+
there are not supported yet, so use `headless: true`.
|
|
65
|
+
- **Runtimes:** Node.js 18 or newer, or Bun.
|
|
66
|
+
|
|
67
|
+
Guide, benchmarks and internals:
|
|
68
|
+
[docs/javascript.md](https://github.com/ppmpreetham/BUI/blob/main/docs/javascript.md).
|
|
69
|
+
License: GPL-2.0-or-later.
|
|
Binary file
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// Types for @blender-ui/native.
|
|
2
|
+
export interface AppOptions {
|
|
3
|
+
/** Draw on the GPU (Direct3D 11). */
|
|
4
|
+
gpu?: boolean
|
|
5
|
+
/** GPU text from glyph outlines (Slug, default) or a per-glyph atlas. */
|
|
6
|
+
slug?: boolean
|
|
7
|
+
/** No native windows: tests, screenshots, CI. */
|
|
8
|
+
headless?: boolean
|
|
9
|
+
/** Quit when the last window closes (default true). */
|
|
10
|
+
quitOnLastWindowClosed?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface WindowOptions {
|
|
14
|
+
title?: string
|
|
15
|
+
width?: number
|
|
16
|
+
height?: number
|
|
17
|
+
resizable?: boolean
|
|
18
|
+
gpu?: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type EventType = 'click' | 'mousedown' | 'mouseup' | 'mousemove' | 'mouseenter' | 'mouseleave' |
|
|
22
|
+
'wheel' | 'keydown' | 'keyup' | 'change' | 'input'
|
|
23
|
+
|
|
24
|
+
export class BuiEvent {
|
|
25
|
+
type: EventType
|
|
26
|
+
target: Element
|
|
27
|
+
currentTarget: Element
|
|
28
|
+
clientX: number
|
|
29
|
+
clientY: number
|
|
30
|
+
x: number
|
|
31
|
+
y: number
|
|
32
|
+
button: number
|
|
33
|
+
detail: number
|
|
34
|
+
deltaY: number
|
|
35
|
+
key?: string
|
|
36
|
+
char?: string
|
|
37
|
+
shiftKey: boolean
|
|
38
|
+
ctrlKey: boolean
|
|
39
|
+
altKey: boolean
|
|
40
|
+
metaKey: boolean
|
|
41
|
+
timeStamp: number
|
|
42
|
+
preventDefault (): void
|
|
43
|
+
stopPropagation (): void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class Node {
|
|
47
|
+
readonly ownerWindow: Window
|
|
48
|
+
readonly id: number
|
|
49
|
+
parentNode: Element | null
|
|
50
|
+
childNodes: Node[]
|
|
51
|
+
readonly firstChild: Node | null
|
|
52
|
+
readonly lastChild: Node | null
|
|
53
|
+
readonly nextSibling: Node | null
|
|
54
|
+
textContent: string
|
|
55
|
+
appendChild<T extends Node> (child: T): T
|
|
56
|
+
insertBefore<T extends Node> (child: T, before: Node | null): T
|
|
57
|
+
removeChild<T extends Node> (child: T): T
|
|
58
|
+
remove (): void
|
|
59
|
+
/** Free the node and its subtree natively (after removing it). */
|
|
60
|
+
destroy (): void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class TextNode extends Node {
|
|
64
|
+
/** A string, or a signal (the text then follows it). */
|
|
65
|
+
data: string | ReadSignal<unknown>
|
|
66
|
+
nodeValue: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class Element extends Node {
|
|
70
|
+
readonly tagName: string
|
|
71
|
+
readonly localName: string
|
|
72
|
+
className: string
|
|
73
|
+
/** CSS text, or an object such as `{ backgroundColor: 'red', width: 40 }`. */
|
|
74
|
+
style: string | Record<string, string | number | null | undefined> | ReadSignal<string | Record<string, string | number | null | undefined>>
|
|
75
|
+
/** Form controls: the latest value from input / change events. */
|
|
76
|
+
value?: string
|
|
77
|
+
checked?: boolean
|
|
78
|
+
/** A value, or a signal: the attribute then follows it (null / false removes it). */
|
|
79
|
+
setAttribute (name: string, value: unknown): void
|
|
80
|
+
getAttribute (name: string): string | null
|
|
81
|
+
hasAttribute (name: string): boolean
|
|
82
|
+
removeAttribute (name: string): void
|
|
83
|
+
addEventListener (type: EventType, listener: (event: BuiEvent) => void): void
|
|
84
|
+
removeEventListener (type: EventType, listener: (event: BuiEvent) => void): void
|
|
85
|
+
getBoundingClientRect (): Promise<{ x: number, y: number, width: number, height: number } | null>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class Window {
|
|
89
|
+
readonly app: App
|
|
90
|
+
readonly id: number
|
|
91
|
+
readonly body: Element
|
|
92
|
+
closed: boolean
|
|
93
|
+
createElement (tag: string): Element
|
|
94
|
+
createTextNode (text: string | ReadSignal<unknown>): TextNode
|
|
95
|
+
/** Send pending changes now (they are otherwise sent once per tick). */
|
|
96
|
+
flush (): void
|
|
97
|
+
/** Run `cb` before the next frame, paced by the display. */
|
|
98
|
+
requestAnimationFrame (cb: (time: number) => void): number
|
|
99
|
+
cancelAnimationFrame (id: number): void
|
|
100
|
+
screenshot (): Promise<{ width: number, height: number, data: Buffer }>
|
|
101
|
+
click (x: number, y: number): void
|
|
102
|
+
close (): void
|
|
103
|
+
on (event: 'close', listener: () => void): this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class App {
|
|
107
|
+
constructor (options?: AppOptions)
|
|
108
|
+
readonly headless: boolean
|
|
109
|
+
readonly windows: Map<number, Window>
|
|
110
|
+
quitOnLastWindowClosed: boolean
|
|
111
|
+
window (options?: WindowOptions): Window
|
|
112
|
+
setSlug (on: boolean): void
|
|
113
|
+
quit (): void
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function styleText (style: Record<string, string | number | null | undefined>): string
|
|
117
|
+
|
|
118
|
+
// Signals: BUI's hooks (the Rust crate's Signal / Memo / Effect).
|
|
119
|
+
// Bind one to an attribute, `style` or text and that node alone updates.
|
|
120
|
+
|
|
121
|
+
export interface ReadSignal<T> {
|
|
122
|
+
/** Read, subscribing the running memo / effect. */
|
|
123
|
+
readonly value: T
|
|
124
|
+
get (): T
|
|
125
|
+
/** Read without subscribing. */
|
|
126
|
+
peek (): T
|
|
127
|
+
with<R> (fn: (value: T) => R): R
|
|
128
|
+
/** Call `fn` now and on every change; returns the unsubscribe function. */
|
|
129
|
+
subscribe (fn: (value: T) => void): () => void
|
|
130
|
+
/** `count + 1`, `count > 5` read the value (`===` and truthiness don't: use `.value`). */
|
|
131
|
+
valueOf (): T
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class Signal<T> implements ReadSignal<T> {
|
|
135
|
+
constructor (value: T, options?: { equals?: (a: T, b: T) => boolean })
|
|
136
|
+
value: T
|
|
137
|
+
get (): T
|
|
138
|
+
peek (): T
|
|
139
|
+
set (value: T): void
|
|
140
|
+
update (fn: (value: T) => T): void
|
|
141
|
+
with<R> (fn: (value: T) => R): R
|
|
142
|
+
subscribe (fn: (value: T) => void): () => void
|
|
143
|
+
valueOf (): T
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class Memo<T> implements ReadSignal<T> {
|
|
147
|
+
readonly value: T
|
|
148
|
+
get (): T
|
|
149
|
+
peek (): T
|
|
150
|
+
with<R> (fn: (value: T) => R): R
|
|
151
|
+
subscribe (fn: (value: T) => void): () => void
|
|
152
|
+
valueOf (): T
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function signal<T> (value: T, options?: { equals?: (a: T, b: T) => boolean }): Signal<T>
|
|
156
|
+
/** A lazily derived, cached value, recomputed when the signals it read change. */
|
|
157
|
+
export function memo<T> (fn: () => T, options?: { equals?: (a: T, b: T) => boolean }): Memo<T>
|
|
158
|
+
export const computed: typeof memo
|
|
159
|
+
/** Runs `fn` now and when what it read changes; `fn` may return a cleanup. Returns `dispose`. */
|
|
160
|
+
export function effect (fn: () => void | (() => void)): () => void
|
|
161
|
+
/** Several writes, one update. */
|
|
162
|
+
export function batch<R> (fn: () => R): R
|
|
163
|
+
export function untrack<R> (fn: () => R): R
|
|
164
|
+
/** Leptos / Solid style getter and setter. */
|
|
165
|
+
export function createSignal<T> (value: T): [() => T, (value: T | ((prev: T) => T)) => void]
|
|
166
|
+
export function isSignal (x: unknown): x is ReadSignal<unknown>
|
|
167
|
+
/** Writes in `fn` update what they affect after urgent work, in slices. Settles
|
|
168
|
+
* when done; rejects with the error of an async `fn`. */
|
|
169
|
+
export function startTransition (fn: () => void | Promise<void>): Promise<void>
|
|
170
|
+
/** Run `fn` at urgent priority, even inside a transition. */
|
|
171
|
+
export function urgent<R> (fn: () => R): R
|
|
172
|
+
|
|
173
|
+
/** A ref whose `.current` effects and memos can follow (reading it while a
|
|
174
|
+
* React component renders doesn't subscribe). */
|
|
175
|
+
export class Ref<T> extends Signal<T> { current: T }
|
|
176
|
+
export function ref<T> (value: T): Ref<T>
|
|
177
|
+
/** Run `fn` outside any owning effect: effects it creates live until disposed. */
|
|
178
|
+
export function unowned<R> (fn: () => R): R
|
|
179
|
+
|
|
180
|
+
/** The addon (internal), plus counters for benchmarks. */
|
|
181
|
+
export const native: {
|
|
182
|
+
/** CPU cycles of every thread of this process (Windows), or null. */
|
|
183
|
+
processCpuCycles (): number | null
|
|
184
|
+
/** The x86-64 time-stamp counter, or null. */
|
|
185
|
+
timestampCounter (): number | null
|
|
186
|
+
[key: string]: unknown
|
|
187
|
+
}
|