@perspective-dev/viewer 5.4.0 → 5.5.1

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.
Files changed (142) hide show
  1. package/dist/cdn/perspective-viewer.js +4 -4
  2. package/dist/cdn/perspective-viewer.js.map +2 -2
  3. package/dist/css/blueprint.css +1 -1
  4. package/dist/css/botanical.css +1 -1
  5. package/dist/css/dracula.css +1 -1
  6. package/dist/css/eggplant.css +1 -1
  7. package/dist/css/gruvbox-dark.css +1 -1
  8. package/dist/css/gruvbox.css +1 -1
  9. package/dist/css/icons.css +1 -1
  10. package/dist/css/intl/ar.css +1 -1
  11. package/dist/css/intl/de.css +1 -1
  12. package/dist/css/intl/es.css +1 -1
  13. package/dist/css/intl/fr.css +1 -1
  14. package/dist/css/intl/ja.css +1 -1
  15. package/dist/css/intl/pt.css +1 -1
  16. package/dist/css/intl/zh.css +1 -1
  17. package/dist/css/intl.css +1 -1
  18. package/dist/css/ledger.css +1 -1
  19. package/dist/css/monokai.css +1 -1
  20. package/dist/css/nord.css +1 -1
  21. package/dist/css/phosphor.css +1 -1
  22. package/dist/css/pro-dark.css +1 -1
  23. package/dist/css/pro.css +1 -1
  24. package/dist/css/solarized-dark.css +1 -1
  25. package/dist/css/solarized.css +1 -1
  26. package/dist/css/themes.css +1 -1
  27. package/dist/css/vaporwave.css +1 -1
  28. package/dist/css/velvet.css +1 -1
  29. package/dist/docs/perspective-docs.json +1 -1
  30. package/dist/esm/perspective-viewer.d.ts +2 -0
  31. package/dist/esm/perspective-viewer.inline.js +9 -9
  32. package/dist/esm/perspective-viewer.inline.js.map +3 -3
  33. package/dist/esm/perspective-viewer.js +4 -4
  34. package/dist/esm/perspective-viewer.js.map +2 -2
  35. package/dist/esm/plugin.d.ts +9 -0
  36. package/dist/esm/ts-rs/AddPanelOptions.d.ts +9 -0
  37. package/dist/esm/ts-rs/ExportMethod.d.ts +1 -1
  38. package/dist/esm/ts-rs/GetViewMode.d.ts +4 -0
  39. package/dist/esm/ts-rs/GetViewOptions.d.ts +8 -0
  40. package/dist/esm/ts-rs/RestoreOptions.d.ts +6 -0
  41. package/dist/esm/ts-rs/RestoreWorkspaceOptions.d.ts +9 -0
  42. package/dist/esm/ts-rs/WorkspaceConfigUpdate.d.ts +30 -22
  43. package/dist/wasm/perspective-viewer.d.ts +102 -47
  44. package/dist/wasm/perspective-viewer.js +233 -142
  45. package/dist/wasm/perspective-viewer.wasm +0 -0
  46. package/dist/wasm/perspective-viewer.wasm.d.ts +18 -18
  47. package/package.json +7 -7
  48. package/src/css/column-selector.css +11 -3
  49. package/src/css/column-style.css +162 -0
  50. package/src/css/config-selector.css +1 -1
  51. package/src/css/form/debug.css +9 -0
  52. package/src/css/status-bar.css +11 -0
  53. package/src/css/ui/containers/tabs.css +2 -1
  54. package/src/rust/agent/tools.rs +80 -7
  55. package/src/rust/components/column_selector/active_column.rs +9 -0
  56. package/src/rust/components/column_selector.rs +11 -0
  57. package/src/rust/components/column_settings_sidebar/style_tab/alignment_field.rs +90 -0
  58. package/src/rust/components/column_settings_sidebar/style_tab/font_field.rs +267 -0
  59. package/src/rust/components/column_settings_sidebar/style_tab/primitive_field.rs +1 -1
  60. package/src/rust/components/column_settings_sidebar/style_tab.rs +68 -9
  61. package/src/rust/components/debug_panel.rs +107 -50
  62. package/src/rust/components/export_dropdown.rs +6 -0
  63. package/src/rust/components/main_panel/msg.rs +4 -0
  64. package/src/rust/components/main_panel.rs +14 -0
  65. package/src/rust/components/mod.rs +0 -1
  66. package/src/rust/components/plugin_tab.rs +57 -3
  67. package/src/rust/components/settings_panel.rs +6 -0
  68. package/src/rust/components/status_bar.rs +12 -3
  69. package/src/rust/components/status_indicator.rs +4 -0
  70. package/src/rust/components/viewer/filters.rs +2 -2
  71. package/src/rust/components/viewer/msg.rs +4 -0
  72. package/src/rust/components/viewer/panels.rs +6 -3
  73. package/src/rust/components/viewer/render.rs +1 -0
  74. package/src/rust/components/viewer/wiring.rs +14 -2
  75. package/src/rust/components/viewer.rs +2 -2
  76. package/src/rust/config/column_config_schema.rs +312 -8
  77. package/src/rust/config/datetime_column_style.rs +0 -11
  78. package/src/rust/config/export_method.rs +24 -0
  79. package/src/rust/config/mod.rs +0 -2
  80. package/src/rust/config/options.rs +50 -0
  81. package/src/rust/config/workspace_config.rs +69 -24
  82. package/src/rust/custom_elements/viewer.rs +214 -113
  83. package/src/rust/js/plugin.rs +3 -1
  84. package/src/rust/lib.rs +7 -3
  85. package/src/rust/queries/plugin_column_styles.rs +6 -5
  86. package/src/rust/renderer/plugin_config.rs +200 -41
  87. package/src/rust/renderer/plugin_store.rs +2 -1
  88. package/src/rust/renderer/props.rs +5 -0
  89. package/src/rust/renderer.rs +9 -0
  90. package/src/rust/session/metadata.rs +5 -10
  91. package/src/rust/session/props.rs +4 -0
  92. package/src/rust/session.rs +273 -195
  93. package/src/rust/tasks/copy_export.rs +67 -0
  94. package/src/rust/tasks/create_panel.rs +16 -13
  95. package/src/rust/tasks/pipeline.rs +26 -8
  96. package/src/rust/tasks/restore_panel.rs +129 -30
  97. package/src/rust/tasks/send_column_config.rs +3 -0
  98. package/src/rust/tasks/sync_update_panels.rs +148 -68
  99. package/src/rust/tasks/table_lifecycle.rs +7 -1
  100. package/src/rust/ui/containers/tab_list.rs +1 -1
  101. package/src/rust/ui/form/intl_label.rs +4 -23
  102. package/src/rust/ui/portal.rs +1 -0
  103. package/src/rust/utils/css_vars.rs +13 -0
  104. package/src/rust/{config/datetime_column_style/color_mode.rs → utils/in_flight.rs} +47 -35
  105. package/src/rust/utils/local_fonts.rs +395 -0
  106. package/src/rust/utils/mod.rs +4 -0
  107. package/src/rust/utils/pending_effects.rs +6 -30
  108. package/src/rust/workspace.rs +12 -0
  109. package/src/svg/status_pending.svg +3 -0
  110. package/src/themes/icons.css +1 -0
  111. package/src/themes/intl/ar.css +20 -7
  112. package/src/themes/intl/de.css +20 -7
  113. package/src/themes/intl/es.css +20 -7
  114. package/src/themes/intl/fr.css +20 -7
  115. package/src/themes/intl/ja.css +20 -7
  116. package/src/themes/intl/pt.css +20 -7
  117. package/src/themes/intl/zh.css +20 -7
  118. package/src/themes/intl.css +20 -7
  119. package/src/themes/monokai.css +1 -1
  120. package/src/themes/vaporwave.css +1 -1
  121. package/src/ts/perspective-viewer.ts +2 -0
  122. package/src/ts/plugin.ts +9 -0
  123. package/src/ts/ts-rs/AddPanelOptions.ts +10 -0
  124. package/src/ts/ts-rs/ExportMethod.ts +1 -1
  125. package/src/ts/ts-rs/{DatetimeColorMode.ts → GetViewMode.ts} +4 -1
  126. package/src/ts/ts-rs/GetViewOptions.ts +7 -0
  127. package/src/ts/ts-rs/RestoreOptions.ts +7 -1
  128. package/src/ts/ts-rs/RestoreWorkspaceOptions.ts +10 -0
  129. package/src/ts/ts-rs/WorkspaceConfigUpdate.ts +31 -20
  130. package/dist/esm/ts-rs/DatetimeColorMode.d.ts +0 -1
  131. package/dist/esm/ts-rs/FormatMode.d.ts +0 -1
  132. package/dist/esm/ts-rs/StringColorMode.d.ts +0 -1
  133. package/src/rust/components/string_column_style.rs +0 -120
  134. package/src/rust/config/string_column_style.rs +0 -135
  135. package/src/ts/ts-rs/FormatMode.ts +0 -3
  136. package/src/ts/ts-rs/StringColorMode.ts +0 -3
  137. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline0.js +0 -0
  138. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline1.js +0 -0
  139. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline2.js +0 -0
  140. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline3.js +0 -0
  141. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline4.js +0 -0
  142. /package/dist/wasm/snippets/{perspective-viewer-adc6f3f79141112b → perspective-viewer-6813b568219c6763}/inline5.js +0 -0
@@ -0,0 +1,395 @@
1
+ // ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2
+ // ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃
3
+ // ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃
4
+ // ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃
5
+ // ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃
6
+ // ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7
+ // ┃ Copyright (c) 2017, the Perspective Authors. ┃
8
+ // ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9
+ // ┃ This file is part of the Perspective library, distributed under the terms ┃
10
+ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
+ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
+
13
+ //! Page-global Local Font Access state shared by every `Font`
14
+ //! control on the page: the `local-fonts` permission, the enumerated
15
+ //! family list once granted, and a measured web-safe fallback list
16
+ //! otherwise.
17
+
18
+ use std::cell::{Cell, RefCell};
19
+ use std::rc::Rc;
20
+
21
+ use perspective_js::utils::global;
22
+ use wasm_bindgen::prelude::*;
23
+ use wasm_bindgen_futures::JsFuture;
24
+ use yew::Callback;
25
+
26
+ use super::{PubSub, Subscription};
27
+
28
+ /// CSS generic families always offered first, in this order.
29
+ pub const GENERIC_FONT_FAMILIES: [&str; 5] =
30
+ ["inherit", "monospace", "sans-serif", "serif", "system-ui"];
31
+
32
+ const FALLBACK_FONT_FAMILIES: [&str; 28] = [
33
+ "Arial",
34
+ "Arial Black",
35
+ "Calibri",
36
+ "Cambria",
37
+ "Comic Sans MS",
38
+ "Consolas",
39
+ "Courier New",
40
+ "DejaVu Sans",
41
+ "DejaVu Serif",
42
+ "Georgia",
43
+ "Helvetica",
44
+ "Helvetica Neue",
45
+ "Impact",
46
+ "Liberation Sans",
47
+ "Liberation Serif",
48
+ "Lucida Console",
49
+ "Menlo",
50
+ "Monaco",
51
+ "Noto Sans",
52
+ "Noto Serif",
53
+ "Roboto",
54
+ "SF Mono",
55
+ "Segoe UI",
56
+ "Tahoma",
57
+ "Times New Roman",
58
+ "Trebuchet MS",
59
+ "Ubuntu",
60
+ "Verdana",
61
+ ];
62
+
63
+ const FONT_TEST_SAMPLE: &str = "mmmmmmmmmmlli ABCDΔ 0123";
64
+ const FONT_TEST_BASELINES: [&str; 3] = ["monospace", "sans-serif", "serif"];
65
+
66
+ /// The `local-fonts` permission as last observed.
67
+ #[derive(Clone, Copy, Debug, Eq, PartialEq)]
68
+ pub enum LocalFontsAccess {
69
+ /// No `window.queryLocalFonts` (non-Chromium, or insecure context).
70
+ Unsupported,
71
+ Prompt,
72
+ Granted,
73
+ Denied,
74
+ }
75
+
76
+ #[derive(Default)]
77
+ struct LocalFontsState {
78
+ access: Cell<Option<LocalFontsAccess>>,
79
+ probing: Cell<bool>,
80
+ families: RefCell<Option<Rc<Vec<String>>>>,
81
+ fallback: RefCell<Option<Rc<Vec<String>>>>,
82
+ on_status_change: RefCell<Option<Closure<dyn Fn()>>>,
83
+ changed: PubSub<()>,
84
+ }
85
+
86
+ thread_local! {
87
+ static STATE: LocalFontsState = LocalFontsState::default();
88
+ }
89
+
90
+ /// The permission state, or `None` before [`probe_local_fonts_access`]
91
+ /// has resolved.
92
+ pub fn local_fonts_access() -> Option<LocalFontsAccess> {
93
+ STATE.with(|s| s.access.get())
94
+ }
95
+
96
+ /// Subscribe to any change in access state or family list.
97
+ pub fn on_local_fonts_changed(cb: Callback<()>) -> Subscription {
98
+ STATE.with(|s| s.changed.add_notify_listener(&cb))
99
+ }
100
+
101
+ /// The options a `Font` control offers for `current`: the generic
102
+ /// keywords, then the enumerated families when granted (and non-empty) or
103
+ /// the measured fallback otherwise, then `current` itself if absent.
104
+ pub fn font_family_options(current: &str) -> Vec<String> {
105
+ let installed = STATE.with(|s| {
106
+ let enumerated = match s.access.get() {
107
+ Some(LocalFontsAccess::Granted) => s.families.borrow().clone(),
108
+ _ => None,
109
+ };
110
+
111
+ match enumerated {
112
+ Some(families) if !families.is_empty() => families,
113
+ _ => fallback_families(s),
114
+ }
115
+ });
116
+
117
+ compose_font_family_options(current, &installed)
118
+ }
119
+
120
+ fn compose_font_family_options(current: &str, installed: &[String]) -> Vec<String> {
121
+ let mut out: Vec<String> = GENERIC_FONT_FAMILIES
122
+ .iter()
123
+ .map(|x| (*x).to_owned())
124
+ .collect();
125
+
126
+ out.extend(installed.iter().cloned());
127
+ if !out.iter().any(|x| x == current) {
128
+ out.push(current.to_owned());
129
+ }
130
+
131
+ out
132
+ }
133
+
134
+ /// Resolve the permission state once, without ever showing a prompt.
135
+ pub fn probe_local_fonts_access() {
136
+ let already = STATE.with(|s| s.access.get().is_some() || s.probing.replace(true));
137
+ if already {
138
+ return;
139
+ }
140
+
141
+ if query_local_fonts_fn().is_none() {
142
+ set_access(LocalFontsAccess::Unsupported);
143
+ return;
144
+ }
145
+
146
+ wasm_bindgen_futures::spawn_local(async move {
147
+ match query_permission_status().await {
148
+ Some(status) => {
149
+ install_status_listener(&status);
150
+ let access = access_of_status(&status);
151
+ set_access(access);
152
+ if access == LocalFontsAccess::Granted {
153
+ load_families().await;
154
+ }
155
+ },
156
+ None => match call_query_local_fonts() {
157
+ Some(promise) => match families_from(promise).await {
158
+ Some(families) => set_granted(families),
159
+ None => set_access(LocalFontsAccess::Prompt),
160
+ },
161
+ None => set_access(LocalFontsAccess::Unsupported),
162
+ },
163
+ }
164
+ });
165
+ }
166
+
167
+ /// Ask the browser for local fonts, synchronously from a user-gesture handler
168
+ /// so `queryLocalFonts()` may prompt.
169
+ pub fn request_local_fonts_access() {
170
+ let Some(promise) = call_query_local_fonts() else {
171
+ set_access(LocalFontsAccess::Unsupported);
172
+ return;
173
+ };
174
+
175
+ wasm_bindgen_futures::spawn_local(async move {
176
+ match families_from(promise).await {
177
+ Some(families) => set_granted(families),
178
+ None => {
179
+ let access = match query_permission_status().await {
180
+ Some(status) => access_of_status(&status),
181
+ None => LocalFontsAccess::Prompt,
182
+ };
183
+
184
+ set_access(access);
185
+ },
186
+ }
187
+ });
188
+ }
189
+
190
+ fn set_access(access: LocalFontsAccess) {
191
+ STATE.with(|s| {
192
+ s.probing.set(false);
193
+ let prev = s.access.replace(Some(access));
194
+ if access != LocalFontsAccess::Granted {
195
+ s.families.borrow_mut().take();
196
+ }
197
+
198
+ if prev != Some(access) {
199
+ s.changed.emit(());
200
+ }
201
+ });
202
+ }
203
+
204
+ fn set_granted(families: Vec<String>) {
205
+ STATE.with(|s| {
206
+ s.probing.set(false);
207
+ s.access.set(Some(LocalFontsAccess::Granted));
208
+ *s.families.borrow_mut() = Some(Rc::new(families));
209
+ s.changed.emit(());
210
+ });
211
+ }
212
+
213
+ async fn load_families() {
214
+ if let Some(promise) = call_query_local_fonts()
215
+ && let Some(families) = families_from(promise).await
216
+ {
217
+ set_granted(families);
218
+ }
219
+ }
220
+
221
+ fn query_local_fonts_fn() -> Option<js_sys::Function> {
222
+ js_sys::Reflect::get(&global::window(), &JsValue::from_str("queryLocalFonts"))
223
+ .ok()?
224
+ .dyn_into::<js_sys::Function>()
225
+ .ok()
226
+ }
227
+
228
+ fn call_query_local_fonts() -> Option<js_sys::Promise> {
229
+ query_local_fonts_fn()?
230
+ .call0(&global::window())
231
+ .ok()?
232
+ .dyn_into::<js_sys::Promise>()
233
+ .ok()
234
+ }
235
+
236
+ /// The enumerated families, or `None` when the call yielded none, which
237
+ /// Chromium resolves in place of rejecting a dismissed or blocked prompt.
238
+ async fn families_from(promise: js_sys::Promise) -> Option<Vec<String>> {
239
+ let fonts = JsFuture::from(promise).await.ok()?;
240
+ let family_key = JsValue::from_str("family");
241
+ let mut families: Vec<String> = js_sys::Array::from(&fonts)
242
+ .iter()
243
+ .filter_map(|font| js_sys::Reflect::get(&font, &family_key).ok()?.as_string())
244
+ .collect();
245
+
246
+ families.sort_unstable();
247
+ families.dedup();
248
+ (!families.is_empty()).then_some(families)
249
+ }
250
+
251
+ async fn query_permission_status() -> Option<JsValue> {
252
+ let navigator: JsValue = global::window().navigator().into();
253
+ let permissions = js_sys::Reflect::get(&navigator, &JsValue::from_str("permissions")).ok()?;
254
+ let query = js_sys::Reflect::get(&permissions, &JsValue::from_str("query"))
255
+ .ok()?
256
+ .dyn_into::<js_sys::Function>()
257
+ .ok()?;
258
+
259
+ let descriptor = js_sys::Object::new();
260
+ js_sys::Reflect::set(
261
+ &descriptor,
262
+ &JsValue::from_str("name"),
263
+ &JsValue::from_str("local-fonts"),
264
+ )
265
+ .ok()?;
266
+
267
+ let promise = query
268
+ .call1(&permissions, &descriptor)
269
+ .ok()?
270
+ .dyn_into::<js_sys::Promise>()
271
+ .ok()?;
272
+
273
+ let status = JsFuture::from(promise).await.ok()?;
274
+ (!status.is_undefined() && !status.is_null()).then_some(status)
275
+ }
276
+
277
+ fn access_of_status(status: &JsValue) -> LocalFontsAccess {
278
+ let state = js_sys::Reflect::get(status, &JsValue::from_str("state"))
279
+ .ok()
280
+ .and_then(|x| x.as_string());
281
+
282
+ match state.as_deref() {
283
+ Some("granted") => LocalFontsAccess::Granted,
284
+ Some("denied") => LocalFontsAccess::Denied,
285
+ _ => LocalFontsAccess::Prompt,
286
+ }
287
+ }
288
+
289
+ fn install_status_listener(status: &JsValue) {
290
+ let target = status.clone();
291
+ let closure = Closure::<dyn Fn()>::new(move || {
292
+ let access = access_of_status(&target);
293
+ set_access(access);
294
+ if access == LocalFontsAccess::Granted {
295
+ wasm_bindgen_futures::spawn_local(load_families());
296
+ }
297
+ });
298
+
299
+ let _ = js_sys::Reflect::set(
300
+ status,
301
+ &JsValue::from_str("onchange"),
302
+ closure.as_ref().unchecked_ref(),
303
+ );
304
+
305
+ STATE.with(|s| *s.on_status_change.borrow_mut() = Some(closure));
306
+ }
307
+
308
+ fn fallback_families(state: &LocalFontsState) -> Rc<Vec<String>> {
309
+ state
310
+ .fallback
311
+ .borrow_mut()
312
+ .get_or_insert_with(|| Rc::new(detect_fallback_families()))
313
+ .clone()
314
+ }
315
+
316
+ /// The subset of [`FALLBACK_FONT_FAMILIES`] the browser can render,
317
+ /// detected by comparing rendered widths against the generic baselines.
318
+ fn detect_fallback_families() -> Vec<String> {
319
+ let Some(ctx) = canvas_context() else {
320
+ return vec![];
321
+ };
322
+
323
+ let baselines: Vec<f64> = FONT_TEST_BASELINES
324
+ .iter()
325
+ .map(|x| measure_width(&ctx, x))
326
+ .collect();
327
+
328
+ FALLBACK_FONT_FAMILIES
329
+ .iter()
330
+ .filter(|family| {
331
+ FONT_TEST_BASELINES
332
+ .iter()
333
+ .zip(&baselines)
334
+ .any(|(baseline, width)| {
335
+ measure_width(&ctx, &format!("\"{family}\", {baseline}")) != *width
336
+ })
337
+ })
338
+ .map(|x| (*x).to_owned())
339
+ .collect()
340
+ }
341
+
342
+ fn canvas_context() -> Option<web_sys::CanvasRenderingContext2d> {
343
+ global::document()
344
+ .create_element("canvas")
345
+ .ok()?
346
+ .dyn_into::<web_sys::HtmlCanvasElement>()
347
+ .ok()?
348
+ .get_context("2d")
349
+ .ok()??
350
+ .dyn_into::<web_sys::CanvasRenderingContext2d>()
351
+ .ok()
352
+ }
353
+
354
+ fn measure_width(ctx: &web_sys::CanvasRenderingContext2d, family: &str) -> f64 {
355
+ ctx.set_font(&format!("72px {family}"));
356
+ ctx.measure_text(FONT_TEST_SAMPLE)
357
+ .map(|x| x.width())
358
+ .unwrap_or_default()
359
+ }
360
+
361
+ #[cfg(test)]
362
+ mod tests {
363
+ use super::*;
364
+
365
+ fn owned(xs: &[&str]) -> Vec<String> {
366
+ xs.iter().map(|x| (*x).to_owned()).collect()
367
+ }
368
+
369
+ #[test]
370
+ fn generic_families_lead_and_current_appends_when_missing() {
371
+ let options = compose_font_family_options("Zapf Chancery", &owned(&["Arial", "Menlo"]));
372
+ assert_eq!(
373
+ options,
374
+ owned(&[
375
+ "inherit",
376
+ "monospace",
377
+ "sans-serif",
378
+ "serif",
379
+ "system-ui",
380
+ "Arial",
381
+ "Menlo",
382
+ "Zapf Chancery"
383
+ ])
384
+ );
385
+ }
386
+
387
+ #[test]
388
+ fn current_is_not_duplicated() {
389
+ let options = compose_font_family_options("Arial", &owned(&["Arial"]));
390
+ assert_eq!(options.iter().filter(|x| *x == "Arial").count(), 1);
391
+
392
+ let options = compose_font_family_options("inherit", &[]);
393
+ assert_eq!(options, owned(&GENERIC_FONT_FAMILIES));
394
+ }
395
+ }
@@ -24,6 +24,8 @@ mod custom_element;
24
24
  mod datetime;
25
25
  mod debounce;
26
26
  mod hooks;
27
+ mod in_flight;
28
+ mod local_fonts;
27
29
  mod modal_position;
28
30
  mod number_format;
29
31
  mod pending_effects;
@@ -43,6 +45,8 @@ pub use custom_element::*;
43
45
  pub use datetime::*;
44
46
  pub use debounce::*;
45
47
  pub use hooks::*;
48
+ pub use in_flight::*;
49
+ pub use local_fonts::*;
46
50
  pub use modal_position::*;
47
51
  pub use number_format::*;
48
52
  pub use pending_effects::*;
@@ -10,10 +10,7 @@
10
10
  // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11
11
  // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
12
 
13
- use std::cell::Cell;
14
- use std::rc::Rc;
15
-
16
- use super::pubsub::PubSub;
13
+ use super::in_flight::{InFlight, InFlightGuard};
17
14
 
18
15
  /// The element-scoped ledger of in-flight EFFECTS: every effectful public
19
16
  /// method (and the internal flows they schedule, e.g. the `before-resize`
@@ -22,41 +19,20 @@ use super::pubsub::PubSub;
22
19
  /// after the ledger drains, so "called before `flush`" implies "applied
23
20
  /// before `flush` resolves".
24
21
  #[derive(Clone, Default)]
25
- pub struct EffectLedger(Rc<EffectLedgerData>);
26
-
27
- #[derive(Default)]
28
- struct EffectLedgerData {
29
- count: Cell<u32>,
30
- on_drain: PubSub<()>,
31
- }
22
+ pub struct EffectLedger(InFlight);
32
23
 
33
- pub struct EffectGuard(EffectLedger);
24
+ pub type EffectGuard = InFlightGuard;
34
25
 
35
26
  impl EffectLedger {
36
27
  pub fn guard(&self) -> EffectGuard {
37
- self.0.count.set(self.0.count.get() + 1);
38
- EffectGuard(self.clone())
28
+ self.0.guard()
39
29
  }
40
30
 
41
31
  pub fn is_empty(&self) -> bool {
42
- self.0.count.get() == 0
32
+ self.0.is_empty()
43
33
  }
44
34
 
45
35
  pub async fn settle(&self) {
46
- while self.0.count.get() > 0 {
47
- if self.0.on_drain.read_next().await.is_err() {
48
- break;
49
- }
50
- }
51
- }
52
- }
53
-
54
- impl Drop for EffectGuard {
55
- fn drop(&mut self) {
56
- let data = &self.0.0;
57
- data.count.set(data.count.get() - 1);
58
- if data.count.get() == 0 {
59
- data.on_drain.emit(());
60
- }
36
+ self.0.settle().await
61
37
  }
62
38
  }
@@ -327,6 +327,11 @@ struct WorkspaceData {
327
327
  /// Fires on every [`PanelPhase`] transition.
328
328
  staged_changed: Rc<PubSub<()>>,
329
329
 
330
+ /// Fires when a layout tree is staged for a RETAINED panel set (a
331
+ /// `panels`-less `restoreWorkspace`), whose props-driven render would
332
+ /// otherwise never consume it.
333
+ layout_staged: Rc<PubSub<()>>,
334
+
330
335
  /// Panels removed from `panels` whose layout cell has not yet been
331
336
  /// reclaimed, so their engines and plugin pixels stay alive until the
332
337
  /// commit.
@@ -357,6 +362,7 @@ impl Workspace {
357
362
  pending_layout: None,
358
363
  effects: EffectLedger::default(),
359
364
  staged_changed: Rc::new(PubSub::default()),
365
+ layout_staged: Rc::new(PubSub::default()),
360
366
  closing: Vec::new(),
361
367
  emitter: LayoutEmitter::default(),
362
368
  })))
@@ -420,6 +426,12 @@ impl Workspace {
420
426
  self.0.borrow().staged_changed.clone()
421
427
  }
422
428
 
429
+ /// A handle to the `layout_staged` PubSub (see
430
+ /// [`WorkspaceData::layout_staged`]).
431
+ pub fn layout_staged(&self) -> Rc<PubSub<()>> {
432
+ self.0.borrow().layout_staged.clone()
433
+ }
434
+
423
435
  /// Promote a staged panel toward layout insertion, returning whether it
424
436
  /// was still staged — restore completion and the staging deadline RACE
425
437
  /// to promote, and only the winner proceeds. Emits `staged_changed` —
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="#042121" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
2
+ <circle cx="10" cy="10" r="4.5" stroke-dasharray="2.36 2.36"/>
3
+ </svg>
@@ -56,6 +56,7 @@ perspective-viewer-tab {
56
56
  --psp-icon--updating--mask-image: url("../svg/updating.gif");
57
57
  --psp-icon--downloading--mask-image: url("../svg/downloading.gif");
58
58
  --psp-icon--status-ok--mask-image: url(../svg/status_ok.svg);
59
+ --psp-icon--status-pending--mask-image: url(../svg/status_pending.svg);
59
60
  --psp-icon--export--mask-image: url("../svg/export-icon.svg");
60
61
  --psp-icon--free-scroll--mask-image: url("../svg/free-scroll-icon.svg");
61
62
  --psp-icon--lock--mask-image: url("../svg/lock.svg");
@@ -69,16 +69,13 @@ perspective-viewer {
69
69
  --psp-label--scroll-lock-alt-toggle--content: "تمرير محاذى";
70
70
 
71
71
  --psp-label--color--content: "اللون";
72
- --psp-label--format--content: "التنسيق";
73
72
  --psp-label--timezone--content: "المنطقة الزمنية";
74
73
  --psp-label--date-style--content: "نمط التاريخ";
75
74
  --psp-label--time-style--content: "نمط الوقت";
76
- --psp-label--number-fg-mode--content: "المقدمة";
77
- --psp-label--number-bg-mode--content: "الخلفية";
78
- --psp-label--fg-colors--content: "المقدمة";
79
- --psp-label--bg-colors--content: "الخلفية";
80
- --psp-label--datetime-color-mode--content: "اللون";
81
- --psp-label--string-color-mode--content: "اللون";
75
+ --psp-label--fg-mode--content: "المقدمة";
76
+ --psp-label--bg-mode--content: "الخلفية";
77
+ --psp-label--fg-color--content: "المقدمة";
78
+ --psp-label--bg-color--content: "الخلفية";
82
79
  --psp-label--series--content: "السلسلة";
83
80
  --psp-label--color-range--content: "نطاق الألوان";
84
81
  --psp-label--style--content: "النمط";
@@ -117,6 +114,7 @@ perspective-viewer {
117
114
  --psp-label--group-legend--content: "مفتاح الرسم";
118
115
  --psp-label--group-tooltip--content: "التلميحات";
119
116
  --psp-label--group-color--content: "اللون";
117
+ --psp-label--group-column--content: "العمود";
120
118
  --psp-label--group-format--content: "التنسيق";
121
119
  --psp-label--column-size-override--content: "عرض العمود";
122
120
 
@@ -150,6 +148,21 @@ perspective-viewer {
150
148
 
151
149
  --psp-label--edit-mode--content: "وضع التحرير";
152
150
  --psp-label--scroll-lock--content: "قفل التمرير";
151
+ --psp-label--column-menus--content: "قوائم الأعمدة";
152
+ --psp-label--group-font--content: "الخط";
153
+ --psp-label--group-rows--content: "الصفوف";
154
+ --psp-label--link--content: "رابط";
155
+ --psp-label--font-family--content: "عائلة الخط";
156
+ --psp-label--font-size--content: "حجم الخط";
157
+ --psp-label--request-local-fonts--content: "الخطوط المحلية";
158
+ --psp-label--local-fonts-blocked--content: "الخطوط المحلية محظورة";
159
+ --psp-label--request-local-fonts-title--content: "السماح بالوصول إلى الخطوط المثبتة على هذا الجهاز";
160
+ --psp-label--local-fonts-blocked-title--content: "الوصول إلى الخطوط المحلية محظور لهذا الموقع. اسمح به في إعدادات الموقع بالمتصفح لعرضها هنا.";
161
+ --psp-label--word-wrap--content: "التفاف النص";
162
+ --psp-label--align--content: "المحاذاة";
163
+ --psp-label--row-height--content: "ارتفاع الصف";
164
+ --psp-label--zebra-rows--content: "صفوف متناوبة";
165
+ --psp-label--zebra-color--content: "لون التناوب";
153
166
  --psp-label--chart-type--content: "نوع الشكل";
154
167
 
155
168
  --psp-label--auto-alt-y-axis--content: "محور Y تلقائي";
@@ -74,16 +74,13 @@ perspective-viewer {
74
74
 
75
75
  /* Column Settings */
76
76
  --psp-label--color--content: "Farbe";
77
- --psp-label--format--content: "Format";
78
77
  --psp-label--timezone--content: "Zeitzone";
79
78
  --psp-label--date-style--content: "Datumsstil";
80
79
  --psp-label--time-style--content: "Zeitstil";
81
- --psp-label--number-fg-mode--content: "Vordergrund";
82
- --psp-label--number-bg-mode--content: "Hintergrund";
83
- --psp-label--fg-colors--content: "Vordergrund";
84
- --psp-label--bg-colors--content: "Hintergrund";
85
- --psp-label--datetime-color-mode--content: "Farbe";
86
- --psp-label--string-color-mode--content: "Farbe";
80
+ --psp-label--fg-mode--content: "Vordergrund";
81
+ --psp-label--bg-mode--content: "Hintergrund";
82
+ --psp-label--fg-color--content: "Vordergrund";
83
+ --psp-label--bg-color--content: "Hintergrund";
87
84
  --psp-label--series--content: "Serie";
88
85
  --psp-label--color-range--content: "Farbspektrum";
89
86
  --psp-label--style--content: "Stil";
@@ -148,6 +145,21 @@ perspective-viewer {
148
145
  /* Plugin settings */
149
146
  --psp-label--edit-mode--content: "Bearbeitungsmodus";
150
147
  --psp-label--scroll-lock--content: "Bildlaufsperre";
148
+ --psp-label--column-menus--content: "Spaltenmenüs";
149
+ --psp-label--group-font--content: "Schrift";
150
+ --psp-label--group-rows--content: "Zeilen";
151
+ --psp-label--link--content: "Link";
152
+ --psp-label--font-family--content: "Schriftfamilie";
153
+ --psp-label--font-size--content: "Schriftgröße";
154
+ --psp-label--request-local-fonts--content: "Lokale Schriften";
155
+ --psp-label--local-fonts-blocked--content: "Lokale Schriften blockiert";
156
+ --psp-label--request-local-fonts-title--content: "Zugriff auf die auf diesem Gerät installierten Schriften erlauben";
157
+ --psp-label--local-fonts-blocked-title--content: "Der Zugriff auf lokale Schriften ist für diese Website blockiert. Erlauben Sie ihn in den Website-Einstellungen des Browsers, um sie hier aufzulisten.";
158
+ --psp-label--word-wrap--content: "Zeilenumbruch";
159
+ --psp-label--align--content: "Ausrichtung";
160
+ --psp-label--row-height--content: "Zeilenhöhe";
161
+ --psp-label--zebra-rows--content: "Zebra-Zeilen";
162
+ --psp-label--zebra-color--content: "Zebra-Farbe";
151
163
  --psp-label--chart-type--content: "Glyphentyp";
152
164
 
153
165
  /* Plugin settings — viewer-charts */
@@ -189,6 +201,7 @@ perspective-viewer {
189
201
  --psp-label--group-legend--content: "Legende";
190
202
  --psp-label--group-tooltip--content: "Tooltips";
191
203
  --psp-label--group-color--content: "Farbe";
204
+ --psp-label--group-column--content: "Spalte";
192
205
  --psp-label--group-format--content: "Format";
193
206
  --psp-label--column-size-override--content: "Spaltenbreite";
194
207
  }
@@ -74,16 +74,13 @@ perspective-viewer {
74
74
 
75
75
  /* Column Settings */
76
76
  --psp-label--color--content: "Color";
77
- --psp-label--format--content: "Formato";
78
77
  --psp-label--timezone--content: "Zona horaria";
79
78
  --psp-label--date-style--content: "Estilo de fecha";
80
79
  --psp-label--time-style--content: "Estilo de hora";
81
- --psp-label--number-fg-mode--content: "Primer plano";
82
- --psp-label--number-bg-mode--content: "Fondo";
83
- --psp-label--fg-colors--content: "Primer plano";
84
- --psp-label--bg-colors--content: "Fondo";
85
- --psp-label--datetime-color-mode--content: "Color";
86
- --psp-label--string-color-mode--content: "Color";
80
+ --psp-label--fg-mode--content: "Primer plano";
81
+ --psp-label--bg-mode--content: "Fondo";
82
+ --psp-label--fg-color--content: "Primer plano";
83
+ --psp-label--bg-color--content: "Fondo";
87
84
  --psp-label--series--content: "Serie";
88
85
  --psp-label--color-range--content: "Gama de colores";
89
86
  --psp-label--style--content: "Estilo";
@@ -148,6 +145,21 @@ perspective-viewer {
148
145
  /* Plugin settings */
149
146
  --psp-label--edit-mode--content: "Modo de edición";
150
147
  --psp-label--scroll-lock--content: "Bloqueo de desplazamiento";
148
+ --psp-label--column-menus--content: "Menús de columna";
149
+ --psp-label--group-font--content: "Fuente";
150
+ --psp-label--group-rows--content: "Filas";
151
+ --psp-label--link--content: "Enlace";
152
+ --psp-label--font-family--content: "Familia de fuente";
153
+ --psp-label--font-size--content: "Tamaño de fuente";
154
+ --psp-label--request-local-fonts--content: "Fuentes locales";
155
+ --psp-label--local-fonts-blocked--content: "Fuentes locales bloqueadas";
156
+ --psp-label--request-local-fonts-title--content: "Permitir el acceso a las fuentes instaladas en este dispositivo";
157
+ --psp-label--local-fonts-blocked-title--content: "El acceso a las fuentes locales está bloqueado para este sitio. Permítalo en la configuración del sitio del navegador para mostrarlas aquí.";
158
+ --psp-label--word-wrap--content: "Ajuste de línea";
159
+ --psp-label--align--content: "Alineación";
160
+ --psp-label--row-height--content: "Altura de fila";
161
+ --psp-label--zebra-rows--content: "Filas cebra";
162
+ --psp-label--zebra-color--content: "Color cebra";
151
163
  --psp-label--chart-type--content: "Tipo de glifo";
152
164
 
153
165
  /* Plugin settings — viewer-charts */
@@ -189,6 +201,7 @@ perspective-viewer {
189
201
  --psp-label--group-legend--content: "Leyenda";
190
202
  --psp-label--group-tooltip--content: "Descripciones emergentes";
191
203
  --psp-label--group-color--content: "Color";
204
+ --psp-label--group-column--content: "Columna";
192
205
  --psp-label--group-format--content: "Formato";
193
206
  --psp-label--column-size-override--content: "Ancho de columna";
194
207
  }