chore: import upstream snapshot with attribution
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:03:19 +08:00
commit 91e75e620b
3227 changed files with 1307078 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
# Rust
/target/
# .NET
**/bin/
**/obj/
# Electron / Node
electron/node_modules/
electron/package-lock.json
+312
View File
@@ -0,0 +1,312 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "crc32fast"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
]
[[package]]
name = "legacy-app"
version = "0.1.0"
dependencies = [
"windows",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]]
name = "moxcms"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
dependencies = [
"num-traits",
"pxfm",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "orchestrator"
version = "0.1.0"
dependencies = [
"image",
"windows",
]
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pxfm"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core",
"windows-targets",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-strings",
"windows-targets",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+6
View File
@@ -0,0 +1,6 @@
[workspace]
resolver = "2"
members = ["legacy-app", "orchestrator"]
[profile.release]
opt-level = 2
+89
View File
@@ -0,0 +1,89 @@
# Multi-cursor background computer-use demo — "National Records System"
A fleet of deliberately **legacy-looking government records terminals** (navy
banner, `UNCLASSIFIED // FOR OFFICIAL USE ONLY` strip, function-key bar,
green-screen records grid, status line) — the kind of internal agency app that,
in the age of AI, has *no* automation integration. cua-driver automates them
anyway.
One human action in the foreground "master" terminal is replayed onto **four
background terminals at the same time**, each driven by its own cua-driver
session = its own uniquely-coloured agent cursor — with **no window ever
raised** and **the user's mouse never moved**.
It also proves cua-driver works **with or without an accessibility tree**: the
five windows span five UI frameworks, and cua-driver's default dispatch
auto-selects UIA-Invoke where an a11y tree exists and falls back to
pixel/pointer-injection where it doesn't.
## Layout (each window = ½ work-width × ½ work-height)
The four corners tile the taskbar-safe work area into quadrants; the master is
centered, **overlapping all four**:
```
┌────────────────────────┬────────────────────────┐
│ Win32 GDI (NO a11y) │ WinForms (.NET) │
│ crimson ● │ amber ● │
│ ┌────────────────────────┐ │
│ │ MASTER — Win32 controls │ ← you │
├───────────│ (foreground, overlaps) │─────────────┤
│ WPF (XAML)│ │ Electron │
│ └────────────────────────┘ mint_lime ● │
│ aqua ● │ (Chromium) │
└────────────────────────┴────────────────────────┘
```
Click **SUBMIT** (or type a subject name then submit) in the center master:
four coloured cursors glide onto the four corner terminals and commit the same
record there — concurrently, in the background. Watch each corner's
green-screen records grid grow and its `RECORDS:` counter tick up, without any
corner ever coming to the front.
## Frameworks (and what they exercise)
| Window | Framework | Accessibility | cua-driver path |
|---|---|---|---|
| TL | Win32 + GDI (custom-drawn) | **none** | pixel hit-test → PostMessage / pointer injection |
| TR | .NET WinForms | MSAA/UIA | UIA Invoke |
| BL | .NET WPF | UIA (XAML) | UIA Invoke (no foreground steal via WS_EX_NOACTIVATE) |
| BR | Electron | UIA (Chromium) | UIA Invoke |
| Center | Win32 standard controls | MSAA | (foreground; the human drives it) |
## Build
```powershell
# from this directory
cargo build # legacy-app + orchestrator (Rust)
dotnet build dotnet/winforms/winforms.csproj # WinForms
dotnet build dotnet/wpf/wpf.csproj # WPF
npm install --prefix electron # Electron (downloads electron once)
```
Also build the driver once (repo root workspace):
```powershell
cargo build -p cua-driver --manifest-path ..\..\libs\cua-driver\rust\Cargo.toml
```
## Run
```powershell
.\target\debug\orchestrator.exe # human-driven: click/type in the center
.\target\debug\orchestrator.exe --auto # self-playing: drives a TYPE+CLICK every few seconds
```
The orchestrator starts the cua-driver daemon, launches + positions all five
windows, and fans every center action out to the four corners over four
concurrent `cua-driver call` sessions (`crimson` / `amber` / `aqua` /
`mint_lime` → four cursor colours). Close the center window (or kill the
orchestrator) to tear everything down — a Windows Job Object kills the whole
tree, so nothing is orphaned.
### Env overrides
`CUA_DRIVER_EXE`, `LEGACY_APP_EXE`, `WINFORMS_EXE`, `WPF_EXE`, `ELECTRON_DIR`.
## How the coloured cursors work
cua-driver assigns each session a cursor colour by name (palette-name sessions
like `crimson` pick that colour directly). Passing `"session":"<color>"` on each
`click`/`type_text` call routes it to that session's overlay cursor, which
glides to the target. Four sessions → four cursors animating at once. See
`docs/windows-background-input-re-plan.md` for the no-z-raise mechanism.
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
// "Meridian CRM — Account Record" : WinForms node (classic Win32 controls,
// MSAA/UIA). Explicit fractional layout (dense, no gaps beyond borders).
// cua-driver drives the Account Name field + the "Add Record" button.
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var f = new Form { Text = "Meridian CRM — Account Record [WinForms]", ClientSize = new Size(960, 600),
StartPosition = FormStartPosition.Manual, Font = new Font("Segoe UI", 9f), BackColor = Color.FromArgb(240,240,240) };
Label band(string t, Color bg, ContentAlignment a = ContentAlignment.MiddleLeft, FontStyle fs = FontStyle.Regular)
=> new Label { Text = t, BackColor = bg, TextAlign = a, Font = new Font("Segoe UI", 9f, fs) };
var menu = band(" File Edit View Record Tools Help", Color.FromArgb(247,247,247));
var tool = band(" New Open Save Delete │ ◀ Prev Next ▶ │ Find", Color.FromArgb(235,235,235));
var nameLbl = band(" Account Name", Color.FromArgb(240,240,240));
var typeLbl = band(" Account Type", Color.FromArgb(240,240,240));
var regionLbl = band(" Region", Color.FromArgb(240,240,240));
var prioLbl = band(" Priority (1-5)", Color.FromArgb(240,240,240));
var creditLbl = band(" Credit Limit", Color.FromArgb(240,240,240));
var nameBox = new TextBox { BorderStyle = BorderStyle.FixedSingle };
var typeCmb = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, FlatStyle = FlatStyle.Flat };
typeCmb.Items.AddRange(new object[] { "Enterprise", "SMB", "Government", "Reseller" }); typeCmb.SelectedIndex = 0;
var regionCmb = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, FlatStyle = FlatStyle.Flat };
regionCmb.Items.AddRange(new object[] { "North", "South", "EMEA", "APAC", "LATAM" }); regionCmb.SelectedIndex = 0;
var prio = new TrackBar { Minimum = 1, Maximum = 5, Value = 3, TickStyle = TickStyle.BottomRight };
var credit = new TrackBar { Minimum = 0, Maximum = 100, Value = 40, TickFrequency = 10, TickStyle = TickStyle.BottomRight };
var saveBtn = new Button { Text = "Add Record", FlatStyle = FlatStyle.System, Font = new Font("Segoe UI", 10f, FontStyle.Bold) };
var sigHdr = band(" Signature / Notes", Color.FromArgb(225,225,225), ContentAlignment.MiddleLeft, FontStyle.Bold);
var sig = new Panel { BackColor = Color.White, BorderStyle = BorderStyle.FixedSingle };
var grid = new DataGridView { AllowUserToAddRows = false, ReadOnly = true, BackgroundColor = Color.White,
BorderStyle = BorderStyle.FixedSingle, RowHeadersVisible = false, AllowUserToResizeRows = false,
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing };
grid.Columns.Add("acct", "Account"); grid.Columns.Add("type", "Type"); grid.Columns.Add("region", "Region");
grid.Columns.Add("pri", "Pri"); grid.Columns.Add("credit", "Credit");
grid.Columns["acct"].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
var status = band(" Ready", Color.FromArgb(232,232,232));
var recLbl = band("Records: 0 USER: SYSTEM ▌ CONNECTED ", Color.FromArgb(232,232,232), ContentAlignment.MiddleRight);
// line tool: one straight segment per press-drag-release (down→up)
var segs = new List<(Point a, Point b)>(); Point? down = null; Point cur = Point.Empty;
sig.MouseDown += (s, e) => { down = e.Location; cur = e.Location; };
sig.MouseMove += (s, e) => { if (down != null) { cur = e.Location; sig.Invalidate(); } };
sig.MouseUp += (s, e) => { if (down != null) { segs.Add((down.Value, e.Location)); down = null; sig.Invalidate(); } };
sig.Paint += (s, e) => { e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
using var p = new Pen(Color.MidnightBlue, 2); foreach (var sg in segs) e.Graphics.DrawLine(p, sg.a, sg.b);
if (down != null) { using var pp = new Pen(Color.FromArgb(110, 25, 25, 112), 1); e.Graphics.DrawLine(pp, down.Value, cur); } };
int records = 0;
saveBtn.Click += (s, e) =>
{
string nm = nameBox.Text.Trim(); if (nm.Length == 0) nm = "(unnamed)";
grid.Rows.Add(nm, typeCmb.Text, regionCmb.Text, prio.Value, "$" + (credit.Value * 1000));
if (grid.Rows.Count > 0) grid.FirstDisplayedScrollingRowIndex = grid.Rows.Count - 1;
records++; status.Text = $" Saved account '{nm}'."; recLbl.Text = $"Records: {records} USER: SYSTEM ▌ CONNECTED ";
nameBox.Clear();
};
foreach (Control c in new Control[] { menu, tool, nameLbl, typeLbl, regionLbl, prioLbl, creditLbl,
nameBox, typeCmb, regionCmb, prio, credit, saveBtn, sigHdr, sig, grid, status, recLbl })
f.Controls.Add(c);
void Layout()
{
int W = f.ClientSize.Width, H = f.ClientSize.Height;
int X(double a) => (int)(W * a); int Y(double a) => (int)(H * a);
Rectangle R(double x0, double y0, double x1, double y1) => Rectangle.FromLTRB(X(x0), Y(y0), X(x1), Y(y1));
menu.Bounds = R(0, 0, 1, 0.045);
tool.Bounds = R(0, 0.045, 1, 0.095);
double lc = 0.13; // label/control split
nameLbl.Bounds = R(0, 0.105, lc, 0.16); nameBox.Bounds = R(lc, 0.105, 0.42, 0.16);
typeLbl.Bounds = R(0, 0.165, lc, 0.22); typeCmb.Bounds = R(lc, 0.167, 0.42, 0.22);
regionLbl.Bounds = R(0, 0.225, lc, 0.28); regionCmb.Bounds = R(lc, 0.227, 0.42, 0.28);
prioLbl.Bounds = R(0, 0.285, lc, 0.345); prio.Bounds = R(lc, 0.285, 0.42, 0.345);
creditLbl.Bounds = R(0, 0.35, lc, 0.41); credit.Bounds = R(lc, 0.35, 0.42, 0.41);
saveBtn.Bounds = R(0.04, 0.45, 0.40, 0.52);
sigHdr.Bounds = R(0.42, 0.095, 1, 0.135);
sig.Bounds = R(0.42, 0.135, 0.995, 0.42);
grid.Bounds = R(0.005, 0.55, 0.995, 0.93);
status.Bounds = R(0, 0.94, 0.5, 1);
recLbl.Bounds = R(0.5, 0.94, 1, 1);
float gfs = Math.Max(8, H / 70f);
grid.Font = new Font("Segoe UI", gfs);
grid.ColumnHeadersDefaultCellStyle.Font = new Font("Segoe UI", gfs, FontStyle.Bold);
}
f.Load += (s, e) => Layout();
f.Resize += (s, e) => Layout();
Application.Run(f);
}
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<AssemblyName>winforms-legacy</AssemblyName>
<ApplicationHighDpiMode>PerMonitorV2</ApplicationHighDpiMode>
</PropertyGroup>
</Project>
@@ -0,0 +1,131 @@
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Shapes;
// "Meridian CRM — Account Record" : WPF node (XAML / UIA). Real Slider /
// ComboBox / InkCanvas (doodle) / DataGrid (spreadsheet). Account Name TextBox
// + SAVE button are what cua-driver drives.
class Program
{
public class RowVM
{
public string Account { get; set; } public string Type { get; set; }
public string Region { get; set; } public int Pri { get; set; } public string Credit { get; set; }
}
[STAThread]
static void Main()
{
var app = new Application();
var rows = new ObservableCollection<RowVM>();
var gray = new SolidColorBrush(Color.FromRgb(0xE0, 0xE0, 0xE0));
// menu
var menu = new Menu();
foreach (var m in new[] { "File", "Edit", "View", "Record", "Tools", "Help" })
menu.Items.Add(new MenuItem { Header = m });
// toolbar
var tray = new ToolBarTray();
var tb = new ToolBar();
foreach (var t in new[] { "New", "Open", "Save", "Delete", "|", "◀ Prev", "Next ▶", "|", "Find" })
tb.Items.Add(t == "|" ? (object)new Separator() : new Button { Content = t, Padding = new Thickness(6, 1, 6, 1) });
tray.ToolBars.Add(tb);
// ── left form (dense grid; cell borders only) ───────────────────────
var form = new Grid { Background = Brushes.White };
form.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(120) });
form.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
TextBlock lbl(string t) => new TextBlock { Text = " " + t, VerticalAlignment = VerticalAlignment.Center, Background = new SolidColorBrush(Color.FromRgb(0xF0, 0xF0, 0xF0)) };
int r = 0;
void AddRow(string label, FrameworkElement c, double h = 30)
{
form.RowDefinitions.Add(new RowDefinition { Height = new GridLength(h) });
var b1 = new Border { BorderBrush = gray, BorderThickness = new Thickness(0, 0, 1, 1), Child = lbl(label) };
Grid.SetRow(b1, r); Grid.SetColumn(b1, 0); form.Children.Add(b1);
var b2 = new Border { BorderBrush = gray, BorderThickness = new Thickness(0, 0, 0, 1), Child = c };
Grid.SetRow(b2, r); Grid.SetColumn(b2, 1); form.Children.Add(b2);
r++;
}
var nameBox = new TextBox { BorderThickness = new Thickness(0), VerticalContentAlignment = VerticalAlignment.Center };
var typeCmb = new ComboBox { BorderThickness = new Thickness(0) };
foreach (var s in new[] { "Enterprise", "SMB", "Government", "Reseller" }) typeCmb.Items.Add(s); typeCmb.SelectedIndex = 0;
var regionCmb = new ComboBox { BorderThickness = new Thickness(0) };
foreach (var s in new[] { "North", "South", "EMEA", "APAC", "LATAM" }) regionCmb.Items.Add(s); regionCmb.SelectedIndex = 0;
var priority = new Slider { Minimum = 1, Maximum = 5, Value = 3, TickFrequency = 1, IsSnapToTickEnabled = true, TickPlacement = TickPlacement.BottomRight, VerticalAlignment = VerticalAlignment.Center };
var credit = new Slider { Minimum = 0, Maximum = 100, Value = 40, TickFrequency = 10, TickPlacement = TickPlacement.BottomRight, VerticalAlignment = VerticalAlignment.Center };
AddRow("Account Name", nameBox);
AddRow("Account Type", typeCmb);
AddRow("Region", regionCmb);
AddRow("Priority (1-5)", priority);
AddRow("Credit Limit", credit);
var saveBtn = new Button { Content = "Add Record", FontWeight = FontWeights.Bold };
form.RowDefinitions.Add(new RowDefinition { Height = new GridLength(40) });
Grid.SetRow(saveBtn, r); Grid.SetColumn(saveBtn, 0); Grid.SetColumnSpan(saveBtn, 2); form.Children.Add(saveBtn);
// ── right top: line-tool sketch (one straight segment per drag) ─────
var ink = new Canvas { Background = Brushes.White, ClipToBounds = true };
Point? dn = null; Line preview = null;
ink.MouseLeftButtonDown += (s, e) => { dn = e.GetPosition(ink); };
ink.MouseMove += (s, e) => { if (dn != null) { var p = e.GetPosition(ink);
if (preview == null) { preview = new Line { Stroke = Brushes.MidnightBlue, StrokeThickness = 1, Opacity = 0.45 }; ink.Children.Add(preview); }
preview.X1 = dn.Value.X; preview.Y1 = dn.Value.Y; preview.X2 = p.X; preview.Y2 = p.Y; } };
ink.MouseLeftButtonUp += (s, e) => { if (dn != null) { var p = e.GetPosition(ink);
ink.Children.Add(new Line { Stroke = Brushes.MidnightBlue, StrokeThickness = 2, X1 = dn.Value.X, Y1 = dn.Value.Y, X2 = p.X, Y2 = p.Y });
if (preview != null) { ink.Children.Remove(preview); preview = null; }
dn = null; } };
var sigHdr = new TextBlock { Text = " Signature / Notes", FontWeight = FontWeights.Bold, Background = new SolidColorBrush(Color.FromRgb(0xE1,0xE1,0xE1)), Padding = new Thickness(2) };
var sigDock = new DockPanel(); DockPanel.SetDock(sigHdr, Dock.Top); sigDock.Children.Add(sigHdr); sigDock.Children.Add(ink);
// ── right bottom: spreadsheet ───────────────────────────────────────
var dg = new DataGrid { AutoGenerateColumns = false, ItemsSource = rows, IsReadOnly = true, GridLinesVisibility = DataGridGridLinesVisibility.All, HeadersVisibility = DataGridHeadersVisibility.Column };
dg.Columns.Add(new DataGridTextColumn { Header = "Account", Binding = new System.Windows.Data.Binding("Account"), Width = new DataGridLength(1, DataGridLengthUnitType.Star) });
dg.Columns.Add(new DataGridTextColumn { Header = "Type", Binding = new System.Windows.Data.Binding("Type") });
dg.Columns.Add(new DataGridTextColumn { Header = "Region", Binding = new System.Windows.Data.Binding("Region") });
dg.Columns.Add(new DataGridTextColumn { Header = "Pri", Binding = new System.Windows.Data.Binding("Pri") });
dg.Columns.Add(new DataGridTextColumn { Header = "Credit", Binding = new System.Windows.Data.Binding("Credit") });
var rightGrid = new Grid();
rightGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(150) });
rightGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(3) });
rightGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
Grid.SetRow(sigDock, 0); rightGrid.Children.Add(sigDock);
var gs2 = new GridSplitter { Height = 3, HorizontalAlignment = HorizontalAlignment.Stretch, Background = gray }; Grid.SetRow(gs2, 1); rightGrid.Children.Add(gs2);
Grid.SetRow(dg, 2); rightGrid.Children.Add(dg);
var main = new Grid();
main.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(380) });
main.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(3) });
main.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
Grid.SetColumn(form, 0); main.Children.Add(form);
var gs1 = new GridSplitter { Width = 3, Background = gray }; Grid.SetColumn(gs1, 1); main.Children.Add(gs1);
Grid.SetColumn(rightGrid, 2); main.Children.Add(rightGrid);
// status bar
var statusBar = new StatusBar();
var statusLbl = new StatusBarItem { Content = "Ready" };
var recLbl = new StatusBarItem { Content = "Records: 0", HorizontalAlignment = HorizontalAlignment.Right };
statusBar.Items.Add(statusLbl);
statusBar.Items.Add(new StatusBarItem { Content = "USER: SYSTEM ▌ CONNECTED", HorizontalAlignment = HorizontalAlignment.Right });
statusBar.Items.Add(recLbl);
int records = 0;
saveBtn.Click += (s, e) =>
{
string nm = (nameBox.Text ?? "").Trim(); if (nm.Length == 0) nm = "(unnamed)";
rows.Add(new RowVM { Account = nm, Type = typeCmb.Text, Region = regionCmb.Text, Pri = (int)priority.Value, Credit = "$" + ((int)credit.Value * 1000) });
records++; recLbl.Content = $"Records: {records}"; statusLbl.Content = $"Saved account '{nm}'.";
nameBox.Text = "";
};
var dock = new DockPanel();
DockPanel.SetDock(menu, Dock.Top); DockPanel.SetDock(tray, Dock.Top); DockPanel.SetDock(statusBar, Dock.Bottom);
dock.Children.Add(menu); dock.Children.Add(tray); dock.Children.Add(statusBar); dock.Children.Add(main);
var win = new Window { Title = "Meridian CRM — Account Record [WPF]", Width = 960, Height = 600, Content = dock, WindowStartupLocation = WindowStartupLocation.Manual };
app.Run(win);
}
}
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<AssemblyName>wpf-legacy</AssemblyName>
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
</PropertyGroup>
</Project>
@@ -0,0 +1,103 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; font-family: "Segoe UI", Tahoma, sans-serif; font-size: 13px; background: #f0f0f0; overflow: hidden; }
#app { display: grid; grid-template-rows: auto auto 1fr auto; height: 100vh; }
/* menu + toolbar */
#menu { background: #f7f7f7; border-bottom: 1px solid #c8c8c8; padding: 2px 0; }
#menu span { padding: 3px 10px; }
#menu span:hover { background: #cde2ff; }
#toolbar { background: #eee; border-bottom: 1px solid #c8c8c8; display: flex; }
#toolbar button { border: 1px solid transparent; background: transparent; padding: 3px 9px; }
#toolbar button:hover { border: 1px solid #b0c4de; background: #e6eefb; }
#toolbar .sep { width: 1px; background: #c8c8c8; margin: 2px 3px; }
/* main split */
#main { display: grid; grid-template-columns: 380px 1px 1fr; overflow: hidden; }
#split { background: #c8c8c8; }
/* dense form: label | control rows, only cell borders */
#form { display: grid; grid-template-columns: 120px 1fr; grid-auto-rows: 30px; background: #fff; align-content: start; }
#form .lbl { background: #f0f0f0; border-right: 1px solid #e0e0e0; border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; padding-left: 8px; }
#form .cell { border-bottom: 1px solid #e0e0e0; display: flex; align-items: center; }
#form .cell > * { width: 100%; height: 100%; border: 0; background: transparent; padding: 0 6px; font: inherit; }
#form input[type=range] { padding: 0; }
#saveRow { grid-column: 1 / span 2; }
#save { width: 100%; height: 38px; font-weight: bold; }
/* right side */
#right { display: grid; grid-template-rows: 150px 1px 1fr; overflow: hidden; }
#sigHdr { background: #e1e1e1; font-weight: bold; padding: 3px 6px; border-bottom: 1px solid #c8c8c8; }
#sigWrap { display: grid; grid-template-rows: auto 1fr; }
#doodle { width: 100%; height: 100%; background: #fff; display: block; }
table { border-collapse: collapse; width: 100%; background: #fff; }
th, td { border: 1px solid #d2d2d2; padding: 2px 6px; text-align: left; font-weight: normal; }
th { background: #eef1f5; font-weight: 600; }
#gridWrap { overflow: auto; }
#status { background: #e8e8e8; border-top: 1px solid #c8c8c8; display: flex; justify-content: space-between; padding: 3px 8px; }
</style>
</head>
<body>
<div id="app">
<div id="menu"><span>File</span><span>Edit</span><span>View</span><span>Record</span><span>Tools</span><span>Help</span></div>
<div id="toolbar">
<button>New</button><button>Open</button><button>Save</button><button>Delete</button>
<div class="sep"></div><button>◀ Prev</button><button>Next ▶</button>
<div class="sep"></div><button>Find</button>
</div>
<div id="main">
<div id="form">
<div class="lbl">Account Name</div><div class="cell"><input id="acct" type="text" /></div>
<div class="lbl">Account Type</div><div class="cell"><select id="type"><option>Enterprise</option><option>SMB</option><option>Government</option><option>Reseller</option></select></div>
<div class="lbl">Region</div><div class="cell"><select id="region"><option>North</option><option>South</option><option>EMEA</option><option>APAC</option><option>LATAM</option></select></div>
<div class="lbl">Priority (1-5)</div><div class="cell"><input id="pri" type="range" min="1" max="5" value="3" /></div>
<div class="lbl">Credit Limit</div><div class="cell"><input id="credit" type="range" min="0" max="100" value="40" /></div>
<div id="saveRow" class="cell"><button id="save">Add Record</button></div>
</div>
<div id="split"></div>
<div id="right">
<div id="sigWrap"><div id="sigHdr">Signature / Notes</div><canvas id="doodle"></canvas></div>
<div id="split"></div>
<div id="gridWrap">
<table id="grid"><thead><tr><th>Account</th><th>Type</th><th>Region</th><th>Pri</th><th>Credit</th></tr></thead><tbody></tbody></table>
</div>
</div>
</div>
<div id="status"><span id="statusMsg">Ready</span><span>USER: SYSTEM ▌ CONNECTED</span><span id="recCount">Records: 0</span></div>
</div>
<script>
// line tool: one straight segment per press-drag-release (down→up)
const cv = document.getElementById('doodle');
const ctx = cv.getContext('2d');
let segs = [], dn = null, cur = null;
function redraw() {
ctx.clearRect(0, 0, cv.width, cv.height);
ctx.strokeStyle = '#191970'; ctx.lineWidth = 2;
for (const s of segs) { ctx.beginPath(); ctx.moveTo(s.x0, s.y0); ctx.lineTo(s.x1, s.y1); ctx.stroke(); }
if (dn && cur) { ctx.save(); ctx.globalAlpha = 0.45; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(dn.x, dn.y); ctx.lineTo(cur.x, cur.y); ctx.stroke(); ctx.restore(); }
}
function fit() { cv.width = cv.clientWidth; cv.height = cv.clientHeight; redraw(); }
window.addEventListener('resize', fit); requestAnimationFrame(fit);
// Pointer events (not mouse): coordinate-routed background injection arrives
// as PEN/touch pointers; pointer* covers mouse + pen + touch uniformly.
cv.style.touchAction = 'none';
cv.addEventListener('pointerdown', e => { dn = { x: e.offsetX, y: e.offsetY }; cur = dn; });
cv.addEventListener('pointermove', e => { if (dn) { cur = { x: e.offsetX, y: e.offsetY }; redraw(); } });
cv.addEventListener('pointerup', e => { if (dn) { segs.push({ x0: dn.x, y0: dn.y, x1: e.offsetX, y1: e.offsetY }); dn = null; cur = null; redraw(); } });
// save
let records = 0;
const acct = document.getElementById('acct'), tbody = document.querySelector('#grid tbody');
document.getElementById('save').addEventListener('click', () => {
let nm = (acct.value || '').trim(); if (!nm) nm = '(unnamed)';
const tr = document.createElement('tr');
const credit = '$' + (document.getElementById('credit').value * 1000);
[nm, document.getElementById('type').value, document.getElementById('region').value, document.getElementById('pri').value, credit]
.forEach(v => { const td = document.createElement('td'); td.textContent = v; tr.appendChild(td); });
tbody.appendChild(tr);
records++; document.getElementById('recCount').textContent = `Records: ${records}`;
document.getElementById('statusMsg').textContent = `Saved account '${nm}'.`;
acct.value = '';
});
</script>
</body>
</html>
@@ -0,0 +1,24 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 560,
resizable: true,
title: 'Meridian CRM — Account Record [Electron]',
autoHideMenuBar: true,
webPreferences: { contextIsolation: true },
});
win.setMenuBarVisibility(false);
win.loadFile(path.join(__dirname, 'index.html'));
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => app.quit());
@@ -0,0 +1,12 @@
{
"name": "cua-demo-electron-legacy",
"version": "0.1.0",
"private": true,
"main": "main.js",
"scripts": {
"start": "electron ."
},
"devDependencies": {
"electron": "^39.8.5"
}
}
@@ -0,0 +1,21 @@
[package]
name = "legacy-app"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "legacy-app"
path = "src/main.rs"
required-features = ["windows-app"]
[features]
default = []
windows-app = []
[dependencies]
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
] }
@@ -0,0 +1,343 @@
//! "Meridian CRM — Account Record" — a dense, realistic Windows office form for
//! the cua-driver multi-cursor demo. Menu bar, toolbar, a packed left form
//! (Account Name, Account Type / Region dropdowns, Priority / Credit sliders),
//! a SAVE button, a signature doodle pad, a spreadsheet grid, and a status bar.
//! No padding between elements beyond their 1px borders.
//!
//! Modes (argv[1]):
//! gdi <node> — fully GDI-drawn (NO accessibility tree → cua-driver drives
//! it via the pixel path). The Account-Name field and SAVE
//! button are drawn; the doodle pad is live.
//! master <node> — same chrome, but Account Name + SAVE are real Win32
//! controls, instrumented to EMIT the user's action on stdout
//! (TYPE<text>, CLICK<rx><ry>) for the orchestrator to replay.
#![windows_subsystem = "windows"]
use std::cell::RefCell;
use std::io::Write;
use windows::core::{w, PCWSTR};
use windows::Win32::Foundation::{COLORREF, HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
use windows::Win32::Graphics::Gdi::{
BeginPaint, CreateFontW, CreatePen, CreateSolidBrush, DeleteObject, DrawTextW, EndPaint, FillRect,
FrameRect, InvalidateRect, LineTo, MoveToEx, SelectObject, SetBkMode, SetTextColor, DT_CENTER, DT_LEFT,
DT_SINGLELINE, DT_VCENTER, GetStockObject, HBRUSH, HFONT, PAINTSTRUCT, PS_SOLID, TRANSPARENT, BLACK_BRUSH,
};
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::*;
// ── dense fractional layout (shared by gdi + master) ──────────────────────────
const MENU_B: f64 = 0.05;
const TOOL_B: f64 = 0.10;
// left form rows
const NAME: [f64; 4] = [0.16, 0.110, 0.41, 0.170]; // Account Name field box
const TYPE: [f64; 4] = [0.16, 0.175, 0.41, 0.235];
const REGION: [f64; 4] = [0.16, 0.240, 0.41, 0.300];
const PRIO: [f64; 4] = [0.16, 0.305, 0.41, 0.365];
const CREDIT: [f64; 4] = [0.16, 0.370, 0.41, 0.430];
const SAVE: [f64; 4] = [0.04, 0.460, 0.40, 0.530]; // SAVE button
const DOODLE: [f64; 4] = [0.42, 0.140, 0.99, 0.420];
const GRID: [f64; 4] = [0.01, 0.560, 0.99, 0.930];
const STATUS_T: f64 = 0.94;
// SAVE button relative center (master emits this for the CLICK event).
const SAVE_CX: f64 = (SAVE[0] + SAVE[2]) / 2.0;
const SAVE_CY: f64 = (SAVE[1] + SAVE[3]) / 2.0;
const ID_EDIT: isize = 1001;
const ID_BUTTON: isize = 1002;
#[derive(Clone, Copy, PartialEq)]
enum Mode { Gdi, Master }
struct State {
mode: Mode,
node: String,
records: Vec<[String; 5]>,
name: String,
// line tool: one straight segment per press-drag-release (down→up)
segs: Vec<(POINT, POINT)>,
down: Option<POINT>,
cur: POINT,
hedit: HWND,
hbtn: HWND,
f_title: HFONT,
f_label: HFONT,
f_mono: HFONT,
cw: i32,
ch: i32,
}
thread_local! { static STATE: RefCell<Option<State>> = const { RefCell::new(None) }; }
fn emit(line: &str) { let _ = writeln!(std::io::stdout(), "{line}"); let _ = std::io::stdout().flush(); }
fn wide(s: &str) -> Vec<u16> { s.encode_utf16().chain(std::iter::once(0)).collect() }
fn fr(cw: i32, ch: i32, r: [f64; 4]) -> RECT {
RECT { left: (cw as f64 * r[0]) as i32, top: (ch as f64 * r[1]) as i32,
right: (cw as f64 * r[2]) as i32, bottom: (ch as f64 * r[3]) as i32 }
}
fn band(cw: i32, ch: i32, y0: f64, y1: f64) -> RECT {
RECT { left: 0, top: (ch as f64 * y0) as i32, right: cw, bottom: (ch as f64 * y1) as i32 }
}
fn pt_in(r: &RECT, x: i32, y: i32) -> bool { x >= r.left && x < r.right && y >= r.top && y < r.bottom }
unsafe fn mk_font(h: i32, w: i32, face: PCWSTR) -> HFONT { CreateFontW(h, 0, 0, 0, w, 0, 0, 0, 0, 0, 0, 0, 0, face) }
fn main() {
let args: Vec<String> = std::env::args().collect();
let mode = if args.get(1).map(|s| s.as_str()) == Some("master") { Mode::Master } else { Mode::Gdi };
let node = args.get(2).cloned().unwrap_or_else(|| match mode { Mode::Master => "Master".into(), Mode::Gdi => "Win32 GDI".into() });
unsafe {
let hmod = GetModuleHandleW(None).unwrap();
let class = w!("MeridianCrmForm");
let bg = CreateSolidBrush(COLORREF(0x00F0F0F0));
let wc = WNDCLASSW { lpfnWndProc: Some(wnd_proc), hInstance: hmod.into(), lpszClassName: class,
hbrBackground: bg, hCursor: LoadCursorW(None, IDC_ARROW).unwrap_or_default(), ..Default::default() };
RegisterClassW(&wc);
STATE.with(|s| *s.borrow_mut() = Some(State { mode, node: node.clone(), records: Vec::new(),
name: String::new(), segs: Vec::new(), down: None, cur: POINT { x: 0, y: 0 }, hedit: HWND::default(), hbtn: HWND::default(),
f_title: HFONT::default(), f_label: HFONT::default(), f_mono: HFONT::default(), cw: 0, ch: 0 }));
let title = format!("Meridian CRM — Account Record [{node}]");
let tw = wide(&title);
let hwnd = CreateWindowExW(WINDOW_EX_STYLE(0), class, PCWSTR(tw.as_ptr()), WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 960, 600, None, None, HINSTANCE(hmod.0), None).expect("CreateWindowExW");
let _ = ShowWindow(hwnd, SW_SHOWNORMAL);
let mut msg = MSG::default();
while GetMessageW(&mut msg, None, 0, 0).as_bool() { let _ = TranslateMessage(&msg); DispatchMessageW(&msg); }
}
}
unsafe fn create_master_controls(parent: HWND) {
let hinst = HINSTANCE(GetModuleHandleW(None).unwrap().0);
let hedit = CreateWindowExW(WS_EX_CLIENTEDGE, w!("EDIT"), w!(""),
WS_CHILD | WS_VISIBLE | WINDOW_STYLE(ES_AUTOHSCROLL as u32),
0, 0, 10, 10, parent, HMENU(ID_EDIT as *mut core::ffi::c_void), hinst, None).unwrap_or_default();
let hbtn = CreateWindowExW(WINDOW_EX_STYLE(0), w!("BUTTON"), w!("Add Record"),
WS_CHILD | WS_VISIBLE | WINDOW_STYLE(BS_PUSHBUTTON as u32),
0, 0, 10, 10, parent, HMENU(ID_BUTTON as *mut core::ffi::c_void), hinst, None).unwrap_or_default();
STATE.with(|s| if let Some(st) = s.borrow_mut().as_mut() { st.hedit = hedit; st.hbtn = hbtn; });
}
unsafe fn relayout(hwnd: HWND, cw: i32, ch: i32) {
STATE.with(|s| {
let mut b = s.borrow_mut(); let Some(st) = b.as_mut() else { return };
st.cw = cw; st.ch = ch;
for f in [st.f_title, st.f_label, st.f_mono] { if !f.is_invalid() { let _ = DeleteObject(f); } }
let seg = wide("Segoe UI"); let mono = wide("Consolas");
st.f_title = mk_font(-(ch / 34).clamp(12, 22), 700, PCWSTR(seg.as_ptr()));
st.f_label = mk_font(-(ch / 40).clamp(11, 18), 400, PCWSTR(seg.as_ptr()));
st.f_mono = mk_font(-(ch / 44).clamp(10, 16), 400, PCWSTR(mono.as_ptr()));
if st.mode == Mode::Master && !st.hedit.0.is_null() {
let n = fr(cw, ch, NAME);
let _ = MoveWindow(st.hedit, n.left + 2, n.top + 2, n.right - n.left - 4, n.bottom - n.top - 4, true);
let sv = fr(cw, ch, SAVE);
let _ = MoveWindow(st.hbtn, sv.left, sv.top, sv.right - sv.left, sv.bottom - sv.top, true);
SendMessageW(st.hedit, WM_SETFONT, WPARAM(st.f_label.0 as usize), LPARAM(1));
SendMessageW(st.hbtn, WM_SETFONT, WPARAM(st.f_label.0 as usize), LPARAM(1));
}
});
let _ = InvalidateRect(hwnd, None, true);
}
extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
unsafe {
match msg {
WM_CREATE => { if STATE.with(|s| s.borrow().as_ref().map(|st| st.mode)) == Some(Mode::Master) { create_master_controls(hwnd); } LRESULT(0) }
WM_SIZE => { let (cw, ch) = ((lp.0 & 0xFFFF) as i16 as i32, ((lp.0 >> 16) & 0xFFFF) as i16 as i32); if cw > 0 && ch > 0 { relayout(hwnd, cw, ch); } LRESULT(0) }
WM_COMMAND => { if (wp.0 & 0xFFFF) as isize == ID_BUTTON && ((wp.0 >> 16) & 0xFFFF) as u32 == BN_CLICKED { on_submit(hwnd); } LRESULT(0) }
WM_LBUTTONDOWN => {
let (x, y) = ((lp.0 & 0xFFFF) as i16 as i32, ((lp.0 >> 16) & 0xFFFF) as i16 as i32);
let (mode, cw, ch) = STATE.with(|s| { let b = s.borrow(); let st = b.as_ref().unwrap(); (st.mode, st.cw, st.ch) });
if mode == Mode::Gdi && pt_in(&fr(cw, ch, SAVE), x, y) { commit(hwnd); }
else if pt_in(&fr(cw, ch, DOODLE), x, y) {
// line tool: remember the press point; commit on button-up.
STATE.with(|s| if let Some(st) = s.borrow_mut().as_mut() { st.down = Some(POINT { x, y }); st.cur = POINT { x, y }; });
}
LRESULT(0)
}
WM_MOUSEMOVE => {
if (wp.0 & 0x0001) != 0 {
let (x, y) = ((lp.0 & 0xFFFF) as i16 as i32, ((lp.0 >> 16) & 0xFFFF) as i16 as i32);
let go = STATE.with(|s| { let mut b = s.borrow_mut(); let st = b.as_mut().unwrap();
if st.down.is_some() { st.cur = POINT { x, y }; true } else { false } });
if go { let (cw, ch) = STATE.with(|s| { let b = s.borrow(); (b.as_ref().unwrap().cw, b.as_ref().unwrap().ch) });
let d = fr(cw, ch, DOODLE); let _ = InvalidateRect(hwnd, Some(&d), false); }
}
LRESULT(0)
}
WM_LBUTTONUP => {
let (x, y) = ((lp.0 & 0xFFFF) as i16 as i32, ((lp.0 >> 16) & 0xFFFF) as i16 as i32);
STATE.with(|s| if let Some(st) = s.borrow_mut().as_mut() {
if let Some(p0) = st.down.take() { st.segs.push((p0, POINT { x, y })); } });
let (cw, ch) = STATE.with(|s| { let b = s.borrow(); (b.as_ref().unwrap().cw, b.as_ref().unwrap().ch) });
let d = fr(cw, ch, DOODLE); let _ = InvalidateRect(hwnd, Some(&d), false);
LRESULT(0)
}
WM_CHAR => {
if STATE.with(|s| s.borrow().as_ref().map(|st| st.mode)) == Some(Mode::Gdi) {
let ch = char::from_u32(wp.0 as u32).unwrap_or('\0');
STATE.with(|s| if let Some(st) = s.borrow_mut().as_mut() {
match ch { '\u{8}' => { st.name.pop(); }, '\r' | '\n' => {}, c if !c.is_control() => st.name.push(c), _ => {} } });
let (cw, ch2) = STATE.with(|s| { let b = s.borrow(); (b.as_ref().unwrap().cw, b.as_ref().unwrap().ch) });
let n = fr(cw, ch2, NAME); let _ = InvalidateRect(hwnd, Some(&n), false);
}
LRESULT(0)
}
WM_PAINT => { paint(hwnd); LRESULT(0) }
WM_DESTROY => { PostQuitMessage(0); LRESULT(0) }
_ => DefWindowProcW(hwnd, msg, wp, lp),
}
}
}
unsafe fn on_submit(hwnd: HWND) {
let text = STATE.with(|s| { let b = s.borrow(); let st = b.as_ref()?; if st.hedit.0.is_null() { return None; }
let len = GetWindowTextLengthW(st.hedit); let mut buf = vec![0u16; (len + 1) as usize];
let n = GetWindowTextW(st.hedit, &mut buf); Some(String::from_utf16_lossy(&buf[..n as usize])) });
if let Some(t) = text {
emit(&format!("TYPE\t{t}"));
emit(&format!("CLICK\t{SAVE_CX:.4}\t{SAVE_CY:.4}"));
STATE.with(|s| if let Some(st) = s.borrow_mut().as_mut() { st.name = t; });
commit(hwnd);
STATE.with(|s| if let Some(st) = s.borrow().as_ref() { if !st.hedit.0.is_null() { let _ = SetWindowTextW(st.hedit, w!("")); } });
}
}
unsafe fn commit(hwnd: HWND) {
STATE.with(|s| if let Some(st) = s.borrow_mut().as_mut() {
let nm = if st.name.trim().is_empty() { "(unnamed)".into() } else { st.name.trim().chars().take(22).collect::<String>() };
st.records.push([nm, "Enterprise".into(), "North".into(), "3".into(), "$40,000".into()]);
st.name.clear();
});
let _ = InvalidateRect(hwnd, None, true);
}
unsafe fn line(hdc: windows::Win32::Graphics::Gdi::HDC, x: i32, y: i32, text: &str) {
let mut r = RECT { left: x, top: y, right: x + 6000, bottom: y + 60 }; let mut t = wide(text);
DrawTextW(hdc, &mut t, &mut r, DT_LEFT | DT_SINGLELINE);
}
unsafe fn paint(hwnd: HWND) {
let mut ps = PAINTSTRUCT::default();
let hdc = BeginPaint(hwnd, &mut ps);
SetBkMode(hdc, TRANSPARENT);
STATE.with(|s| {
let b = s.borrow(); let Some(st) = b.as_ref() else { return };
let (cw, ch) = (st.cw.max(1), st.ch.max(1));
let face = CreateSolidBrush(COLORREF(0x00F0F0F0));
let bar = CreateSolidBrush(COLORREF(0x00E8E8E8));
let hdr = CreateSolidBrush(COLORREF(0x00F5F1EE));
let white = CreateSolidBrush(COLORREF(0x00FFFFFF));
let line_c = CreateSolidBrush(COLORREF(0x00D2D2D2));
let labelbg = CreateSolidBrush(COLORREF(0x00F0F0F0));
let black = GetStockObject(BLACK_BRUSH);
let sel = CreateSolidBrush(COLORREF(0x00F5C28A)); // accent for slider thumb / save
let frame = |r: &RECT| { FrameRect(hdc, r, HBRUSH(black.0)); };
let hline = |y: i32| { let r = RECT { left: 0, top: y, right: cw, bottom: y + 1 }; FillRect(hdc, &r, line_c); };
let vline = |x: i32, y0: i32, y1: i32| { let r = RECT { left: x, top: y0, right: x + 1, bottom: y1 }; FillRect(hdc, &r, line_c); };
// menu bar
let mut m = band(cw, ch, 0.0, MENU_B); FillRect(hdc, &m, bar);
SelectObject(hdc, st.f_label); SetTextColor(hdc, COLORREF(0x00000000));
let mut mt = wide(" File Edit View Record Tools Help");
DrawTextW(hdc, &mut mt, &mut m, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
hline((ch as f64 * MENU_B) as i32);
// toolbar
let mut tb = band(cw, ch, MENU_B, TOOL_B); FillRect(hdc, &tb, face);
let mut tt = wide(" New Open Save Delete │ ◀ Prev Next ▶ │ Find");
DrawTextW(hdc, &mut tt, &mut tb, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
hline((ch as f64 * TOOL_B) as i32);
// ── left form ──
let label = |r: [f64; 4], text: &str| {
let lr = RECT { left: (cw as f64 * 0.01) as i32, top: (ch as f64 * r[1]) as i32,
right: (cw as f64 * (r[0] - 0.005)) as i32, bottom: (ch as f64 * r[3]) as i32 };
FillRect(hdc, &lr, labelbg);
let mut t = wide(text);
let mut tr = RECT { left: lr.left + 6, ..lr };
DrawTextW(hdc, &mut t, &mut tr, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
};
let dropdown = |r: [f64; 4], val: &str| {
let bx = fr(cw, ch, r); FillRect(hdc, &bx, white); frame(&bx);
let mut tr = bx; tr.left += 6; tr.right -= 24; let mut t = wide(val);
DrawTextW(hdc, &mut t, &mut tr, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
let ar = RECT { left: bx.right - 20, top: bx.top, right: bx.right, bottom: bx.bottom };
FillRect(hdc, &ar, bar); frame(&ar); let mut a = wide("");
DrawTextW(hdc, &mut a, &mut ar.clone(), DT_CENTER | DT_VCENTER | DT_SINGLELINE);
};
let slider = |r: [f64; 4], frac: f64| {
let bx = fr(cw, ch, r); let midy = (bx.top + bx.bottom) / 2;
let track = RECT { left: bx.left + 4, top: midy - 1, right: bx.right - 4, bottom: midy + 1 }; FillRect(hdc, &track, line_c);
let tx = bx.left + 4 + ((bx.right - bx.left - 8) as f64 * frac) as i32;
let thumb = RECT { left: tx - 4, top: bx.top + 6, right: tx + 4, bottom: bx.bottom - 6 }; FillRect(hdc, &thumb, sel); frame(&thumb);
};
SetTextColor(hdc, COLORREF(0x00000000));
label(NAME, "Account Name"); label(TYPE, "Account Type"); label(REGION, "Region");
label(PRIO, "Priority"); label(CREDIT, "Credit Limit");
if st.mode == Mode::Gdi {
let n = fr(cw, ch, NAME); FillRect(hdc, &n, white); frame(&n);
let mut tr = n; tr.left += 6; let mut nm = wide(&st.name);
DrawTextW(hdc, &mut nm, &mut tr, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
let sv = fr(cw, ch, SAVE); FillRect(hdc, &sv, sel); frame(&sv);
let mut bt = wide("Add Record"); SelectObject(hdc, st.f_title);
DrawTextW(hdc, &mut bt, &mut sv.clone(), DT_CENTER | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, st.f_label);
}
// dropdowns + sliders are visual in both modes
dropdown(TYPE, "Enterprise"); dropdown(REGION, "North");
slider(PRIO, 0.5); slider(CREDIT, 0.4);
// ── signature doodle ──
let dh = RECT { left: (cw as f64 * DOODLE[0]) as i32, top: (ch as f64 * 0.105) as i32,
right: (cw as f64 * DOODLE[2]) as i32, bottom: (ch as f64 * DOODLE[1]) as i32 };
FillRect(hdc, &dh, hdr); let mut sh = wide(" Signature / Notes");
SelectObject(hdc, st.f_title); DrawTextW(hdc, &mut sh, &mut dh.clone(), DT_LEFT | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, st.f_label);
let dz = fr(cw, ch, DOODLE); FillRect(hdc, &dz, white); frame(&dz);
// line-tool segments (blue, 2px) + thin rubber-band preview while drawing
let pen = CreatePen(PS_SOLID, 2, COLORREF(0x00701919));
let old_pen = SelectObject(hdc, pen);
for (a, b) in &st.segs { let _ = MoveToEx(hdc, a.x, a.y, None); let _ = LineTo(hdc, b.x, b.y); }
if let Some(a) = st.down { let _ = MoveToEx(hdc, a.x, a.y, None); let _ = LineTo(hdc, st.cur.x, st.cur.y); }
SelectObject(hdc, old_pen); let _ = DeleteObject(pen);
// ── spreadsheet ──
let g = fr(cw, ch, GRID); FillRect(hdc, &g, white); frame(&g);
let cols = [("Account", 0.45), ("Type", 0.62), ("Region", 0.76), ("Pri", 0.85), ("Credit", 0.99)];
let cx = |fx: f64| (cw as f64 * (GRID[0] + (GRID[2] - GRID[0]) * ((fx - 0.0) / 1.0))) as i32; // placeholder
let _ = cx;
// column x positions in absolute (fractions are of full width but cols listed as cumulative within grid)
let gx = |fx: f64| (cw as f64 * fx) as i32;
let row_h = ((st.ch as f64 / 44.0).clamp(10.0, 16.0) * 1.7) as i32;
// header
let hrow = RECT { left: g.left, top: g.top, right: g.right, bottom: g.top + row_h }; FillRect(hdc, &hrow, hdr);
SelectObject(hdc, st.f_label);
let mut prev = g.left + 6;
for (name, fx) in cols { let mut t = wide(name); let mut tr = RECT { left: prev, top: g.top, right: gx(fx), bottom: g.top + row_h };
DrawTextW(hdc, &mut t, &mut tr, DT_LEFT | DT_VCENTER | DT_SINGLELINE); vline(gx(fx), g.top, g.bottom); prev = gx(fx) + 6; }
hline(g.top + row_h);
// rows
SelectObject(hdc, st.f_mono);
let mut y = g.top + row_h + 2;
for rec in &st.records {
if y + row_h > g.bottom { break; }
let xs = [g.left + 6, gx(0.45) + 6, gx(0.62) + 6, gx(0.76) + 6, gx(0.85) + 6];
for (i, val) in rec.iter().enumerate() { line(hdc, xs[i], y + 2, val); }
y += row_h; hline(y);
}
// ── status bar ──
let mut sb = band(cw, ch, STATUS_T, 1.0); FillRect(hdc, &sb, bar); hline((ch as f64 * STATUS_T) as i32);
SelectObject(hdc, st.f_label); SetTextColor(hdc, COLORREF(0x00000000));
let mut sbt = wide(&format!(" Ready │ Records: {} │ USER: SYSTEM ▌ CONNECTED │ [{}]", st.records.len(), st.node));
DrawTextW(hdc, &mut sbt, &mut sb, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
for o in [face, bar, hdr, white, line_c, labelbg, sel] { let _ = DeleteObject(o); }
});
let _ = EndPaint(hwnd, &ps);
}
@@ -0,0 +1,26 @@
[package]
name = "orchestrator"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "orchestrator"
path = "src/main.rs"
required-features = ["windows-app"]
[features]
default = []
windows-app = []
[dependencies]
# PNG decode + resize for the screenshot-based maze line-diff verification.
image = { version = "0.25", default-features = false, features = ["png"] }
windows = { version = "0.58", features = [
"Win32_Foundation",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
"Win32_Graphics_Dwm",
"Win32_System_Threading",
"Win32_System_JobObjects",
"Win32_Security",
] }
@@ -0,0 +1,567 @@
//! Multi-cursor background computer-use demo orchestrator.
//!
//! Launches the "National Records System" terminal in five UI frameworks: a 2x2
//! grid of background windows (each = half the work area) around one foreground
//! "master" in the middle that overlaps all four. When the human submits a
//! record in the master, the master emits the action on stdout; this
//! orchestrator replays it onto all four background corners concurrently, each
//! via its OWN cua-driver session (= its own uniquely-coloured agent cursor),
//! in the background — no window raised, the user's cursor never moved.
//!
//! Driving is element-based where an accessibility tree exists (set_value for
//! the SUBJECT-NAME field + UIA-Invoke for SUBMIT — no foreground steal, no
//! SendInput) and pixel-based for the GDI corner that has no a11y tree. Each
//! a11y corner is VERIFIED by reading the records grid back: if the typed
//! record didn't land, it's logged as FAIL.
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{channel, Sender};
use std::thread;
use std::time::{Duration, Instant};
use windows::core::PWSTR;
use windows::Win32::Foundation::{BOOL, HANDLE, HWND, LPARAM, POINT, RECT, TRUE};
use windows::Win32::Graphics::Dwm::{DwmGetWindowAttribute, DWMWA_EXTENDED_FRAME_BOUNDS};
use windows::Win32::Graphics::Gdi::ClientToScreen;
use windows::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, SetInformationJobObject,
JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetClientRect, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible,
SetForegroundWindow, SetWindowPos, HWND_TOP, SWP_NOACTIVATE, SWP_NOZORDER, SWP_SHOWWINDOW,
};
const FIELD_FRAC: (f64, f64) = (0.28, 0.145); // Account-Name field (GDI pixel fallback)
// ── maze (line tool) ───────────────────────────────────────────────────────────
// A spiral maze as straight segments in [0,1] of the drawing REGION. cua-driver
// draws each as one press-drag-release (no curves = "line tool only"). The same
// segments rasterized are the reference the screenshot is diffed against.
const MAZE: [(f64, f64, f64, f64); 7] = [
(0.10, 0.12, 0.90, 0.12),
(0.90, 0.12, 0.90, 0.88),
(0.90, 0.88, 0.26, 0.88),
(0.26, 0.88, 0.26, 0.40),
(0.26, 0.40, 0.66, 0.40),
(0.66, 0.40, 0.66, 0.64),
(0.66, 0.64, 0.46, 0.64),
];
// Client-fraction rect of the window that sits INSIDE every framework's drawing
// pad (all four put the doodle top-right). cua-driver draws here and the
// verifier crops the same rect — so the rasterized reference aligns regardless
// of where each framework actually lays its canvas out. Tuned via screenshots.
// Kept inside the SHORTEST drawing pad: WPF/Electron host theirs in a fixed
// ~150px row, so the usable band ends well above the spreadsheet below it.
// Overshooting here drops the lower maze onto the grid (and selects rows).
const REGION: [f64; 4] = [0.470, 0.180, 0.900, 0.295]; // l, t, r, b (client fractions)
// A drawn maze must score at least this (F1 of ink overlap vs the reference,
// dilated) to count as COMMITTED; below it = FAIL.
const MAZE_PASS: f64 = 0.45;
// ── kill-on-exit job ──────────────────────────────────────────────────────────
static JOB: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
fn job() -> HANDLE {
let raw = *JOB.get_or_init(|| unsafe {
let h = CreateJobObjectW(None, windows::core::PCWSTR::null()).expect("CreateJobObjectW");
let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let _ = SetInformationJobObject(h, JobObjectExtendedLimitInformation,
&info as *const _ as *const core::ffi::c_void,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32);
h.0 as usize
});
HANDLE(raw as *mut core::ffi::c_void)
}
fn assign_to_job(child: &Child) {
use std::os::windows::io::AsRawHandle;
unsafe { let _ = AssignProcessToJobObject(job(), HANDLE(child.as_raw_handle() as *mut core::ffi::c_void)); }
}
struct Corner {
title: &'static str, // unique substring to find the window
session: &'static str, // palette name -> cursor color
hwnd: HWND,
pid: u32,
field_idx: Option<u64>, // UIA element for the SUBJECT-NAME field (a11y corners)
submit_idx: Option<u64>,// UIA element for the SUBMIT button
}
fn main() {
let demo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent().unwrap().to_path_buf();
let repo_root = demo_root.parent().unwrap().parent().unwrap().to_path_buf();
let cua = std::env::var("CUA_DRIVER_EXE").map(PathBuf::from)
.unwrap_or_else(|_| repo_root.join("libs/cua-driver/rust/target/debug/cua-driver.exe"));
let legacy_app = std::env::var("LEGACY_APP_EXE").map(PathBuf::from)
.unwrap_or_else(|_| demo_root.join("target/debug/legacy-app.exe"));
if !cua.exists() { eprintln!("cua-driver.exe not found at {cua:?}"); std::process::exit(1); }
if !legacy_app.exists() { eprintln!("legacy-app.exe not found at {legacy_app:?}"); std::process::exit(1); }
eprintln!("[orch] starting cua-driver daemon…");
let mut daemon = Command::new(&cua).arg("serve").stdout(Stdio::null()).stderr(Stdio::null())
.spawn().expect("spawn cua-driver serve");
assign_to_job(&daemon);
thread::sleep(Duration::from_millis(1500));
// Launch corners + master.
let mut kids: Vec<Child> = Vec::new();
let mut launch = |kids: &mut Vec<Child>, cmd: &mut Command| {
if let Ok(c) = cmd.stdout(Stdio::null()).stderr(Stdio::null()).spawn() { assign_to_job(&c); kids.push(c); }
};
launch(&mut kids, Command::new(&legacy_app).args(["gdi", "Win32 GDI (no a11y)"]));
let winforms = demo_root.join("dotnet/winforms/bin/Debug/net10.0-windows/winforms-legacy.exe");
if winforms.exists() { launch(&mut kids, &mut Command::new(&winforms)); } else { eprintln!("[orch] (skip) winforms missing"); }
let wpf = demo_root.join("dotnet/wpf/bin/Debug/net10.0-windows/wpf-legacy.exe");
if wpf.exists() { launch(&mut kids, &mut Command::new(&wpf)); } else { eprintln!("[orch] (skip) wpf missing"); }
let electron_bin = demo_root.join("electron/node_modules/.bin/electron.cmd");
if electron_bin.exists() {
let mut c = Command::new(&electron_bin); c.arg(".").current_dir(demo_root.join("electron"));
launch(&mut kids, &mut c);
} else { eprintln!("[orch] (skip) electron not installed"); }
let mut master = Command::new(&legacy_app).args(["master", "Master (Win32 controls)"])
.stdout(Stdio::piped()).stderr(Stdio::null()).spawn().expect("spawn master");
assign_to_job(&master);
let master_out = master.stdout.take().unwrap();
thread::sleep(Duration::from_secs(3)); // window + electron warmup
let mut corners = vec![
Corner { title: "Win32 GDI", session: "crimson", hwnd: HWND::default(), pid: 0, field_idx: None, submit_idx: None },
Corner { title: "WinForms", session: "amber", hwnd: HWND::default(), pid: 0, field_idx: None, submit_idx: None },
Corner { title: "WPF", session: "aqua", hwnd: HWND::default(), pid: 0, field_idx: None, submit_idx: None },
Corner { title: "Electron", session: "mint_lime", hwnd: HWND::default(), pid: 0, field_idx: None, submit_idx: None },
];
for c in corners.iter_mut() {
if let Some((h, pid)) = find_window_by_title(c.title) { c.hwnd = h; c.pid = pid; }
else { eprintln!("[orch] (skip) no window for '{}'", c.title); }
}
corners.retain(|c| !c.hwnd.0.is_null());
let master_hwnd = find_window_by_title("Master (Win32").map(|(h, _)| h);
// Layout: each window half the work area; corners tile quadrants, master centered on top.
let wa = work_area();
let ww = (wa.right - wa.left) / 2;
let wh = (wa.bottom - wa.top) / 2;
let positions = [(wa.left, wa.top), (wa.left + ww, wa.top), (wa.left, wa.top + wh), (wa.left + ww, wa.top + wh)];
for (i, c) in corners.iter().enumerate() { let (x, y) = positions[i % 4]; place(c.hwnd, x, y, ww, wh, false); }
if let Some(mh) = master_hwnd {
// Master is smaller than a quadrant so it overlaps the central seam of
// all four corners without hiding their forms (which sit at the
// quadrant centers, outside this box). Centered on the work area.
let (mw, mh2) = (ww / 2, wh / 2);
let (mx, my) = (wa.left + ww - mw / 2, wa.top + wh - mh2 / 2);
place(mh, mx, my, mw, mh2, true);
unsafe { let _ = SetForegroundWindow(mh); }
}
thread::sleep(Duration::from_millis(800)); // let resized layouts settle
// Discover UIA elements per corner (field + SUBMIT). GDI has none -> pixel.
for c in corners.iter_mut() {
let tree = run_call_out(&cua, "get_window_state",
&format!(r#"{{"pid":{},"window_id":{},"capture_mode":"ax","session":"{}"}}"#, c.pid, c.hwnd.0 as isize, c.session));
c.field_idx = find_idx(&tree, |line| line.contains("] Edit"));
c.submit_idx = find_idx(&tree, |line| line.contains("] Button") && line.to_uppercase().contains("ADD RECORD"));
eprintln!("[orch] {:<9} session={:<9} field={:?} submit={:?} {}",
c.title, c.session, c.field_idx, c.submit_idx,
if c.field_idx.is_some() { "(a11y: set_value+invoke)" } else { "(no a11y: pixel)" });
// Pre-arm a coloured cursor for this session.
let _ = run_call(&cua, "set_agent_cursor_enabled", &format!(r#"{{"enabled":true,"session":"{}"}}"#, c.session));
}
// One driver thread per corner.
let mut senders: Vec<Sender<Action>> = Vec::new();
let mut handles = Vec::new();
for c in &corners {
let (tx, rx) = channel::<Action>();
senders.push(tx);
let cua = cua.clone();
let pid = c.pid;
let hwnd_addr = c.hwnd.0 as isize;
let session = c.session.to_string();
let title = c.title.to_string();
handles.push(thread::spawn(move || {
let hwnd = HWND(hwnd_addr as *mut _);
let ax = format!(r#"{{"pid":{pid},"window_id":{hwnd_addr},"capture_mode":"ax","session":"{session}"}}"#);
let mut seen_records: i64 = -1;
for act in rx {
// Re-discover element indices on a FRESH snapshot each action —
// a11y vs pixel is decided per action (Chromium's tree appears
// late and re-numbers as rows are added; GDI never has a tree).
let tree = run_call_out(&cua, "get_window_state", &ax);
let field = find_idx(&tree, |l| l.contains("] Edit"));
let submit = find_idx(&tree, |l| l.contains("] Button") && l.to_uppercase().contains("ADD RECORD"));
match act {
Action::Type { text } => {
let ok = match field {
// a11y: plain type_text with element_index — cua-driver
// auto-routes to UIA ValuePattern.SetValue on its own.
Some(idx) => run_call(&cua, "type_text", &format!(
r#"{{"pid":{pid},"window_id":{hwnd_addr},"element_index":{idx},"text":"{}","session":"{session}"}}"#, json_escape(&text))),
// no a11y (GDI): focus the field by pixel, then WM_CHAR.
None => {
if let Some((x, y)) = client_rel_to_local_px(hwnd, FIELD_FRAC.0, FIELD_FRAC.1) {
let _ = run_call(&cua, "click", &format!(
r#"{{"pid":{pid},"window_id":{hwnd_addr},"x":{x},"y":{y},"session":"{session}"}}"#));
}
run_call(&cua, "type_text", &format!(
r#"{{"pid":{pid},"window_id":{hwnd_addr},"text":"{}","session":"{session}"}}"#, json_escape(&text)))
}
};
eprintln!("[drive {session}] type {text:?} -> {}", if ok { "ok" } else { "FAIL" });
}
Action::Click { rx, ry } => {
let ok = match submit {
Some(idx) => run_call(&cua, "click", &format!(
r#"{{"pid":{pid},"window_id":{hwnd_addr},"element_index":{idx},"session":"{session}"}}"#)),
None => {
if let Some((x, y)) = client_rel_to_local_px(hwnd, rx, ry) {
run_call(&cua, "click", &format!(
r#"{{"pid":{pid},"window_id":{hwnd_addr},"x":{x},"y":{y},"session":"{session}"}}"#))
} else { false }
}
};
eprintln!("[drive {session}] click Add Record -> {}", if ok { "ok" } else { "FAIL" });
// Verify a record was actually COMMITTED: the "Records: N"
// counter must increment. (Checking for the typed text
// alone is fooled by it sitting in the field.) Only the
// a11y corners expose the counter; GDI's is drawn pixels,
// so it's verified visually (its grid grows on screen).
thread::sleep(Duration::from_millis(300));
let t2 = run_call_out(&cua, "get_window_state", &ax);
if let Some(n) = records_count(&t2) {
let landed = n as i64 > seen_records;
seen_records = n as i64;
eprintln!("[verify {session}] {title} Records={n} -> {}",
if landed { "COMMITTED ✓" } else { "FAIL (no new record)" });
}
}
Action::DrawMaze => {
// Pure coordinate drawing — NO element targeting, NO app
// cooperation. Each maze segment is one straight
// press-drag-release into the common REGION; cua-driver
// PostMessages the drag where it can and pen-injects it
// where the canvas (Chromium/WPF) drops posted mouse.
let map = |mx: f64, my: f64| {
let cfx = REGION[0] + mx * (REGION[2] - REGION[0]);
let cfy = REGION[1] + my * (REGION[3] - REGION[1]);
client_rel_to_local_px(hwnd, cfx, cfy)
};
let mut drawn = 0;
for (x0, y0, x1, y1) in MAZE.iter() {
if let (Some((fx, fy)), Some((tx, ty))) = (map(*x0, *y0), map(*x1, *y1)) {
if run_call(&cua, "drag", &format!(
r#"{{"pid":{pid},"window_id":{hwnd_addr},"from_x":{fx},"from_y":{fy},"to_x":{tx},"to_y":{ty},"dispatch":"background","session":"{session}"}}"#)) {
drawn += 1;
}
}
thread::sleep(Duration::from_millis(120));
}
eprintln!("[draw {session}] maze: {drawn}/{} segments", MAZE.len());
// Read back by SCREENSHOT (WGC captures the occluded
// window) and diff the rendered ink against the
// rasterized reference maze — normalized into the same
// REGION grid, pass only under threshold.
thread::sleep(Duration::from_millis(400));
let shot = std::env::temp_dir().join(format!("maze_{session}.png"));
let _ = std::fs::remove_file(&shot);
run_call_shot(&cua, &format!(
r#"{{"pid":{pid},"window_id":{hwnd_addr},"capture_mode":"vision","max_image_dimension":4096,"session":"{session}"}}"#), &shot);
match score_maze(hwnd, &shot) {
Some(score) => eprintln!("[verify {session}] {title} maze score={score:.2} -> {}",
if score >= MAZE_PASS { "DREW MAZE ✓" } else { "FAIL (lines don't match)" }),
None => eprintln!("[verify {session}] {title} maze -> FAIL (no screenshot/region)"),
}
}
}
}
}));
}
eprintln!("[orch] ready — submit a record in the center MASTER; watch {} coloured cursors \
drive the corner terminals in the background.", corners.len());
if std::env::args().any(|a| a == "--auto") {
let s2 = senders.clone();
thread::spawn(move || {
for i in 1..=3 {
thread::sleep(Duration::from_secs(3));
eprintln!("[orch] AUTO {i}: TYPE then CLICK");
for tx in &s2 { let _ = tx.send(Action::Type { text: format!("SUBJECT-{i:03}") }); }
thread::sleep(Duration::from_millis(1800));
// SAVE/"Add Record" button center (matches the GDI/master layout)
// for the pixel-driven GDI corner; a11y corners ignore rx/ry.
for tx in &s2 { let _ = tx.send(Action::Click { rx: 0.22, ry: 0.495 }); }
}
// Finally: draw the same maze into every corner's drawing pad by
// pure coordinate drags, then screenshot-diff each against the
// reference. Proves canvas actuation with no a11y, no app help.
thread::sleep(Duration::from_secs(2));
eprintln!("[orch] AUTO 4: DRAW MAZE (line tool) + screenshot diff");
for tx in &s2 { let _ = tx.send(Action::DrawMaze); }
});
}
let reader = BufReader::new(master_out);
for line in reader.lines().map_while(Result::ok) {
let parts: Vec<&str> = line.trim().split('\t').collect();
let action = match parts.as_slice() {
["CLICK", rx, ry] => rx.parse::<f64>().ok().zip(ry.parse::<f64>().ok()).map(|(rx, ry)| Action::Click { rx, ry }),
["TYPE", text] => Some(Action::Type { text: (*text).to_string() }),
_ => None,
};
if let Some(a) = action {
eprintln!("[orch] user action: {a:?} -> driving {} corners", senders.len());
for tx in &senders { let _ = tx.send(a.clone()); }
}
}
drop(senders);
for h in handles { let _ = h.join(); }
let _ = master.kill();
for mut k in kids { let _ = k.kill(); }
let _ = daemon.kill();
}
#[derive(Clone, Debug)]
enum Action { Click { rx: f64, ry: f64 }, Type { text: String }, DrawMaze }
fn json_escape(s: &str) -> String {
let mut o = String::with_capacity(s.len());
for ch in s.chars() {
match ch { '"' => o.push_str("\\\""), '\\' => o.push_str("\\\\"), '\n' => o.push_str("\\n"),
'\r' => {}, '\t' => o.push_str("\\t"), c => o.push(c) }
}
o
}
/// `cua-driver call <tool> <json>` (proxies to the daemon). True on success.
fn run_call(cua: &PathBuf, tool: &str, json: &str) -> bool {
Command::new(cua).arg("call").arg(tool).arg(json)
.stdout(Stdio::null()).stderr(Stdio::null())
.status().map(|s| s.success()).unwrap_or(false)
}
/// Same, capturing stdout (for get_window_state readback).
fn run_call_out(cua: &PathBuf, tool: &str, json: &str) -> String {
Command::new(cua).arg("call").arg(tool).arg(json)
.stderr(Stdio::null()).output()
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned()).unwrap_or_default()
}
/// get_window_state writing the screenshot PNG to `out` (WGC; captures even a
/// fully occluded window). Used to read back what cua-driver drew.
fn run_call_shot(cua: &PathBuf, json: &str, out: &std::path::Path) -> bool {
Command::new(cua).arg("call").arg("get_window_state").arg(json)
.arg("--screenshot-out-file").arg(out)
.stdout(Stdio::null()).stderr(Stdio::null())
.status().map(|s| s.success()).unwrap_or(false)
}
/// DWM extended-frame size of `hwnd` — the pixel size of the WGC bitmap before
/// get_window_state downscales it to fit `max_image_dimension`.
fn window_frame_size(hwnd: HWND) -> Option<(i32, i32)> {
unsafe {
let mut d = RECT::default();
if DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut d as *mut _ as *mut core::ffi::c_void, std::mem::size_of::<RECT>() as u32).is_err() {
return None;
}
Some((d.right - d.left, d.bottom - d.top))
}
}
/// Shape-only F1 (0..1) of the maze cua-driver drew vs the reference maze.
///
/// Reads the ink from the window screenshot (cropped to the drawing REGION),
/// then normalizes BOTH the drawn ink and the reference into a unit grid by
/// their own bounding boxes — so the score reflects the *shape* of the lines,
/// not where on the canvas they landed or at what scale (the user only wants
/// the shape penalized). The screenshot is downscaled to fit
/// `max_image_dimension`, so REGION (full-res bitmap px) is scaled by the
/// PNG/frame ratio before cropping.
fn score_maze(hwnd: HWND, png: &std::path::Path) -> Option<f64> {
let img = image::open(png).ok()?.to_luma8();
let (iw, ih) = (img.width() as i32, img.height() as i32);
let (fw, fh) = window_frame_size(hwnd)?;
if fw <= 0 || fh <= 0 { return None; }
let (fx, fy) = (iw as f64 / fw as f64, ih as f64 / fh as f64);
let (ax, ay) = client_rel_to_local_px(hwnd, REGION[0], REGION[1])?;
let (bx, by) = client_rel_to_local_px(hwnd, REGION[2], REGION[3])?;
let cx0 = (((ax.min(bx)) as f64 * fx).floor() as i32).max(0);
let cy0 = (((ay.min(by)) as f64 * fy).floor() as i32).max(0);
let cx1 = (((ax.max(bx)) as f64 * fx).ceil() as i32).min(iw - 1);
let cy1 = (((ay.max(by)) as f64 * fy).ceil() as i32).min(ih - 1);
if cx1 - cx0 < 12 || cy1 - cy0 < 12 { return None; }
// Drawn ink + its bounding box.
let mut ink: Vec<(i32, i32)> = Vec::new();
let (mut nx, mut ny, mut xx, mut xy) = (i32::MAX, i32::MAX, i32::MIN, i32::MIN);
for y in cy0..=cy1 {
for x in cx0..=cx1 {
if img.get_pixel(x as u32, y as u32).0[0] < 150 {
ink.push((x, y));
if x < nx { nx = x } if x > xx { xx = x }
if y < ny { ny = y } if y > xy { xy = y }
}
}
}
const N: usize = 96;
if ink.len() < 20 || xx <= nx || xy <= ny { return None; }
let (bw, bh) = ((xx - nx) as f64, (xy - ny) as f64);
let mut a = vec![false; N * N];
for (x, y) in ink {
let gx = (((x - nx) as f64 / bw) * (N as f64 - 1.0)).round() as usize;
let gy = (((y - ny) as f64 / bh) * (N as f64 - 1.0)).round() as usize;
a[gy.min(N - 1) * N + gx.min(N - 1)] = true;
}
// Reference maze, normalized to ITS bounding box the same way.
let (mut rnx, mut rny, mut rxx, mut rxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
for (x0, y0, x1, y1) in MAZE.iter() {
for (px, py) in [(*x0, *y0), (*x1, *y1)] {
if px < rnx { rnx = px } if px > rxx { rxx = px }
if py < rny { rny = py } if py > rxy { rxy = py }
}
}
let (rbw, rbh) = ((rxx - rnx).max(1e-6), (rxy - rny).max(1e-6));
let mut b = vec![false; N * N];
for (x0, y0, x1, y1) in MAZE.iter() {
raster_line(&mut b, N, (x0 - rnx) / rbw, (y0 - rny) / rbh, (x1 - rnx) / rbw, (y1 - rny) / rbh);
}
Some(mask_f1(&a, &b, N, 2))
}
/// Bresenham rasterize a normalized [0,1] segment into the N×N reference mask.
fn raster_line(mask: &mut [bool], n: usize, x0: f64, y0: f64, x1: f64, y1: f64) {
let s = n as f64 - 1.0;
let (px1, py1) = ((x1 * s) as i32, (y1 * s) as i32);
let (mut x, mut y) = ((x0 * s) as i32, (y0 * s) as i32);
let (dx, dy) = ((px1 - x).abs(), -(py1 - y).abs());
let (sx, sy) = (if x < px1 { 1 } else { -1 }, if y < py1 { 1 } else { -1 });
let mut err = dx + dy;
loop {
if x >= 0 && y >= 0 && (x as usize) < n && (y as usize) < n { mask[y as usize * n + x as usize] = true; }
if x == px1 && y == py1 { break; }
let e2 = 2 * err;
if e2 >= dy { err += dy; x += sx; }
if e2 <= dx { err += dx; y += sy; }
}
}
/// Chebyshev dilation by radius `r`.
fn dilate(m: &[bool], n: usize, r: i32) -> Vec<bool> {
let mut o = vec![false; n * n];
for y in 0..n as i32 {
for x in 0..n as i32 {
if !m[y as usize * n + x as usize] { continue; }
for dy in -r..=r {
for dx in -r..=r {
let (nx, ny) = (x + dx, y + dy);
if nx >= 0 && ny >= 0 && (nx as usize) < n && (ny as usize) < n { o[ny as usize * n + nx as usize] = true; }
}
}
}
}
o
}
/// F1 of two ink masks, each dilated by `r` so near-misses count as matches.
fn mask_f1(a: &[bool], b: &[bool], n: usize, r: i32) -> f64 {
let (da, db) = (dilate(a, n, r), dilate(b, n, r));
let (mut ca, mut cb, mut ma, mut mb) = (0usize, 0usize, 0usize, 0usize);
for i in 0..n * n {
if a[i] { ca += 1; if db[i] { ma += 1; } }
if b[i] { cb += 1; if da[i] { mb += 1; } }
}
if ca == 0 || cb == 0 { return 0.0; }
let precision = ma as f64 / ca as f64; // drawn ink that is near the reference
let recall = mb as f64 / cb as f64; // reference covered by drawn ink
if precision + recall == 0.0 { 0.0 } else { 2.0 * precision * recall / (precision + recall) }
}
/// First `[N]` element index on a tree line matching `pred`.
fn find_idx(tree: &str, pred: impl Fn(&str) -> bool) -> Option<u64> {
// The tree is one JSON line with literal "\n" separators between rows.
for line in tree.split("\\n") {
if !pred(line) { continue; }
let st = line.find('[')? + 1;
let en = line[st..].find(']')? + st;
if let Ok(n) = line[st..en].trim().parse() { return Some(n); }
}
None
}
/// Parse the "Records: N" counter from a window-state tree (status bar).
fn records_count(tree: &str) -> Option<u32> {
let pos = tree.find("Records:")?;
let rest = &tree[pos + "Records:".len()..];
let digits: String = rest.trim_start().chars().take_while(|c| c.is_ascii_digit()).collect();
digits.parse().ok()
}
fn work_area() -> RECT {
use windows::Win32::UI::WindowsAndMessaging::{SystemParametersInfoW, SPI_GETWORKAREA, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS};
let mut wa = RECT::default();
unsafe { let _ = SystemParametersInfoW(SPI_GETWORKAREA, 0, Some(&mut wa as *mut _ as *mut core::ffi::c_void), SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0)); }
if wa.right <= wa.left || wa.bottom <= wa.top {
use windows::Win32::UI::WindowsAndMessaging::{GetSystemMetrics, SM_CXSCREEN, SM_CYSCREEN};
unsafe { wa = RECT { left: 0, top: 0, right: GetSystemMetrics(SM_CXSCREEN), bottom: GetSystemMetrics(SM_CYSCREEN) }; }
}
wa
}
fn place(hwnd: HWND, x: i32, y: i32, w: i32, h: i32, activate: bool) {
if hwnd.0.is_null() { return; }
let flags = if activate { SWP_SHOWWINDOW } else { SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOZORDER };
unsafe { let _ = SetWindowPos(hwnd, HWND_TOP, x, y, w, h, flags); }
}
/// Client-relative (0..1) -> the click tool's window-local screenshot-pixel
/// space (ClientToScreen minus the DWM extended-frame top-left + 1px inset).
fn client_rel_to_local_px(hwnd: HWND, rx: f64, ry: f64) -> Option<(i32, i32)> {
unsafe {
let mut cr = RECT::default();
GetClientRect(hwnd, &mut cr).ok()?;
let mut pt = POINT { x: (rx * (cr.right - cr.left) as f64) as i32, y: (ry * (cr.bottom - cr.top) as f64) as i32 };
let _ = ClientToScreen(hwnd, &mut pt);
let mut dwm = RECT::default();
if DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &mut dwm as *mut _ as *mut core::ffi::c_void, std::mem::size_of::<RECT>() as u32).is_err() {
return Some((pt.x, pt.y));
}
Some((pt.x - dwm.left - 1, pt.y - dwm.top - 1))
}
}
// ── window discovery by title substring (case-insensitive) ────────────────────
struct Finder { needle: String, hwnd: HWND, pid: u32 }
unsafe extern "system" fn enum_cb(hwnd: HWND, lparam: LPARAM) -> BOOL {
let f = &mut *(lparam.0 as *mut Finder);
if !IsWindowVisible(hwnd).as_bool() { return TRUE; }
let mut buf = [0u16; 256];
let n = GetWindowTextW(hwnd, &mut buf);
if n > 0 {
let title = String::from_utf16_lossy(&buf[..n as usize]);
if title.to_lowercase().contains(&f.needle.to_lowercase()) {
let mut pid = 0u32;
GetWindowThreadProcessId(hwnd, Some(&mut pid));
f.hwnd = hwnd; f.pid = pid;
return BOOL(0);
}
}
let _ = PWSTR::null();
TRUE
}
fn find_window_by_title(needle: &str) -> Option<(HWND, u32)> {
let deadline = Instant::now() + Duration::from_secs(8);
loop {
let mut f = Finder { needle: needle.to_string(), hwnd: HWND::default(), pid: 0 };
unsafe { let _ = EnumWindows(Some(enum_cb), LPARAM(&mut f as *mut _ as isize)); }
if !f.hwnd.0.is_null() { return Some((f.hwnd, f.pid)); }
if Instant::now() > deadline { return None; }
thread::sleep(Duration::from_millis(300));
}
}