Merge pull request 'Show add-page indicator as a bar and palm-check its taps' (#8) from flat-pages-grid into main
Reviewed-on: #8 Reviewed-by: reudy <jakubadorfman@proton.me>
This commit is contained in:
commit
2e52f1be2c
1 changed files with 83 additions and 15 deletions
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, untrack } from 'svelte';
|
import { onMount, untrack } from 'svelte';
|
||||||
import CirclePlusIcon from '@lucide/svelte/icons/circle-plus';
|
|
||||||
import DownloadIcon from '@lucide/svelte/icons/download';
|
import DownloadIcon from '@lucide/svelte/icons/download';
|
||||||
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
import EllipsisIcon from '@lucide/svelte/icons/ellipsis';
|
||||||
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
import ArrowUpIcon from '@lucide/svelte/icons/arrow-up';
|
||||||
|
|
@ -118,6 +117,17 @@
|
||||||
return rectBeside(a, slot.dir, s);
|
return rectBeside(a, slot.dir, s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Hit area of the add-page bar: a strip hugging the anchor's free edge. */
|
||||||
|
function barRect(slot: Slot): Rect | null {
|
||||||
|
const a = doc.rectOf(slot.anchor);
|
||||||
|
if (!a) return null;
|
||||||
|
const t = 20 / viewport.scale;
|
||||||
|
if (slot.dir === 'right') return { x: a.x + a.width, y: a.y, width: t, height: a.height };
|
||||||
|
if (slot.dir === 'left') return { x: a.x - t, y: a.y, width: t, height: a.height };
|
||||||
|
if (slot.dir === 'down') return { x: a.x, y: a.y + a.height, width: a.width, height: t };
|
||||||
|
return { x: a.x, y: a.y - t, width: a.width, height: t };
|
||||||
|
}
|
||||||
|
|
||||||
/** Run a tree change without the anchor page jumping on screen. */
|
/** Run a tree change without the anchor page jumping on screen. */
|
||||||
function keepAnchor(anchor: string, fn: () => void) {
|
function keepAnchor(anchor: string, fn: () => void) {
|
||||||
const before = doc.rectOf(anchor);
|
const before = doc.rectOf(anchor);
|
||||||
|
|
@ -227,6 +237,7 @@
|
||||||
| { kind: 'draw'; pointerId: number; pointerType: string; pageId: string; rec: StrokeRecorder }
|
| { kind: 'draw'; pointerId: number; pointerType: string; pageId: string; rec: StrokeRecorder }
|
||||||
| { kind: 'erase'; pointerId: number; pointerType: string; hits: Map<string, Set<string>> }
|
| { kind: 'erase'; pointerId: number; pointerType: string; hits: Map<string, Set<string>> }
|
||||||
| { kind: 'pan'; pointerId: number; lastX: number; lastY: number; startX: number; startY: number; moved: boolean; tap: boolean }
|
| { kind: 'pan'; pointerId: number; lastX: number; lastY: number; startX: number; startY: number; moved: boolean; tap: boolean }
|
||||||
|
| { kind: 'add'; pointerId: number; pointerType: string; startX: number; startY: number; slot: Slot }
|
||||||
| { kind: 'pinch' };
|
| { kind: 'pinch' };
|
||||||
|
|
||||||
let gesture: Gesture | null = null;
|
let gesture: Gesture | null = null;
|
||||||
|
|
@ -280,8 +291,10 @@
|
||||||
|
|
||||||
// Fingers ink only when enabled and no pen has been used; otherwise they pan/zoom.
|
// Fingers ink only when enabled and no pen has been used; otherwise they pan/zoom.
|
||||||
const fingerInks = tools.inking && settings.data.fingerDraw && !palm.penSeen;
|
const fingerInks = tools.inking && settings.data.fingerDraw && !palm.penSeen;
|
||||||
|
// Taps on an add-page bar get the same palm policy as writing.
|
||||||
|
const bar = target.closest<HTMLElement>('.ghost');
|
||||||
if (e.pointerType === 'touch') {
|
if (e.pointerType === 'touch') {
|
||||||
const decision = palm.evaluate(e, fingerInks ? 'draw' : 'navigate');
|
const decision = palm.evaluate(e, fingerInks || bar ? 'draw' : 'navigate');
|
||||||
if (!decision.accept) return; // palm: ignore entirely
|
if (!decision.accept) return; // palm: ignore entirely
|
||||||
touches.set(e.pointerId, p);
|
touches.set(e.pointerId, p);
|
||||||
if (touches.size >= 2) {
|
if (touches.size >= 2) {
|
||||||
|
|
@ -292,6 +305,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bar && e.button === 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
const slot = { anchor: bar.dataset.anchor!, dir: bar.dataset.dir as Dir };
|
||||||
|
gesture = { kind: 'add', pointerId: e.pointerId, pointerType: e.pointerType, startX: p.x, startY: p.y, slot };
|
||||||
|
capture(e.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const onPage = pageAt(p.x, p.y);
|
const onPage = pageAt(p.x, p.y);
|
||||||
const inEditor = !!target.closest('.cm-editor');
|
const inEditor = !!target.closest('.cm-editor');
|
||||||
// Pressing anywhere outside the text leaves the editor (preventDefault
|
// Pressing anywhere outside the text leaves the editor (preventDefault
|
||||||
|
|
@ -303,10 +324,10 @@
|
||||||
e.button === 1 ||
|
e.button === 1 ||
|
||||||
spaceDown ||
|
spaceDown ||
|
||||||
(e.pointerType === 'touch' && (!fingerInks || !onPage)) ||
|
(e.pointerType === 'touch' && (!fingerInks || !onPage)) ||
|
||||||
(e.pointerType === 'mouse' && e.button === 0 && !onPage && !target.closest('.ghost, .slot'));
|
(e.pointerType === 'mouse' && e.button === 0 && !onPage && !target.closest('.slot'));
|
||||||
if (wantsPan) {
|
if (wantsPan) {
|
||||||
if (onPage && !inEditor) doc.activeId = onPage;
|
if (onPage && !inEditor) doc.activeId = onPage;
|
||||||
gesture = { kind: 'pan', pointerId: e.pointerId, lastX: p.x, lastY: p.y, startX: p.x, startY: p.y, moved: false, tap: inEditor || !!target.closest('.ghost, .slot') };
|
gesture = { kind: 'pan', pointerId: e.pointerId, lastX: p.x, lastY: p.y, startX: p.x, startY: p.y, moved: false, tap: inEditor || !!target.closest('.slot') };
|
||||||
if (e.pointerType !== 'touch' || !gesture.tap) {
|
if (e.pointerType !== 'touch' || !gesture.tap) {
|
||||||
if (e.pointerType !== 'touch') e.preventDefault();
|
if (e.pointerType !== 'touch') e.preventDefault();
|
||||||
capture(e.pointerId);
|
capture(e.pointerId);
|
||||||
|
|
@ -375,6 +396,12 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (gesture.kind === 'add') {
|
||||||
|
// Dragging off the bar is not a tap.
|
||||||
|
if (Math.hypot(p.x - gesture.startX, p.y - gesture.startY) > 12) gesture = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (gesture.kind === 'erase') {
|
if (gesture.kind === 'erase') {
|
||||||
eraseAt(e);
|
eraseAt(e);
|
||||||
return;
|
return;
|
||||||
|
|
@ -402,7 +429,9 @@
|
||||||
if (!gesture || gesture.pointerId !== e.pointerId) return;
|
if (!gesture || gesture.pointerId !== e.pointerId) return;
|
||||||
const g = gesture;
|
const g = gesture;
|
||||||
gesture = null;
|
gesture = null;
|
||||||
if (g.kind === 'draw') {
|
if (g.kind === 'add') {
|
||||||
|
if (e.type === 'pointerup' && (g.pointerType !== 'touch' || palm.evaluate(e).accept)) addPage(g.slot.anchor, g.slot.dir);
|
||||||
|
} else if (g.kind === 'draw') {
|
||||||
live = null;
|
live = null;
|
||||||
if (e.type === 'pointerup') doc.addStroke(g.pageId, g.rec.stroke);
|
if (e.type === 'pointerup') doc.addStroke(g.pageId, g.rec.stroke);
|
||||||
} else if (g.kind === 'erase') {
|
} else if (g.kind === 'erase') {
|
||||||
|
|
@ -564,18 +593,17 @@
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#each ghostSlots as slot (slot.anchor + slot.dir)}
|
{#each ghostSlots as slot (slot.anchor + slot.dir)}
|
||||||
{@const r = slotRect(slot)}
|
{@const r = barRect(slot)}
|
||||||
{#if r}
|
{#if r}
|
||||||
|
<!-- Pointer taps go through the palm-checked gesture; onclick only serves the keyboard. -->
|
||||||
<button
|
<button
|
||||||
class="ghost"
|
class="ghost"
|
||||||
|
data-anchor={slot.anchor}
|
||||||
|
data-dir={slot.dir}
|
||||||
style="left:{r.x}px; top:{r.y}px; width:{r.width}px; height:{r.height}px"
|
style="left:{r.x}px; top:{r.y}px; width:{r.width}px; height:{r.height}px"
|
||||||
title="Add page ({slot.dir})"
|
title="Add page ({slot.dir})"
|
||||||
onclick={() => addPage(slot.anchor, slot.dir)}
|
onclick={(e) => e.detail === 0 && addPage(slot.anchor, slot.dir)}
|
||||||
>
|
></button>
|
||||||
<span class="plus" style="--icon: {Math.min(r.width, r.height) * 0.14}px">
|
|
||||||
<CirclePlusIcon size="100%" strokeWidth={1.25} />
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
|
@ -696,7 +724,49 @@
|
||||||
top: 0;
|
top: 0;
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
}
|
}
|
||||||
.ghost,
|
.ghost {
|
||||||
|
position: absolute;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
/* The visible bar: 4 screen px thick, 6 px off the page edge. */
|
||||||
|
.ghost::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background: var(--ghost-border);
|
||||||
|
border-radius: calc(2px / var(--s));
|
||||||
|
transition: background 0.12s;
|
||||||
|
}
|
||||||
|
.ghost[data-dir='left']::before,
|
||||||
|
.ghost[data-dir='right']::before {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(4px / var(--s));
|
||||||
|
}
|
||||||
|
.ghost[data-dir='up']::before,
|
||||||
|
.ghost[data-dir='down']::before {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: calc(4px / var(--s));
|
||||||
|
}
|
||||||
|
.ghost[data-dir='right']::before {
|
||||||
|
left: calc(6px / var(--s));
|
||||||
|
}
|
||||||
|
.ghost[data-dir='left']::before {
|
||||||
|
right: calc(6px / var(--s));
|
||||||
|
}
|
||||||
|
.ghost[data-dir='down']::before {
|
||||||
|
top: calc(6px / var(--s));
|
||||||
|
}
|
||||||
|
.ghost[data-dir='up']::before {
|
||||||
|
bottom: calc(6px / var(--s));
|
||||||
|
}
|
||||||
|
.ghost:hover::before,
|
||||||
|
.ghost:focus-visible::before {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
.slot {
|
.slot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -709,7 +779,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
transition: background 0.12s, color 0.12s;
|
transition: background 0.12s, color 0.12s;
|
||||||
}
|
}
|
||||||
.ghost:hover,
|
|
||||||
.slot:hover {
|
.slot:hover {
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
|
@ -720,7 +789,6 @@
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
border-color: color-mix(in srgb, var(--accent) 50%, transparent);
|
border-color: color-mix(in srgb, var(--accent) 50%, transparent);
|
||||||
}
|
}
|
||||||
.plus,
|
|
||||||
.arrow {
|
.arrow {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: var(--icon);
|
width: var(--icon);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue