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
|
**Window resize (STOPGAP)**: undecorating the window also removes the WM's
|
||||||
resize borders, and electrobun 1.18.1 has no native resize gesture — so
|
resize borders, and electrobun 1.18.1 has no native resize gesture — so
|
||||||
`skins/7like/window-resize/` synthesizes one: eight handles overlaying the frame
|
`skins/7like/window-resize/` synthesizes one: a bottom-right grip drags the
|
||||||
drag it through `windowGetFrame`/`windowSetFrame` → `BrowserWindow.setFrame`.
|
frame through `windowGetFrame`/`windowSetFrame` → `BrowserWindow.setFrame`.
|
||||||
Mounted by all three win7 roots, hidden while maximized by a CSS `:has()` rule
|
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
|
`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.
|
`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
|
**This is meant to be deleted**: the component header carries the removal
|
||||||
checklist and `scripts/check-electrobun-resize.ts` (wired into `postinstall`)
|
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
|
- **A frameless window cannot be resized, and `preventDefault` will not save
|
||||||
you from the drag region.** Undecorating the GTK window (what
|
you from the drag region.** Undecorating the GTK window (what
|
||||||
`titleBarStyle: "hidden"` does) takes the WM's resize borders with it, and
|
`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
|
`ui/src/app/skins/7like/window-resize/` (a STOPGAP; that file carries the
|
||||||
removal checklist, and `scripts/check-electrobun-resize.ts` fires from
|
removal checklist, and `scripts/check-electrobun-resize.ts` fires from
|
||||||
`postinstall` when upstream makes it deletable). Three traps live in that
|
`postinstall` when upstream makes it deletable).
|
||||||
code, all verified against `dist/api/bun/preload/dragRegions.ts`:
|
**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
|
- It starts a window **move** from a `mousedown` listener on `document`, in
|
||||||
the **bubble** phase. It never reads `defaultPrevented`, so
|
the **bubble** phase. It never reads `defaultPrevented`, so
|
||||||
`event.preventDefault()` does **nothing** to stop it.
|
`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
|
has neither `setResizable` nor min-size constructor options. The window is still
|
||||||
*programmatically* resizable (`setFrame`/`getFrame` work, and
|
*programmatically* resizable (`setFrame`/`getFrame` work, and
|
||||||
`gtk_window_set_resizable(false)` is never called), so the app **synthesizes the
|
`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
|
input affordance**: a bottom-right grip in the webview drags the native frame
|
||||||
(`ui/src/app/skins/7like/window-resize/`).
|
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
|
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
|
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
|
// 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
|
// 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.
|
// 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 {
|
import {
|
||||||
MIN_HEIGHT,
|
MIN_HEIGHT,
|
||||||
MIN_WIDTH,
|
MIN_WIDTH,
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ export interface Frame {
|
|||||||
/** Compass point of the grabbed edge or corner. */
|
/** Compass point of the grabbed edge or corner. */
|
||||||
export type ResizeEdge = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
|
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'];
|
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 { Component, inject } from '@angular/core';
|
||||||
import { RpcService } from '@core/rpc.service';
|
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
|
* ⚠️ 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
|
* The window is still PROGRAMMATICALLY resizable — nothing ever calls
|
||||||
* `gtk_window_set_resizable(false)`, and `setFrame`/`getFrame` work. So only
|
* `gtk_window_set_resizable(false)`, and `setFrame`/`getFrame` work. So only
|
||||||
* the input affordance is missing, and that is exactly what this synthesizes:
|
* 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)
|
* 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
|
* 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
|
* 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
|
* 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
|
* min-size option. The grip occupies real estate INSIDE the window that a
|
||||||
* native frame would have provided outside it.
|
* 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:
|
* 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.
|
* 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 }`
|
* ONE handle: the bottom-right grip, exactly where Win7 put its resize gripper.
|
||||||
* 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
|
* A full eight-handle set was tried and reverted. The handles are invisible and
|
||||||
* the interface scale like everything else. Corners are listed last: same
|
* sit ON TOP of the frame (the overlay is above the title bar in paint order),
|
||||||
* z-index, later in the DOM, so they win the overlap. */
|
* so the north strip ran the full width across the caption buttons and the
|
||||||
const HANDLE_BOX: Record<ResizeEdge, string> = {
|
* north-east square sat on the close button's own top-right corner. Crossing
|
||||||
n: 'top-0 right-0 left-0 h-1.5 cursor-ns-resize',
|
* one stole `:hover` from the button underneath, and since 7.css cross-fades
|
||||||
s: 'right-0 bottom-0 left-0 h-1.5 cursor-ns-resize',
|
* the caption glow, a stolen hover reads as the glow flickering or sticking —
|
||||||
w: 'top-0 bottom-0 left-0 w-1.5 cursor-ew-resize',
|
* "weird artifacts on the buttons", with no visual bug anywhere in the CSS.
|
||||||
e: 'top-0 right-0 bottom-0 w-1.5 cursor-ew-resize',
|
*
|
||||||
nw: 'top-0 left-0 size-3 cursor-nwse-resize',
|
* The bottom-right corner is the one place that overlaps nothing: it lands in
|
||||||
ne: 'top-0 right-0 size-3 cursor-nesw-resize',
|
* the 6px gutter that `.window-body { margin: 0 6px 6px }` leaves below and
|
||||||
sw: 'bottom-0 left-0 size-3 cursor-nesw-resize',
|
* right of the content. `resizeFrame` stays general (and the probe still covers
|
||||||
se: 'right-0 bottom-0 size-3 cursor-nwse-resize',
|
* 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({
|
@Component({
|
||||||
selector: 'app-window-resize',
|
selector: 'app-window-resize',
|
||||||
template: `
|
template: `
|
||||||
@for (edge of edges; track edge) {
|
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto absolute touch-none electrobun-webkit-app-region-no-drag {{ box[edge] }}"
|
class="pointer-events-auto absolute touch-none electrobun-webkit-app-region-no-drag {{ gripBox }}"
|
||||||
(pointerdown)="onPointerDown($event, edge)"
|
(pointerdown)="onPointerDown($event)"
|
||||||
(pointermove)="onPointerMove($event)"
|
(pointermove)="onPointerMove($event)"
|
||||||
(pointerup)="onPointerEnd()"
|
(pointerup)="onPointerEnd()"
|
||||||
(pointercancel)="onPointerEnd()"
|
(pointercancel)="onPointerEnd()"
|
||||||
></div>
|
></div>
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
// The overlay spans the frame but is transparent to the pointer; only the
|
// 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.
|
// 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' },
|
host: { class: 'w7-resize pointer-events-none absolute inset-0 z-10' },
|
||||||
})
|
})
|
||||||
export class WindowResize {
|
export class WindowResize {
|
||||||
protected readonly edges = RESIZE_EDGES;
|
protected readonly gripBox = GRIP_BOX;
|
||||||
protected readonly box = HANDLE_BOX;
|
|
||||||
|
|
||||||
private readonly rpc = inject(RpcService);
|
private readonly rpc = inject(RpcService);
|
||||||
|
|
||||||
/** Null except during a drag. Set only once the frame read resolves. */
|
/** 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. */
|
/** Newest target frame not yet sent, if a send is already in flight. */
|
||||||
private pending: Frame | null = null;
|
private pending: Frame | null = null;
|
||||||
private sending = false;
|
private sending = false;
|
||||||
|
|
||||||
onPointerDown(event: PointerEvent, edge: ResizeEdge): void {
|
onPointerDown(event: PointerEvent): void {
|
||||||
if (!this.rpc.available || event.button !== 0) return;
|
if (!this.rpc.available || event.button !== 0) return;
|
||||||
(event.target as HTMLElement).setPointerCapture(event.pointerId);
|
(event.target as HTMLElement).setPointerCapture(event.pointerId);
|
||||||
// captured synchronously — the frame read below is a round trip
|
// captured synchronously — the frame read below is a round trip
|
||||||
@@ -149,7 +158,6 @@ export class WindowResize {
|
|||||||
void this.rpc.request.windowGetFrame({}).then((res) => {
|
void this.rpc.request.windowGetFrame({}).then((res) => {
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
this.drag = {
|
this.drag = {
|
||||||
edge,
|
|
||||||
screenX,
|
screenX,
|
||||||
screenY,
|
screenY,
|
||||||
frame: { x: res.x, y: res.y, width: res.width, height: res.height },
|
frame: { x: res.x, y: res.y, width: res.width, height: res.height },
|
||||||
@@ -161,7 +169,7 @@ export class WindowResize {
|
|||||||
const drag = this.drag;
|
const drag = this.drag;
|
||||||
if (!drag) return; // moved before the frame arrived, or not dragging
|
if (!drag) return; // moved before the frame arrived, or not dragging
|
||||||
this.pending = resizeFrame(
|
this.pending = resizeFrame(
|
||||||
drag.edge,
|
GRIP_EDGE,
|
||||||
drag.frame,
|
drag.frame,
|
||||||
event.screenX - drag.screenX,
|
event.screenX - drag.screenX,
|
||||||
event.screenY - drag.screenY,
|
event.screenY - drag.screenY,
|
||||||
|
|||||||
Reference in New Issue
Block a user