Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a0c708140 |
@@ -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)"
|
||||
(pointermove)="onPointerMove($event)"
|
||||
(pointerup)="onPointerEnd()"
|
||||
(pointercancel)="onPointerEnd()"
|
||||
></div>
|
||||
}
|
||||
<div
|
||||
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