@vestra-player/sdk 1.5.57 → 1.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.0
4
+
5
+ **Who must change: nobody.** No change to the TypeScript API, the wire or `TestHost`, `apiVersion` stays `vestra/1`, no new capability. New since 1.5.58 for plugin authors: Markdown the user can select and copy (`selectable` on `MarkdownView` and `MarkdownBlocks`, `plain` and `links` on `MarkdownBlock`, in the SDK's copy of the player's `@vestra/markdown.slint`; a plugin that uses them sets `"minPlayerVersion": "0.6.8"`), and, from player 0.6.7, plugins started on demand by default: a plugin starts on its first use and is stopped when idle, so `activate` can run many times and must never wait for its own commands or views (see 1.5.58). This is a minor release because it adds backward-compatible API and marks that milestone; nothing is removed or changed, and a plugin built with 1.5.x keeps working as it is.
6
+
7
+ - **Selectable Markdown** (player 0.6.8; the SDK's `slint/vestra/markdown.slint` is synced with the player's). `MarkdownView` and `MarkdownBlocks` take `in property <bool> selectable: false`. With `true`, a paragraph, heading, list item or quote without links is shown as its plain text in a read-only selectable text field (right-click Copy / Select All, where Copy with nothing selected copies the whole block; Ctrl+C / Ctrl+A; each field is a Tab stop); a block with links keeps its clickable links, and its right-click menu copies it as plain text with each address after its link text, `text (url)`, plus Copy Link when it has one link. Code blocks are always selectable, whatever `selectable` says (in player 0.6.8 this applies to every plugin's Markdown). A selection stays inside one block and is lost when its row scrolls out of a `MarkdownView`. `MarkdownBlock` gains `plain: string` (the block as plain text) and `links: [string]` (its link addresses). Older players do not have the property: set `"minPlayerVersion": "0.6.8"` before using it. See [Reference: theme › Markdown](docs/reference/theme.md#markdown).
8
+ - **Start retries corrected** ([How the player runs and talks to plugins](docs/explanation/host-and-node.md#the-start-deadline), [Reference: limits](docs/reference/limits.md#plugin-processes), [How a plugin runs](docs/explanation/plugins.md#restarts-activate-can-run-more-than-once)). 1.5.58 said a start that is not ready within the start timeout is tried up to three more times. That was player 0.6.6; from player 0.6.7 a timed-out start gets one try (60 s) and is not retried, and only a process that cannot be created is tried again (about 250 ms, 500 ms and 1 s apart).
9
+
10
+ ## 1.5.58
11
+
12
+ **Who must change: nobody.** Documentation only: no change to the TypeScript API, the wire or `TestHost`, `apiVersion` stays `vestra/1`, no new capability, no player version needed. Plugin authors should read what the player's new plugin supervisor does with their plugin (player 0.6.6, and the defaults planned for 0.6.7), and check that `activate` never waits for its own commands or views.
13
+
14
+ - **The plugin supervisor, as a plugin sees it** ([How the player runs and talks to plugins](docs/explanation/host-and-node.md), [How a plugin runs](docs/explanation/plugins.md)). Player 0.6.6: a failed start is tried up to three more times; a built-in that crashes starts again by itself after 3 s, 6 s…; the fifth crash within 10 minutes stops a plugin until it is reloaded; an own process's JavaScript heap is capped at 512 MB (V8's heap only, not `Buffer` contents or addons), and passing it ends the process with "<name> ran out of memory"; a process that ended by itself gets no `runtime.stop`; without on-demand start a third-party plugin is listed as failed on its first crash, as before; at quit every plugin is stopped at once with 1 s in all; and two processes of one plugin never use its data directory at the same time. With on-demand start (lazy activation, `VESTRA_LAZY=1` in 0.6.6): a crashed plugin keeps its entries, shown disabled with the reason during a pause of 1 s, 2 s, 4 s…, and starts again on its next use (a resident plugin by itself); third-party plugins start a few at a time, clicks first; every start holds its clicks, resolutions, drops and view callbacks until `runtime.ready` (so an `activate` that awaits its own command or view callback waits until the start timeout); at most 64 are held per plugin, and one that cannot be taken is answered ("<name> is busy", "… could not start", "… is disabled") instead of dropped; busy work (a `busy` command, a dialog, a settings page shown, a resolution) keeps a plugin running with no time limit, and its idle time counts from when the work ends. Planned for player 0.6.7: on-demand start for everyone; a process budget for the machine's memory and the system's memory pressure put idle plugins in standby early, with the stop reason `idle`; idle time 30 s, start timeout 60 s, 5 s grace for a disable, uninstall, reload, upgrade or standby, a 5-minute cap on a player request awaiting the plugin's reply (a resolution; declared busy work stays uncapped), and a resident plugin never goes to standby. The source excerpts of host code that no longer exists (the lazy-activation trigger list, the 15-second activation record, the 250 ms idle poll, the stop's 5 ms poll) are replaced by descriptions of the current behaviour.
15
+ - **[Reference: limits › Plugin processes](docs/reference/limits.md#plugin-processes)**: heap cap, start timeout, retries, start limit, restart pauses, stop graces, quit, idle time, request timeout, start slots (`min(4, max(2, cores / 2))` plus a reserve for clicks), held triggers (64), waiting starts (256), the process budget (`clamp(physical MiB / 2 × 0.30 / 85, 3, 16)`) and the memory pressure thresholds (both planned for 0.6.7), for player 0.6.6 and as planned for 0.6.7.
16
+ - **`StopReason`** lists when each reason comes with the supervisor: `quit` also after a lost GPU device, `disable` clears session storage, `idle` also for the process budget and memory pressure (planned for 0.6.7; never for a busy plugin), `failed` also for a start timeout (possibly while `activate` runs) and, planned for 0.6.7, an unanswered request; a crash or running out of memory is no stop at all. The stop reasons are no longer described as planned: they shipped in player 0.6.4. **`PluginInfo.process`** `"idle"` also covers the pause after a crash before a plugin starts again. `Plugin`, `DisposeInfo` and [Store a plugin's data](docs/how-to/store-data.md) give the new stop graces.
17
+ - **`appearance.changed` says what changed** (player 0.6.6): `"changed": ["theme"]` or `["language"]` beside `theme` and `language`. This SDK does not read it yet; a later release uses it so that a plugin woken by a language change no longer hears its unchanged theme once.
18
+
3
19
  ## 1.5.57
4
20
 
5
21
  **Who must change: nobody.** The first npm release with 1.5.56's fix: no change to the TypeScript API or the wire, `apiVersion` stays `vestra/1`, no new capability, no player version needed.
Binary file
@@ -524,8 +524,10 @@ export interface Scope extends Disposable {
524
524
  * (menus, windows, panels, filters, subtitle layers, snapshots, jobs,
525
525
  * resolvers, streams); pending requests then fail with `disabled` and the
526
526
  * root scope is disposed for local cleanup (timers, signals, sockets). A
527
- * process that has not exited after 300 ms is killed. Media the plugin
528
- * opened with {@link Player.open} keeps playing.
527
+ * process that has not exited after its grace is killed: 300 ms in player
528
+ * 0.6.6 (2 s when it goes to standby), planned to be 5 s from player 0.6.7
529
+ * (300 ms after a fault); at quit all plugins together get 1 s. Media the
530
+ * plugin opened with {@link Player.open} keeps playing.
529
531
  *
530
532
  * @example Write a minimal plugin with one menu item
531
533
  * Tools ▸ Say hello, or Ctrl+H, shows "Hello from" and the plugin's id.
@@ -575,8 +577,8 @@ export interface Plugin extends Scope {
575
577
  */
576
578
  readonly dataDirectory: string;
577
579
  /**
578
- * Why the player stops this plugin (SDK 1.5.54; the reasons are planned
579
- * for player 0.6.4): set when the stop begins, before any
580
+ * Why the player stops this plugin (SDK 1.5.54, player 0.6.4): set when
581
+ * the stop begins, before any
580
582
  * {@link Scope.onDispose} cleanup runs. `undefined` while the plugin
581
583
  * runs, and for every stop by an older player (one whose own SDK is older
582
584
  * than 1.5.54 does not have the property at all). See {@link StopReason}.
@@ -587,23 +589,40 @@ export interface Plugin extends Scope {
587
589
  * Why the player stopped the plugin ({@link DisposeInfo.reason},
588
590
  * {@link Plugin.stopReason}).
589
591
  *
590
- * - `quit`: the player quits.
591
- * - `disable`: the user disabled the plugin.
592
+ * - `quit`: the player quits (or restarts after a lost GPU device). Every
593
+ * plugin is stopped at the same time, with 1 s in all (300 ms after a
594
+ * lost device; player 0.6.6).
595
+ * - `disable`: the user, or a plugin with `plugins.manage`, switched the
596
+ * plugin off. Its session storage is cleared.
592
597
  * - `uninstall`: the user uninstalls it.
593
598
  * - `reload`: it is reloaded (from the plugin manager, or a development
594
599
  * load with `--plugin`).
595
600
  * - `upgrade`: an install or update replaces the running version; also a
596
601
  * new version stopped because its install was cancelled (the previous
597
602
  * version comes back).
598
- * - `idle`: lazy activation parks it; its next use starts it again
603
+ * - `idle`: the plugin goes to standby and its next use starts it again;
604
+ * its menu items, keys, settings pages, resolvers and drop targets stay,
605
+ * and so does its session storage
599
606
  * ([Idle plugins](../../docs/explanation/plugins.md#idle-plugins-are-stopped)).
600
- * Lazy activation is off by default (`VESTRA_LAZY=1` turns it on); when
601
- * on, it parks a plugin after a while without use, and parks one whose
602
- * first run it judges lazy right after it is ready.
607
+ * Only with on-demand start (lazy activation), off by default up to
608
+ * player 0.6.6 (`VESTRA_LAZY=1` turns it on) and planned for everyone in
609
+ * player 0.6.7. It comes after the idle time without use (60 s; 30 s
610
+ * planned for 0.6.7), right after a first run that holds nothing live,
611
+ * and when the player's limit on third-party processes or low system
612
+ * memory puts an idle plugin in standby early (planned for 0.6.7). A busy
613
+ * plugin never gets it; a resident one (it held something live when it
614
+ * became ready) only after the idle time, and from player 0.6.7 (planned)
615
+ * never.
603
616
  * - `failed`: the player stops it for a fault it can still tell the plugin
604
- * about (planned to cover a malformed request, an unreadable or
605
- * oversized line, an activation timeout, an uncaught exception the
606
- * shared process attributes to this plugin).
617
+ * about: a malformed request, an unreadable or oversized line, a start
618
+ * that is not ready within the start timeout (15 s; 60 s planned for
619
+ * player 0.6.7; the stop may come while `activate` still runs), an
620
+ * uncaught exception the shared process attributes to this plugin, and,
621
+ * planned for player 0.6.7, a request from the player (a resolution) it
622
+ * leaves unanswered for 5 minutes. From player 0.6.6 the player may
623
+ * start it again afterwards (a built-in or resident plugin by itself, a
624
+ * plugin with on-demand start on its next use), until the fifth fault
625
+ * within 10 minutes.
607
626
  * - `host-lost`: the plugin's runner saw its pipe to the player close
608
627
  * without a stop: the player crashed or was killed. Requests are refused
609
628
  * with `disabled`; clean up locally only. Player 0.6.4 sends `quit` on a
@@ -614,20 +633,22 @@ export interface Plugin extends Scope {
614
633
  * {@link Scope.onDispose} cleanups do not run. That is the case when its
615
634
  * own process is killed because its input queue is full or a write to it
616
635
  * failed, when its own process fails to activate (the runner exits without
617
- * cleaning up), when the process has already exited, and when the shared
618
- * runner is restarted as a whole (a hang, a fault it cannot attribute, an
619
- * unreadable line of its own, a member's full input queue, the runner
620
- * exiting): every member is killed with it. Keep state that must survive
621
- * such an end in storage as it changes, not only in a cleanup.
622
- *
623
- * Planned for player 0.6.4: the player sends the reason with
624
- * `runtime.stop` and its runner reports `host-lost`; the exact list may
625
- * still change with that release. Players up to 0.6.3 give no reason for
626
- * any stop, a lost player included (`undefined`); so does a stop that
627
- * does not come from the player (a `TestHost` `stop()` without a reason).
628
- * A value this SDK does not know reads as `undefined`.
629
- *
630
- * @player 0.6.4 (planned); older players give no reason (`undefined`).
636
+ * cleaning up), when the process ended by itself (it crashed, or its
637
+ * JavaScript heap passed the 512 MB cap of player 0.6.6 and V8 ended it:
638
+ * "ran out of memory"), and when the shared runner is restarted as a whole
639
+ * (a hang, a fault it cannot attribute, an unreadable line of its own, a
640
+ * member's full input queue, the runner exiting): every member is killed
641
+ * with it. Keep state that must survive such an end in storage as it
642
+ * changes, not only in a cleanup. Timings and restarts are in
643
+ * [How the player runs and talks to plugins](../../docs/explanation/host-and-node.md#crashes-and-restarts).
644
+ *
645
+ * Player 0.6.4 and later send the reason with `runtime.stop`, and their
646
+ * runner reports `host-lost`. Players up to 0.6.3 give no reason for any
647
+ * stop, a lost player included (`undefined`); so does a stop that does not
648
+ * come from the player (a `TestHost` `stop()` without a reason). A value
649
+ * this SDK does not know reads as `undefined`.
650
+ *
651
+ * @player 0.6.4; older players give no reason (`undefined`).
631
652
  * @since 1.5.54
632
653
  * @category Plugin
633
654
  */
@@ -647,8 +668,9 @@ export interface DisposeInfo {
647
668
  */
648
669
  readonly stopping: boolean;
649
670
  /**
650
- * Why the player stops the plugin (planned for player 0.6.4); `undefined` while
651
- * `stopping` is false, and for every stop by an older player.
671
+ * Why the player stops the plugin (player 0.6.4; the causes are listed at
672
+ * {@link StopReason}); `undefined` while `stopping` is false, and for
673
+ * every stop by an older player.
652
674
  */
653
675
  readonly reason?: StopReason;
654
676
  }
@@ -1751,6 +1773,13 @@ export interface Ui {
1751
1773
  * the list on every streamed chunk keeps the scroll position and costs
1752
1774
  * little.
1753
1775
  *
1776
+ * From player 0.6.8 both views take `selectable: true` (default `false`):
1777
+ * the user can select and copy the text, one block at a time; blocks with
1778
+ * links keep their clickable links and copy as `text (url)`. Code blocks
1779
+ * are always selectable. `MarkdownBlock.plain` (the block as plain text)
1780
+ * and `MarkdownBlock.links` (its link addresses) are readable in Slint.
1781
+ * See [Reference: theme › Markdown](../../docs/reference/theme.md#markdown).
1782
+ *
1754
1783
  * @capability None.
1755
1784
  * @player 0.2.0 (set `"minPlayerVersion": "0.2.0"` in the manifest)
1756
1785
  *
@@ -8813,11 +8842,19 @@ export interface PluginInfo {
8813
8842
  display?: PluginDisplay;
8814
8843
  /**
8815
8844
  * For `state: "running"`: `"running"` while its process runs, `"idle"`
8816
- * while the player stopped it because nothing used it (lazy activation:
8817
- * it starts again on its next use, its menu items, keys, settings pages,
8818
- * resolvers and drop targets stay). Absent in other states and from
8819
- * players before 0.5.18. A change between the two alone does not fire
8820
- * {@link Packages.onChange}.
8845
+ * while the plugin is switched on but has no process. That is standby
8846
+ * with on-demand start (lazy activation): nothing used it for the idle
8847
+ * time, or the player's limit on third-party processes or low system
8848
+ * memory stopped it early (planned for player 0.6.7); it starts again on its next
8849
+ * use, and its menu items, keys, settings pages, resolvers and drop
8850
+ * targets stay. From player 0.6.6 it is also the pause after a crash: a
8851
+ * built-in or resident plugin waiting to start again by itself, or a
8852
+ * plugin with on-demand start whose commands are shown disabled with the
8853
+ * reason until the pause ends. Absent in other states and from players
8854
+ * before 0.5.18. A change between the two alone does not fire
8855
+ * {@link Packages.onChange}; a plugin stopped by the start limit (five
8856
+ * crashes within 10 minutes) is `state: "failed"` with that in
8857
+ * {@link PluginInfo.error}.
8821
8858
  *
8822
8859
  * @since 1.5.38
8823
8860
  */
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  # Vestra Player SDK
2
2
 
3
- > @vestra-player/sdk 1.5.57: 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. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
3
+ > @vestra-player/sdk 1.6.0: 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. The documentation is a tree of short pages read by path, like `go doc`: the same pages and API as https://vestrasys.github.io/vestra-player-sdk/.
4
4
 
5
5
  ## Read the documentation
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vestra-player/sdk",
3
- "version": "1.5.57",
3
+ "version": "1.6.0",
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": {
@@ -9,7 +9,15 @@
9
9
  // row (a chat message: a `[MarkdownBlock]` field of the row's struct).
10
10
  // `code-copy: true` adds a Copy button to code blocks that reports
11
11
  // `copy-code(code)`; the plugin copies it (`plugin.clipboard`).
12
- import { ListView } from "std-widgets.slint";
12
+ // `selectable: true` lets the user select and copy text with the mouse:
13
+ // paragraphs, headings, list items and quotes without links then show their
14
+ // plain text (`MarkdownBlock.plain`, no inline styles) in a read-only text
15
+ // field, one field per block, with a Copy / Select All menu. A block with
16
+ // links keeps its styled text (links stay clickable); its right-click menu
17
+ // copies the block as plain text with each address after its link text,
18
+ // `text (url)`, and offers Copy Link when the block has one link. Code
19
+ // blocks are always selectable.
20
+ import { ListView, Palette } from "std-widgets.slint";
13
21
  import { Theme } from "theme.slint";
14
22
  import { Icon } from "icons.slint";
15
23
 
@@ -36,21 +44,130 @@ export struct MarkdownBlock {
36
44
  // Task list item: 0 none, 1 open, 2 done (a drawn box, no font glyph).
37
45
  task: int,
38
46
  text: styled-text,
47
+ // The block's text without styles, what `selectable` shows: a code
48
+ // block's code, a table's cells (tab-separated lines), an image's alt.
49
+ plain: string,
39
50
  code: string,
40
51
  image: image,
41
52
  has-image: bool,
42
53
  image-width: int,
43
54
  image-height: int,
44
55
  alt: string,
56
+ // The addresses of the block's links, in order.
57
+ links: [string],
45
58
  rows: [MarkdownRow],
46
59
  // Table column alignment: "left", "center", "right" or "" (left).
47
60
  aligns: [string],
48
61
  }
49
62
 
63
+ // Read-only text the user can select: a right-click menu with Copy and
64
+ // Select All (as Slint's own text fields; Copy without a selection copies
65
+ // the whole block), and Ctrl+C / Ctrl+A (⌘C / ⌘A) while it has focus. No
66
+ // caret, as selectable text on a web page. Each one is a Tab stop, and
67
+ // while one has focus the arrow keys move in it (Slint 1.18's read-only
68
+ // TextInput).
69
+ component SelectableText {
70
+ in property <string> text;
71
+ in property <brush> color: Theme.text;
72
+ in property <length> font-size: Theme.font-tab;
73
+ in property <int> font-weight: 400;
74
+ in property <string> font-family;
75
+ min-height: input.min-height;
76
+ preferred-height: input.preferred-height;
77
+ ContextMenuArea {
78
+ Menu {
79
+ MenuItem {
80
+ title: @tr("Copy");
81
+ enabled: root.text != "";
82
+ activated => {
83
+ if input.anchor-position-byte-offset == input.cursor-position-byte-offset {
84
+ input.select-all();
85
+ input.copy();
86
+ input.clear-selection();
87
+ } else {
88
+ input.copy();
89
+ }
90
+ }
91
+ }
92
+ MenuItem {
93
+ title: @tr("Select All");
94
+ enabled: root.text != "";
95
+ activated => { input.select-all(); }
96
+ }
97
+ }
98
+ input := TextInput {
99
+ width: root.width;
100
+ height: root.height;
101
+ text: root.text;
102
+ read-only: true;
103
+ single-line: false;
104
+ wrap: word-wrap;
105
+ color: root.color;
106
+ font-size: root.font-size;
107
+ font-weight: root.font-weight;
108
+ font-family: root.font-family;
109
+ text-cursor-width: 0px;
110
+ // The std-widgets LineEdit's selection colours, legible in both
111
+ // palettes (as the info plugin's properties).
112
+ selection-background-color: Palette.selection-background;
113
+ selection-foreground-color: Palette.selection-foreground;
114
+ }
115
+ }
116
+ }
117
+
118
+ // A block with links in a selectable view: its styled text (links stay
119
+ // clickable) under a right-click menu that copies `plain` (with the
120
+ // addresses) and, for one link, the link. The menu lies above the text: a
121
+ // left click passes through it to the links.
122
+ component LinkText {
123
+ in property <styled-text> text;
124
+ in property <string> plain;
125
+ in property <[string]> links;
126
+ in property <brush> color: Theme.text;
127
+ in property <length> font-size: Theme.font-tab;
128
+ callback link-clicked(string);
129
+ min-height: styled.min-height;
130
+ preferred-height: styled.preferred-height;
131
+ function put(text: string) {
132
+ clip.text = text;
133
+ clip.select-all();
134
+ clip.copy();
135
+ }
136
+ styled := StyledText {
137
+ width: root.width;
138
+ height: root.height;
139
+ text: root.text;
140
+ default-color: root.color;
141
+ default-font-size: root.font-size;
142
+ link-color: Theme.accent;
143
+ link-clicked(url) => { root.link-clicked(url); }
144
+ }
145
+ ContextMenuArea {
146
+ Menu {
147
+ MenuItem {
148
+ title: @tr("Copy");
149
+ activated => { root.put(root.plain); }
150
+ }
151
+ if root.links.length == 1: MenuItem {
152
+ title: @tr("Copy Link");
153
+ activated => { root.put(root.links[0]); }
154
+ }
155
+ }
156
+ }
157
+ // Puts text on the clipboard (Slint has no clipboard function).
158
+ clip := TextInput {
159
+ visible: false;
160
+ width: 0px;
161
+ height: 0px;
162
+ read-only: true;
163
+ }
164
+ }
165
+
50
166
  component MarkdownBlockView {
51
167
  in property <MarkdownBlock> block;
52
168
  in property <length> available;
53
169
  in property <bool> code-copy;
170
+ in property <bool> selectable;
54
171
  callback link-clicked(string);
55
172
  callback copy-code(string);
56
173
  private property <length> quote-indent: block.quote * 12px;
@@ -61,7 +178,21 @@ component MarkdownBlockView {
61
178
  HorizontalLayout {
62
179
  padding-left: root.quote-indent + root.list-indent;
63
180
  VerticalLayout {
64
- if block.kind == "heading": StyledText {
181
+ if block.kind == "heading" && root.selectable && block.links.length > 0: LinkText {
182
+ width: root.inner;
183
+ text: block.text;
184
+ plain: block.plain;
185
+ links: block.links;
186
+ font-size: root.heading-size;
187
+ link-clicked(url) => { root.link-clicked(url); }
188
+ }
189
+ if block.kind == "heading" && root.selectable && block.links.length == 0: SelectableText {
190
+ width: root.inner;
191
+ text: block.plain;
192
+ font-size: root.heading-size;
193
+ font-weight: 700;
194
+ }
195
+ if block.kind == "heading" && !root.selectable: StyledText {
65
196
  width: root.inner;
66
197
  text: block.text;
67
198
  default-color: Theme.text;
@@ -69,7 +200,20 @@ component MarkdownBlockView {
69
200
  link-color: Theme.accent;
70
201
  link-clicked(url) => { root.link-clicked(url); }
71
202
  }
72
- if block.kind == "paragraph": StyledText {
203
+ if block.kind == "paragraph" && root.selectable && block.links.length > 0: LinkText {
204
+ width: root.inner;
205
+ text: block.text;
206
+ plain: block.plain;
207
+ links: block.links;
208
+ color: block.quote > 0 ? Theme.text-dim : Theme.text;
209
+ link-clicked(url) => { root.link-clicked(url); }
210
+ }
211
+ if block.kind == "paragraph" && root.selectable && block.links.length == 0: SelectableText {
212
+ width: root.inner;
213
+ text: block.plain;
214
+ color: block.quote > 0 ? Theme.text-dim : Theme.text;
215
+ }
216
+ if block.kind == "paragraph" && !root.selectable: StyledText {
73
217
  width: root.inner;
74
218
  text: block.text;
75
219
  default-color: block.quote > 0 ? Theme.text-dim : Theme.text;
@@ -106,7 +250,18 @@ component MarkdownBlockView {
106
250
  }
107
251
  }
108
252
  }
109
- StyledText {
253
+ if root.selectable && block.links.length > 0: LinkText {
254
+ width: root.inner - 20px;
255
+ text: block.text;
256
+ plain: block.plain;
257
+ links: block.links;
258
+ link-clicked(url) => { root.link-clicked(url); }
259
+ }
260
+ if root.selectable && block.links.length == 0: SelectableText {
261
+ width: root.inner - 20px;
262
+ text: block.plain;
263
+ }
264
+ if !root.selectable: StyledText {
110
265
  width: root.inner - 20px;
111
266
  text: block.text;
112
267
  default-color: Theme.text;
@@ -123,12 +278,12 @@ component MarkdownBlockView {
123
278
  VerticalLayout {
124
279
  padding: 6px;
125
280
  // Plain lines in the monospace font; CJK falls back.
126
- StyledText {
281
+ // Always selectable: code is there to be copied.
282
+ SelectableText {
127
283
  width: root.inner - 12px;
128
- text: block.text;
129
- default-color: Theme.text;
130
- default-font-family: Theme.font-mono;
131
- default-font-size: Theme.font-body;
284
+ text: block.code;
285
+ font-family: Theme.font-mono;
286
+ font-size: Theme.font-body;
132
287
  }
133
288
  }
134
289
  if root.code-copy: Rectangle {
@@ -216,6 +371,8 @@ export component MarkdownBlocks inherits Rectangle {
216
371
  in property <[MarkdownBlock]> blocks;
217
372
  // A Copy button on code blocks, reporting `copy-code(code)`.
218
373
  in property <bool> code-copy: false;
374
+ // Text the user can select and copy (see the top of this file).
375
+ in property <bool> selectable: false;
219
376
  callback link-clicked(string);
220
377
  callback copy-code(string);
221
378
  min-height: column.min-height;
@@ -228,6 +385,7 @@ export component MarkdownBlocks inherits Rectangle {
228
385
  block: block;
229
386
  available: root.width;
230
387
  code-copy: root.code-copy;
388
+ selectable: root.selectable;
231
389
  link-clicked(url) => { root.link-clicked(url); }
232
390
  copy-code(code) => { root.copy-code(code); }
233
391
  }
@@ -244,6 +402,10 @@ export component MarkdownView inherits Rectangle {
244
402
  in property <length> inset: 10px;
245
403
  // A Copy button on code blocks, reporting `copy-code(code)`.
246
404
  in property <bool> code-copy: false;
405
+ // Text the user can select and copy (see the top of this file). A
406
+ // selection is lost when its block scrolls out of view (the `ListView`
407
+ // drops the row).
408
+ in property <bool> selectable: false;
247
409
  callback link-clicked(string);
248
410
  callback copy-code(string);
249
411
  list := ListView {
@@ -261,6 +423,7 @@ export component MarkdownView inherits Rectangle {
261
423
  block: block;
262
424
  available: self.width;
263
425
  code-copy: root.code-copy;
426
+ selectable: root.selectable;
264
427
  link-clicked(url) => { root.link-clicked(url); }
265
428
  copy-code(code) => { root.copy-code(code); }
266
429
  }