use std::collections::HashMap;
use denise::{Rect, Size};
use denise_ui::widgets::describe::{
ALIGNMENTS, FITS, ORIENTATIONS, PRESENCES, Payload, Property, PropertyKind, RADII, ROLES,
Value, WidgetInfo, role_from_name,
};
use denise_ui::widgets::{
Alert, Avatar, Badge, Button, Carousel, Checkbox, Collapse, Column, Divider, Fit, Image, Label,
List, ListItem, MenuBar, Panel, Progress, RadialProgress, RadioGroup, Rating, Select, Slider,
Spinner, Table, Tabs, TextInput, Timeline, TimelineItem, Toggle, Tree, TreeItem, Video,
};
use denise_ui::{Anchors, Dock, NodeId, Ui};
use kdl::{KdlDocument, KdlNode, KdlValue};
use crate::error::{At, Error, Reason};
use crate::form::{Form, FormKind, MAX_DEPTH, Placement};
#[derive(Clone, Debug)]
pub struct Picture {
pub pixels: Vec<u32>,
pub size: Size,
}
#[derive(Clone, Copy, Debug)]
pub enum Handler<M> {
Plain(M),
Bool(fn(bool) -> M),
Index(fn(usize) -> M),
Number(fn(f32) -> M),
}
impl<M> Handler<M> {
fn wanted(payload: Payload) -> &'static str {
match payload {
Payload::None => "the message itself",
Payload::Bool => "a `fn(bool) -> M`",
Payload::Index => "a `fn(usize) -> M`",
Payload::Number => "a `fn(f32) -> M`",
}
}
}
pub trait Wiring<M> {
fn message(&mut self, name: &str, payload: Payload) -> Option<Handler<M>>;
fn asset(&mut self, path: &str) -> Option<Picture> {
let _ = path;
None
}
}
impl<M, F> Wiring<M> for F
where
F: FnMut(&str, Payload) -> Option<Handler<M>>,
{
fn message(&mut self, name: &str, payload: Payload) -> Option<Handler<M>> {
self(name, payload)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Placed {
pub id: NodeId,
pub parent: Option<NodeId>,
pub kind: &'static str,
pub name: Option<String>,
pub path: Vec<usize>,
}
#[derive(Clone, Debug, Default)]
pub struct Built {
names: HashMap<String, NodeId>,
placed: Vec<Placed>,
pages: Vec<Page>,
}
#[derive(Clone, Debug)]
pub struct Page {
pub path: Vec<usize>,
pub ordinal: usize,
pub id: NodeId,
}
impl Built {
pub fn node(&self, name: &str) -> Option<NodeId> {
self.names.get(name).copied()
}
pub fn names(&self) -> impl Iterator<Item = (&str, NodeId)> {
self.names.iter().map(|(name, &id)| (name.as_str(), id))
}
pub fn pages(&self) -> &[Page] {
&self.pages
}
pub fn placed(&self) -> &[Placed] {
&self.placed
}
pub fn at(&self, path: &[usize]) -> Option<&Placed> {
self.placed.iter().find(|p| p.path == path)
}
pub fn len(&self) -> usize {
self.names.len()
}
pub fn is_empty(&self) -> bool {
self.names.is_empty()
}
}
const ANCHOR_EDGES: &[&str] = &["left", "top", "right", "bottom"];
const DOCK_SIDES: &[&str] = &["top", "bottom", "left", "right", "fill"];
const ANYWHERE: PropertyKind = PropertyKind::Int {
min: -8192,
max: 8192,
};
pub const FORM_PROPERTIES: &[Property] = &[
Property::new(
"name",
PropertyKind::Text,
"What the application calls this form. Names what the typed layer generates.",
),
Property::new(
"kind",
PropertyKind::Enum(FormKind::NAMES),
"What this form is for: a screen, a window, a dialog, a drawer, a shelf, or a fragment.",
),
Property::new(
"width",
PropertyKind::Int { min: 1, max: 8192 },
"The width the form was designed at, in logical pixels.",
),
Property::new(
"height",
PropertyKind::Int { min: 1, max: 8192 },
"The height the form was designed at, in logical pixels.",
),
Property::new(
"theme",
PropertyKind::Enum(crate::form::THEMES),
"Which built-in theme the form is drawn with.",
),
Property::new(
"background",
PropertyKind::Enum(denise_ui::widgets::ROLES),
"The surface the form is drawn on.",
),
Property::new(
"scaling",
PropertyKind::Enum(crate::form::Scaling::NAMES),
"Whether this form may be drawn at another size: none, proportional or stretch.",
),
];
const WINDOW_PROPERTIES: &[Property] = &[
Property::new(
"resizable",
PropertyKind::Bool,
"Whether the window may be resized. Windows only.",
),
Property::new(
"min-width",
PropertyKind::Int { min: 0, max: 8192 },
"The narrowest the window may be made. Windows only.",
),
Property::new(
"min-height",
PropertyKind::Int { min: 0, max: 8192 },
"The shortest the window may be made. Windows only.",
),
];
const DIALOG_PROPERTIES: &[Property] = &[Property::new(
"dim",
PropertyKind::Int { min: 0, max: 255 },
"How dark the backdrop behind the dialog is, 0 to 255. Dialogs only.",
)];
const EDGE_PROPERTIES: &[Property] = &[
Property::new(
"side",
PropertyKind::Enum(denise_ui::widgets::SIDES),
"Which edge it comes in from.",
),
Property::new(
"extent",
PropertyKind::Int { min: 1, max: 8192 },
"How far it comes in. Required; across the other axis it covers the surface.",
),
];
pub const fn kind_properties(kind: FormKind) -> &'static [Property] {
match kind {
FormKind::Window => WINDOW_PROPERTIES,
FormKind::Dialog => DIALOG_PROPERTIES,
FormKind::Drawer | FormKind::Shelf => EDGE_PROPERTIES,
FormKind::Screen | FormKind::Fragment => &[],
}
}
pub fn form_property(kind: FormKind, name: &str) -> Option<&'static Property> {
FORM_PROPERTIES
.iter()
.chain(kind_properties(kind))
.find(|property| property.name == name)
}
pub const NODE_PROPERTIES: &[Property] = &[
Property::new(
"name",
PropertyKind::Text,
"What the application calls this node. Unique within the form.",
),
Property::new("x", ANYWHERE, "Left edge, relative to the parent."),
Property::new("y", ANYWHERE, "Top edge, relative to the parent."),
Property::new(
"w",
PropertyKind::Int { min: 0, max: 8192 },
"Width in pixels.",
),
Property::new(
"h",
PropertyKind::Int { min: 0, max: 8192 },
"Height in pixels.",
),
Property::new(
"visible",
PropertyKind::Bool,
"Drawn and able to be touched, or neither.",
),
Property::new(
"enabled",
PropertyKind::Bool,
"Takes input, or is greyed out and does not.",
),
Property::new(
"z",
PropertyKind::Int {
min: -1000,
max: 1000,
},
"Paint order among siblings; higher is nearer the front.",
),
Property::new(
"tooltip",
PropertyKind::Text,
"What resting the pointer on this node says.",
),
Property::new(
"scroll",
PropertyKind::Bool,
"Whether children reaching past this node can be scrolled to.",
),
Property::new(
"stack",
PropertyKind::Int { min: 0, max: 1000 },
"Stacks the children down the node with this many pixels between them.",
),
Property::new(
"focus",
PropertyKind::Bool,
"Whether this node holds the caret when the form opens. One per form.",
),
Property::new(
"anchor",
PropertyKind::Text,
"Edges held as the parent resizes: any of left, top, right, bottom.",
),
Property::new(
"dock",
PropertyKind::Enum(DOCK_SIDES),
"An edge of the parent this node takes for itself, before the rest are placed.",
),
];
pub fn node_property(name: &str) -> Option<&'static Property> {
NODE_PROPERTIES.iter().find(|p| p.name == name)
}
const COLLECTIONS: &[&str] = &[
"option", "item", "column", "row", "event", "picture", "tab", "title",
];
pub const DESIGN: &str = "design";
pub(crate) fn is_placeholder(kind: &str, name: &str) -> bool {
denise_ui::widgets::all()
.iter()
.find(|info| info.kind == kind)
.is_some_and(|info| {
info.properties
.iter()
.any(|p| p.name == name && p.kind == PropertyKind::Placeholder)
})
}
pub fn owns_children(kind: &str) -> bool {
matches!(kind, "panel" | "collapse")
}
const ARGUMENT: &[&str] = &[
"label", "badge", "divider", "alert", "button", "checkbox", "toggle", "collapse",
];
pub fn default_size(kind: &str) -> Size {
let (width, height) = match kind {
"alert" => (320, 36),
"avatar" => (40, 40),
"badge" => (60, 20),
"button" => (100, 32),
"carousel" => (224, 120),
"checkbox" | "toggle" => (200, 24),
"collapse" => (224, 40),
"divider" => (160, 16),
"image" => (120, 90),
"list" => (200, 160),
"panel" => (200, 120),
"progress" => (200, 8),
"radial-progress" => (48, 48),
"radio-group" => (220, 76),
"rating" => (140, 24),
"select" | "text-input" => (220, 34),
"slider" => (200, 24),
"spinner" => (24, 24),
"table" => (320, 180),
"tree" => (220, 180),
"tabs" => (320, 36),
"menubar" => (320, 28),
"timeline" => (220, 140),
"video" => (160, 90),
_ => (120, 20),
};
Size::new(width, height)
}
pub fn seed(kind: &str, rect: Rect) -> String {
let mut node = String::from(kind);
if ARGUMENT.contains(&kind) {
node.push_str(&format!(" {:?}", kind));
}
node.push_str(&format!(
" x={} y={} w={} h={}",
rect.x, rect.y, rect.width, rect.height
));
node.push_str(match kind {
"alert" => " role=info",
"slider" => " min=0 max=100",
"image" => " src=\"picture.png\"",
_ => "",
});
node
}
pub fn seed_form(title: &str, kind: FormKind, size: Size) -> String {
let mut out = format!("form {title:?} version={}", crate::form::VERSION);
if kind != FormKind::Screen {
out.push_str(&format!(" kind={}", FormKind::NAMES[kind as usize]));
}
out.push_str(&format!(" width={} height={}", size.width, size.height));
if matches!(kind, FormKind::Drawer | FormKind::Shelf) {
let along = match kind.default_side() {
denise_ui::Side::Above | denise_ui::Side::Below => size.height,
denise_ui::Side::Before | denise_ui::Side::After => size.width,
};
out.push_str(&format!(" extent={}", (along / 3).max(1)));
}
out.push('\n');
out
}
impl Form {
pub fn build<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error> {
self.build_fitted(
ui,
parent,
Placement {
x: 1.0,
y: 1.0,
rect: Rect::from_size(self.size()),
},
wiring,
)
}
pub fn build_scaled<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
scale: f32,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error> {
self.build_fitted(
ui,
parent,
Placement {
x: scale,
y: scale,
rect: Rect::from_size(self.size()).scaled(scale),
},
wiring,
)
}
pub fn build_with_design<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
scale: f32,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error> {
self.build_inner(
ui,
parent,
Placement {
x: scale,
y: scale,
rect: Rect::from_size(self.size()).scaled(scale),
},
wiring,
true,
)
}
pub fn build_fitted<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
fit: Placement,
wiring: &mut impl Wiring<M>,
) -> Result<Built, Error> {
self.build_inner(ui, parent, fit, wiring, false)
}
fn build_inner<M: Clone + 'static>(
&self,
ui: &mut Ui<M>,
parent: NodeId,
fit: Placement,
wiring: &mut impl Wiring<M>,
designing: bool,
) -> Result<Built, Error> {
let mut builder = Builder {
form: self,
ui,
wiring,
fit,
designing,
built: Built::default(),
focused: None,
};
let children: Vec<&KdlNode> = self
.root()
.children()
.map(|d| d.nodes().iter().collect())
.unwrap_or_default();
for (index, node) in children.into_iter().enumerate() {
builder.node(node, parent, 0, &[index])?;
}
let focused = builder.focused;
let built = builder.built;
if let Some(id) = focused {
ui.focus(Some(id));
}
Ok(built)
}
}
struct Builder<'a, M: 'static, W> {
form: &'a Form,
ui: &'a mut Ui<M>,
wiring: &'a mut W,
fit: Placement,
designing: bool,
built: Built,
focused: Option<NodeId>,
}
impl<M: Clone + 'static, W: Wiring<M>> Builder<'_, M, W> {
fn err(&self, node: &KdlNode, reason: Reason) -> Error {
Error::new(self.form.at_node(node), reason)
}
fn node(
&mut self,
node: &KdlNode,
parent: NodeId,
depth: usize,
path: &[usize],
) -> Result<(), Error> {
if depth >= MAX_DEPTH {
return Err(self.err(node, Reason::TooDeep { limit: MAX_DEPTH }));
}
let kind = node.name().value();
if COLLECTIONS.contains(&kind) {
return Err(self.err(
node,
Reason::UnexpectedChild {
parent: String::from("form"),
found: kind.to_string(),
},
));
}
let info = *denise_ui::widgets::all()
.iter()
.find(|w| w.kind == kind)
.ok_or_else(|| {
self.err(
node,
Reason::UnknownWidget {
found: kind.to_string(),
},
)
})?;
self.check_properties(node, &info)?;
let rect = self.rect(node)?;
let id = self.construct(node, &info, parent, rect)?;
self.apply_properties(node, &info, id)?;
self.apply_node_properties(node, id)?;
self.built.placed.push(Placed {
id,
parent: (depth > 0).then_some(parent),
kind: info.kind,
name: self.string(node, "name"),
path: path.to_vec(),
});
if let Some(children) = node.children() {
let owns_children = owns_children(kind);
let mut tabs_seen = 0usize;
for (index, child) in children.nodes().iter().enumerate() {
let name = child.name().value();
if name == DESIGN {
self.check_design(child, kind)?;
continue;
}
if is_placeholder(kind, name) {
return Err(self.err(
child,
Reason::PlaceholderOutside {
kind: kind.to_string(),
found: name.to_string(),
},
));
}
if name == "tab" && kind == "tabs" {
let ordinal = tabs_seen;
tabs_seen += 1;
if child.children().is_some_and(|b| !b.nodes().is_empty()) {
let mut below = path.to_vec();
below.push(index);
self.page(child, node, id, depth, &below, ordinal)?;
}
continue;
}
if COLLECTIONS.contains(&name) {
continue;
}
if !owns_children {
return Err(self.err(
child,
Reason::UnexpectedChild {
parent: kind.to_string(),
found: name.to_string(),
},
));
}
let mut below = path.to_vec();
below.push(index);
self.node(child, id, depth + 1, &below)?;
}
}
Ok(())
}
fn check_design(&self, block: &KdlNode, kind: &str) -> Result<(), Error> {
let Some(children) = block.children() else {
return Ok(());
};
for child in children.nodes() {
let name = child.name().value();
if !is_placeholder(kind, name) {
return Err(self.err(
child,
Reason::UnexpectedChild {
parent: format!("{kind}'s `design`"),
found: name.to_string(),
},
));
}
}
Ok(())
}
fn check_properties(&self, node: &KdlNode, info: &WidgetInfo) -> Result<(), Error> {
for entry in node.entries() {
let Some(name) = entry.name() else {
continue;
};
let name = name.value();
if node_property(name).is_some() || info.property(name).is_some() {
continue;
}
return Err(Error::new(
self.form.at(entry.span().offset()),
Reason::UnknownProperty {
kind: info.kind,
found: name.to_string(),
accepted: info.properties,
},
));
}
Ok(())
}
fn rect(&self, node: &KdlNode) -> Result<Rect, Error> {
let mut axes = [0i32; 4];
for (slot, name) in axes.iter_mut().zip(["x", "y", "w", "h"]) {
let value = node
.get(name)
.and_then(KdlValue::as_integer)
.ok_or_else(|| {
self.err(
node,
Reason::Missing {
kind: node.name().value().to_string(),
name: match name {
"x" => "x",
"y" => "y",
"w" => "w",
_ => "h",
},
},
)
})?;
*slot = i32::try_from(value).unwrap_or(i32::MAX);
}
Ok(Rect::new(axes[0], axes[1], axes[2], axes[3]).scaled_by(self.fit.x, self.fit.y))
}
fn arg(&self, node: &KdlNode) -> Option<String> {
node.entries()
.iter()
.find(|e| e.name().is_none())
.and_then(|e| e.value().as_string())
.map(str::to_string)
}
fn string(&self, node: &KdlNode, name: &str) -> Option<String> {
node.get(name)
.and_then(KdlValue::as_string)
.map(str::to_string)
}
fn number(&self, node: &KdlNode, name: &str) -> Option<f32> {
node.get(name).and_then(|v| {
v.as_float()
.map(|f| f as f32)
.or_else(|| v.as_integer().map(|i| i as f32))
})
}
fn handler(
&mut self,
node: &KdlNode,
property: &str,
payload: Payload,
) -> Result<Option<Handler<M>>, Error> {
let Some(name) = self.string(node, property) else {
return Ok(None);
};
match self.wiring.message(&name, payload) {
Some(handler) => Ok(Some(handler)),
None => Err(self.err(node, Reason::UnknownMessage { found: name })),
}
}
fn plain(&self, node: &KdlNode, name: &str, handler: Handler<M>) -> Result<M, Error> {
match handler {
Handler::Plain(message) => Ok(message),
_ => Err(self.wrong(node, name, Payload::None)),
}
}
fn on_bool(
&self,
node: &KdlNode,
name: &str,
handler: Handler<M>,
) -> Result<fn(bool) -> M, Error> {
match handler {
Handler::Bool(f) => Ok(f),
_ => Err(self.wrong(node, name, Payload::Bool)),
}
}
fn on_index(
&self,
node: &KdlNode,
name: &str,
handler: Handler<M>,
) -> Result<fn(usize) -> M, Error> {
match handler {
Handler::Index(f) => Ok(f),
_ => Err(self.wrong(node, name, Payload::Index)),
}
}
fn on_number(
&self,
node: &KdlNode,
name: &str,
handler: Handler<M>,
) -> Result<fn(f32) -> M, Error> {
match handler {
Handler::Number(f) => Ok(f),
_ => Err(self.wrong(node, name, Payload::Number)),
}
}
fn wrong(&self, node: &KdlNode, property: &str, payload: Payload) -> Error {
self.err(
node,
Reason::WrongMessage {
found: self.string(node, property).unwrap_or_default(),
wanted: Handler::<M>::wanted(payload),
},
)
}
fn required(&self, node: &KdlNode, name: &'static str) -> Error {
self.err(
node,
Reason::Missing {
kind: node.name().value().to_string(),
name,
},
)
}
fn collection<'n>(&self, node: &'n KdlNode, name: &str) -> Vec<&'n KdlNode> {
let holder = if is_placeholder(node.name().value(), name) {
if !self.designing {
return Vec::new();
}
let Some(design) = self.design_block(node) else {
return Vec::new();
};
design
} else {
let Some(children) = node.children() else {
return Vec::new();
};
children
};
holder
.nodes()
.iter()
.filter(|n| n.name().value() == name)
.collect()
}
fn page(
&mut self,
tab: &KdlNode,
tabs: &KdlNode,
strip: NodeId,
depth: usize,
path: &[usize],
ordinal: usize,
) -> Result<(), Error> {
let Some(bounds) = self.ui.bounds(strip) else {
return Ok(());
};
let band = self.ui.theme().metrics.size_field.max(1);
let rect = Rect::new(0, band, bounds.width, (bounds.height - band).max(0));
let page = self
.ui
.add(strip, Panel::bare(), rect)
.ok_or_else(|| self.err(tab, Reason::TreeRefused))?;
if let Some(children) = tab.children() {
for (index, child) in children.nodes().iter().enumerate() {
let mut below = path.to_vec();
below.push(index);
self.node(child, page, depth + 1, &below)?;
}
}
let selected = tabs
.get("selected")
.and_then(KdlValue::as_integer)
.unwrap_or(0);
let shown = usize::try_from(selected).unwrap_or(0) == ordinal;
self.ui.set_visible(page, shown);
self.built.pages.push(Page {
path: path.to_vec(),
ordinal,
id: page,
});
Ok(())
}
fn has_pages(&self, node: &KdlNode) -> bool {
self.collection(node, "tab").into_iter().any(|tab| {
tab.children()
.is_some_and(|block| !block.nodes().is_empty())
})
}
fn design_block<'n>(&self, node: &'n KdlNode) -> Option<&'n KdlDocument> {
node.children()?
.nodes()
.iter()
.find(|n| n.name().value() == DESIGN)?
.children()
}
fn strings(&self, node: &KdlNode, name: &str) -> Vec<String> {
self.collection(node, name)
.into_iter()
.map(|n| self.arg(n).unwrap_or_default())
.collect()
}
fn picture(&mut self, node: &KdlNode, path: &str) -> Result<Picture, Error> {
self.wiring.asset(path).ok_or_else(|| {
Error::new(
self.form.at_node(node),
Reason::Asset {
path: path.to_string(),
},
)
})
}
}
impl<M: Clone + 'static, W: Wiring<M>> Builder<'_, M, W> {
fn construct(
&mut self,
node: &KdlNode,
info: &WidgetInfo,
parent: NodeId,
rect: Rect,
) -> Result<NodeId, Error> {
let text = self.arg(node).unwrap_or_default();
let id = match info.kind {
"label" => self.ui.add(parent, Label::new(text), rect),
"panel" => self.ui.add(parent, Panel::default(), rect),
"badge" => self.ui.add(parent, Badge::new(text), rect),
"divider" => {
let divider = if self.arg(node).is_some() {
Divider::labelled(text)
} else {
Divider::new()
};
self.ui.add(parent, divider, rect)
}
"alert" => {
let role = self
.string(node, "role")
.ok_or_else(|| self.required(node, "role"))?;
let role = role_from_name(&role).ok_or_else(|| {
self.err(
node,
Reason::NotAName {
name: String::from("colour role"),
found: role.clone(),
accepted: ROLES,
},
)
})?;
self.ui.add(parent, Alert::new(role, text), rect)
}
"spinner" => self.ui.add(parent, Spinner::new(), rect),
"video" => self.ui.add(parent, Video::new(), rect),
"progress" => {
let value = self.number(node, "value").unwrap_or(0.0);
self.ui.add(parent, Progress::new(value), rect)
}
"radial-progress" => {
let value = self.number(node, "value").unwrap_or(0.0);
self.ui.add(parent, RadialProgress::new(value), rect)
}
"button" => {
let button = match self.handler(node, "on-press", Payload::None)? {
Some(h) => Button::new(text, self.plain(node, "on-press", h)?),
None => Button::inert(text),
};
self.ui.add(parent, button, rect)
}
"text-input" => {
let mut field = TextInput::<M>::new();
if let Some(h) = self.handler(node, "on-submit", Payload::None)? {
field = field.with_submit(self.plain(node, "on-submit", h)?);
}
self.ui.add(parent, field, rect)
}
"checkbox" => {
let widget = match self.handler(node, "on-change", Payload::Bool)? {
Some(h) => Checkbox::new(text, self.on_bool(node, "on-change", h)?),
None => Checkbox::inert(text),
};
self.ui.add(parent, widget, rect)
}
"toggle" => {
let widget = match self.handler(node, "on-change", Payload::Bool)? {
Some(h) => Toggle::new(text, self.on_bool(node, "on-change", h)?),
None => Toggle::inert(text),
};
self.ui.add(parent, widget, rect)
}
"slider" => {
let min = self
.number(node, "min")
.ok_or_else(|| self.required(node, "min"))?;
let max = self
.number(node, "max")
.ok_or_else(|| self.required(node, "max"))?;
let value = self.number(node, "value").unwrap_or(min);
let widget = match self.handler(node, "on-change", Payload::Number)? {
Some(h) => Slider::new(min, max, value, self.on_number(node, "on-change", h)?),
None => Slider::inert(min, max, value),
};
self.ui.add(parent, widget, rect)
}
"rating" => {
let value = self.number(node, "value").unwrap_or(0.0);
let widget = match self.handler(node, "on-change", Payload::Number)? {
Some(h) => Rating::new(value, self.on_number(node, "on-change", h)?),
None => Rating::display(value),
};
self.ui.add(parent, widget, rect)
}
"radio-group" => {
let options = self.strings(node, "option");
let widget = match self.handler(node, "on-change", Payload::Index)? {
Some(h) => RadioGroup::new(options, self.on_index(node, "on-change", h)?),
None => RadioGroup::inert(options),
};
self.ui.add(parent, widget, rect)
}
"menubar" => {
let titles = self.strings(node, "title");
let widget = match self.handler(node, "on-open", Payload::Index)? {
Some(h) => MenuBar::new(titles, self.on_index(node, "on-open", h)?),
None => MenuBar::inert(titles),
};
self.ui.add(parent, widget, rect)
}
"tabs" => {
let labels = self.strings(node, "tab");
let widget = match self.handler(node, "on-change", Payload::Index)? {
Some(h) => Tabs::new(labels, self.on_index(node, "on-change", h)?),
None => Tabs::inert(labels),
};
let widget = if self.has_pages(node) {
widget.over_pages()
} else {
widget
};
self.ui.add(parent, widget, rect)
}
"select" => {
let options = self.strings(node, "option");
let widget = match self.handler(node, "on-change", Payload::None)? {
Some(h) => Select::new(options, self.plain(node, "on-change", h)?),
None => Select::inert(options),
};
self.ui.add(parent, widget, rect)
}
"collapse" => {
let widget = match self.handler(node, "on-toggle", Payload::Bool)? {
Some(h) => Collapse::new(text, self.on_bool(node, "on-toggle", h)?),
None => Collapse::inert(text),
};
self.ui.add(parent, widget, rect)
}
"list" => {
let items: Vec<ListItem> = self
.collection(node, "item")
.into_iter()
.map(|n| {
let mut item = ListItem::new(self.arg(n).unwrap_or_default());
if let Some(leading) = self.string(n, "leading") {
item = item.with_leading(leading);
}
if let Some(trailing) = self.string(n, "trailing") {
item = item.with_trailing(trailing);
}
if n.get("enabled").and_then(KdlValue::as_bool) == Some(false) {
item = item.disabled();
}
item
})
.collect();
let mut widget = match self.handler(node, "on-select", Payload::Index)? {
Some(h) => List::new(items, self.on_index(node, "on-select", h)?),
None => List::inert(items),
};
if let Some(h) = self.handler(node, "on-activate", Payload::Index)? {
widget = widget.on_activate(self.on_index(node, "on-activate", h)?);
}
self.ui.add(parent, widget, rect)
}
"tree" => {
let items: Vec<TreeItem> = self
.collection(node, "item")
.into_iter()
.map(|n| {
let mut item = TreeItem::new(self.arg(n).unwrap_or_default());
if let Some(depth) = n.get("depth").and_then(KdlValue::as_integer) {
item = item.at_depth(depth.clamp(0, i128::from(u16::MAX)) as u16);
}
if n.get("open").and_then(KdlValue::as_bool) == Some(false) {
item = item.shut();
}
if let Some(leading) = self.string(n, "leading") {
item = item.with_leading(leading);
}
if let Some(trailing) = self.string(n, "trailing") {
item = item.with_trailing(trailing);
}
if n.get("enabled").and_then(KdlValue::as_bool) == Some(false) {
item = item.disabled();
}
item
})
.collect();
let mut widget = match self.handler(node, "on-select", Payload::Index)? {
Some(h) => Tree::new(items, self.on_index(node, "on-select", h)?),
None => Tree::inert(items),
};
if let Some(h) = self.handler(node, "on-activate", Payload::Index)? {
widget = widget.on_activate(self.on_index(node, "on-activate", h)?);
}
if let Some(h) = self.handler(node, "on-toggle", Payload::Index)? {
widget = widget.on_toggle(self.on_index(node, "on-toggle", h)?);
}
self.ui.add(parent, widget, rect)
}
"table" => {
let columns: Vec<Column> = self
.collection(node, "column")
.into_iter()
.map(|n| {
let title = self.arg(n).unwrap_or_default();
let mut column = match n.get("width").and_then(KdlValue::as_integer) {
Some(width) => Column::new(title, width as i32),
None => Column::flex(title),
};
match n.get("align").and_then(KdlValue::as_string) {
Some("end") => column = column.align_end(),
Some("center") => column = column.align_center(),
_ => {}
}
column
})
.collect();
let rows: Vec<Vec<String>> = self
.collection(node, "row")
.into_iter()
.map(|n| {
n.entries()
.iter()
.filter(|e| e.name().is_none())
.map(|e| e.value().as_string().unwrap_or_default().to_string())
.collect()
})
.collect();
let mut widget = match self.handler(node, "on-select", Payload::Index)? {
Some(h) => Table::new(columns, self.on_index(node, "on-select", h)?),
None => Table::inert(columns),
};
widget = widget.with_rows(rows);
if let Some(h) = self.handler(node, "on-activate", Payload::Index)? {
widget = widget.on_activate(self.on_index(node, "on-activate", h)?);
}
self.ui.add(parent, widget, rect)
}
"timeline" => {
let events: Vec<TimelineItem> = self
.collection(node, "event")
.into_iter()
.map(|n| {
let mut item = TimelineItem::new(self.arg(n).unwrap_or_default());
if let Some(time) = self.string(n, "time") {
item = item.with_time(time);
}
if let Some(role) =
self.string(n, "role").as_deref().and_then(role_from_name)
{
item = item.with_role(role);
}
if n.get("pending").and_then(KdlValue::as_bool) == Some(true) {
item = item.pending();
}
item
})
.collect();
self.ui.add(parent, Timeline::new(events), rect)
}
"image" => {
let path = self
.string(node, "src")
.ok_or_else(|| self.required(node, "src"))?;
let picture = self.picture(node, &path)?;
self.ui
.add(parent, Image::new(picture.pixels, picture.size), rect)
}
"avatar" => {
let avatar = match self.string(node, "src") {
Some(path) => {
let picture = self.picture(node, &path)?;
Avatar::new(picture.pixels, picture.size)
}
None => {
Avatar::initials(self.string(node, "initials").unwrap_or_default().as_str())
}
};
self.ui.add(parent, avatar, rect)
}
"carousel" => {
let mut widget = match self.handler(node, "on-change", Payload::Index)? {
Some(h) => Carousel::new(self.on_index(node, "on-change", h)?),
None => Carousel::inert(),
};
for picture_node in self.collection(node, "picture") {
let path = self
.string(picture_node, "src")
.ok_or_else(|| self.required(picture_node, "src"))?;
let picture = self.picture(picture_node, &path)?;
let fit = match self.string(picture_node, "fit").as_deref() {
Some("fill") => Fit::Fill,
Some("cover") => Fit::Cover,
Some("center") => Fit::Center,
_ => Fit::Contain,
};
widget = widget.with_picture_fit(picture.pixels, picture.size, fit);
}
self.ui.add(parent, widget, rect)
}
other => {
return Err(self.err(
node,
Reason::UnknownWidget {
found: other.to_string(),
},
));
}
};
id.ok_or_else(|| self.err(node, Reason::TreeRefused))
}
fn apply_properties(
&mut self,
node: &KdlNode,
info: &WidgetInfo,
id: NodeId,
) -> Result<(), Error> {
for property in info.properties {
if !property.is_settable() {
continue;
}
let Some(entry) = node
.entries()
.iter()
.find(|e| e.name().map(kdl::KdlIdentifier::value) == Some(property.name))
else {
continue;
};
let at = self.form.at(entry.span().offset());
let mut value = self.convert(at, info.kind, property, entry.value())?;
if property.pixels {
value = self.lengthened(value);
}
if let Some(Err(error)) = self.ui.set_property(id, property.name, value) {
return Err(Error::new(
at,
Reason::WrongType {
kind: info.kind,
name: property.name.to_string(),
wanted: match error.mismatch {
denise_ui::widgets::Mismatch::WrongType { expected } => expected.noun(),
_ => "something else",
},
},
));
}
}
Ok(())
}
fn lengthened(&self, value: Value) -> Value {
let scale = self.fit.uniform();
match value {
Value::Int(n) if n != 0 => {
let scaled = (n as f32 * scale + 0.5) as i32;
Value::Int(if n > 0 { scaled.max(1) } else { scaled.min(-1) })
}
Value::Float(f) => Value::Float(f * scale),
other => other,
}
}
fn convert(
&self,
at: At,
kind: &'static str,
property: &Property,
value: &KdlValue,
) -> Result<Value, Error> {
let wrong = |wanted: &'static str| {
Error::new(
at,
Reason::WrongType {
kind,
name: property.name.to_string(),
wanted,
},
)
};
Ok(match property.kind {
PropertyKind::Text | PropertyKind::Color => {
Value::text(value.as_string().ok_or_else(|| wrong("a string"))?)
}
PropertyKind::Bool => {
Value::Bool(value.as_bool().ok_or_else(|| wrong("true or false"))?)
}
PropertyKind::Int { .. } => {
let number = value.as_integer().ok_or_else(|| wrong("a whole number"))?;
Value::Int(i32::try_from(number).map_err(|_| wrong("a whole number"))?)
}
PropertyKind::Float { .. } => {
let number = value
.as_float()
.map(|f| f as f32)
.or_else(|| value.as_integer().map(|i| i as f32))
.ok_or_else(|| wrong("a number"))?;
Value::Float(number)
}
PropertyKind::Enum(names) => {
let found = value
.as_string()
.ok_or_else(|| wrong("one of the listed names"))?;
let name = names.iter().copied().find(|n| *n == found).ok_or_else(|| {
Error::new(
at,
Reason::NotAName {
name: property.name.to_string(),
found: found.to_string(),
accepted: names,
},
)
})?;
Value::Enum(name)
}
PropertyKind::Message(_) | PropertyKind::Asset => return Err(wrong("nothing here")),
_ => return Err(wrong("a value this crate does not know")),
})
}
fn apply_node_properties(&mut self, node: &KdlNode, id: NodeId) -> Result<(), Error> {
if let Some(name) = self.string(node, "name") {
if self.built.names.contains_key(&name) {
return Err(self.err(node, Reason::DuplicateName { name }));
}
self.built.names.insert(name, id);
}
if let Some(text) = self.string(node, "tooltip") {
self.ui.set_tooltip(id, text);
}
if let Some(z) = node.get("z").and_then(KdlValue::as_integer) {
self.ui.set_z(id, z as i32);
}
if node.get("scroll").and_then(KdlValue::as_bool) == Some(true) {
self.ui.set_scrollable(id, true);
}
if let Some(spacing) = node.get("stack").and_then(KdlValue::as_integer) {
self.ui.set_stack(id, spacing as i32);
}
if let Some(anchor) = self.string(node, "anchor") {
let mut anchors = Anchors::new(false, false, false, false);
for edge in anchor.split_whitespace() {
match edge {
"left" => anchors.left = true,
"top" => anchors.top = true,
"right" => anchors.right = true,
"bottom" => anchors.bottom = true,
other => {
return Err(self.err(
node,
Reason::NotAName {
name: String::from("anchor edge"),
found: other.to_string(),
accepted: ANCHOR_EDGES,
},
));
}
}
}
self.ui.set_anchors(id, anchors);
}
if let Some(dock) = self.string(node, "dock") {
let side = match dock.as_str() {
"top" => Dock::Top,
"bottom" => Dock::Bottom,
"left" => Dock::Left,
"right" => Dock::Right,
"fill" => Dock::Fill,
other => {
return Err(self.err(
node,
Reason::NotAName {
name: String::from("dock side"),
found: other.to_string(),
accepted: DOCK_SIDES,
},
));
}
};
self.ui.set_dock(id, Some(side));
}
if node.get("enabled").and_then(KdlValue::as_bool) == Some(false) {
self.ui.set_enabled(id, false);
}
if node.get("focus").and_then(KdlValue::as_bool) == Some(true) {
if self.focused.is_some() {
return Err(self.err(node, Reason::TwoFocuses));
}
self.focused = Some(id);
}
if node.get("visible").and_then(KdlValue::as_bool) == Some(false) {
self.ui.set_visible(id, false);
}
Ok(())
}
}
const _: &[&[&str]] = &[ALIGNMENTS, FITS, ORIENTATIONS, PRESENCES, RADII];