chore: auto-commit (286 archivos)
- .claude/agents/fn-orquestador/SKILL.md - .claude/commands/fn_claude.md - .claude/rules/INDEX.md - .claude/rules/cpp_apps.md - .claude/rules/ids_naming.md - CHANGELOG.md - apps/dag_engine/README.md - apps/dag_engine/api.go - apps/dag_engine/dags_migrated/example.yaml - apps/dag_engine/dags_migrated/example_lineage_tracking.yaml - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+221
-17
@@ -23,12 +23,14 @@
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <sys/stat.h>
|
||||
#include <unordered_map>
|
||||
|
||||
#ifdef _WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#include <windowsx.h> // GET_X_LPARAM / GET_Y_LPARAM
|
||||
#define GLFW_EXPOSE_NATIVE_WIN32
|
||||
#include <GLFW/glfw3native.h>
|
||||
#else
|
||||
@@ -57,37 +59,148 @@ static void glfw_error_callback(int error, const char* description) {
|
||||
// the existing buffer pixels. We replicate that contract: while sizemove is
|
||||
// active, skip render + glfwSwapBuffers, only pump the message queue. As soon
|
||||
// as WM_EXITSIZEMOVE arrives, normal rendering resumes.
|
||||
//
|
||||
// IMPORTANT: the subclass must cover EVERY HWND owned by the process — main
|
||||
// window AND every secondary viewport platform window the ImGui GLFW backend
|
||||
// creates when the user drags a panel outside the main. Otherwise AltSnap on
|
||||
// a secondary HWND would not be observed, the main loop would keep rendering,
|
||||
// and the visible jitter would return on that panel. g_in_sizemove stays
|
||||
// global on purpose: any external move on ANY of our HWNDs pauses the whole
|
||||
// render pipeline, exactly like the native title-bar drag contract.
|
||||
static std::atomic<bool> g_in_sizemove{false};
|
||||
static WNDPROC g_orig_wndproc = nullptr;
|
||||
static HWND g_subclassed_hwnd = nullptr;
|
||||
// Test observability — monotonic counters. fn::internal exposes accessors.
|
||||
static std::atomic<int> g_sizemove_enter_count{0};
|
||||
static std::atomic<int> g_alt_rmb_resize_count{0};
|
||||
static std::atomic<int> g_alt_lmb_move_count{0};
|
||||
// Test hook — bypasses GetAsyncKeyState(VK_MENU) so headless tests can drive
|
||||
// the Alt+RMB / Alt+LMB paths without UI-access for keybd_event.
|
||||
static std::atomic<bool> g_force_alt_for_test{false};
|
||||
// Diagnostic: every WM_RBUTTONDOWN this subclass sees (Alt-or-not). Used to
|
||||
// distinguish "message never arrived" from "Alt check failed".
|
||||
static std::atomic<int> g_rbuttondown_seen_count{0};
|
||||
// Accessed only from the main (render) thread. Map value is the original
|
||||
// WNDPROC for that HWND so we can restore and chain CallWindowProcW.
|
||||
static std::unordered_map<HWND, WNDPROC> g_subclassed;
|
||||
|
||||
// Pick the WMSZ_* direction whose modal resize will feel natural depending
|
||||
// on which quadrant of the client rect the cursor is in. Matches AltSnap's
|
||||
// quadrant rule (top-left -> shrink toward top-left, etc.).
|
||||
static int alt_rmb_resize_direction(HWND hwnd, int client_x, int client_y) {
|
||||
RECT rc{};
|
||||
if (!GetClientRect(hwnd, &rc)) return 8 /* WMSZ_BOTTOMRIGHT */;
|
||||
int cx = (rc.right - rc.left) / 2;
|
||||
int cy = (rc.bottom - rc.top) / 2;
|
||||
bool top = (client_y < cy);
|
||||
bool left = (client_x < cx);
|
||||
if (top && left) return 4; // WMSZ_TOPLEFT
|
||||
if (top && !left) return 5; // WMSZ_TOPRIGHT
|
||||
if (!top && left) return 7; // WMSZ_BOTTOMLEFT
|
||||
return 8; // WMSZ_BOTTOMRIGHT
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK fn_subclass_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
switch (msg) {
|
||||
case WM_ENTERSIZEMOVE:
|
||||
g_in_sizemove.store(true, std::memory_order_release);
|
||||
g_sizemove_enter_count.fetch_add(1, std::memory_order_acq_rel);
|
||||
break;
|
||||
case WM_EXITSIZEMOVE:
|
||||
g_in_sizemove.store(false, std::memory_order_release);
|
||||
break;
|
||||
case WM_LBUTTONDOWN:
|
||||
// Alt + LMB anywhere on the window initiates a native modal MOVE
|
||||
// via WM_SYSCOMMAND, SC_MOVE | HTCAPTION. Same pattern as our
|
||||
// Alt+RMB resize: ReleaseCapture, post the syscommand, return 0
|
||||
// to consume the click. Windows then drives a normal move modal
|
||||
// (DefWindowProc blocks the thread) and our existing
|
||||
// WM_ENTERSIZEMOVE gate pauses render so there's no jitter.
|
||||
{
|
||||
bool alt_real = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
|
||||
bool alt_test = g_force_alt_for_test.load(std::memory_order_acquire);
|
||||
if (alt_real || alt_test) {
|
||||
g_alt_lmb_move_count.fetch_add(1, std::memory_order_acq_rel);
|
||||
if (alt_test) {
|
||||
// Test mode: skip SC_MOVE post to keep the harness
|
||||
// out of Windows' modal move loop.
|
||||
return 0;
|
||||
}
|
||||
ReleaseCapture();
|
||||
PostMessageW(hwnd, WM_SYSCOMMAND,
|
||||
(WPARAM)(0xF010 /* SC_MOVE */ | 2 /* HTCAPTION */), 0);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case WM_RBUTTONDOWN:
|
||||
g_rbuttondown_seen_count.fetch_add(1, std::memory_order_acq_rel);
|
||||
// Alt + RMB anywhere on the window initiates a native modal
|
||||
// resize. Direction is chosen by cursor quadrant relative to
|
||||
// window center so dragging "feels" like grabbing the nearest
|
||||
// corner. ReleaseCapture is required before WM_SYSCOMMAND
|
||||
// SC_SIZE so the modal loop can take input. The subsequent
|
||||
// WM_ENTERSIZEMOVE bracket is observed above, so render is
|
||||
// gated for free and no jitter appears.
|
||||
{
|
||||
bool alt_real = (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
|
||||
bool alt_test = g_force_alt_for_test.load(std::memory_order_acquire);
|
||||
if (alt_real || alt_test) {
|
||||
int cx = GET_X_LPARAM(lp);
|
||||
int cy = GET_Y_LPARAM(lp);
|
||||
int dir = alt_rmb_resize_direction(hwnd, cx, cy);
|
||||
g_alt_rmb_resize_count.fetch_add(1, std::memory_order_acq_rel);
|
||||
if (alt_test) {
|
||||
// Test mode: skip the SC_SIZE post to keep the modal
|
||||
// out of the headless test harness. The counter is
|
||||
// sufficient to verify the path was taken; the modal
|
||||
// entry is exercised in the real-input manual test.
|
||||
return 0;
|
||||
}
|
||||
ReleaseCapture();
|
||||
PostMessageW(hwnd, WM_SYSCOMMAND,
|
||||
(WPARAM)(0xF000 /* SC_SIZE */ | dir), 0);
|
||||
return 0; // consume so ImGui doesn't see a right-click
|
||||
}
|
||||
}
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
return CallWindowProcW(g_orig_wndproc, hwnd, msg, wp, lp);
|
||||
auto it = g_subclassed.find(hwnd);
|
||||
WNDPROC orig = (it != g_subclassed.end()) ? it->second : nullptr;
|
||||
if (orig) return CallWindowProcW(orig, hwnd, msg, wp, lp);
|
||||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
static void install_sizemove_subclass_hwnd(HWND hwnd) {
|
||||
if (!hwnd) return;
|
||||
if (g_subclassed.find(hwnd) != g_subclassed.end()) return; // idempotent
|
||||
WNDPROC orig = (WNDPROC)SetWindowLongPtrW(
|
||||
hwnd, GWLP_WNDPROC, (LONG_PTR)fn_subclass_wndproc);
|
||||
g_subclassed[hwnd] = orig;
|
||||
}
|
||||
|
||||
static void install_sizemove_subclass(GLFWwindow* w) {
|
||||
HWND hwnd = glfwGetWin32Window(w);
|
||||
if (!hwnd) return;
|
||||
g_subclassed_hwnd = hwnd;
|
||||
g_orig_wndproc = (WNDPROC)SetWindowLongPtrW(
|
||||
hwnd, GWLP_WNDPROC, (LONG_PTR)fn_subclass_wndproc);
|
||||
if (!w) return;
|
||||
install_sizemove_subclass_hwnd(glfwGetWin32Window(w));
|
||||
}
|
||||
|
||||
static void uninstall_sizemove_subclass() {
|
||||
if (g_subclassed_hwnd && g_orig_wndproc) {
|
||||
SetWindowLongPtrW(g_subclassed_hwnd, GWLP_WNDPROC, (LONG_PTR)g_orig_wndproc);
|
||||
// Reap stale entries: when a secondary viewport is destroyed (panel re-docked
|
||||
// back into main), the GLFW backend calls glfwDestroyWindow and the HWND is
|
||||
// invalidated. Drop those entries so we don't hold dangling pointers and so a
|
||||
// fresh HWND at the same address gets re-subclassed cleanly.
|
||||
static void prune_dead_subclassed() {
|
||||
for (auto it = g_subclassed.begin(); it != g_subclassed.end();) {
|
||||
if (!IsWindow(it->first)) it = g_subclassed.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
g_subclassed_hwnd = nullptr;
|
||||
g_orig_wndproc = nullptr;
|
||||
}
|
||||
|
||||
static void uninstall_sizemove_subclass_all() {
|
||||
for (auto& kv : g_subclassed) {
|
||||
if (IsWindow(kv.first) && kv.second) {
|
||||
SetWindowLongPtrW(kv.first, GWLP_WNDPROC, (LONG_PTR)kv.second);
|
||||
}
|
||||
}
|
||||
g_subclassed.clear();
|
||||
}
|
||||
|
||||
static inline bool external_sizemove_active() {
|
||||
@@ -312,6 +425,15 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
// Title-bar-only move for ImGui windows. Critical for secondary viewports
|
||||
// (floating panels) whose entire OS window is a single borderless ImGui
|
||||
// window: without this flag, ImGui moves the window when the user drags
|
||||
// any empty client-area pixel, which translates to the OS viewport
|
||||
// following the mouse "from anywhere" with no modifier. With this flag,
|
||||
// floating panels obey the same "header only" contract as a native
|
||||
// decorated window. Alt+LMB anywhere still moves via our WndProc subclass
|
||||
// (consumed before ImGui sees the click).
|
||||
io.ConfigWindowsMoveFromTitleBarOnly = true;
|
||||
|
||||
// Convencion local_files: imgui.ini y app_settings.ini viven en
|
||||
// <exe_dir>/local_files/. Migra automaticamente desde el cwd o
|
||||
@@ -406,17 +528,59 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
while (!glfwWindowShouldClose(window)) {
|
||||
glfwPollEvents();
|
||||
|
||||
// When the main window is iconified we used to glfwWaitEvents+continue
|
||||
// to save CPU. That is wrong when floating panels (secondary
|
||||
// viewports) exist: skipping the frame stops UpdatePlatformWindows /
|
||||
// RenderPlatformWindowsDefault so those panels go blank or get
|
||||
// ungrouped by the WM. We therefore detect secondary viewports first
|
||||
// and, if any are alive, fall through to a normal frame (main GL
|
||||
// clear/swap is harmless on the hidden main HWND, secondary GL
|
||||
// contexts keep refreshing). Only when there are NO floating panels
|
||||
// do we sleep on glfwWaitEvents the way we used to.
|
||||
if (glfwGetWindowAttrib(window, GLFW_ICONIFIED)) {
|
||||
glfwWaitEvents();
|
||||
continue;
|
||||
bool has_secondary_viewport = false;
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||
ImGuiPlatformIO& pio_ic = ImGui::GetPlatformIO();
|
||||
for (int i = 0; i < pio_ic.Viewports.Size; ++i) {
|
||||
ImGuiViewport* vp = pio_ic.Viewports[i];
|
||||
if (!vp || !vp->PlatformHandle) continue;
|
||||
if ((GLFWwindow*)vp->PlatformHandle == window) continue;
|
||||
has_secondary_viewport = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!has_secondary_viewport) {
|
||||
glfwWaitEvents();
|
||||
continue;
|
||||
}
|
||||
// fallthrough: render normally so floating panels stay alive.
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Subclass any platform window we haven't subclassed yet. Covers the
|
||||
// main window AND every secondary viewport (panels dragged outside
|
||||
// main) so AltSnap's WM_ENTERSIZEMOVE/WM_EXITSIZEMOVE brackets are
|
||||
// observed regardless of which HWND it targets. Runs BEFORE the
|
||||
// sizemove gate below so newly-created secondaries are protected from
|
||||
// their very first frame onwards.
|
||||
if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable) {
|
||||
prune_dead_subclassed();
|
||||
ImGuiPlatformIO& pio_sub = ImGui::GetPlatformIO();
|
||||
for (int i = 0; i < pio_sub.Viewports.Size; ++i) {
|
||||
ImGuiViewport* vp = pio_sub.Viewports[i];
|
||||
if (!vp || !vp->PlatformHandle) continue;
|
||||
install_sizemove_subclass((GLFWwindow*)vp->PlatformHandle);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// While an external mover (AltSnap on Win32, tiling WMs) is dragging
|
||||
// the window we mirror the native title-bar contract: do not render,
|
||||
// do not swap, just pump events. The DWM compositor scrolls the last
|
||||
// presented framebuffer with the window — no race between SetWindowPos
|
||||
// (async) and glfwSwapBuffers, so no jitter. WM_EXITSIZEMOVE clears
|
||||
// the flag and the main loop resumes normal rendering.
|
||||
// the flag and the main loop resumes normal rendering. Applies to
|
||||
// brackets on ANY subclassed HWND (main or secondary viewports).
|
||||
if (external_sizemove_active()) {
|
||||
// Bound the busy loop so the message queue gets drained but we
|
||||
// don't burn CPU when AltSnap pauses between mouse moves.
|
||||
@@ -576,7 +740,7 @@ int run_app(AppConfig config, std::function<void()> render_fn) {
|
||||
ImPlot::DestroyContext();
|
||||
ImGui::DestroyContext();
|
||||
#ifdef _WIN32
|
||||
uninstall_sizemove_subclass();
|
||||
uninstall_sizemove_subclass_all();
|
||||
#endif
|
||||
glfwDestroyWindow(window);
|
||||
glfwTerminate();
|
||||
@@ -588,6 +752,46 @@ int run_app(std::function<void()> render_fn) {
|
||||
return run_app(AppConfig{}, render_fn);
|
||||
}
|
||||
|
||||
// Test-only observability of the Win32 subclass. Always defined (zero cost);
|
||||
// on non-Windows the counters never increment.
|
||||
namespace internal {
|
||||
int sizemove_enter_count() {
|
||||
#ifdef _WIN32
|
||||
return g_sizemove_enter_count.load(std::memory_order_acquire);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
int alt_rmb_resize_count() {
|
||||
#ifdef _WIN32
|
||||
return g_alt_rmb_resize_count.load(std::memory_order_acquire);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
void set_force_alt_for_test(bool v) {
|
||||
#ifdef _WIN32
|
||||
g_force_alt_for_test.store(v, std::memory_order_release);
|
||||
#else
|
||||
(void)v;
|
||||
#endif
|
||||
}
|
||||
int rbuttondown_seen_count() {
|
||||
#ifdef _WIN32
|
||||
return g_rbuttondown_seen_count.load(std::memory_order_acquire);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
int alt_lmb_move_count() {
|
||||
#ifdef _WIN32
|
||||
return g_alt_lmb_move_count.load(std::memory_order_acquire);
|
||||
#else
|
||||
return 0;
|
||||
#endif
|
||||
}
|
||||
} // namespace internal
|
||||
|
||||
} // namespace fn
|
||||
|
||||
#ifdef IMGUI_ENABLE_TEST_ENGINE
|
||||
|
||||
Reference in New Issue
Block a user