@vestra-player/sdk 1.5.18 → 1.5.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms-full.txt CHANGED
@@ -1,4 +1,4 @@
1
- # @vestra-player/sdk 1.5.18: full reference
1
+ # @vestra-player/sdk 1.5.21: full reference
2
2
 
3
3
  > Everything needed to write a Vestra Player plugin (plugin API `vestra/1`): the guides, every public declaration of `@vestra-player/sdk` and `@vestra-player/sdk/testing` with its documentation, and the complete source of the example plugins. Generated from the SDK's sources by scripts/llms.mjs; the short index is llms.txt.
4
4
 
@@ -445,7 +445,7 @@ Key names describe keys, not characters, with two deliberate exceptions:
445
445
  - **Option combinations match the physical key**, so `Alt+O` fires for ⌥O although that key types `ø`, and dead keys (⌥E, ⌥N…) fire too. On layouts that need Option to type a symbol (German ⌥5 = ``, ⌥L = `@`), a binding for the typed symbol (`"["`, `"@"`) wins over one for the physical key (`Alt+5`).
446
446
  - **`Ctrl` is portable, `Cmd` is not.** On macOS `Ctrl+X` fires for both ⌃X and ⌘X, `Cmd+X` only for ⌘X. On Windows `Cmd` is the Windows logo key, which the system mostly keeps, so write `Ctrl+…` for a binding that works on both systems.
447
447
 
448
- Mouse bindings apply only over the video area. `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.)
448
+ Mouse bindings apply only over the video area. `Mouse:LeftDoubleClick:LeftHalf` and `:RightHalf` win over the plain double click; wheel bindings count accumulated scroll steps. The right button is reserved for the context menu, and a left drag in window mode moves the window instead of clicking. A `Mouse:LeftClick` fires on release, except when the same modifiers also have a double-click binding and the click's command is not checkable: then the player waits the system's double-click interval to tell a click from a double click. (A checkable command such as play/pause runs at once; a double click runs it twice, which restores its state, and the double-click binding still fires.) The player's defaults (player 0.5.2): a left click on the video is Play/Pause and nothing is bound to a double click (up to 0.5.1 a double click was Toggle Fullscreen); a click whose command is disabled does nothing.
449
449
 
450
450
  ##### The user's bindings
451
451
 
@@ -579,10 +579,27 @@ The player's **Previous** and **Next** (menu, keys, the bottom bar) are not tied
579
579
 
580
580
  #### Core actions
581
581
 
582
- Play/pause, stop, previous/next, frame steps, relative seeks, speed, volume and mute are **core actions**: commands the player registers itself, so they work with no plugin running. plugin.host.run(action) runs one exactly as its menu item or key would, with the same checks and the same on-screen text. A plugin's play button should call `host.run("play-pause")` rather than `setPaused`, so that it behaves like the Space key in every case (restart after the end, the "Paused" message, disabled without a source).
582
+ Play/pause, stop, previous/next, frame steps, relative seeks, speed, volume and mute are **core actions**: commands the player registers itself, so they work with no plugin running. plugin.host.run(action) runs one exactly as its menu item or key would, with the same checks and the same on-screen text. A plugin's play button should call `host.run("play-pause")` rather than `setPaused`, so that it behaves like the Space key in every case (restart after the end, the "Paused" message, disabled without a source). A fullscreen button calls `host.run("fullscreen")` (player 0.5.2), which toggles the main window like Enter; show the button's state from `window.state.fullscreen`.
583
583
 
584
584
  The difference from the `Player` methods: a `Player` control is a precise operation the plugin is responsible for (conditional, silent, its own error handling); a core action is the user's action, performed on the plugin's behalf. The action ids, menu places and default keys are in Reference: commands; how keys reach them is in Commands, menus and key bindings.
585
585
 
586
+ ##### Step seeks: exact or keyframe
587
+
588
+ ← / → (`seek-backward` / `seek-forward`) seek by the step that player.setStepSeek sets (5 s by default). By default they land on the exact time, which on a file with keyframes far apart means decoding from the previous keyframe first: a visible stall on 4K video. `setStepSeek({ seconds, keyframes: true })` (player 0.5.1, SDK 1.5.19) makes them land on a keyframe instead: forward on the first keyframe at or after position + step, backward on the last at or before position − step, and every press moves. Presses during a seek add up and continue from where it landed, and the on-screen text shows the landed time. Sources without moving video still seek exactly. The seek bar, `player.seek`, scrubs, chapters, loops and `player.step` are never affected.
589
+
590
+ For more steps of your own (Ctrl+→ and the like), call `plugin.host.run("seek-forward", { seconds: 30 })`: the same checks, mode, accumulation and text as →. In keyframe mode it resolves once the seek has landed, so `player.state.position` is current afterwards; do not show your own text for it. Set `"minPlayerVersion": "0.5.1"` when you rely on either option: older players ignore them.
591
+
592
+ ```ts
593
+ const keyframes = true; // the plugin's own setting
594
+ await plugin.player.setStepSeek({ seconds: 5, keyframes });
595
+ const seek = (delta: number) =>
596
+ plugin.host.run(delta < 0 ? "seek-backward" : "seek-forward", {
597
+ seconds: Math.abs(delta),
598
+ });
599
+ ```
600
+
601
+ In tests, `TestHost.keyframes` models the source's keyframes, and `host.coreSeeks` records each step seek with where it landed.
602
+
586
603
  ## Concept: Video and audio processing
587
604
 
588
605
  A plugin can change what the user sees and hears in three different places. They look alike in the API (each is a handle with `set`, a `status` and `onStatus`), but they act at different stages and have different owners, so choosing the right one matters:
@@ -856,7 +873,8 @@ export component ListPanel inherits Rectangle {
856
873
  ##### Names and values
857
874
 
858
875
  - Write names in camelCase or kebab-case in TypeScript: the SDK turns every capital letter into `-` plus the lower-case letter (`canMark` → `can-mark`, `strengthChanged` → `strength-changed`). Fields inside struct values are **not** converted, so spell struct fields exactly as the Slint struct does (and without `_`, which Slint treats as `-`).
859
- - Values must be JSON: booleans, numbers, strings, arrays (Slint models) and objects (Slint structs). Two exceptions: an image from ui.image, only as the **whole** value of an `image` property (nested in an array or object it is `invalid`), and a document from ui.markdown wherever the Slint type is `[MarkdownBlock]`: the whole value, or (SDK 1.5.4, the player after 0.4.0) a field at any depth, such as the `body` of each message in a chat list.
876
+ - Values must be JSON: booleans, numbers, strings, arrays (Slint models) and objects (Slint structs). Two exceptions: an image from ui.image or ui.remoteImage wherever the Slint type is `image`: the whole value, or (SDK 1.5.19, player 0.5.1) a field at any depth, such as the logo of each row of a list; and a document from ui.markdown wherever the Slint type is `[MarkdownBlock]`: the whole value, or (SDK 1.5.4, the player after 0.4.0) a field at any depth, such as the `body` of each message in a chat list.
877
+ - Images in rows: give small pictures `maxSide` (about twice the drawn size, `64` for a 32 px logo), so that the player decodes them small. The images nested in one view's values may show at most 32 MB of decoded pixels together; a write past that is refused with `limit`. A nested image whose file fails the player's checks is drawn empty, and the rest of the row is shown. Rows with images update in place, so a `ListView` keeps its scroll position when the list is written again.
860
878
  - Every name must exist in the Slint file. Writing a property the file does not declare makes a tool window's `show()` or a panel's `ready` fail, and in a settings page it keeps the Options window from opening; for a tool window, so does a callback bound with `on()` that the file does not declare. Once the view is open, such writes are reported to onError.
861
879
 
862
880
  ##### set, bind and the update pass
@@ -2157,7 +2175,8 @@ The table lists the API whose documentation names a player version, newest last:
2157
2175
  | 0.4.1 | PluginError, FileResolverOptions, PackageCandidate.install, PluginWarning.code, Packages.validate |
2158
2176
  | 0.4.2 | Resolvers.fileTypes, Resolvers.onFileTypesChange |
2159
2177
  | 0.4.3 | CommandOptions.path, PropValue, View.set, Appearance.setLanguage, TemplateArgs, Text, ConfirmOptions.args, OsdPanel.set, OsdPanelOptions.maxColumns, Osd.show |
2160
- | 0.5.1 | SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps |
2178
+ | 0.5.1 | ImageRef, ImageOptions.maxSide, PropValue, Ui.image, Ui.remoteImage, SlotState.reason, Panel.setInteraction, PlaybackStats.presentedFps, Player.setStepSeek, Host.run, Packages.reload |
2179
+ | 0.5.2 | HostAction |
2161
2180
 
2162
2181
  The Slint library carries no version tags: `Theme.danger` needs player 0.3.0; `@vestra/icons.slint` and the palette `vestra-light` need 0.4.0 (Theme).
2163
2182
 
@@ -2178,7 +2197,7 @@ A plugin lists what it may do in the manifest's `capabilities` (Manifest). A cal
2178
2197
  | `ui.window` | Windows.create | — | Open tool windows | |
2179
2198
  | `ui.mount` | Panels.mount, Panels.bottom, Panels.sidebar, Panels.backdrop, PanelSlot.onChange, Panel.onChange, BackdropSlot.onChange | — | Show panels in the bottom bar, the sidebar or behind the video | |
2180
2199
  | `ui.dialog` | Dialogs.confirm, Dialogs.openFile, Dialogs.openFiles, Dialogs.openDirectory, Dialogs.saveFile | — | Show file pickers and confirmations | |
2181
- | `plugins.manage` | Packages.validate, Packages.validateArchive, Packages.setEnabled, Packages.uninstall | — | Install, uninstall and disable other plugins | yes |
2200
+ | `plugins.manage` | Packages.validate, Packages.validateArchive, Packages.setEnabled, Packages.reload, Packages.uninstall | — | Install, uninstall and disable other plugins | yes |
2182
2201
  | `processing.video` | Plugin.video.filter, Plugin.video.view, Plugin.video.render | `playback.read` | Process the picture (filters, shaders) | |
2183
2202
  | `processing.audio` | Plugin.audio.filter | `playback.read`, `events` | Process the sound | |
2184
2203
  | `subtitles.read` | Subtitles.state, Subtitles.onChange, Subtitles.refresh | — | Read subtitles | |
@@ -2392,7 +2411,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
2392
2411
  | Copy to Clipboard ▸ File Name | | `vestra.playback/copy-name` |
2393
2412
  | Copy to Clipboard ▸ Folder Path | | `vestra.playback/copy-folder` |
2394
2413
  | Copy to Clipboard ▸ Full Path | | `vestra.playback/copy-path` |
2395
- | Playback ▸ Play/Pause | `Space` | `host/play-pause` |
2414
+ | Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` | `host/play-pause` |
2396
2415
  | Playback ▸ Previous | `PageUp`, `Mouse:X1` | `host/previous` |
2397
2416
  | Playback ▸ Next | `PageDown`, `Mouse:X2` | `host/next` |
2398
2417
  | Playback ▸ Seek ▸ Previous Frame | `D` | `host/frame-back` |
@@ -2622,7 +2641,7 @@ Every command the player (`host`) and its built-in plugins register when the pla
2622
2641
  | Emulator ▸ Open NES ROM… | | `vestra.nes/nes.open` |
2623
2642
  | Emulator ▸ Stop NES | | `vestra.nes/nes.stop` |
2624
2643
  | Emulator ▸ NES Tools… | | `vestra.nes/nes.window` |
2625
- | Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11`, `Mouse:LeftDoubleClick` | `host/fullscreen` |
2644
+ | Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11` | `host/fullscreen` |
2626
2645
  | Screen ▸ Picture-in-Picture | | `vestra.window/pip` |
2627
2646
  | Screen ▸ Mini Size | ``` | `vestra.window/mini` |
2628
2647
  | Screen ▸ 0.5× | `1` | `vestra.window/size.0.5` |
@@ -2651,7 +2670,6 @@ Every command the player (`host`) and its built-in plugins register when the pla
2651
2670
  | Settings ▸ Keyboard Shortcuts… | | `vestra.hotkeys/keys` |
2652
2671
  | Settings ▸ Mouse… | | `vestra.hotkeys/mouse` |
2653
2672
  | Settings ▸ Ignore Default Shortcuts | | `vestra.hotkeys/ignore-defaults` |
2654
- | Settings ▸ Plugin Manager… | | `vestra.plugins/manage` |
2655
2673
  | Settings ▸ Appearance ▸ Vestra Dark | | `vestra.preferences/theme.vestra` |
2656
2674
  | Settings ▸ Appearance ▸ Vestra Light | | `vestra.preferences/theme.vestra-light` |
2657
2675
  | Settings ▸ Appearance ▸ Show Logo When Idle | | `vestra.visualizer/idle-logo` |
@@ -2736,7 +2754,7 @@ The player's own playback commands, which plugin.host.run runs by id (capability
2736
2754
  | Action | Menu item | Default bindings |
2737
2755
  | --- | --- | --- |
2738
2756
  | `stop` | Close | `F4` |
2739
- | `play-pause` | Playback ▸ Play/Pause | `Space` |
2757
+ | `play-pause` | Playback ▸ Play/Pause | `Space`, `Mouse:LeftClick` |
2740
2758
  | `previous` | Playback ▸ Previous | `PageUp`, `Mouse:X1` |
2741
2759
  | `next` | Playback ▸ Next | `PageDown`, `Mouse:X2` |
2742
2760
  | `frame-back` | Playback ▸ Seek ▸ Previous Frame | `D` |
@@ -2749,6 +2767,7 @@ The player's own playback commands, which plugin.host.run runs by id (capability
2749
2767
  | `volume-up` | Audio ▸ Volume + | `ArrowUp`, `Mouse:WheelUp` |
2750
2768
  | `volume-down` | Audio ▸ Volume − | `ArrowDown`, `Mouse:WheelDown` |
2751
2769
  | `mute` | Audio ▸ Mute | `M` |
2770
+ | `fullscreen` | Toggle Fullscreen | `Enter`, `Alt+Enter`, `F11` |
2752
2771
 
2753
2772
  The player also owns **Toggle Fullscreen** (`host/fullscreen`), **Leave Fullscreen** (`host/exit-fullscreen`, `Escape`, only in fullscreen) and **Quit** (`host/quit`); their keys are in Built-in commands.
2754
2773
 
@@ -2923,17 +2942,19 @@ Icon { name: "close"; width: 20px; height: 20px; } // scaled
2923
2942
  | `add` | 10 × 10 | 1.5px |
2924
2943
  | `remove` | 10 × 10 | 1.5px |
2925
2944
  | `sort` | 10 × 10 | 1.5px |
2945
+ | `fullscreen` | 14 × 12 | 1.5px |
2946
+ | `exit-fullscreen` | 14 × 12 | 1.5px |
2926
2947
 
2927
2948
  #### Widgets
2928
2949
 
2929
2950
  `@vestra/widgets.slint`: compact controls drawn with the theme. Other controls come from `std-widgets.slint`.
2930
2951
 
2931
- **`ToolTipArea`**: Hover for 600 ms to show `text` beside the pointer. The tip closes when the pointer leaves or presses; it shows again on the next hover.
2952
+ **`ToolTipArea`**: Hover for about half a second to show `text` beside the pointer. The tip closes when the pointer leaves, or presses on content that does not take the press itself; it shows again on the next hover. It is Slint's input-transparent tooltip: every press reaches the control under the pointer, and the tip does not take the keyboard focus. (Up to SDK 1.5.20 the tip was a PopupWindow, which swallowed the first press after it showed and took the focus while it was open.)
2932
2953
 
2933
2954
  | Member | Kind | Default | Note |
2934
2955
  | --- | --- | --- | --- |
2935
2956
  | `text` | in `string` | | |
2936
- | `showing` | out `bool` | | The tip is showing (read-only). |
2957
+ | `showing` | out `bool` | | The tip is showing (read-only); it follows the tip's delay closely, not exactly. |
2937
2958
 
2938
2959
  **`Button`**: Dialog button: text (21px, or 18px when `small`), or an icon button when `text` is empty and `icon` holds Path data drawn in an icon-width × icon-height viewbox (filled, or 1.5px round-capped lines when `stroked`, as the @vestra/icons.slint set). The caller sizes icon buttons.
2939
2960
 
@@ -3537,6 +3558,20 @@ interface ImageRef {
3537
3558
  }
3538
3559
  ```
3539
3560
 
3561
+ ### ImageOptions
3562
+
3563
+ Options of Ui.image and Ui.remoteImage.
3564
+
3565
+ ```ts
3566
+ interface ImageOptions {
3567
+ /**
3568
+ * Decode the image scaled down, aspect kept, so that neither side is over this many pixels (an integer 1–4096); smaller images stay as they are. Use about twice the drawn size (64 for a 32 px logo) so that it stays sharp on high-density screens. It keeps the decoded memory small: a 1024×1024 icon is 4 MB decoded, 16 KB at `maxSide: 64`.
3569
+ * @player 0.5.1; older players refuse the reference (`invalid`).
3570
+ */
3571
+ maxSide?: number;
3572
+ }
3573
+ ```
3574
+
3540
3575
  ### MarkdownRef
3541
3576
 
3542
3577
  A Markdown document for `MarkdownView` or `MarkdownBlocks`, made by
@@ -3571,9 +3606,8 @@ interface MarkdownOptions {
3571
3606
 
3572
3607
  ### PropValue
3573
3608
 
3574
- The value of one Slint property: JSON with Markdown documents at any
3575
- depth (PropData), or an image as the whole value of an `image`
3576
- property.
3609
+ The value of one Slint property: JSON with Markdown documents and images
3610
+ at any depth (PropData).
3577
3611
 
3578
3612
  ```ts
3579
3613
  type PropValue = PropData | ImageRef;
@@ -3581,11 +3615,11 @@ type PropValue = PropData | ImageRef;
3581
3615
 
3582
3616
  ### PropData
3583
3617
 
3584
- JSON whose values may also be Markdown documents, at any depth: the part
3585
- of PropValue that nests.
3618
+ JSON whose values may also be Markdown documents and (SDK 1.5.19) images,
3619
+ at any depth: the part of PropValue that nests.
3586
3620
 
3587
3621
  ```ts
3588
- type PropData = null | boolean | number | string | MarkdownRef | Text | TextList | PropData[] | {
3622
+ type PropData = null | boolean | number | string | MarkdownRef | ImageRef | Text | TextList | PropData[] | {
3589
3623
  [key: string]: PropData;
3590
3624
  };
3591
3625
  ```
@@ -3611,10 +3645,22 @@ interface Ui {
3611
3645
  * Reference a PNG or JPEG file for an `image` property.
3612
3646
  * @capability None.
3613
3647
  * @param path - Absolute path of the image file, inside the package directory or the data directory.
3648
+ * @param options - `maxSide` (SDK 1.5.19, player 0.5.1): decode the image scaled down, aspect kept, so that neither side is over it.
3614
3649
  * @returns A new ImageRef, owned by this scope.
3615
- * @throws invalid (throws) `path` is not a non-empty string.
3650
+ * @throws invalid (throws) `path` is not a non-empty string, or `maxSide` is not an integer 1–4096.
3616
3651
  */
3617
- image(path: string): ImageRef;
3652
+ image(path: string, options?: ImageOptions): ImageRef;
3653
+ /**
3654
+ * Download an `https:` PNG or JPEG image (a thumbnail, a repository's plugin icon) and reference the local copy, as Ui.image does.
3655
+ * @capability None (the plugin process has network access).
3656
+ * @player Any (the reference is an ordinary ImageRef); nested in a value, player 0.5.1.
3657
+ * @param url - The image's `https:` address.
3658
+ * @param options - As for Ui.image.
3659
+ * @returns The reference, owned by this scope, once the file is in the cache.
3660
+ * @throws invalid (rejects) `url` is not an `https:` address, or `maxSide` is not an integer 1–4096.
3661
+ * @throws unavailable (rejects) the download failed, timed out, was too large, or is not a PNG or JPEG image; or the plugin has no data directory.
3662
+ */
3663
+ remoteImage(url: string, options?: ImageOptions): Promise<ImageRef>;
3618
3664
  /**
3619
3665
  * Prepare a Markdown document for `MarkdownView` (`@vestra/markdown.slint`).
3620
3666
  * @capability None.
@@ -5181,10 +5227,28 @@ interface Player {
5181
5227
  */
5182
5228
  step(unit: "frame" | "keyframe", direction: "backward" | "forward", options?: Conditional): Promise<void>;
5183
5229
  /**
5184
- * The core's first-tier relative seek (`seek-backward` / `seek-forward`, ← / →): 0.1–3600 seconds in tenths, default 5. The newest call of any plugin wins; it reverts when that plugin stops.
5230
+ * The core's first-tier relative seek (`seek-backward` / `seek-forward`,
5231
+ * ← / →): 0.1–3600 seconds in tenths, default 5. The newest call of any
5232
+ * plugin wins; it reverts when that plugin stops.
5233
+ *
5234
+ * `keyframes: true` (player 0.5.1, SDK 1.5.19): the step seeks
5235
+ * (`seek-backward` / `seek-forward`, including Host.run with
5236
+ * `seconds` and the picture-in-picture skips) land on a keyframe instead
5237
+ * of the exact time: forward on the first keyframe at or after position +
5238
+ * step, backward on the last at or before position − step, and they
5239
+ * always move. Presses during a seek add up and continue from where it
5240
+ * landed; the on-screen text shows the landed time. Sources without moving
5241
+ * video seek exactly. The seek bar, Player.seek, scrubs, chapters,
5242
+ * loops and Player.step are unaffected. Omitted or `false`: exact
5243
+ * seeks, as before. `seconds` and `keyframes` are written as a pair: the
5244
+ * newest call sets both.
5245
+ *
5246
+ * Older players ignore `keyframes`: set `"minPlayerVersion": "0.5.1"` in
5247
+ * the manifest when you rely on it.
5185
5248
  */
5186
5249
  setStepSeek(options: {
5187
5250
  seconds: number;
5251
+ keyframes?: boolean;
5188
5252
  }): Promise<void>;
5189
5253
  /** The chapters of the current media (empty without media or chapters), read on demand; `sourceId` tells which source they belong to. `playback.read`. */
5190
5254
  chapters(): Promise<{
@@ -5218,23 +5282,29 @@ interface Player {
5218
5282
 
5219
5283
  ### HostAction
5220
5284
 
5221
- The core's basic playback actions (menu commands owned by the player).
5285
+ The core's basic playback actions (menu commands owned by the player),
5286
+ and the main window's fullscreen toggle.
5222
5287
 
5223
5288
  | action | default bindings | behaviour |
5224
5289
  | --- | --- | --- |
5225
5290
  | `stop` | F4 | close the source |
5226
- | `play-pause` | Space | pause / resume, replay after the end |
5291
+ | `play-pause` | Space, a left click on the video (player 0.5.2) | pause / resume, replay after the end |
5227
5292
  | `previous` / `next` | PageUp, Mouse:X1 / PageDown, Mouse:X2 | ask the sequence provider |
5228
5293
  | `frame-back` / `frame-forward` | D / F | `player.step("frame", …)` |
5229
- | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds |
5294
+ | `seek-backward` / `seek-forward` | ← / → | relative seek by `setStepSeek` seconds (or `host.run`'s `seconds`), exact or to keyframes as `setStepSeek` says |
5230
5295
  | `rate-normal`, `rate-down`, `rate-up` | Z, X, C | 1×, ±0.1 within 0.25–4 |
5231
5296
  | `volume-up` / `volume-down` | ↑ / ↓, wheel | ±5 (`volume-up` stops at 100) |
5232
5297
  | `mute` | M | toggle mute |
5298
+ | `fullscreen` | Enter, Alt+Enter, F11 | toggle the main window's fullscreen: enter it when windowed, leave it when fullscreen, as the Toggle Fullscreen command does (SDK 1.5.21, player 0.5.2; `unavailable` when the player has no window) |
5233
5299
 
5234
- In the binding overlay their keys are `host/<action>`.
5300
+ In the binding overlay their keys are `host/<action>`. `fullscreen`
5301
+ changes no playback state: read the result from `window.state.fullscreen`
5302
+ (`plugin.window.onChange`), which also changes when the user toggles it.
5303
+ Players before 0.5.2 reject it with `invalid`: set
5304
+ `"minPlayerVersion": "0.5.2"` when you use it.
5235
5305
 
5236
5306
  ```ts
5237
- type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute";
5307
+ type HostAction = "play-pause" | "stop" | "previous" | "next" | "frame-back" | "frame-forward" | "seek-backward" | "seek-forward" | "rate-down" | "rate-up" | "rate-normal" | "volume-up" | "volume-down" | "mute" | "fullscreen";
5238
5308
  ```
5239
5309
 
5240
5310
  ### Host
@@ -5252,6 +5322,23 @@ interface Host {
5252
5322
  * Prefer this over reimplementing playback buttons.
5253
5323
  */
5254
5324
  run(action: HostAction): Promise<void>;
5325
+ /**
5326
+ * One step seek of `seconds` (0.1–3600 in tenths) instead of the
5327
+ * Player.setStepSeek step: the same checks, mode (exact or
5328
+ * keyframes), accumulation and on-screen text as ← / →. Use it for more
5329
+ * seek steps of your own (Ctrl+→ and the like) so that they add up with
5330
+ * the core's and land the same way.
5331
+ *
5332
+ * In keyframe mode it resolves once the seek has landed (so
5333
+ * `player.state.position` is current after the `await`); presses that add
5334
+ * up resolve together, the last with the text. In exact mode it resolves
5335
+ * once the seek is submitted, as before. `invalid` for `seconds` out of
5336
+ * range.
5337
+ * @player 0.5.1 (set `"minPlayerVersion": "0.5.1"`: older players ignore `seconds` and seek by the `setStepSeek` step)
5338
+ */
5339
+ run(action: "seek-backward" | "seek-forward", options: {
5340
+ seconds: number;
5341
+ }): Promise<void>;
5255
5342
  }
5256
5343
  ```
5257
5344
 
@@ -6092,6 +6179,23 @@ interface Packages {
6092
6179
  * Stop or start a plugin (`plugins.manage`). An installed plugin stays disabled across launches; a development load only for this run. `invalid` to disable oneself, `missing` unknown. `busy` while it is being installed. `unsupported` to disable a built-in (PluginInfo.builtin): users manage third-party plugins only (players released with SDK 1.5.18 on; older ones disabled built-ins too).
6093
6180
  */
6094
6181
  setEnabled(id: string, enabled: boolean): Promise<void>;
6182
+ /**
6183
+ * Restart a plugin (`plugins.manage`): stop it and start it again from the
6184
+ * same package with the same data directory, for example to recover one
6185
+ * that misbehaves. Built-ins included. A stopped or failed plugin is just
6186
+ * started. The disabled flag is never written, so a reload never leaves
6187
+ * a plugin disabled.
6188
+ *
6189
+ * Resolves once the plugin is stopped and its start is scheduled; the
6190
+ * player starts it as soon as the old process is gone (at most 20 s).
6191
+ * Follow it with Packages.onChange: `starting`, then `running`,
6192
+ * or `failed` with the reason.
6193
+ *
6194
+ * `invalid` for oneself or a disabled plugin (enable it instead),
6195
+ * `missing` unknown, `busy` while it is being installed or reloaded.
6196
+ * @player 0.5.1; older players reject it as an unknown request (`unsupported`).
6197
+ */
6198
+ reload(id: string): Promise<void>;
6095
6199
  /**
6096
6200
  * Remove an installed plugin (`plugins.manage`); `removeData` also deletes its data directory, otherwise reinstalling it from the same source reconnects to its data. Its user bindings stay (as orphans). `unsupported` for built-ins and development loads, `missing` unknown, `invalid` for oneself.
6097
6201
  */
@@ -6991,12 +7095,16 @@ class TestHost implements Transport {
6991
7095
  kind: string;
6992
7096
  title?: string | null;
6993
7097
  }[]): void;
6994
- /** Runtime images written to views (`plugin.ui.image`), newest last. The SDK refuses images of released scopes before anything is sent. */
7098
+ /**
7099
+ * Runtime images written to views (`plugin.ui.image`, `plugin.ui.remoteImage`), whole values and (SDK 1.5.19) images nested in rows, newest last. The SDK refuses images of released scopes before anything is sent. The test host does not read the files, so it does not check the player's per-view budget (`IMAGE_LIMITS.viewBytes`).
7100
+ */
6995
7101
  readonly images: {
6996
7102
  /** The image file's path, as given to `ui.image`. */
6997
7103
  path: string;
6998
7104
  /** The SDK's reference number for that `ImageRef`. */
6999
7105
  ref: number;
7106
+ /** Its `maxSide` option, when given. */
7107
+ maxSide?: number;
7000
7108
  }[];
7001
7109
  /**
7002
7110
  * The file an image property shows, or `null` when the value is not a runtime image. View properties hold images in their wire form; this reads the path back.
@@ -7317,6 +7425,25 @@ class TestHost implements Transport {
7317
7425
  constructor();
7318
7426
  /** The `player.setStepSeek` value in effect (default 5). */
7319
7427
  stepSeek: number;
7428
+ /** The `player.setStepSeek` `keyframes` in effect (default `false`, SDK 1.5.19). */
7429
+ stepSeekKeyframes: boolean;
7430
+ /**
7431
+ * Test data: the keyframe times (seconds, ascending) of the current source, for keyframe step seeks. `null` (the default): none modelled, and step seeks are exact even with `stepSeekKeyframes`.
7432
+ */
7433
+ keyframes: number[] | null;
7434
+ /**
7435
+ * Every step seek the core ran (`seek-backward` / `seek-forward`, from a key, the menu or `host.run`), newest last: the step, whether it went to a keyframe, and where it landed.
7436
+ */
7437
+ readonly coreSeeks: {
7438
+ /** `seek-backward` or `seek-forward`. */
7439
+ action: string;
7440
+ /** The step: `host.run`'s `seconds`, else `setStepSeek`'s. */
7441
+ seconds: number;
7442
+ /** It landed on a keyframe (`stepSeekKeyframes` and keyframes modelled). */
7443
+ keyframes: boolean;
7444
+ /** The new position. */
7445
+ landed: number;
7446
+ }[];
7320
7447
  /** Every `player.step`, newest last. */
7321
7448
  readonly steps: {
7322
7449
  /** `"frame"` or `"keyframe"`. */
@@ -7430,7 +7557,7 @@ class TestHost implements Transport {
7430
7557
  /** Every `packages.setEnabled` / `uninstall` / `install`, newest last. */
7431
7558
  readonly packageCalls: {
7432
7559
  /** Which call. */
7433
- method: "setEnabled" | "uninstall" | "install";
7560
+ method: "setEnabled" | "reload" | "uninstall" | "install";
7434
7561
  /** The target plugin id. */
7435
7562
  id: string;
7436
7563
  /** `setEnabled`'s value. */
@@ -7447,7 +7574,7 @@ class TestHost implements Transport {
7447
7574
  */
7448
7575
  readonly candidates: Map<string, PackageManifest>;
7449
7576
  /**
7450
- * Remote Markdown images (`plugin.ui.markdown`): address → a local PNG or JPEG file served for it, or `null` for a failing download. Nothing is fetched from the network in tests; unknown addresses fail.
7577
+ * Remote images (`plugin.ui.markdown`, `plugin.ui.remoteImage`): address → a local PNG or JPEG file served for it, or `null` for a failing download. Nothing is fetched from the network in tests; unknown addresses fail.
7451
7578
  */
7452
7579
  readonly remoteImages: Map<string, string | null>;
7453
7580
  /**
@@ -7876,6 +8003,25 @@ const UI_VALUE_LIMITS: {
7876
8003
  };
7877
8004
  ```
7878
8005
 
8006
+ ### IMAGE_LIMITS
8007
+
8008
+ The player's runtime image limits (`host/src/ui_image.rs`): a file's
8009
+ bytes and sides, the decoded pixels each plugin caches, and (SDK 1.5.19,
8010
+ player 0.5.1) the decoded pixels the nested images of one view may show.
8011
+
8012
+ ```ts
8013
+ const IMAGE_LIMITS: Readonly<{
8014
+ /** The largest image file the player reads, in bytes. */
8015
+ fileBytes: number;
8016
+ /** The longest side, in pixels, the player accepts for an image file. */
8017
+ side: 4096;
8018
+ /** The decoded pixel bytes the player caches for each plugin's images. */
8019
+ pluginBytes: number;
8020
+ /** The decoded pixel bytes the nested images of one view may show (player 0.5.1). */
8021
+ viewBytes: number;
8022
+ }>;
8023
+ ```
8024
+
7879
8025
  ### HostError
7880
8026
 
7881
8027
  A definite refusal from the player (or the plugin runner), with a stable
@@ -9594,7 +9740,7 @@ From top to bottom:
9594
9740
  - **The window.** A fixed `width`; `Theme.bg-panel` instead of a hard-coded colour, so the background follows the player's palette.
9595
9741
  - **The properties.** `level` is `in-out` because both the plugin and the user change it; `label` is only written by the plugin. `level-changed` reports a drag; `reset` reports the reset buttons.
9596
9742
  - **The layout.** `Theme.dialog-padding` and `Theme.font-body` give the spacing and text size of the player's dialogs.
9597
- - **`ToolTipArea`** shows its `text` after the pointer rests on its content for 600 ms.
9743
+ - **`ToolTipArea`** shows its `text` after the pointer rests on its content for about half a second (600 ms before player 0.5.2). The tip never takes a press or the keyboard focus: a click goes to the control under the pointer even while the tip shows.
9598
9744
  - **The sliders** are bound _two-way_ to `level` with `<=>`, so dragging one moves the other at once, inside Slint. `changed` passes the value to the plugin. A one-way binding (`value: root.level`) would be broken by Slint the first time the user moved the slider. `panel: true` is the panel style; without it, the bar style.
9599
9745
  - **`Button`** with `text` is a dialog button; `small: true` is the 18-pixel one.
9600
9746
  - **Icon buttons** have no `text`; `icon` is SVG-style path data drawn in an `icon-width` × `icon-height` box (14×12 by default). `stroked: true` draws lines instead of filling. `checked` draws the icon in the accent colour.
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.18: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n> are line numbers in llms-full.txt, next to this file: every page, declaration and example.
3
+ > @vestra-player/sdk 1.5.21: write plugins for Vestra Player in TypeScript (plugin API `vestra/1`). A plugin is a folder with `manifest.json` and an entry module whose default export `activate(plugin)` the player calls; it runs in its own Node.js process. Everything a plugin creates (commands, windows, panels, filters, listeners…) is a handle that is released automatically when the plugin stops. L<n> are line numbers in llms-full.txt, next to this file: every page, declaration and example.
4
4
 
5
5
  **Rules**
6
6
 
@@ -33,7 +33,7 @@ my-plugin/
33
33
  }
34
34
  ```
35
35
 
36
- Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2102–2165.
36
+ Fields: `id`, `version`, `apiVersion`, `entry`, `capabilities`, `name`, `author`, `punchline`, `description`, `tags`, `icon`, `readme`, `changelog`, `sourceUrl`, `license`, `minPlayerVersion`; unknown fields and capabilities are rejected. Rules and limits: L2120–2184.
37
37
 
38
38
  **main.ts**
39
39
 
@@ -60,7 +60,7 @@ export default async function activate(plugin: Plugin) {
60
60
  - `ui.window`: Windows.create
61
61
  - `ui.mount`: Panels.mount/bottom/sidebar/backdrop, PanelSlot/Panel/BackdropSlot.onChange
62
62
  - `ui.dialog`: Dialogs.confirm/openFile/openFiles/openDirectory/saveFile
63
- - `plugins.manage`: Packages.validate/validateArchive/setEnabled/uninstall
63
+ - `plugins.manage`: Packages.validate/validateArchive/setEnabled/reload/uninstall
64
64
  - `processing.video`: Plugin.video.filter/video.view/video.render (also `playback.read`)
65
65
  - `processing.audio`: Plugin.audio.filter (also `playback.read`, `events`)
66
66
  - `subtitles.read`: Subtitles.state/onChange/refresh
@@ -76,9 +76,9 @@ export default async function activate(plugin: Plugin) {
76
76
  - `ui.settings`: Settings.page/show
77
77
  - `ui.theme`: Appearance.set/setLanguage
78
78
  - `secrets`: Secrets.get/set/delete
79
- - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2166–2208.
79
+ - No capability: `tools`, `storage`, `system`, `app`, `ui` (images and Markdown), `i18n`, reading window.state, appearance.theme and packages.list. Details: L2185–2227.
80
80
 
81
- **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1588–1753; TestHost: L6751.
81
+ **Test**: `tests/*.test.ts` with `node:test`, run by `vestra-plugin test`. `startPlugin(activate, { directory: new URL("..", import.meta.url) })` from `@vestra-player/sdk/testing` returns `{ host, stop }`; `host` is a TestHost (an in-memory player): `host.setSource(…)`, `host.command(id).run()`, `host.press(key)`, `await host.settle()`, then assert on `host.osd`, `host.window(id).props`… Guide: L1606–1771; TestHost: L6855.
82
82
 
83
83
  **Command line**
84
84
 
@@ -106,39 +106,39 @@ export default async function activate(plugin: Plugin) {
106
106
  - `limit`: A player-wide limit was reached, a property value is over the limits of PropValue
107
107
  - `reserved`: The id is `vestra` or starts with `vestra.`
108
108
  - `versionMismatch`: The package needs a newer player or another `apiVersion`
109
- - Others: PluginError, llms-full.txt L3105.
109
+ - Others: PluginError, llms-full.txt L3126.
110
110
 
111
111
  ## Reference
112
112
 
113
113
  - [llms-full.txt](https://vestrasys.github.io/vestra-player-sdk/llms-full.txt), also node_modules/@vestra-player/sdk/llms-full.txt; the pages are on the [site](https://vestrasys.github.io/vestra-player-sdk/).
114
114
  - Tutorials: Getting started L9–214
115
- - Concepts: How a plugin runs L215–364; Commands, menus and key bindings L365–462; Playback state, controls and sources L463–585; Video and audio processing L586–663; Windows, panels, settings pages, dialogs and the OSD L664–950
116
- - How-to: Keep a slider responsive while dragging L951–1125; Remember a window's state between runs L1126–1255; Translate a plugin L1256–1439; Migrate a plugin to English source text L1440–1587; Test a plugin L1588–1753; Publish a plugin L1754–1842; Migrating off package rollback (1.5.16, removed in 1.5.17) L1843–1918
117
- - Guidelines: Interface guidelines L1919–2003; Menu guidelines L2004–2101
118
- - Reference: Manifest L2102–2165; Capabilities L2166–2208; Limits L2209–2303; Commands, menus and bindings L2304–2772; Platforms L2773–2801; Theme L2802–3006; Command line L3007–3062
119
- - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3063–6586
120
- - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L6587–7909
115
+ - Concepts: How a plugin runs L215–364; Commands, menus and key bindings L365–462; Playback state, controls and sources L463–602; Video and audio processing L603–680; Windows, panels, settings pages, dialogs and the OSD L681–968
116
+ - How-to: Keep a slider responsive while dragging L969–1143; Remember a window's state between runs L1144–1273; Translate a plugin L1274–1457; Migrate a plugin to English source text L1458–1605; Test a plugin L1606–1771; Publish a plugin L1772–1860; Migrating off package rollback (1.5.16, removed in 1.5.17) L1861–1936
117
+ - Guidelines: Interface guidelines L1937–2021; Menu guidelines L2022–2119
118
+ - Reference: Manifest L2120–2184; Capabilities L2185–2227; Limits L2228–2322; Commands, menus and bindings L2323–2791; Platforms L2792–2820; Theme L2821–3027; Command line L3028–3083
119
+ - [API: @vestra-player/sdk](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk.html): L3084–6690
120
+ - [API: @vestra-player/sdk/testing](https://vestrasys.github.io/vestra-player-sdk/modules/_vestra-player_sdk_testing.html): L6691–8055
121
121
 
122
122
  ## API
123
123
 
124
124
  Where each module of `plugin` (`plugin.<module>`) is declared:
125
125
 
126
- - Plugin L3297, Scope L3166, PluginError L3105, Disposable L3078
127
- - Commands L3509; Windows L3714; Panels L4357; Dialogs L4643; Player L5091; video: VideoFilter L5356, VideoView L5419, RenderOptions L5461; audio: AudioFilter L5380; Subtitles L5559; media: MediaCapture L5719; Resolvers L5824; Packages L6066; Osd L5292; Host L5240; Clipboard L6181; Power L6201; Tools L6234; Storage L6277; System L6300; Secrets L6318; window: MainWindow L4480; App L4561; Bindings L6151; Ui L3601; Timeline L4437; Drops L4013; Appearance L4042; I18n L4184; Settings L4290.
126
+ - Plugin L3318, Scope L3187, PluginError L3126, Disposable L3099
127
+ - Commands L3530; Windows L3760; Panels L4403; Dialogs L4689; Player L5137; video: VideoFilter L5443, VideoView L5506, RenderOptions L5548; audio: AudioFilter L5467; Subtitles L5646; media: MediaCapture L5806; Resolvers L5911; Packages L6153; Osd L5379; Host L5310; Clipboard L6285; Power L6305; Tools L6338; Storage L6381; System L6404; Secrets L6422; window: MainWindow L4526; App L4607; Bindings L6255; Ui L3635; Timeline L4483; Drops L4059; Appearance L4088; I18n L4230; Settings L4336.
128
128
 
129
129
  ## Examples
130
130
 
131
131
  Tested plugins in [examples/](https://github.com/vestrasys/vestra-player-sdk/tree/main/examples):
132
132
 
133
- - hello: The README Quick start: one menu command and an on-screen message. L8333–8422
134
- - blur: A tool window with one slider that blurs the playing video. L8423–8684
135
- - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L8685–9017
136
- - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9018–9202
137
- - panel-menu: A sidebar panel with its own right-click menu. L9203–9408
138
- - menus: Radio menu items, separator groups and a menu item whose title changes. L9409–9568
139
- - theme-widgets: A tool window drawn with the player's theme and widgets. L9569–9732
140
- - markdown: A window that shows a Markdown document. L9733–9927
141
- - i18n: A plugin in the player's language: English source text, a Chinese translation. L9928–10095
133
+ - hello: The README Quick start: one menu command and an on-screen message. L8479–8568
134
+ - blur: A tool window with one slider that blurs the playing video. L8569–8830
135
+ - signal-generator: Generates its own video and audio: colour bars and a 1 kHz tone. L8831–9163
136
+ - bookmark: A tool window bound to the playback state: remember the position and jump back to it. L9164–9348
137
+ - panel-menu: A sidebar panel with its own right-click menu. L9349–9554
138
+ - menus: Radio menu items, separator groups and a menu item whose title changes. L9555–9714
139
+ - theme-widgets: A tool window drawn with the player's theme and widgets. L9715–9878
140
+ - markdown: A window that shows a Markdown document. L9879–10073
141
+ - i18n: A plugin in the player's language: English source text, a Chinese translation. L10074–10241
142
142
 
143
143
  ## Optional
144
144
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.18",
3
+ "version": "1.5.21",
4
4
  "description": "Plugin API types, test host and plugin CLI for Vestra Player (apiVersion vestra/1)",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -45,6 +45,9 @@ export global Icons {
45
45
  if name == "add" { return "M 5 1.5 V 8.5 M 1.5 5 H 8.5"; }
46
46
  if name == "remove" { return "M 1.5 5 H 8.5"; }
47
47
  if name == "sort" { return "M 3 1.5 V 8.5 M 1.2 6.7 L 3 8.5 L 4.8 6.7 M 7 8.5 V 1.5 M 5.2 3.3 L 7 1.5 L 8.8 3.3"; }
48
+ // Four corners pointing out (enter) or in (leave), the default 14 × 12 box.
49
+ if name == "fullscreen" { return "M 1.5 4.5 V 1.5 H 4.5 M 9.5 1.5 H 12.5 V 4.5 M 12.5 7.5 V 10.5 H 9.5 M 4.5 10.5 H 1.5 V 7.5"; }
50
+ if name == "exit-fullscreen" { return "M 4.5 1.5 V 4.5 H 1.5 M 9.5 1.5 V 4.5 H 12.5 M 12.5 7.5 H 9.5 V 10.5 M 1.5 7.5 H 4.5 V 10.5"; }
48
51
  return "";
49
52
  }
50
53
  // Line width.
@@ -5,15 +5,20 @@
5
5
  import { TextEdit } from "std-widgets.slint";
6
6
  import { Theme } from "theme.slint";
7
7
 
8
- // Hover for 600 ms to show `text` beside the pointer. The tip closes when
9
- // the pointer leaves or presses; it shows again on the next hover.
8
+ // Hover for about half a second to show `text` beside the pointer. The tip
9
+ // closes when the pointer leaves, or presses on content that does not take
10
+ // the press itself; it shows again on the next hover. It is Slint's
11
+ // input-transparent tooltip: every press reaches the control under the
12
+ // pointer, and the tip does not take the keyboard focus. (Up to SDK 1.5.20 the tip was a
13
+ // PopupWindow, which swallowed the first press after it showed and took the
14
+ // focus while it was open.)
10
15
  export component ToolTipArea {
11
16
  in property <string> text;
12
- // The tip is showing (read-only).
17
+ // The tip is showing (read-only); it follows the tip's delay closely,
18
+ // not exactly.
13
19
  out property <bool> showing;
14
- // Shown (or pressed) during this hover: wait for the pointer to leave.
15
- // (Never assign the timer's `running`: that drops its binding, and the
16
- // tip would show only once.)
20
+ // Pressed during this hover: no tip until the pointer leaves.
21
+ // (Never assign the timer's `running`: that drops its binding.)
17
22
  private property <bool> done;
18
23
  area := TouchArea {
19
24
  width: 100%; height: 100%;
@@ -22,40 +27,24 @@ export component ToolTipArea {
22
27
  if !self.has-hover {
23
28
  root.done = false;
24
29
  root.showing = false;
25
- tip.close();
26
30
  }
27
31
  }
28
32
  pointer-event(event) => {
29
33
  if event.kind == PointerEventKind.down {
30
34
  root.done = true;
31
35
  root.showing = false;
32
- tip.close();
33
36
  }
34
37
  }
35
- }
36
- Timer {
37
- interval: 600ms;
38
- running: area.has-hover && root.text != "" && !root.done;
39
- triggered => {
40
- root.done = true;
41
- root.showing = true;
42
- tip.show();
38
+ // Removing it closes an open tip.
39
+ if root.text != "" && !root.done: Tooltip {
40
+ // Interpolated text is plain: `*` or `_` in it are not markup.
41
+ text: @markdown("\{root.text}");
43
42
  }
44
43
  }
45
- tip := PopupWindow {
46
- x: area.mouse-x + 12px;
47
- y: area.mouse-y + 16px;
48
- close-policy: close-on-click-outside;
49
- Rectangle {
50
- background: Theme.bg-panel;
51
- border-color: Theme.line;
52
- border-width: 1px;
53
- border-radius: Theme.radius-control;
54
- HorizontalLayout {
55
- padding-left: 5px; padding-right: 5px; padding-top: 2px; padding-bottom: 2px;
56
- Text { text: root.text; color: Theme.text; font-size: Theme.font-body; }
57
- }
58
- }
44
+ Timer {
45
+ interval: 500ms;
46
+ running: area.has-hover && root.text != "" && !root.done && !root.showing;
47
+ triggered => { root.showing = true; }
59
48
  }
60
49
  }
61
50