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
103enum GridMode {
104 Fixed(usize),
105 AutoRepeat { kind: &'static str, min_px: f32 },
106}
107
108fn auto_repeat_kind(count: taffy::style::RepetitionCount) -> Option<&'static str> {
109 match count {
110 taffy::style::RepetitionCount::AutoFill => Some("auto-fill"),
111 taffy::style::RepetitionCount::AutoFit => Some("auto-fit"),
112 taffy::style::RepetitionCount::Count(_) => None,
113 }
114}
115
116fn detect_grid_mode(columns: &[taffy::style::GridTemplateComponent<String>]) -> GridMode {
117 let Some(taffy::style::GridTemplateComponent::Repeat(rep)) = columns.first() else {
118 return GridMode::Fixed(columns.len());
119 };
120 let Some(kind) = auto_repeat_kind(rep.count) else {
121 return GridMode::Fixed(columns.len());
122 };
123 let min_px = rep
124 .tracks
125 .first()
126 .map(|t| t.min_sizing_function().into_raw().value())
127 .unwrap_or(0.0);
128 GridMode::AutoRepeat { kind, min_px }
129}
130
131fn write_grid_rule(selector: &str, style: &taffy::Style, is_root: bool, css: &mut String) {
132 let _ = write!(css, "{selector} {{ display: grid;");
133 match detect_grid_mode(&style.grid_template_columns) {
134 GridMode::Fixed(cols) => {
135 let _ = write!(css, " grid-template-columns: repeat({cols}, 1fr);");
136 }
137 GridMode::AutoRepeat { kind, min_px } => {
138 let _ = write!(
139 css,
140 " grid-template-columns: repeat({kind}, minmax({min_px}px, 1fr));"
141 );
142 }
143 }
144 if !style.grid_auto_rows.is_empty() {
145 css.push_str(" grid-auto-rows: 1fr;");
146 }
147 let gap = style.gap.width.into_raw().value();
148 if gap > 0.0 {
149 let _ = write!(css, " gap: {gap}px;");
150 }
151 if !is_root {
152 css.push_str(" flex-grow: 1; flex-basis: 0px;");
153 }
154 css.push_str(" }\n");
155}
156
157fn emit_grid_children(tree: &LayoutTree, children: &[NodeId], counter: &mut u32, css: &mut String) {
158 for &child_id in children {
159 match tree.node(child_id) {
160 Some(Node::TaffyPassthrough { style, .. }) => {
161 let sel = container_selector(false, counter);
162 write_grid_card_rule(&sel, style, css);
163 emit_grid_card_panels(tree, child_id, css);
164 }
165 Some(Node::Panel {
166 kind, constraints, ..
167 }) => {
168 write_panel_rule(kind, constraints, Axis::Horizontal, css);
169 }
170 _ => {}
171 }
172 }
173}
174
175fn write_grid_card_rule(sel: &str, style: &taffy::Style, css: &mut String) {
176 let span = grid_column_span(style);
177 let _ = write!(css, "{sel} {{ display: flex;");
178 if span > 1 {
179 let _ = write!(css, " grid-column: span {span};");
180 }
181 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1; }\n");
182}
183
184fn emit_grid_card_panels(tree: &LayoutTree, card_id: NodeId, css: &mut String) {
185 let Some(node) = tree.node(card_id) else {
186 return;
187 };
188 for &grandchild in node.children() {
189 let Some(Node::Panel {
190 kind, constraints, ..
191 }) = tree.node(grandchild)
192 else {
193 continue;
194 };
195 write_panel_rule(kind, constraints, Axis::Horizontal, css);
196 }
197}
198
199fn grid_column_span(style: &taffy::Style) -> u16 {
200 match style.grid_column.end {
201 taffy::GridPlacement::Span(n) => n,
202 _ => 1,
203 }
204}
205
206fn write_passthrough_rule(selector: &str, is_root: bool, css: &mut String) {
207 let _ = write!(css, "{selector} {{ display: flex;");
208 if !is_root {
209 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
210 }
211 css.push_str(" }\n");
212}
213
214fn write_flex_sizing(constraints: &Constraints, css: &mut String) {
215 match (constraints.grow, constraints.fixed) {
216 (Some(g), _) => {
217 let _ = write!(css, "flex-grow: {g}; flex-basis: 0px; flex-shrink: 1;");
218 }
219 (_, Some(f)) => {
220 let _ = write!(css, "flex-grow: 0; flex-basis: {f}px; flex-shrink: 0;");
221 }
222 (None, None) => {
223 css.push_str("flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
224 }
225 }
226}
227
228pub fn emit_adaptive(breakpoints: &[(u32, &Layout)]) -> String {
234 let mut css = String::new();
235 let len = breakpoints.len();
236 for (i, (min_width, layout)) in breakpoints.iter().enumerate() {
237 let inner = emit(layout);
238 let query = match (i, i + 1 < len) {
239 (0, true) => {
240 let next_min = breakpoints[i + 1].0;
241 format!("@media (max-width: {}px)", next_min.saturating_sub(1))
242 }
243 (0, false) => {
244 css.push_str(&inner);
246 continue;
247 }
248 (_, true) => {
249 let next_min = breakpoints[i + 1].0;
250 format!(
251 "@media (min-width: {min_width}px) and (max-width: {}px)",
252 next_min.saturating_sub(1)
253 )
254 }
255 (_, false) => format!("@media (min-width: {min_width}px)"),
256 };
257 let _ = write!(css, "{query} {{\n{inner}}}\n");
258 }
259 css
260}
261
262fn write_min_max(constraints: &Constraints, axis: Axis, css: &mut String) {
263 let (min_prop, max_prop) = match axis {
264 Axis::Horizontal => ("min-width", "max-width"),
265 Axis::Vertical => ("min-height", "max-height"),
266 };
267 if let Some(min) = constraints.min {
268 let _ = write!(css, " {min_prop}: {min}px;");
269 }
270 if let Some(max) = constraints.max {
271 let _ = write!(css, " {max_prop}: {max}px;");
272 }
273}