1use std::fmt::Write as _;
2
3use panes::compiler::Axis;
6use panes::{Constraints, Layout, LayoutTree, Node, NodeId};
7
8struct EmitCtx {
10 css: String,
11 counter: u32,
12}
13
14pub fn emit(layout: &Layout) -> String {
20 let tree = layout.tree();
21 let Some(root_id) = tree.root() else {
22 return String::new();
23 };
24 let mut ctx = EmitCtx {
25 css: String::new(),
26 counter: 0,
27 };
28 emit_node(tree, root_id, Axis::Horizontal, true, &mut ctx);
29 ctx.css
30}
31
32fn emit_node(tree: &LayoutTree, nid: NodeId, parent_axis: Axis, is_root: bool, ctx: &mut EmitCtx) {
33 let Some(node) = tree.node(nid) else { return };
34 match node {
35 Node::Panel {
36 kind, constraints, ..
37 } => {
38 write_panel_rule(kind, constraints, parent_axis, &mut ctx.css);
39 }
40 Node::Row { gap, children } => {
41 let sel = container_selector(is_root, &mut ctx.counter);
42 write_container_rule(&sel, "row", *gap, is_root, &mut ctx.css);
43 emit_children(tree, children, Axis::Horizontal, ctx);
44 }
45 Node::Col { gap, children } => {
46 let sel = container_selector(is_root, &mut ctx.counter);
47 write_container_rule(&sel, "column", *gap, is_root, &mut ctx.css);
48 emit_children(tree, children, Axis::Vertical, ctx);
49 }
50 Node::TaffyPassthrough { style, children } if style.display == taffy::Display::Grid => {
51 let sel = container_selector(is_root, &mut ctx.counter);
52 write_grid_rule(&sel, style, is_root, &mut ctx.css);
53 emit_grid_children(tree, children, &mut ctx.counter, &mut ctx.css);
54 }
55 Node::TaffyPassthrough { children, .. } => {
56 let sel = container_selector(is_root, &mut ctx.counter);
57 write_passthrough_rule(&sel, is_root, &mut ctx.css);
58 emit_children(tree, children, parent_axis, ctx);
59 }
60 }
61}
62
63fn emit_children(tree: &LayoutTree, children: &[NodeId], axis: Axis, ctx: &mut EmitCtx) {
64 for &child_id in children {
65 emit_node(tree, child_id, axis, false, ctx);
66 }
67}
68
69fn container_selector(is_root: bool, counter: &mut u32) -> String {
70 match is_root {
71 true => "[data-pane-root]".to_string(),
72 false => {
73 *counter += 1;
74 format!("[data-pane-node=\"{}\"]", counter)
75 }
76 }
77}
78
79fn write_container_rule(
80 selector: &str,
81 direction: &str,
82 gap: f32,
83 is_root: bool,
84 css: &mut String,
85) {
86 let _ = write!(
87 css,
88 "{selector} {{ display: flex; flex-direction: {direction}; gap: {gap}px;"
89 );
90 if !is_root {
91 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
92 }
93 css.push_str(" }\n");
94}
95
96fn write_panel_rule(kind: &str, constraints: &Constraints, parent_axis: Axis, css: &mut String) {
97 let _ = write!(css, "[data-pane=\"{kind}\"] {{ ");
98 write_flex_sizing(constraints, css);
99 write_min_max(constraints, parent_axis, css);
100 css.push_str(" }\n");
101}
102
103fn write_grid_rule(selector: &str, style: &taffy::Style, is_root: bool, css: &mut String) {
104 let cols = style.grid_template_columns.len();
105 let _ = write!(
106 css,
107 "{selector} {{ display: grid; grid-template-columns: repeat({cols}, 1fr);"
108 );
109 if !style.grid_auto_rows.is_empty() {
110 css.push_str(" grid-auto-rows: 1fr;");
111 }
112 let gap = style.gap.width.into_raw().value();
113 if gap > 0.0 {
114 let _ = write!(css, " gap: {gap}px;");
115 }
116 if !is_root {
117 css.push_str(" flex-grow: 1; flex-basis: 0px;");
118 }
119 css.push_str(" }\n");
120}
121
122fn emit_grid_children(tree: &LayoutTree, children: &[NodeId], counter: &mut u32, css: &mut String) {
123 for &child_id in children {
124 let Some(Node::TaffyPassthrough { style, .. }) = tree.node(child_id) else {
125 continue;
126 };
127 let sel = container_selector(false, counter);
128 write_grid_card_rule(&sel, style, css);
129 emit_grid_card_panels(tree, child_id, css);
130 }
131}
132
133fn write_grid_card_rule(sel: &str, style: &taffy::Style, css: &mut String) {
134 let span = grid_column_span(style);
135 let _ = write!(css, "{sel} {{ display: flex;");
136 if span > 1 {
137 let _ = write!(css, " grid-column: span {span};");
138 }
139 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1; }\n");
140}
141
142fn emit_grid_card_panels(tree: &LayoutTree, card_id: NodeId, css: &mut String) {
143 let Some(node) = tree.node(card_id) else {
144 return;
145 };
146 for &grandchild in node.children() {
147 let Some(Node::Panel {
148 kind, constraints, ..
149 }) = tree.node(grandchild)
150 else {
151 continue;
152 };
153 write_panel_rule(kind, constraints, Axis::Horizontal, css);
154 }
155}
156
157fn grid_column_span(style: &taffy::Style) -> u16 {
158 match style.grid_column.end {
159 taffy::GridPlacement::Span(n) => n,
160 _ => 1,
161 }
162}
163
164fn write_passthrough_rule(selector: &str, is_root: bool, css: &mut String) {
165 let _ = write!(css, "{selector} {{ display: flex;");
166 if !is_root {
167 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
168 }
169 css.push_str(" }\n");
170}
171
172fn write_flex_sizing(constraints: &Constraints, css: &mut String) {
173 match (constraints.grow, constraints.fixed) {
174 (Some(g), _) => {
175 let _ = write!(css, "flex-grow: {g}; flex-basis: 0px; flex-shrink: 1;");
176 }
177 (_, Some(f)) => {
178 let _ = write!(css, "flex-grow: 0; flex-basis: {f}px; flex-shrink: 0;");
179 }
180 (None, None) => {
181 css.push_str("flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
182 }
183 }
184}
185
186fn write_min_max(constraints: &Constraints, axis: Axis, css: &mut String) {
187 let (min_prop, max_prop) = match axis {
188 Axis::Horizontal => ("min-width", "max-width"),
189 Axis::Vertical => ("min-height", "max-height"),
190 };
191 if let Some(min) = constraints.min {
192 let _ = write!(css, " {min_prop}: {min}px;");
193 }
194 if let Some(max) = constraints.max {
195 let _ = write!(css, " {max_prop}: {max}px;");
196 }
197}