Reduce the synthesized resize to the bottom-right grip only
The handles are invisible and paint above the frame, so the north strip ran the full window width across the caption buttons and the north-east square sat on the close button's own top-right corner. Crossing one dropped that button's :hover mid-glow, which reads as the Aero glow flickering or sticking — an artifact with nothing wrong in the CSS at all. Only the bottom-right corner overlaps nothing: it lands in the 6px gutter that .window-body's margin leaves below and right of the content. That is also where Win7 put its own resize gripper, so one grip is the more faithful answer anyway. Bumped to 1rem from 0.75rem since it is now the only way to resize. resizeFrame stays general and the probe still exercises all eight edges: the origin-moving cases are the subtle ones and are worth keeping proven, so restoring a side later is one entry in the component against tested math. Both the component and gotchas.md now spell out which edges are safe to restore (south, east, west) and which are not (north and both top corners). Harness diffs 0; math, hover and menu probes clean. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -483,12 +483,16 @@ lights to reserve `pl-24` for.
|
||||
|
||||
**Window resize (STOPGAP)**: undecorating the window also removes the WM's
|
||||
resize borders, and electrobun 1.18.1 has no native resize gesture — so
|
||||
`skins/7like/window-resize/` synthesizes one: eight handles overlaying the frame
|
||||
drag it through `windowGetFrame`/`windowSetFrame` → `BrowserWindow.setFrame`.
|
||||
`skins/7like/window-resize/` synthesizes one: a bottom-right grip drags the
|
||||
frame through `windowGetFrame`/`windowSetFrame` → `BrowserWindow.setFrame`.
|
||||
Mounted by all three win7 roots, hidden while maximized by a CSS `:has()` rule
|
||||
(no second `windowState` subscription), and every handle carries
|
||||
(no second `windowState` subscription), and the grip carries
|
||||
`electrobun-webkit-app-region-no-drag` or the drag region would move the window
|
||||
instead. The frame math is a dependency-free module so
|
||||
instead. **One corner, not eight handles**: the handles are invisible and paint
|
||||
above the frame, so a north strip or top corner overlays the caption buttons and
|
||||
steals their `:hover`, which reads as the Aero glow flickering on those buttons.
|
||||
The bottom-right corner is the only one that overlaps nothing (it sits in the
|
||||
6px gutter `.window-body`'s margin leaves). The frame math is a dependency-free module so
|
||||
`scripts/resize-math-probe.ts` can assert all eight edges without a window.
|
||||
**This is meant to be deleted**: the component header carries the removal
|
||||
checklist and `scripts/check-electrobun-resize.ts` (wired into `postinstall`)
|
||||
|
||||
+13
-3
@@ -269,11 +269,21 @@ touching the adjacent code.
|
||||
- **A frameless window cannot be resized, and `preventDefault` will not save
|
||||
you from the drag region.** Undecorating the GTK window (what
|
||||
`titleBarStyle: "hidden"` does) takes the WM's resize borders with it, and
|
||||
electrobun exposes no resize gesture — hence the synthesized handles in
|
||||
electrobun exposes no resize gesture — hence the synthesized grip in
|
||||
`ui/src/app/skins/7like/window-resize/` (a STOPGAP; that file carries the
|
||||
removal checklist, and `scripts/check-electrobun-resize.ts` fires from
|
||||
`postinstall` when upstream makes it deletable). Three traps live in that
|
||||
code, all verified against `dist/api/bun/preload/dragRegions.ts`:
|
||||
`postinstall` when upstream makes it deletable).
|
||||
**An invisible resize handle over a control steals that control's hover.** A
|
||||
full eight-handle set shipped briefly and was reverted: the north strip ran
|
||||
the whole width across the caption buttons and the north-east square sat on
|
||||
the close button's corner, so crossing one dropped the button's `:hover`
|
||||
mid-glow and looked like a rendering artifact in 7.css — nothing in the CSS
|
||||
was wrong. Only the bottom-right corner overlaps nothing (it lands in the 6px
|
||||
gutter `.window-body { margin: 0 6px 6px }` leaves). Any new handle must be
|
||||
checked against what sits under it; south/east/west are innocent, north and
|
||||
both top corners are not.
|
||||
Three more traps live in that code, all verified against
|
||||
`dist/api/bun/preload/dragRegions.ts`:
|
||||
- It starts a window **move** from a `mousedown` listener on `document`, in
|
||||
the **bubble** phase. It never reads `defaultPrevented`, so
|
||||
`event.preventDefault()` does **nothing** to stop it.
|
||||
|
||||
@@ -44,8 +44,10 @@ counterpart, does not link `gtk_window_begin_resize_drag`, and `BrowserWindow`
|
||||
has neither `setResizable` nor min-size constructor options. The window is still
|
||||
*programmatically* resizable (`setFrame`/`getFrame` work, and
|
||||
`gtk_window_set_resizable(false)` is never called), so the app **synthesizes the
|
||||
input affordance**: eight handles in the webview drag the native frame over RPC
|
||||
(`ui/src/app/skins/7like/window-resize/`).
|
||||
input affordance**: a bottom-right grip in the webview drags the native frame
|
||||
over RPC (`ui/src/app/skins/7like/window-resize/`). Only that one corner — an
|
||||
invisible handle anywhere else overlays a control and steals its hover, which is
|
||||
what a full edge set did to the caption buttons.
|
||||
|
||||
That is a **stopgap** — each drag tick is an RPC hop plus an FFI call, where a
|
||||
real WM grab is compositor-side. `scripts/check-electrobun-resize.ts` watches the
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
// The failure this guards against is not "the window does not resize" (obvious
|
||||
// on sight) but "dragging the west edge inward past the minimum walks the whole
|
||||
// window across the screen" — correct-looking until you hit the limit.
|
||||
//
|
||||
// This covers MORE than the UI currently mounts, deliberately: only the
|
||||
// bottom-right grip is wired up (an invisible handle over a caption button
|
||||
// steals its hover — see the component), but the math stays general so that
|
||||
// restoring a side later is one line against already-proven arithmetic.
|
||||
import {
|
||||
MIN_HEIGHT,
|
||||
MIN_WIDTH,
|
||||
|
||||
@@ -18,7 +18,14 @@ export interface Frame {
|
||||
/** Compass point of the grabbed edge or corner. */
|
||||
export type ResizeEdge = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
||||
|
||||
/** Every handle, in DOM order: edges first so the corners overlay them. */
|
||||
/**
|
||||
* Every edge the arithmetic supports — NOT what the UI mounts. The window
|
||||
* currently offers only the bottom-right grip (`window-resize.ts` explains why
|
||||
* the others were removed: an invisible handle over a caption button steals its
|
||||
* hover). This stays complete because the origin-moving cases are the subtle
|
||||
* ones and the probe is where they are proven; restoring a side is then one
|
||||
* entry in the component, against already-tested math.
|
||||
*/
|
||||
export const RESIZE_EDGES: readonly ResizeEdge[] = ['n', 's', 'w', 'e', 'nw', 'ne', 'sw', 'se'];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RpcService } from '@core/rpc.service';
|
||||
import { type Frame, RESIZE_EDGES, type ResizeEdge, resizeFrame } from './resize-math';
|
||||
import { type Frame, type ResizeEdge, resizeFrame } from './resize-math';
|
||||
|
||||
/* ============================================================================
|
||||
* ⚠️ STOPGAP — DELETE THIS WHOLE FEATURE WHEN ELECTROBUN SHIPS WINDOW RESIZE
|
||||
@@ -33,15 +33,19 @@ import { type Frame, RESIZE_EDGES, type ResizeEdge, resizeFrame } from './resize
|
||||
* The window is still PROGRAMMATICALLY resizable — nothing ever calls
|
||||
* `gtk_window_set_resizable(false)`, and `setFrame`/`getFrame` work. So only
|
||||
* the input affordance is missing, and that is exactly what this synthesizes:
|
||||
* eight handles → pointer drag → `windowSetFrame` RPC → `BrowserWindow.setFrame`.
|
||||
* a bottom-right grip → pointer drag → `windowSetFrame` RPC →
|
||||
* `BrowserWindow.setFrame`. One corner, not eight handles — see GRIP_BOX for
|
||||
* why the others had to go.
|
||||
*
|
||||
* WHAT IT COSTS (i.e. why it should die as soon as it can)
|
||||
*
|
||||
* Every drag tick is an RPC hop plus a synchronous FFI call, where a real WM
|
||||
* grab is a compositor-side operation — so it is jerkier than a native resize
|
||||
* and always will be. Minimum size is enforced in JS because electrobun has no
|
||||
* min-size option. And the handles occupy real estate inside the window that a
|
||||
* native frame would have provided outside it.
|
||||
* min-size option. The grip occupies real estate INSIDE the window that a
|
||||
* native frame would have provided outside it — which is also why there is only
|
||||
* one of them: an invisible handle over a control steals that control's hover
|
||||
* (see GRIP_BOX). A real WM resize border has neither problem.
|
||||
*
|
||||
* ────────────────────────────────────────────────────────────────────────────
|
||||
* REMOVAL CHECKLIST — when upstream ships a resize gesture, delete ALL of:
|
||||
@@ -94,53 +98,58 @@ import { type Frame, RESIZE_EDGES, type ResizeEdge, resizeFrame } from './resize
|
||||
* have no `off()`, so every subscription is permanent.
|
||||
*/
|
||||
|
||||
/** Edge → the Tailwind box that places and cues it. Edge thickness is 0.375rem
|
||||
* = 6px at 1×, matching the gutter 7.css's `.window-body { margin: 0 6px 6px }`
|
||||
* leaves around the content, so left/right/bottom handles sit on frame chrome
|
||||
* instead of stealing clicks from the panels. Rem, so the grab area scales with
|
||||
* the interface scale like everything else. Corners are listed last: same
|
||||
* z-index, later in the DOM, so they win the overlap. */
|
||||
const HANDLE_BOX: Record<ResizeEdge, string> = {
|
||||
n: 'top-0 right-0 left-0 h-1.5 cursor-ns-resize',
|
||||
s: 'right-0 bottom-0 left-0 h-1.5 cursor-ns-resize',
|
||||
w: 'top-0 bottom-0 left-0 w-1.5 cursor-ew-resize',
|
||||
e: 'top-0 right-0 bottom-0 w-1.5 cursor-ew-resize',
|
||||
nw: 'top-0 left-0 size-3 cursor-nwse-resize',
|
||||
ne: 'top-0 right-0 size-3 cursor-nesw-resize',
|
||||
sw: 'bottom-0 left-0 size-3 cursor-nesw-resize',
|
||||
se: 'right-0 bottom-0 size-3 cursor-nwse-resize',
|
||||
};
|
||||
/**
|
||||
* ONE handle: the bottom-right grip, exactly where Win7 put its resize gripper.
|
||||
*
|
||||
* A full eight-handle set was tried and reverted. The handles are invisible and
|
||||
* sit ON TOP of the frame (the overlay is above the title bar in paint order),
|
||||
* so the north strip ran the full width across the caption buttons and the
|
||||
* north-east square sat on the close button's own top-right corner. Crossing
|
||||
* one stole `:hover` from the button underneath, and since 7.css cross-fades
|
||||
* the caption glow, a stolen hover reads as the glow flickering or sticking —
|
||||
* "weird artifacts on the buttons", with no visual bug anywhere in the CSS.
|
||||
*
|
||||
* The bottom-right corner is the one place that overlaps nothing: it lands in
|
||||
* the 6px gutter that `.window-body { margin: 0 6px 6px }` leaves below and
|
||||
* right of the content. `resizeFrame` stays general (and the probe still covers
|
||||
* all eight edges) so restoring a side is one entry here — but any new handle
|
||||
* must be checked against what it covers first. South, east and west are the
|
||||
* innocent ones; north and both top corners are not.
|
||||
*
|
||||
* 1rem, not the 0.75rem the corners had: this is now the only way to resize the
|
||||
* window, so the target earns the extra pixels. Rem, so it scales with the
|
||||
* interface scale like everything else.
|
||||
*/
|
||||
const GRIP_EDGE: ResizeEdge = 'se';
|
||||
const GRIP_BOX = 'right-0 bottom-0 size-4 cursor-nwse-resize';
|
||||
|
||||
@Component({
|
||||
selector: 'app-window-resize',
|
||||
template: `
|
||||
@for (edge of edges; track edge) {
|
||||
<div
|
||||
class="pointer-events-auto absolute touch-none electrobun-webkit-app-region-no-drag {{ box[edge] }}"
|
||||
(pointerdown)="onPointerDown($event, edge)"
|
||||
class="pointer-events-auto absolute touch-none electrobun-webkit-app-region-no-drag {{ gripBox }}"
|
||||
(pointerdown)="onPointerDown($event)"
|
||||
(pointermove)="onPointerMove($event)"
|
||||
(pointerup)="onPointerEnd()"
|
||||
(pointercancel)="onPointerEnd()"
|
||||
></div>
|
||||
}
|
||||
`,
|
||||
// The overlay spans the frame but is transparent to the pointer; only the
|
||||
// handles inside it take hits. `w7-resize` is what the maximized rule hides.
|
||||
host: { class: 'w7-resize pointer-events-none absolute inset-0 z-10' },
|
||||
})
|
||||
export class WindowResize {
|
||||
protected readonly edges = RESIZE_EDGES;
|
||||
protected readonly box = HANDLE_BOX;
|
||||
protected readonly gripBox = GRIP_BOX;
|
||||
|
||||
private readonly rpc = inject(RpcService);
|
||||
|
||||
/** Null except during a drag. Set only once the frame read resolves. */
|
||||
private drag: { edge: ResizeEdge; screenX: number; screenY: number; frame: Frame } | null = null;
|
||||
private drag: { screenX: number; screenY: number; frame: Frame } | null = null;
|
||||
/** Newest target frame not yet sent, if a send is already in flight. */
|
||||
private pending: Frame | null = null;
|
||||
private sending = false;
|
||||
|
||||
onPointerDown(event: PointerEvent, edge: ResizeEdge): void {
|
||||
onPointerDown(event: PointerEvent): void {
|
||||
if (!this.rpc.available || event.button !== 0) return;
|
||||
(event.target as HTMLElement).setPointerCapture(event.pointerId);
|
||||
// captured synchronously — the frame read below is a round trip
|
||||
@@ -149,7 +158,6 @@ export class WindowResize {
|
||||
void this.rpc.request.windowGetFrame({}).then((res) => {
|
||||
if (!res.ok) return;
|
||||
this.drag = {
|
||||
edge,
|
||||
screenX,
|
||||
screenY,
|
||||
frame: { x: res.x, y: res.y, width: res.width, height: res.height },
|
||||
@@ -161,7 +169,7 @@ export class WindowResize {
|
||||
const drag = this.drag;
|
||||
if (!drag) return; // moved before the frame arrived, or not dragging
|
||||
this.pending = resizeFrame(
|
||||
drag.edge,
|
||||
GRIP_EDGE,
|
||||
drag.frame,
|
||||
event.screenX - drag.screenX,
|
||||
event.screenY - drag.screenY,
|
||||
|
||||
Reference in New Issue
Block a user