mod catalog;
mod chart;
mod controls;
mod date;
mod dock;
mod embed;
pub(crate) mod inputs;
#[cfg(feature = "islands")]
mod island;
mod layout;
mod lines;
mod lookup;
#[cfg(feature = "markdown")]
mod markdown;
mod media;
mod menus;
mod overlay;
mod shell;
mod tree;
mod widgets;
#[cfg(feature = "islands")]
pub(crate) use island::MARKER as ISLAND_MARKER;
#[cfg(feature = "islands")]
pub use island::{Canvas, Island, IslandLoad, Remote};
#[cfg(feature = "markdown")]
pub use markdown::{MAX_MARKDOWN, Markdown};
#[cfg(feature = "realtime")]
mod live;
#[cfg(feature = "realtime")]
pub use live::Live;
pub(crate) use dock::panel_ids as dock_panel_ids;
pub use dock::{DOCK_SIZE, Dock, DockArea, DockLayout, DockPanelState, DockSizes};
pub use embed::{Complete, EMBED_SANDBOX, Embed, MAX_EMBED_CONSENT, EmbedPermission, ExternalLink, Given, Image, NoAlt, NoSize, NoTitle};
pub use layout::{Card, Panel, Row, Split, Stack, Tabs};
pub use shell::{MenuBar, MenuList, PropertyInspector, StatusBar, Toolbox};
#[allow(deprecated)]
pub use catalog::PosterCard;
pub use catalog::{CardGrid, ImageCard, MAX_SUGGESTIONS, Rail, SearchBox, SearchParams};
pub use chart::{Better, BulletCell, Chart, ChartKind, Kpi, SparklineCell};
pub use controls::Control;
pub(crate) use controls::{
Slot, choices_text, read_choices, render_bool, render_choice, render_choices, render_number, render_text,
};
pub use date::DateOptions;
pub(crate) use date::{input_text, picker_config, range_reversed, render_date_input, render_range};
pub use inputs::{mask_example, mask_fits, mask_raw};
pub(crate) use lines::LinesEditor;
pub use lines::{Lines, MAX_LINES};
pub use lookup::{LookupItem, LookupQuery, LookupValue, MAX_LOOKUP_COLUMNS, MAX_LOOKUP_IDS, MAX_LOOKUP_PAGE};
pub(crate) use lookup::{parse_ids, render_lookup};
pub use media::{Audio, PLAYBACK_SPEEDS, Video};
pub use menus::MenuButton;
pub(crate) use overlay::tip;
pub use overlay::{BUSY_DELAY_MS, Busy, MAX_TIP, Popover};
pub use tree::{MAX_TREE_CHILDREN, Tree, TreeItem, TreeQuery};
pub use widgets::{
ActionButton, Alert, Badge, Breadcrumb, DescriptionList, DescriptionOf, EmptyState, Progress, Skeleton, Timeline,
};
use crate::action::{Action, ActionMeta, Result};
use crate::ctx::{BoxFuture, Ctx};
use crate::render::Render;
use crate::screen::{Node, Screen};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value as Json;
use std::fmt;
use std::marker::PhantomData;
pub const MAX_NODE_DEPTH: usize = 16;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct NodePath(String);
impl NodePath {
pub fn parse(s: &str) -> Option<NodePath> {
let parts: Vec<&str> = s.split('.').collect();
let valid = parts.len() <= MAX_NODE_DEPTH
&& parts.iter().all(|p| !p.is_empty() && p.len() <= 5 && p.bytes().all(|b| b.is_ascii_digit()));
valid.then(|| {
NodePath(parts.iter().map(|p| p.parse::<usize>().unwrap_or(0).to_string()).collect::<Vec<_>>().join("."))
})
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn indexes(&self) -> impl Iterator<Item = usize> + '_ {
self.0.split('.').filter_map(|p| p.parse().ok())
}
}
impl From<usize> for NodePath {
fn from(i: usize) -> Self {
NodePath(i.to_string())
}
}
impl fmt::Display for NodePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for NodePath {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl<'de> Deserialize<'de> for NodePath {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
Index(u64),
Path(String),
}
let text = match Raw::deserialize(deserializer)? {
Raw::Index(i) => i.to_string(),
Raw::Path(s) => s,
};
NodePath::parse(&text)
.ok_or_else(|| serde::de::Error::custom("node must be an index or a dotted path such as 1.0.2"))
}
}
impl Screen {
pub fn node(&self, path: &NodePath) -> Option<&Node> {
let mut indexes = path.indexes();
let mut node = self.nodes().get(indexes.next()?)?;
for i in indexes {
node = node.children().get(i)?;
}
Some(node)
}
pub fn walk(&self) -> Vec<(NodePath, &Node)> {
let mut out = Vec::new();
for (i, node) in self.nodes().iter().enumerate() {
walk_into(NodePath::from(i), node, &mut out);
}
out
}
pub fn walk_from(&self, path: &NodePath) -> Vec<(NodePath, &Node)> {
let mut out = Vec::new();
if let Some(node) = self.node(path) {
walk_into(path.clone(), node, &mut out);
}
out
}
}
fn walk_into<'a>(path: NodePath, node: &'a Node, out: &mut Vec<(NodePath, &'a Node)>) {
let children = node.eager_children();
out.push((path.clone(), node));
for (i, child) in children.iter().enumerate() {
walk_into(NodePath(format!("{}.{i}", path.0)), child, out);
}
}
pub fn render_node(out: &mut String, r: &Render, path: &str, node: &Node) {
out.push_str(&format!(r#"<div class="z-node" data-zeyn-node="{path}">"#));
let r = Render { path, ..*r };
match (node.grid(), r.loaded.and_then(|l| l.get(path))) {
(Some(grid), Some(page)) => grid.render_loaded(out, &r, page),
_ => node.render(out, &r),
}
out.push_str("</div>");
}
pub fn render_child(out: &mut String, r: &Render, index: usize, node: &Node) {
let path = if r.path.is_empty() { index.to_string() } else { format!("{}.{index}", r.path) };
render_node(out, r, &path, node);
}
pub(crate) fn dom_id(r: &Render, what: &str) -> String {
format!("{}-{what}-{}", r.scope, r.path.replace('.', "-"))
}
pub trait DataSource: Send + Sync {
fn action(&self) -> ActionMeta;
fn fetch(&self, ctx: Ctx, input: Json) -> BoxFuture<Result<Json>>;
}
pub(crate) struct ActionSource<A>(PhantomData<fn() -> A>);
impl<A> ActionSource<A> {
pub(crate) fn new() -> Self {
ActionSource(PhantomData)
}
}
impl<A, T> DataSource for ActionSource<A>
where
A: Action<Output = Result<T>>,
A::Input: DeserializeOwned,
T: Serialize + Send + 'static,
{
fn action(&self) -> ActionMeta {
A::META
}
fn fetch(&self, ctx: Ctx, input: Json) -> BoxFuture<Result<Json>> {
Box::pin(async move {
let input: A::Input = serde_json::from_value(input).map_err(|_| crate::action::Error::NotFound)?;
let output = A::call(ctx.clone(), input).await??;
crate::privacy::client_json(&output, &ctx).map_err(|e| crate::action::Error::Internal(e.to_string()))
})
}
}