quickwin 1.2.0 → 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 +956 -215
- package/examples/pdf_viewer.tsx +29 -19
- package/examples/test_gallery.js +922 -194
- package/examples/test_react_lazy.js +905 -177
- package/examples/test_react_listbox.js +905 -177
- package/examples/test_root_window.js +905 -177
- package/examples/test_tab.js +905 -177
- package/lib/fetch.js +35 -7
- package/lib/fetch.ts +24 -4
- 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 -24
- package/lib/text-measure.ts +14 -24
- package/lib/win-common-structs.js +39 -0
- package/lib/win-common-structs.ts +41 -0
- package/main.js +46 -0
- package/package.json +2 -1
- package/quickwin.d.ts +7 -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 +3 -0
- package/test/run.ts +3 -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_net_fetch.js +62 -0
- package/test/test_net_fetch.ts +72 -0
- package/test/test_react_complex.js +908 -180
- package/test/test_react_counter.js +905 -177
- package/test/test_react_flex.js +905 -177
- package/test/test_react_hidden.js +907 -179
- package/test/test_react_listview_link.js +905 -177
- package/test/test_react_render.js +905 -177
- package/test/test_wolfssl_read.js +201 -0
- package/test/test_wolfssl_read.ts +181 -0
|
@@ -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,29 +1,19 @@
|
|
|
1
1
|
import * as ffi from 'ffi';
|
|
2
|
-
import * as win from 'win';
|
|
3
2
|
import * as gui from 'gui';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
throw new Error('Failed to load system DLLs');
|
|
14
|
-
const DrawTextW = loadProc(user32, 'DrawTextW');
|
|
15
|
-
const GetDC = loadProc(user32, 'GetDC');
|
|
16
|
-
const ReleaseDC = loadProc(user32, 'ReleaseDC');
|
|
17
|
-
const SelectObject = loadProc(gdi32, 'SelectObject');
|
|
18
|
-
const FFI_TYPE_UINT64 = ffi.FFI_TYPE_UINT64;
|
|
19
|
-
const FFI_S32 = ffi.FFI_TYPE_SINT32;
|
|
20
|
-
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
|
+
});
|
|
21
12
|
export function measureText(hdc, text, maxWidth) {
|
|
22
|
-
const textBuf = new TextEncoder('utf-16le').encode(text + '\0').buffer;
|
|
23
13
|
const rect = new ArrayBuffer(16);
|
|
24
14
|
const dv = new DataView(rect);
|
|
25
15
|
dv.setInt32(8, maxWidth, true);
|
|
26
|
-
|
|
16
|
+
user32.DrawTextW(hdc, text, -1, rect, 1024 /* gui.DrawTextFlag.CALCRECT */);
|
|
27
17
|
return { width: dv.getInt32(8, true), height: dv.getInt32(12, true) };
|
|
28
18
|
}
|
|
29
19
|
export function getButtonIdealSize(hwnd) {
|
|
@@ -33,15 +23,15 @@ export function getButtonIdealSize(hwnd) {
|
|
|
33
23
|
return { width: dv.getInt32(0, true), height: dv.getInt32(4, true) };
|
|
34
24
|
}
|
|
35
25
|
export function measureTextForHwnd(hwnd, text) {
|
|
36
|
-
const hdc =
|
|
26
|
+
const hdc = user32.GetDC(hwnd);
|
|
37
27
|
if (!hdc)
|
|
38
28
|
return { width: 0, height: 0 };
|
|
39
29
|
const hFont = gui.SendMessage(hwnd, 49 /* gui.WmMsg.GETFONT */, 0, 0);
|
|
40
|
-
const oldFont = hFont ?
|
|
30
|
+
const oldFont = hFont ? gdi32.SelectObject(hdc, hFont) : 0;
|
|
41
31
|
const result = measureText(hdc, text, 0);
|
|
42
32
|
if (hFont)
|
|
43
|
-
|
|
44
|
-
|
|
33
|
+
gdi32.SelectObject(hdc, oldFont);
|
|
34
|
+
user32.ReleaseDC(hwnd, hdc);
|
|
45
35
|
const wr = gui.GetWindowRect(hwnd);
|
|
46
36
|
const cr = gui.GetClientRect(hwnd);
|
|
47
37
|
if (wr && cr) {
|
package/lib/text-measure.ts
CHANGED
|
@@ -1,31 +1,21 @@
|
|
|
1
1
|
import * as ffi from 'ffi'
|
|
2
|
-
import * as win from 'win'
|
|
3
2
|
import * as gui from 'gui'
|
|
3
|
+
import { bindLib } from './ffi-bind.js'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (!user32 || !gdi32) throw new Error('Failed to load system DLLs')
|
|
14
|
-
const DrawTextW = loadProc(user32, 'DrawTextW')
|
|
15
|
-
const GetDC = loadProc(user32, 'GetDC')
|
|
16
|
-
const ReleaseDC = loadProc(user32, 'ReleaseDC')
|
|
17
|
-
const SelectObject = loadProc(gdi32, 'SelectObject')
|
|
18
|
-
|
|
19
|
-
const FFI_TYPE_UINT64 = ffi.FFI_TYPE_UINT64
|
|
20
|
-
const FFI_S32 = ffi.FFI_TYPE_SINT32
|
|
21
|
-
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
|
+
})
|
|
22
13
|
|
|
23
14
|
export function measureText(hdc: number, text: string, maxWidth: number): { width: number; height: number } {
|
|
24
|
-
const textBuf = new TextEncoder('utf-16le').encode(text + '\0').buffer
|
|
25
15
|
const rect = new ArrayBuffer(16)
|
|
26
16
|
const dv = new DataView(rect)
|
|
27
17
|
dv.setInt32(8, maxWidth, true)
|
|
28
|
-
|
|
18
|
+
user32.DrawTextW(hdc, text, -1, rect, gui.DrawTextFlag.CALCRECT)
|
|
29
19
|
return { width: dv.getInt32(8, true), height: dv.getInt32(12, true) }
|
|
30
20
|
}
|
|
31
21
|
|
|
@@ -37,13 +27,13 @@ export function getButtonIdealSize(hwnd: gui.HWND): { width: number; height: num
|
|
|
37
27
|
}
|
|
38
28
|
|
|
39
29
|
export function measureTextForHwnd(hwnd: gui.HWND, text: string): { width: number; height: number } {
|
|
40
|
-
const hdc =
|
|
30
|
+
const hdc = user32.GetDC(hwnd)
|
|
41
31
|
if (!hdc) return { width: 0, height: 0 }
|
|
42
32
|
const hFont = gui.SendMessage(hwnd, gui.WmMsg.GETFONT, 0, 0)
|
|
43
|
-
const oldFont = hFont ?
|
|
33
|
+
const oldFont = hFont ? gdi32.SelectObject(hdc, hFont) : 0
|
|
44
34
|
const result = measureText(hdc, text, 0)
|
|
45
|
-
if (hFont)
|
|
46
|
-
|
|
35
|
+
if (hFont) gdi32.SelectObject(hdc, oldFont)
|
|
36
|
+
user32.ReleaseDC(hwnd, hdc)
|
|
47
37
|
const wr = gui.GetWindowRect(hwnd)
|
|
48
38
|
const cr = gui.GetClientRect(hwnd)
|
|
49
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/main.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { CallWindowProc, CreateSystemDpiFont, CreateWindow as CreateWindowWithoutScale, DefWindowProc, GetScaleFactor, GetWindowLongPtr, GetWindowText, RegisterClass, SendMessage, SetWindowProc, ShowWindow, } from "gui";
|
|
2
|
+
const scaleFactor = GetScaleFactor();
|
|
3
|
+
const dpiFont = CreateSystemDpiFont();
|
|
4
|
+
if (!dpiFont)
|
|
5
|
+
throw new Error('Failed to create DPI font');
|
|
6
|
+
const CreateWindow = (className, title, style, x, y, cx, cy, parent, id) => {
|
|
7
|
+
let hwnd = CreateWindowWithoutScale(className, title, style, x * scaleFactor, y * scaleFactor, cx * scaleFactor, cy * scaleFactor, parent, id);
|
|
8
|
+
if (hwnd)
|
|
9
|
+
SendMessage(hwnd, 48 /* WmMsg.SETFONT */, dpiFont, 1);
|
|
10
|
+
return hwnd;
|
|
11
|
+
};
|
|
12
|
+
let btn = null;
|
|
13
|
+
let edit = null;
|
|
14
|
+
let list = null;
|
|
15
|
+
function wndProc(hwnd, msg, wParam, lParam) {
|
|
16
|
+
if (msg === 1 /* WmMsg.CREATE */) {
|
|
17
|
+
btn = CreateWindow("BUTTON", "添加", 1073741824 /* WindowStyle.CHILD */ | 268435456 /* WindowStyle.VISIBLE */ | 0 /* ButtonStyle.PUSHBUTTON */, 120, 10, 80, 30, hwnd, null);
|
|
18
|
+
edit = CreateWindow("EDIT", "", 1073741824 /* WindowStyle.CHILD */ | 268435456 /* WindowStyle.VISIBLE */ | 8388608 /* WindowStyle.BORDER */, 10, 10, 100, 20, hwnd, null);
|
|
19
|
+
list = CreateWindow("LISTBOX", "", 1073741824 /* WindowStyle.CHILD */ | 268435456 /* WindowStyle.VISIBLE */ | 8388608 /* WindowStyle.BORDER */, 10, 40, 200, 100, hwnd, null);
|
|
20
|
+
if (!btn)
|
|
21
|
+
return 0;
|
|
22
|
+
const oldProc = GetWindowLongPtr(btn, -4 /* Gwlp.WNDPROC */);
|
|
23
|
+
SetWindowProc(btn, (hwnd, msg, wParam, lParam) => {
|
|
24
|
+
if (msg == 513 /* WmMsg.LBUTTONDOWN */) {
|
|
25
|
+
if (!edit)
|
|
26
|
+
return 0;
|
|
27
|
+
if (!list)
|
|
28
|
+
return 0;
|
|
29
|
+
const text = GetWindowText(edit);
|
|
30
|
+
if (text.trim())
|
|
31
|
+
SendMessage(list, 384 /* LbMsg.ADDSTRING */, 0, text);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
return CallWindowProc(oldProc, hwnd, msg, wParam, lParam);
|
|
35
|
+
});
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
else if (msg === 273 /* WmMsg.COMMAND */) {
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
return DefWindowProc(hwnd, msg, wParam, lParam);
|
|
42
|
+
}
|
|
43
|
+
RegisterClass("MainWindow", wndProc);
|
|
44
|
+
var hwnd = CreateWindow("MainWindow", "Todo List", 13565952 /* WindowStyle.OVERLAPPEDWINDOW */, 0, 0, 600, 400, null, null);
|
|
45
|
+
if (hwnd)
|
|
46
|
+
ShowWindow(hwnd);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "quickwin",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.0",
|
|
5
5
|
"description": "QuickJS Win32 runtime — GUI, networking, WASM, FFI, and more",
|
|
6
6
|
"bin": {
|
|
7
7
|
"quickwin": "qwin.exe",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"qwin-nowasm.exe",
|
|
19
19
|
"qwin-x86.exe",
|
|
20
20
|
"qwin-nowasm-x86.exe",
|
|
21
|
+
"main.js",
|
|
21
22
|
"lib/",
|
|
22
23
|
"test/",
|
|
23
24
|
"examples/",
|
package/quickwin.d.ts
CHANGED
|
@@ -386,6 +386,7 @@ declare module "wolfssl" {
|
|
|
386
386
|
function wolfSSL_connect(ssl: WOLFSSL): number;
|
|
387
387
|
function wolfSSL_shutdown(ssl: WOLFSSL): number;
|
|
388
388
|
function wolfSSL_write(ssl: WOLFSSL, buf: ArrayBuffer): number;
|
|
389
|
+
/** 成功返回 ArrayBuffer;EOF/暂无数据/错误返回 null(区分用 wolfSSL_get_error:6=ZERO_RETURN 关闭,2=WANT_READ) */
|
|
389
390
|
function wolfSSL_read(ssl: WOLFSSL, sz: number): ArrayBuffer | null;
|
|
390
391
|
function wolfSSL_get_error(ssl: WOLFSSL, ret: number): number;
|
|
391
392
|
function wolfSSL_UseSNI(ssl: WOLFSSL, type: SniType, name: string, len?: number): number;
|
|
@@ -553,6 +554,8 @@ declare module "ffi" {
|
|
|
553
554
|
type TYPE_OF_FFI_TYPE_SINT32 = number & { readonly __label: unique symbol }
|
|
554
555
|
type TYPE_OF_FFI_TYPE_UINT64 = number & { readonly __label: unique symbol }
|
|
555
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 }
|
|
556
559
|
type TYPE_OF_FFI_TYPE_POINTER = number & { readonly __label: unique symbol }
|
|
557
560
|
|
|
558
561
|
const FFI_TYPE_VOID: TYPE_OF_FFI_TYPE_VOID;
|
|
@@ -564,10 +567,12 @@ declare module "ffi" {
|
|
|
564
567
|
const FFI_TYPE_SINT32: TYPE_OF_FFI_TYPE_SINT32;
|
|
565
568
|
const FFI_TYPE_UINT64: TYPE_OF_FFI_TYPE_UINT64;
|
|
566
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;
|
|
567
572
|
const FFI_TYPE_POINTER: TYPE_OF_FFI_TYPE_POINTER;
|
|
568
573
|
|
|
569
|
-
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;
|
|
570
|
-
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;
|
|
571
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;
|
|
572
577
|
|
|
573
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,7 +23,10 @@ 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'] },
|
|
29
|
+
{ name: 'net-wolfssl-read', file: './test_wolfssl_read.js', tags: ['net'] },
|
|
27
30
|
{ name: 'net-websocket', file: './test_net_websocket.js', tags: ['net'] },
|
|
28
31
|
{ name: 'net-event', file: './test_net_event.js', tags: ['net'] },
|
|
29
32
|
{ name: 'http-server', file: './test_http_server.js', tags: ['net'] },
|
package/test/run.ts
CHANGED
|
@@ -27,7 +27,10 @@ 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'] },
|
|
33
|
+
{ name: 'net-wolfssl-read', file: './test_wolfssl_read.js', tags: ['net'] },
|
|
31
34
|
{ name: 'net-websocket', file: './test_net_websocket.js', tags: ['net'] },
|
|
32
35
|
{ name: 'net-event', file: './test_net_event.js', tags: ['net'] },
|
|
33
36
|
{ name: 'http-server', file: './test_http_server.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
|
};
|