Fix active-pane click via document-capture pointerdown
Root cause: xterm.js attaches its own pointerdown handler inside the
terminal and calls e.stopPropagation(), which prevents the .leaf
div's onpointerdown from firing for any click landing inside the
terminal body. That's why clicking pane bodies never moved the blue
active border — the event simply never reached our handler.
Fix: register a document-level CAPTURE-phase pointerdown listener
in App.svelte. Capture fires before xterm.js's bubble-phase handler
runs (and before it can stop propagation), so we always see the
click. The handler walks up via Element.closest('[data-leaf-id]')
to find which pane was clicked, then calls orch.setActive.
- LeafPane.svelte: add data-leaf-id={leaf.id} attribute so the
document handler can identify the clicked pane.
- App.svelte: $effect attaches document.addEventListener('pointerdown',
..., true) and cleans up on teardown.
- Keep the per-leaf onpointerdown as a redundant backup for clicks
on toolbar buttons (which sit outside the xterm subtree). Cheap
+ idempotent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0
dev.err
Normal file
BIN
dev.log
Normal file
BIN
screen0.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
screen1.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
screen2.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
screenshot.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
shot01-initial.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
shot02-clicked-pane1.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
shot03-f12.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
shot04-clicked-pane2-immediate.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
shot05-clicked-pane2-after-1s.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
shot06-clicked-pane2-toolbar.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
shot07-devtools-attempt.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
shot08-after-hmr.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot09-after-reload.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot10-after-restart.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot11-clicked-pane2-toolbar.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot12-clicked-bcast.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot13-back-to-pane1.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot14-pane1-bcast.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot15-palette.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
shot16-pane1-body.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot17-capture-fix-test.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot18-restart.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot19-pane1-body-after-capture.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot20-pane2-body.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot21-baseline.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot22-pane1-body.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot23-pane2-body.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
shot24-pane1-again.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot25-slow-pane2.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot26-slow-pane1.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot27-slow-pane2-again.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot28-fresh.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot29-click1.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot30-click2.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
shot31-click3.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
|
|
@ -184,6 +184,26 @@
|
|||
return () => window.removeEventListener("keydown", onKey, true);
|
||||
});
|
||||
|
||||
// ---- Document-level active-pane detector --------------------------------
|
||||
// xterm.js calls `stopPropagation` on pointerdown inside terminals, so a
|
||||
// per-leaf `onpointerdown` never fires for body clicks. A document-level
|
||||
// CAPTURE-phase listener fires before xterm.js can intercept, then finds
|
||||
// the nearest `data-leaf-id` ancestor to know which pane was clicked.
|
||||
// Toolbar buttons also pass through (they're outside the xterm container,
|
||||
// their own onclick still fires in the bubble phase afterwards).
|
||||
$effect(() => {
|
||||
function onAnyPointerDown(e: PointerEvent) {
|
||||
const t = e.target as Element | null;
|
||||
if (!t) return;
|
||||
const leafEl = t.closest("[data-leaf-id]");
|
||||
if (!leafEl) return;
|
||||
const id = leafEl.getAttribute("data-leaf-id");
|
||||
if (id) orch.setActive(id);
|
||||
}
|
||||
document.addEventListener("pointerdown", onAnyPointerDown, true);
|
||||
return () => document.removeEventListener("pointerdown", onAnyPointerDown, true);
|
||||
});
|
||||
|
||||
// ---- preset layouts ------------------------------------------------------
|
||||
function applyPreset(make: (d: { distro?: string }) => TreeNode) {
|
||||
const count = leafCount(tree);
|
||||
|
|
|
|||
|
|
@ -103,9 +103,11 @@
|
|||
if (active) focusTrigger += 1;
|
||||
});
|
||||
|
||||
// Backup setActive for toolbar clicks (which can't reach the document
|
||||
// capture listener if a bubble-phase handler stops propagation). Cheap;
|
||||
// idempotent if the document listener also fired.
|
||||
function onPaneClick() {
|
||||
console.log("[tiletopia] pane click:", leaf.id, "currentlyActive:", active);
|
||||
if (!active) orch.setActive(leaf.id);
|
||||
orch.setActive(leaf.id);
|
||||
}
|
||||
|
||||
// ---- pane id registration ------------------------------------------------
|
||||
|
|
@ -121,6 +123,7 @@
|
|||
class:broadcasting={leaf.broadcast}
|
||||
role="group"
|
||||
aria-label={"Terminal pane: " + (leaf.label ?? leaf.distro ?? "unnamed")}
|
||||
data-leaf-id={leaf.id}
|
||||
onpointerdown={onPaneClick}
|
||||
>
|
||||
<div class="pane-toolbar">
|
||||
|
|
|
|||
60
tilescript.ps1
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Helpers for driving the tiletopia window from PowerShell.
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
Add-Type @'
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class W {
|
||||
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
||||
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
|
||||
[DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int n);
|
||||
[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);
|
||||
[DllImport("user32.dll")] public static extern void mouse_event(uint flags, uint dx, uint dy, uint dwData, int extraInfo);
|
||||
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
||||
}
|
||||
'@
|
||||
|
||||
function Get-TileWindow {
|
||||
$p = Get-Process tiletopia -ErrorAction Stop | Where-Object { $_.MainWindowHandle -ne 0 } | Select-Object -First 1
|
||||
if (-not $p) { throw "no tiletopia window" }
|
||||
return $p
|
||||
}
|
||||
|
||||
function Bring-ToFront {
|
||||
$p = Get-TileWindow
|
||||
[W]::ShowWindow($p.MainWindowHandle, 9) | Out-Null # SW_RESTORE
|
||||
[W]::SetForegroundWindow($p.MainWindowHandle) | Out-Null
|
||||
Start-Sleep -Milliseconds 300
|
||||
$r = New-Object W+RECT
|
||||
[W]::GetWindowRect($p.MainWindowHandle, [ref] $r) | Out-Null
|
||||
return $r
|
||||
}
|
||||
|
||||
function Save-Shot([string]$path) {
|
||||
$r = Bring-ToFront
|
||||
$w = $r.Right - $r.Left
|
||||
$h = $r.Bottom - $r.Top
|
||||
$bmp = New-Object System.Drawing.Bitmap $w, $h
|
||||
$g = [System.Drawing.Graphics]::FromImage($bmp)
|
||||
$g.CopyFromScreen($r.Left, $r.Top, 0, 0, (New-Object System.Drawing.Size $w, $h))
|
||||
$bmp.Save($path)
|
||||
$g.Dispose()
|
||||
$bmp.Dispose()
|
||||
Write-Host ("saved " + $path + " (" + $w + "x" + $h + " at " + $r.Left + "," + $r.Top + ")")
|
||||
return @{ x = $r.Left; y = $r.Top; w = $w; h = $h }
|
||||
}
|
||||
|
||||
# Click at *window-relative* coordinates (so we don't depend on the window's
|
||||
# screen position from one run to the next).
|
||||
function Click-Win([int]$relX, [int]$relY) {
|
||||
$r = Bring-ToFront
|
||||
$absX = $r.Left + $relX
|
||||
$absY = $r.Top + $relY
|
||||
[W]::SetCursorPos($absX, $absY) | Out-Null
|
||||
Start-Sleep -Milliseconds 80
|
||||
[W]::mouse_event(0x0002, 0, 0, 0, 0) # LEFTDOWN
|
||||
Start-Sleep -Milliseconds 30
|
||||
[W]::mouse_event(0x0004, 0, 0, 0, 0) # LEFTUP
|
||||
Start-Sleep -Milliseconds 120
|
||||
Write-Host ("clicked window-rel " + $relX + "," + $relY + " (abs " + $absX + "," + $absY + ")")
|
||||
}
|
||||
BIN
tiletopia-window.png
Normal file
|
After Width: | Height: | Size: 52 KiB |