mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-30 00:22:10 -04:00
fix(ui): restore all-edge modal snap zones (#2260)
This commit is contained in:
+21
-13
@@ -6,16 +6,13 @@
|
|||||||
* when the cursor is near a snap zone. On release, snaps the modal-content
|
* when the cursor is near a snap zone. On release, snaps the modal-content
|
||||||
* to fill that zone with a springy animation.
|
* to fill that zone with a springy animation.
|
||||||
*
|
*
|
||||||
* Snap zones (9):
|
* Snap zones:
|
||||||
* - top edge (10% strip) → maximize
|
* - over top edge → fullscreen
|
||||||
* - top-left corner → top-left quarter
|
* - top strip → maximize
|
||||||
* - top-right corner → top-right quarter
|
* - top edge → top half
|
||||||
* - left edge → left half
|
* - left edge → left half
|
||||||
* - right edge → right half
|
* - right edge → right half
|
||||||
* - bottom-left corner → bottom-left quarter
|
|
||||||
* - bottom-right corner → bottom-right quarter
|
|
||||||
* - bottom edge → bottom half
|
* - bottom edge → bottom half
|
||||||
* - sidebar edge (if present) → snap next to the sidebar
|
|
||||||
*
|
*
|
||||||
* Mobile (≤768px) is excluded — the swipe-dismiss UX takes precedence.
|
* Mobile (≤768px) is excluded — the swipe-dismiss UX takes precedence.
|
||||||
*
|
*
|
||||||
@@ -24,7 +21,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const EDGE_THRESHOLD_PX = 24; // how close to an edge counts as "near"
|
const EDGE_THRESHOLD_PX = 24; // how close to an edge counts as "near"
|
||||||
const CORNER_THRESHOLD_PX = 64; // corner box size
|
|
||||||
const TOP_FULL_STRIP_PX = 8; // top strip → maximize
|
const TOP_FULL_STRIP_PX = 8; // top strip → maximize
|
||||||
|
|
||||||
let _ghost = null;
|
let _ghost = null;
|
||||||
@@ -111,9 +107,13 @@ function _zoneForPointer(x, y) {
|
|||||||
return { name: 'maximize', rect: { left: safe.left, top: safe.top, width: W, height: H } };
|
return { name: 'maximize', rect: { left: safe.left, top: safe.top, width: W, height: H } };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Corner quarter-snaps DISABLED (user request) — only the top strip
|
// Symmetric edge half-snaps. The safe rect already starts to the right of
|
||||||
// (maximize) and the right/bottom half-snaps remain. The LEFT-half snap
|
// the sidebar/rail, so left-half fills the left side of the workspace
|
||||||
// is also disabled (the sidebar lives there; docking over it is awkward).
|
// without covering navigation.
|
||||||
|
if (y <= safe.top + EDGE_THRESHOLD_PX)
|
||||||
|
return { name: 'top-half', rect: { left: safe.left, top: safe.top, width: W, height: H / 2 } };
|
||||||
|
if (x <= safe.left + EDGE_THRESHOLD_PX)
|
||||||
|
return { name: 'left-half', rect: { left: safe.left, top: safe.top, width: W / 2, height: H } };
|
||||||
if (x >= safe.right - EDGE_THRESHOLD_PX)
|
if (x >= safe.right - EDGE_THRESHOLD_PX)
|
||||||
return { name: 'right-half', rect: { left: safe.left + W / 2, top: safe.top, width: W / 2, height: H } };
|
return { name: 'right-half', rect: { left: safe.left + W / 2, top: safe.top, width: W / 2, height: H } };
|
||||||
if (y >= safe.bottom - EDGE_THRESHOLD_PX)
|
if (y >= safe.bottom - EDGE_THRESHOLD_PX)
|
||||||
@@ -131,8 +131,7 @@ function _zoneForContent(content, x, y) {
|
|||||||
// flip to top tabs via CSS when the window gets narrow.
|
// flip to top tabs via CSS when the window gets narrow.
|
||||||
if (modal && modal.id === 'settings-modal' && zone.name !== 'right-half') return null;
|
if (modal && modal.id === 'settings-modal' && zone.name !== 'right-half') return null;
|
||||||
if (modal && (modal.id === 'cookbook-modal'
|
if (modal && (modal.id === 'cookbook-modal'
|
||||||
|| modal.id === 'theme-modal'
|
|| modal.id === 'theme-modal')
|
||||||
|| modal.id === 'memory-modal')
|
|
||||||
&& zone.name !== 'fullscreen') return null;
|
&& zone.name !== 'fullscreen') return null;
|
||||||
return zone;
|
return zone;
|
||||||
}
|
}
|
||||||
@@ -304,6 +303,7 @@ function _reclampAll(animate = false) {
|
|||||||
switch (name) {
|
switch (name) {
|
||||||
case 'fullscreen': r = { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }; break;
|
case 'fullscreen': r = { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight }; break;
|
||||||
case 'maximize': r = { left: safe.left, top: safe.top, width: W, height: H }; break;
|
case 'maximize': r = { left: safe.left, top: safe.top, width: W, height: H }; break;
|
||||||
|
case 'top-half': r = { left: safe.left, top: safe.top, width: W, height: H/2 }; break;
|
||||||
case 'left-half': r = { left: safe.left, top: safe.top, width: W/2, height: H }; break;
|
case 'left-half': r = { left: safe.left, top: safe.top, width: W/2, height: H }; break;
|
||||||
case 'right-half': r = { left: safe.left + W/2, top: safe.top, width: W/2, height: H }; break;
|
case 'right-half': r = { left: safe.left + W/2, top: safe.top, width: W/2, height: H }; break;
|
||||||
case 'bottom-half': r = { left: safe.left, top: safe.top + H/2, width: W, height: H/2 }; break;
|
case 'bottom-half': r = { left: safe.left, top: safe.top + H/2, width: W, height: H/2 }; break;
|
||||||
@@ -374,6 +374,14 @@ export function clearPreview() {
|
|||||||
_activeZone = null;
|
_activeZone = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function _zoneForPointerForTests(x, y) {
|
||||||
|
return _zoneForPointer(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function _zoneForContentForTests(content, x, y) {
|
||||||
|
return _zoneForContent(content, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
// Snap a modal (its .modal-content) into a previously-detected zone.
|
// Snap a modal (its .modal-content) into a previously-detected zone.
|
||||||
export function snapModalToZone(modal, zone) {
|
export function snapModalToZone(modal, zone) {
|
||||||
if (!modal || !zone) return;
|
if (!modal || !zone) return;
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
"""Regression coverage for desktop modal tile snap edge zones."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import textwrap
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_REPO = Path(__file__).resolve().parent.parent
|
||||||
|
_HELPER = _REPO / "static" / "js" / "tileManager.js"
|
||||||
|
_HAS_NODE = shutil.which("node") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def _run_tile_case():
|
||||||
|
script = textwrap.dedent(
|
||||||
|
f"""
|
||||||
|
globalThis.window = {{
|
||||||
|
innerWidth: 1200,
|
||||||
|
innerHeight: 800,
|
||||||
|
addEventListener() {{}},
|
||||||
|
}};
|
||||||
|
globalThis.document = {{
|
||||||
|
readyState: 'loading',
|
||||||
|
body: {{ appendChild() {{}} }},
|
||||||
|
documentElement: {{ style: {{ setProperty() {{}}, removeProperty() {{}} }} }},
|
||||||
|
addEventListener() {{}},
|
||||||
|
getElementById() {{ return null; }},
|
||||||
|
querySelector() {{ return null; }},
|
||||||
|
querySelectorAll() {{ return []; }},
|
||||||
|
createElement() {{
|
||||||
|
return {{
|
||||||
|
style: {{}},
|
||||||
|
classList: {{ add() {{}}, remove() {{}} }},
|
||||||
|
remove() {{}},
|
||||||
|
}};
|
||||||
|
}},
|
||||||
|
}};
|
||||||
|
globalThis.requestAnimationFrame = (fn) => fn();
|
||||||
|
globalThis.MutationObserver = class {{
|
||||||
|
observe() {{}}
|
||||||
|
disconnect() {{}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
const mod = await import('{_HELPER.as_posix()}');
|
||||||
|
const pick = (zone) => zone ? {{
|
||||||
|
name: zone.name,
|
||||||
|
rect: {{
|
||||||
|
left: zone.rect.left,
|
||||||
|
top: zone.rect.top,
|
||||||
|
width: zone.rect.width,
|
||||||
|
height: zone.rect.height,
|
||||||
|
}},
|
||||||
|
}} : null;
|
||||||
|
|
||||||
|
const memoryModal = {{ id: 'memory-modal' }};
|
||||||
|
const memoryContent = {{ closest() {{ return memoryModal; }} }};
|
||||||
|
const settingsModal = {{ id: 'settings-modal' }};
|
||||||
|
const settingsContent = {{ closest() {{ return settingsModal; }} }};
|
||||||
|
|
||||||
|
console.log(JSON.stringify({{
|
||||||
|
fullscreen: pick(mod._zoneForPointerForTests(500, 0)),
|
||||||
|
maximize: pick(mod._zoneForPointerForTests(500, 8)),
|
||||||
|
top: pick(mod._zoneForPointerForTests(500, 20)),
|
||||||
|
left: pick(mod._zoneForPointerForTests(20, 300)),
|
||||||
|
right: pick(mod._zoneForPointerForTests(1190, 300)),
|
||||||
|
bottom: pick(mod._zoneForPointerForTests(500, 790)),
|
||||||
|
memoryBottom: pick(mod._zoneForContentForTests(memoryContent, 500, 790)),
|
||||||
|
settingsTop: pick(mod._zoneForContentForTests(settingsContent, 500, 20)),
|
||||||
|
settingsRight: pick(mod._zoneForContentForTests(settingsContent, 1190, 300)),
|
||||||
|
}}));
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
proc = subprocess.run(
|
||||||
|
["node", "--input-type=module"],
|
||||||
|
input=script,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=str(_REPO),
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
assert proc.returncode == 0, proc.stderr
|
||||||
|
return json.loads(proc.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
|
||||||
|
def test_tile_manager_detects_all_four_workspace_edges():
|
||||||
|
zones = _run_tile_case()
|
||||||
|
|
||||||
|
assert zones["fullscreen"]["name"] == "fullscreen"
|
||||||
|
assert zones["maximize"]["name"] == "maximize"
|
||||||
|
assert zones["top"] == {
|
||||||
|
"name": "top-half",
|
||||||
|
"rect": {"left": 4, "top": 4, "width": 1192, "height": 396},
|
||||||
|
}
|
||||||
|
assert zones["left"] == {
|
||||||
|
"name": "left-half",
|
||||||
|
"rect": {"left": 4, "top": 4, "width": 596, "height": 792},
|
||||||
|
}
|
||||||
|
assert zones["right"] == {
|
||||||
|
"name": "right-half",
|
||||||
|
"rect": {"left": 600, "top": 4, "width": 596, "height": 792},
|
||||||
|
}
|
||||||
|
assert zones["bottom"] == {
|
||||||
|
"name": "bottom-half",
|
||||||
|
"rect": {"left": 4, "top": 400, "width": 1192, "height": 396},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH")
|
||||||
|
def test_regular_tool_modals_are_not_limited_to_fullscreen_only():
|
||||||
|
zones = _run_tile_case()
|
||||||
|
|
||||||
|
assert zones["memoryBottom"]["name"] == "bottom-half"
|
||||||
|
assert zones["settingsTop"] is None
|
||||||
|
assert zones["settingsRight"]["name"] == "right-half"
|
||||||
Reference in New Issue
Block a user