use super::{dom_id, render_child};
use crate::i18n::Text;
use crate::render::{Render, slug};
use crate::screen::{Component, Node, Toolbar};
use serde_json::{Value as Json, json};
macro_rules! into_node {
($($t:ty),* $(,)?) => {
$(impl From<$t> for Node {
fn from(c: $t) -> Self {
Node::new(c)
}
})*
};
}
pub(crate) use into_node;
into_node!(Row, Stack, Panel, Card, Tabs, Split);
fn children_schema(nodes: &[Node]) -> Vec<Json> {
nodes.iter().map(Node::schema).collect()
}
#[derive(Default)]
pub struct Row {
nodes: Vec<Node>,
weights: Vec<u8>,
compact: bool,
}
impl Row {
pub fn new() -> Self {
Self::default()
}
pub fn col(mut self, node: impl Into<Node>) -> Self {
self.nodes.push(node.into());
self.weights.push(1);
self
}
pub fn compact(mut self) -> Self {
self.compact = true;
self
}
pub fn weight(mut self, weight: u8) -> Self {
if let Some(w) = self.weights.last_mut() {
*w = weight.clamp(1, 12);
}
self
}
}
impl Component for Row {
fn render(&self, out: &mut String, r: &Render) {
out.push_str(if self.compact { r#"<div class="z-row z-row-compact">"# } else { r#"<div class="z-row">"# });
for (i, node) in self.nodes.iter().enumerate() {
out.push_str(&format!(r#"<div class="z-col" style="--z-flex:{}">"#, self.weights[i]));
render_child(out, r, i, node);
out.push_str("</div>");
}
out.push_str("</div>");
}
fn schema(&self) -> Json {
let columns: Vec<Json> =
self.nodes.iter().zip(&self.weights).map(|(n, w)| json!({ "weight": w, "node": n.schema() })).collect();
json!({ "type": "row", "columns": columns })
}
fn children(&self) -> &[Node] {
&self.nodes
}
}
#[derive(Default)]
pub struct Stack {
nodes: Vec<Node>,
gap: u8,
}
impl Stack {
pub fn new() -> Self {
Self { nodes: Vec::new(), gap: 3 }
}
pub fn add(mut self, node: impl Into<Node>) -> Self {
self.nodes.push(node.into());
self
}
pub fn gap(mut self, units: u8) -> Self {
self.gap = units.min(12);
self
}
}
impl Component for Stack {
fn render(&self, out: &mut String, r: &Render) {
out.push_str(&format!(r#"<div class="z-stack" style="--z-stack-gap:{}">"#, self.gap));
for (i, node) in self.nodes.iter().enumerate() {
render_child(out, r, i, node);
}
out.push_str("</div>");
}
fn schema(&self) -> Json {
json!({ "type": "stack", "gap": self.gap, "children": children_schema(&self.nodes) })
}
fn children(&self) -> &[Node] {
&self.nodes
}
}
pub struct Panel {
title: Text,
nodes: Vec<Node>,
toolbar: Option<Toolbar>,
collapsible: bool,
collapsed: bool,
}
impl Panel {
pub fn new(title: impl Into<Text>) -> Self {
Self { title: title.into(), nodes: Vec::new(), toolbar: None, collapsible: false, collapsed: false }
}
pub fn add(mut self, node: impl Into<Node>) -> Self {
self.nodes.push(node.into());
self
}
pub fn collapsible(mut self) -> Self {
self.collapsible = true;
self
}
pub fn collapsed(mut self) -> Self {
self.collapsible = true;
self.collapsed = true;
self
}
pub fn toolbar<F, R>(mut self, build: F) -> Self
where
F: FnOnce(Toolbar) -> R,
R: Into<Toolbar>,
{
self.toolbar = Some(build(Toolbar::default()).into());
self
}
}
impl Component for Panel {
fn render(&self, out: &mut String, r: &Render) {
let id = dom_id(r, "panel");
let title = r.text(&self.title);
out.push_str(&format!(
r#"<section class="z-panel" aria-labelledby="{id}-title"><header class="z-panel-head">"#
));
if self.collapsible {
out.push_str(&format!(
r#"<h2 class="z-panel-title" id="{id}-title"><button type="button" class="z-panel-toggle" data-zeyn-toggle aria-expanded="{}" aria-controls="{id}-body" data-testid="{}.panel.{}"><span class="z-chevron" aria-hidden="true"></span>{title}</button></h2>"#,
!self.collapsed,
r.scope,
slug(&self.title.english())
));
} else {
out.push_str(&format!(r#"<h2 class="z-panel-title" id="{id}-title">{title}</h2>"#));
}
if let Some(t) = &self.toolbar {
t.render(out, r, &title);
}
let hidden = if self.collapsed { " hidden" } else { "" };
out.push_str(&format!(r#"</header><div class="z-panel-body" id="{id}-body"{hidden}>"#));
for (i, node) in self.nodes.iter().enumerate() {
render_child(out, r, i, node);
}
out.push_str("</div></section>");
}
fn schema(&self) -> Json {
json!({
"type": "panel",
"title": self.title,
"collapsible": self.collapsible,
"collapsed": self.collapsed,
"toolbar": self.toolbar.as_ref().map(Toolbar::schema),
"children": children_schema(&self.nodes),
})
}
fn children(&self) -> &[Node] {
&self.nodes
}
}
#[derive(Default)]
pub struct Card {
title: Option<Text>,
nodes: Vec<Node>,
}
impl Card {
pub fn new() -> Self {
Self::default()
}
pub fn title(mut self, title: impl Into<Text>) -> Self {
self.title = Some(title.into());
self
}
pub fn add(mut self, node: impl Into<Node>) -> Self {
self.nodes.push(node.into());
self
}
}
impl Component for Card {
fn render(&self, out: &mut String, r: &Render) {
match &self.title {
Some(t) => {
let id = dom_id(r, "card");
out.push_str(&format!(
r#"<article class="z-card" aria-labelledby="{id}-title"><h3 class="z-card-title" id="{id}-title">{}</h3>"#,
r.text(t)
));
}
None => out.push_str(r#"<article class="z-card">"#),
}
for (i, node) in self.nodes.iter().enumerate() {
render_child(out, r, i, node);
}
out.push_str("</article>");
}
fn schema(&self) -> Json {
json!({ "type": "card", "title": self.title, "children": children_schema(&self.nodes) })
}
fn children(&self) -> &[Node] {
&self.nodes
}
}
pub struct Tabs {
label: Text,
titles: Vec<Text>,
nodes: Vec<Node>,
}
impl Tabs {
pub fn new(label: impl Into<Text>) -> Self {
Self { label: label.into(), titles: Vec::new(), nodes: Vec::new() }
}
pub fn tab(mut self, title: impl Into<Text>, node: impl Into<Node>) -> Self {
self.titles.push(title.into());
self.nodes.push(node.into());
self
}
}
impl Component for Tabs {
fn render(&self, out: &mut String, r: &Render) {
let id = dom_id(r, "tabs");
out.push_str(&format!(
r#"<div class="z-tabs" data-zeyn-tabs="{id}" data-zeyn-loading="{}" data-zeyn-failed="{}"><div class="z-tablist" role="tablist" aria-label="{}">"#,
r.text(&Text::key("zeyn.ui.loading")),
r.text(&Text::key("zeyn.ui.load_failed")),
r.text(&self.label)
));
for (i, title) in self.titles.iter().enumerate() {
let selected = i == 0;
out.push_str(&format!(
r#"<button type="button" role="tab" class="z-tab" id="{id}-tab-{i}" aria-selected="{selected}" aria-controls="{id}-panel-{i}" tabindex="{}" data-zeyn-tab="{}" data-testid="{}.tab.{}">{}</button>"#,
if selected { 0 } else { -1 },
slug(&title.english()),
r.scope,
slug(&title.english()),
r.text(title)
));
}
out.push_str("</div>");
for (i, node) in self.nodes.iter().enumerate() {
let path = if r.path.is_empty() { i.to_string() } else { format!("{}.{i}", r.path) };
if i == 0 {
out.push_str(&format!(r#"<div class="z-tabpanel" role="tabpanel" id="{id}-panel-{i}" aria-labelledby="{id}-tab-{i}" tabindex="0">"#));
render_child(out, r, i, node);
} else {
out.push_str(&format!(
r#"<div class="z-tabpanel" role="tabpanel" id="{id}-panel-{i}" aria-labelledby="{id}-tab-{i}" tabindex="0" hidden data-zeyn-lazy="{path}">"#
));
}
out.push_str("</div>");
}
out.push_str("</div>");
}
fn schema(&self) -> Json {
let tabs: Vec<Json> =
self.titles.iter().zip(&self.nodes).map(|(t, n)| json!({ "title": t, "node": n.schema() })).collect();
json!({ "type": "tabs", "label": self.label, "tabs": tabs })
}
fn children(&self) -> &[Node] {
&self.nodes
}
fn eager_children(&self) -> &[Node] {
&self.nodes[..self.nodes.len().min(1)]
}
}
pub struct Split {
nodes: Vec<Node>,
ratio: u8,
vertical: bool,
}
impl Split {
pub fn new(first: impl Into<Node>, second: impl Into<Node>) -> Self {
Self { nodes: vec![first.into(), second.into()], ratio: 40, vertical: false }
}
pub fn ratio(mut self, percent: u8) -> Self {
self.ratio = percent.clamp(10, 90);
self
}
pub fn vertical(mut self) -> Self {
self.vertical = true;
self
}
}
impl Component for Split {
fn render(&self, out: &mut String, r: &Render) {
let id = dom_id(r, "split");
let (class, orientation) =
if self.vertical { ("z-split z-split-v", "horizontal") } else { ("z-split", "vertical") };
out.push_str(&format!(r#"<div class="{class}" data-zeyn-split style="--z-split:{}%">"#, self.ratio));
out.push_str(&format!(r#"<div class="z-split-pane" id="{id}-first">"#));
render_child(out, r, 0, &self.nodes[0]);
out.push_str(&format!(
r#"</div><div class="z-split-bar" role="separator" tabindex="0" aria-orientation="{orientation}" aria-controls="{id}-first" aria-valuenow="{}" aria-valuemin="10" aria-valuemax="90" aria-label="{}" data-testid="{}.split"></div><div class="z-split-pane">"#,
self.ratio,
r.text(&Text::key("zeyn.ui.resize")),
r.scope
));
render_child(out, r, 1, &self.nodes[1]);
out.push_str("</div></div>");
}
fn schema(&self) -> Json {
json!({ "type": "split", "ratio": self.ratio, "vertical": self.vertical, "children": children_schema(&self.nodes) })
}
fn children(&self) -> &[Node] {
&self.nodes
}
}