quickwin 1.2.1 → 2.0.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/README.en.md +3 -3
- package/README.md +21 -24
- package/examples/PdfCanvas.js +8 -37
- package/examples/PdfCanvas.tsx +11 -41
- package/examples/exec_server.js +20054 -1423
- package/examples/exec_server.tsx +489 -0
- package/examples/exec_server_worker.js +383 -33
- package/examples/exec_server_worker.ts +408 -30
- package/examples/pdf_preview2.js +31 -73
- package/examples/pdf_preview2.ts +33 -76
- package/examples/pdf_viewer.js +1496 -616
- package/examples/pdf_viewer.tsx +23 -64
- package/examples/setres.js +22 -47
- package/examples/setres.ts +23 -57
- package/examples/test_gallery.js +1491 -525
- package/examples/test_react_lazy.js +1455 -500
- package/examples/test_react_listbox.js +1454 -499
- package/examples/test_root_window.js +1450 -495
- package/examples/test_tab.js +1455 -500
- package/lib/abort.js +70 -0
- package/lib/abort.ts +69 -0
- package/lib/certs.js +214 -0
- package/lib/certs.ts +224 -0
- package/lib/exec-log.js +15 -0
- package/lib/exec-log.ts +30 -0
- package/lib/fetch.js +37 -4
- package/lib/fetch.ts +35 -4
- package/lib/ffi/bind.js +352 -0
- package/lib/ffi/bind.ts +442 -0
- package/lib/ffi/ctype.js +168 -0
- package/lib/ffi/ctype.ts +331 -0
- package/lib/ffi/index.js +6 -0
- package/lib/ffi/index.ts +6 -0
- package/lib/ffi/struct.js +412 -0
- package/lib/ffi/struct.ts +664 -0
- package/lib/http-common.js +43 -5
- package/lib/http-common.ts +48 -6
- package/lib/http-server.js +187 -29
- package/lib/http-server.ts +177 -26
- package/lib/react-qw/components/DateTimePicker.js +20 -33
- package/lib/react-qw/components/Input.js +3 -10
- package/lib/react-qw/components/Link.js +7 -22
- package/lib/react-qw/components/ListView.js +80 -141
- package/lib/react-qw/components/PathPicker.js +36 -80
- package/lib/react-qw/components/ScrollView.js +15 -8
- package/lib/react-qw/components/Slider.js +2 -1
- package/lib/react-qw/components/Tab.js +15 -20
- package/lib/react-qw/components/Tooltip.js +21 -20
- package/lib/react-qw/components/TreeView.js +14 -47
- package/lib/react-qw/index.js +4 -2
- package/lib/react-qw/index.ts +4 -2
- package/lib/react-qw/nmhdr.js +5 -0
- package/lib/react-qw/nmhdr.ts +7 -0
- package/lib/react-qw/props.js +7 -2
- package/lib/react-qw/props.ts +7 -3
- package/lib/react-qw/reconciler.js +23 -13
- package/lib/react-qw/reconciler.ts +22 -13
- package/lib/text-measure.js +18 -35
- package/lib/text-measure.ts +20 -37
- package/lib/websocket.js +12 -2
- package/lib/websocket.ts +15 -2
- package/lib/windows/comctl32.js +18 -0
- package/lib/windows/comctl32.ts +20 -0
- package/lib/windows/comdlg32.js +7 -0
- package/lib/windows/comdlg32.ts +9 -0
- package/lib/windows/gdi32.js +90 -0
- package/lib/windows/gdi32.ts +97 -0
- package/lib/windows/kernel32.js +66 -0
- package/lib/windows/kernel32.ts +76 -0
- package/lib/windows/ntdll.js +8 -0
- package/lib/windows/ntdll.ts +10 -0
- package/lib/windows/ole32.js +7 -0
- package/lib/windows/ole32.ts +9 -0
- package/lib/windows/shell32.js +11 -0
- package/lib/windows/shell32.ts +14 -0
- package/lib/windows/structs.js +384 -0
- package/lib/windows/structs.ts +422 -0
- package/lib/windows/user32.js +469 -0
- package/lib/windows/user32.ts +502 -0
- package/package.json +1 -1
- package/quickwin.d.ts +22 -33
- package/quickwin_const.d.ts +1 -0
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/test/bench_ffi_callraw.js +236 -0
- package/test/bench_ffi_callraw.ts +236 -0
- package/test/bench_ffi_overhead.js +139 -0
- package/test/bench_ffi_overhead.ts +141 -0
- package/test/bench_sendmessage.js +104 -0
- package/test/bench_sendmessage.ts +100 -0
- package/test/bench_sendmessage_nonnull.js +187 -0
- package/test/bench_sendmessage_nonnull.ts +188 -0
- package/test/run.js +14 -4
- package/test/run.ts +15 -5
- package/test/test_basic_fetch.js +8 -0
- package/test/test_basic_fetch.ts +9 -0
- package/test/test_ffi.js +33 -47
- package/test/test_ffi.ts +41 -60
- package/test/test_ffi_abi.js +235 -0
- package/test/test_ffi_abi.ts +218 -0
- package/test/test_ffi_bind.js +572 -0
- package/test/test_ffi_bind.ts +597 -0
- package/test/test_ffi_struct.js +745 -0
- package/test/test_ffi_struct.ts +798 -0
- package/test/test_ffi_struct_layout.js +495 -0
- package/test/test_ffi_struct_layout.ts +543 -0
- package/test/test_ffi_user32.js +103 -0
- package/test/test_ffi_user32.ts +113 -0
- package/test/test_ffi_windows.js +101 -0
- package/test/test_ffi_windows.ts +113 -0
- package/test/test_helper.js +8 -1
- package/test/test_helper.ts +9 -1
- package/test/test_http_import.js +18 -0
- package/test/test_http_import.ts +19 -0
- package/test/test_http_server.js +73 -1
- package/test/test_http_server.ts +75 -1
- package/test/test_ipv6.js +2 -2
- package/test/test_ipv6.ts +2 -2
- package/test/test_net_fetch.js +70 -8
- package/test/test_net_fetch.ts +69 -7
- package/test/test_net_websocket.js +1 -1
- package/test/test_net_websocket.ts +1 -1
- package/test/test_react_complex.js +1465 -510
- package/test/test_react_counter.js +1455 -500
- package/test/test_react_flex.js +1475 -513
- package/test/test_react_hidden.js +1458 -503
- package/test/test_react_listview_link.js +1455 -500
- package/test/test_react_notify.js +18534 -0
- package/test/test_react_render.js +1455 -500
- package/test/test_wolfssl_read.js +3 -2
- package/test/test_wolfssl_read.ts +3 -1
- package/test/test_worker_net.js +35 -17
- package/test/test_worker_net.ts +27 -15
- package/tsconfig.json +1 -1
- package/examples/exec_server.ts +0 -80
package/lib/http-server.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import './polyfill.js'
|
|
2
2
|
import * as sock from 'sock'
|
|
3
3
|
import { HeadersImpl, RequestImpl, ResponseImpl, _readStream } from './http-common.js'
|
|
4
|
+
import { AbortController } from './abort.js'
|
|
4
5
|
|
|
5
6
|
type SockHandle = import('sock').SockHandle
|
|
6
7
|
|
|
@@ -36,10 +37,14 @@ interface Conn {
|
|
|
36
37
|
pending: PendingReq | null
|
|
37
38
|
current: ActiveReq | null
|
|
38
39
|
outQueue: Uint8Array | null
|
|
40
|
+
resumeRead: (() => void) | null
|
|
39
41
|
closed: boolean
|
|
40
42
|
closeAfterFlush: boolean
|
|
41
43
|
keepAlive: boolean
|
|
42
44
|
owner: HttpServer
|
|
45
|
+
/** AbortController for the request currently served on this connection,
|
|
46
|
+
* aborted when the client disconnects mid-response. */
|
|
47
|
+
ac: AbortController | null
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
const STATUS_TEXT: Record<number, string> = {
|
|
@@ -74,6 +79,20 @@ function findCrLfCrLf(buf: Uint8Array): number {
|
|
|
74
79
|
return -1
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
/** Flatten a `HeadersInit` into `[name, value]` pairs, preserving the
|
|
83
|
+
* casing the caller provided (Node `addTrailers` semantics). */
|
|
84
|
+
function headersInitToPairs(init: HeadersInit): [string, string][] {
|
|
85
|
+
if (init instanceof HeadersImpl) {
|
|
86
|
+
const out: [string, string][] = []
|
|
87
|
+
for (const [name, value] of init) out.push([name, value])
|
|
88
|
+
return out
|
|
89
|
+
}
|
|
90
|
+
if (Array.isArray(init)) return init.map(([name, value]) => [name, String(value)])
|
|
91
|
+
const out: [string, string][] = []
|
|
92
|
+
for (const name of Object.keys(init)) out.push([name, String((init as Record<string, string>)[name])])
|
|
93
|
+
return out
|
|
94
|
+
}
|
|
95
|
+
|
|
77
96
|
/** Decode a chunked-transfer body starting at `start`. Returns the decoded
|
|
78
97
|
* data and the offset just past the terminating CRLF, or null if the buffer
|
|
79
98
|
* does not yet contain the complete chunked body. */
|
|
@@ -196,10 +215,12 @@ export class HttpServer {
|
|
|
196
215
|
pending: null,
|
|
197
216
|
current: null,
|
|
198
217
|
outQueue: null,
|
|
218
|
+
resumeRead: null,
|
|
199
219
|
closed: false,
|
|
200
220
|
closeAfterFlush: false,
|
|
201
221
|
keepAlive: true,
|
|
202
222
|
owner: this,
|
|
223
|
+
ac: null,
|
|
203
224
|
}
|
|
204
225
|
this.conns.set(c.sock, c)
|
|
205
226
|
sock.set_on_event(c.sock, (event: NetEvent) => this.onConnEvent(c, event))
|
|
@@ -212,7 +233,8 @@ export class HttpServer {
|
|
|
212
233
|
if (event.lNetworkEvents & sock.FdEvent.FD_WRITE) flushQueue(c)
|
|
213
234
|
if (event.lNetworkEvents & sock.FdEvent.FD_CLOSE) {
|
|
214
235
|
onRead(c)
|
|
215
|
-
|
|
236
|
+
abortConn(c)
|
|
237
|
+
closeConn(c)
|
|
216
238
|
}
|
|
217
239
|
}
|
|
218
240
|
}
|
|
@@ -233,48 +255,137 @@ function onRead(c: Conn): void {
|
|
|
233
255
|
if (!c.closed) tryProcess(c)
|
|
234
256
|
}
|
|
235
257
|
|
|
236
|
-
|
|
237
|
-
|
|
258
|
+
/** Send `data`, or queue it when the kernel send buffer is full.
|
|
259
|
+
* Returns true when the data was fully handed to the kernel, or false
|
|
260
|
+
* when it has been buffered (or partially handed over) and the caller
|
|
261
|
+
* should back off until `flushQueue` drains and calls `c.resumeRead`. */
|
|
262
|
+
function queueSend(c: Conn, data: Uint8Array): boolean {
|
|
263
|
+
if (c.closed || data.length === 0) return true
|
|
238
264
|
if (c.outQueue && c.outQueue.length > 0) {
|
|
239
265
|
c.outQueue = concatU8([c.outQueue, data])
|
|
240
|
-
return
|
|
266
|
+
return false
|
|
241
267
|
}
|
|
242
268
|
const ret = sock.send(c.sock, toArrayBuffer(data))
|
|
243
269
|
if (ret < 0) {
|
|
244
|
-
c
|
|
245
|
-
|
|
270
|
+
abortConn(c)
|
|
271
|
+
closeConn(c)
|
|
272
|
+
return true
|
|
246
273
|
}
|
|
247
274
|
if (ret < data.length) {
|
|
248
275
|
c.outQueue = data.subarray(ret)
|
|
249
|
-
return
|
|
276
|
+
return false
|
|
250
277
|
}
|
|
251
|
-
|
|
278
|
+
return true
|
|
252
279
|
}
|
|
253
280
|
|
|
254
281
|
function flushQueue(c: Conn): void {
|
|
255
282
|
if (c.closed) return
|
|
256
283
|
if (!c.outQueue || c.outQueue.length === 0) {
|
|
257
284
|
if (c.closeAfterFlush) closeConn(c)
|
|
285
|
+
else resumeReader(c)
|
|
258
286
|
return
|
|
259
287
|
}
|
|
260
288
|
const q = c.outQueue
|
|
261
289
|
const ret = sock.send(c.sock, toArrayBuffer(q))
|
|
262
|
-
if (ret < 0)
|
|
290
|
+
if (ret < 0) {
|
|
291
|
+
abortConn(c)
|
|
292
|
+
closeConn(c)
|
|
293
|
+
return
|
|
294
|
+
}
|
|
263
295
|
if (ret < q.length) {
|
|
264
296
|
c.outQueue = q.subarray(ret)
|
|
265
297
|
return
|
|
266
298
|
}
|
|
267
299
|
c.outQueue = null
|
|
268
300
|
if (c.closeAfterFlush) closeConn(c)
|
|
301
|
+
else resumeReader(c)
|
|
269
302
|
}
|
|
270
303
|
|
|
271
304
|
function closeConn(c: Conn): void {
|
|
272
305
|
if (c.closed) return
|
|
273
306
|
c.closed = true
|
|
307
|
+
resumeReader(c)
|
|
274
308
|
if (c.sock >= 0) sock.closesocket(c.sock)
|
|
275
309
|
c.owner.conns.delete(c.sock)
|
|
276
310
|
}
|
|
277
311
|
|
|
312
|
+
/** Abort the in-flight request (notifying its handler via `req.signal`).
|
|
313
|
+
* Safe to call when idle or already aborted: the controller is cleared so
|
|
314
|
+
* a fresh request on this connection gets a fresh signal. */
|
|
315
|
+
function abortConn(c: Conn): void {
|
|
316
|
+
const ac = c.ac
|
|
317
|
+
c.ac = null
|
|
318
|
+
if (ac) ac.abort()
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Resolve the pending back-pressure awaiter, if any. Called whenever the
|
|
322
|
+
* send queue drains or the connection closes. */
|
|
323
|
+
function resumeReader(c: Conn): void {
|
|
324
|
+
const r = c.resumeRead
|
|
325
|
+
c.resumeRead = null
|
|
326
|
+
if (r) r()
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Close the connection once a response is fully flushed, when the
|
|
330
|
+
* handler or client asked for `Connection: close`. */
|
|
331
|
+
function finishResponse(c: Conn): void {
|
|
332
|
+
if (c.closeAfterFlush && !c.outQueue) closeConn(c)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Frame `data` as a single chunked-transfer block: `<hex size>\r\n<data>\r\n`. */
|
|
336
|
+
function encodeChunkFrame(data: Uint8Array, enc: TextEncoder): Uint8Array {
|
|
337
|
+
return concatU8([enc.encode(data.byteLength.toString(16) + '\r\n'), data, enc.encode('\r\n')])
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Read `body` one chunk at a time and send it out. When the kernel send
|
|
341
|
+
* buffer fills up (`queueSend` returns false) we pause on `c.resumeRead`,
|
|
342
|
+
* which `flushQueue` resumes once the socket drains — backpressure. */
|
|
343
|
+
async function writeStreamingBody(c: Conn, body: ReadableStream<Uint8Array> | null, chunked: boolean, trailer: Promise<HeadersInit> | undefined): Promise<void> {
|
|
344
|
+
if (c.closed || body == null) return
|
|
345
|
+
const reader = body.getReader()
|
|
346
|
+
const enc = new TextEncoder()
|
|
347
|
+
try {
|
|
348
|
+
while (!c.closed) {
|
|
349
|
+
const { done, value } = await reader.read()
|
|
350
|
+
if (done) break
|
|
351
|
+
if (value && value.byteLength > 0) {
|
|
352
|
+
const frame = chunked ? encodeChunkFrame(value, enc) : value
|
|
353
|
+
if (!queueSend(c, frame)) {
|
|
354
|
+
await new Promise<void>((resolve) => { c.resumeRead = resolve })
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (c.closed) {
|
|
359
|
+
// 客户端断开(FD_CLOSE / send 失败):取消响应体,让 producer 感知
|
|
360
|
+
//(exec_server 借此 kill 子进程),而不是干等它自然结束。
|
|
361
|
+
try { await reader.cancel() } catch (e) {
|
|
362
|
+
console.error('[http-server] body cancel error:', e)
|
|
363
|
+
}
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
if (chunked) {
|
|
367
|
+
let terminator = '0\r\n\r\n'
|
|
368
|
+
if (trailer) {
|
|
369
|
+
try {
|
|
370
|
+
const pairs = headersInitToPairs(await trailer)
|
|
371
|
+
if (pairs.length > 0) {
|
|
372
|
+
let s = '0\r\n'
|
|
373
|
+
for (const [name, value] of pairs) s += name + ': ' + value + '\r\n'
|
|
374
|
+
s += '\r\n'
|
|
375
|
+
terminator = s
|
|
376
|
+
}
|
|
377
|
+
} catch (e) {
|
|
378
|
+
console.error('[http-server] trailers promise rejected:', e)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
queueSend(c, enc.encode(terminator))
|
|
382
|
+
}
|
|
383
|
+
finishResponse(c)
|
|
384
|
+
} finally {
|
|
385
|
+
reader.releaseLock()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
278
389
|
// ── Request state machine ──
|
|
279
390
|
|
|
280
391
|
function whole(c: Conn): Uint8Array {
|
|
@@ -355,12 +466,15 @@ function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: n
|
|
|
355
466
|
const isAbsolute = parsed.target.startsWith('http://') || parsed.target.startsWith('https://')
|
|
356
467
|
const url = isAbsolute ? parsed.target : 'http://' + host + parsed.target
|
|
357
468
|
|
|
469
|
+
const ac = new AbortController()
|
|
358
470
|
const req = new RequestImpl(url, {
|
|
359
471
|
method: parsed.method,
|
|
360
472
|
headers: new HeadersImpl(parsed.headers),
|
|
361
473
|
body: body.length > 0 ? body : undefined,
|
|
474
|
+
signal: ac.signal,
|
|
362
475
|
})
|
|
363
476
|
|
|
477
|
+
c.ac = ac
|
|
364
478
|
c.current = { req }
|
|
365
479
|
trimParts(c, whole(c), end)
|
|
366
480
|
void handleRequest(c)
|
|
@@ -385,40 +499,77 @@ async function handleRequest(c: Conn): Promise<void> {
|
|
|
385
499
|
}
|
|
386
500
|
|
|
387
501
|
if (c.closed) return
|
|
388
|
-
|
|
502
|
+
try {
|
|
503
|
+
await sendResponse(c, req.method, res)
|
|
504
|
+
} catch (e) {
|
|
505
|
+
console.error('[http-server] response send error:', e)
|
|
506
|
+
if (!c.closed) closeConn(c)
|
|
507
|
+
return
|
|
508
|
+
}
|
|
389
509
|
if (c.closed) return
|
|
390
510
|
|
|
391
511
|
c.current = null
|
|
392
512
|
if (!c.closeAfterFlush) tryProcess(c)
|
|
393
513
|
}
|
|
394
514
|
|
|
515
|
+
function buildHead(status: number, statusText: string, headers: HeadersImpl): Uint8Array {
|
|
516
|
+
let head = 'HTTP/1.1 ' + status + ' ' + statusText + '\r\n'
|
|
517
|
+
for (const [name, value] of headers) head += name + ': ' + value + '\r\n'
|
|
518
|
+
head += '\r\n'
|
|
519
|
+
return new TextEncoder().encode(head)
|
|
520
|
+
}
|
|
521
|
+
|
|
395
522
|
async function sendResponse(c: Conn, method: string, res: Response): Promise<void> {
|
|
396
523
|
const status = res.status
|
|
397
524
|
const statusText = res.statusText || STATUS_TEXT[status] || 'OK'
|
|
398
525
|
const headers = new HeadersImpl(res.headers)
|
|
399
526
|
|
|
400
|
-
let body: Uint8Array
|
|
401
|
-
try {
|
|
402
|
-
body = await _readStream(res.body)
|
|
403
|
-
} catch {
|
|
404
|
-
body = new Uint8Array(0)
|
|
405
|
-
}
|
|
406
|
-
if (c.closed) return
|
|
407
|
-
|
|
408
527
|
const headOnly = method === 'HEAD'
|
|
409
528
|
const bodyless = (status >= 100 && status < 200) || status === 204 || status === 304
|
|
410
529
|
|
|
411
|
-
if (!headers.has('content-length') && !bodyless) headers.set('Content-Length', String(body.length))
|
|
412
530
|
if (!headers.has('connection')) headers.set('Connection', c.keepAlive ? 'keep-alive' : 'close')
|
|
413
|
-
|
|
414
531
|
const connHdr = (headers.get('connection') || '').toLowerCase()
|
|
415
532
|
if (!c.keepAlive || connHdr === 'close') c.closeAfterFlush = true
|
|
416
533
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
534
|
+
if (bodyless) {
|
|
535
|
+
headers.delete('Transfer-Encoding')
|
|
536
|
+
headers.delete('Content-Length')
|
|
537
|
+
headers.delete('Trailer')
|
|
538
|
+
queueSend(c, buildHead(status, statusText, headers))
|
|
539
|
+
finishResponse(c)
|
|
540
|
+
return
|
|
541
|
+
}
|
|
420
542
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
543
|
+
if (headOnly) {
|
|
544
|
+
headers.delete('Transfer-Encoding')
|
|
545
|
+
headers.delete('Trailer')
|
|
546
|
+
if (!headers.has('content-length')) {
|
|
547
|
+
try {
|
|
548
|
+
const full = await _readStream(res.body)
|
|
549
|
+
headers.set('Content-Length', String(full.length))
|
|
550
|
+
} catch { /* leave length unspecified */ }
|
|
551
|
+
}
|
|
552
|
+
if (c.closed) return
|
|
553
|
+
queueSend(c, buildHead(status, statusText, headers))
|
|
554
|
+
finishResponse(c)
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Body is streamed out chunk by chunk. A declared Content-Length sends
|
|
559
|
+
// the bytes as-is; otherwise we use chunked framing.
|
|
560
|
+
const chunked = (headers.get('transfer-encoding') || '').toLowerCase() === 'chunked' ||
|
|
561
|
+
!headers.has('content-length')
|
|
562
|
+
// Trailing headers are only emitted when the body is chunk-framed and a
|
|
563
|
+
// `Trailer` declaration header is present (Node addTrailers semantics).
|
|
564
|
+
const trailerDeclared = (headers.get('trailer') || '').trim().length > 0
|
|
565
|
+
if (chunked) {
|
|
566
|
+
headers.delete('Content-Length')
|
|
567
|
+
if (!headers.has('transfer-encoding')) headers.set('Transfer-Encoding', 'chunked')
|
|
568
|
+
if (!trailerDeclared) headers.delete('Trailer')
|
|
569
|
+
} else {
|
|
570
|
+
headers.delete('Transfer-Encoding')
|
|
571
|
+
headers.delete('Trailer')
|
|
572
|
+
}
|
|
573
|
+
queueSend(c, buildHead(status, statusText, headers))
|
|
574
|
+
await writeStreamingBody(c, res.body, chunked, (chunked && trailerDeclared) ? res.trailers : undefined)
|
|
424
575
|
}
|
|
@@ -1,29 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useRef, useEffect, useState } from 'react';
|
|
3
3
|
import * as gui from 'gui';
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
function readI32(ptr, offset) {
|
|
9
|
-
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
10
|
-
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24);
|
|
11
|
-
}
|
|
12
|
-
function bufPtr(buf) {
|
|
13
|
-
return ffi.bufferPtr(buf);
|
|
14
|
-
}
|
|
4
|
+
import { InvalidateRect } from '../../windows/user32.js';
|
|
5
|
+
import { NULL } from '../../ffi/ctype.js';
|
|
6
|
+
import { SYSTEMTIME, NMDATETIMECHANGE } from '../../windows/structs.js';
|
|
7
|
+
import { nmCode } from '../nmhdr.js';
|
|
15
8
|
function dateToSysTimeBuf(d) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return buf;
|
|
9
|
+
// wDayOfWeek 设置时被忽略,缺省跳过 = 0
|
|
10
|
+
return SYSTEMTIME.encode({
|
|
11
|
+
wYear: d.getFullYear(),
|
|
12
|
+
wMonth: d.getMonth() + 1,
|
|
13
|
+
wDay: d.getDate(),
|
|
14
|
+
wHour: d.getHours(),
|
|
15
|
+
wMinute: d.getMinutes(),
|
|
16
|
+
wSecond: d.getSeconds(),
|
|
17
|
+
wMilliseconds: d.getMilliseconds(),
|
|
18
|
+
});
|
|
27
19
|
}
|
|
28
20
|
const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, defaultValue, format = 'short', allowNone, updown, style }, ref) {
|
|
29
21
|
const [internalDate, setInternalDate] = useState(defaultValue ?? null);
|
|
@@ -48,7 +40,7 @@ const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, def
|
|
|
48
40
|
const d = effectiveDate;
|
|
49
41
|
if (d) {
|
|
50
42
|
const buf = dateToSysTimeBuf(d);
|
|
51
|
-
gui.SendMessage(h, 4098 /* gui.DtMsg.SETSYSTEMTIME */, 0 /* gui.DtFlag.GDT_VALID */,
|
|
43
|
+
gui.SendMessage(h, 4098 /* gui.DtMsg.SETSYSTEMTIME */, 0 /* gui.DtFlag.GDT_VALID */, buf.ptr);
|
|
52
44
|
}
|
|
53
45
|
else {
|
|
54
46
|
gui.SendMessage(h, 4098 /* gui.DtMsg.SETSYSTEMTIME */, 1 /* gui.DtFlag.GDT_NONE */, 0);
|
|
@@ -58,7 +50,7 @@ const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, def
|
|
|
58
50
|
const h = dpRef.current;
|
|
59
51
|
if (!h)
|
|
60
52
|
return;
|
|
61
|
-
|
|
53
|
+
InvalidateRect(h, NULL, 1);
|
|
62
54
|
// 发送 WM_SIZE 让控件(尤其是 DTS_UPDOWN)重新布局内部子窗口
|
|
63
55
|
const cr = gui.GetClientRect(h);
|
|
64
56
|
if (cr) {
|
|
@@ -69,22 +61,17 @@ const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, def
|
|
|
69
61
|
}, []);
|
|
70
62
|
return (_jsx("w", { type: "STATIC", ws: 268435456 /* gui.WindowStyle.VISIBLE */ | 33554432 /* gui.WindowStyle.CLIPCHILDREN */, style: { ...style, flexDirection: 'column', alignItems: 'stretch' }, ref: ref, onEvent: (e) => {
|
|
71
63
|
if (e.msg === 78 /* gui.WmMsg.NOTIFY */) {
|
|
72
|
-
const code =
|
|
64
|
+
const code = nmCode(e.lParam);
|
|
73
65
|
if (code === -759 /* gui.DtNotifyCode.DATETIMECHANGE */) {
|
|
74
|
-
|
|
66
|
+
// NMDATETIMECHANGE: NMHDR + DWORD dwFlags + SYSTEMTIME st
|
|
67
|
+
const { dwFlags, st } = NMDATETIMECHANGE.decode(e.lParam);
|
|
75
68
|
if (dwFlags === 1 /* gui.DtFlag.GDT_NONE */) {
|
|
76
69
|
if (!isControlled)
|
|
77
70
|
setInternalDate(null);
|
|
78
71
|
onChangeRef.current?.(null);
|
|
79
72
|
}
|
|
80
73
|
else {
|
|
81
|
-
const
|
|
82
|
-
const month = readI16(e.lParam, 30);
|
|
83
|
-
const day = readI16(e.lParam, 34);
|
|
84
|
-
const hour = readI16(e.lParam, 36);
|
|
85
|
-
const min = readI16(e.lParam, 38);
|
|
86
|
-
const sec = readI16(e.lParam, 40);
|
|
87
|
-
const d = new Date(year, month - 1, day, hour, min, sec);
|
|
74
|
+
const d = new Date(st.wYear, st.wMonth - 1, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
|
88
75
|
if (!isControlled)
|
|
89
76
|
setInternalDate(d);
|
|
90
77
|
onChangeRef.current?.(d);
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useState, useRef, useEffect } from 'react';
|
|
3
3
|
import * as gui from 'gui';
|
|
4
|
-
import
|
|
5
|
-
function textToUtf16(s) {
|
|
6
|
-
const buf = new ArrayBuffer((s.length + 1) * 2);
|
|
7
|
-
const dv = new DataView(buf);
|
|
8
|
-
for (let i = 0; i < s.length; i++)
|
|
9
|
-
dv.setUint16(i * 2, s.charCodeAt(i), true);
|
|
10
|
-
return buf;
|
|
11
|
-
}
|
|
4
|
+
import { WCHAR } from '../../ffi/bind.js';
|
|
12
5
|
export const Input = forwardRef(({ value: controlledValue, defaultValue = '', onChange, placeholder, password, multiline, readonly, number, disabled, style }, ref) => {
|
|
13
6
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
14
7
|
const isControlled = controlledValue !== undefined;
|
|
@@ -27,8 +20,8 @@ export const Input = forwardRef(({ value: controlledValue, defaultValue = '', on
|
|
|
27
20
|
const h = inputRef.current;
|
|
28
21
|
if (!h || !placeholder)
|
|
29
22
|
return;
|
|
30
|
-
const buf =
|
|
31
|
-
gui.SendMessage(h, 5377 /* gui.EditMsg.SETCUEBANNER */, 1,
|
|
23
|
+
const buf = WCHAR.encode(placeholder);
|
|
24
|
+
gui.SendMessage(h, 5377 /* gui.EditMsg.SETCUEBANNER */, 1, buf.ptr);
|
|
32
25
|
}, [placeholder]);
|
|
33
26
|
return (_jsx("w", { type: "EDIT", text: displayValue, ws: ws, style: style, disabled: disabled, ref: (h) => {
|
|
34
27
|
inputRef.current = h;
|
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { forwardRef, useRef, useEffect } from 'react';
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24);
|
|
8
|
-
}
|
|
9
|
-
function readUtf16(ptr, offset, maxWords) {
|
|
10
|
-
const chars = [];
|
|
11
|
-
for (let i = 0; i < maxWords; i++) {
|
|
12
|
-
const lo = ffi.readByte(ptr + offset + i * 2);
|
|
13
|
-
const hi = ffi.readByte(ptr + offset + i * 2 + 1);
|
|
14
|
-
const code = (hi << 8) | lo;
|
|
15
|
-
if (code === 0)
|
|
16
|
-
break;
|
|
17
|
-
chars.push(String.fromCharCode(code));
|
|
18
|
-
}
|
|
19
|
-
return chars.join('');
|
|
20
|
-
}
|
|
3
|
+
import { InvalidateRect } from '../../windows/user32.js';
|
|
4
|
+
import { NULL } from '../../ffi/ctype.js';
|
|
5
|
+
import { NMLINK } from '../../windows/structs.js';
|
|
6
|
+
import { nmCode } from '../nmhdr.js';
|
|
21
7
|
const Link = forwardRef(function Link({ href, children, onClick, style }, ref) {
|
|
22
8
|
const displayText = children != null ? String(children) : (href ?? '');
|
|
23
9
|
const linkText = href ? `<A HREF="${href}">${displayText}</A>` : displayText;
|
|
@@ -28,14 +14,13 @@ const Link = forwardRef(function Link({ href, children, onClick, style }, ref) {
|
|
|
28
14
|
const h = linkRef.current;
|
|
29
15
|
if (!h)
|
|
30
16
|
return;
|
|
31
|
-
|
|
17
|
+
InvalidateRect(h, NULL, 1);
|
|
32
18
|
}, [linkText]);
|
|
33
19
|
return (_jsx("w", { type: "STATIC", ws: 268435456 /* gui.WindowStyle.VISIBLE */ | 33554432 /* gui.WindowStyle.CLIPCHILDREN */, style: { ...style, flexDirection: 'column', alignItems: 'stretch' }, ref: ref, onEvent: (e) => {
|
|
34
20
|
if (e.msg === 78 /* gui.WmMsg.NOTIFY */) {
|
|
35
|
-
const code =
|
|
21
|
+
const code = nmCode(e.lParam);
|
|
36
22
|
if (code === -2 /* gui.SysLinkNotifyCode.CLICK */ || code === -4 /* gui.SysLinkNotifyCode.RETURN */) {
|
|
37
|
-
|
|
38
|
-
onClickRef.current?.(url);
|
|
23
|
+
onClickRef.current?.(NMLINK.decode(e.lParam).item.szUrl);
|
|
39
24
|
}
|
|
40
25
|
}
|
|
41
26
|
}, children: _jsx("w", { type: "SysLink", ws: 268435456 /* gui.WindowStyle.VISIBLE */ | 65536 /* gui.WindowStyle.TABSTOP */, text: linkText, style: { flexGrow: 1 }, ref: (h) => { linkRef.current = h; } }) }));
|