Skip to content

fix(windows): WinUI 3 shell focus, input topology, and action plumbing#157

Merged
deblasis merged 7 commits intowindowsfrom
feat/winui3-shell-followup
Apr 6, 2026
Merged

fix(windows): WinUI 3 shell focus, input topology, and action plumbing#157
deblasis merged 7 commits intowindowsfrom
feat/winui3-shell-followup

Conversation

@deblasis
Copy link
Copy Markdown
Owner

@deblasis deblasis commented Apr 6, 2026

IMPORTANT: Stacked PR — depends on #156. Merge order: # 156 first, then this. The next stacked branch (feat/winui3-shell-dx12-fixes) will address the remaining DX12 renderer issues.

Follow-up fixes on top of the WinUI 3 shell scaffold. Three areas:

Action callback plumbing

  • Fix the GhosttyActionCb P/Invoke signature: ghostty_target_s and ghostty_action_s are passed by hidden pointer on Windows x64, not by value.
  • Decode ghostty_action_s and dispatch SET_TITLE, RING_BELL, CLOSE_WINDOW. Everything else falls through to core defaults.
  • New TitleChanged and CloseRequested events on TerminalControl; MainWindow subscribes to update the chrome.
  • MessageBeep(MB_OK) for the bell.

Focus and input topology

  • Move GotFocus/LostFocus/KeyDown/KeyUp/CharacterReceived from the SwapChainPanel to the outer UserControl. SwapChainPanel inherits from Grid, not Control, so it cannot hold keyboard focus directly — Panel.Focus(...) was a silent no-op. Subscribing CharacterReceived to a SwapChainPanel also inserts a TSF input scope that thrashes focus on every layout pass.
  • Dedupe focus state forwarding so libghostty never sees redundant focus events.
  • Pointer-press now calls this.Focus(FocusState.Pointer) only when not already focused.

Resize and initial sizing

  • Replace the OnLoaded TryEnqueue retry loop with a one-shot Panel.LayoutUpdated subscription so the initial size is primed once layout is settled and CompositionScaleX/Y is valid.
  • OnCompositionScaleChanged now re-kicks the resize debounce so pixel dimensions are recomputed when DPI changes.
  • Extracted KickResizeDebounce helper to avoid duplicating the timer init.

Out of scope (deferred to next stacked PR feat/winui3-shell-dx12-fixes)

  • Pixel-perfect fill: panel dims match the window but the rendered terminal still appears smaller. Lives in the Zig DX12 renderer.
  • Resize crash: dragging crashes the process. Memory note from # 156 already flagged this — DX12 renderer recreates the swap chain on every set_size. Real fix is in the Zig renderer.
  • VirtualKey translation table (~130 entries) and the remaining ~70 ghostty.h bindings stay deferred as scaffold-PR memory called out.

Target: feat/winui3-shell

@deblasis deblasis marked this pull request as ready for review April 6, 2026 13:54
Base automatically changed from feat/winui3-shell to windows April 6, 2026 15:50
deblasis added 7 commits April 6, 2026 17:52
ghostty_target_s is a 16-byte struct and on Windows x64 is passed by
hidden pointer. The current delegate declared it as a value struct,
which is ABI-incorrect. It has never crashed only because the handler
returns false without reading target. Switch both target and action to
IntPtr to match the ABI before adding real decode logic.
Mirrors ghostty_action_tag_e from ghostty.h so the runtime action
callback can dispatch on tag without blind pointer reads. Also adds
MessageBeep for the upcoming RING_BELL handling.
Plumbing for the next task, which decodes the ghostty action union and
raises these when the core asks us to update the title or close.
First real handling of ghostty_action_s. Reads the tag at offset 0,
dispatches the three variants this single-window shell can act on, and
leaves everything else falling through to core defaults.

SET_TITLE and CLOSE_WINDOW marshal onto the UI dispatcher before
raising the events MainWindow consumes. RING_BELL calls MessageBeep
directly because it is thread-safe.
Subscribe MainWindow to the TerminalControl events so cmd.exe title
changes reach the window chrome and exit closes the window. Switch the
resize source from SizeChangedEventArgs to Panel.ActualWidth/ActualHeight
to eliminate a DPI-rounding gap that was letterboxing the terminal by a
pixel or two at the edges.

Also add a comment on IsTabStop="False" explaining why it must stay.
The previous WinUI 3 shell put GotFocus/LostFocus, KeyDown/Up,
CharacterReceived, and IsTabStop on the SwapChainPanel itself, which
caused continuous focus ping-pong. Two reasons:

  1) SwapChainPanel inherits from Grid, not Control, so it cannot
     hold keyboard focus directly - Panel.Focus(...) was a silent
     no-op.
  2) Subscribing CharacterReceived/KeyDown on a SwapChainPanel
     causes the framework to insert a TSF/IME input scope which
     transiently steals and returns focus on every layout pass.

Move all input/focus to the outer UserControl. Leave SizeChanged and
CompositionScaleChanged on the SwapChainPanel. Replace the OnLoaded
TryEnqueue retry loop with a one-shot Panel.LayoutUpdated subscription
so initial sizing sees a settled layout. CompositionScaleChanged now
also re-kicks the resize debounce so pixel dimensions are recomputed
when DPI changes. Dedupe focus state forwarding so libghostty never
sees redundant focus events.

Pixel-perfect fill and DX12 resize-crash issues remain - they live
below this layer in the Zig DX12 renderer and will be addressed in a
follow-up stacked PR.
- Return false from OnAction on null dispatcher so the core falls back
  to its default instead of treating a silently dropped action as
  handled.
- Replace hand-rolled PtrToStringUtf8 with Marshal.PtrToStringUTF8.
- Pin GhosttyActionTag to the three variants we actually dispatch on,
  with explicit upstream-matched indices, so a reorder in ghostty.h
  cannot silently misroute one tag to another handler.
- Unsubscribe Panel.LayoutUpdated in OnUnloaded so the one-shot init
  handler cannot keep firing if the control is torn down before its
  size ever settles.
- Don't flip _focused before the surface check in SetFocusState: a
  stale value would dedupe the next focus change after the surface is
  recreated.
- Drop unused app parameter shadowing _app in OnAction; remove stale
  step-number comments in OnLoaded that no longer match.
@deblasis deblasis force-pushed the feat/winui3-shell-followup branch from 10d735b to 35d666f Compare April 6, 2026 16:02
@deblasis deblasis merged commit 146fe17 into windows Apr 6, 2026
@deblasis deblasis deleted the feat/winui3-shell-followup branch April 6, 2026 16:05
@deblasis deblasis restored the feat/winui3-shell-followup branch April 6, 2026 16:05
@deblasis deblasis deleted the feat/winui3-shell-followup branch April 6, 2026 16:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant