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.
Files changed (50) hide show
  1. package/README.en.md +2 -1
  2. package/README.md +2 -1
  3. package/examples/PdfCanvas.js +3 -3
  4. package/examples/PdfCanvas.tsx +3 -3
  5. package/examples/pdf_preview2.js +34 -19
  6. package/examples/pdf_preview2.ts +34 -20
  7. package/examples/pdf_viewer.js +959 -219
  8. package/examples/pdf_viewer.tsx +29 -19
  9. package/examples/test_gallery.js +925 -198
  10. package/examples/test_react_lazy.js +908 -181
  11. package/examples/test_react_listbox.js +908 -181
  12. package/examples/test_root_window.js +908 -181
  13. package/examples/test_tab.js +908 -181
  14. package/lib/ffi-bind.js +85 -0
  15. package/lib/ffi-bind.ts +170 -0
  16. package/lib/ffi-struct.js +224 -0
  17. package/lib/ffi-struct.ts +239 -0
  18. package/lib/react-qw/components/DateTimePicker.js +21 -15
  19. package/lib/react-qw/components/Link.js +4 -6
  20. package/lib/react-qw/components/ListView.js +84 -40
  21. package/lib/react-qw/components/PathPicker.js +40 -24
  22. package/lib/react-qw/components/Tab.js +21 -10
  23. package/lib/react-qw/components/Tooltip.js +27 -15
  24. package/lib/react-qw/components/TreeView.js +23 -33
  25. package/lib/react-qw/nmhdr.js +26 -0
  26. package/lib/react-qw/nmhdr.ts +32 -0
  27. package/lib/text-measure.js +14 -26
  28. package/lib/text-measure.ts +14 -26
  29. package/lib/win-common-structs.js +39 -0
  30. package/lib/win-common-structs.ts +41 -0
  31. package/package.json +1 -1
  32. package/quickwin.d.ts +6 -2
  33. package/qwin-nowasm-x86.exe +0 -0
  34. package/qwin-nowasm.exe +0 -0
  35. package/qwin-x86.exe +0 -0
  36. package/qwin.exe +0 -0
  37. package/test/run.js +2 -0
  38. package/test/run.ts +2 -0
  39. package/test/test_ffi.js +12 -8
  40. package/test/test_ffi.ts +12 -12
  41. package/test/test_ffi_bind.js +42 -0
  42. package/test/test_ffi_bind.ts +46 -0
  43. package/test/test_ffi_struct.js +201 -0
  44. package/test/test_ffi_struct.ts +223 -0
  45. package/test/test_react_complex.js +911 -184
  46. package/test/test_react_counter.js +908 -181
  47. package/test/test_react_flex.js +908 -181
  48. package/test/test_react_hidden.js +910 -183
  49. package/test/test_react_listview_link.js +908 -181
  50. 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 = new ArrayBuffer(40);
29
- const dv = new DataView(tci);
30
- dv.setUint32(0, 1 /* gui.TcItemFlag.TEXT */, true);
31
- dv.setBigUint64(16, BigInt(ffi.bufferPtr(titleBuf)), true);
32
- dv.setInt32(24, tabs[i].title.length + 1, true);
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 = readI32(e.lParam, 16);
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
- const TTTOOLINFO_CBSIZE = 64;
5
- function setQword(dv, offset, val) {
6
- dv.setUint32(offset, val & 0xFFFFFFFF, true);
7
- dv.setUint32(offset + 4, Math.floor(val / 0x100000000), true);
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 textOff = TTTOOLINFO_CBSIZE;
17
+ const size = TTTOOLINFOW.size;
11
18
  const textLen = (text.length + 1) * 2;
12
- const buf = new ArrayBuffer(TTTOOLINFO_CBSIZE + textLen);
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(textOff + i * 2, text.charCodeAt(i), true);
20
- dv.setUint16(textOff + text.length * 2, 0, true);
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
- const buf = new ArrayBuffer(56);
28
- const dv = new DataView(buf);
29
- dv.setUint32(0, mask, true); // mask (offset 0)
30
- setPtr(dv, 8, 0); // hItem
31
- dv.setUint32(16, 0, true); // state
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 tvins = buildTvInsertStruct(parentHandle, -65536 /* gui.TvInsertAfter.ROOT */, itemBuf);
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 = readI32(e.lParam, 16);
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
+ }
@@ -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
- function loadProc(lib, name) {
6
- const ptr = win.GetProcAddress(lib, name);
7
- if (!ptr)
8
- throw new Error(`Cannot load ${name}`);
9
- return ptr;
10
- }
11
- const user32 = win.LoadLibrary('user32.dll');
12
- const gdi32 = win.LoadLibrary('gdi32.dll');
13
- if (!user32 || !gdi32)
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
- ffi.ffiCall(DrawTextW, [FFI_HND, FFI_PTR, FFI_S32, FFI_PTR, FFI_S32], [hdc, textBuf, -1, rect, 1024 /* gui.DrawTextFlag.CALCRECT */], FFI_S32);
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 = ffi.ffiCall(GetDC, [FFI_HND], [hwnd], FFI_HND);
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 ? ffi.ffiCall(SelectObject, [FFI_HND, FFI_HND], [hdc, hFont], FFI_HND) : 0;
30
+ const oldFont = hFont ? gdi32.SelectObject(hdc, hFont) : 0;
43
31
  const result = measureText(hdc, text, 0);
44
32
  if (hFont)
45
- ffi.ffiCall(SelectObject, [FFI_HND, FFI_HND], [hdc, oldFont], FFI_HND);
46
- ffi.ffiCall(ReleaseDC, [FFI_HND, FFI_HND], [hwnd, hdc], FFI_S32);
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) {
@@ -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
- function loadProc(lib: win.HMODULE, name: string): number {
7
- const ptr = win.GetProcAddress(lib, name)
8
- if (!ptr) throw new Error(`Cannot load ${name}`)
9
- return ptr
10
- }
11
-
12
- const user32 = win.LoadLibrary('user32.dll')
13
- const gdi32 = win.LoadLibrary('gdi32.dll')
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
- ffi.ffiCall(DrawTextW, [FFI_HND, FFI_PTR, FFI_S32, FFI_PTR, FFI_S32], [hdc, textBuf, -1, rect, gui.DrawTextFlag.CALCRECT], FFI_S32)
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 = ffi.ffiCall(GetDC, [FFI_HND], [hwnd], FFI_HND)
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 ? ffi.ffiCall(SelectObject, [FFI_HND, FFI_HND], [hdc, hFont], FFI_HND) : 0
33
+ const oldFont = hFont ? gdi32.SelectObject(hdc, hFont) : 0
46
34
  const result = measureText(hdc, text, 0)
47
- if (hFont) ffi.ffiCall(SelectObject, [FFI_HND, FFI_HND], [hdc, oldFont], FFI_HND)
48
- ffi.ffiCall(ReleaseDC, [FFI_HND, FFI_HND], [hwnd, hdc], FFI_S32)
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "quickwin",
4
- "version": "1.2.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",
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>;
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('pointer arg type check');
163
- let threw = false;
164
- try {
165
- ffi.ffiCall(enumPrinters, [ffi.FFI_TYPE_POINTER], [123], ffi.FFI_TYPE_VOID);
166
- }
167
- catch (e) {
168
- threw = true;
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('pointer arg type check')
177
- let threw = false
178
- try {
179
- ffi.ffiCall(
180
- enumPrinters,
181
- [ffi.FFI_TYPE_POINTER] as any,
182
- [123] as any,
183
- ffi.FFI_TYPE_VOID
184
- )
185
- } catch (e) {
186
- threw = true
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
+ };