Skip to content

[Windows] Caption drag region (SetPropW) freezes during modal SIZEMOVE loop when the UI isolate runs on a separate thread; WinEvent-based events still require the platform thread #18

Open

Description

@TATboy210

Environment

  • nativeapi / cnativeapi 0.5.2 (pub.dev)
  • Flutter 3.47.6 / 3.47.7 stable, Windows desktop
  • Windows 11 Home build 26300, 150% scaling (DPI 144), NVIDIA RTX 4060 Laptop GPU
  • Reproduced in debug and release builds

Setup

Frameless desktop app (TitleBarStyle.hidden) with a custom Flutter title bar. The native caption drag region follows the recommended pattern: Dart reports the title-bar rect every frame and stores it via SetPropW; the runner-side subclass answers HTCAPTION over the reported rect (on both the FlutterView child and the top level).

Unlike #17's repro, the app runs the UI isolate with Flutter's default thread policy (UIThreadPolicy::Default = RunOnSeparateThread in dart_project.h) instead of RunOnPlatformThread.

What works on the separate thread

  • Moved/resized events: the 0.5.2 WM_WINDOWPOSCHANGED subclass emits after the window procedure finishes and NativeCallable.listener delivers cross-thread — the resize event pipeline works (verified across 12 instrumented resize sessions with per-session FrameTiming metrics; Dart build P50 0.3–0.7 ms, so the Dart side is not the bottleneck).
  • Programmatic geometry changes: rect reporting tracks every step — with an external SetWindowPos tool, the SetPropW caption rect followed the window correctly (e.g. R=1043 → 1562 → 1112 physical px).

What breaks

During real modal SIZEMOVE dragging (mouse on the window border, native modal loop), the Dart-side rect reporting freezes: the caption rect property kept a stale mid-drag value (R=1043 physical) across 10 subsequent resize sessions spanning window widths 928–1436 physical px, and never updated again.

Consequence: after enlarging the window beyond the stale rect, the title-bar area outside the stale rect answers HTCLIENT on WM_NCHITTEST (verified by probing both the FlutterView child and the top level with external SendMessageW(WM_NCHITTEST)) — the native caption drag region is dead there, which reads to the user as "the top drag area is gone".

Notes on the mechanism: the property itself is readable cross-thread (GetPropW returns the stale value from an external process), so the value is set; it is the Dart-side paint → postFrameCallback → SetPropW reporting that stops reaching the property while the modal loop is in play. Programmatic resizes outside the modal loop report fine, which localizes the freeze to the modal-loop interaction with the frame/post-frame callback pipeline on a separate UI thread.

Additionally, focus / lifetime / occlusion events still use WINEVENT_OUTOFCONTEXT hooks installed on the calling thread (window_manager_windows.cpp: g_foreground_hook, g_lifetime_hook, g_occlusion_hooks). With the UI isolate on its own thread, those hooks end up on a thread that never pumps Win32 messages, so those events can't be delivered. Geometry events no longer have this constraint after #17's subclass fix.

Question / request

Is calling the C API from the UI isolate thread (≠ the window-owning platform thread) a hard same-thread requirement? #17's note ("Flutter apps have all their windows on the platform thread") addresses the window-owning thread, but the caller of the Dart FFI API is the UI isolate thread, and with Flutter's default policy these are different threads.

If practical, marshaling the Win32 work to the platform thread internally (the WindowMessageDispatcher infrastructure looks positioned for exactly this) would make the API thread-agnostic and let Flutter apps keep the default thread policy. That also keeps Flutter's frame pipeline decoupled from the modal resize loop — on the merged-thread configuration we measured resize frames missing the 60 fps budget 20–56% of the time during border drags, while the default policy tracks the pointer smoothly.

Activity

  1. TATboy210 commented on Oct 21, 2026

    @TATboy210
    Author

    A few clarifications from further testing that narrow the failure surface:

    Control group. The exact same reporting loop (paint → postFrameCallback → SetPropW) ran for weeks under RunOnPlatformThread with manual border drags and never froze — the caption region tracked every step. The freeze appears only under UIThreadPolicy::Default / RunOnSeparateThread, which isolates the thread policy as the variable.

    The dead zone is asymmetric. With the stale rect, shrinking the window keeps working (the stale rect is a superset and still covers the title bar), while growing the window past the stale rect kills the caption region on the newly covered area (the stale rect becomes a subset). This matches "the top drag area is gone" only after enlarging the window.

    The child/parent transparency mechanism is intact. External WM_NCHITTEST probing shows the FlutterView child answering HTTRANSPARENT over the reported rect and the top level answering HTCAPTION there — so the child/parent hit-test chain works and the failure is purely the stale rect value, not a broken transparency pass.

    Happy to provide a minimal repro if useful. The pattern is: frameless app, TitleBarStyle.hidden, Dart-side rect reporting via SetPropW with the runner answering HTCAPTION over the reported rect, UIThreadPolicy::Default.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions