use super::{attrs::ThemePairs, id::Id};
use dioxus::prelude::*;
use std::fmt;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Capabilities {
pub modal: bool,
pub popover: bool,
pub clipboard: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum MediaPreference {
DarkScheme,
MoreContrast,
ForcedColors,
}
impl MediaPreference {
const fn query(self) -> &'static str {
match self {
Self::DarkScheme => "(prefers-color-scheme: dark)",
Self::MoreContrast => "(prefers-contrast: more)",
Self::ForcedColors => "(forced-colors: active)",
}
}
}
pub struct PreferenceWatch {
eval: document::Eval,
started: bool,
}
impl PreferenceWatch {
pub async fn next(&mut self) -> Option<bool> {
if !self.started {
return None;
}
self.eval.recv::<bool>().await.ok()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum DomError {
Unsupported,
MissingTarget,
Rejected,
}
impl fmt::Display for DomError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "host operation: {self:?}")
}
}
impl std::error::Error for DomError {}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScrollBlock {
#[default]
Nearest,
Start,
Center,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Edge {
Left,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MenuPoint {
pub x: f64,
pub y: f64,
pub flip_y: Option<f64>,
pub edge: Option<Edge>,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct DomBridge;
impl DomBridge {
async fn send(operation: &str, data: &str, option: &str) -> Result<(), DomError> {
let eval = document::eval(include_str!("platform.js"));
eval.send((operation, data, option))
.map_err(|_| DomError::Unsupported)?;
match eval
.join::<String>()
.await
.map_err(|_| DomError::Unsupported)?
.as_str()
{
"ok" => Ok(()),
"missing" => Err(DomError::MissingTarget),
"unsupported" => Err(DomError::Unsupported),
_ => Err(DomError::Rejected),
}
}
pub async fn show_modal(self, id: &Id) -> Result<(), DomError> {
Self::send("modal", id.as_str(), "").await
}
pub async fn close_dialog(self, id: &Id) -> Result<(), DomError> {
Self::send("close", id.as_str(), "").await
}
pub async fn toggle_popover(self, id: &Id, open: bool) -> Result<(), DomError> {
Self::send("popover", id.as_str(), if open { "open" } else { "close" }).await
}
pub async fn focus(self, id: &Id) -> Result<(), DomError> {
Self::send("focus", id.as_str(), "").await
}
pub async fn scroll_into_view(self, id: &Id, block: ScrollBlock) -> Result<(), DomError> {
Self::send(
"scroll",
id.as_str(),
match block {
ScrollBlock::Nearest => "nearest",
ScrollBlock::Start => "start",
ScrollBlock::Center => "center",
},
)
.await
}
pub async fn scroll_edges(self, id: &Id) -> Result<(bool, bool), DomError> {
let eval = document::eval(include_str!("platform.js"));
eval.send(("edges", id.as_str(), ""))
.map_err(|_| DomError::Unsupported)?;
let answer = eval
.join::<String>()
.await
.map_err(|_| DomError::Unsupported)?;
match answer.split_once(' ') {
Some((start, end)) => Ok((start == "true", end == "true")),
None if answer == "missing" => Err(DomError::MissingTarget),
None => Err(DomError::Unsupported),
}
}
pub async fn page(self, id: &Id, forward: bool) -> Result<(), DomError> {
Self::send("page", id.as_str(), if forward { "next" } else { "prev" }).await
}
pub async fn place_below(self, list: &Id, invoker: &Id) -> Result<(), DomError> {
Self::send("place", list.as_str(), invoker.as_str()).await
}
pub async fn place_at(self, id: &Id, point: MenuPoint) -> Result<(), DomError> {
let edge = match point.edge {
Some(Edge::Left) => "left",
Some(Edge::Right) => "right",
None => "",
};
let flip = point.flip_y.map(|f| f.to_string()).unwrap_or_default();
Self::send(
"point",
id.as_str(),
&format!("{},{},{flip},{edge}", point.x, point.y),
)
.await
}
pub async fn point_below(self, trigger: &Id) -> Result<MenuPoint, DomError> {
let eval = document::eval(include_str!("platform.js"));
eval.send(("below", trigger.as_str(), ""))
.map_err(|_| DomError::Unsupported)?;
let answer = eval
.join::<String>()
.await
.map_err(|_| DomError::Unsupported)?;
let parts: Vec<&str> = answer.split(',').collect();
match parts.as_slice() {
[x, y, flip, edge] => Ok(MenuPoint {
x: x.parse().map_err(|_| DomError::Rejected)?,
y: y.parse().map_err(|_| DomError::Rejected)?,
flip_y: flip.parse().ok(),
edge: Some(if *edge == "right" {
Edge::Right
} else {
Edge::Left
}),
}),
_ if answer == "missing" => Err(DomError::MissingTarget),
_ => Err(DomError::Unsupported),
}
}
pub async fn capture_pointer(self, id: &Id, pointer: i32) -> Result<(), DomError> {
Self::send("capture", id.as_str(), &pointer.to_string()).await
}
pub async fn set_indeterminate(self, id: &Id, mixed: bool) -> Result<(), DomError> {
Self::send(
"indeterminate",
id.as_str(),
if mixed { "on" } else { "off" },
)
.await
}
pub async fn copy_text(self, text: &str) -> Result<(), DomError> {
Self::send("copy", text, "").await
}
pub async fn delay(self, ms: u32) -> Result<(), DomError> {
let eval = document::eval(
"const ms = await dioxus.recv(); await new Promise(r => setTimeout(r, ms)); return 'ok';",
);
eval.send(ms).map_err(|_| DomError::Unsupported)?;
match eval.join::<String>().await {
Ok(_) => Ok(()),
Err(_) => Err(DomError::Unsupported),
}
}
pub async fn reload_frame(self, id: &Id) -> Result<(), DomError> {
Self::send("reload", id.as_str(), "").await
}
pub async fn prefers(self, preference: MediaPreference) -> Option<bool> {
let eval = document::eval(
"const query = await dioxus.recv(); return typeof matchMedia === 'function' ? matchMedia(query).matches : null;",
);
eval.send(preference.query()).ok()?;
eval.join::<Option<bool>>().await.ok().flatten()
}
#[must_use]
pub fn watch(self, preference: MediaPreference) -> PreferenceWatch {
let eval = document::eval(
"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(() => {});",
);
let started = eval.send(preference.query()).is_ok();
PreferenceWatch { eval, started }
}
pub(crate) async fn mirror_document_theme(
self,
token: u64,
generation: u64,
pairs: Option<&ThemePairs>,
) -> Result<(), DomError> {
let eval = Self::send_document_theme(token, generation, pairs)?;
match eval.join::<String>().await {
Ok(_) => Ok(()),
Err(_) => Err(DomError::Unsupported),
}
}
#[allow(clippy::unused_self)]
pub(crate) fn release_document_theme(self, token: u64, generation: u64) {
let _ = Self::send_document_theme(token, generation, None);
}
fn send_document_theme(
token: u64,
generation: u64,
pairs: Option<&ThemePairs>,
) -> Result<document::Eval, DomError> {
let eval = document::eval(include_str!("document-theme.js"));
eval.send((token, generation, pairs.map(|pairs| pairs.to_vec())))
.map_err(|_| DomError::Unsupported)?;
Ok(eval)
}
pub async fn capabilities(self) -> Capabilities {
let eval = document::eval("return [typeof HTMLDialogElement !== 'undefined' && 'showModal' in HTMLDialogElement.prototype, 'showPopover' in HTMLElement.prototype, !!navigator.clipboard?.writeText];");
let (modal, popover, clipboard) =
eval.join::<(bool, bool, bool)>().await.unwrap_or_default();
Capabilities {
modal,
popover,
clipboard,
}
}
}
#[must_use]
pub fn location_query() -> Option<String> {
#[cfg(all(feature = "web", target_arch = "wasm32"))]
{
web_sys::window().and_then(|window| window.location().search().ok())
}
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
{
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum EditorEvent {
Change(String),
Marks(Vec<crate::models::rich_text::Mark>),
}
pub struct EditorSession(document::Eval);
impl EditorSession {
pub async fn next(&mut self) -> Option<EditorEvent> {
let (kind, html, marks) = self
.0
.recv::<(String, String, Vec<crate::models::rich_text::Mark>)>()
.await
.ok()?;
match kind.as_str() {
"change" => Some(EditorEvent::Change(html)),
_ => Some(EditorEvent::Marks(marks)),
}
}
pub fn set_value(&self, html: &str) {
let _ = self.0.send(("set", html));
}
}
impl Drop for EditorSession {
fn drop(&mut self) {
let _ = self.0.send(("destroy", ""));
}
}
impl DomBridge {
pub async fn bind_editor(self, id: &Id) -> Result<EditorSession, DomError> {
let mut eval = document::eval(
r#"const [id, base] = await dioxus.recv();
const root = document.getElementById(id);
if (!root) { dioxus.send(["missing", "", []]); return; }
const { FutureCssEditor } = await import(`${base}/editor.js`);
const editor = new FutureCssEditor({
root,
onChange: html => dioxus.send(["change", html, []]),
onSelectionChange: active => dioxus.send(["marks", "", Object.keys(active).filter(mark => active[mark])]),
});
dioxus.send(["ready", "", []]);
while (true) {
const [op, html] = await dioxus.recv();
if (op === "destroy") { editor.destroy(); return; }
editor.value = html;
}"#,
);
eval.send((id.as_str(), super::assets::BINDINGS_JS.to_string()))
.map_err(|_| DomError::Unsupported)?;
match eval
.recv::<(String, String, Vec<crate::models::rich_text::Mark>)>()
.await
{
Ok((kind, _, _)) if kind == "ready" => Ok(EditorSession(eval)),
Ok(_) => Err(DomError::MissingTarget),
Err(_) => Err(DomError::Unsupported),
}
}
}