1use std::fmt::Write as _;
2
3use panes::Direction;
6use panes::{Align, 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, Direction::Horizontal, true, &mut ctx);
29 ctx.css
30}
31
32fn emit_node(
33 tree: &LayoutTree,
34 nid: NodeId,
35 parent_axis: Direction,
36 is_root: bool,
37 ctx: &mut EmitCtx,
38) {
39 let Some(node) = tree.node(nid) else { return };
40 match node {
41 Node::Panel {
42 kind, constraints, ..
43 } => {
44 write_panel_rule(kind, constraints, parent_axis, &mut ctx.css);
45 }
46 Node::Row { gap, children } => {
47 let sel = container_selector(is_root, &mut ctx.counter);
48 write_container_rule(&sel, "row", *gap, is_root, &mut ctx.css);
49 emit_children(tree, children, Direction::Horizontal, ctx);
50 }
51 Node::Col { gap, children } => {
52 let sel = container_selector(is_root, &mut ctx.counter);
53 write_container_rule(&sel, "column", *gap, is_root, &mut ctx.css);
54 emit_children(tree, children, Direction::Vertical, ctx);
55 }
56 Node::TaffyPassthrough { style, children } if style.display == taffy::Display::Grid => {
57 let sel = container_selector(is_root, &mut ctx.counter);
58 write_grid_rule(&sel, style, is_root, &mut ctx.css);
59 emit_grid_children(tree, children, &mut ctx.counter, &mut ctx.css);
60 }
61 Node::TaffyPassthrough { children, .. } => {
62 let sel = container_selector(is_root, &mut ctx.counter);
63 write_passthrough_rule(&sel, is_root, &mut ctx.css);
64 emit_children(tree, children, parent_axis, ctx);
65 }
66 }
67}
68
69fn emit_children(tree: &LayoutTree, children: &[NodeId], axis: Direction, ctx: &mut EmitCtx) {
70 for &child_id in children {
71 emit_node(tree, child_id, axis, false, ctx);
72 }
73}
74
75fn container_selector(is_root: bool, counter: &mut u32) -> String {
76 match is_root {
77 true => "[data-pane-root]".to_string(),
78 false => {
79 *counter += 1;
80 format!("[data-pane-node=\"{}\"]", counter)
81 }
82 }
83}
84
85fn write_container_rule(
86 selector: &str,
87 direction: &str,
88 gap: f32,
89 is_root: bool,
90 css: &mut String,
91) {
92 let _ = write!(
93 css,
94 "{selector} {{ display: flex; flex-direction: {direction}; gap: {gap}px;"
95 );
96 if !is_root {
97 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
98 }
99 css.push_str(" }\n");
100}
101
102fn write_panel_rule(
103 kind: &str,
104 constraints: &Constraints,
105 parent_axis: Direction,
106 css: &mut String,
107) {
108 let _ = write!(css, "[data-pane=\"{kind}\"] {{ ");
109 write_flex_sizing(constraints, css);
110 write_min_max(constraints, parent_axis, css);
111 write_cross_axis_constraints(constraints, css);
112 write_align_self(constraints, css);
113 css.push_str(" }\n");
114}
115
116enum GridMode {
117 Fixed(usize),
118 AutoRepeat { kind: &'static str, min_px: f32 },
119}
120
121fn auto_repeat_kind(count: taffy::style::RepetitionCount) -> Option<&'static str> {
122 match count {
123 taffy::style::RepetitionCount::AutoFill => Some("auto-fill"),
124 taffy::style::RepetitionCount::AutoFit => Some("auto-fit"),
125 taffy::style::RepetitionCount::Count(_) => None,
126 }
127}
128
129fn detect_grid_mode(columns: &[taffy::style::GridTemplateComponent<String>]) -> GridMode {
130 let Some(taffy::style::GridTemplateComponent::Repeat(rep)) = columns.first() else {
131 return GridMode::Fixed(columns.len());
132 };
133 let Some(kind) = auto_repeat_kind(rep.count) else {
134 return GridMode::Fixed(columns.len());
135 };
136 let min_px = rep
137 .tracks
138 .first()
139 .map(|t| t.min_sizing_function().into_raw().value())
140 .unwrap_or(0.0);
141 GridMode::AutoRepeat { kind, min_px }
142}
143
144fn write_grid_rule(selector: &str, style: &taffy::Style, is_root: bool, css: &mut String) {
145 let _ = write!(css, "{selector} {{ display: grid;");
146 match detect_grid_mode(&style.grid_template_columns) {
147 GridMode::Fixed(cols) => {
148 let _ = write!(css, " grid-template-columns: repeat({cols}, 1fr);");
149 }
150 GridMode::AutoRepeat { kind, min_px } => {
151 let _ = write!(
152 css,
153 " grid-template-columns: repeat({kind}, minmax({min_px}px, 1fr));"
154 );
155 }
156 }
157 match style.grid_auto_rows.is_empty() {
158 true => {}
159 false if is_auto_rows(&style.grid_auto_rows) => {
160 css.push_str(" grid-auto-rows: auto;");
161 }
162 false => css.push_str(" grid-auto-rows: 1fr;"),
163 }
164 let gap = style.gap.width.into_raw().value();
165 if gap > 0.0 {
166 let _ = write!(css, " gap: {gap}px;");
167 }
168 if !is_root {
169 css.push_str(" flex-grow: 1; flex-basis: 0px;");
170 }
171 css.push_str(" }\n");
172}
173
174fn emit_grid_children(tree: &LayoutTree, children: &[NodeId], counter: &mut u32, css: &mut String) {
175 for &child_id in children {
176 match tree.node(child_id) {
177 Some(Node::TaffyPassthrough { style, .. }) => {
178 let sel = container_selector(false, counter);
179 write_grid_card_rule(&sel, style, css);
180 emit_grid_card_panels(tree, child_id, css);
181 }
182 Some(Node::Panel {
183 kind, constraints, ..
184 }) => {
185 write_panel_rule(kind, constraints, Direction::Horizontal, css);
186 }
187 _ => {}
188 }
189 }
190}
191
192fn write_grid_card_rule(sel: &str, style: &taffy::Style, css: &mut String) {
193 let _ = write!(css, "{sel} {{ display: flex;");
194 match grid_column_placement(style) {
195 GridColumnPlacement::FullWidth => {
196 css.push_str(" grid-column: 1 / -1;");
197 }
198 GridColumnPlacement::Span(n) if n > 1 => {
199 let _ = write!(css, " grid-column: span {n};");
200 }
201 _ => {}
202 }
203 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1; }\n");
204}
205
206fn emit_grid_card_panels(tree: &LayoutTree, card_id: NodeId, css: &mut String) {
207 let Some(node) = tree.node(card_id) else {
208 return;
209 };
210 for &grandchild in node.children() {
211 let Some(Node::Panel {
212 kind, constraints, ..
213 }) = tree.node(grandchild)
214 else {
215 continue;
216 };
217 write_panel_rule(kind, constraints, Direction::Horizontal, css);
218 }
219}
220
221enum GridColumnPlacement {
222 Span(u16),
223 FullWidth,
224}
225
226fn grid_column_placement(style: &taffy::Style) -> GridColumnPlacement {
227 match (&style.grid_column.start, &style.grid_column.end) {
228 (taffy::GridPlacement::Line(s), taffy::GridPlacement::Line(e))
229 if s.as_i16() == 1 && e.as_i16() == -1 =>
230 {
231 GridColumnPlacement::FullWidth
232 }
233 (_, taffy::GridPlacement::Span(n)) => GridColumnPlacement::Span(*n),
234 _ => GridColumnPlacement::Span(1),
235 }
236}
237
238fn is_auto_rows(tracks: &[taffy::style::TrackSizingFunction]) -> bool {
239 let auto_track = taffy::prelude::minmax(
240 taffy::style::MinTrackSizingFunction::auto(),
241 taffy::style::MaxTrackSizingFunction::auto(),
242 );
243 matches!(tracks.first(), Some(t) if *t == auto_track)
244}
245
246fn write_passthrough_rule(selector: &str, is_root: bool, css: &mut String) {
247 let _ = write!(css, "{selector} {{ display: flex;");
248 if !is_root {
249 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
250 }
251 css.push_str(" }\n");
252}
253
254fn write_flex_sizing(constraints: &Constraints, css: &mut String) {
255 match (constraints.grow, constraints.fixed) {
256 (Some(g), _) => {
257 let _ = write!(css, "flex-grow: {g}; flex-basis: 0px; flex-shrink: 1;");
258 }
259 (_, Some(f)) => {
260 let _ = write!(css, "flex-grow: 0; flex-basis: {f}px; flex-shrink: 0;");
261 }
262 (None, None) => {
263 css.push_str("flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
264 }
265 }
266}
267
268pub fn emit_adaptive(breakpoints: &[(u32, &Layout)]) -> String {
274 let mut css = String::new();
275 let len = breakpoints.len();
276 for (i, (min_width, layout)) in breakpoints.iter().enumerate() {
277 let inner = emit(layout);
278 let query = match (i, i + 1 < len) {
279 (0, true) => {
280 let next_min = breakpoints[i + 1].0;
281 format!("@media (max-width: {}px)", next_min.saturating_sub(1))
282 }
283 (0, false) => {
284 css.push_str(&inner);
286 continue;
287 }
288 (_, true) => {
289 let next_min = breakpoints[i + 1].0;
290 format!(
291 "@media (min-width: {min_width}px) and (max-width: {}px)",
292 next_min.saturating_sub(1)
293 )
294 }
295 (_, false) => format!("@media (min-width: {min_width}px)"),
296 };
297 let _ = write!(css, "{query} {{\n{inner}}}\n");
298 }
299 css
300}
301
302fn write_min_max(constraints: &Constraints, axis: Direction, css: &mut String) {
303 let (min_prop, max_prop) = match axis {
304 Direction::Horizontal => ("min-width", "max-width"),
305 Direction::Vertical => ("min-height", "max-height"),
306 };
307 if let Some(min) = constraints.min {
308 let _ = write!(css, " {min_prop}: {min}px;");
309 }
310 if let Some(max) = constraints.max {
311 let _ = write!(css, " {max_prop}: {max}px;");
312 }
313}
314
315fn write_cross_axis_constraints(constraints: &Constraints, css: &mut String) {
316 if let Some(v) = constraints.min_width {
317 let _ = write!(css, " min-width: {v}px;");
318 }
319 if let Some(v) = constraints.max_width {
320 let _ = write!(css, " max-width: {v}px;");
321 }
322 if let Some(v) = constraints.min_height {
323 let _ = write!(css, " min-height: {v}px;");
324 }
325 if let Some(v) = constraints.max_height {
326 let _ = write!(css, " max-height: {v}px;");
327 }
328}
329
330fn write_align_self(constraints: &Constraints, css: &mut String) {
331 let Some(align) = constraints.align else {
332 return;
333 };
334 let value = match align {
335 Align::Start => "start",
336 Align::Center => "center",
337 Align::End => "end",
338 Align::Stretch => return,
339 };
340 let _ = write!(css, " align-self: {value};");
341}