feat(orchestrator): mobile-artboard layout cleanup passes
Geometry-driven post-passes on generated mobile screens: equalize side-by-side card heights, collapse a bloating fill_container content section on a fixed-height artboard, pad a clipping horizontal scroll row so child strokes aren't shaved on any side, expand a collapsed absolute (layout:none) image wrapper to its image height, and collapse nested double horizontal padding. Wired into run_cleanup_passes + loop_finalize.
This commit is contained in:
parent
ea27689d0e
commit
8a9919579e
|
|
@ -11,14 +11,14 @@ use crate::cleanup_typography::repair_overbold_text_hierarchy;
|
|||
use crate::plan::OrchestratorPlan;
|
||||
use crate::types::DocSink;
|
||||
use jian_ops_schema::node::{
|
||||
container::{ContainerProps, Padding},
|
||||
container::{ContainerProps, LayoutMode, Padding},
|
||||
PenNode,
|
||||
};
|
||||
use jian_ops_schema::sizing::{SizingBehavior, SizingKeyword};
|
||||
use jian_ops_schema::style::PenEffect;
|
||||
use op_editor_core::{
|
||||
first_fill_type, first_solid_fill_hex, EditorCommand, EditorState, FillType, LayoutPropValue,
|
||||
NodeId, PenNodeExt,
|
||||
fills::node_stroke_width, first_fill_type, first_solid_fill_hex, EditorCommand, EditorState,
|
||||
FillType, LayoutPropValue, NodeId, PenNodeExt,
|
||||
};
|
||||
|
||||
#[path = "cleanup_desktop_dashboard.rs"]
|
||||
|
|
@ -110,6 +110,126 @@ pub(crate) fn collapse_nested_horizontal_padding_for_all_roots(sink: &mut dyn Do
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_absolute_container_to_children_for_all_roots(sink: &mut dyn DocSink) {
|
||||
let root_ids: Vec<String> = sink
|
||||
.state()
|
||||
.active_children()
|
||||
.iter()
|
||||
.map(|node| node.id_str().to_string())
|
||||
.collect();
|
||||
for root_id in root_ids {
|
||||
expand_absolute_container_to_children(sink, &root_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn pad_clipping_horizontal_row_for_stroke_for_all_roots(sink: &mut dyn DocSink) {
|
||||
let root_ids: Vec<String> = sink
|
||||
.state()
|
||||
.active_children()
|
||||
.iter()
|
||||
.map(|node| node.id_str().to_string())
|
||||
.collect();
|
||||
for root_id in root_ids {
|
||||
pad_clipping_horizontal_row_for_stroke(sink, &root_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn collapse_fill_container_content_sections_for_all_roots(sink: &mut dyn DocSink) {
|
||||
let root_ids: Vec<String> = sink
|
||||
.state()
|
||||
.active_children()
|
||||
.iter()
|
||||
.map(|node| node.id_str().to_string())
|
||||
.collect();
|
||||
for root_id in root_ids {
|
||||
collapse_fill_container_content_sections(sink, &root_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn equalize_horizontal_card_heights_for_all_roots(sink: &mut dyn DocSink) {
|
||||
let root_ids: Vec<String> = sink
|
||||
.state()
|
||||
.active_children()
|
||||
.iter()
|
||||
.map(|node| node.id_str().to_string())
|
||||
.collect();
|
||||
for root_id in root_ids {
|
||||
equalize_horizontal_card_heights(sink, &root_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn pad_clipping_horizontal_row_for_stroke(sink: &mut dyn DocSink, root_id: &str) {
|
||||
let repairs: Vec<ClipRowStrokePaddingRepair> = {
|
||||
let Some(root) = find_root(sink.state(), root_id) else {
|
||||
return;
|
||||
};
|
||||
let mut repairs = Vec::new();
|
||||
collect_clip_row_stroke_padding_repairs(root, &mut repairs);
|
||||
repairs
|
||||
};
|
||||
|
||||
for repair in repairs {
|
||||
sink.apply(EditorCommand::SetNodeLayoutProp {
|
||||
node_id: repair.node_id,
|
||||
property: "padding".to_string(),
|
||||
value: LayoutPropValue::NumberArray(vec![
|
||||
repair.padding[0],
|
||||
repair.padding[1],
|
||||
repair.padding[2],
|
||||
repair.padding[3],
|
||||
]),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ClipRowStrokePaddingRepair {
|
||||
node_id: NodeId,
|
||||
padding: [f64; 4],
|
||||
}
|
||||
|
||||
fn collect_clip_row_stroke_padding_repairs(
|
||||
node: &PenNode,
|
||||
repairs: &mut Vec<ClipRowStrokePaddingRepair>,
|
||||
) {
|
||||
if let Some(repair) = clip_row_stroke_padding_repair(node) {
|
||||
repairs.push(repair);
|
||||
}
|
||||
if let Some(children) = node.children() {
|
||||
for child in children {
|
||||
collect_clip_row_stroke_padding_repairs(child, repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_row_stroke_padding_repair(node: &PenNode) -> Option<ClipRowStrokePaddingRepair> {
|
||||
let props = frame_container_props(node)?;
|
||||
if props.layout.as_ref() != Some(&LayoutMode::Horizontal) || props.clip_content != Some(true) {
|
||||
return None;
|
||||
}
|
||||
let max_stroke = node
|
||||
.children()?
|
||||
.iter()
|
||||
.filter_map(node_stroke_width)
|
||||
.max_by(f64::total_cmp)?;
|
||||
let mut padding = props
|
||||
.padding
|
||||
.as_ref()
|
||||
.map(padding_sides)
|
||||
.unwrap_or([0.0, 0.0, 0.0, 0.0]);
|
||||
let stroke_padding = max_stroke.ceil();
|
||||
if padding.iter().all(|side| *side >= stroke_padding) {
|
||||
return None;
|
||||
}
|
||||
for side in &mut padding {
|
||||
*side = side.max(stroke_padding);
|
||||
}
|
||||
Some(ClipRowStrokePaddingRepair {
|
||||
node_id: NodeId::new(node.id_str().to_string()),
|
||||
padding,
|
||||
})
|
||||
}
|
||||
|
||||
/// Mobile root-level bottom-nav dedupe. Weak-model Chinese prompts can produce
|
||||
/// both a localized bottom nav section and an English normalized bottom nav.
|
||||
/// Keep the bottom-most/last top-level nav and remove earlier duplicates.
|
||||
|
|
@ -151,6 +271,211 @@ fn remove_duplicate_bottom_nav_sections(sink: &mut dyn DocSink, root_id: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Fixed-height vertical mobile artboards must keep direct content sections
|
||||
/// content-driven; fill-height direct children can consume all leftover space
|
||||
/// when a sibling subtask emits zero nodes.
|
||||
fn collapse_fill_container_content_sections(sink: &mut dyn DocSink, root_id: &str) {
|
||||
let repairs: Vec<NodeId> = {
|
||||
let Some(root) = find_root(sink.state(), root_id) else {
|
||||
return;
|
||||
};
|
||||
if !is_fixed_height_vertical_root(root) {
|
||||
return;
|
||||
}
|
||||
let Some(children) = root.children() else {
|
||||
return;
|
||||
};
|
||||
children
|
||||
.iter()
|
||||
.filter(|child| is_fill_height_content_frame(child))
|
||||
.map(|child| NodeId::new(child.id_str().to_string()))
|
||||
.collect()
|
||||
};
|
||||
|
||||
for node_id in repairs {
|
||||
sink.apply(EditorCommand::SetNodeLayoutProp {
|
||||
node_id,
|
||||
property: "height".to_string(),
|
||||
value: LayoutPropValue::Keyword("fit_content".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn is_fixed_height_vertical_root(root: &PenNode) -> bool {
|
||||
let Some(props) = frame_container_props(root) else {
|
||||
return false;
|
||||
};
|
||||
props
|
||||
.layout
|
||||
.as_ref()
|
||||
.is_some_and(|layout| layout == &LayoutMode::Vertical)
|
||||
&& matches!(props.height.as_ref(), Some(SizingBehavior::Number(_)))
|
||||
}
|
||||
|
||||
fn is_fill_height_content_frame(node: &PenNode) -> bool {
|
||||
let Some(props) = frame_container_props(node) else {
|
||||
return false;
|
||||
};
|
||||
matches!(
|
||||
props.height.as_ref(),
|
||||
Some(SizingBehavior::Keyword(SizingKeyword::FillContainer))
|
||||
) && node
|
||||
.children()
|
||||
.map(|children| !children.is_empty())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn equalize_horizontal_card_heights(sink: &mut dyn DocSink, root_id: &str) {
|
||||
let repairs: Vec<CardHeightRepair> = {
|
||||
let Some(root) = find_root(sink.state(), root_id) else {
|
||||
return;
|
||||
};
|
||||
let mut repairs = Vec::new();
|
||||
collect_horizontal_card_height_repairs(root, &mut repairs);
|
||||
repairs
|
||||
};
|
||||
|
||||
for repair in repairs {
|
||||
sink.apply(EditorCommand::SetNodeLayoutProp {
|
||||
node_id: repair.card_id,
|
||||
property: "height".to_string(),
|
||||
value: LayoutPropValue::Keyword("fill_container".to_string()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CardHeightRepair {
|
||||
card_id: NodeId,
|
||||
}
|
||||
|
||||
fn collect_horizontal_card_height_repairs(node: &PenNode, repairs: &mut Vec<CardHeightRepair>) {
|
||||
if let Some(card_repairs) = horizontal_card_height_repairs(node) {
|
||||
repairs.extend(card_repairs);
|
||||
}
|
||||
if let Some(children) = node.children() {
|
||||
for child in children {
|
||||
collect_horizontal_card_height_repairs(child, repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_card_height_repairs(node: &PenNode) -> Option<Vec<CardHeightRepair>> {
|
||||
let props = frame_container_props(node)?;
|
||||
if props.layout.as_ref() != Some(&LayoutMode::Horizontal) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut cards = Vec::new();
|
||||
for child in node.children()? {
|
||||
let Some(child_props) = frame_container_props(child) else {
|
||||
continue;
|
||||
};
|
||||
if child
|
||||
.children()
|
||||
.map(|children| children.is_empty())
|
||||
.unwrap_or(true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if !width_is_fill_container(child_props) {
|
||||
return None;
|
||||
}
|
||||
if matches!(
|
||||
child_props.height.as_ref(),
|
||||
Some(SizingBehavior::Keyword(SizingKeyword::FitContent))
|
||||
) {
|
||||
cards.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
(cards.len() >= 2).then(|| {
|
||||
cards
|
||||
.into_iter()
|
||||
.map(|card| CardHeightRepair {
|
||||
card_id: NodeId::new(card.id_str().to_string()),
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
fn expand_absolute_container_to_children(sink: &mut dyn DocSink, root_id: &str) {
|
||||
let repairs: Vec<AbsoluteContainerRepair> = {
|
||||
let Some(root) = find_root(sink.state(), root_id) else {
|
||||
return;
|
||||
};
|
||||
let mut repairs = Vec::new();
|
||||
collect_absolute_container_repairs(root, &mut repairs);
|
||||
repairs
|
||||
};
|
||||
|
||||
for repair in repairs {
|
||||
sink.apply(EditorCommand::UpdateNode {
|
||||
node_id: repair.node_id,
|
||||
x: None,
|
||||
y: None,
|
||||
width: None,
|
||||
height: Some(repair.height.ceil() as i32),
|
||||
name: None,
|
||||
fill_hex: None,
|
||||
page_id: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct AbsoluteContainerRepair {
|
||||
node_id: NodeId,
|
||||
height: f64,
|
||||
}
|
||||
|
||||
fn collect_absolute_container_repairs(node: &PenNode, repairs: &mut Vec<AbsoluteContainerRepair>) {
|
||||
if let Some(repair) = absolute_container_repair(node) {
|
||||
repairs.push(repair);
|
||||
}
|
||||
if let Some(children) = node.children() {
|
||||
for child in children {
|
||||
collect_absolute_container_repairs(child, repairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn absolute_container_repair(node: &PenNode) -> Option<AbsoluteContainerRepair> {
|
||||
let props = frame_container_props(node)?;
|
||||
if props.layout.as_ref() != Some(&LayoutMode::None)
|
||||
|| !matches!(
|
||||
props.height.as_ref(),
|
||||
Some(SizingBehavior::Keyword(SizingKeyword::FitContent))
|
||||
)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let children = node.children()?;
|
||||
if !children
|
||||
.iter()
|
||||
.any(|child| matches!(child, PenNode::Image(_)))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let height = children
|
||||
.iter()
|
||||
.filter_map(|child| {
|
||||
if !matches!(child, PenNode::Image(_)) {
|
||||
return None;
|
||||
}
|
||||
child
|
||||
.height_px()
|
||||
.map(|height| child.base().y.unwrap_or(0.0) + height)
|
||||
})
|
||||
.max_by(f64::total_cmp)?;
|
||||
(height > 0.0).then(|| AbsoluteContainerRepair {
|
||||
node_id: NodeId::new(node.id_str().to_string()),
|
||||
height,
|
||||
})
|
||||
}
|
||||
|
||||
/// Collapse `section > transparent fill-width wrapper` double horizontal
|
||||
/// padding while preserving all vertical padding.
|
||||
fn collapse_nested_horizontal_padding(sink: &mut dyn DocSink, root_id: &str) {
|
||||
|
|
@ -1017,6 +1342,10 @@ pub fn run_cleanup_passes(sink: &mut dyn DocSink, plan: &OrchestratorPlan, root_
|
|||
remove_duplicate_status_bars(sink, rid);
|
||||
remove_duplicate_bottom_nav_sections(sink, rid);
|
||||
collapse_nested_horizontal_padding(sink, rid);
|
||||
expand_absolute_container_to_children(sink, rid);
|
||||
pad_clipping_horizontal_row_for_stroke(sink, rid);
|
||||
equalize_horizontal_card_heights(sink, rid);
|
||||
collapse_fill_container_content_sections(sink, rid);
|
||||
repair_light_mobile_nav_surfaces(sink, rid);
|
||||
repair_mobile_content_sections(sink, rid);
|
||||
cleanup_mobile_chrome::repair_mobile_structural_chrome(sink, rid);
|
||||
|
|
@ -1171,6 +1500,22 @@ mod tests_mobile_bottom_nav_dedup;
|
|||
#[path = "cleanup_nested_horizontal_padding_tests.rs"]
|
||||
mod tests_nested_horizontal_padding;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cleanup_absolute_container_tests.rs"]
|
||||
mod tests_absolute_container;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cleanup_fill_container_content_tests.rs"]
|
||||
mod tests_fill_container_content;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cleanup_clip_row_stroke_tests.rs"]
|
||||
mod tests_clip_row_stroke;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cleanup_card_height_equalize_tests.rs"]
|
||||
mod tests_card_height_equalize;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "cleanup_desktop_dashboard_tests.rs"]
|
||||
mod tests_desktop_dashboard;
|
||||
|
|
|
|||
289
crates/op-orchestrator/src/cleanup_absolute_container_tests.rs
Normal file
289
crates/op-orchestrator/src/cleanup_absolute_container_tests.rs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
use super::*;
|
||||
use crate::test_support::VecDocSink;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn insert_tree(sink: &mut VecDocSink, json: &str) {
|
||||
let tree: PenNode = serde_json::from_str(json).expect("test tree json");
|
||||
sink.state.apply(EditorCommand::InsertAuthoredSubtree {
|
||||
nodes: vec![tree],
|
||||
parent_id: NodeId::NONE,
|
||||
page_id: None,
|
||||
});
|
||||
sink.applied.clear();
|
||||
}
|
||||
|
||||
fn find_node<'a>(node: &'a PenNode, id: &str) -> Option<&'a PenNode> {
|
||||
if node.id_str() == id {
|
||||
return Some(node);
|
||||
}
|
||||
node.children()?
|
||||
.iter()
|
||||
.find_map(|child| find_node(child, id))
|
||||
}
|
||||
|
||||
fn find_active_node<'a>(sink: &'a VecDocSink, id: &str) -> &'a PenNode {
|
||||
sink.state
|
||||
.active_children()
|
||||
.iter()
|
||||
.find_map(|node| find_node(node, id))
|
||||
.expect("node exists")
|
||||
}
|
||||
|
||||
fn node_json(sink: &VecDocSink, id: &str) -> Value {
|
||||
serde_json::to_value(find_active_node(sink, id)).expect("serialize node")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_fit_content_wrapper_expands_to_image_height() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Root",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "image-wrapper",
|
||||
"name": "Image Wrapper",
|
||||
"width": 210,
|
||||
"height": "fit_content",
|
||||
"layout": "none",
|
||||
"children": [
|
||||
{
|
||||
"type": "image",
|
||||
"id": "product-image",
|
||||
"name": "Product Image",
|
||||
"src": "",
|
||||
"width": 210,
|
||||
"height": 240
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
|
||||
expand_absolute_container_to_children(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "image-wrapper")["height"], json!(240.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_wrapper_with_y_offset_uses_y_plus_height() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Root",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "image-wrapper",
|
||||
"name": "Image Wrapper",
|
||||
"width": 210,
|
||||
"height": "fit_content",
|
||||
"layout": "none",
|
||||
"children": [
|
||||
{
|
||||
"type": "image",
|
||||
"id": "product-image",
|
||||
"name": "Product Image",
|
||||
"src": "",
|
||||
"y": 10,
|
||||
"width": 210,
|
||||
"height": 240
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
|
||||
expand_absolute_container_to_children(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "image-wrapper")["height"], json!(250.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_none_container_without_image_child_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Root",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "guest-counter",
|
||||
"name": "Guest Counter",
|
||||
"width": 112,
|
||||
"height": "fit_content",
|
||||
"layout": "none",
|
||||
"children": [
|
||||
{
|
||||
"type": "icon_font",
|
||||
"id": "minus-icon",
|
||||
"name": "Minus Icon",
|
||||
"iconFontName": "minus",
|
||||
"y": 200,
|
||||
"width": 20,
|
||||
"height": 20
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"id": "guest-count",
|
||||
"name": "Guest Count",
|
||||
"content": "2",
|
||||
"y": 212,
|
||||
"width": 16,
|
||||
"height": 20
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "guest-counter");
|
||||
|
||||
expand_absolute_container_to_children(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "guest-counter"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flex_fit_content_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Root",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "image-wrapper",
|
||||
"name": "Image Wrapper",
|
||||
"width": 210,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "image",
|
||||
"id": "product-image",
|
||||
"name": "Product Image",
|
||||
"src": "",
|
||||
"width": 210,
|
||||
"height": 240
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "image-wrapper");
|
||||
|
||||
expand_absolute_container_to_children(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "image-wrapper"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_wrapper_with_numeric_height_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Root",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "status-bar",
|
||||
"name": "Status Bar",
|
||||
"width": "fill_container",
|
||||
"height": 62,
|
||||
"layout": "none",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "status-content",
|
||||
"name": "Status Content",
|
||||
"width": 210,
|
||||
"height": 240,
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "status-bar");
|
||||
|
||||
expand_absolute_container_to_children(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "status-bar"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn absolute_wrapper_only_fill_children_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Root",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "image-wrapper",
|
||||
"name": "Image Wrapper",
|
||||
"width": 210,
|
||||
"height": "fit_content",
|
||||
"layout": "none",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "fill-child",
|
||||
"name": "Fill Child",
|
||||
"width": "fill_container",
|
||||
"height": "fill_container",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "image-wrapper");
|
||||
|
||||
expand_absolute_container_to_children(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "image-wrapper"), before);
|
||||
}
|
||||
288
crates/op-orchestrator/src/cleanup_card_height_equalize_tests.rs
Normal file
288
crates/op-orchestrator/src/cleanup_card_height_equalize_tests.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
use super::*;
|
||||
use crate::test_support::VecDocSink;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn insert_tree(sink: &mut VecDocSink, json: &str) {
|
||||
let tree: PenNode = serde_json::from_str(json).expect("test tree json");
|
||||
sink.state.apply(EditorCommand::InsertAuthoredSubtree {
|
||||
nodes: vec![tree],
|
||||
parent_id: NodeId::NONE,
|
||||
page_id: None,
|
||||
});
|
||||
sink.applied.clear();
|
||||
}
|
||||
|
||||
fn find_node<'a>(node: &'a PenNode, id: &str) -> Option<&'a PenNode> {
|
||||
if node.id_str() == id {
|
||||
return Some(node);
|
||||
}
|
||||
node.children()?
|
||||
.iter()
|
||||
.find_map(|child| find_node(child, id))
|
||||
}
|
||||
|
||||
fn find_active_node<'a>(sink: &'a VecDocSink, id: &str) -> &'a PenNode {
|
||||
sink.state
|
||||
.active_children()
|
||||
.iter()
|
||||
.find_map(|node| find_node(node, id))
|
||||
.expect("node exists")
|
||||
}
|
||||
|
||||
fn node_json(sink: &VecDocSink, id: &str) -> Value {
|
||||
serde_json::to_value(find_active_node(sink, id)).expect("serialize node")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_fill_width_fit_height_cards_in_row_become_fill_height() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Page",
|
||||
"width": 390,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "deals-row",
|
||||
"name": "Deals of the Week",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "card-a",
|
||||
"name": "Deal Card A",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "card-a-title", "content": "Weekend Bento"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "card-b",
|
||||
"name": "Deal Card B",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "card-b-title", "content": "Family feast with extra dessert"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
|
||||
equalize_horizontal_card_heights(&mut sink, "root");
|
||||
|
||||
assert_eq!(
|
||||
node_json(&sink, "card-a")["height"],
|
||||
json!("fill_container")
|
||||
);
|
||||
assert_eq!(
|
||||
node_json(&sink, "card-b")["height"],
|
||||
json!("fill_container")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_card_row_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Page",
|
||||
"width": 390,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "deals-row",
|
||||
"name": "Deals of the Week",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "card-a",
|
||||
"name": "Deal Card A",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "card-a-title", "content": "Weekend Bento"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "root");
|
||||
|
||||
equalize_horizontal_card_heights(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "root"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_width_row_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Page",
|
||||
"width": 390,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "deals-row",
|
||||
"name": "Deals of the Week",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "card-a",
|
||||
"name": "Deal Card A",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "card-a-title", "content": "Weekend Bento"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "card-b",
|
||||
"name": "Deal Card B",
|
||||
"width": 180,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "card-b-title", "content": "Family feast"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "root");
|
||||
|
||||
equalize_horizontal_card_heights(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "root"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_container_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Page",
|
||||
"width": 390,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "card-a",
|
||||
"name": "Deal Card A",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "card-a-title", "content": "Weekend Bento"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "card-b",
|
||||
"name": "Deal Card B",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "card-b-title", "content": "Family feast"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "root");
|
||||
|
||||
equalize_horizontal_card_heights(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "root"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_frame_siblings_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_tree(
|
||||
&mut sink,
|
||||
r##"{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Page",
|
||||
"width": 390,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "deals-row",
|
||||
"name": "Deals of the Week",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "spacer-a",
|
||||
"name": "Spacer A",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "spacer-b",
|
||||
"name": "Spacer B",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"##,
|
||||
);
|
||||
let before = node_json(&sink, "root");
|
||||
|
||||
equalize_horizontal_card_heights(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "root"), before);
|
||||
}
|
||||
171
crates/op-orchestrator/src/cleanup_clip_row_stroke_tests.rs
Normal file
171
crates/op-orchestrator/src/cleanup_clip_row_stroke_tests.rs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
use super::*;
|
||||
use crate::test_support::VecDocSink;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn insert_tree(sink: &mut VecDocSink, json: &str) {
|
||||
let tree: PenNode = serde_json::from_str(json).expect("test tree json");
|
||||
sink.state.apply(EditorCommand::InsertAuthoredSubtree {
|
||||
nodes: vec![tree],
|
||||
parent_id: NodeId::NONE,
|
||||
page_id: None,
|
||||
});
|
||||
sink.applied.clear();
|
||||
}
|
||||
|
||||
fn find_node<'a>(node: &'a PenNode, id: &str) -> Option<&'a PenNode> {
|
||||
if node.id_str() == id {
|
||||
return Some(node);
|
||||
}
|
||||
node.children()?
|
||||
.iter()
|
||||
.find_map(|child| find_node(child, id))
|
||||
}
|
||||
|
||||
fn find_active_node<'a>(sink: &'a VecDocSink, id: &str) -> &'a PenNode {
|
||||
sink.state
|
||||
.active_children()
|
||||
.iter()
|
||||
.find_map(|node| find_node(node, id))
|
||||
.expect("node exists")
|
||||
}
|
||||
|
||||
fn node_json(sink: &VecDocSink, id: &str) -> Value {
|
||||
serde_json::to_value(find_active_node(sink, id)).expect("serialize node")
|
||||
}
|
||||
|
||||
fn insert_date_scroller(
|
||||
sink: &mut VecDocSink,
|
||||
row_layout: &str,
|
||||
clip_content: bool,
|
||||
row_padding: &str,
|
||||
child_stroke: &str,
|
||||
) {
|
||||
insert_tree(
|
||||
sink,
|
||||
&format!(
|
||||
r##"{{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Mobile Root",
|
||||
"width": 390,
|
||||
"height": 844,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{{
|
||||
"type": "frame",
|
||||
"id": "section",
|
||||
"name": "Upcoming",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{{
|
||||
"type": "frame",
|
||||
"id": "date-row",
|
||||
"name": "Date Scroller",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "{row_layout}",
|
||||
"clipContent": {clip_content},
|
||||
"padding": {row_padding},
|
||||
"children": [
|
||||
{{
|
||||
"type": "frame",
|
||||
"id": "date-chip",
|
||||
"name": "Tue 14",
|
||||
"width": 48,
|
||||
"height": 60,
|
||||
"layout": "vertical",
|
||||
{child_stroke}
|
||||
"children": []
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}
|
||||
]
|
||||
}}"##
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipping_horizontal_row_with_stroked_chip_gets_stroke_padding_on_all_sides() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_date_scroller(
|
||||
&mut sink,
|
||||
"horizontal",
|
||||
true,
|
||||
"[0, 0, 0, 0]",
|
||||
r##""stroke": {"thickness": 1, "fill": [{"type": "solid", "color": "#E5E7EB"}]},"##,
|
||||
);
|
||||
|
||||
pad_clipping_horizontal_row_for_stroke(&mut sink, "root");
|
||||
|
||||
assert_eq!(
|
||||
node_json(&sink, "date-row")["padding"],
|
||||
json!([1.0, 1.0, 1.0, 1.0])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_clipping_row_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_date_scroller(
|
||||
&mut sink,
|
||||
"horizontal",
|
||||
false,
|
||||
"[0, 12]",
|
||||
r##""stroke": {"thickness": 1, "fill": [{"type": "solid", "color": "#E5E7EB"}]},"##,
|
||||
);
|
||||
let before = node_json(&sink, "date-row");
|
||||
|
||||
pad_clipping_horizontal_row_for_stroke(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "date-row"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_without_stroked_children_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_date_scroller(&mut sink, "horizontal", true, "[0, 12]", "");
|
||||
let before = node_json(&sink, "date-row");
|
||||
|
||||
pad_clipping_horizontal_row_for_stroke(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "date-row"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_with_sufficient_vertical_padding_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_date_scroller(
|
||||
&mut sink,
|
||||
"horizontal",
|
||||
true,
|
||||
"[2, 12, 2, 12]",
|
||||
r##""stroke": {"thickness": 1, "fill": [{"type": "solid", "color": "#E5E7EB"}]},"##,
|
||||
);
|
||||
let before = node_json(&sink, "date-row");
|
||||
|
||||
pad_clipping_horizontal_row_for_stroke(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "date-row"), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_container_untouched() {
|
||||
let mut sink = VecDocSink::new();
|
||||
insert_date_scroller(
|
||||
&mut sink,
|
||||
"vertical",
|
||||
true,
|
||||
"[0, 12]",
|
||||
r##""stroke": {"thickness": 1, "fill": [{"type": "solid", "color": "#E5E7EB"}]},"##,
|
||||
);
|
||||
let before = node_json(&sink, "date-row");
|
||||
|
||||
pad_clipping_horizontal_row_for_stroke(&mut sink, "root");
|
||||
|
||||
assert_eq!(node_json(&sink, "date-row"), before);
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
use super::*;
|
||||
use crate::plan::{OrchestratorPlan, RootFrameSpec};
|
||||
use crate::test_support::VecDocSink;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
fn plan() -> OrchestratorPlan {
|
||||
OrchestratorPlan {
|
||||
root_frame: RootFrameSpec {
|
||||
id: "root".into(),
|
||||
name: "Mobile Food App".into(),
|
||||
width: 375.0,
|
||||
height: 812.0,
|
||||
layout: None,
|
||||
gap: None,
|
||||
padding: None,
|
||||
fill: None,
|
||||
},
|
||||
subtasks: vec![],
|
||||
style_guide_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_root(value: Value) -> VecDocSink {
|
||||
let mut sink = VecDocSink::new();
|
||||
let root: PenNode = serde_json::from_value(value).expect("root json");
|
||||
sink.state.apply(EditorCommand::InsertAuthoredSubtree {
|
||||
nodes: vec![root],
|
||||
parent_id: NodeId::NONE,
|
||||
page_id: None,
|
||||
});
|
||||
sink.applied.clear();
|
||||
sink
|
||||
}
|
||||
|
||||
fn find_node<'a>(node: &'a PenNode, id: &str) -> Option<&'a PenNode> {
|
||||
if node.id_str() == id {
|
||||
return Some(node);
|
||||
}
|
||||
node.children()?
|
||||
.iter()
|
||||
.find_map(|child| find_node(child, id))
|
||||
}
|
||||
|
||||
fn active_node_value(sink: &VecDocSink, id: &str) -> Value {
|
||||
let node = sink
|
||||
.state
|
||||
.active_children()
|
||||
.iter()
|
||||
.find_map(|node| find_node(node, id))
|
||||
.expect("node exists");
|
||||
serde_json::to_value(node).expect("serialize node")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_container_content_section_on_fixed_vertical_root_collapses() {
|
||||
let mut sink = insert_root(json!({
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Food App",
|
||||
"width": 375,
|
||||
"height": 812,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "promo",
|
||||
"name": "Featured Promo Banner",
|
||||
"width": "fill_container",
|
||||
"height": "fill_container",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "promo-title", "content": "Half-price ramen"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "categories",
|
||||
"name": "Food Categories",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "horizontal",
|
||||
"children": [
|
||||
{"type": "text", "id": "cat-title", "content": "Sushi"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "restaurants",
|
||||
"name": "Popular Restaurants",
|
||||
"width": "fill_container",
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
run_cleanup_passes(&mut sink, &plan(), &["root"]);
|
||||
|
||||
assert_eq!(
|
||||
active_node_value(&sink, "promo")["height"],
|
||||
json!("fit_content")
|
||||
);
|
||||
assert_eq!(
|
||||
active_node_value(&sink, "categories")["height"],
|
||||
json!("fit_content")
|
||||
);
|
||||
assert_eq!(
|
||||
active_node_value(&sink, "restaurants")["height"],
|
||||
json!("fit_content")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_spacer_fill_container_not_collapsed() {
|
||||
let mut sink = insert_root(json!({
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Food App",
|
||||
"width": 375,
|
||||
"height": 812,
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "spacer",
|
||||
"name": "Flexible Spacer",
|
||||
"width": "fill_container",
|
||||
"height": "fill_container",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
run_cleanup_passes(&mut sink, &plan(), &["root"]);
|
||||
|
||||
assert_eq!(
|
||||
active_node_value(&sink, "spacer")["height"],
|
||||
json!("fill_container")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_container_on_horizontal_root_child_untouched() {
|
||||
let mut sink = insert_root(json!({
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Web App Shell",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"layout": "horizontal",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "sidebar",
|
||||
"name": "Sidebar",
|
||||
"width": 260,
|
||||
"height": "fill_container",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "nav", "content": "Home"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
run_cleanup_passes(&mut sink, &plan(), &["root"]);
|
||||
|
||||
assert_eq!(
|
||||
active_node_value(&sink, "sidebar")["height"],
|
||||
json!("fill_container")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fit_content_root_untouched() {
|
||||
let mut sink = insert_root(json!({
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"name": "Scrolling Food App",
|
||||
"width": 375,
|
||||
"height": "fit_content",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "promo",
|
||||
"name": "Featured Promo Banner",
|
||||
"width": "fill_container",
|
||||
"height": "fill_container",
|
||||
"layout": "vertical",
|
||||
"children": [
|
||||
{"type": "text", "id": "promo-title", "content": "Daily deal"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}));
|
||||
|
||||
run_cleanup_passes(&mut sink, &plan(), &["root"]);
|
||||
|
||||
assert_eq!(
|
||||
active_node_value(&sink, "promo")["height"],
|
||||
json!("fill_container")
|
||||
);
|
||||
}
|
||||
|
|
@ -448,6 +448,10 @@ pub fn apply_loop_finalize(state: &mut EditorState) {
|
|||
crate::abandoned_duplicate_roots::remove_abandoned_duplicate_roots(&mut sink);
|
||||
crate::cleanup::remove_duplicate_bottom_nav_sections_for_all_roots(&mut sink);
|
||||
crate::cleanup::collapse_nested_horizontal_padding_for_all_roots(&mut sink);
|
||||
crate::cleanup::expand_absolute_container_to_children_for_all_roots(&mut sink);
|
||||
crate::cleanup::pad_clipping_horizontal_row_for_stroke_for_all_roots(&mut sink);
|
||||
crate::cleanup::equalize_horizontal_card_heights_for_all_roots(&mut sink);
|
||||
crate::cleanup::collapse_fill_container_content_sections_for_all_roots(&mut sink);
|
||||
}
|
||||
if state.active_children().is_empty() {
|
||||
return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue