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
|
@@ -2,6 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } 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
|
+
import { struct } from '../../ffi-struct.js';
|
|
6
|
+
import { nmCode } from '../nmhdr.js';
|
|
7
|
+
const TCITEMW = struct({
|
|
8
|
+
mask: 'u32',
|
|
9
|
+
dwState: 'u32',
|
|
10
|
+
dwStateMask: 'u32',
|
|
11
|
+
pszText: 'ptr',
|
|
12
|
+
cchTextMax: 'i32',
|
|
13
|
+
iImage: 'i32',
|
|
14
|
+
lParam: 'ptr',
|
|
15
|
+
});
|
|
5
16
|
function textToUtf16(s) {
|
|
6
17
|
const buf = new ArrayBuffer((s.length + 1) * 2);
|
|
7
18
|
const dv = new DataView(buf);
|
|
@@ -9,10 +20,6 @@ function textToUtf16(s) {
|
|
|
9
20
|
dv.setUint16(i * 2, s.charCodeAt(i), true);
|
|
10
21
|
return buf;
|
|
11
22
|
}
|
|
12
|
-
function readI32(ptr, offset) {
|
|
13
|
-
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
14
|
-
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24);
|
|
15
|
-
}
|
|
16
23
|
export const Tab = forwardRef(({ tabs, selectedIndex: controlledIndex, defaultSelectedIndex = 0, onChange, style }, ref) => {
|
|
17
24
|
const [internalIndex, setInternalIndex] = useState(defaultSelectedIndex);
|
|
18
25
|
const isControlled = controlledIndex !== undefined;
|
|
@@ -25,11 +32,15 @@ export const Tab = forwardRef(({ tabs, selectedIndex: controlledIndex, defaultSe
|
|
|
25
32
|
gui.SendMessage(h, 4873 /* gui.TcMsg.DELETEALLITEMS */, 0, 0);
|
|
26
33
|
for (let i = 0; i < tabs.length; i++) {
|
|
27
34
|
const titleBuf = textToUtf16(tabs[i].title);
|
|
28
|
-
const tci =
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
const tci = TCITEMW.write({
|
|
36
|
+
mask: 1 /* gui.TcItemFlag.TEXT */,
|
|
37
|
+
dwState: 0,
|
|
38
|
+
dwStateMask: 0,
|
|
39
|
+
pszText: ffi.bufferPtr(titleBuf),
|
|
40
|
+
cchTextMax: tabs[i].title.length + 1,
|
|
41
|
+
iImage: 0,
|
|
42
|
+
lParam: 0,
|
|
43
|
+
});
|
|
33
44
|
const tciPtr = ffi.bufferPtr(tci);
|
|
34
45
|
gui.SendMessage(h, 4926 /* gui.TcMsg.INSERTITEMW */, i, tciPtr);
|
|
35
46
|
}
|
|
@@ -45,7 +56,7 @@ export const Tab = forwardRef(({ tabs, selectedIndex: controlledIndex, defaultSe
|
|
|
45
56
|
const h = tabHwnd.current;
|
|
46
57
|
if (!h)
|
|
47
58
|
return;
|
|
48
|
-
const code =
|
|
59
|
+
const code = nmCode(e.lParam);
|
|
49
60
|
// SysTabControl32 在 comctl32 v6 下不发标准的 TCN_SELCHANGE (-550),
|
|
50
61
|
// 收到 NM_CLICK (-2) 或 TCN_SELCHANGING (-551) 时读实际选中项
|
|
51
62
|
if (code === -552 /* gui.TcNotifyCode.SELCHANGING */ || code === -2 /* gui.SysLinkNotifyCode.CLICK */) {
|
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
import { useRef, useEffect, Children, cloneElement } from 'react';
|
|
2
2
|
import * as gui from 'gui';
|
|
3
3
|
import * as ffi from 'ffi';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import { struct } from '../../ffi-struct.js';
|
|
5
|
+
const TTTOOLINFOW = struct({
|
|
6
|
+
cbSize: 'u32',
|
|
7
|
+
uFlags: 'u32',
|
|
8
|
+
hwnd: 'ptr',
|
|
9
|
+
uId: 'ptr',
|
|
10
|
+
rect: 'i32[4]',
|
|
11
|
+
hinst: 'ptr',
|
|
12
|
+
lpszText: 'ptr',
|
|
13
|
+
lParam: 'ptr',
|
|
14
|
+
lpReserved: 'ptr', // WinXP+ 追加字段(系统 sizeof 含之)
|
|
15
|
+
});
|
|
9
16
|
function buildToolInfo(hTarget, text) {
|
|
10
|
-
const
|
|
17
|
+
const size = TTTOOLINFOW.size;
|
|
11
18
|
const textLen = (text.length + 1) * 2;
|
|
12
|
-
const buf = new ArrayBuffer(
|
|
19
|
+
const buf = new ArrayBuffer(size + textLen);
|
|
20
|
+
TTTOOLINFOW.write({
|
|
21
|
+
cbSize: size,
|
|
22
|
+
uFlags: 16 /* gui.TtToolFlag.SUBCLASS */ | 1 /* gui.TtToolFlag.IDISHWND */,
|
|
23
|
+
hwnd: hTarget,
|
|
24
|
+
uId: hTarget,
|
|
25
|
+
rect: [0, 0, 0, 0],
|
|
26
|
+
hinst: 0,
|
|
27
|
+
lpszText: ffi.bufferPtr(buf) + size,
|
|
28
|
+
lParam: 0,
|
|
29
|
+
lpReserved: 0,
|
|
30
|
+
}, buf);
|
|
13
31
|
const dv = new DataView(buf);
|
|
14
|
-
dv.setUint32(0, TTTOOLINFO_CBSIZE, true);
|
|
15
|
-
dv.setUint32(4, 16 /* gui.TtToolFlag.SUBCLASS */ | 1 /* gui.TtToolFlag.IDISHWND */, true);
|
|
16
|
-
setQword(dv, 8, hTarget);
|
|
17
|
-
setQword(dv, 16, hTarget);
|
|
18
32
|
for (let i = 0; i < text.length; i++)
|
|
19
|
-
dv.setUint16(
|
|
20
|
-
dv.setUint16(
|
|
21
|
-
const bufPtr = ffi.bufferPtr(buf);
|
|
22
|
-
setQword(dv, 48, bufPtr + textOff);
|
|
33
|
+
dv.setUint16(size + i * 2, text.charCodeAt(i), true);
|
|
34
|
+
dv.setUint16(size + text.length * 2, 0, true);
|
|
23
35
|
return buf;
|
|
24
36
|
}
|
|
25
37
|
function Tooltip({ text, children, balloon }) {
|
|
@@ -2,6 +2,21 @@ 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
|
+
import { struct } from '../../ffi-struct.js';
|
|
6
|
+
import { nmCode } from '../nmhdr.js';
|
|
7
|
+
const TVITEM = struct({
|
|
8
|
+
mask: 'u32',
|
|
9
|
+
hItem: 'ptr',
|
|
10
|
+
state: 'u32',
|
|
11
|
+
stateMask: 'u32',
|
|
12
|
+
pszText: 'ptr',
|
|
13
|
+
cchTextMax: 'i32',
|
|
14
|
+
iImage: 'i32',
|
|
15
|
+
iSelectedImage: 'i32',
|
|
16
|
+
cChildren: 'i32',
|
|
17
|
+
lParam: 'ptr',
|
|
18
|
+
});
|
|
19
|
+
const TVINSERTSTRUCT = struct({ hParent: 'ptr', hInsertAfter: 'ptr', item: TVITEM });
|
|
5
20
|
function textToUtf16(s) {
|
|
6
21
|
const buf = new ArrayBuffer((s.length + 1) * 2);
|
|
7
22
|
const dv = new DataView(buf);
|
|
@@ -9,51 +24,26 @@ function textToUtf16(s) {
|
|
|
9
24
|
dv.setUint16(i * 2, s.charCodeAt(i), true);
|
|
10
25
|
return buf;
|
|
11
26
|
}
|
|
12
|
-
function readI32(ptr, offset) {
|
|
13
|
-
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
14
|
-
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24);
|
|
15
|
-
}
|
|
16
27
|
function bufPtr(buf) {
|
|
17
28
|
return ffi.bufferPtr(buf);
|
|
18
29
|
}
|
|
19
|
-
function setPtr(dv, offset, ptr) {
|
|
20
|
-
dv.setUint32(offset, ptr & 0xFFFFFFFF, true);
|
|
21
|
-
dv.setUint32(offset + 4, Math.floor(ptr / 0x100000000), true);
|
|
22
|
-
}
|
|
23
30
|
function buildTvItem(textPtr, cChildren) {
|
|
24
31
|
let mask = 1 /* gui.TvIfFlag.TEXT */;
|
|
25
32
|
if (cChildren > 0)
|
|
26
33
|
mask |= 64 /* gui.TvIfFlag.CHILDREN */;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
dv.setUint32(20, 0, true); // stateMask
|
|
33
|
-
setPtr(dv, 24, textPtr); // pszText
|
|
34
|
-
dv.setInt32(32, 260, true); // cchTextMax
|
|
35
|
-
dv.setInt32(36, 0, true); // iImage
|
|
36
|
-
dv.setInt32(40, 0, true); // iSelectedImage
|
|
37
|
-
dv.setInt32(44, cChildren, true); // cChildren
|
|
38
|
-
setPtr(dv, 48, 0); // lParam
|
|
39
|
-
return buf;
|
|
40
|
-
}
|
|
41
|
-
function buildTvInsertStruct(hParent, hInsertAfter, itemBuf) {
|
|
42
|
-
const buf = new ArrayBuffer(72);
|
|
43
|
-
const dv = new DataView(buf);
|
|
44
|
-
setPtr(dv, 0, hParent); // hParent (offset 0)
|
|
45
|
-
setPtr(dv, 8, hInsertAfter); // hInsertAfter (offset 8)
|
|
46
|
-
const src = new DataView(itemBuf);
|
|
47
|
-
for (let i = 0; i < 56; i += 4)
|
|
48
|
-
dv.setUint32(16 + i, src.getUint32(i, true), true);
|
|
49
|
-
return buf;
|
|
34
|
+
return TVITEM.write({
|
|
35
|
+
mask, hItem: 0, state: 0, stateMask: 0,
|
|
36
|
+
pszText: textPtr, cchTextMax: 260,
|
|
37
|
+
iImage: 0, iSelectedImage: 0, cChildren, lParam: 0,
|
|
38
|
+
});
|
|
50
39
|
}
|
|
51
40
|
function insertItems(hTree, nodes, parentHandle, hItemMap, keyMap) {
|
|
52
41
|
for (const node of nodes) {
|
|
53
42
|
const cChildren = node.children && node.children.length > 0 ? 1 : 0;
|
|
54
43
|
const textBuf = textToUtf16(node.label);
|
|
55
44
|
const itemBuf = buildTvItem(bufPtr(textBuf), cChildren);
|
|
56
|
-
const
|
|
45
|
+
const item = TVITEM.read(itemBuf);
|
|
46
|
+
const tvins = TVINSERTSTRUCT.write({ hParent: parentHandle, hInsertAfter: -65536 /* gui.TvInsertAfter.ROOT */, item });
|
|
57
47
|
const hItem = gui.SendMessage(hTree, 4402 /* gui.TvMsg.INSERTITEMW */, 0, bufPtr(tvins));
|
|
58
48
|
hItemMap.set(hItem, node);
|
|
59
49
|
if (node.key)
|
|
@@ -112,7 +102,7 @@ const TreeView = forwardRef(function TreeViewInner({ data, onSelect, defaultSele
|
|
|
112
102
|
}, [selKey]);
|
|
113
103
|
return (_jsx("w", { type: "STATIC", ws: 268435456 /* gui.WindowStyle.VISIBLE */ | 33554432 /* gui.WindowStyle.CLIPCHILDREN */, style: { ...style, flexDirection: 'column', alignItems: 'stretch' }, ref: ref, onEvent: (e) => {
|
|
114
104
|
if (e.msg === 78 /* gui.WmMsg.NOTIFY */) {
|
|
115
|
-
const code =
|
|
105
|
+
const code = nmCode(e.lParam);
|
|
116
106
|
if (code === -451 /* gui.TvNotifyCode.SELCHANGEDW */) {
|
|
117
107
|
const h = tvRef.current;
|
|
118
108
|
if (!h)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as ffi from 'ffi';
|
|
3
|
+
import { struct } from '../ffi-struct.js';
|
|
4
|
+
// 架构相关常量:32 位(ia32)/64 位(x64) Windows ABI
|
|
5
|
+
export const IS64 = os.arch === 'x64';
|
|
6
|
+
export const PTR_SIZE = IS64 ? 8 : 4;
|
|
7
|
+
// NMHDR = hwndFrom(ptr) + idFrom(ptr) + code(i32)。
|
|
8
|
+
// 用 ffi-struct 定义以复现 MSVC/嵌套结构的“尾 padding 传染”(内嵌 NMHDR 占满 24/12 字节)。
|
|
9
|
+
export const NMHDR = struct({ hwndFrom: 'ptr', idFrom: 'ptr', code: 'i32' });
|
|
10
|
+
export const NMHDR_SIZE = NMHDR.size; // 24 (x64) / 12 (ia32)
|
|
11
|
+
export const NMHDR_CODE = NMHDR.offsetOf('code'); // 16 / 8
|
|
12
|
+
export function readI32At(ptr, offset) {
|
|
13
|
+
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
14
|
+
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24);
|
|
15
|
+
}
|
|
16
|
+
export function readU32At(ptr, offset) {
|
|
17
|
+
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
18
|
+
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24) >>> 0;
|
|
19
|
+
}
|
|
20
|
+
export function readU16At(ptr, offset) {
|
|
21
|
+
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8);
|
|
22
|
+
}
|
|
23
|
+
// 读 NMHDR.code(WM_NOTIFY 通知码)
|
|
24
|
+
export function nmCode(lParam) {
|
|
25
|
+
return readI32At(lParam, NMHDR_CODE);
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as os from 'os'
|
|
2
|
+
import * as ffi from 'ffi'
|
|
3
|
+
import { struct } from '../ffi-struct.js'
|
|
4
|
+
|
|
5
|
+
// 架构相关常量:32 位(ia32)/64 位(x64) Windows ABI
|
|
6
|
+
export const IS64 = os.arch === 'x64'
|
|
7
|
+
export const PTR_SIZE = IS64 ? 8 : 4
|
|
8
|
+
|
|
9
|
+
// NMHDR = hwndFrom(ptr) + idFrom(ptr) + code(i32)。
|
|
10
|
+
// 用 ffi-struct 定义以复现 MSVC/嵌套结构的“尾 padding 传染”(内嵌 NMHDR 占满 24/12 字节)。
|
|
11
|
+
export const NMHDR = struct({ hwndFrom: 'ptr', idFrom: 'ptr', code: 'i32' })
|
|
12
|
+
export const NMHDR_SIZE = NMHDR.size // 24 (x64) / 12 (ia32)
|
|
13
|
+
export const NMHDR_CODE = NMHDR.offsetOf('code') // 16 / 8
|
|
14
|
+
|
|
15
|
+
export function readI32At(ptr: number, offset: number): number {
|
|
16
|
+
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
17
|
+
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readU32At(ptr: number, offset: number): number {
|
|
21
|
+
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8) |
|
|
22
|
+
(ffi.readByte(ptr + offset + 2) << 16) | (ffi.readByte(ptr + offset + 3) << 24) >>> 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readU16At(ptr: number, offset: number): number {
|
|
26
|
+
return ffi.readByte(ptr + offset) | (ffi.readByte(ptr + offset + 1) << 8)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 读 NMHDR.code(WM_NOTIFY 通知码)
|
|
30
|
+
export function nmCode(lParam: number): number {
|
|
31
|
+
return readI32At(lParam, NMHDR_CODE)
|
|
32
|
+
}
|
package/lib/text-measure.js
CHANGED
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
import * as ffi from 'ffi';
|
|
2
|
-
import * as os from 'os';
|
|
3
|
-
import * as win from 'win';
|
|
4
2
|
import * as gui from 'gui';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
throw new Error('Failed to load system DLLs');
|
|
15
|
-
const DrawTextW = loadProc(user32, 'DrawTextW');
|
|
16
|
-
const GetDC = loadProc(user32, 'GetDC');
|
|
17
|
-
const ReleaseDC = loadProc(user32, 'ReleaseDC');
|
|
18
|
-
const SelectObject = loadProc(gdi32, 'SelectObject');
|
|
19
|
-
// 句柄宽度随架构:ia32 上 HDC/HWND 是 32-bit,错用 UINT64 会导致 DrawTextW 参数错位、测宽为 0
|
|
20
|
-
const FFI_HND = os.arch === 'ia32' ? ffi.FFI_TYPE_UINT32 : ffi.FFI_TYPE_UINT64;
|
|
21
|
-
const FFI_S32 = ffi.FFI_TYPE_SINT32;
|
|
22
|
-
const FFI_PTR = ffi.FFI_TYPE_POINTER;
|
|
3
|
+
import { bindLib } from './ffi-bind.js';
|
|
4
|
+
const user32 = bindLib('user32.dll', {
|
|
5
|
+
DrawTextW: 'ptr wstr i32 ptr i32 -> i32',
|
|
6
|
+
GetDC: 'ptr -> ptr',
|
|
7
|
+
ReleaseDC: 'ptr ptr -> i32',
|
|
8
|
+
});
|
|
9
|
+
const gdi32 = bindLib('gdi32.dll', {
|
|
10
|
+
SelectObject: 'ptr ptr -> ptr',
|
|
11
|
+
});
|
|
23
12
|
export function measureText(hdc, text, maxWidth) {
|
|
24
|
-
const textBuf = new TextEncoder('utf-16le').encode(text + '\0').buffer;
|
|
25
13
|
const rect = new ArrayBuffer(16);
|
|
26
14
|
const dv = new DataView(rect);
|
|
27
15
|
dv.setInt32(8, maxWidth, true);
|
|
28
|
-
|
|
16
|
+
user32.DrawTextW(hdc, text, -1, rect, 1024 /* gui.DrawTextFlag.CALCRECT */);
|
|
29
17
|
return { width: dv.getInt32(8, true), height: dv.getInt32(12, true) };
|
|
30
18
|
}
|
|
31
19
|
export function getButtonIdealSize(hwnd) {
|
|
@@ -35,15 +23,15 @@ export function getButtonIdealSize(hwnd) {
|
|
|
35
23
|
return { width: dv.getInt32(0, true), height: dv.getInt32(4, true) };
|
|
36
24
|
}
|
|
37
25
|
export function measureTextForHwnd(hwnd, text) {
|
|
38
|
-
const hdc =
|
|
26
|
+
const hdc = user32.GetDC(hwnd);
|
|
39
27
|
if (!hdc)
|
|
40
28
|
return { width: 0, height: 0 };
|
|
41
29
|
const hFont = gui.SendMessage(hwnd, 49 /* gui.WmMsg.GETFONT */, 0, 0);
|
|
42
|
-
const oldFont = hFont ?
|
|
30
|
+
const oldFont = hFont ? gdi32.SelectObject(hdc, hFont) : 0;
|
|
43
31
|
const result = measureText(hdc, text, 0);
|
|
44
32
|
if (hFont)
|
|
45
|
-
|
|
46
|
-
|
|
33
|
+
gdi32.SelectObject(hdc, oldFont);
|
|
34
|
+
user32.ReleaseDC(hwnd, hdc);
|
|
47
35
|
const wr = gui.GetWindowRect(hwnd);
|
|
48
36
|
const cr = gui.GetClientRect(hwnd);
|
|
49
37
|
if (wr && cr) {
|
package/lib/text-measure.ts
CHANGED
|
@@ -1,33 +1,21 @@
|
|
|
1
1
|
import * as ffi from 'ffi'
|
|
2
|
-
import * as os from 'os'
|
|
3
|
-
import * as win from 'win'
|
|
4
2
|
import * as gui from 'gui'
|
|
3
|
+
import { bindLib } from './ffi-bind.js'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (!user32 || !gdi32) throw new Error('Failed to load system DLLs')
|
|
15
|
-
const DrawTextW = loadProc(user32, 'DrawTextW')
|
|
16
|
-
const GetDC = loadProc(user32, 'GetDC')
|
|
17
|
-
const ReleaseDC = loadProc(user32, 'ReleaseDC')
|
|
18
|
-
const SelectObject = loadProc(gdi32, 'SelectObject')
|
|
19
|
-
|
|
20
|
-
// 句柄宽度随架构:ia32 上 HDC/HWND 是 32-bit,错用 UINT64 会导致 DrawTextW 参数错位、测宽为 0
|
|
21
|
-
const FFI_HND = os.arch === 'ia32' ? ffi.FFI_TYPE_UINT32 : ffi.FFI_TYPE_UINT64
|
|
22
|
-
const FFI_S32 = ffi.FFI_TYPE_SINT32
|
|
23
|
-
const FFI_PTR = ffi.FFI_TYPE_POINTER
|
|
5
|
+
const user32 = bindLib('user32.dll', {
|
|
6
|
+
DrawTextW: 'ptr wstr i32 ptr i32 -> i32',
|
|
7
|
+
GetDC: 'ptr -> ptr',
|
|
8
|
+
ReleaseDC: 'ptr ptr -> i32',
|
|
9
|
+
})
|
|
10
|
+
const gdi32 = bindLib('gdi32.dll', {
|
|
11
|
+
SelectObject: 'ptr ptr -> ptr',
|
|
12
|
+
})
|
|
24
13
|
|
|
25
14
|
export function measureText(hdc: number, text: string, maxWidth: number): { width: number; height: number } {
|
|
26
|
-
const textBuf = new TextEncoder('utf-16le').encode(text + '\0').buffer
|
|
27
15
|
const rect = new ArrayBuffer(16)
|
|
28
16
|
const dv = new DataView(rect)
|
|
29
17
|
dv.setInt32(8, maxWidth, true)
|
|
30
|
-
|
|
18
|
+
user32.DrawTextW(hdc, text, -1, rect, gui.DrawTextFlag.CALCRECT)
|
|
31
19
|
return { width: dv.getInt32(8, true), height: dv.getInt32(12, true) }
|
|
32
20
|
}
|
|
33
21
|
|
|
@@ -39,13 +27,13 @@ export function getButtonIdealSize(hwnd: gui.HWND): { width: number; height: num
|
|
|
39
27
|
}
|
|
40
28
|
|
|
41
29
|
export function measureTextForHwnd(hwnd: gui.HWND, text: string): { width: number; height: number } {
|
|
42
|
-
const hdc =
|
|
30
|
+
const hdc = user32.GetDC(hwnd)
|
|
43
31
|
if (!hdc) return { width: 0, height: 0 }
|
|
44
32
|
const hFont = gui.SendMessage(hwnd, gui.WmMsg.GETFONT, 0, 0)
|
|
45
|
-
const oldFont = hFont ?
|
|
33
|
+
const oldFont = hFont ? gdi32.SelectObject(hdc, hFont) : 0
|
|
46
34
|
const result = measureText(hdc, text, 0)
|
|
47
|
-
if (hFont)
|
|
48
|
-
|
|
35
|
+
if (hFont) gdi32.SelectObject(hdc, oldFont)
|
|
36
|
+
user32.ReleaseDC(hwnd, hdc)
|
|
49
37
|
const wr = gui.GetWindowRect(hwnd)
|
|
50
38
|
const cr = gui.GetClientRect(hwnd)
|
|
51
39
|
if (wr && cr) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { struct } from './ffi-struct.js';
|
|
2
|
+
// 跨组件/示例共享的 Windows 结构体(ffi-struct 按本机架构 MSVC 对齐布局)。
|
|
3
|
+
// OPENFILENAMEW(GetOpenFileNameW)。Win2000+ 定义含尾部 pvReserved/dwReserved/FlagsEx(即 XP 系统上的真实 sizeof=152/88):
|
|
4
|
+
export const OPENFILENAMEW = struct({
|
|
5
|
+
lStructSize: 'u32',
|
|
6
|
+
hwndOwner: 'ptr',
|
|
7
|
+
hInstance: 'ptr',
|
|
8
|
+
lpstrFilter: 'ptr',
|
|
9
|
+
lpstrCustomFilter: 'ptr',
|
|
10
|
+
nMaxCustFilter: 'u32',
|
|
11
|
+
nFilterIndex: 'u32',
|
|
12
|
+
lpstrFile: 'ptr',
|
|
13
|
+
nMaxFile: 'u32',
|
|
14
|
+
lpstrFileTitle: 'ptr',
|
|
15
|
+
nMaxFileTitle: 'u32',
|
|
16
|
+
lpstrInitialDir: 'ptr',
|
|
17
|
+
lpstrTitle: 'ptr',
|
|
18
|
+
Flags: 'u32',
|
|
19
|
+
nFileOffset: 'u16',
|
|
20
|
+
nFileExtension: 'u16',
|
|
21
|
+
lpstrDefExt: 'ptr',
|
|
22
|
+
lCustData: 'ptr',
|
|
23
|
+
lpfnHook: 'ptr',
|
|
24
|
+
lpTemplateName: 'ptr',
|
|
25
|
+
pvReserved: 'ptr',
|
|
26
|
+
dwReserved: 'u32',
|
|
27
|
+
FlagsEx: 'u32',
|
|
28
|
+
});
|
|
29
|
+
// BROWSEINFOW(SHBrowseForFolderW):
|
|
30
|
+
export const BROWSEINFOW = struct({
|
|
31
|
+
hwndOwner: 'ptr',
|
|
32
|
+
pidlRoot: 'ptr',
|
|
33
|
+
pszDisplayName: 'ptr',
|
|
34
|
+
lpszTitle: 'ptr',
|
|
35
|
+
ulFlags: 'u32',
|
|
36
|
+
lpfn: 'ptr',
|
|
37
|
+
lParam: 'ptr',
|
|
38
|
+
iImage: 'i32',
|
|
39
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { struct } from './ffi-struct.js'
|
|
2
|
+
|
|
3
|
+
// 跨组件/示例共享的 Windows 结构体(ffi-struct 按本机架构 MSVC 对齐布局)。
|
|
4
|
+
// OPENFILENAMEW(GetOpenFileNameW)。Win2000+ 定义含尾部 pvReserved/dwReserved/FlagsEx(即 XP 系统上的真实 sizeof=152/88):
|
|
5
|
+
export const OPENFILENAMEW = struct({
|
|
6
|
+
lStructSize: 'u32',
|
|
7
|
+
hwndOwner: 'ptr',
|
|
8
|
+
hInstance: 'ptr',
|
|
9
|
+
lpstrFilter: 'ptr',
|
|
10
|
+
lpstrCustomFilter: 'ptr',
|
|
11
|
+
nMaxCustFilter: 'u32',
|
|
12
|
+
nFilterIndex: 'u32',
|
|
13
|
+
lpstrFile: 'ptr',
|
|
14
|
+
nMaxFile: 'u32',
|
|
15
|
+
lpstrFileTitle: 'ptr',
|
|
16
|
+
nMaxFileTitle: 'u32',
|
|
17
|
+
lpstrInitialDir: 'ptr',
|
|
18
|
+
lpstrTitle: 'ptr',
|
|
19
|
+
Flags: 'u32',
|
|
20
|
+
nFileOffset: 'u16',
|
|
21
|
+
nFileExtension: 'u16',
|
|
22
|
+
lpstrDefExt: 'ptr',
|
|
23
|
+
lCustData: 'ptr',
|
|
24
|
+
lpfnHook: 'ptr',
|
|
25
|
+
lpTemplateName: 'ptr',
|
|
26
|
+
pvReserved: 'ptr',
|
|
27
|
+
dwReserved: 'u32',
|
|
28
|
+
FlagsEx: 'u32',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// BROWSEINFOW(SHBrowseForFolderW):
|
|
32
|
+
export const BROWSEINFOW = struct({
|
|
33
|
+
hwndOwner: 'ptr',
|
|
34
|
+
pidlRoot: 'ptr',
|
|
35
|
+
pszDisplayName: 'ptr',
|
|
36
|
+
lpszTitle: 'ptr',
|
|
37
|
+
ulFlags: 'u32',
|
|
38
|
+
lpfn: 'ptr',
|
|
39
|
+
lParam: 'ptr',
|
|
40
|
+
iImage: 'i32',
|
|
41
|
+
})
|
package/package.json
CHANGED
package/quickwin.d.ts
CHANGED
|
@@ -554,6 +554,8 @@ declare module "ffi" {
|
|
|
554
554
|
type TYPE_OF_FFI_TYPE_SINT32 = number & { readonly __label: unique symbol }
|
|
555
555
|
type TYPE_OF_FFI_TYPE_UINT64 = number & { readonly __label: unique symbol }
|
|
556
556
|
type TYPE_OF_FFI_TYPE_SINT64 = number & { readonly __label: unique symbol }
|
|
557
|
+
type TYPE_OF_FFI_TYPE_FLOAT = number & { readonly __label: unique symbol }
|
|
558
|
+
type TYPE_OF_FFI_TYPE_DOUBLE = number & { readonly __label: unique symbol }
|
|
557
559
|
type TYPE_OF_FFI_TYPE_POINTER = number & { readonly __label: unique symbol }
|
|
558
560
|
|
|
559
561
|
const FFI_TYPE_VOID: TYPE_OF_FFI_TYPE_VOID;
|
|
@@ -565,10 +567,12 @@ declare module "ffi" {
|
|
|
565
567
|
const FFI_TYPE_SINT32: TYPE_OF_FFI_TYPE_SINT32;
|
|
566
568
|
const FFI_TYPE_UINT64: TYPE_OF_FFI_TYPE_UINT64;
|
|
567
569
|
const FFI_TYPE_SINT64: TYPE_OF_FFI_TYPE_SINT64;
|
|
570
|
+
const FFI_TYPE_FLOAT: TYPE_OF_FFI_TYPE_FLOAT;
|
|
571
|
+
const FFI_TYPE_DOUBLE: TYPE_OF_FFI_TYPE_DOUBLE;
|
|
568
572
|
const FFI_TYPE_POINTER: TYPE_OF_FFI_TYPE_POINTER;
|
|
569
573
|
|
|
570
|
-
type FfiType = TYPE_OF_FFI_TYPE_VOID | TYPE_OF_FFI_TYPE_UINT8 | TYPE_OF_FFI_TYPE_SINT8 | TYPE_OF_FFI_TYPE_UINT16 | TYPE_OF_FFI_TYPE_SINT16 | TYPE_OF_FFI_TYPE_UINT32 | TYPE_OF_FFI_TYPE_SINT32 | TYPE_OF_FFI_TYPE_UINT64 | TYPE_OF_FFI_TYPE_SINT64 | TYPE_OF_FFI_TYPE_POINTER;
|
|
571
|
-
type TypeArg<T extends FfiType> = T extends Exclude<FfiType, TYPE_OF_FFI_TYPE_VOID | TYPE_OF_FFI_TYPE_POINTER> ? number : T extends TYPE_OF_FFI_TYPE_POINTER ? (ArrayBuffer | null) : never;
|
|
574
|
+
type FfiType = TYPE_OF_FFI_TYPE_VOID | TYPE_OF_FFI_TYPE_UINT8 | TYPE_OF_FFI_TYPE_SINT8 | TYPE_OF_FFI_TYPE_UINT16 | TYPE_OF_FFI_TYPE_SINT16 | TYPE_OF_FFI_TYPE_UINT32 | TYPE_OF_FFI_TYPE_SINT32 | TYPE_OF_FFI_TYPE_UINT64 | TYPE_OF_FFI_TYPE_SINT64 | TYPE_OF_FFI_TYPE_FLOAT | TYPE_OF_FFI_TYPE_DOUBLE | TYPE_OF_FFI_TYPE_POINTER;
|
|
575
|
+
type TypeArg<T extends FfiType> = T extends Exclude<FfiType, TYPE_OF_FFI_TYPE_VOID | TYPE_OF_FFI_TYPE_POINTER> ? number : T extends TYPE_OF_FFI_TYPE_POINTER ? (ArrayBuffer | number | null) : never;
|
|
572
576
|
type TypeArgs<T extends FfiType[], Args extends(number | null | ArrayBuffer)[] = []> = T extends [infer T1 extends FfiType, ...infer RES extends FfiType[]] ? TypeArgs<RES, [...Args, TypeArg<T1>]> : Args;
|
|
573
577
|
|
|
574
578
|
function ffiCall<const T extends Exclude<FfiType, TYPE_OF_FFI_TYPE_VOID>[], const R extends FfiType>(func: number, argTypes: T, args: TypeArgs<T>, retType: R): R extends TYPE_OF_FFI_TYPE_VOID ? undefined : R extends TYPE_OF_FFI_TYPE_POINTER ? number | null : TypeArg<R>;
|
package/qwin-nowasm-x86.exe
CHANGED
|
Binary file
|
package/qwin-nowasm.exe
CHANGED
|
Binary file
|
package/qwin-x86.exe
CHANGED
|
Binary file
|
package/qwin.exe
CHANGED
|
Binary file
|
package/test/run.js
CHANGED
|
@@ -23,6 +23,8 @@ const suiteDefs = [
|
|
|
23
23
|
{ name: 'mupdf-twice', file: './test_mupdf_twice.js', tags: ['wasm', 'mupdf'] },
|
|
24
24
|
{ name: 'mupdf-render', file: './test_mupdf_render.js', tags: ['wasm', 'mupdf'] },
|
|
25
25
|
{ name: 'ffi', file: './test_ffi.js', tags: [] },
|
|
26
|
+
{ name: 'ffi-bind', file: './test_ffi_bind.js', tags: [] },
|
|
27
|
+
{ name: 'ffi-struct', file: './test_ffi_struct.js', tags: [] },
|
|
26
28
|
{ name: 'net-fetch', file: './test_net_fetch.js', tags: ['net'] },
|
|
27
29
|
{ name: 'net-wolfssl-read', file: './test_wolfssl_read.js', tags: ['net'] },
|
|
28
30
|
{ name: 'net-websocket', file: './test_net_websocket.js', tags: ['net'] },
|
package/test/run.ts
CHANGED
|
@@ -27,6 +27,8 @@ const suiteDefs = [
|
|
|
27
27
|
{ name: 'mupdf-twice', file: './test_mupdf_twice.js', tags: ['wasm', 'mupdf'] },
|
|
28
28
|
{ name: 'mupdf-render', file: './test_mupdf_render.js', tags: ['wasm', 'mupdf'] },
|
|
29
29
|
{ name: 'ffi', file: './test_ffi.js', tags: [] },
|
|
30
|
+
{ name: 'ffi-bind', file: './test_ffi_bind.js', tags: [] },
|
|
31
|
+
{ name: 'ffi-struct', file: './test_ffi_struct.js', tags: [] },
|
|
30
32
|
{ name: 'net-fetch', file: './test_net_fetch.js', tags: ['net'] },
|
|
31
33
|
{ name: 'net-wolfssl-read', file: './test_wolfssl_read.js', tags: ['net'] },
|
|
32
34
|
{ name: 'net-websocket', file: './test_net_websocket.js', tags: ['net'] },
|
package/test/test_ffi.js
CHANGED
|
@@ -159,14 +159,18 @@ export const suite = {
|
|
|
159
159
|
else {
|
|
160
160
|
std.printf(' no printers installed (skip)\n');
|
|
161
161
|
}
|
|
162
|
-
t.section('
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
162
|
+
t.section('PTR accepts raw number handles (XP 32-bit fix)');
|
|
163
|
+
const hUser32 = win.LoadLibrary('user32.dll');
|
|
164
|
+
t.checkTrue('LoadLibrary("user32.dll") succeeds', hUser32 !== null);
|
|
165
|
+
if (hUser32) {
|
|
166
|
+
const getDC = win.GetProcAddress(hUser32, 'GetDC');
|
|
167
|
+
t.checkTrue('GetProcAddress("GetDC") succeeds', getDC !== null);
|
|
168
|
+
if (getDC) {
|
|
169
|
+
const screenDc = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0], ffi.FFI_TYPE_POINTER);
|
|
170
|
+
t.checkTrue('PTR slot takes number (NULL hwnd) and returns a DC handle', screenDc !== 0 && screenDc !== null);
|
|
171
|
+
const bogus = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0x12345678], ffi.FFI_TYPE_POINTER);
|
|
172
|
+
t.checkTrue('PTR slot takes non-zero number without throwing', bogus === 0 || bogus === null);
|
|
173
|
+
}
|
|
169
174
|
}
|
|
170
|
-
t.checkTrue('non-ArrayBuffer pointer arg throws TypeError', threw);
|
|
171
175
|
}
|
|
172
176
|
};
|
package/test/test_ffi.ts
CHANGED
|
@@ -173,18 +173,18 @@ export const suite = {
|
|
|
173
173
|
std.printf(' no printers installed (skip)\n')
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
t.section('
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
ffi.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
t.section('PTR accepts raw number handles (XP 32-bit fix)')
|
|
177
|
+
const hUser32 = win.LoadLibrary('user32.dll')
|
|
178
|
+
t.checkTrue('LoadLibrary("user32.dll") succeeds', hUser32 !== null)
|
|
179
|
+
if (hUser32) {
|
|
180
|
+
const getDC = win.GetProcAddress(hUser32, 'GetDC')
|
|
181
|
+
t.checkTrue('GetProcAddress("GetDC") succeeds', getDC !== null)
|
|
182
|
+
if (getDC) {
|
|
183
|
+
const screenDc = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0], ffi.FFI_TYPE_POINTER)
|
|
184
|
+
t.checkTrue('PTR slot takes number (NULL hwnd) and returns a DC handle', screenDc !== 0 && screenDc !== null)
|
|
185
|
+
const bogus = ffi.ffiCall(getDC, [ffi.FFI_TYPE_POINTER], [0x12345678], ffi.FFI_TYPE_POINTER)
|
|
186
|
+
t.checkTrue('PTR slot takes non-zero number without throwing', bogus === 0 || bogus === null)
|
|
187
|
+
}
|
|
187
188
|
}
|
|
188
|
-
t.checkTrue('non-ArrayBuffer pointer arg throws TypeError', threw)
|
|
189
189
|
}
|
|
190
190
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import * as std from 'std';
|
|
2
|
+
import { bind, bindLib } from '../lib/ffi-bind.js';
|
|
3
|
+
export const suite = {
|
|
4
|
+
name: 'ffi-bind',
|
|
5
|
+
run: (t) => {
|
|
6
|
+
t.section('bind single proc');
|
|
7
|
+
const getDC = bind('user32.dll', 'GetDC', 'ptr -> ptr');
|
|
8
|
+
const dc = getDC(0);
|
|
9
|
+
t.checkTrue('bind GetDC(NULL) returns screen DC', !!dc);
|
|
10
|
+
t.section('bindLib batch with wstr auto-encode');
|
|
11
|
+
const user32 = bindLib('user32.dll', {
|
|
12
|
+
DrawTextW: 'ptr wstr i32 ptr i32 -> i32',
|
|
13
|
+
ReleaseDC: 'ptr ptr -> i32',
|
|
14
|
+
});
|
|
15
|
+
const rect = new ArrayBuffer(16);
|
|
16
|
+
const dv = new DataView(rect);
|
|
17
|
+
dv.setInt32(8, 500, true);
|
|
18
|
+
const lines = user32.DrawTextW(dc, 'hello ffi-bind', -1, rect, 1024 /* gui.DrawTextFlag.CALCRECT */);
|
|
19
|
+
t.checkTrue('DrawTextW returns line count > 0', lines > 0);
|
|
20
|
+
t.checkTrue('measured width > 0', dv.getInt32(8, true) > 0);
|
|
21
|
+
t.checkTrue('measured height > 0', dv.getInt32(12, true) > 0);
|
|
22
|
+
if (dc)
|
|
23
|
+
user32.ReleaseDC(0, dc);
|
|
24
|
+
t.section('signedness read-back: i32 vs u32');
|
|
25
|
+
const lstrcmpI = bind('kernel32.dll', 'lstrcmpW', 'wstr wstr -> i32');
|
|
26
|
+
const lstrcmpU = bind('kernel32.dll', 'lstrcmpW', 'wstr wstr -> u32');
|
|
27
|
+
const setLastError = bind('kernel32.dll', 'SetLastError', 'u32 -> void');
|
|
28
|
+
const getErrU = bind('kernel32.dll', 'GetLastError', ' -> u32');
|
|
29
|
+
const getErrI = bind('kernel32.dll', 'GetLastError', ' -> i32');
|
|
30
|
+
const ri = lstrcmpI('a', 'b');
|
|
31
|
+
const ru = lstrcmpU('a', 'b');
|
|
32
|
+
std.printf(' lstrcmpW("a","b"): i32=%s u32=%s\n', String(ri), String(ru));
|
|
33
|
+
t.checkTrue('i32 read-back keeps negative sign', ri < 0);
|
|
34
|
+
t.check('u32 read-back is 0xFFFFFFFF', 4294967295, ru);
|
|
35
|
+
setLastError(0x80000000);
|
|
36
|
+
const glU = getErrU();
|
|
37
|
+
const glI = getErrI();
|
|
38
|
+
std.printf(' GetLastError after SetLastError(0x80000000): u32=%s i32=%s\n', String(glU), String(glI));
|
|
39
|
+
t.check('GetLastError u32 keeps big value', 2147483648, glU);
|
|
40
|
+
t.check('GetLastError i32 reads as negative', -2147483648, glI);
|
|
41
|
+
},
|
|
42
|
+
};
|