use std::collections::BTreeMap;
use zenith_core::{Diagnostic, Document, GroupNode, Node};
use crate::op::Position;
use super::super::{find_node_shared, node_id_of, record_affected, subtree_contains};
use super::finders::{find_container_children_mut, remove_node_by_id, resolve_position};
fn find_common_parent_children_mut<'doc>(
doc: &'doc mut Document,
node_ids: &[String],
) -> Option<&'doc mut Vec<Node>> {
struct Hit {
page_index: usize,
container_id: Option<String>,
}
let hit: Option<Hit> = 'outer: {
for (pi, page) in doc.body.pages.iter().enumerate() {
if node_ids.iter().all(|id| {
page.children
.iter()
.any(|n| node_id_of(n) == Some(id.as_str()))
}) {
break 'outer Some(Hit {
page_index: pi,
container_id: None,
});
}
if let Some(cid) = find_container_with_all_children(&page.children, node_ids) {
break 'outer Some(Hit {
page_index: pi,
container_id: Some(cid),
});
}
}
None
};
let Hit {
page_index,
container_id,
} = hit?;
match container_id {
None => doc.body.pages.get_mut(page_index).map(|p| &mut p.children),
Some(cid) => find_container_children_mut(doc, &cid),
}
}
fn find_container_with_all_children(children: &[Node], node_ids: &[String]) -> Option<String> {
for node in children {
let (container_id, grandchildren) = match node {
Node::Frame(f) => (f.id.as_str(), f.children.as_slice()),
Node::Group(g) => (g.id.as_str(), g.children.as_slice()),
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Footnote(_)
| Node::Toc(_)
| Node::Table(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Unknown(_) => continue,
};
if node_ids.iter().all(|id| {
grandchildren
.iter()
.any(|n| node_id_of(n) == Some(id.as_str()))
}) {
return Some(container_id.to_owned());
}
if let Some(found) = find_container_with_all_children(grandchildren, node_ids) {
return Some(found);
}
}
None
}
pub(in crate::engine) fn apply_group(
node_ids: &[String],
group_id: &str,
doc: &mut Document,
diagnostics: &mut Vec<Diagnostic>,
affected: &mut Vec<String>,
) {
if node_ids.is_empty() {
diagnostics.push(Diagnostic::error(
"tx.invalid_parent",
"group requires at least one node id".to_owned(),
None,
None,
));
return;
}
let children = match find_common_parent_children_mut(doc, node_ids) {
Some(c) => c,
None => {
diagnostics.push(Diagnostic::error(
"tx.invalid_parent",
"group requires all nodes to share a parent".to_owned(),
None,
None,
));
return;
}
};
let mut indices: Vec<usize> = node_ids
.iter()
.filter_map(|id| {
children
.iter()
.position(|n| node_id_of(n) == Some(id.as_str()))
})
.collect();
if indices.len() != node_ids.len() {
diagnostics.push(Diagnostic::error(
"tx.invalid_parent",
"group requires all nodes to share a parent".to_owned(),
None,
None,
));
return;
}
indices.sort_unstable();
let Some(&insert_at) = indices.first() else {
return; };
let group_children: Vec<Node> = indices
.iter()
.filter_map(|&i| children.get(i).cloned())
.collect();
for &i in indices.iter().rev() {
children.remove(i);
}
let insert_at = insert_at.min(children.len());
let group_node = Node::Group(GroupNode {
id: group_id.to_owned(),
name: None,
role: None,
x: None,
y: None,
w: None,
h: None,
opacity: None,
visible: None,
locked: None,
rotate: None,
blend_mode: None,
blur: None,
style: None,
semantic_role: None,
intensity: None,
layer_priority: None,
anchor: None,
anchor_zone: None,
anchor_sibling: None,
anchor_edge: None,
anchor_gap: None,
anchor_parent: None,
children: group_children,
protected_regions: Vec::new(),
editable_param_ids: Vec::new(),
source_span: None,
unknown_props: BTreeMap::new(),
});
children.insert(insert_at, group_node);
record_affected(group_id, affected);
}
pub(in crate::engine) fn apply_ungroup(
group_id: &str,
doc: &mut Document,
diagnostics: &mut Vec<Diagnostic>,
affected: &mut Vec<String>,
) {
struct GroupInfo {
page_index: usize,
has_nonzero_offset: bool,
}
let info: Option<Result<GroupInfo, &'static str>> = {
let mut result = None;
'outer: for (pi, page) in doc.body.pages.iter().enumerate() {
if let Some(node) = find_node_shared(&page.children, group_id) {
let info = match node {
Node::Group(g) => {
let nonzero = |pv: Option<&zenith_core::PropertyValue>| match pv {
Some(zenith_core::PropertyValue::Dimension(d)) => d.value != 0.0,
Some(zenith_core::PropertyValue::TokenRef(_)) => true,
Some(zenith_core::PropertyValue::Literal(_))
| Some(zenith_core::PropertyValue::DataRef(_))
| None => false,
};
let has_offset = nonzero(g.x.as_ref()) || nonzero(g.y.as_ref());
Ok(GroupInfo {
page_index: pi,
has_nonzero_offset: has_offset,
})
}
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Frame(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Footnote(_)
| Node::Toc(_)
| Node::Table(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Unknown(_) => Err("not a group"),
};
result = Some(info);
break 'outer;
}
}
result
};
let info = match info {
None => {
diagnostics.push(Diagnostic::error(
"tx.unknown_node",
format!("node {:?} not found in document", group_id),
None,
Some(group_id.to_owned()),
));
return;
}
Some(Err(reason)) => {
diagnostics.push(Diagnostic::error(
"tx.unsupported_property",
format!("ungroup: {:?} is {}", group_id, reason),
None,
Some(group_id.to_owned()),
));
return;
}
Some(Ok(info)) => info,
};
if info.has_nonzero_offset {
diagnostics.push(Diagnostic::advisory(
"tx.noop",
format!(
"ungroup: group {:?} has a non-zero x/y offset; v0 does not \
apply the offset to children on ungroup — child positions may \
shift visually",
group_id
),
None,
Some(group_id.to_owned()),
));
}
let Some(page) = doc.body.pages.get_mut(info.page_index) else {
return; };
splice_ungroup(&mut page.children, group_id);
record_affected(group_id, affected);
}
fn splice_ungroup(children: &mut Vec<Node>, group_id: &str) -> bool {
if let Some(i) = children
.iter()
.position(|n| node_id_of(n) == Some(group_id))
{
let group_children = match children.get(i) {
Some(Node::Group(g)) => g.children.clone(),
Some(Node::Rect(_))
| Some(Node::Ellipse(_))
| Some(Node::Line(_))
| Some(Node::Text(_))
| Some(Node::Code(_))
| Some(Node::Frame(_))
| Some(Node::Image(_))
| Some(Node::Polygon(_))
| Some(Node::Polyline(_))
| Some(Node::Instance(_))
| Some(Node::Field(_))
| Some(Node::Footnote(_))
| Some(Node::Toc(_))
| Some(Node::Table(_))
| Some(Node::Shape(_))
| Some(Node::Connector(_))
| Some(Node::Pattern(_))
| Some(Node::Chart(_))
| Some(Node::Unknown(_))
| None => return false,
};
children.remove(i);
for (offset, child) in group_children.into_iter().enumerate() {
children.insert(i + offset, child);
}
return true;
}
for child in children.iter_mut() {
let grandchildren = match child {
Node::Frame(f) => &mut f.children,
Node::Group(g) => &mut g.children,
Node::Rect(_)
| Node::Ellipse(_)
| Node::Line(_)
| Node::Text(_)
| Node::Code(_)
| Node::Image(_)
| Node::Polygon(_)
| Node::Polyline(_)
| Node::Instance(_)
| Node::Field(_)
| Node::Footnote(_)
| Node::Toc(_)
| Node::Table(_)
| Node::Shape(_)
| Node::Connector(_)
| Node::Pattern(_)
| Node::Chart(_)
| Node::Unknown(_) => continue,
};
if splice_ungroup(grandchildren, group_id) {
return true;
}
}
false
}
pub(in crate::engine) fn apply_reparent(
node_id: &str,
new_parent: &str,
position: &Position,
doc: &mut Document,
diagnostics: &mut Vec<Diagnostic>,
affected: &mut Vec<String>,
) {
let node_page_index = doc.body.pages.iter().enumerate().find_map(|(pi, page)| {
if page.children.iter().any(|n| subtree_contains(n, node_id)) {
Some(pi)
} else {
None
}
});
let pi = match node_page_index {
Some(pi) => pi,
None => {
diagnostics.push(Diagnostic::error(
"tx.unknown_node",
format!("node {:?} not found in document", node_id),
None,
Some(node_id.to_owned()),
));
return;
}
};
{
let page = match doc.body.pages.get(pi) {
Some(p) => p,
None => return, };
if let Some(node_ref) = find_node_shared(&page.children, node_id)
&& subtree_contains(node_ref, new_parent)
{
diagnostics.push(Diagnostic::error(
"tx.invalid_parent",
format!(
"cannot reparent {:?} into {:?}: new_parent is within \
the node's own subtree",
node_id, new_parent
),
None,
Some(new_parent.to_owned()),
));
return;
}
}
let node = {
let page = match doc.body.pages.get_mut(pi) {
Some(p) => p,
None => return, };
match remove_node_by_id(&mut page.children, node_id) {
Some(n) => n,
None => {
diagnostics.push(Diagnostic::error(
"tx.unknown_node",
format!("node {:?} disappeared during reparent", node_id),
None,
Some(node_id.to_owned()),
));
return;
}
}
};
let new_children = match find_container_children_mut(doc, new_parent) {
Some(c) => c,
None => {
if let Some(page) = doc.body.pages.get_mut(pi) {
page.children.push(node);
}
diagnostics.push(Diagnostic::error(
"tx.invalid_parent",
format!(
"no container with id {:?} (new_parent must be a page, group, or frame)",
new_parent
),
None,
Some(new_parent.to_owned()),
));
return;
}
};
let idx = match resolve_position(position, new_children, new_parent, diagnostics) {
Some(i) => i,
None => {
if let Some(page) = doc.body.pages.get_mut(pi) {
page.children.push(node);
}
return;
}
};
new_children.insert(idx, node);
record_affected(node_id, affected);
}