docs: preview drag-out design spec
This commit is contained in:
1 parent
74dff593eb
commit
328b3f058e
1 file changed
+146
@@ -0,0 +1,146 @@
|
|||||||
|
# valw: drag the preview out (design)
|
||||||
|
|
||||||
|
Date: 2026-10-01
|
||||||
|
Status: approved in brainstorming, pending written-spec review
|
||||||
|
Builds on: `2026-09-30-valw-phase-2-preview-design.md` (Phase 2, on `main`)
|
||||||
|
|
||||||
|
## 1. Context and goal
|
||||||
|
|
||||||
|
After a day of real use, the one thing missing from the preview was being
|
||||||
|
able to grab a thumbnail and drop the screenshot into another app (a chat,
|
||||||
|
a browser upload field, a file manager, an image editor) without looking
|
||||||
|
for the file. The vision document lists this as "Dışarı sürükle" in 4.4.
|
||||||
|
|
||||||
|
Success: dragging a thumbnail out works with the user's everyday apps, and
|
||||||
|
click → Satty and swipe → dismiss keep working exactly as before.
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- A config switch for drag-and-drop (it is always on).
|
||||||
|
- Anything else from the post-test-day list (hover pause, notifications,
|
||||||
|
right-click menu, the Phase 2 deferred minors).
|
||||||
|
|
||||||
|
## 2. Decisions
|
||||||
|
|
||||||
|
| Question | Decision | Why |
|
||||||
|
|---|---|---|
|
||||||
|
| Swipe vs drag-out | By direction: mostly rightward = swipe, anything else = drag-out | Thumbnails sit in the bottom-right corner, so drop targets are always left/up; nothing existing changes |
|
||||||
|
| What is carried | `text/uri-list` (the file) and `image/png` (the bytes) | Each app takes what it understands |
|
||||||
|
| After a drag | Dropped → the thumbnail closes; cancelled → it comes back | Like macOS; a missed drop can be retried |
|
||||||
|
| What moves under the pointer | The thumbnail image as the drag icon; the original hides | You see what you carry |
|
||||||
|
| Action | Copy | The saved file stays where it is |
|
||||||
|
|
||||||
|
## 3. Gesture
|
||||||
|
|
||||||
|
A press on a thumbnail is classified once, as soon as the pointer has moved
|
||||||
|
at least `CLICK_SLOP` (4 logical px) from the press point, and never
|
||||||
|
changes afterwards:
|
||||||
|
|
||||||
|
- **Swipe:** `dx > 0` and `|dy| ≤ dx` (rightward, at most 45° off
|
||||||
|
horizontal). Behaviour unchanged from Phase 2.
|
||||||
|
- **Drag-out:** any other direction.
|
||||||
|
- **Click:** released before the slop is reached. Behaviour unchanged
|
||||||
|
(opens Satty).
|
||||||
|
|
||||||
|
`stack::classify(dx, dy) -> Option<Gesture>` (`None` below the slop) is a
|
||||||
|
pure function; the thumbnail remembers the first `Some`.
|
||||||
|
|
||||||
|
## 4. Drag flow
|
||||||
|
|
||||||
|
1. On the move that classifies the press as a drag-out, the host creates a
|
||||||
|
`wl_data_source` (SCTK `create_drag_and_drop_source`, mime types from
|
||||||
|
section 5, action `Copy`) and calls `start_drag` with the thumbnail's
|
||||||
|
surface as origin, a new icon surface, and the serial of the pointer
|
||||||
|
press that started it.
|
||||||
|
2. The icon surface shows the thumbnail image at thumbnail size (buffer at
|
||||||
|
the output's scale, viewporter to logical size, 1 px border as before).
|
||||||
|
Its offset (`wl_surface.offset`, wl_compositor v5+) keeps the point the
|
||||||
|
user grabbed under the pointer.
|
||||||
|
3. The original thumbnail hides itself (transparent buffer, empty input
|
||||||
|
region: the Phase 2 mechanism) for the duration of the drag. Only this
|
||||||
|
thumbnail; the rest of the stack is unaffected. "Hidden for a capture"
|
||||||
|
and "hidden for a drag" are separate flags, and a thumbnail is visible
|
||||||
|
only when neither is set: a capture that ends mid-drag must not bring
|
||||||
|
back the thumbnail being dragged.
|
||||||
|
4. While dragging, the compositor owns the pointer. If the thumbnail's
|
||||||
|
timer fires, it is only recorded (`expired`); the thumbnail doesn't
|
||||||
|
close mid-drag.
|
||||||
|
5. `dnd_finished` (dropped and accepted): the thumbnail closes at once (it
|
||||||
|
is already invisible), and the stack restacks.
|
||||||
|
6. `cancelled` (released on nothing, rejected, Esc): the icon goes away and
|
||||||
|
the thumbnail reappears with the slide-in animation, or, if it expired
|
||||||
|
during the drag, slides out right away.
|
||||||
|
7. The data source and icon surface are destroyed after either outcome.
|
||||||
|
|
||||||
|
The host binds `wl_data_device_manager` (SCTK `DataDeviceManagerState`)
|
||||||
|
and gets the seat's data device when the pointer capability appears. The
|
||||||
|
host never receives drops; SCTK's `DataDeviceHandler` and
|
||||||
|
`DataOfferHandler` get empty implementations.
|
||||||
|
|
||||||
|
A missing `wl_data_device_manager` (not the case on niri) disables
|
||||||
|
drag-out: the gesture falls back to snap-back, and a warning is logged
|
||||||
|
once.
|
||||||
|
|
||||||
|
## 5. Data
|
||||||
|
|
||||||
|
| Mime type | Written to the receiving app's pipe |
|
||||||
|
|---|---|
|
||||||
|
| `text/uri-list` | `file_uri(path)` followed by `\r\n` |
|
||||||
|
| `image/png` | the file's bytes, read from disk when requested |
|
||||||
|
|
||||||
|
`file_uri(path)`: `file://` plus the absolute path with every byte
|
||||||
|
percent-encoded except the RFC 3986 unreserved characters
|
||||||
|
(`A-Z a-z 0-9 - . _ ~`) and `/`. Spaces become `%20`, UTF-8 (e.g. Turkish
|
||||||
|
`ı`, `ş`) is encoded byte by byte, `%` and `#` are encoded.
|
||||||
|
|
||||||
|
Each `send_request` writes on its own short-lived thread, so a slow reader
|
||||||
|
or a large PNG can't block the host's event loop (and with it every
|
||||||
|
thumbnail). A failed read or write (file removed, reader gone) is logged;
|
||||||
|
the pipe is closed and the host keeps running.
|
||||||
|
|
||||||
|
## 6. Code layout
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|---|---|
|
||||||
|
| `src/stack.rs` | `enum Gesture { Swipe, DragOut }`, `classify(dx, dy)` |
|
||||||
|
| `src/dnd.rs` (new) | `file_uri`, `MIME_TYPES`, `write_offer(mime, path, fd)` (the thread body), the after-drag decision |
|
||||||
|
| `src/thumbnail.rs` | gesture state; drag icon surface; per-thumbnail hide/show around the drag |
|
||||||
|
| `src/host.rs` | data device, drag source, `DataSourceHandler` callbacks routed to the thumbnail |
|
||||||
|
| `src/wayland.rs` | `DataDeviceManagerState` in `State`, delegate impls |
|
||||||
|
|
||||||
|
## 7. Testing
|
||||||
|
|
||||||
|
### 7.1 Unit tests (`cargo nextest`)
|
||||||
|
|
||||||
|
- `stack::classify`: under 4 px → `None`; right and 30° off right →
|
||||||
|
`Swipe`; exactly 45° → `Swipe`; left, up, down, and steep right →
|
||||||
|
`DragOut`.
|
||||||
|
- `dnd::file_uri`: plain path; spaces; Turkish characters; `%` and `#`;
|
||||||
|
`/` kept.
|
||||||
|
- After-drag decision: dropped → close; cancelled and not expired → come
|
||||||
|
back; cancelled and expired → slide out.
|
||||||
|
- `dnd::write_offer` through a real pipe: `text/uri-list` produces the
|
||||||
|
exact line; `image/png` produces the file bytes; a missing file is an
|
||||||
|
error, not a panic.
|
||||||
|
|
||||||
|
### 7.2 Headless check
|
||||||
|
|
||||||
|
Unchanged: drag-and-drop needs synthetic pointer input. The existing
|
||||||
|
checks guard click/swipe/hide against regressions.
|
||||||
|
|
||||||
|
### 7.3 Manual checks on niri
|
||||||
|
|
||||||
|
- A drag can be started from the thumbnail (layer surface) at all
|
||||||
|
(verified while prototyping, before the plan).
|
||||||
|
- Drop into Dolphin → file copied; into a browser upload field → uploads;
|
||||||
|
into a Discord message → attaches.
|
||||||
|
- Swipe right still dismisses; click still opens Satty.
|
||||||
|
- Drag left/up and release on nothing → the thumbnail comes back.
|
||||||
|
- A thumbnail whose timer fires during a drag slides out after the cancel.
|
||||||
|
- Drag onto the other monitor works.
|
||||||
|
|
||||||
|
## 8. Definition of done
|
||||||
|
|
||||||
|
`cargo clippy -- -D warnings`, `cargo fmt --check`, `cargo nextest run`,
|
||||||
|
`nix flake check` pass, the 7.3 checks are done on niri, and copland picks
|
||||||
|
it up with `nix flake update valw`.
|
||||||
Reference in new issue
Block a user