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> {
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()),
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ViewError {
UnknownWidget {
id: ObjectId,
},
PropertyRefused {
id: ObjectId,
name: String,
reason: String,
},
UnknownParent {
parent: ObjectId,
},
UnknownWidgetType {
widget: 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 } => {
write!(f, "no constructor is registered for widget type '{widget}'")
}
}
}
}
#[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 apply(
layout: &mut crate::json::BoundJsonLayout,
patches: &[Patch],
create: &dyn Fn(&Node) -> Option<ObjectId>,
) -> ApplyReport {
let mut report = ApplyReport::default();
for patch in patches {
apply_one(layout, patch, create, &mut report);
}
report
}
fn apply_one(
layout: &mut crate::json::BoundJsonLayout,
patch: &Patch,
create: &dyn Fn(&Node) -> Option<ObjectId>,
report: &mut ApplyReport,
) {
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, report);
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, report);
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>,
report: &mut ApplyReport,
) -> usize {
let id = match create(node) {
Some(id) if id != 0 => id,
_ => {
report.errors.push(ViewError::UnknownWidgetType { widget: node.widget.clone() });
return 0;
}
};
let key = node.key.clone().unwrap_or_default();
layout.register_node(id, node.widget.clone(), key, Some(parent));
place_child_at(layout, parent, id, index);
let mut count = 1usize;
for (i, child) in node.children.iter().enumerate() {
count += insert_subtree(layout, id, i, child, create, report);
}
for (name, value) in &node.props {
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)
}
#[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() }]
);
}
#[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() }
.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");
}
}