quickwin 1.2.1 → 1.3.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 +2 -1
- package/README.md +2 -1
- package/examples/PdfCanvas.js +3 -3
- package/examples/PdfCanvas.tsx +3 -3
- package/examples/pdf_preview2.js +34 -19
- package/examples/pdf_preview2.ts +34 -20
- package/examples/pdf_viewer.js +959 -219
- package/examples/pdf_viewer.tsx +29 -19
- package/examples/test_gallery.js +925 -198
- package/examples/test_react_lazy.js +908 -181
- package/examples/test_react_listbox.js +908 -181
- package/examples/test_root_window.js +908 -181
- package/examples/test_tab.js +908 -181
- package/lib/ffi-bind.js +85 -0
- package/lib/ffi-bind.ts +170 -0
- package/lib/ffi-struct.js +224 -0
- package/lib/ffi-struct.ts +239 -0
- package/lib/react-qw/components/DateTimePicker.js +21 -15
- package/lib/react-qw/components/Link.js +4 -6
- package/lib/react-qw/components/ListView.js +84 -40
- package/lib/react-qw/components/PathPicker.js +40 -24
- package/lib/react-qw/components/Tab.js +21 -10
- package/lib/react-qw/components/Tooltip.js +27 -15
- package/lib/react-qw/components/TreeView.js +23 -33
- package/lib/react-qw/nmhdr.js +26 -0
- package/lib/react-qw/nmhdr.ts +32 -0
- package/lib/text-measure.js +14 -26
- package/lib/text-measure.ts +14 -26
- package/lib/win-common-structs.js +39 -0
- package/lib/win-common-structs.ts +41 -0
- package/package.json +1 -1
- package/quickwin.d.ts +6 -2
- 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/run.js +2 -0
- package/test/run.ts +2 -0
- package/test/test_ffi.js +12 -8
- package/test/test_ffi.ts +12 -12
- package/test/test_ffi_bind.js +42 -0
- package/test/test_ffi_bind.ts +46 -0
- package/test/test_ffi_struct.js +201 -0
- package/test/test_ffi_struct.ts +223 -0
- package/test/test_react_complex.js +911 -184
- package/test/test_react_counter.js +908 -181
- package/test/test_react_flex.js +908 -181
- package/test/test_react_hidden.js +910 -183
- package/test/test_react_listview_link.js +908 -181
- package/test/test_react_render.js +908 -181
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import * as os from 'os'
|
|
2
|
+
import { normKind, type Norm } from './ffi-bind.js'
|
|
3
|
+
|
|
4
|
+
// 声明式结构体定义:struct({ field: 'i32', p: 'ptr', ws: 'wstr[32]', child: SubStruct })
|
|
5
|
+
// layout 采用 MSVC 对齐语义:
|
|
6
|
+
// i8/u8→1 i16/u16→2 i32/u32/f32→4 i64/u64/f64→8 ptr→arch 宽(4/8)
|
|
7
|
+
// struct 对齐 = 最大字段对齐,总尺寸末尾补齐
|
|
8
|
+
// 字段类型:
|
|
9
|
+
// 标量 kind(含 ffi-bind 的 C typedef 别名:int/DWORD/LPARAM/HANDLE...)
|
|
10
|
+
// 定长数组 'u16[8]' / 'i32[4]' / 'wstr[32]'(wstr 读 string、写 string,自动截断+补 0)
|
|
11
|
+
// 嵌套 struct:字段值直接传子 struct 定义对象
|
|
12
|
+
|
|
13
|
+
type FieldKind = 'u8' | 'i8' | 'u16' | 'i16' | 'u32' | 'i32' | 'u64' | 'i64' | 'f32' | 'f64' | 'ptr' | 'wstr'
|
|
14
|
+
|
|
15
|
+
const PTR = os.arch === 'x64' ? 8 : 4
|
|
16
|
+
|
|
17
|
+
// kind → [align, size]
|
|
18
|
+
const AS: Record<FieldKind, [number, number]> = {
|
|
19
|
+
u8: [1, 1], i8: [1, 1],
|
|
20
|
+
u16: [2, 2], i16: [2, 2],
|
|
21
|
+
u32: [4, 4], i32: [4, 4],
|
|
22
|
+
u64: [8, 8], i64: [8, 8],
|
|
23
|
+
f32: [4, 4], f64: [8, 8],
|
|
24
|
+
ptr: [PTR, PTR],
|
|
25
|
+
wstr: [2, 2], // 仅定长数组元素用(wchar)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type KnownScalar = 'u8' | 'i8' | 'u16' | 'i16' | 'u32' | 'i32' | 'u64' | 'i64' | 'f32' | 'f64' | 'ptr'
|
|
29
|
+
|
|
30
|
+
type ValOf<S extends string> = S extends `${infer B}[${number}]`
|
|
31
|
+
? (B extends 'wstr' ? string : Norm<B> extends KnownScalar ? number[] : never)
|
|
32
|
+
: S extends KnownScalar ? number
|
|
33
|
+
: Norm<S> extends KnownScalar ? number : never
|
|
34
|
+
|
|
35
|
+
export type StructShape<T extends Record<string, unknown> = Record<string, unknown>> = {
|
|
36
|
+
readonly __fields: T
|
|
37
|
+
readonly size: number
|
|
38
|
+
readonly structAlign: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ShapeOf<T extends Record<string, unknown>> = {
|
|
42
|
+
[K in keyof T]: T[K] extends string ? ValOf<T[K]> : T[K] extends StructShape ? ShapeOf<T[K]['__fields']> : never
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type StructDef<F extends Record<string, string | StructShape>> = StructShape<F> & {
|
|
46
|
+
read(buf: ArrayBuffer, offset?: number): ShapeOf<F>
|
|
47
|
+
write(v: ShapeOf<F>, buf?: ArrayBuffer, offset?: number): ArrayBuffer
|
|
48
|
+
offsetOf(name: keyof F): number
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Field = {
|
|
52
|
+
name: string
|
|
53
|
+
base: FieldKind
|
|
54
|
+
count: number
|
|
55
|
+
offset: number
|
|
56
|
+
child?: StructShape
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function alignUp(v: number, a: number): number {
|
|
60
|
+
return (v + a - 1) & ~(a - 1)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseField(f: string): { base: FieldKind; count: number } {
|
|
64
|
+
const arr = /^(.+)\[(\d+)\]$/.exec(f)
|
|
65
|
+
if (arr) {
|
|
66
|
+
const b = normKind(arr[1]!)
|
|
67
|
+
if (b === 'void') throw new Error(`ffi-struct: "void" cannot be an array base`)
|
|
68
|
+
return { base: b as FieldKind, count: Number(arr[2]) }
|
|
69
|
+
}
|
|
70
|
+
const b = normKind(f)
|
|
71
|
+
if (b === 'void' || b === 'wstr') throw new Error(`ffi-struct: invalid scalar field type "${f}"`)
|
|
72
|
+
return { base: b as FieldKind, count: 1 }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function layout<F extends Record<string, string | StructShape>>(def: F): { fields: Field[]; size: number; structAlign: number } {
|
|
76
|
+
const fields: Field[] = []
|
|
77
|
+
let cursor = 0
|
|
78
|
+
let maxAlign = 1
|
|
79
|
+
for (const key of Object.keys(def) as (keyof F)[]) {
|
|
80
|
+
const v = def[key]
|
|
81
|
+
let a: number
|
|
82
|
+
let w: number
|
|
83
|
+
if (typeof v === 'string') {
|
|
84
|
+
const { base, count } = parseField(v)
|
|
85
|
+
const [align, elem] = AS[base]
|
|
86
|
+
a = align
|
|
87
|
+
w = count >= 1 ? elem * count : elem
|
|
88
|
+
fields.push({ name: key as string, base, count, offset: 0 })
|
|
89
|
+
} else {
|
|
90
|
+
const child = v as StructShape
|
|
91
|
+
a = child.structAlign
|
|
92
|
+
w = child.size
|
|
93
|
+
fields.push({ name: key as string, base: 'u8', count: 0, offset: 0, child })
|
|
94
|
+
}
|
|
95
|
+
const off = alignUp(cursor, a)
|
|
96
|
+
const f = fields[fields.length - 1]!
|
|
97
|
+
f.offset = off
|
|
98
|
+
cursor = off + w
|
|
99
|
+
if (a > maxAlign) maxAlign = a
|
|
100
|
+
}
|
|
101
|
+
return { fields, size: alignUp(cursor, maxAlign), structAlign: maxAlign }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const _layoutCache = new WeakMap<object, { fields: Field[]; size: number; structAlign: number }>()
|
|
105
|
+
|
|
106
|
+
function buildLayout<F extends Record<string, string | StructShape>>(def: F): { fields: Field[]; size: number; structAlign: number } {
|
|
107
|
+
let hit = _layoutCache.get(def)
|
|
108
|
+
if (hit) return hit
|
|
109
|
+
hit = layout(def)
|
|
110
|
+
_layoutCache.set(def, hit)
|
|
111
|
+
return hit
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readScalar(dv: DataView, off: number, k: FieldKind): number {
|
|
115
|
+
switch (k) {
|
|
116
|
+
case 'u8': return dv.getUint8(off)
|
|
117
|
+
case 'i8': return dv.getInt8(off)
|
|
118
|
+
case 'u16': return dv.getUint16(off, true)
|
|
119
|
+
case 'i16': return dv.getInt16(off, true)
|
|
120
|
+
case 'u32': return dv.getUint32(off, true)
|
|
121
|
+
case 'i32': return dv.getInt32(off, true)
|
|
122
|
+
case 'u64': return Number(dv.getBigUint64(off, true))
|
|
123
|
+
case 'i64': return Number(dv.getBigInt64(off, true))
|
|
124
|
+
case 'f32': return dv.getFloat32(off, true)
|
|
125
|
+
case 'f64': return dv.getFloat64(off, true)
|
|
126
|
+
case 'ptr': return readPtr(dv, off)
|
|
127
|
+
case 'wstr': return dv.getUint16(off, true)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function readPtr(dv: DataView, off: number): number {
|
|
132
|
+
if (PTR === 8) return Number(dv.getBigUint64(off, true))
|
|
133
|
+
return dv.getUint32(off, true)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function writeScalar(dv: DataView, off: number, k: FieldKind, val: number): void {
|
|
137
|
+
switch (k) {
|
|
138
|
+
case 'u8': dv.setUint8(off, val); break
|
|
139
|
+
case 'i8': dv.setInt8(off, val); break
|
|
140
|
+
case 'u16': dv.setUint16(off, val, true); break
|
|
141
|
+
case 'i16': dv.setInt16(off, val, true); break
|
|
142
|
+
case 'u32': dv.setUint32(off, val >>> 0, true); break
|
|
143
|
+
case 'i32': dv.setInt32(off, val | 0, true); break
|
|
144
|
+
case 'u64': dv.setBigUint64(off, BigInt(Math.trunc(val)), true); break
|
|
145
|
+
case 'i64': dv.setBigInt64(off, BigInt(Math.trunc(val)), true); break
|
|
146
|
+
case 'f32': dv.setFloat32(off, val, true); break
|
|
147
|
+
case 'f64': dv.setFloat64(off, val, true); break
|
|
148
|
+
case 'ptr': writePtr(dv, off, val); break
|
|
149
|
+
case 'wstr': dv.setUint16(off, val, true); break
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function writePtr(dv: DataView, off: number, val: number): void {
|
|
154
|
+
if (PTR === 8) dv.setBigUint64(off, BigInt(Math.trunc(val)), true)
|
|
155
|
+
else dv.setUint32(off, val >>> 0, true)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readField(dv: DataView, base: number, f: Field): unknown {
|
|
159
|
+
const off = base + f.offset
|
|
160
|
+
if (f.child) return readInto(f.child, dv.buffer as ArrayBuffer, off)
|
|
161
|
+
if (f.count > 1) {
|
|
162
|
+
if (f.base === 'wstr') {
|
|
163
|
+
const arr = new Uint16Array(dv.buffer, dv.byteOffset + off, f.count)
|
|
164
|
+
let end = 0
|
|
165
|
+
while (end < arr.length && arr[end] !== 0) end++
|
|
166
|
+
let s = ''
|
|
167
|
+
for (let i = 0; i < end; i++) s += String.fromCharCode(arr[i]!)
|
|
168
|
+
return s
|
|
169
|
+
}
|
|
170
|
+
const out: number[] = []
|
|
171
|
+
const step = AS[f.base]![1]
|
|
172
|
+
for (let i = 0; i < f.count; i++) out.push(readScalar(dv, off + i * step, f.base))
|
|
173
|
+
return out
|
|
174
|
+
}
|
|
175
|
+
return readScalar(dv, off, f.base)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function writeField(dv: DataView, base: number, f: Field, v: unknown): void {
|
|
179
|
+
const off = base + f.offset
|
|
180
|
+
if (f.child) { writeInto(f.child, v as Record<string, unknown>, dv.buffer as ArrayBuffer, off); return }
|
|
181
|
+
if (f.count > 1) {
|
|
182
|
+
if (f.base === 'wstr') {
|
|
183
|
+
const s = String(v ?? '')
|
|
184
|
+
const arr = new Uint16Array(dv.buffer, dv.byteOffset + off, f.count)
|
|
185
|
+
for (let i = 0; i < arr.length; i++) arr[i] = 0
|
|
186
|
+
const n = Math.min(s.length, f.count - 1)
|
|
187
|
+
for (let i = 0; i < n; i++) arr[i] = s.charCodeAt(i)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
const step = AS[f.base]![1]
|
|
191
|
+
const list = (v ?? []) as number[]
|
|
192
|
+
for (let i = 0; i < f.count; i++) writeScalar(dv, off + i * step, f.base, list[i] ?? 0)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
writeScalar(dv, off, f.base, Number(v))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function doSize<F extends Record<string, string | StructShape>>(def: F): number {
|
|
199
|
+
return buildLayout(def).size
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readInto<S>(child: StructShape, buf: ArrayBuffer, offset: number): S {
|
|
203
|
+
const r = child as unknown as { read(b: ArrayBuffer, o: number): S }
|
|
204
|
+
return r.read(buf, offset)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function doRead<F extends Record<string, string | StructShape>>(_def: F, fields: Field[], buf: ArrayBuffer, offset: number): ShapeOf<F> {
|
|
208
|
+
const out: Record<string, unknown> = {}
|
|
209
|
+
const dv = new DataView(buf)
|
|
210
|
+
for (const f of fields) out[f.name] = readField(dv, offset, f)
|
|
211
|
+
return out as ShapeOf<F>
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function doWrite<F extends Record<string, string | StructShape>>(def: F, fields: Field[], v: ShapeOf<F>, buf?: ArrayBuffer, offset = 0): ArrayBuffer {
|
|
215
|
+
buf = buf ?? new ArrayBuffer(doSize(def))
|
|
216
|
+
const dv = new DataView(buf)
|
|
217
|
+
for (const f of fields) writeField(dv, offset, f, (v as Record<string, unknown>)[f.name])
|
|
218
|
+
return buf
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function writeInto<W>(child: StructShape, v: Record<string, unknown>, buf: ArrayBuffer, offset: number): void {
|
|
222
|
+
const w = child as unknown as { write(i: W, b: ArrayBuffer, o: number): unknown }
|
|
223
|
+
w.write(v as W, buf, offset)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function struct<const F extends Record<string, string | StructShape>>(def: F): StructDef<F> {
|
|
227
|
+
const { fields, size, structAlign } = buildLayout(def)
|
|
228
|
+
return {
|
|
229
|
+
__fields: def,
|
|
230
|
+
size,
|
|
231
|
+
structAlign,
|
|
232
|
+
read: (buf: ArrayBuffer, offset = 0) => doRead(def, fields, buf, offset),
|
|
233
|
+
write: (v, buf?: ArrayBuffer, offset = 0) => doWrite(def, fields, v, buf, offset),
|
|
234
|
+
offsetOf: (name: keyof F) => {
|
|
235
|
+
for (const f of fields) if (f.name === name) return f.offset
|
|
236
|
+
throw new Error(`ffi-struct: no field "${String(name)}"`)
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -2,13 +2,17 @@ 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
4
|
import * as ffi from 'ffi';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
import { struct } from '../../ffi-struct.js';
|
|
6
|
+
import { NMHDR, nmCode, readI32At, readU16At } from '../nmhdr.js';
|
|
7
|
+
const SYSTEMTIME = struct({
|
|
8
|
+
wYear: 'u16', wMonth: 'u16', wDayOfWeek: 'u16', wDay: 'u16',
|
|
9
|
+
wHour: 'u16', wMinute: 'u16', wSecond: 'u16', wMilliseconds: 'u16',
|
|
10
|
+
});
|
|
11
|
+
const NMDATETIMECHANGE = struct({
|
|
12
|
+
hdr: NMHDR,
|
|
13
|
+
dwFlags: 'u32',
|
|
14
|
+
st: SYSTEMTIME,
|
|
15
|
+
});
|
|
12
16
|
function bufPtr(buf) {
|
|
13
17
|
return ffi.bufferPtr(buf);
|
|
14
18
|
}
|
|
@@ -69,21 +73,23 @@ const DateTimePicker = forwardRef(function DateTimePicker({ value, onChange, def
|
|
|
69
73
|
}, []);
|
|
70
74
|
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
75
|
if (e.msg === 78 /* gui.WmMsg.NOTIFY */) {
|
|
72
|
-
const code =
|
|
76
|
+
const code = nmCode(e.lParam);
|
|
73
77
|
if (code === -759 /* gui.DtNotifyCode.DATETIMECHANGE */) {
|
|
74
|
-
|
|
78
|
+
// NMDATETIMECHANGE: NMHDR + DWORD dwFlags + SYSTEMTIME st
|
|
79
|
+
const st = NMDATETIMECHANGE.offsetOf('st');
|
|
80
|
+
const dwFlags = readI32At(e.lParam, NMDATETIMECHANGE.offsetOf('dwFlags'));
|
|
75
81
|
if (dwFlags === 1 /* gui.DtFlag.GDT_NONE */) {
|
|
76
82
|
if (!isControlled)
|
|
77
83
|
setInternalDate(null);
|
|
78
84
|
onChangeRef.current?.(null);
|
|
79
85
|
}
|
|
80
86
|
else {
|
|
81
|
-
const year =
|
|
82
|
-
const month =
|
|
83
|
-
const day =
|
|
84
|
-
const hour =
|
|
85
|
-
const min =
|
|
86
|
-
const sec =
|
|
87
|
+
const year = readU16At(e.lParam, st + 0);
|
|
88
|
+
const month = readU16At(e.lParam, st + 2);
|
|
89
|
+
const day = readU16At(e.lParam, st + 6);
|
|
90
|
+
const hour = readU16At(e.lParam, st + 8);
|
|
91
|
+
const min = readU16At(e.lParam, st + 10);
|
|
92
|
+
const sec = readU16At(e.lParam, st + 12);
|
|
87
93
|
const d = new Date(year, month - 1, day, hour, min, sec);
|
|
88
94
|
if (!isControlled)
|
|
89
95
|
setInternalDate(d);
|
|
@@ -2,10 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef, useRef, useEffect } from 'react';
|
|
3
3
|
import * as gui from 'gui';
|
|
4
4
|
import * as ffi from 'ffi';
|
|
5
|
-
|
|
6
|
-
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
7
|
-
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24);
|
|
8
|
-
}
|
|
5
|
+
import { nmCode, NMHDR_SIZE } from '../nmhdr.js';
|
|
9
6
|
function readUtf16(ptr, offset, maxWords) {
|
|
10
7
|
const chars = [];
|
|
11
8
|
for (let i = 0; i < maxWords; i++) {
|
|
@@ -32,9 +29,10 @@ const Link = forwardRef(function Link({ href, children, onClick, style }, ref) {
|
|
|
32
29
|
}, [linkText]);
|
|
33
30
|
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
31
|
if (e.msg === 78 /* gui.WmMsg.NOTIFY */) {
|
|
35
|
-
const code =
|
|
32
|
+
const code = nmCode(e.lParam);
|
|
36
33
|
if (code === -2 /* gui.SysLinkNotifyCode.CLICK */ || code === -4 /* gui.SysLinkNotifyCode.RETURN */) {
|
|
37
|
-
|
|
34
|
+
// NMLINK: NMHDR(12/20) + LITEM{ mask,iLink,state,stateMask (16B) + szID[48] (96B) } → szUrl
|
|
35
|
+
const url = readUtf16(e.lParam, NMHDR_SIZE + 112, 2048);
|
|
38
36
|
onClickRef.current?.(url);
|
|
39
37
|
}
|
|
40
38
|
}
|
|
@@ -3,6 +3,8 @@ import { forwardRef, useRef, useEffect } from 'react';
|
|
|
3
3
|
import * as gui from 'gui';
|
|
4
4
|
import * as ffi from 'ffi';
|
|
5
5
|
import * as win from 'win';
|
|
6
|
+
import { struct } from '../../ffi-struct.js';
|
|
7
|
+
import { NMHDR, PTR_SIZE, nmCode } from '../nmhdr.js';
|
|
6
8
|
export function makeColorBlock(size, bgra) {
|
|
7
9
|
const n = size * size;
|
|
8
10
|
const buf = new ArrayBuffer(n * 4);
|
|
@@ -46,16 +48,46 @@ function bufPtr(buf) {
|
|
|
46
48
|
}
|
|
47
49
|
const LV_WS = 268435456 /* gui.WindowStyle.VISIBLE */ | 8388608 /* gui.WindowStyle.BORDER */ | 2097152 /* gui.WindowStyle.VSCROLL */ | 1048576 /* gui.WindowStyle.HSCROLL */
|
|
48
50
|
| 1 /* gui.ListViewStyle.REPORT */ | 4 /* gui.ListViewStyle.SINGLESEL */;
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
// ListView 通知结构(嵌套 NMHDR 以复现 MSVC 尾 padding 传染,offsetOf 保证 ia32/x64 均正确)。
|
|
52
|
+
const NMCUSTOMDRAW = struct({
|
|
53
|
+
hdr: NMHDR,
|
|
54
|
+
dwDrawStage: 'u32', hdc: 'ptr', rc: 'i32[4]',
|
|
55
|
+
dwItemSpec: 'ptr', uItemState: 'u32', lItemlParam: 'ptr',
|
|
56
|
+
});
|
|
57
|
+
const NMLVCUSTOMDRAW = struct({
|
|
58
|
+
hdr: NMHDR,
|
|
59
|
+
dwDrawStage: 'u32', hdc: 'ptr', rc: 'i32[4]',
|
|
60
|
+
dwItemSpec: 'ptr', uItemState: 'u32', lItemlParam: 'ptr',
|
|
61
|
+
clrText: 'u32', clrTextBk: 'u32', iSubItem: 'i32', dwItemType: 'u32',
|
|
62
|
+
});
|
|
63
|
+
// NMLISTVIEW / NMITEMACTIVATE 前缀字段布局一致(iItem..uChanged)
|
|
64
|
+
const NMLISTVIEW = struct({
|
|
65
|
+
hdr: NMHDR,
|
|
66
|
+
iItem: 'i32', iSubItem: 'i32', uNewState: 'u32', uOldState: 'u32', uChanged: 'u32',
|
|
67
|
+
ptAction: 'i32[2]', lParam: 'ptr',
|
|
68
|
+
});
|
|
69
|
+
const CD_STAGE = NMCUSTOMDRAW.offsetOf('dwDrawStage');
|
|
70
|
+
const CD_HDC = NMCUSTOMDRAW.offsetOf('hdc');
|
|
71
|
+
const CD_ITEM = NMCUSTOMDRAW.offsetOf('dwItemSpec');
|
|
72
|
+
const CD_CLRTEXT = NMLVCUSTOMDRAW.offsetOf('clrText');
|
|
73
|
+
const CD_CLRTEXTBK = NMLVCUSTOMDRAW.offsetOf('clrTextBk');
|
|
74
|
+
const CD_SUBITEM = NMLVCUSTOMDRAW.offsetOf('iSubItem');
|
|
75
|
+
const NMIA_ITEM = NMLISTVIEW.offsetOf('iItem');
|
|
76
|
+
const NMIA_SUBITEM = NMLISTVIEW.offsetOf('iSubItem');
|
|
77
|
+
const NMLV_UNEW = NMLISTVIEW.offsetOf('uNewState');
|
|
78
|
+
const NMLV_UOLD = NMLISTVIEW.offsetOf('uOldState');
|
|
79
|
+
const LVITEMW = struct({
|
|
80
|
+
mask: 'u32', iItem: 'i32', iSubItem: 'i32',
|
|
81
|
+
state: 'u32', stateMask: 'u32',
|
|
82
|
+
pszText: 'ptr', cchTextMax: 'i32', iImage: 'i32', lParam: 'ptr',
|
|
83
|
+
iIndent: 'i32', iGroupId: 'i32', cColumns: 'u32',
|
|
84
|
+
puColumns: 'ptr', piColFmt: 'ptr', iGroup: 'i32',
|
|
85
|
+
});
|
|
86
|
+
const LVCOLUMNW = struct({
|
|
87
|
+
mask: 'u32', fmt: 'i32', cx: 'i32',
|
|
88
|
+
pszText: 'ptr', cchTextMax: 'i32', iSubItem: 'i32', iImage: 'i32', iOrder: 'i32',
|
|
89
|
+
cxMin: 'i32', cxDefault: 'i32', cxIdeal: 'i32',
|
|
90
|
+
});
|
|
59
91
|
const fontCache = new Map();
|
|
60
92
|
let gdi32 = null;
|
|
61
93
|
let createFontIndirectW = null;
|
|
@@ -99,7 +131,7 @@ function getCellFont(hwnd, style) {
|
|
|
99
131
|
const dv = new DataView(lf);
|
|
100
132
|
const cur = gui.SendMessage(hwnd, 49 /* gui.WmMsg.GETFONT */, 0, 0);
|
|
101
133
|
if (cur) {
|
|
102
|
-
const got = ffi.ffiCall(getObjectW, [ffi.
|
|
134
|
+
const got = ffi.ffiCall(getObjectW, [ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_SINT32, ffi.FFI_TYPE_POINTER], [cur, 92, lf], ffi.FFI_TYPE_SINT32);
|
|
103
135
|
if (!got)
|
|
104
136
|
return null;
|
|
105
137
|
}
|
|
@@ -112,9 +144,9 @@ function getCellFont(hwnd, style) {
|
|
|
112
144
|
dv.setUint8(20, 1);
|
|
113
145
|
if (style.underline)
|
|
114
146
|
dv.setUint8(21, 1);
|
|
115
|
-
const h = ffi.ffiCall(createFontIndirectW, [ffi.FFI_TYPE_POINTER], [lf], ffi.
|
|
116
|
-
fontCache.set(key, h
|
|
117
|
-
return h
|
|
147
|
+
const h = ffi.ffiCall(createFontIndirectW, [ffi.FFI_TYPE_POINTER], [lf], ffi.FFI_TYPE_POINTER);
|
|
148
|
+
fontCache.set(key, h ? h : 0);
|
|
149
|
+
return h ? h : null;
|
|
118
150
|
}
|
|
119
151
|
function handleCustomDraw(lParam, columns, data, hwnd) {
|
|
120
152
|
const stage = readU32(lParam, CD_STAGE);
|
|
@@ -134,9 +166,9 @@ function handleCustomDraw(lParam, columns, data, hwnd) {
|
|
|
134
166
|
writeU32(lParam, CD_CLRTEXTBK, style.background);
|
|
135
167
|
const hfont = getCellFont(hwnd, style);
|
|
136
168
|
if (hfont && selectObjectFn) {
|
|
137
|
-
const hdc = readU64(lParam, CD_HDC);
|
|
169
|
+
const hdc = PTR_SIZE === 8 ? readU64(lParam, CD_HDC) : readU32(lParam, CD_HDC);
|
|
138
170
|
if (hdc) {
|
|
139
|
-
ffi.ffiCall(selectObjectFn, [ffi.
|
|
171
|
+
ffi.ffiCall(selectObjectFn, [ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_POINTER], [hdc, hfont], ffi.FFI_TYPE_POINTER);
|
|
140
172
|
return 2 /* gui.CustomDrawFlag.NEWFONT */;
|
|
141
173
|
}
|
|
142
174
|
}
|
|
@@ -169,18 +201,24 @@ function resolveCellStyle(columns, data, row, colIndex) {
|
|
|
169
201
|
return style || undefined;
|
|
170
202
|
}
|
|
171
203
|
function makeLVItem(i, sub, text, image) {
|
|
172
|
-
const b = new ArrayBuffer(84);
|
|
173
|
-
const dv = new DataView(b);
|
|
174
|
-
dv.setInt32(4, i, true);
|
|
175
|
-
dv.setInt32(8, sub, true);
|
|
176
|
-
let mask = 1 /* LvItemFlag.TEXT */;
|
|
177
|
-
if (image !== undefined) {
|
|
178
|
-
mask |= 2 /* LvItemFlag.IMAGE */;
|
|
179
|
-
dv.setInt32(36, image, true);
|
|
180
|
-
}
|
|
181
|
-
dv.setUint32(0, mask, true);
|
|
182
204
|
const textBuf = textToUtf16(text);
|
|
183
|
-
|
|
205
|
+
const b = LVITEMW.write({
|
|
206
|
+
mask: 1 /* LvItemFlag.TEXT */ | (image !== undefined ? 2 /* LvItemFlag.IMAGE */ : 0),
|
|
207
|
+
iItem: i,
|
|
208
|
+
iSubItem: sub,
|
|
209
|
+
state: 0,
|
|
210
|
+
stateMask: 0,
|
|
211
|
+
pszText: bufPtr(textBuf),
|
|
212
|
+
cchTextMax: 0,
|
|
213
|
+
iImage: image ?? 0,
|
|
214
|
+
lParam: 0,
|
|
215
|
+
iIndent: 0,
|
|
216
|
+
iGroupId: 0,
|
|
217
|
+
cColumns: 0,
|
|
218
|
+
puColumns: 0,
|
|
219
|
+
piColFmt: 0,
|
|
220
|
+
iGroup: 0,
|
|
221
|
+
});
|
|
184
222
|
b.__textBuf = textBuf;
|
|
185
223
|
return b;
|
|
186
224
|
}
|
|
@@ -228,13 +266,19 @@ const ListView = forwardRef(function ListViewInner({ columns, data, style, icons
|
|
|
228
266
|
const n = columns.length;
|
|
229
267
|
for (let j = 0; j < n; j++) {
|
|
230
268
|
const titleBuf = textToUtf16(columns[j].name);
|
|
231
|
-
const lvc =
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
269
|
+
const lvc = LVCOLUMNW.write({
|
|
270
|
+
mask: 4 /* LvColumnMask.TEXT */ | 2 /* LvColumnMask.WIDTH */ | 1 /* LvColumnMask.FORMAT */,
|
|
271
|
+
fmt: alignToFmt(columns[j].align),
|
|
272
|
+
cx: columns[j].width ?? 100,
|
|
273
|
+
pszText: bufPtr(titleBuf),
|
|
274
|
+
cchTextMax: 0,
|
|
275
|
+
iSubItem: j,
|
|
276
|
+
iImage: 0,
|
|
277
|
+
iOrder: 0,
|
|
278
|
+
cxMin: 0,
|
|
279
|
+
cxDefault: 0,
|
|
280
|
+
cxIdeal: 0,
|
|
281
|
+
});
|
|
238
282
|
lvc.__titleBuf = titleBuf;
|
|
239
283
|
gui.SendMessage(h, 4193 /* gui.LvMsg.INSERTCOLUMNW */, j, bufPtr(lvc));
|
|
240
284
|
}
|
|
@@ -263,13 +307,13 @@ const ListView = forwardRef(function ListViewInner({ columns, data, style, icons
|
|
|
263
307
|
}, [data, columns, getIcon]);
|
|
264
308
|
return (_jsx("w", { type: "STATIC", ws: 268435456 /* gui.WindowStyle.VISIBLE */ | 33554432 /* gui.WindowStyle.CLIPCHILDREN */, style: { ...style, flexDirection: 'column', alignItems: 'stretch' }, ref: ref, onEvent: (e) => {
|
|
265
309
|
if (e.msg === 78 /* gui.WmMsg.NOTIFY */) {
|
|
266
|
-
const code =
|
|
310
|
+
const code = nmCode(e.lParam);
|
|
267
311
|
if (code === -12 /* gui.LvNotifyCode.CUSTOMDRAW */) {
|
|
268
312
|
return handleCustomDraw(e.lParam, columns, data, lvRef.current);
|
|
269
313
|
}
|
|
270
314
|
if (code === -100 /* gui.LvNotifyCode.ITEMCHANGING */) {
|
|
271
|
-
const uNewState = readU32(e.lParam,
|
|
272
|
-
const uOldState = readU32(e.lParam,
|
|
315
|
+
const uNewState = readU32(e.lParam, NMLV_UNEW);
|
|
316
|
+
const uOldState = readU32(e.lParam, NMLV_UOLD);
|
|
273
317
|
if ((uNewState & 2 /* LvItemState.SELECTED */) !== (uOldState & 2 /* LvItemState.SELECTED */))
|
|
274
318
|
return 1;
|
|
275
319
|
}
|
|
@@ -303,7 +347,7 @@ const ListView = forwardRef(function ListViewInner({ columns, data, style, icons
|
|
|
303
347
|
const sdv = new DataView(sbuf);
|
|
304
348
|
sdv.setInt32(0, sp[0], true);
|
|
305
349
|
sdv.setInt32(4, sp[1], true);
|
|
306
|
-
ffi.ffiCall(screenToClient, [ffi.
|
|
350
|
+
ffi.ffiCall(screenToClient, [ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_POINTER], [h, sbuf], ffi.FFI_TYPE_SINT32);
|
|
307
351
|
const lvhi = new ArrayBuffer(24);
|
|
308
352
|
const lvd = new DataView(lvhi);
|
|
309
353
|
lvd.setInt32(0, sdv.getInt32(0, true), true);
|
|
@@ -317,9 +361,9 @@ const ListView = forwardRef(function ListViewInner({ columns, data, style, icons
|
|
|
317
361
|
const style = resolveCellStyle(columns, data, iItem, iSubItem);
|
|
318
362
|
if (!style || style.cursor === undefined)
|
|
319
363
|
return;
|
|
320
|
-
const hc = ffi.ffiCall(loadCursorW, [ffi.
|
|
364
|
+
const hc = ffi.ffiCall(loadCursorW, [ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_POINTER], [0, style.cursor], ffi.FFI_TYPE_POINTER);
|
|
321
365
|
if (hc) {
|
|
322
|
-
ffi.ffiCall(setCursorFn, [ffi.
|
|
366
|
+
ffi.ffiCall(setCursorFn, [ffi.FFI_TYPE_POINTER], [hc], ffi.FFI_TYPE_POINTER);
|
|
323
367
|
return 1;
|
|
324
368
|
}
|
|
325
369
|
return;
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { forwardRef, useState, useRef } from 'react';
|
|
3
3
|
import * as ffi from 'ffi';
|
|
4
4
|
import * as win from 'win';
|
|
5
|
+
import { OPENFILENAMEW, BROWSEINFOW } from '../../win-common-structs.js';
|
|
5
6
|
const FFI_PTR = ffi.FFI_TYPE_POINTER;
|
|
6
7
|
const FFI_U32 = ffi.FFI_TYPE_UINT32;
|
|
7
8
|
let _GetOpenFileNameW = 0;
|
|
@@ -32,31 +33,42 @@ function wideToStr(buf, offset = 0) {
|
|
|
32
33
|
const nullIdx = str.indexOf('\0');
|
|
33
34
|
return nullIdx >= 0 ? str.substring(0, nullIdx) : str;
|
|
34
35
|
}
|
|
35
|
-
function setPtr(dv, off, ptr) {
|
|
36
|
-
dv.setUint32(off, ptr & 0xFFFFFFFF, true);
|
|
37
|
-
dv.setUint32(off + 4, Math.floor(ptr / 0x100000000), true);
|
|
38
|
-
}
|
|
39
36
|
function openFileDialog(owner, filter, title, multiple) {
|
|
40
37
|
if (!ensureDlls())
|
|
41
38
|
return null;
|
|
42
|
-
const structBuf = new ArrayBuffer(152);
|
|
43
|
-
const sv = new DataView(structBuf);
|
|
44
39
|
const fileBuf = new ArrayBuffer(260 * 2);
|
|
45
40
|
const filterWide = strToWide(filter);
|
|
46
41
|
const titleWide = title ? strToWide(title) : null;
|
|
47
|
-
sv.setUint32(0, 152, true);
|
|
48
|
-
setPtr(sv, 8, owner);
|
|
49
|
-
setPtr(sv, 24, ffi.bufferPtr(filterWide));
|
|
50
|
-
setPtr(sv, 48, ffi.bufferPtr(fileBuf));
|
|
51
|
-
sv.setUint32(56, 260, true);
|
|
52
42
|
let flags = 0x1000 | 0x0800 | 0x0008;
|
|
53
43
|
if (multiple)
|
|
54
44
|
flags |= 0x0200;
|
|
55
45
|
flags |= 0x80000;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
46
|
+
const ofn = OPENFILENAMEW.write({
|
|
47
|
+
lStructSize: OPENFILENAMEW.size,
|
|
48
|
+
hwndOwner: owner,
|
|
49
|
+
hInstance: 0,
|
|
50
|
+
lpstrFilter: ffi.bufferPtr(filterWide),
|
|
51
|
+
lpstrCustomFilter: 0,
|
|
52
|
+
nMaxCustFilter: 0,
|
|
53
|
+
nFilterIndex: 0,
|
|
54
|
+
lpstrFile: ffi.bufferPtr(fileBuf),
|
|
55
|
+
nMaxFile: 260,
|
|
56
|
+
lpstrFileTitle: 0,
|
|
57
|
+
nMaxFileTitle: 0,
|
|
58
|
+
lpstrInitialDir: 0,
|
|
59
|
+
lpstrTitle: titleWide ? ffi.bufferPtr(titleWide) : 0,
|
|
60
|
+
Flags: flags,
|
|
61
|
+
nFileOffset: 0,
|
|
62
|
+
nFileExtension: 0,
|
|
63
|
+
lpstrDefExt: 0,
|
|
64
|
+
lCustData: 0,
|
|
65
|
+
lpfnHook: 0,
|
|
66
|
+
lpTemplateName: 0,
|
|
67
|
+
pvReserved: 0,
|
|
68
|
+
dwReserved: 0,
|
|
69
|
+
FlagsEx: 0,
|
|
70
|
+
});
|
|
71
|
+
const ret = ffi.ffiCall(_GetOpenFileNameW, [FFI_PTR], [ofn], FFI_U32);
|
|
60
72
|
if (!ret)
|
|
61
73
|
return null;
|
|
62
74
|
if (!multiple) {
|
|
@@ -79,19 +91,23 @@ function openFileDialog(owner, filter, title, multiple) {
|
|
|
79
91
|
function openFolderDialog(owner, title) {
|
|
80
92
|
if (!ensureDlls())
|
|
81
93
|
return null;
|
|
82
|
-
const structBuf = new ArrayBuffer(64);
|
|
83
|
-
const sv = new DataView(structBuf);
|
|
84
94
|
const titleWide = title ? strToWide(title) : null;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
const bi = BROWSEINFOW.write({
|
|
96
|
+
hwndOwner: owner,
|
|
97
|
+
pidlRoot: 0,
|
|
98
|
+
pszDisplayName: 0,
|
|
99
|
+
lpszTitle: titleWide ? ffi.bufferPtr(titleWide) : 0,
|
|
100
|
+
ulFlags: 0x00000041,
|
|
101
|
+
lpfn: 0,
|
|
102
|
+
lParam: 0,
|
|
103
|
+
iImage: 0,
|
|
104
|
+
});
|
|
105
|
+
const pidl = ffi.ffiCall(_SHBrowseForFolderW, [FFI_PTR], [bi], FFI_PTR);
|
|
90
106
|
if (!pidl)
|
|
91
107
|
return null;
|
|
92
108
|
const pathBuf = new ArrayBuffer(260 * 2);
|
|
93
|
-
const ok = ffi.ffiCall(_SHGetPathFromIDListW, [ffi.
|
|
94
|
-
ffi.ffiCall(_CoTaskMemFree, [ffi.
|
|
109
|
+
const ok = ffi.ffiCall(_SHGetPathFromIDListW, [ffi.FFI_TYPE_POINTER, FFI_PTR], [pidl, pathBuf], FFI_U32);
|
|
110
|
+
ffi.ffiCall(_CoTaskMemFree, [ffi.FFI_TYPE_POINTER], [pidl], ffi.FFI_TYPE_VOID);
|
|
95
111
|
return ok ? wideToStr(pathBuf) : null;
|
|
96
112
|
}
|
|
97
113
|
export const PathPicker = forwardRef((props, ref) => {
|