Add cut/copy/paste with persistent clipboard and filename ellipsis toggle

Cut/Copy/Paste:
- Separate icons in StatusBar at fixed positions (no reflow)
- Clipboard persisted to .pad/clipboard.json (survives process death)
- Cut: copies to clipboard, deletes from buffer, appends to undo chain
- Copy: copies to clipboard, keeps text in buffer
- Paste: inserts clipboard text at cursor, appends to undo chain
- Visibility: Cut/Copy when selection exists, Paste when clipboard non-empty

Filename ellipsis toggle:
- Filename on separate line at top of StatusBar
- Truncated with ellipsis if too long
- Tap ellipsis toggles between truncated and full multi-line view
- filename_expanded state field (not persisted)

StatusBar layout:
- Line 1: filename (truncated or full)
- Line 2: [Cut] [Copy] Ln X, Col Y [Paste] size [Conflict]
- Fixed icon slots: Cut (left), Copy (36DP from Cut), Paste (right), Conflict (rightmost)
- Region.H computed dynamically based on filename expansion

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
Greg Pomerantz 2026-05-08 15:02:00 -04:00
parent fdb51810f4
commit c210f9a2f1
2 changed files with 173 additions and 7 deletions

View File

@ -213,13 +213,31 @@ Used for: conflict resolution UI. The logic layer produces one `MergeHunk` eleme
```go
type StatusBar struct {
/* region, visible, id — unexported */
Left string // e.g., "Ln 10, Col 5" or filename
Filename string // e.g., "notes.txt" (truncated with ellipsis if too long)
FilenameExp bool // true = show full filename (multi-line), false = truncated
Left string // e.g., "Ln 10, Col 5"
Right string // e.g., "1024 / 50000 bytes"
ConflictIcon bool // true = show conflict warning icon (tap → merge UI)
CutCopy bool // true = show cut icon in left slot
Copy bool // true = show copy icon in left slot (next to cut)
Paste bool // true = show paste icon in right slot
ConflictIcon bool // true = show conflict warning icon (rightmost)
}
```
The status bar appears at the top of the editor page. When a sync conflict is detected for the active file, `ConflictIcon` is set to true. Tapping the icon navigates to the merge resolution page. The icon persists across process death — if a conflict file exists for the active file on restore, the icon appears.
The status bar appears at the top of the editor page. It has a **two-line layout**:
- **Line 1**: Filename (truncated with ellipsis if too long). Tapping the ellipsis toggles between truncated and full multi-line view.
- **Line 2**: Action icons (Cut, Copy, Paste) + line/col info + file size + conflict icon.
**Fixed icon slots** (24×24 DP each, fixed positions):
- **Cut icon**: leftmost action icon
- **Copy icon**: second from left (36 DP from Cut icon)
- **Paste icon**: second from right
- **Conflict icon**: rightmost
Icons appear/disappear without reflowing other elements. The StatusBar's `Region.H` is computed dynamically based on whether the filename is truncated (2 lines) or expanded (3+ lines).
When a sync conflict is detected for the active file, `ConflictIcon` is set to true. Tapping the icon navigates to the merge resolution page. The icon persists across process death — if a conflict file exists for the active file on restore, the icon appears.
### 3.10 Toast / Notification
@ -336,15 +354,24 @@ Elements that don't specify explicit colors/sizes use theme defaults. The theme
```
[]Element{
Label{Text: "notes.txt"}, // header (filename)
StatusBar{
Filename: "notes.txt", FilenameExp: false,
Left: "Ln 47, Col 12", Right: "1024 / 50000",
CutCopy: true, Copy: false, Paste: true, ConflictIcon: false,
},
SearchBar{Query: "foo", Match: 1, Total: 3}, // search (when active)
TextField{Multiline: true, VisibleLines: [...], ScrollOffset: 420, WordWrap: true},
Cursor{Line: 5, Column: 12},
StatusBar{Left: "Ln 47, Col 12", Right: "1024 / 50000"},
Cursor{Line: 5, Column: 12, Selection: &Selection{...}},
}
```
The search bar is only present when the user has activated search. When visible, it sits between the header and the text area. Typing in the search bar moves the cursor to the current match. Up/down arrows cycle through matches.
The search bar is only present when the user has activated search. When visible, it sits between the StatusBar and the text area. Typing in the search bar moves the cursor to the current match. Up/down arrows cycle through matches.
The StatusBar contains:
- **Filename** on line 1 (truncated with ellipsis if too long)
- **Action icons** (Cut, Copy, Paste) on line 2, fixed positions
- **Line/col info** between Cut/Copy and Paste icons
- **File size** between Paste and Conflict icons
### 8.3 Merge Page

View File

@ -278,6 +278,145 @@ IME events are **not** returned by `w.Event()` — they arrive asynchronously vi
- The `EditText` is transparent (`android:background="@android:color/transparent"`) and zero-size (`android:layout_width="0dp" android:layout_height="0dp"`), so it does not affect the UI.
- The `EditText` is **not** focusable by touch — it only receives focus programmatically when the Gio editor needs the keyboard.
## 5.6 Cut, Copy, and Paste
Pad implements its own clipboard (not Android's `ClipboardManager`) with **persistent storage** — the clipboard survives process death and app restart.
### 5.6.1 Clipboard Storage
The clipboard is stored as a string field in the Logic goroutine's state and persisted to disk:
- **File**: `.pad/clipboard.json`
- **Format**: `{"text": "..."}` (UTF-8 string)
- **Write trigger**: Every cut or copy operation triggers an async write to disk (debounced, 100ms).
- **Load trigger**: On app start, Logic reads `.pad/clipboard.json` and restores the clipboard.
- **Atomic write**: Written via temp file + rename in `.pad/tmp/` (same pattern as auto-save).
This ensures the clipboard is available even if the system kills the app while it is in the background.
### 5.6.2 StatusBar Layout with Cut/Copy/Paste Icons
The StatusBar (top bar) has **fixed icon slots** for cut, copy, and paste. Icons appear/disappear without reflowing other elements.
**Layout** (vertical stack, top to bottom):
```
┌──────────────────────────────────────────────────────────────┐
│ filename.txt │ ← Line 1: filename (truncated with ellipsis)
│ [Cut] [Copy] Ln 10, Col 5 [Paste] 1024 / 50000 [Conf] │ ← Line 2: action icons + info
└──────────────────────────────────────────────────────────────┘
```
**Fixed slot positions** (in Dp, from the edges):
- **Cut icon**: leftmost action icon (24×24 DP, fixed position)
- **Copy icon**: second from left (24×24 DP, fixed position, 36 DP from Cut icon)
- **Paste icon**: second from right (24×24 DP, fixed position)
- **Conflict icon**: rightmost (24×24 DP, fixed position)
- **Filename**: top line, truncated with ellipsis if too long
- **Line/col info**: between Cut/Copy and Paste icons
- **File size**: between Paste and Conflict icons
**Visibility rules**:
| Icon | Visible When |
|---|---|
| **Cut** | `selection_start != selection_end` |
| **Copy** | `selection_start != selection_end` |
| **Paste** | `clipboard != ""` |
| **Conflict** | sync conflict detected for active file |
Icons are rendered as `Button` elements with fixed sizes. The StatusBar's `Region` height is computed dynamically based on whether the filename line is visible (2 lines when filename is shown, 1 line when in merge/search mode).
### 5.6.3 Cut Operation
1. User taps the Cut icon.
2. Logic extracts text: `clipboard = buffer[selection_start:selection_end]`.
3. Logic writes clipboard to `.pad/clipboard.json` (async).
4. Logic deletes selected text from buffer (append to delete chain).
5. Logic sets `selection_start = selection_end = cursor_pos` (clears selection).
6. Logic produces a new frame (Cut/Copy icons hidden, Paste icon may appear).
### 5.6.4 Copy Operation
1. User taps the Copy icon.
2. Logic extracts text: `clipboard = buffer[selection_start:selection_end]`.
3. Logic writes clipboard to `.pad/clipboard.json` (async).
4. Selection is **not** cleared (text remains in buffer).
5. Logic produces a new frame (Cut/Copy icons still visible, Paste icon appears).
### 5.6.5 Paste Operation
1. User taps the Paste icon.
2. Logic inserts `clipboard` text at cursor position.
3. Logic appends an insert operation to the undo chain.
4. Logic advances cursor past the inserted text.
5. Logic produces a new frame (no change to Cut/Copy/Paste visibility).
### 5.6.6 Input Routing
Cut, Copy, and Paste icons are tagged with unique `op.Tag` values. When the user taps an icon:
1. Main goroutine receives `pointer.Event` (Type: `Press`) with the icon's tag.
2. Main goroutine sends `InputEvent{ElementID: "cut"}`, `InputEvent{ElementID: "copy"}`, or `InputEvent{ElementID: "paste"}` to Logic.
3. Logic processes the event and updates state.
### 5.6.7 Clipboard State Machine
The clipboard is a simple string field. It has no state machine — it is either empty or contains text. On cut/copy, the text is replaced. On paste, the text is unchanged.
**Edge cases**:
- **Paste into empty clipboard**: No-op (Paste icon is hidden when clipboard is empty).
- **Cut with no selection**: No-op (Cut icon is hidden when there is no selection).
- **Copy with no selection**: No-op (Copy icon is hidden when there is no selection).
- **Multiple cuts/copies**: Each cut/copy replaces the previous clipboard content.
- **Clipboard persistence**: If the app is killed, the clipboard is restored from `.pad/clipboard.json` on restart.
## 5.7 Filename Display with Ellipsis Toggle
The filename is displayed on a separate line at the top of the StatusBar. When the filename is too long to fit the screen width, it is truncated with an ellipsis (`...`). Tapping the ellipsis toggles between the truncated single-line view and a full multi-line view.
### 5.7.1 Truncation Behavior
- **Truncated view**: Filename is truncated to fit the screen width, with `...` at the end. Example: `very_long_filename...`
- **Full view**: Filename is displayed in full, potentially spanning multiple lines. Example: `very_long_filename_that_does_not_fit_on_a_single_line.txt`
### 5.7.2 Toggle Interaction
1. **Tap on ellipsis**: Toggling between truncated and full views.
2. **Tap elsewhere on filename**: No action (filename text is not interactive).
3. **Ellipsis region**: The ellipsis (`...`) is a separate `Button` element with its own `op.Tag`. Tapping it triggers the toggle.
### 5.7.3 Input Routing
- The ellipsis is tagged with `op.Tag` value `"filename_toggle"`.
- When the user taps the ellipsis, the Main goroutine sends `InputEvent{ElementID: "filename_toggle"}` to Logic.
- Logic toggles the `filename_expanded` state field.
- Logic produces a new frame with the updated filename display.
### 5.7.4 State Field
The Logic goroutine maintains a boolean field `filename_expanded`:
- `false` (default): truncated view with ellipsis.
- `true`: full multi-line view.
This field is **not** persisted to disk — it is reset on app restart (same as gesture state).
### 5.7.5 Layout
In the truncated view, the StatusBar has 2 lines:
```
Line 1: filename.txt...
Line 2: [Cut] [Copy] Ln 10, Col 5 [Paste] 1024 / 50000 [Conf]
```
In the full view, the StatusBar has 3+ lines (depending on filename length):
```
Line 1: very_long_filename_that_does_not_fit_on_a_single_line.txt
Line 2: [Cut] [Copy] Ln 10, Col 5 [Paste] 1024 / 50000 [Conf]
```
The `Region.H` of the StatusBar is computed dynamically based on the number of lines.
## 6. Scroll Momentum
Pad implements a momentum model in the Logic goroutine to provide smooth scrolling.