# Right-click context menu

Shipped in v0.4.0 (#23). Replaces Chromium's native context menu with a
custom-rendered `buffr-ui::ContextMenuOverlay` panel, driven by CEF's
`ContextMenuHandler`. Items are bucketed by hit-test type; only the items
relevant to the click target are shown.

## Triggering the menu

Right-click anywhere in the page area. The menu appears at the cursor position
(clamped to the viewport). Dismiss it with `<Esc>`, a click outside the panel,
or by activating an item.

**Navigation in the menu:** `<Up>` / `<Down>` move row selection. `<Enter>`
activates the selected item. Disabled rows (greyed text) cannot be activated.
Any non-navigation key dismisses the menu and is passed to the normal page-mode
dispatcher.

## Bucket priority

When multiple flags apply to a click target, the highest-priority bucket wins:

| Priority | Bucket    | Trigger condition                                               |
| -------- | --------- | --------------------------------------------------------------- |
| 1        | Editable  | `TYPEFLAG_EDITABLE` or `is_editable`                            |
| 2        | Link      | `TYPEFLAG_LINK` or a non-empty `link_url`                       |
| 3        | Image     | `TYPEFLAG_MEDIA` + `MEDIATYPE_IMAGE`                            |
| 4        | Media     | `TYPEFLAG_MEDIA` + `MEDIATYPE_VIDEO/AUDIO`                      |
| 5        | Selection | `TYPEFLAG_SELECTION` or a non-empty selection, and not Editable |
| 6        | Page      | Fallback — always shown when no bucket matches                  |

Right-clicks on the tab strip do not go through this table at all; they build a
separate model — see [Tab strip](#tab-strip) below.

## Per-bucket items

### Page (fallback)

Shown on a right-click on the page background, margin, or any element that
doesn't match a higher-priority bucket.

| Variant                            | Label            | Notes                                             |
| ---------------------------------- | ---------------- | ------------------------------------------------- |
| `HistoryBack { enabled: bool }`    | Back             | Greyed when there is no previous history entry.   |
| `HistoryForward { enabled: bool }` | Forward          | Greyed when there is no forward history entry.    |
| `Reload`                           | Reload           | Only when the page is not loading.                |
| `StopLoading`                      | Stop Loading     | Only while the page is loading (replaces Reload). |
| `ViewPageSource`                   | View Page Source | Opens `buffr-src:<url>` in a new tab. See below.  |
| `InspectElement`                   | Inspect Element  | Opens DevTools at the right-click hit-point.      |

### Link

Right-clicking a hyperlink (`<a href="...">` or any element with a URL).

| Variant                   | Label                       | Notes                                        |
| ------------------------- | --------------------------- | -------------------------------------------- |
| `OpenLinkInNewTab`        | Open Link in New Tab        | Foreground tab.                              |
| `OpenLinkInBackgroundTab` | Open Link in Background Tab | Background tab (tab strip focus unchanged).  |
| `OpenLinkInNewWindow`     | Open Link in New Window     | Currently treated as a new tab (#18).        |
| `CopyLinkAddress`         | Copy Link Address           | Writes the URL to the clipboard.             |
| `SaveLinkAs`              | Save Link As…               | Triggers a CEF download of the link URL.     |
| `InspectElement`          | Inspect Element             | Opens DevTools at the right-click hit-point. |

### Image

Right-clicking an `<img>` or other image-type media element.

| Variant             | Label                 | Notes                                                    |
| ------------------- | --------------------- | -------------------------------------------------------- |
| `OpenImageInNewTab` | Open Image in New Tab | Navigates to the image URL directly.                     |
| `SaveImageAs`       | Save Image As…        | Triggers a CEF download.                                 |
| `CopyImage`         | Copy Image            | Fetches off-thread, transcodes to PNG, writes clipboard. |
| `CopyImageAddress`  | Copy Image Address    | Writes the image URL as text to the clipboard.           |
| `InspectElement`    | Inspect Element       | Opens DevTools at the right-click hit-point.             |

`CopyImage` falls back to writing the image URL as text when the clipboard
backend doesn't support image MIME (e.g. OSC52 over SSH).

### Media (video / audio)

Right-clicking a `<video>` or `<audio>` element. Items whose media-state flag is
not set are omitted (e.g. `MediaSaveAs` only appears when `CAN_SAVE` is
reported; `PictureInPicture` only when `CAN_PICTURE_IN_PICTURE` is reported).

| Variant                            | Label               | Notes                                                            |
| ---------------------------------- | ------------------- | ---------------------------------------------------------------- |
| `MediaPlayPause { playing: bool }` | Pause / Play        | "Pause" when playing; "Play" when paused.                        |
| `MediaMute { muted: bool }`        | Mute / Unmute       | "Unmute" when already muted.                                     |
| `MediaLoop { looped: bool }`       | Enable/Disable Loop | Toggles `<video>.loop`.                                          |
| `MediaShowControls`                | Show Controls       | Toggling native controls; only shown when `CAN_TOGGLE_CONTROLS`. |
| `MediaSaveAs`                      | Save Media As…      | Only shown when `CAN_SAVE` flag is set.                          |
| `CopyMediaAddress`                 | Copy Media Address  | Writes the media URL as text to the clipboard.                   |
| `PictureInPicture`                 | Picture in Picture  | Only shown when `CAN_PICTURE_IN_PICTURE` is set.                 |
| `InspectElement`                   | Inspect Element     | Opens DevTools at the right-click hit-point.                     |

All media actions resolve the target element via `document.elementFromPoint`
with a `querySelector('video, audio')` fallback for sites (e.g. YouTube) where
the click target is a sibling/overlay rather than an ancestor of the media
element.

### Selection

Right-clicking a text selection that is _not_ inside an editable field.

| Variant           | Label                | Notes                                                |
| ----------------- | -------------------- | ---------------------------------------------------- |
| `CopySelection`   | Copy                 | Writes `selection_text` to clipboard (CEF fallback). |
| `SearchSelection` | Search for Selection | Opens the selection as a search query in a new tab.  |
| `InspectElement`  | Inspect Element      | Opens DevTools at the right-click hit-point.         |

### Editable

Right-clicking an editable field (`<input>`, `<textarea>`, contenteditable). All
ops are dispatched as CEF frame edit commands to the focused frame.

| Variant            | Label               | Notes                                 |
| ------------------ | ------------------- | ------------------------------------- |
| `Cut`              | Cut                 | `cef_frame_t` edit command.           |
| `Copy`             | Copy                | `cef_frame_t` edit command.           |
| `Paste`            | Paste               | `cef_frame_t` edit command.           |
| `PasteAsPlainText` | Paste as Plain Text | Strips rich formatting before insert. |
| `SelectAll`        | Select All          | `cef_frame_t` edit command.           |
| `Undo`             | Undo                | `cef_frame_t` edit command.           |
| `Redo`             | Redo                | `cef_frame_t` edit command.           |

### Tab strip

Right-clicking an entry in the tab strip builds its own model via
`build_tab_model(tab_count, tab_index, pinned)` in
`crates/buffr-core/src/context_menu.rs` — it does not consult the page hit-test
buckets above.

| Variant           | Label                   |
| ----------------- | ----------------------- |
| `TabReload`       | Reload Tab              |
| `TabDuplicate`    | Duplicate Tab           |
| `TabPin`          | Pin Tab / Unpin Tab     |
| `TabCopyUrl`      | Copy Tab URL            |
| `TabClose`        | Close Tab               |
| `TabCloseOthers`  | Close Other Tabs        |
| `TabCloseToRight` | Close Tabs to the Right |

`TabPin`'s label flips on the tab's current pinned bit. `TabCloseOthers` is
enabled only when `tab_count > 1` and `TabCloseToRight` only when
`tab_index + 1 < tab_count`; otherwise they render dimmed and cannot be
activated.

## `buffr-src:` URL prefix

`ViewPageSource` navigates to `buffr-src:<url>` — the user-facing alias for
Chromium's `view-source:` scheme. Navigations to `buffr-src:<url>` are rewritten
to `view-source:<url>` at the CEF boundary. The omnibar, tab strip, and session
file all store the `buffr-src:` prefixed form uniformly.

You can also type `buffr-src:https://example.com` directly in the omnibar to
view source without using the menu.

### What `buffr-src:` refuses to fetch

The scheme handler runs in the **browser** process, outside Chromium's network
stack, so it enforces its own allowlist before fetching:

- The underlying URL must be `http:` or `https:`. Everything else (`file:`,
  `data:`, `ftp:`, a nested `buffr-src:`, …) is refused.
- Non-public destinations — loopback, link-local `169.254/16` (including the
  cloud metadata endpoint), and RFC1918 ranges — are refused **unless** the page
  that initiated the navigation is already on that same host. That keeps "view
  source" working for buffr's own `buffr://` pages (served from `127.0.0.1`)
  while stopping a public page from pivoting into your LAN.

A rejected target still renders: you get an error page explaining which rule
fired, not a silent failure.

## Known issues

- **`PictureInPicture` no-ops on YouTube.** The item fires
  `media_picture_in_picture`, which calls the browser's PiP API via JS
  injection. YouTube's embedded player rejects PiP calls unless they are
  initiated from a trusted user gesture — CEF's JS injection does not carry
  transient user activation across the OSR boundary. Tracked in
  [issue #31](https://github.com/kryptic-sh/buffr/issues/31).
