use std::fmt::Write as _;
use yog_gfx::image::Stretch;
use yog_ids::yui as y;
use yog_ids::yui_attr as at;
use yog_ui::attr::Attr;
use super::attr_code;
use super::elements::{
COMMON_ATTRS, CONTAINERS, LEAVES, component_expr, is_component_name, kind_attrs, wpf_hint,
};
use super::values::{
Names, color_attr, font_attr, image_source, is_image_value, number, text_attr,
};
use super::{Element, RAW_CALL, err};
use crate::rust_code::f32_lit;
pub(crate) struct Gen {
pub(crate) events: Names,
pub(crate) ids: Names,
mod_id: Option<String>,
}
const EVENT_PREFIX: &str = "on_";
const MOD_SCHEME: &str = yog_ids::concatcp!(yog_ids::image_scheme::MOD, yog_ids::sep::NAMESPACE);
const ONE: u32 = 1;
pub(crate) fn static_widget(
el: &Element,
in_canvas: bool,
depth: usize,
mod_id: Option<&str>,
) -> Result<(String, Vec<String>), String> {
let mut codegen = Gen::new(mod_id);
let expr = codegen.widget(el, in_canvas, depth)?;
Ok((expr, codegen.ids.into_vec()))
}
impl Gen {
pub(crate) fn new(mod_id: Option<&str>) -> Self {
Gen {
events: Names::default(),
ids: Names::default(),
mod_id: mod_id.map(str::to_string),
}
}
pub(crate) fn widget(
&mut self,
el: &Element,
in_canvas: bool,
depth: usize,
) -> Result<String, String> {
let kind = el.name.as_str();
let is_component = check_element(el, depth)?;
let mut expr = self.base(el, is_component)?;
let indent = " ".repeat(depth + 2);
let mut calls: Vec<String> = Vec::new();
position(el, in_canvas, &mut calls)?;
for (key, value) in &el.attrs {
self.attr_call(el, is_component, key, value, &mut calls)?;
}
for c in calls {
let _ = write!(expr, "\n{indent}.{c}");
}
let canvas = matches!(kind, y::SCREEN | y::CANVAS);
for child in &el.children {
let c = self.widget(child, canvas, depth + 1)?;
let _ = write!(expr, "\n{indent}.child({c})");
}
Ok(expr)
}
fn base(&mut self, el: &Element, is_component: bool) -> Result<String, String> {
let text = || text_attr(el, at::TEXT, el.attr(at::TEXT).unwrap_or_default());
Ok(match el.name.as_str() {
y::SCREEN | y::CANVAS | y::COLUMN => "widget::panel(FlexDir::Column)".to_string(),
y::ROW => "widget::panel(FlexDir::Row)".to_string(),
y::SCROLL_VIEW => "widget::scroll_view()".to_string(),
y::GRID => "widget::grid()".to_string(),
y::LABEL => format!("widget::label({})", text()?),
y::BUTTON => format!("widget::button({})", text()?),
y::TEXT_BOX => format!("widget::text_box({})", text()?),
y::ITEM_SLOT => format!(
"widget::item_slot({:?})",
el.attr(at::ITEM).unwrap_or_default()
),
y::MACHINE => machine(el),
y::INV_SLOT => inv_slot(el)?,
y::SLOT => slot(el)?,
y::PLAYER_INVENTORY => "widget::player_inventory()".to_string(),
y::IMAGE => self.image(el)?,
y::SURFACE => self.surface(el)?,
_ if is_component => component_expr(el)?,
_ => "widget::spacer()".to_string(),
})
}
fn attr_call(
&mut self,
el: &Element,
is_component: bool,
key: &str,
v: &str,
calls: &mut Vec<String>,
) -> Result<(), String> {
if key.starts_with(RAW_CALL) {
calls.push(v.to_string());
return Ok(());
}
if is_component && !COMMON_ATTRS.contains(&key) {
if key.starts_with(EVENT_PREFIX) && !v.trim().is_empty() {
self.events.push(v);
}
return Ok(());
}
let call = match key {
at::ID => {
self.ids.push(v);
format!("{key}({v:?})")
}
at::ON_CLICK | at::ON_CHANGE | at::ON_SUBMIT => {
self.events.push(v);
format!("{key}({v:?})")
}
at::PLACEHOLDER | at::TOOLTIP => format!("{key}({})", text_attr(el, key, v)?),
at::FONT => format!("{key}({:?})", font_attr(el, v)?),
at::BG if is_image_value(v) => format!(
"bg_image({:?})",
image_source(el, key, v.trim(), self.mod_id.as_deref())?
),
at::BG | at::COLOR | at::FOCUS_COLOR => format!(
"{key}(::yog_api::ui::Color(0x{:08X}))",
color_attr(el, key, v)?
),
_ => match Attr::parse(key, v) {
Ok(Some(attr)) => match attr_code::call(&attr) {
Some(c) => c,
None => return Ok(()),
},
Ok(None) => return Ok(()),
Err(want) => return err(el, format_args!("{key}=\"{v}\": expected {want}")),
},
};
calls.push(call);
Ok(())
}
fn surface(&mut self, el: &Element) -> Result<String, String> {
let line = el.line;
let source = el
.attr(at::SOURCE)
.ok_or_else(|| format!("line {line}: <Surface> needs a source (the id its code registers, e.g. source=\"mod:grid\")"))?
.trim();
let id = match (source.strip_prefix(MOD_SCHEME), &self.mod_id) {
(Some(path), Some(m)) => yog_ids::join(m, path),
(Some(_), None) => {
return Err(format!(
"line {line}: <Surface> source=\"{source}\": mod: needs the mod id (no yog.toml found)"
));
}
(None, _) => source.to_string(),
};
if !id.contains(yog_ids::sep::NAMESPACE) || !crate::content::valid_id(&id) {
return Err(format!(
"line {line}: <Surface> source=\"{source}\": expected <namespace>:<name> or mod:<name> \
(lower case a-z, 0-9, `_`, `-`, `.`, `/`)"
));
}
Ok(format!("widget::surface({id:?})"))
}
fn image(&mut self, el: &Element) -> Result<String, String> {
let line = el.line;
let source = el
.attr(at::SOURCE)
.ok_or_else(|| {
format!("line {line}: <Image> needs a source (e.g. source=\"item:apple\")")
})?
.trim();
let source = image_source(el, at::SOURCE, source, self.mod_id.as_deref())?;
let mut expr = format!("widget::image({source:?})");
if let Some(v) = el.attr(at::STRETCH) {
let names = || Stretch::ALL.map(|(_, n)| n).join(", ");
let s = Stretch::parse(v).map(Stretch::name).ok_or_else(|| {
format!(
"line {line}: <Image> stretch=\"{v}\": expected one of {}",
names()
)
})?;
let _ = write!(expr, ".stretch(::yog_api::ui::Stretch::{s})");
}
if let Some(v) = el.attr(at::UV) {
let nums: Vec<f32> = v
.split([',', ' '])
.filter(|p| !p.trim().is_empty())
.map(|p| number(el, at::UV, p))
.collect::<Result<_, _>>()?;
let [u, vv, w, h] = nums[..] else {
return Err(format!(
"line {line}: <Image> uv=\"{v}\": expected \"u v width height\""
));
};
let _ = write!(
expr,
".region([{}, {}, {}, {}])",
f32_lit(u),
f32_lit(vv),
f32_lit(w),
f32_lit(h)
);
}
if let Some(v) = el.attr(at::TINT) {
let _ = write!(
expr,
".tint(::yog_api::ui::Color(0x{:08X}))",
color_attr(el, at::TINT, v)?
);
}
Ok(expr)
}
}
fn check_element(el: &Element, depth: usize) -> Result<bool, String> {
let kind = el.name.as_str();
let is_container = CONTAINERS.contains(&kind);
let is_component = !is_container && !LEAVES.contains(&kind) && is_component_name(kind);
if !is_container && !LEAVES.contains(&kind) && !is_component {
let (_, containers) = CONTAINERS.split_at(1);
return Err(format!(
"line {}: unknown element <{kind}> (expected one of {}, {}, or a custom component's PascalCase name)",
el.line,
containers.join(", "),
LEAVES.join(", ")
));
}
if kind == y::SCREEN && depth > 0 {
return Err(format!(
"line {}: <Screen> must be the root element",
el.line
));
}
if !is_container && !el.children.is_empty() {
return Err(format!("line {}: <{kind}> can't have children", el.line));
}
let foreign = |key: &str| {
!key.starts_with(RAW_CALL)
&& !COMMON_ATTRS.contains(&key)
&& !kind_attrs(kind).contains(&key)
};
if !is_component && let Some((key, _)) = el.attrs.iter().find(|(k, _)| foreign(k)) {
let hint = wpf_hint(key)
.map(|h| format!(" — .yui writes it `{h}`"))
.unwrap_or_default();
return Err(format!(
"line {}: <{kind}> has no attribute `{key}`{hint}",
el.line
));
}
Ok(is_component)
}
fn position(el: &Element, in_canvas: bool, calls: &mut Vec<String>) -> Result<(), String> {
if el.name == y::SCREEN {
for (key, call) in [(at::WIDTH, at::W), (at::HEIGHT, at::H)] {
if let Some(v) = el.attr(key) {
calls.push(format!(
"{call}(Units::Px({}))",
f32_lit(number(el, key, v)?)
));
}
}
}
let x = el.attr(at::X).map(|v| number(el, at::X, v)).transpose()?;
let y = el.attr(at::Y).map(|v| number(el, at::Y, v)).transpose()?;
if in_canvas || x.is_some() || y.is_some() {
calls.push(format!(
"at({}, {})",
f32_lit(x.unwrap_or(0.0)),
f32_lit(y.unwrap_or(0.0))
));
}
Ok(())
}
fn machine(el: &Element) -> String {
let places: Vec<String> = el
.attr(at::PLACES)
.unwrap_or_default()
.split(',')
.filter(|p| !p.is_empty())
.map(|p| {
let (slot, rest) = p.split_once('/').unwrap_or((p, "0=0"));
let (i, inv) = rest.split_once('=').unwrap_or(("0", "0"));
format!("({slot:?}, {i}, {inv})")
})
.collect();
format!(
"widget::machine({:?}, &[{}], {})",
el.attr(at::KIND).unwrap_or_default(),
places.join(", "),
el.attr(at::RESULT).unwrap_or("0")
)
}
fn inv_slot(el: &Element) -> Result<String, String> {
let count = match el.attr(at::COUNT) {
Some(c) => c.trim().parse::<u32>().map_err(|_| {
format!(
"line {}: <InvSlot> count=\"{c}\": expected a whole number",
el.line
)
})?,
None => ONE,
};
Ok(format!(
"widget::inv_slot({:?}, {count})",
el.attr(at::ITEM).unwrap_or_default()
))
}
fn slot(el: &Element) -> Result<String, String> {
let index = el.attr(at::INDEX).unwrap_or_default();
let index = index.trim().parse::<usize>().map_err(|_| {
format!(
"line {}: <Slot> index=\"{index}\": expected the container slot's number (0, 1, ...)",
el.line
)
})?;
let accepts: Vec<&str> = el
.attr(at::ACCEPTS)
.unwrap_or_default()
.split(|c: char| c.is_whitespace() || c == ',')
.filter(|a| !a.is_empty())
.collect();
for a in &accepts {
let id = a.strip_prefix(yog_ids::sep::TAG).unwrap_or(a);
let ok = id
.split_once(yog_ids::sep::NAMESPACE)
.is_some_and(|(ns, path)| yog_ids::valid_namespace(ns) && yog_ids::valid_path(path));
if !ok {
return Err(format!(
"line {}: <Slot> accepts=\"{a}\": expected an item id (minecraft:redstone) or an item tag (#c:dusts), with its namespace",
el.line
));
}
}
Ok(if accepts.is_empty() {
format!("widget::slot({index})")
} else {
format!("widget::slot_accepting({index}, &{accepts:?})")
})
}