use crate::compat::{format, String, Vec};
use crate::json::project::ProjectNode;
use crate::json::JsonProject;
use crate::widget::capability::{WidgetFactory, WIRE_RULES};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetProfile {
Default,
Stripped,
}
impl TargetProfile {
pub fn feature_hint(self) -> &'static str {
match self {
Self::Default => "desktop",
Self::Stripped => "mini",
}
}
pub fn child_capacity(self) -> usize {
match self {
Self::Default => DEFAULT_CHILD_CAPACITY,
Self::Stripped => MINI_CHILD_CAPACITY,
}
}
}
pub const MINI_CHILD_CAPACITY: usize = 64;
pub const DEFAULT_CHILD_CAPACITY: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Availability {
Local,
Unavailable,
Unknown,
CrossProfileCaveat,
}
impl Availability {
pub fn permits_generation(&self) -> bool {
matches!(self, Self::Local | Self::CrossProfileCaveat)
}
pub fn needs_attention(&self) -> bool {
!matches!(self, Self::Local)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerationGap {
pub path: Vec<usize>,
pub widget: String,
pub reason: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GenerationReport {
pub nodes_emitted: usize,
pub properties_emitted: usize,
pub wires_emitted: usize,
pub resolved_at_generation: Vec<String>,
pub unsupported: Vec<GenerationGap>,
pub capacity_overflow: Vec<GenerationGap>,
pub cross_profile_caveats: Vec<GenerationGap>,
}
impl GenerationReport {
pub fn is_clean(&self) -> bool {
self.unsupported.is_empty()
&& self.capacity_overflow.is_empty()
&& self.cross_profile_caveats.is_empty()
}
pub fn summary(&self) -> String {
format!(
"{} nodes, {} properties, {} wires, {} resolved at generation time, {} unsupported, \
{} over capacity, {} cross-profile caveats",
self.nodes_emitted,
self.properties_emitted,
self.wires_emitted,
self.resolved_at_generation.len(),
self.unsupported.len(),
self.capacity_overflow.len(),
self.cross_profile_caveats.len()
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GeneratedSource {
pub source: String,
pub report: GenerationReport,
}
pub fn is_style_only_property(name: &str) -> bool {
matches!(
name,
"css_class"
| "selector"
| "transition"
| "animation"
| "hover_style"
| "active_style"
| "focus_style"
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerationRequest {
pub json: String,
pub target: TargetProfile,
pub width: u32,
pub height: u32,
pub function_name: String,
}
pub fn generate(request: &GenerationRequest) -> Result<GeneratedSource, String> {
validate_function_name(&request.function_name)?;
let project = JsonProject::parse(&request.json)?;
let factory = WidgetFactory::new_with_defaults();
let mut report = GenerationReport::default();
let availability = availability_with(&factory, &project.root_widget, request.target);
match availability {
Availability::Unavailable | Availability::Unknown => {
report.unsupported.push(GenerationGap {
path: Vec::new(),
widget: project.root_widget.clone(),
reason: availability_reason(availability),
});
return Ok(GeneratedSource { source: empty_source(request), report });
}
Availability::CrossProfileCaveat => {
report.cross_profile_caveats.push(GenerationGap {
path: Vec::new(),
widget: project.root_widget.clone(),
reason: availability_reason(availability),
});
}
Availability::Local => {}
}
check_capacity(&project, request.target, &mut report);
let geometry = plan_geometry(request, &project, &mut report);
let body = match request.target {
TargetProfile::Default => {
emit_default_mode(&project, request, &geometry, &factory, &mut report)
}
TargetProfile::Stripped => {
emit_stripped_mode(&project, request, &geometry, &factory, &mut report)
}
};
let source = assemble(request, &project, &body, &report, &factory);
Ok(GeneratedSource { source, report })
}
pub fn validate_function_name(name: &str) -> Result<(), String> {
if name.is_empty() {
return Err("generated function name must not be empty".to_string());
}
let mut chars = name.chars();
let first = chars.next().expect("non-empty checked above");
if !(first == '_' || first.is_alphabetic()) {
return Err(format!(
"generated function name {name:?} is not a valid Rust identifier: it must start with a
letter or underscore, not {first:?}"
));
}
if let Some(bad) = chars.find(|c| !(*c == '_' || c.is_alphanumeric())) {
return Err(format!(
"generated function name {name:?} is not a valid Rust identifier: it contains {bad:?},
which is not permitted in an identifier"
));
}
if is_rust_keyword(name) {
return Err(format!(
"generated function name {name:?} is a reserved Rust keyword and cannot be a function name"
));
}
Ok(())
}
fn is_rust_keyword(name: &str) -> bool {
matches!(
name,
"as" | "break" | "const" | "continue" | "crate" | "else" | "enum" | "extern"
| "false" | "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match"
| "mod" | "move" | "mut" | "pub" | "ref" | "return" | "self" | "Self" | "static"
| "struct" | "super" | "trait" | "true" | "type" | "unsafe" | "use" | "where"
| "while" | "async" | "await" | "dyn"
| "abstract" | "become" | "box" | "do" | "final" | "macro" | "override" | "priv"
| "typeof" | "unsized" | "virtual" | "yield" | "try"
)
}
pub fn availability(name: &str, target: TargetProfile) -> Availability {
availability_with(&WidgetFactory::new_with_defaults(), name, target)
}
pub fn availability_with(
factory: &WidgetFactory,
name: &str,
target: TargetProfile,
) -> Availability {
if factory.create(name, crate::core::Rect::new(0, 0, 1, 1), "").is_some() {
if host_profile_matches(target) {
Availability::Local
} else {
Availability::CrossProfileCaveat
}
} else if factory.capability(name).is_some() {
Availability::Unavailable
} else {
Availability::Unknown
}
}
fn host_profile_matches(target: TargetProfile) -> bool {
let host_is_stripped = cfg!(all(not(full_widgets), not(widgets_unstripped)));
host_is_stripped == (target == TargetProfile::Stripped)
}
fn availability_reason(availability: Availability) -> String {
match availability {
Availability::Unavailable => String::from(
"the running profile cannot construct this control, so it is gated out of the target \
too (this is how `mini` gates most `create_*` functions)",
),
Availability::Unknown => String::from(
"no capability and no constructor is registered for this name: it is a typo or a \
control this version does not ship",
),
Availability::CrossProfileCaveat => String::from(
"resolved under the running profile, which differs from the target; regenerate under \
the target's features to make this conclusive",
),
Availability::Local => String::new(),
}
}
fn empty_source(request: &GenerationRequest) -> String {
format!(
"// generated for `{}` — nothing was emitted; see the generation report.\n\
pub fn {}() {{}}\n",
request.target.feature_hint(),
request.function_name
)
}
fn check_capacity(project: &JsonProject, target: TargetProfile, report: &mut GenerationReport) {
let capacity = target.child_capacity();
for node in project.walk() {
if node.children.len() <= capacity {
continue;
}
report.capacity_overflow.push(GenerationGap {
path: node.path.clone(),
widget: node.widget.clone(),
reason: format!(
"{} children exceeds the target's capacity of {capacity}; the fixed-capacity storage \
drops the excess silently, so this must be split rather than generated",
node.children.len()
),
});
}
}
fn emit_default_mode(
project: &JsonProject,
request: &GenerationRequest,
geometry: &GeometryPlan,
factory: &WidgetFactory,
report: &mut GenerationReport,
) -> String {
let mut node_code = String::new();
emit_node(project, request, &[], 1, geometry, factory, &mut node_code, report);
let mut body = String::new();
body.push_str(" let tree = ");
body.push_str(&node_code);
body.push_str(";\n\n");
body.push_str(
" // The `View` value is the generated tree, and `create_for` is generated too, so this\n\
\x20 // program links no JSON parser and no widget-name table (mode 1's weight).\n\
\x20 let mut engine = rust_widgets::view::ViewEngine::new();\n\
\x20 let report = engine.mount(&GeneratedTree { tree }, &create_for);\n\
\x20 let _ = report;\n",
);
body
}
#[allow(clippy::too_many_arguments)]
fn emit_node(
project: &JsonProject,
request: &GenerationRequest,
path: &[usize],
depth: usize,
geometry: &GeometryPlan,
factory: &WidgetFactory,
out: &mut String,
report: &mut GenerationReport,
) {
let Some(node) = project.node(path) else {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: String::from("?"),
reason: String::from(
"the parsed project has no node at this path; the parser and the generator \
disagree about the document's shape",
),
});
out.push_str("Node::new(\"__missing__\")");
return;
};
let availability = availability_with(factory, &node.widget, request.target);
if !availability.permits_generation() {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: availability_reason(availability),
});
out.push_str("Node::new(\"__unsupported__\")");
return;
}
if availability == Availability::CrossProfileCaveat {
report.cross_profile_caveats.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: availability_reason(availability),
});
}
report.nodes_emitted += 1;
let mut line = format!("Node::new({})", quote(&node.widget));
line.push_str(&format!(".key({})", quote(&node.key)));
if depth == 1 {
report.resolved_at_generation.push(format!(
"the root `{}` is sized {}x{} through its own geometry (a window publishes neither \
`width` nor `height`, and `geometry` is read-only through the property contract)",
node.widget, geometry.root.2, geometry.root.3
));
}
for (name, value) in node.scalar_properties() {
if is_style_only_property(&name) {
report.resolved_at_generation.push(format!(
"`{name}` on {} is resolved into the style",
node_label(path, &node.widget)
));
continue;
}
if is_wire_key(&name) {
continue;
}
if crate::json::is_loader_owned_key(&name) && !carries_constructor_text(&name) {
report.resolved_at_generation.push(format!(
"`{name}` on {} is applied through the loader-owned route, not a property write",
node_label(path, &node.widget)
));
continue;
}
line.push_str(&format!(".prop({}, {})", quote(&name), capability_value_expr(&value)));
report.properties_emitted += 1;
}
out.push_str(&line);
for (key, handler) in node.declared_handlers() {
let prop =
format!("{WIRE_PROP_PREFIX}{EVENTS_KEY}.{key}", EVENTS_KEY = crate::json::EVENTS_KEY);
out.push_str(&format!(
".prop({}, CapabilityValue::String({}.to_string()))",
quote(&prop),
quote(&handler),
));
report.wires_emitted += 1;
}
for (key, handler) in node.declared_marker_handlers() {
let prop = format!("{WIRE_PROP_PREFIX}{key}");
out.push_str(&format!(
".prop({}, CapabilityValue::String({}.to_string()))",
quote(&prop),
quote(&handler),
));
report.wires_emitted += 1;
}
let next_depth = depth + 1;
for (child_index, child) in node.children.iter().enumerate() {
let Some(child_path) = project_child_path(project, node, child_index) else {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: format!(
"child entry {child} does not resolve to a node, so this subtree is incomplete"
),
});
continue;
};
out.push_str(".child(");
let mut child_code = String::new();
emit_node(
project,
request,
&child_path,
next_depth,
geometry,
factory,
&mut child_code,
report,
);
out.push_str(&child_code);
out.push(')');
}
}
fn emit_stripped_mode(
project: &JsonProject,
request: &GenerationRequest,
geometry: &GeometryPlan,
factory: &WidgetFactory,
report: &mut GenerationReport,
) -> String {
let mut nodes: Vec<(Vec<usize>, String, bool)> = Vec::new();
collect_stripped_nodes(project, request, &[], 1, geometry, factory, &mut nodes, report);
if nodes.is_empty() {
return String::from(" // nothing could be emitted; see the generation report\n");
}
let mut body = String::new();
let (root_path, root_expr, _) = &nodes[0];
debug_assert!(root_path.is_empty(), "the root must be collected first");
body.push_str(&format!(" let mut root = {root_expr};\n"));
for (path, expr, _setters) in nodes.iter().skip(1) {
body.push_str(&format!(" let {} = {expr};\n", binding_name(path)));
}
body.push('\n');
for (path, _expr, _setters) in nodes.iter().skip(1) {
let parent = if path.len() == 1 {
String::from("root")
} else {
binding_name(&path[..path.len() - 1])
};
let child = binding_name(path);
body.push_str(&format!(
" // A refused add must be *observable*: `try_add_child` records it in\n\
\x20 // `child_overflow_count`, and the assertion below turns a silent drop into a loud\n\
\x20 // failure. On `alloc_frugal` storage the extra child would otherwise vanish.\n\
\x20 let before = {parent}.base().child_overflow_count();\n\
\x20 {parent}.base_mut().try_add_child({child}.base().id());\n\
\x20 debug_assert_eq!(\n\
\x20 {parent}.base().child_overflow_count(),\n\
\x20 before,\n\
\x20 \"child capacity exceeded while adding a child; split this container\"\n\
\x20 );\n"
));
}
body.push_str("\n let _ = &root;\n");
body
}
#[allow(clippy::too_many_arguments)]
fn collect_stripped_nodes(
project: &JsonProject,
request: &GenerationRequest,
path: &[usize],
depth: usize,
geometry: &GeometryPlan,
factory: &WidgetFactory,
out: &mut Vec<(Vec<usize>, String, bool)>,
report: &mut GenerationReport,
) {
let Some(node) = project.node(path) else {
return;
};
let availability = availability_with(factory, &node.widget, request.target);
if !availability.permits_generation() {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: availability_reason(availability),
});
for descendant in project.descendants(path) {
report.unsupported.push(GenerationGap {
path: descendant,
widget: String::from("(under an unsupported parent)"),
reason: String::from(
"this node's parent could not be generated, so it has no control to be added to",
),
});
}
return;
}
report.nodes_emitted += 1;
let rect = geometry.rect_for(path, depth);
let type_name = constructor_path(&node.widget);
let canonical = WidgetFactory::new_with_defaults().canonical_name(&node.widget);
let mut expr = if let Some(text) = node.text().filter(|_| constructor_takes_text(&canonical)) {
format!(
"{type_name}::new(String::from({}), Rect::new({}, {}, {}, {}))",
quote(&text),
rect.0,
rect.1,
rect.2,
rect.3
)
} else {
format!("{type_name}::new(Rect::new({}, {}, {}, {}))", rect.0, rect.1, rect.2, rect.3)
};
let binding = binding_name(path);
let mut setters = String::new();
for (name, value) in node.scalar_properties() {
if is_style_only_property(&name) {
report.resolved_at_generation.push(format!(
"`{name}` on {} is resolved into the style",
node_label(path, &node.widget)
));
continue;
}
if is_wire_key(&name) {
continue;
}
if let Some(call) = setter_call(&binding, &name, &value) {
setters.push_str(&format!("\n // {name}\n {call}"));
report.properties_emitted += 1;
} else if is_known_setter_name(&name) && value.is_number() {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: format!(
"`{name}` is {value} which does not fit the generated setter's `i32` parameter"
),
});
}
}
for (key, handler) in node.declared_handlers() {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: format!(
"`{EVENTS_KEY}.{key}` -> `{handler}`: a stripped target has no widget registry, so \
there is no control id to bind a handler to; the wire is refused rather than \
generated as a subscription that could never fire",
EVENTS_KEY = crate::json::EVENTS_KEY
),
});
}
for (key, handler) in node.declared_marker_handlers() {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: format!(
"`{key}` -> `{handler}`: a stripped target has no widget registry, so there is \
no control id to bind a handler to; the wire is refused rather than generated as \
a subscription that could never fire"
),
});
}
if !setters.is_empty() {
expr =
format!("{{\n let mut {binding} = {expr};{setters}\n {binding}\n }}");
}
out.push((path.to_vec(), expr, !setters.is_empty()));
let next_depth = depth + 1;
for (child_index, child) in node.children.iter().enumerate() {
let Some(child_path) = project_child_path(project, node, child_index) else {
report.unsupported.push(GenerationGap {
path: path.to_vec(),
widget: node.widget.clone(),
reason: format!(
"child entry {child} does not resolve to a node, so this subtree is incomplete"
),
});
continue;
};
collect_stripped_nodes(
project,
request,
&child_path,
next_depth,
geometry,
factory,
out,
report,
);
}
}
fn placeholder_id(node: &ProjectNode) -> crate::core::ObjectId {
let mut id: crate::core::ObjectId = 1;
for index in &node.path {
id = id.wrapping_mul(31).wrapping_add(*index as crate::core::ObjectId + 1);
}
id
}
fn child_stretch(node: &ProjectNode) -> u32 {
node.property("stretch")
.and_then(|v| v.as_u64())
.map(|raw| u32::try_from(raw).unwrap_or(u32::MAX))
.unwrap_or(1)
}
pub fn constructor_type_name(widget: &str) -> &'static str {
constructor_type_for(&WidgetFactory::new_with_defaults(), widget)
.and_then(|path| path.rsplit("::").next())
.unwrap_or("UnsupportedControl")
}
fn constructor_takes_text(canonical: &str) -> bool {
matches!(
canonical,
"window"
| "button"
| "label"
| "check_box"
| "radio_button"
| "group_box"
| "line_edit"
| "text_edit"
)
}
fn canonical_constructor_path(canonical: &str) -> &'static str {
match canonical {
"window" => "rust_widgets::widget::Window",
"button" => "rust_widgets::widget::Button",
"label" => "rust_widgets::widget::Label",
"check_box" => "rust_widgets::widget::CheckBox",
"radio_button" => "rust_widgets::widget::RadioButton",
"slider" => "rust_widgets::widget::Slider",
"progress_bar" => "rust_widgets::widget::ProgressBar",
"line_edit" => "rust_widgets::widget::LineEdit",
"text_edit" => "rust_widgets::widget::TextEdit",
"combo_box" => "rust_widgets::widget::ComboBox",
"spin_box" => "rust_widgets::widget::SpinBox",
"list_box" => "rust_widgets::widget::ListBox",
"group_box" => "rust_widgets::widget::GroupBox",
"frame" => "rust_widgets::widget::Frame",
"arc" => "rust_widgets::widget::Arc",
"meter" => "rust_widgets::widget::Meter",
"stacked_widget" => "rust_widgets::widget::StackedWidget",
"splitter" => "rust_widgets::widget::Splitter",
"scroll_area" => "rust_widgets::widget::ScrollArea",
"tab_widget" => "rust_widgets::widget::TabWidget",
_ => "rust_widgets::designer::UnsupportedControl",
}
}
fn constructor_type_for(factory: &WidgetFactory, widget: &str) -> Option<&'static str> {
let canonical = factory.canonical_name(widget);
let path = canonical_constructor_path(&canonical);
(!path.ends_with("UnsupportedControl")).then_some(path)
}
pub fn constructor_path(widget: &str) -> &'static str {
constructor_type_for(&WidgetFactory::new_with_defaults(), widget)
.unwrap_or("rust_widgets::designer::UnsupportedControl")
}
fn setter_call(binding: &str, name: &str, value: &Value) -> Option<String> {
if let Some(flag) = value.as_bool() {
return match name {
"visible" => Some(format!("{binding}.set_visible({flag});")),
"enabled" => Some(format!("{binding}.set_enabled({flag});")),
"checked" => Some(format!("{binding}.set_checked({flag});")),
_ => None,
};
}
if let Some(number) = value.as_i64() {
let fits = i32::try_from(number).ok()?;
return match name {
"value" => Some(format!("{binding}.set_value({fits});")),
"minimum" | "min" => Some(format!("{binding}.set_minimum({fits});")),
"maximum" | "max" => Some(format!("{binding}.set_maximum({fits});")),
_ => None,
};
}
if let Some(text) = value.as_str() {
return match name {
"text" | "title" => None,
_ => {
let _ = text;
None
}
};
}
None
}
fn is_known_setter_name(name: &str) -> bool {
matches!(name, "value" | "minimum" | "min" | "maximum" | "max")
}
struct GeometryPlan {
rects: Vec<(Vec<usize>, (i32, i32, u32, u32))>,
root: (i32, i32, u32, u32),
}
impl GeometryPlan {
fn rect_for(&self, path: &[usize], depth: usize) -> (i32, i32, u32, u32) {
if let Some((_, rect)) = self.rects.iter().find(|(p, _)| p.as_slice() == path) {
return *rect;
}
if depth == 1 {
return self.root;
}
(0, 0, self.root.2.max(1), self.root.3.max(1))
}
}
fn plan_geometry(
request: &GenerationRequest,
project: &JsonProject,
report: &mut GenerationReport,
) -> GeometryPlan {
let root = (0i32, 0i32, request.width, request.height);
let mut rects: Vec<(Vec<usize>, (i32, i32, u32, u32))> = vec![(Vec::new(), root)];
let Some((kind, container_children)) = project.layout_declaration() else {
for node in project.walk() {
if !node.path.is_empty() {
rects.push((node.path.clone(), root));
}
}
return GeometryPlan { rects, root };
};
let mut engine = crate::json::create_layout_from_kind(&kind);
let mut solved: Vec<crate::core::Rect> = Vec::new();
let container_rect = crate::core::Rect::new(root.0, root.1, root.2, root.3);
for child in &container_children {
if let Some(node) = project.node(child) {
let placeholder = placeholder_id(node);
engine.add_widget(placeholder, child_stretch(node));
}
}
engine.update(container_rect, &mut |_id, rect| {
solved.push(rect);
});
report.resolved_at_generation.push(format!(
"the document's layout was solved into constant coordinates against {}x{}",
request.width, request.height
));
for (index, node) in container_children.iter().enumerate() {
match solved.get(index) {
Some(rect) => rects.push((node.clone(), (rect.x, rect.y, rect.width, rect.height))),
None => report.unsupported.push(GenerationGap {
path: node.clone(),
widget: String::from("?"),
reason: format!(
"the layout engine reported no rect for this node ({} placements for {} \
children), so its coordinates are not known",
solved.len(),
container_children.len()
),
}),
}
}
GeometryPlan { rects, root }
}
fn binding_name(path: &[usize]) -> String {
if path.is_empty() {
return String::from("root");
}
let mut name = String::from("n");
for index in path {
name.push('_');
name.push_str(&format!("{index}"));
}
name
}
fn node_label(path: &[usize], widget: &str) -> String {
if path.is_empty() {
return format!("<root> (a `{widget}`)");
}
format!("{} (a `{widget}`)", binding_name(path))
}
fn quote(value: &str) -> String {
let mut out = String::with_capacity(value.len() + 2);
out.push('"');
for ch in value.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 || c as u32 == 0x7f => {
out.push_str(&format!("\\u{{{:x}}}", c as u32));
}
_ => out.push(ch),
}
}
out.push('"');
out
}
fn capability_value_expr(value: &Value) -> String {
if let Some(v) = value.as_bool() {
return format!("CapabilityValue::Bool({v})");
}
if let Some(v) = value.as_i64() {
return format!("CapabilityValue::Int({v})");
}
if let Some(v) = value.as_u64() {
return format!("CapabilityValue::UInt({v})");
}
if let Some(v) = value.as_f64() {
return format!("CapabilityValue::Float({v})");
}
if let Some(s) = value.as_str() {
return format!("CapabilityValue::String(String::from({}))", quote(s));
}
String::from("CapabilityValue::Null")
}
fn carries_constructor_text(name: &str) -> bool {
matches!(name, "text" | "title")
}
fn project_child_path(
project: &JsonProject,
node: &ProjectNode,
child_index: usize,
) -> Option<Vec<usize>> {
project.child_of(node, child_index).map(|child| child.path.clone())
}
pub fn is_wire_key(name: &str) -> bool {
crate::json::is_marker_key(name) || name == crate::json::EVENTS_KEY
}
pub const WIRE_PROP_PREFIX: &str = "__wire_";
pub fn shared_wire_rule_count() -> usize {
WIRE_RULES.len()
}
pub fn wire_verdict_for(
source: Option<crate::widget::capability::PropertyValueKind>,
target: crate::widget::capability::WireTarget,
) -> crate::widget::capability::WireCompatibility {
crate::widget::capability::compatibility(source, target)
}
fn assemble(
request: &GenerationRequest,
project: &JsonProject,
body: &str,
report: &GenerationReport,
_factory: &WidgetFactory,
) -> String {
let mut source = String::new();
source.push_str(&format!(
"// Generated by the rust_widgets designer. Do not edit by hand.\n\
//\n\
// target profile : {} (build with `cargo build --no-default-features --features {}`)\n\
// root control : {}\n\
// nodes : {}\n\
// properties : {}\n",
request.target.feature_hint(),
request.target.feature_hint(),
project.root_widget,
report.nodes_emitted,
report.properties_emitted
));
if !report.resolved_at_generation.is_empty() {
source.push_str("//\n// resolved at generation time (not calls in this file):\n");
for item in &report.resolved_at_generation {
source.push_str(&format!("// - {item}\n"));
}
}
if !report.unsupported.is_empty() {
source.push_str("//\n// REFUSED — present in the document, absent from this file:\n");
for gap in &report.unsupported {
source.push_str(&format!(
"// - path {:?} `{}`: {}\n",
gap.path, gap.widget, gap.reason
));
}
}
if !report.capacity_overflow.is_empty() {
source.push_str("//\n// OVER CAPACITY — the target would drop these children silently:\n");
for gap in &report.capacity_overflow {
source.push_str(&format!(
"// - path {:?} `{}`: {}\n",
gap.path, gap.widget, gap.reason
));
}
}
match request.target {
TargetProfile::Default => {
source.push_str("\nuse rust_widgets::view::Node;\n");
source.push_str("use rust_widgets::widget::capability::CapabilityValue;\n");
}
TargetProfile::Stripped => {
source.push_str("\nuse rust_widgets::core::Rect;\n");
source.push_str("use rust_widgets::widget::Widget;\n");
}
}
source.push('\n');
source.push_str(&format!("pub fn {}() {{\n", request.function_name));
source.push_str(body);
source.push_str("}\n");
if request.target == TargetProfile::Default {
source.push_str(
"\n/// The generated tree, as a `View`.\n\
struct GeneratedTree {\n\
\x20 tree: Node,\n\
}\n\n\
impl rust_widgets::view::View for GeneratedTree {\n\
\x20 fn build(&self) -> Node {\n\
\x20 self.tree.clone()\n\
\x20 }\n\
}\n",
);
source.push_str(
"\n/// Builds one control for the generated tree, and wires the events it declares.\n\
///\n\
/// The control is built by the **widget registry**, so this file holds no per-type\n\
/// constructor table: every control the factory registers is constructible here. That\n\
/// is what lets a document place any control without the generator maintaining a\n\
/// parallel list that drifts behind the registry (BLUE19 rule #101).\n\
///\n\
/// # Why the wires are bound here rather than by the caller\n\
///\n\
/// `node` is the description the tree was built from, and it carries the handler names\n\
/// the document declared (see `WIRE_PROP_PREFIX`). Binding them at creation is what\n\
/// makes `events: { clicked: \"on_save\" }` mean something: before this the wires were\n\
/// parsed and dropped, so the generated program contained no reference to the handler\n\
/// and it could never run.\n\
///\n\
/// The *decision* of which callback an event needs is the library's, not this file's:\n\
/// each wire is handed to `bind_published_event` / `bind_marker_event`, which consult\n\
/// the control's own capability. A generated file that reimplemented that would be a\n\
/// second rule set (rule #98).\n\
fn create_for(node: &rust_widgets::view::Node) -> Option<rust_widgets::core::ObjectId> {\n\
\x20 let geometry = rust_widgets::core::Rect::new(0, 0, 0, 0);\n\
\x20 let text = node\n\
\x20 .prop_value(\"text\")\n\
\x20 .or_else(|| node.prop_value(\"title\"))\n\
\x20 .and_then(|v| v.as_str())\n\
\x20 .unwrap_or(\"\");\n\
\x20 // A fresh registry per call is what the factory's own docs recommend for a\n\
\x20 // one-shot build; a generated program has exactly one tree, so the cost is paid\n\
\x20 // once at mount.\n\
\x20 let factory = rust_widgets::widget::capability::WidgetFactory::new_with_defaults();\n\
\x20 let control = factory.create(node.widget.as_str(), geometry, text)?;\n\
\x20 // The text a text-bearing control takes at construction is already applied by\n\
\x20 // `create`; the remaining declared properties are written by `ViewEngine` through\n\
\x20 // the same property contract every other front end uses.\n\
\x20 let id = rust_widgets::widget::runtime::register(control)?;\n\
\x20 // Bind every wire the document declared. A refused name is reported at `warn!` by\n\
\x20 // the library and counted, so a wire that cannot fire is visible rather than silent.\n\
\x20 let prefix = \"__wire_\";\n\
\x20 for (name, value) in node.props.iter() {\n\
\x20 let Some(suffix) = name.strip_prefix(prefix) else { continue };\n\
\x20 let Some(handler) = value.as_str() else { continue };\n\
\x20 if let Some(event) = suffix.strip_prefix(\"events.\") {\n\
\x20 rust_widgets::json::bind_published_event(id, node.widget.as_str(), event, handler);\n\
\x20 } else if let Some(marker) = rust_widgets::json::marker_for_key(suffix) {\n\
\x20 rust_widgets::json::bind_marker_event(id, suffix, marker, handler);\n\
\x20 }\n\
\x20 // An unrecognised suffix is impossible: the generator emits only keys from\n\
\x20 // `is_wire_key`, and `node.props` is built by this file.\n\
\x20 }\n\
\x20 Some(id)\n\
}\n",
);
}
source
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn style_only_names_are_the_css_concepts_with_no_storage() {
assert!(is_style_only_property("css_class"));
assert!(is_style_only_property("transition"));
assert!(!is_style_only_property("width"));
assert!(!is_style_only_property("text"));
}
#[test]
fn wire_keys_are_not_properties() {
assert!(is_wire_key("events"));
assert!(is_wire_key("on_click"));
assert!(!is_wire_key("text"));
}
#[test]
fn an_out_of_i32_range_number_is_not_emitted_as_a_setter() {
let overflowing = serde_json::json!(2147483648i64);
assert_eq!(setter_call("w", "value", &overflowing), None);
assert_eq!(setter_call("w", "minimum", &overflowing), None);
assert!(is_known_setter_name("value"));
let fits = serde_json::json!(7i64);
assert_eq!(setter_call("w", "value", &fits).as_deref(), Some("w.set_value(7);"));
assert!(setter_call("w", "value", &serde_json::json!(i32::MIN as i64)).is_some());
assert!(setter_call("w", "value", &serde_json::json!(i32::MAX as i64)).is_some());
}
#[test]
fn an_out_of_range_stretch_clamps_rather_than_wrapping() {
let make = |props: Vec<(String, Value)>| ProjectNode {
widget: String::from("button"),
path: crate::compat::vec![],
key: String::from("button"),
properties: props,
wire_declaration: None,
marker_declarations: crate::compat::vec![],
children: crate::compat::vec![],
};
let huge =
make(crate::compat::vec![(String::from("stretch"), serde_json::json!(4294967296u64),)]);
assert_eq!(child_stretch(&huge), u32::MAX);
let default = make(crate::compat::vec![]);
assert_eq!(child_stretch(&default), 1);
let small = make(crate::compat::vec![(String::from("stretch"), serde_json::json!(3u64))]);
assert_eq!(child_stretch(&small), 3);
}
#[test]
fn the_generator_shares_the_runtime_wire_table() {
assert!(
shared_wire_rule_count() > 0,
"the generator must consult the same rule table the runtime uses (T-3)"
);
}
#[test]
fn a_wire_verdict_comes_from_the_shared_table() {
use crate::widget::capability::types::PropertyValueKind;
use crate::widget::capability::{WireCompatibility, WireTarget as Target};
assert!(matches!(
wire_verdict_for(
Some(PropertyValueKind::String),
Target::Property(PropertyValueKind::Int)
),
WireCompatibility::Rejected(_)
));
assert_eq!(
wire_verdict_for(
Some(PropertyValueKind::Bool),
Target::Property(PropertyValueKind::Bool)
),
WireCompatibility::Direct
);
}
#[test]
fn the_two_targets_report_different_capacities() {
assert_eq!(TargetProfile::Stripped.child_capacity(), MINI_CHILD_CAPACITY);
assert!(TargetProfile::Default.child_capacity() > MINI_CHILD_CAPACITY);
}
#[test]
fn availability_distinguishes_unknown_from_unavailable() {
assert!(matches!(
availability("definitely_not_a_control", TargetProfile::Default),
Availability::Unknown
));
}
#[test]
fn only_permitting_answers_allow_generation() {
assert!(Availability::Local.permits_generation());
assert!(Availability::CrossProfileCaveat.permits_generation());
assert!(!Availability::Unavailable.permits_generation());
assert!(!Availability::Unknown.permits_generation());
assert!(Availability::CrossProfileCaveat.needs_attention());
assert!(!Availability::Local.needs_attention());
}
#[test]
fn a_report_is_clean_only_when_nothing_needs_attention() {
let mut report = GenerationReport::default();
assert!(report.is_clean());
report.unsupported.push(GenerationGap {
path: Vec::new(),
widget: String::from("x"),
reason: String::from("nope"),
});
assert!(!report.is_clean());
}
#[test]
fn quoting_escapes_what_would_break_the_literal() {
assert_eq!(quote("plain"), "\"plain\"");
assert_eq!(quote("a\"b"), "\"a\\\"b\"");
assert_eq!(quote("a\\b"), "\"a\\\\b\"");
assert_eq!(quote("a\nb"), "\"a\\nb\"");
}
#[test]
fn quoting_escapes_the_whole_control_range() {
assert_eq!(quote("\u{7}"), "\"\\u{7}\"");
assert_eq!(quote("\u{1b}"), "\"\\u{1b}\"");
assert_eq!(quote("\u{0}"), "\"\\u{0}\"");
assert_eq!(quote("\u{7f}"), "\"\\u{7f}\"");
assert_eq!(quote("中"), "\"中\"");
}
#[test]
fn scalar_values_become_capability_values() {
assert_eq!(capability_value_expr(&Value::Bool(true)), "CapabilityValue::Bool(true)");
assert_eq!(
capability_value_expr(&Value::String(String::from("hi"))),
"CapabilityValue::String(String::from(\"hi\"))"
);
assert_eq!(capability_value_expr(&Value::Null), "CapabilityValue::Null");
}
#[test]
fn a_non_scalar_does_not_become_a_misleading_string() {
assert_eq!(capability_value_expr(&Value::Array(Vec::new())), "CapabilityValue::Null");
}
#[test]
fn binding_names_are_derived_from_the_path() {
assert_eq!(binding_name(&[]), "root");
assert_eq!(binding_name(&[3]), "n_3");
assert_eq!(binding_name(&[1, 2]), "n_1_2");
}
#[test]
fn declared_wires_reach_the_generated_source() {
const DOCUMENT: &str = r#"{
"window": {
"id": "root",
"title": "Wires",
"children": [
{
"button": {
"id": "save",
"text": "Save",
"events": { "clicked": "on_save" },
"on_change": "on_save_marker"
}
}
]
}
}"#;
for target in [TargetProfile::Default, TargetProfile::Stripped] {
let request = GenerationRequest {
json: DOCUMENT.to_string(),
target,
width: 320,
height: 240,
function_name: String::from("build_wires"),
};
let generated = generate(&request).expect("the document must generate");
for handler in ["on_save", "on_save_marker"] {
assert!(
generated.source.contains(handler),
"{target:?}: the declared handler `{handler}` is absent from the generated \
source, so it could never run"
);
}
}
}
#[test]
fn the_default_target_carries_wires_as_prefixed_props() {
const DOCUMENT: &str = r#"{
"window": {
"id": "root",
"title": "Wires",
"children": [
{ "button": { "id": "save", "text": "S", "events": { "clicked": "on_save" } } }
]
}
}"#;
let request = GenerationRequest {
json: DOCUMENT.to_string(),
target: TargetProfile::Default,
width: 320,
height: 240,
function_name: String::from("build_wires"),
};
let generated = generate(&request).expect("the document must generate");
assert!(
generated.source.contains(&format!("{WIRE_PROP_PREFIX}events.clicked")),
"the wire must travel as a prefixed prop, not under its document key"
);
assert!(
generated.source.contains("bind_published_event"),
"the generated file must hand the wire to the library's own binder (rule #98)"
);
assert_eq!(generated.report.wires_emitted, 1, "one declared wire, one emitted");
}
#[test]
fn a_stripped_target_refuses_a_wire_rather_than_dropping_it() {
const DOCUMENT: &str = r#"{
"window": {
"id": "root",
"title": "Wires",
"children": [
{ "button": { "id": "save", "text": "S", "events": { "clicked": "on_save" } } }
]
}
}"#;
let request = GenerationRequest {
json: DOCUMENT.to_string(),
target: TargetProfile::Stripped,
width: 320,
height: 240,
function_name: String::from("build_wires"),
};
let generated = generate(&request).expect("the document must generate");
assert!(
generated.report.unsupported.iter().any(|gap| gap.reason.contains("on_save")),
"the refusal must name the handler so the reader can act on it: {:?}",
generated.report.unsupported
);
assert!(!generated.report.is_clean(), "a refused wire is not a clean run");
}
#[test]
fn mixed_direct_and_layout_children_both_reach_the_generated_source() {
const DOCUMENT: &str = r#"{
"window": {
"title": "Mixed",
"children": [ { "label": { "text": "direct" } } ],
"layout": { "type": "vbox", "children": [ { "button": { "text": "layout" } } ] }
}
}"#;
for target in [TargetProfile::Default, TargetProfile::Stripped] {
let request = GenerationRequest {
json: DOCUMENT.to_string(),
target,
width: 320,
height: 240,
function_name: String::from("build_mixed"),
};
let generated = generate(&request).expect("the document must generate");
assert!(
generated.source.contains("String::from(\"direct\")"),
"{target:?}: the direct child's text must reach the generated source"
);
assert!(
generated.source.contains("String::from(\"layout\")"),
"{target:?}: the layout child's text must reach the generated source, not be \
replaced by the direct child"
);
assert_eq!(
generated.source.matches("direct").count(),
1,
"{target:?}: the direct child must be emitted exactly once, not in place of the \
layout child"
);
}
}
#[test]
fn an_invalid_function_name_is_refused() {
const DOCUMENT: &str = r#"{ "label": { "text": "hi" } }"#;
for name in ["123name", "has space", "has-dash", "", "a.b", "match", "fn"] {
for target in [TargetProfile::Default, TargetProfile::Stripped] {
let request = GenerationRequest {
json: DOCUMENT.to_string(),
target,
width: 320,
height: 240,
function_name: name.to_string(),
};
let result = generate(&request);
assert!(
result.is_err(),
"{target:?}: generating with function_name {name:?} must fail, not emit
uncompilable source"
);
}
}
let request = GenerationRequest {
json: DOCUMENT.to_string(),
target: TargetProfile::Default,
width: 320,
height: 240,
function_name: String::from("build_ui"),
};
let generated = generate(&request).expect("a legal name must generate");
assert!(generated.source.contains("pub fn build_ui()"));
}
#[test]
fn function_name_validation_accepts_identifiers_only() {
for good in ["build", "build_ui", "_private", "a1", "_", "Build"] {
assert!(validate_function_name(good).is_ok(), "{good:?} must be accepted");
}
for bad in ["", "1name", "na me", "a-b", "a.b", "match", "fn", "self", "crate"] {
assert!(validate_function_name(bad).is_err(), "{bad:?} must be rejected");
}
}
}