1use super::{attrs::ThemePairs, id::Id};
7use dioxus::prelude::*;
8use std::fmt;
9
10#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
12pub struct Capabilities {
13 pub modal: bool,
15 pub popover: bool,
17 pub clipboard: bool,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24#[non_exhaustive]
25pub enum MediaPreference {
26 DarkScheme,
28 MoreContrast,
30 ForcedColors,
32}
33impl MediaPreference {
34 const fn query(self) -> &'static str {
35 match self {
36 Self::DarkScheme => "(prefers-color-scheme: dark)",
37 Self::MoreContrast => "(prefers-contrast: more)",
38 Self::ForcedColors => "(forced-colors: active)",
39 }
40 }
41}
42
43pub struct PreferenceWatch {
45 eval: document::Eval,
46 started: bool,
47}
48impl PreferenceWatch {
49 pub async fn next(&mut self) -> Option<bool> {
51 if !self.started {
52 return None;
53 }
54 self.eval.recv::<bool>().await.ok()
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[non_exhaustive]
61pub enum DomError {
62 Unsupported,
64 MissingTarget,
66 Rejected,
68}
69impl fmt::Display for DomError {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "host operation: {self:?}")
72 }
73}
74impl std::error::Error for DomError {}
75
76#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
78pub enum ScrollBlock {
79 #[default]
81 Nearest,
82 Start,
84 Center,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90pub enum Edge {
91 Left,
93 Right,
95}
96
97#[derive(Debug, Clone, Copy, PartialEq)]
99pub struct MenuPoint {
100 pub x: f64,
102 pub y: f64,
104 pub flip_y: Option<f64>,
107 pub edge: Option<Edge>,
109}
110
111#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
113pub struct DomBridge;
114
115impl DomBridge {
116 async fn send(operation: &str, data: &str, option: &str) -> Result<(), DomError> {
117 let eval = document::eval(include_str!("platform.js"));
118 eval.send((operation, data, option))
119 .map_err(|_| DomError::Unsupported)?;
120 match eval
121 .join::<String>()
122 .await
123 .map_err(|_| DomError::Unsupported)?
124 .as_str()
125 {
126 "ok" => Ok(()),
127 "missing" => Err(DomError::MissingTarget),
128 "unsupported" => Err(DomError::Unsupported),
129 _ => Err(DomError::Rejected),
130 }
131 }
132 pub async fn show_modal(self, id: &Id) -> Result<(), DomError> {
136 Self::send("modal", id.as_str(), "").await
137 }
138 pub async fn close_dialog(self, id: &Id) -> Result<(), DomError> {
142 Self::send("close", id.as_str(), "").await
143 }
144 pub async fn toggle_popover(self, id: &Id, open: bool) -> Result<(), DomError> {
148 Self::send("popover", id.as_str(), if open { "open" } else { "close" }).await
149 }
150 pub async fn focus(self, id: &Id) -> Result<(), DomError> {
154 Self::send("focus", id.as_str(), "").await
155 }
156 pub async fn scroll_into_view(self, id: &Id, block: ScrollBlock) -> Result<(), DomError> {
160 Self::send(
161 "scroll",
162 id.as_str(),
163 match block {
164 ScrollBlock::Nearest => "nearest",
165 ScrollBlock::Start => "start",
166 ScrollBlock::Center => "center",
167 },
168 )
169 .await
170 }
171 pub async fn scroll_edges(self, id: &Id) -> Result<(bool, bool), DomError> {
176 let eval = document::eval(include_str!("platform.js"));
177 eval.send(("edges", id.as_str(), ""))
178 .map_err(|_| DomError::Unsupported)?;
179 let answer = eval
180 .join::<String>()
181 .await
182 .map_err(|_| DomError::Unsupported)?;
183 match answer.split_once(' ') {
184 Some((start, end)) => Ok((start == "true", end == "true")),
185 None if answer == "missing" => Err(DomError::MissingTarget),
186 None => Err(DomError::Unsupported),
187 }
188 }
189 pub async fn page(self, id: &Id, forward: bool) -> Result<(), DomError> {
194 Self::send("page", id.as_str(), if forward { "next" } else { "prev" }).await
195 }
196 pub async fn place_below(self, list: &Id, invoker: &Id) -> Result<(), DomError> {
201 Self::send("place", list.as_str(), invoker.as_str()).await
202 }
203 pub async fn place_at(self, id: &Id, point: MenuPoint) -> Result<(), DomError> {
209 let edge = match point.edge {
210 Some(Edge::Left) => "left",
211 Some(Edge::Right) => "right",
212 None => "",
213 };
214 let flip = point.flip_y.map(|f| f.to_string()).unwrap_or_default();
215 Self::send(
216 "point",
217 id.as_str(),
218 &format!("{},{},{flip},{edge}", point.x, point.y),
219 )
220 .await
221 }
222 pub async fn point_below(self, trigger: &Id) -> Result<MenuPoint, DomError> {
227 let eval = document::eval(include_str!("platform.js"));
228 eval.send(("below", trigger.as_str(), ""))
229 .map_err(|_| DomError::Unsupported)?;
230 let answer = eval
231 .join::<String>()
232 .await
233 .map_err(|_| DomError::Unsupported)?;
234 let parts: Vec<&str> = answer.split(',').collect();
235 match parts.as_slice() {
236 [x, y, flip, edge] => Ok(MenuPoint {
237 x: x.parse().map_err(|_| DomError::Rejected)?,
238 y: y.parse().map_err(|_| DomError::Rejected)?,
239 flip_y: flip.parse().ok(),
240 edge: Some(if *edge == "right" {
241 Edge::Right
242 } else {
243 Edge::Left
244 }),
245 }),
246 _ if answer == "missing" => Err(DomError::MissingTarget),
247 _ => Err(DomError::Unsupported),
248 }
249 }
250 pub async fn capture_pointer(self, id: &Id, pointer: i32) -> Result<(), DomError> {
254 Self::send("capture", id.as_str(), &pointer.to_string()).await
255 }
256 pub async fn set_indeterminate(self, id: &Id, mixed: bool) -> Result<(), DomError> {
261 Self::send(
262 "indeterminate",
263 id.as_str(),
264 if mixed { "on" } else { "off" },
265 )
266 .await
267 }
268 pub async fn copy_text(self, text: &str) -> Result<(), DomError> {
272 Self::send("copy", text, "").await
273 }
274 pub async fn delay(self, ms: u32) -> Result<(), DomError> {
280 let eval = document::eval(
281 "const ms = await dioxus.recv(); await new Promise(r => setTimeout(r, ms)); return 'ok';",
282 );
283 eval.send(ms).map_err(|_| DomError::Unsupported)?;
284 match eval.join::<String>().await {
285 Ok(_) => Ok(()),
286 Err(_) => Err(DomError::Unsupported),
287 }
288 }
289 pub async fn reload_frame(self, id: &Id) -> Result<(), DomError> {
293 Self::send("reload", id.as_str(), "").await
294 }
295 pub async fn prefers(self, preference: MediaPreference) -> Option<bool> {
298 let eval = document::eval(
299 "const query = await dioxus.recv(); return typeof matchMedia === 'function' ? matchMedia(query).matches : null;",
300 );
301 eval.send(preference.query()).ok()?;
302 eval.join::<Option<bool>>().await.ok().flatten()
303 }
304 #[must_use]
307 pub fn watch(self, preference: MediaPreference) -> PreferenceWatch {
308 let eval = document::eval(
309 "const query = await dioxus.recv(); if (typeof matchMedia !== 'function') return; const media = matchMedia(query); dioxus.send(media.matches); media.addEventListener('change', event => dioxus.send(event.matches)); await new Promise(() => {});",
310 );
311 let started = eval.send(preference.query()).is_ok();
312 PreferenceWatch { eval, started }
313 }
314 pub(crate) async fn mirror_document_theme(
321 self,
322 token: u64,
323 generation: u64,
324 pairs: Option<&ThemePairs>,
325 ) -> Result<(), DomError> {
326 let eval = Self::send_document_theme(token, generation, pairs)?;
327 match eval.join::<String>().await {
328 Ok(_) => Ok(()),
329 Err(_) => Err(DomError::Unsupported),
330 }
331 }
332 #[allow(clippy::unused_self)]
336 pub(crate) fn release_document_theme(self, token: u64, generation: u64) {
337 let _ = Self::send_document_theme(token, generation, None);
338 }
339 fn send_document_theme(
340 token: u64,
341 generation: u64,
342 pairs: Option<&ThemePairs>,
343 ) -> Result<document::Eval, DomError> {
344 let eval = document::eval(include_str!("document-theme.js"));
346 eval.send((token, generation, pairs.map(|pairs| pairs.to_vec())))
347 .map_err(|_| DomError::Unsupported)?;
348 Ok(eval)
349 }
350 pub async fn capabilities(self) -> Capabilities {
352 let eval = document::eval("return [typeof HTMLDialogElement !== 'undefined' && 'showModal' in HTMLDialogElement.prototype, 'showPopover' in HTMLElement.prototype, !!navigator.clipboard?.writeText];");
353 let (modal, popover, clipboard) =
354 eval.join::<(bool, bool, bool)>().await.unwrap_or_default();
355 Capabilities {
356 modal,
357 popover,
358 clipboard,
359 }
360 }
361}
362
363#[must_use]
366pub fn location_query() -> Option<String> {
367 #[cfg(all(feature = "web", target_arch = "wasm32"))]
368 {
369 web_sys::window().and_then(|window| window.location().search().ok())
370 }
371 #[cfg(not(all(feature = "web", target_arch = "wasm32")))]
372 {
373 None
374 }
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
379#[non_exhaustive]
380pub enum EditorEvent {
381 Change(String),
383 Marks(Vec<crate::models::rich_text::Mark>),
385}
386
387pub struct EditorSession(document::Eval);
391impl EditorSession {
392 pub async fn next(&mut self) -> Option<EditorEvent> {
394 let (kind, html, marks) = self
395 .0
396 .recv::<(String, String, Vec<crate::models::rich_text::Mark>)>()
397 .await
398 .ok()?;
399 match kind.as_str() {
400 "change" => Some(EditorEvent::Change(html)),
401 _ => Some(EditorEvent::Marks(marks)),
402 }
403 }
404 pub fn set_value(&self, html: &str) {
407 let _ = self.0.send(("set", html));
408 }
409}
410impl Drop for EditorSession {
411 fn drop(&mut self) {
412 let _ = self.0.send(("destroy", ""));
413 }
414}
415
416impl DomBridge {
417 pub async fn bind_editor(self, id: &Id) -> Result<EditorSession, DomError> {
424 let mut eval = document::eval(
425 r#"const [id, base] = await dioxus.recv();
426const root = document.getElementById(id);
427if (!root) { dioxus.send(["missing", "", []]); return; }
428const { FutureCssEditor } = await import(`${base}/editor.js`);
429const editor = new FutureCssEditor({
430 root,
431 onChange: html => dioxus.send(["change", html, []]),
432 onSelectionChange: active => dioxus.send(["marks", "", Object.keys(active).filter(mark => active[mark])]),
433});
434dioxus.send(["ready", "", []]);
435while (true) {
436 const [op, html] = await dioxus.recv();
437 if (op === "destroy") { editor.destroy(); return; }
438 editor.value = html;
439}"#,
440 );
441 eval.send((id.as_str(), super::assets::BINDINGS_JS.to_string()))
442 .map_err(|_| DomError::Unsupported)?;
443 match eval
444 .recv::<(String, String, Vec<crate::models::rich_text::Mark>)>()
445 .await
446 {
447 Ok((kind, _, _)) if kind == "ready" => Ok(EditorSession(eval)),
448 Ok(_) => Err(DomError::MissingTarget),
449 Err(_) => Err(DomError::Unsupported),
450 }
451 }
452}