use crate::core::ObjectId;
use crate::widget::capability::properties_trait::widget_property_set;
use crate::widget::capability::CapabilityValue;
use super::diff::Patch;
use super::node::Node;
pub(crate) fn write_property(
id: ObjectId,
name: &str,
value: CapabilityValue,
) -> Result<(), String> {
let value = if matches!(value, CapabilityValue::Null) {
resolve_null_reset(id, name).unwrap_or(value)
} else {
value
};
let written = match crate::widget::runtime::with_widget_mut(id, |widget| {
widget_property_set(widget, name, value).map_err(|e| format!("{e:?}"))
}) {
Some(result) => result,
None => Err("the control is not registered with the runtime".to_string()),
};
if written.is_ok() {
crate::widget::runtime::request_repaint(id);
}
written
}
fn resolve_null_reset(id: ObjectId, name: &str) -> Option<CapabilityValue> {
let kind = crate::widget::runtime::with_widget_mut(id, |widget| widget.kind())?;
let default =
crate::widget::capability::access::default_widget_property_default_value(kind, name)?;
(!matches!(default, CapabilityValue::Null)).then_some(default)
}
#[derive(Debug, Clone, PartialEq)]
pub enum ViewError {
UnknownWidget {
id: ObjectId,
},
PropertyRefused {
id: ObjectId,
name: String,
reason: String,
},
UnknownParent {
parent: ObjectId,
},
UnknownWidgetType {
widget: String,
key: Option<String>,
path: Vec<String>,
},
}
impl core::fmt::Display for ViewError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
ViewError::UnknownWidget { id } => {
write!(f, "patch targets widget {id}, which is not mounted")
}
ViewError::PropertyRefused { id, name, reason } => {
write!(f, "widget {id} refused a write to '{name}': {reason}")
}
ViewError::UnknownParent { parent } => {
write!(f, "patch targets parent {parent}, which is not mounted")
}
ViewError::UnknownWidgetType { widget, key, path } => {
write!(f, "no constructor is registered for widget type '{widget}'")?;
if let Some(key) = key {
write!(f, " (key '{key}'")?;
} else {
write!(f, " (keyless")?;
}
if path.is_empty() {
write!(f, ", at the root)")
} else {
write!(f, ", under {})", path.join(" > "))
}
}
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ApplyReport {
pub properties_written: usize,
pub widgets_created: usize,
pub widgets_removed: usize,
pub widgets_moved: usize,
pub errors: Vec<ViewError>,
}
impl ApplyReport {
pub fn is_clean(&self) -> bool {
self.errors.is_empty()
}
pub fn total_removed(&self) -> usize {
self.widgets_removed
}
pub fn failed_nodes(&self) -> Vec<String> {
self.errors
.iter()
.filter_map(|error| match error {
ViewError::UnknownWidgetType { widget, key, path } => {
let mut chain = path.clone();
chain.push(widget.clone());
match key {
Some(key) => Some(format!("{}#{key}", chain.join(" > "))),
None => Some(chain.join(" > ")),
}
}
_ => None,
})
.collect()
}
pub fn failed_count(&self) -> usize {
self.errors
.iter()
.filter(|error| matches!(error, ViewError::UnknownWidgetType { .. }))
.count()
}
pub fn placeholders(&self) -> Vec<Node> {
self.failed_nodes()
.into_iter()
.map(|location| {
Node::new("panel")
.key(format!("__view_error::{location}"))
.child(Node::new("label").prop("text", CapabilityValue::String(location)))
})
.collect()
}
}
pub fn apply(
layout: &mut crate::json::BoundJsonLayout,
patches: &[Patch],
create: &dyn Fn(&Node) -> Option<ObjectId>,
) -> ApplyReport {
apply_with_reservations(layout, patches, create, &mut crate::compat::HashMap::new())
}
pub type OverlayHost<'a> = &'a dyn Fn() -> Option<ObjectId>;
pub fn apply_with_reserved_and_overlay(
layout: &mut crate::json::BoundJsonLayout,
patches: &[Patch],
create: &dyn Fn(&Node) -> Option<ObjectId>,
reserved: &mut crate::compat::HashMap<String, ObjectId>,
overlay: Option<OverlayHost<'_>>,
) -> ApplyReport {
let mut report = ApplyReport::default();
for patch in patches {
apply_one(layout, patch, create, reserved, &mut report, overlay);
}
report
}
pub fn apply_with_reservations(
layout: &mut crate::json::BoundJsonLayout,
patches: &[Patch],
create: &dyn Fn(&Node) -> Option<ObjectId>,
reserved: &mut crate::compat::HashMap<String, ObjectId>,
) -> ApplyReport {
apply_with_reserved_and_overlay(layout, patches, create, reserved, None)
}
fn apply_one(
layout: &mut crate::json::BoundJsonLayout,
patch: &Patch,
create: &dyn Fn(&Node) -> Option<ObjectId>,
reserved: &mut crate::compat::HashMap<String, ObjectId>,
report: &mut ApplyReport,
overlay: Option<OverlayHost<'_>>,
) {
match patch {
Patch::SetProperty { id, name, value } => {
if !is_mounted(layout, *id) {
report.errors.push(ViewError::UnknownWidget { id: *id });
return;
}
match write_property(*id, name, value.clone()) {
Ok(()) => report.properties_written += 1,
Err(reason) => report.errors.push(ViewError::PropertyRefused {
id: *id,
name: name.clone(),
reason,
}),
}
}
Patch::Insert { parent, index, node } => {
if !is_mounted(layout, *parent) {
report.errors.push(ViewError::UnknownParent { parent: *parent });
return;
}
let created =
insert_subtree(layout, *parent, *index, node, create, reserved, report, overlay);
report.widgets_created += created;
}
Patch::Remove { id } => {
if !is_mounted(layout, *id) {
report.errors.push(ViewError::UnknownWidget { id: *id });
return;
}
report.widgets_removed += remove_subtree(layout, *id);
}
Patch::Move { id, parent, index } => {
if !is_mounted(layout, *id) {
report.errors.push(ViewError::UnknownWidget { id: *id });
return;
}
if !is_mounted(layout, *parent) {
report.errors.push(ViewError::UnknownParent { parent: *parent });
return;
}
reparent(layout, *id, *parent, *index);
report.widgets_moved += 1;
}
Patch::Replace { id, parent, index, node } => {
if !is_mounted(layout, *id) {
report.errors.push(ViewError::UnknownWidget { id: *id });
return;
}
if !is_mounted(layout, *parent) {
report.errors.push(ViewError::UnknownParent { parent: *parent });
return;
}
report.widgets_removed += remove_subtree(layout, *id);
let created =
insert_subtree(layout, *parent, *index, node, create, reserved, report, overlay);
report.widgets_created += created;
}
}
}
fn is_mounted(layout: &crate::json::BoundJsonLayout, id: ObjectId) -> bool {
if id == 0 {
return false;
}
layout.root() == Some(id) || layout.parent(id).is_some() || layout.widget_name(id).is_some()
}
fn insert_subtree(
layout: &mut crate::json::BoundJsonLayout,
parent: ObjectId,
index: usize,
node: &Node,
create: &dyn Fn(&Node) -> Option<ObjectId>,
reserved: &mut crate::compat::HashMap<String, ObjectId>,
report: &mut ApplyReport,
overlay: Option<OverlayHost<'_>>,
) -> usize {
if node.widget.eq_ignore_ascii_case("spacer") {
let mut count = 0usize;
for (i, child) in node.children.iter().enumerate() {
count +=
insert_subtree(layout, parent, index + i, child, create, reserved, report, overlay);
}
return count;
}
let reservation_key = node.key.clone().unwrap_or_default();
let reserved_id = reserved.remove(&reservation_key);
let id = match reserved_id.or_else(|| create(node)) {
Some(id) if id != 0 => id,
_ => {
report.errors.push(ViewError::UnknownWidgetType {
widget: node.widget.clone(),
key: node.key.clone(),
path: Vec::new(),
});
return 0;
}
};
let key = node.key.clone().unwrap_or_default();
let host = match node.host {
super::Host::Declared => Some(parent),
super::Host::Overlay => overlay.and_then(|resolve| resolve()).or(Some(parent)),
};
layout.register_node(id, node.widget.clone(), key, host);
if let Some(host) = host {
place_child_at(layout, host, id, index);
}
let mut count = 1usize;
for (i, child) in node.children.iter().enumerate() {
count += insert_subtree(layout, id, i, child, create, reserved, report, overlay);
}
for (name, value) in &node.props {
if super::is_generated_wire_prop(name) {
continue;
}
match write_property(id, name, value.clone()) {
Ok(()) => report.properties_written += 1,
Err(reason) => {
report.errors.push(ViewError::PropertyRefused { id, name: name.clone(), reason })
}
}
}
count
}
fn remove_subtree(layout: &mut crate::json::BoundJsonLayout, id: ObjectId) -> usize {
layout.detach(id).len()
}
fn reparent(
layout: &mut crate::json::BoundJsonLayout,
id: ObjectId,
parent: ObjectId,
index: usize,
) {
let key = layout.node_key(id).unwrap_or_default().to_string();
let widget = layout.widget_name(id).unwrap_or_default().to_string();
layout.register_node(id, widget, key, Some(parent));
place_child_at(layout, parent, id, index);
}
fn place_child_at(
layout: &mut crate::json::BoundJsonLayout,
parent: ObjectId,
child: ObjectId,
index: usize,
) {
layout.move_child_to(parent, child, index);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::widget::capability::CapabilityValue;
fn s(v: &str) -> CapabilityValue {
CapabilityValue::String(v.to_string())
}
struct StubBackend {
next: std::cell::Cell<ObjectId>,
}
impl StubBackend {
fn new() -> Self {
Self { next: std::cell::Cell::new(100) }
}
fn creator(&self) -> impl Fn(&Node) -> Option<ObjectId> + '_ {
move |_node: &Node| {
let id = self.next.get();
self.next.set(id + 1);
Some(id)
}
}
}
fn fixture_root() -> (crate::json::BoundJsonLayout, ObjectId) {
let mut layout = crate::json::BoundJsonLayout::new();
layout.register_node(1, "window", "main", None);
(layout, 1)
}
fn real_creator() -> impl Fn(&Node) -> Option<ObjectId> {
let factory = crate::widget::WidgetFactory::new_with_defaults();
move |node: &Node| {
let widget: Box<dyn crate::widget::Widget> = factory.create(
&node.widget,
crate::core::Rect::new(0, 0, 80, 24),
node.key_str().unwrap_or("anon"),
)?;
crate::widget::runtime::register(widget)
}
}
fn read(id: ObjectId, name: &str) -> Option<CapabilityValue> {
crate::widget::runtime::with_widget(id, |widget| {
crate::widget::capability::properties_trait::widget_property_get(widget, name).ok()
})
.flatten()
}
#[test]
fn both_write_routes_agree_on_what_a_property_write_does() {
let id = crate::widget::runtime::register(
crate::widget::WidgetFactory::new_with_defaults()
.create("label", crate::core::Rect::new(0, 0, 80, 24), "r")
.expect("label is a registered control"),
)
.expect("the runtime accepts a freshly created control");
crate::widget::capability::write_widget_property_by_id(id, "text", s("from-name"))
.expect("a writable property on a live control must accept the write");
assert_eq!(read(id, "text"), Some(s("from-name")));
write_property(id, "text", s("from-node")).expect("the declarative route must succeed");
assert_eq!(
read(id, "text"),
Some(s("from-node")),
"the declarative write and the by-name write must reach the same implementation"
);
}
#[test]
fn a_dropped_property_resets_to_the_schema_default_and_really_lands() {
let (mut layout, root) = fixture_root();
let create = real_creator();
let created = apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("progress_bar").key("bar") }],
&create,
);
assert!(created.is_clean(), "mount failed: {:?}", created.errors);
let bar = layout.child_by_key(Some(root), "bar").expect("bar must be indexed");
let set = apply(
&mut layout,
&[Patch::SetProperty {
id: bar,
name: "text_visible".to_string(),
value: CapabilityValue::Bool(false),
}],
&create,
);
assert!(set.is_clean(), "declaring `text_visible` failed: {:?}", set.errors);
assert_eq!(read(bar, "text_visible"), Some(CapabilityValue::Bool(false)));
let reset = apply(
&mut layout,
&[Patch::SetProperty {
id: bar,
name: "text_visible".to_string(),
value: CapabilityValue::Null,
}],
&create,
);
assert!(
reset.is_clean(),
"a dropped property must not be refused; errors: {:?}",
reset.errors
);
let after = read(bar, "text_visible");
assert_ne!(
after,
Some(CapabilityValue::Bool(false)),
"the reset did not land: the control still holds the dropped value"
);
assert_eq!(
after,
crate::widget::capability::access::default_widget_property_default_value(
crate::widget::WidgetKind::ProgressBar,
"text_visible"
),
"the reset must reach the schema-declared default"
);
}
#[test]
fn a_null_reset_for_an_undeclared_property_is_still_written_as_null() {
let (mut layout, root) = fixture_root();
let create = real_creator();
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("label").key("l") }],
&create,
);
let id = layout.child_by_key(Some(root), "l").expect("label must be indexed");
assert_eq!(resolve_null_reset(id, "tooltip"), Some(CapabilityValue::String(String::new())));
assert_eq!(resolve_null_reset(id, "not_a_real_property"), None);
}
#[test]
fn insert_creates_the_whole_declared_subtree() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let node = Node::new("panel")
.key("card")
.child(Node::new("label").key("title"))
.child(Node::new("button").key("ok"));
let report = apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node }],
&backend.creator(),
);
assert!(report.is_clean(), "errors: {:?}", report.errors);
assert_eq!(report.widgets_created, 3, "panel + label + button");
assert_eq!(layout.children(root).len(), 1);
let panel = layout.child_by_key(Some(root), "card").expect("panel must be indexed");
assert_eq!(layout.children(panel).len(), 2);
assert!(layout.child_by_key(Some(panel), "title").is_some());
}
#[test]
fn insert_honours_the_declared_position() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let create = backend.creator();
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("label").key("first") }],
&create,
);
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 1, node: Node::new("label").key("second") }],
&create,
);
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("label").key("zeroth") }],
&create,
);
let keys: Vec<Option<&str>> =
layout.children(root).iter().map(|&id| layout.node_key(id)).collect();
assert_eq!(keys, [Some("zeroth"), Some("first"), Some("second")]);
}
#[test]
fn insert_with_an_index_past_the_end_appends() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let create = backend.creator();
for key in ["a", "b"] {
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 99, node: Node::new("label").key(key) }],
&create,
);
}
let keys: Vec<Option<&str>> =
layout.children(root).iter().map(|&id| layout.node_key(id)).collect();
assert_eq!(keys, [Some("a"), Some("b")], "an out-of-range index appends, never panics");
}
#[test]
fn removing_a_subtree_also_removes_its_descendants() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let create = backend.creator();
apply(
&mut layout,
&[Patch::Insert {
parent: root,
index: 0,
node: Node::new("panel").key("card").child(Node::new("label").key("title")),
}],
&create,
);
let panel = layout.child_by_key(Some(root), "card").expect("panel");
let report = apply(&mut layout, &[Patch::Remove { id: panel }], &create);
assert!(report.is_clean(), "errors: {:?}", report.errors);
assert_eq!(report.widgets_removed, 2, "the panel and its label");
assert!(layout.children(root).is_empty());
assert_eq!(layout.id("title"), None, "the descendant's name must go too");
}
#[test]
fn a_patch_naming_a_detached_control_is_reported_not_ignored() {
let (mut layout, _root) = fixture_root();
let backend = StubBackend::new();
let report = apply(&mut layout, &[Patch::Remove { id: 999 }], &backend.creator());
assert!(!report.is_clean());
assert_eq!(report.errors, [ViewError::UnknownWidget { id: 999 }]);
}
#[test]
fn an_insert_under_an_unknown_parent_is_reported() {
let (mut layout, _root) = fixture_root();
let backend = StubBackend::new();
let report = apply(
&mut layout,
&[Patch::Insert { parent: 999, index: 0, node: Node::new("label") }],
&backend.creator(),
);
assert_eq!(report.errors, [ViewError::UnknownParent { parent: 999 }]);
assert_eq!(report.widgets_created, 0, "nothing was created");
}
#[test]
fn a_refused_constructor_is_reported_as_an_unknown_widget_type() {
let (mut layout, root) = fixture_root();
let report = apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("no_such_control") }],
&|_n: &Node| None,
);
assert_eq!(
report.errors,
[ViewError::UnknownWidgetType {
widget: "no_such_control".to_string(),
key: None,
path: Vec::new(),
}]
);
}
#[test]
fn move_keeps_the_controls_identity() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let create = backend.creator();
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("panel").key("left") }],
&create,
);
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 1, node: Node::new("panel").key("right") }],
&create,
);
let left = layout.child_by_key(Some(root), "left").expect("left");
let right = layout.child_by_key(Some(root), "right").expect("right");
let report =
apply(&mut layout, &[Patch::Move { id: left, parent: right, index: 0 }], &create);
assert!(report.is_clean(), "errors: {:?}", report.errors);
assert_eq!(report.widgets_moved, 1);
assert_eq!(report.widgets_created, 0, "a move must not create anything");
assert_eq!(report.widgets_removed, 0, "a move must not destroy anything");
assert_eq!(layout.children(root), &[right]);
assert_eq!(layout.children(right), &[left]);
assert_eq!(layout.parent(left), Some(right), "the same id, a new parent");
assert_eq!(layout.node_key(left), Some("left"), "its identity is unchanged");
}
#[test]
fn replace_destroys_one_subtree_and_builds_another() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let create = backend.creator();
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("label").key("slot") }],
&create,
);
let old = layout.child_by_key(Some(root), "slot").expect("slot");
let report = apply(
&mut layout,
&[Patch::Replace {
id: old,
parent: root,
index: 0,
node: Node::new("button").key("slot"),
}],
&create,
);
assert!(report.is_clean(), "errors: {:?}", report.errors);
assert_eq!(report.widgets_removed, 1);
assert_eq!(report.widgets_created, 1);
let fresh = layout.child_by_key(Some(root), "slot").expect("slot");
assert_ne!(fresh, old, "a replace must not reuse the id");
assert_eq!(layout.widget_name(fresh), Some("button"));
assert_eq!(layout.children(root), &[fresh], "it takes the old position");
}
#[test]
fn an_inserted_nodes_declared_properties_are_written() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let node = Node::new("label").key("t").prop("text", s("Hello"));
let report = apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node }],
&backend.creator(),
);
assert_eq!(report.widgets_created, 1);
assert!(
report.properties_written + report.errors.len() >= 1,
"the declared property must be attempted"
);
}
#[test]
fn apply_is_ordered_so_a_later_patch_can_address_an_earlier_insert() {
let (mut layout, root) = fixture_root();
let backend = StubBackend::new();
let create = backend.creator();
apply(
&mut layout,
&[Patch::Insert { parent: root, index: 0, node: Node::new("panel").key("card") }],
&create,
);
let card = layout.child_by_key(Some(root), "card").expect("card");
let follow_up = apply(&mut layout, &[Patch::Remove { id: card }], &create);
assert!(
follow_up.is_clean(),
"the inserted node must be addressable: {:?}",
follow_up.errors
);
}
#[test]
fn an_empty_batch_reports_a_clean_no_op() {
let (mut layout, _root) = fixture_root();
let report = apply(&mut layout, &[], &|_n: &Node| Some(1));
assert!(report.is_clean());
assert_eq!(report.properties_written, 0);
assert_eq!(report.widgets_created, 0);
assert_eq!(report.widgets_removed, 0);
assert_eq!(report.total_removed(), 0);
}
#[test]
fn error_display_names_the_thing_that_went_wrong() {
assert!(ViewError::UnknownWidget { id: 7 }.to_string().contains('7'));
assert!(ViewError::UnknownWidgetType {
widget: "widget_x".into(),
key: None,
path: Vec::new(),
}
.to_string()
.contains("widget_x"));
assert!(ViewError::PropertyRefused {
id: 1,
name: "text".into(),
reason: "not writable".into()
}
.to_string()
.contains("text"));
}
#[test]
fn a_layout_with_no_structure_reports_a_missing_parent_rather_than_mounting() {
let mut layout = crate::json::BoundJsonLayout::new();
layout.register_node(1, "window", "main", None);
layout.detach(1);
let report = apply(
&mut layout,
&[Patch::Insert { parent: 1, index: 0, node: Node::new("label") }],
&|_n: &Node| Some(2),
);
assert!(!report.is_clean(), "an id that was detached must not be treated as mounted");
}
#[test]
fn a_structure_free_layout_is_still_usable_by_a_caller_that_populates_it_first() {
let mut layout = crate::json::BoundJsonLayout::new();
layout.register_node(1, "window", "main", None);
let report = apply(
&mut layout,
&[Patch::Insert {
parent: 1,
index: 0,
node: Node::new("label").key("a").child(Node::new("icon").key("i")),
}],
&|n: &Node| Some(50 + n.node_count() as u64),
);
assert_eq!(report.widgets_created, 2);
assert_eq!(layout.children(1).len(), 1);
let outer = layout.children(1)[0];
assert_eq!(layout.children(outer).len(), 1, "the nested child was attached too");
}
#[test]
fn a_spacer_creates_no_control_but_keeps_its_subtree() {
let mut layout = crate::json::BoundJsonLayout::new();
layout.register_node(1, "window", "main", None);
let factory = crate::widget::WidgetFactory::new_with_defaults();
let create = move |node: &Node| {
let widget = factory.create(
&node.widget,
crate::core::Rect::new(0, 0, 80, 24),
node.key.as_deref().unwrap_or("anon"),
)?;
crate::widget::runtime::register(widget)
};
let report = apply(
&mut layout,
&[Patch::Insert {
parent: 1,
index: 0,
node: Node::new("spacer")
.key("gap")
.child(Node::new("label").key("a").child(Node::new("label").key("nested"))),
}],
&create,
);
assert!(
!report.errors.iter().any(|e| matches!(e, ViewError::UnknownWidgetType { .. })),
"a spacer is a known transparent node, not an unknown widget: {:?}",
report.errors
);
assert_eq!(report.widgets_created, 2, "the spacer's two descendants were created");
let top = layout.children(1);
assert_eq!(top.len(), 1, "the spacer's first child took its place in the window: {top:?}");
assert_eq!(layout.children(top[0]).len(), 1, "and its own child is still beneath it");
assert!(
!top.iter().any(|id| layout.widget_name(*id) == Some("spacer")),
"a pseudo-widget must never be registered as a control"
);
}
#[test]
fn an_empty_spacer_is_a_clean_no_op() {
let mut layout = crate::json::BoundJsonLayout::new();
layout.register_node(1, "window", "main", None);
let factory = crate::widget::WidgetFactory::new_with_defaults();
let create = move |node: &Node| {
factory
.create(&node.widget, crate::core::Rect::new(0, 0, 40, 20), "")
.and_then(crate::widget::runtime::register)
};
let report = apply(
&mut layout,
&[Patch::Insert { parent: 1, index: 0, node: Node::new("spacer") }],
&create,
);
assert!(report.is_clean(), "an empty spacer reports nothing: {:?}", report.errors);
assert_eq!(report.widgets_created, 0, "and creates nothing");
assert!(layout.children(1).is_empty(), "leaving the parent's child list untouched");
}
fn mount_layout(
root: &Node,
create: &dyn Fn(&Node) -> Option<ObjectId>,
) -> (crate::json::BoundJsonLayout, ObjectId) {
let mut layout = crate::json::BoundJsonLayout::new();
let mut register = |node: &Node, parent: Option<ObjectId>| -> Option<ObjectId> {
if node.widget.eq_ignore_ascii_case("spacer") {
return None;
}
let id = create(node)?;
layout.register_node(
id,
node.widget.clone(),
node.key_str().unwrap_or("").to_string(),
parent,
);
Some(id)
};
let root_id = register(root, None).expect("the root must be creatable");
let mut stack = vec![(root.clone(), root_id)];
while let Some((node, id)) = stack.pop() {
for child in &node.children {
if let Some(child_id) = register(child, Some(id)) {
stack.push((child.clone(), child_id));
}
}
}
(layout, root_id)
}
fn child_keys(layout: &crate::json::BoundJsonLayout, parent: ObjectId) -> Vec<String> {
layout
.children(parent)
.iter()
.map(|&id| layout.node_key(id).unwrap_or_default().to_string())
.collect()
}
#[test]
fn a_three_node_cyclic_reorder_reaches_the_declared_order() {
let create = real_creator();
let old = Node::new("window")
.key("root")
.child(Node::new("label").key("a"))
.child(Node::new("label").key("b"))
.child(Node::new("label").key("c"));
let new = Node::new("window")
.key("root")
.child(Node::new("label").key("c"))
.child(Node::new("label").key("a"))
.child(Node::new("label").key("b"));
let (mut layout, root) = mount_layout(&old, &create);
let ids: Vec<ObjectId> = layout.children(root).to_vec();
let id_of = {
let ids = ids.clone();
move |path: &[usize], _index: usize| -> Option<ObjectId> {
if path.is_empty() {
Some(root)
} else {
path.first().and_then(|&i| ids.get(i).copied())
}
}
};
let report = crate::view::diff::diff(&old, &new, &id_of);
let applied = apply(&mut layout, &report.patches, &create);
assert!(applied.is_clean(), "apply errors: {:?}", applied.errors);
assert_eq!(
child_keys(&layout, root),
["c", "a", "b"],
"the applied order must match the declaration"
);
let mut after = layout.children(root).to_vec();
after.sort_unstable();
let mut before = ids.clone();
before.sort_unstable();
assert_eq!(after, before, "a reorder must keep every control's identity");
}
#[test]
fn a_type_change_on_a_keyed_child_replaces_it_in_place() {
let create = real_creator();
let old =
Node::new("window").key("root").child(Node::new("label").key("a").prop("text", s("A")));
let new = Node::new("window")
.key("root")
.child(Node::new("button").key("a").prop("text", s("A")));
let (mut layout, root) = mount_layout(&old, &create);
let old_id = layout.children(root)[0];
let id_of = move |path: &[usize], _index: usize| -> Option<ObjectId> {
if path.is_empty() {
Some(root)
} else if path == [0] {
Some(old_id)
} else {
None
}
};
let report = crate::view::diff::diff(&old, &new, &id_of);
let applied = apply(&mut layout, &report.patches, &create);
assert!(applied.is_clean(), "apply errors: {:?}", applied.errors);
assert_eq!(layout.children(root).len(), 1, "the window keeps exactly one child");
let replaced = layout.children(root)[0];
assert_eq!(layout.widget_name(replaced), Some("button"));
assert_eq!(layout.node_key(replaced), Some("a"));
assert_ne!(
replaced, old_id,
"a type change replaces the control rather than reusing its id"
);
}
#[test]
fn a_keyless_insert_does_not_steal_a_keyed_sibling() {
let create = real_creator();
let old =
Node::new("window").key("root").child(Node::new("label").key("a").prop("text", s("A")));
let new = Node::new("window")
.key("root")
.child(Node::new("label").prop("text", s("new")))
.child(Node::new("label").key("a").prop("text", s("A")));
let (mut layout, root) = mount_layout(&old, &create);
let old_id = layout.children(root)[0];
let id_of = move |path: &[usize], _index: usize| -> Option<ObjectId> {
if path.is_empty() {
Some(root)
} else if path == [0] {
Some(old_id)
} else {
None
}
};
let report = crate::view::diff::diff(&old, &new, &id_of);
let applied = apply(&mut layout, &report.patches, &create);
assert!(applied.is_clean(), "apply errors: {:?}", applied.errors);
let children = layout.children(root).to_vec();
assert_eq!(children.len(), 2, "the keyless insert adds one child, nothing is lost");
assert_eq!(layout.node_key(children[1]), Some("a"));
assert_eq!(children[1], old_id, "the keyed control keeps its id");
assert_ne!(children[0], old_id, "the keyless child must be a new control");
let a_claimants = children.iter().filter(|&&id| layout.node_key(id) == Some("a")).count();
assert_eq!(a_claimants, 1, "a key must be unique among live controls");
}
}