1use std::fmt::Write as _;
2
3use panes::Direction;
4use panes::{
5 Align, Constraints, ExtentValue, HAlign, Layout, LayoutTree, Node, NodeId, OverlayAnchor,
6 OverlayDef, SizeMode, VAlign,
7};
8
9struct EmitCtx {
10 css: String,
11 counter: u32,
12 root_position_relative: bool,
13 transitions: bool,
14}
15
16pub fn emit(layout: &Layout) -> String {
22 emit_tree(layout, false, false)
23}
24
25pub fn emit_with_overlays(layout: &Layout, overlays: &[OverlayDef]) -> String {
31 emit_with_options(layout, overlays, false)
32}
33
34pub fn emit_with_transitions(layout: &Layout) -> String {
40 emit_tree(layout, false, true)
41}
42
43pub fn emit_full(layout: &Layout, overlays: &[OverlayDef]) -> String {
45 emit_with_options(layout, overlays, true)
46}
47
48fn emit_with_options(layout: &Layout, overlays: &[OverlayDef], transitions: bool) -> String {
49 let mut css = emit_tree(layout, !overlays.is_empty(), transitions);
50 for (i, def) in overlays.iter().enumerate() {
51 write_overlay_rule(def, i + 1, &mut css);
52 }
53 css
54}
55
56fn emit_tree(layout: &Layout, root_position_relative: bool, transitions: bool) -> String {
57 let tree = layout.tree();
58 let Some(root_id) = tree.root() else {
59 return String::new();
60 };
61 let estimated_bytes = tree.node_count() * 80;
62 let mut ctx = EmitCtx {
63 css: String::with_capacity(estimated_bytes),
64 counter: 0,
65 root_position_relative,
66 transitions,
67 };
68 emit_node(tree, root_id, Direction::Horizontal, true, &mut ctx);
69 ctx.css
70}
71
72fn emit_node(
73 tree: &LayoutTree,
74 nid: NodeId,
75 parent_axis: Direction,
76 is_root: bool,
77 ctx: &mut EmitCtx,
78) {
79 let Some(node) = tree.node(nid) else { return };
80 match node {
81 Node::Panel {
82 kind, constraints, ..
83 } => {
84 write_panel_rule(
85 kind,
86 constraints,
87 parent_axis,
88 ctx.transitions,
89 &mut ctx.css,
90 );
91 }
92 Node::Row { gap, children } => {
93 emit_flex_container(
94 tree,
95 children,
96 "row",
97 *gap,
98 Direction::Horizontal,
99 is_root,
100 ctx,
101 );
102 }
103 Node::Col { gap, children } => {
104 emit_flex_container(
105 tree,
106 children,
107 "column",
108 *gap,
109 Direction::Vertical,
110 is_root,
111 ctx,
112 );
113 }
114 Node::TaffyPassthrough { style, children } if style.display == taffy::Display::Grid => {
115 write_container_selector(is_root, &mut ctx.counter, &mut ctx.css);
116 write_grid_rule(style, is_root, &mut ctx.css);
117 inject_root_extras(is_root, ctx);
118 emit_grid_children(tree, children, ctx);
119 }
120 Node::TaffyPassthrough { style, children }
121 if is_scrollable_container(style, tree, children) =>
122 {
123 write_container_selector(is_root, &mut ctx.counter, &mut ctx.css);
124 let axis = scroll_axis(style);
125 write_scrollable_rule(axis, is_root, &mut ctx.css);
126 inject_root_extras(is_root, ctx);
127 emit_scrollable_children(tree, children, axis, ctx);
128 }
129 Node::TaffyPassthrough { children, .. } => {
130 write_container_selector(is_root, &mut ctx.counter, &mut ctx.css);
131 write_passthrough_rule(is_root, &mut ctx.css);
132 inject_root_extras(is_root, ctx);
133 emit_children(tree, children, parent_axis, ctx);
134 }
135 }
136}
137
138fn inject_root_extras(is_root: bool, ctx: &mut EmitCtx) {
145 let extra = match (is_root, ctx.root_position_relative, ctx.transitions) {
146 (true, true, true) => " position: relative; --pane-transition: 0.2s ease;",
147 (true, true, false) => " position: relative;",
148 (true, false, true) => " --pane-transition: 0.2s ease;",
149 _ => return,
150 };
151 debug_assert!(ctx.css.ends_with(" }\n"));
153 ctx.css.truncate(ctx.css.len() - 3);
154 ctx.css.push_str(extra);
155 ctx.css.push_str(" }\n");
156}
157
158fn emit_children(tree: &LayoutTree, children: &[NodeId], axis: Direction, ctx: &mut EmitCtx) {
159 for &child_id in children {
160 emit_node(tree, child_id, axis, false, ctx);
161 }
162}
163
164fn write_container_selector(is_root: bool, counter: &mut u32, css: &mut String) {
166 match is_root {
167 true => css.push_str("[data-pane-root]"),
168 false => {
169 *counter += 1;
170 let _ = write!(css, "[data-pane-node=\"{}\"]", counter);
171 }
172 }
173}
174
175fn emit_flex_container(
176 tree: &LayoutTree,
177 children: &[NodeId],
178 direction: &str,
179 gap: f32,
180 axis: Direction,
181 is_root: bool,
182 ctx: &mut EmitCtx,
183) {
184 write_container_selector(is_root, &mut ctx.counter, &mut ctx.css);
185 let _ = write!(
186 ctx.css,
187 " {{ display: flex; flex-direction: {direction}; gap: {gap}px;"
188 );
189 write_container_flex(is_root, &mut ctx.css);
190 inject_root_extras(is_root, ctx);
191 emit_children(tree, children, axis, ctx);
192}
193
194fn write_container_flex(is_root: bool, css: &mut String) {
195 if !is_root {
196 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
197 }
198 css.push_str(" }\n");
199}
200
201fn write_panel_rule(
202 kind: &str,
203 constraints: &Constraints,
204 parent_axis: Direction,
205 transitions: bool,
206 css: &mut String,
207) {
208 let _ = write!(css, "[data-pane=\"{kind}\"] {{ ");
209 write_flex_sizing(constraints, parent_axis, css);
210 write_min_max(constraints, parent_axis, css);
211 write_cross_axis_constraints(constraints, css);
212 write_align_self(constraints, css);
213 write_transition(transitions, css);
214 css.push_str(" }\n");
215}
216
217enum GridMode {
218 Fixed(usize),
219 AutoRepeat { kind: &'static str, min_px: f32 },
220}
221
222fn auto_repeat_kind(count: taffy::style::RepetitionCount) -> Option<&'static str> {
223 match count {
224 taffy::style::RepetitionCount::AutoFill => Some("auto-fill"),
225 taffy::style::RepetitionCount::AutoFit => Some("auto-fit"),
226 taffy::style::RepetitionCount::Count(_) => None,
227 }
228}
229
230fn detect_grid_mode(columns: &[taffy::style::GridTemplateComponent<String>]) -> GridMode {
231 let Some(taffy::style::GridTemplateComponent::Repeat(rep)) = columns.first() else {
232 return GridMode::Fixed(columns.len());
233 };
234 let Some(kind) = auto_repeat_kind(rep.count) else {
235 return GridMode::Fixed(columns.len());
236 };
237 let min_px = rep
238 .tracks
239 .first()
240 .map(|t| t.min_sizing_function().into_raw().value())
241 .unwrap_or(0.0);
242 GridMode::AutoRepeat { kind, min_px }
243}
244
245fn write_grid_rule(style: &taffy::Style, is_root: bool, css: &mut String) {
246 css.push_str(" { display: grid;");
247 match detect_grid_mode(&style.grid_template_columns) {
248 GridMode::Fixed(cols) => {
249 let _ = write!(css, " grid-template-columns: repeat({cols}, 1fr);");
250 }
251 GridMode::AutoRepeat { kind, min_px } => {
252 let _ = write!(
253 css,
254 " grid-template-columns: repeat({kind}, minmax({min_px}px, 1fr));"
255 );
256 }
257 }
258 match style.grid_auto_rows.is_empty() {
259 true => {}
260 false if is_auto_rows(&style.grid_auto_rows) => {
261 css.push_str(" grid-auto-rows: auto;");
262 }
263 false => css.push_str(" grid-auto-rows: 1fr;"),
264 }
265 let gap = style.gap.width.into_raw().value();
266 if gap > 0.0 {
267 let _ = write!(css, " gap: {gap}px;");
268 }
269 if !is_root {
270 css.push_str(" flex-grow: 1; flex-basis: 0px;");
271 }
272 css.push_str(" }\n");
273}
274
275fn emit_grid_children(tree: &LayoutTree, children: &[NodeId], ctx: &mut EmitCtx) {
276 for &child_id in children {
277 match tree.node(child_id) {
278 Some(Node::TaffyPassthrough { style, .. }) => {
279 write_container_selector(false, &mut ctx.counter, &mut ctx.css);
280 write_grid_card_rule(style, &mut ctx.css);
281 emit_grid_card_panels(tree, child_id, ctx.transitions, &mut ctx.css);
282 }
283 Some(Node::Panel {
284 kind, constraints, ..
285 }) => {
286 write_panel_rule(
287 kind,
288 constraints,
289 Direction::Horizontal,
290 ctx.transitions,
291 &mut ctx.css,
292 );
293 }
294 _ => {}
295 }
296 }
297}
298
299fn write_grid_card_rule(style: &taffy::Style, css: &mut String) {
300 css.push_str(" { display: flex;");
301 match grid_column_placement(style) {
302 GridColumnPlacement::FullWidth => {
303 css.push_str(" grid-column: 1 / -1;");
304 }
305 GridColumnPlacement::Span(n) if n > 1 => {
306 let _ = write!(css, " grid-column: span {n};");
307 }
308 _ => {}
309 }
310 css.push_str(" flex-grow: 1; flex-basis: 0px; flex-shrink: 1; }\n");
311}
312
313fn emit_grid_card_panels(tree: &LayoutTree, card_id: NodeId, transitions: bool, css: &mut String) {
314 let Some(node) = tree.node(card_id) else {
315 return;
316 };
317 for &grandchild in node.children() {
318 let Some(Node::Panel {
319 kind, constraints, ..
320 }) = tree.node(grandchild)
321 else {
322 continue;
323 };
324 write_panel_rule(kind, constraints, Direction::Horizontal, transitions, css);
325 }
326}
327
328enum GridColumnPlacement {
329 Span(u16),
330 FullWidth,
331}
332
333fn grid_column_placement(style: &taffy::Style) -> GridColumnPlacement {
334 match (&style.grid_column.start, &style.grid_column.end) {
335 (taffy::GridPlacement::Line(s), taffy::GridPlacement::Line(e))
336 if s.as_i16() == 1 && e.as_i16() == -1 =>
337 {
338 GridColumnPlacement::FullWidth
339 }
340 (_, taffy::GridPlacement::Span(n)) => GridColumnPlacement::Span(*n),
341 _ => GridColumnPlacement::Span(1),
342 }
343}
344
345fn is_auto_rows(tracks: &[taffy::style::TrackSizingFunction]) -> bool {
346 let auto_track = taffy::prelude::minmax(
347 taffy::style::MinTrackSizingFunction::auto(),
348 taffy::style::MaxTrackSizingFunction::auto(),
349 );
350 matches!(tracks.first(), Some(t) if *t == auto_track)
351}
352
353fn write_passthrough_rule(is_root: bool, css: &mut String) {
354 css.push_str(" { display: flex;");
355 write_container_flex(is_root, css);
356}
357
358fn is_scrollable_container(style: &taffy::Style, tree: &LayoutTree, children: &[NodeId]) -> bool {
361 style.display == taffy::Display::Flex
362 && matches!(
363 style.flex_direction,
364 taffy::FlexDirection::Row | taffy::FlexDirection::Column
365 )
366 && style.flex_wrap == taffy::FlexWrap::NoWrap
367 && !children.is_empty()
368 && children.iter().all(|&nid| {
369 matches!(
370 tree.node(nid),
371 Some(Node::Panel { constraints, .. }) if constraints.fixed.is_some()
372 )
373 })
374}
375
376#[derive(Clone, Copy)]
377enum ScrollAxis {
378 X,
379 Y,
380}
381
382fn scroll_axis(style: &taffy::Style) -> ScrollAxis {
383 match style.flex_direction {
384 taffy::FlexDirection::Column | taffy::FlexDirection::ColumnReverse => ScrollAxis::Y,
385 _ => ScrollAxis::X,
386 }
387}
388
389fn write_scrollable_rule(axis: ScrollAxis, is_root: bool, css: &mut String) {
390 css.push_str(" { display: flex;");
391 match axis {
392 ScrollAxis::X => {
393 css.push_str(" flex-direction: row;");
394 css.push_str(" overflow-x: auto; scroll-snap-type: x mandatory;");
395 }
396 ScrollAxis::Y => {
397 css.push_str(" flex-direction: column;");
398 css.push_str(" overflow-y: auto; scroll-snap-type: y mandatory;");
399 }
400 }
401 css.push_str(" overscroll-behavior: contain;");
402 write_container_flex(is_root, css);
403}
404
405fn emit_scrollable_children(
406 tree: &LayoutTree,
407 children: &[NodeId],
408 axis: ScrollAxis,
409 ctx: &mut EmitCtx,
410) {
411 let parent_axis = match axis {
412 ScrollAxis::X => Direction::Horizontal,
413 ScrollAxis::Y => Direction::Vertical,
414 };
415 for &child_id in children {
416 let Some(Node::Panel {
417 kind, constraints, ..
418 }) = tree.node(child_id)
419 else {
420 continue;
421 };
422 write_panel_rule(
423 kind,
424 constraints,
425 parent_axis,
426 ctx.transitions,
427 &mut ctx.css,
428 );
429 debug_assert!(ctx.css.ends_with(" }\n"));
431 ctx.css.truncate(ctx.css.len() - 3);
432 ctx.css.push_str(" scroll-snap-align: start; }\n");
433 }
434}
435
436fn write_flex_sizing(constraints: &Constraints, parent_axis: Direction, css: &mut String) {
437 match (constraints.grow, constraints.fixed) {
438 (Some(g), _) => {
439 let _ = write!(css, "flex-grow: {g}; flex-basis: 0px; flex-shrink: 1;");
440 }
441 (_, Some(f)) => {
442 let _ = write!(css, "flex-grow: 0; flex-basis: {f}px; flex-shrink: 0;");
443 }
444 (None, None) => {
445 css.push_str("flex-grow: 1; flex-basis: 0px; flex-shrink: 1;");
446 }
447 }
448 write_size_mode(constraints.size_mode, parent_axis, css);
449}
450
451fn write_size_mode(size_mode: Option<SizeMode>, parent_axis: Direction, css: &mut String) {
452 let Some(mode) = size_mode else { return };
453 let prop = match parent_axis {
454 Direction::Horizontal => "width",
455 Direction::Vertical => "height",
456 };
457 match mode {
458 SizeMode::MinContent => {
459 let _ = write!(css, " flex-basis: min-content; {prop}: min-content;");
460 }
461 SizeMode::MaxContent => {
462 let _ = write!(css, " flex-basis: max-content; {prop}: max-content;");
463 }
464 SizeMode::FitContent(v) => {
465 let _ = write!(
466 css,
467 " flex-basis: fit-content({v}px); {prop}: fit-content({v}px);"
468 );
469 }
470 }
471}
472
473pub fn emit_adaptive(breakpoints: &[(u32, &Layout)]) -> String {
479 let mut css = String::new();
480 let len = breakpoints.len();
481 for (i, (min_width, layout)) in breakpoints.iter().enumerate() {
482 let inner = emit_tree(layout, false, false);
483 match (i, i + 1 < len) {
484 (0, true) => {
485 let next_min = breakpoints[i + 1].0;
486 let _ = write!(
487 css,
488 "@media (max-width: {}px) {{\n{inner}}}\n",
489 next_min.saturating_sub(1)
490 );
491 }
492 (0, false) => {
493 css.push_str(&inner);
494 }
495 (_, true) => {
496 let next_min = breakpoints[i + 1].0;
497 let _ = write!(
498 css,
499 "@media (min-width: {min_width}px) and (max-width: {}px) {{\n{inner}}}\n",
500 next_min.saturating_sub(1)
501 );
502 }
503 (_, false) => {
504 let _ = write!(css, "@media (min-width: {min_width}px) {{\n{inner}}}\n");
505 }
506 }
507 }
508 css
509}
510
511fn write_min_max(constraints: &Constraints, axis: Direction, css: &mut String) {
512 let (min_prop, max_prop) = match axis {
513 Direction::Horizontal => ("min-width", "max-width"),
514 Direction::Vertical => ("min-height", "max-height"),
515 };
516 if let Some(min) = constraints.min {
517 let _ = write!(css, " {min_prop}: {min}px;");
518 }
519 if let Some(max) = constraints.max {
520 let _ = write!(css, " {max_prop}: {max}px;");
521 }
522}
523
524fn write_cross_axis_constraints(constraints: &Constraints, css: &mut String) {
525 if let Some(v) = constraints.min_width {
526 let _ = write!(css, " min-width: {v}px;");
527 }
528 if let Some(v) = constraints.max_width {
529 let _ = write!(css, " max-width: {v}px;");
530 }
531 if let Some(v) = constraints.min_height {
532 let _ = write!(css, " min-height: {v}px;");
533 }
534 if let Some(v) = constraints.max_height {
535 let _ = write!(css, " max-height: {v}px;");
536 }
537}
538
539fn write_align_self(constraints: &Constraints, css: &mut String) {
540 let Some(align) = constraints.align else {
541 return;
542 };
543 let value = match align {
544 Align::Start => "start",
545 Align::Center => "center",
546 Align::End => "end",
547 Align::Stretch => return,
548 };
549 let _ = write!(css, " align-self: {value};");
550}
551
552fn write_transition(transitions: bool, css: &mut String) {
553 match transitions {
554 true => css.push_str(concat!(
555 " transition: left var(--pane-transition),",
556 " top var(--pane-transition),",
557 " width var(--pane-transition),",
558 " height var(--pane-transition);"
559 )),
560 false => {}
561 }
562}
563
564fn write_overlay_rule(def: &OverlayDef, z_index: usize, css: &mut String) {
565 let kind = def.kind();
566 write_panel_anchor_container(def.anchor(), css);
567 let _ = write!(css, "[data-pane-overlay=\"{kind}\"] {{ position: absolute;");
568 let _ = write!(css, " z-index: {z_index};");
569 write_overlay_anchor(def.anchor(), css);
570 write_overlay_extent("width", def.width(), css);
571 write_overlay_extent("height", def.height(), css);
572 css.push_str(" }\n");
573}
574
575fn write_panel_anchor_container(anchor: &OverlayAnchor, css: &mut String) {
577 match anchor {
578 OverlayAnchor::Panel { kind, .. } => {
579 let _ = writeln!(css, "[data-pane=\"{kind}\"] {{ position: relative; }}");
580 }
581 OverlayAnchor::Viewport { .. } => {}
582 }
583}
584
585fn write_overlay_anchor(anchor: &OverlayAnchor, css: &mut String) {
586 match anchor {
587 OverlayAnchor::Viewport {
588 h,
589 v,
590 margin_x,
591 margin_y,
592 } => write_viewport_anchor(*h, *v, *margin_x, *margin_y, css),
593 OverlayAnchor::Panel {
594 h,
595 v,
596 offset_x,
597 offset_y,
598 ..
599 } => write_viewport_anchor(*h, *v, *offset_x, *offset_y, css),
600 }
601}
602
603fn write_viewport_anchor(h: HAlign, v: VAlign, margin_x: f32, margin_y: f32, css: &mut String) {
604 let needs_translate_x = matches!(h, HAlign::Center);
605 let needs_translate_y = matches!(v, VAlign::Center);
606
607 match h {
608 HAlign::Left => {
609 let _ = write!(css, " left: {margin_x}px;");
610 }
611 HAlign::Center => {
612 css.push_str(" left: 50%;");
613 }
614 HAlign::Right => {
615 let _ = write!(css, " right: {margin_x}px;");
616 }
617 }
618
619 match v {
620 VAlign::Top => {
621 let _ = write!(css, " top: {margin_y}px;");
622 }
623 VAlign::Center => {
624 css.push_str(" top: 50%;");
625 }
626 VAlign::Bottom => {
627 let _ = write!(css, " bottom: {margin_y}px;");
628 }
629 }
630
631 match (needs_translate_x, needs_translate_y) {
632 (true, true) => css.push_str(" transform: translate(-50%, -50%);"),
633 (true, false) => css.push_str(" transform: translateX(-50%);"),
634 (false, true) => css.push_str(" transform: translateY(-50%);"),
635 (false, false) => {}
636 }
637}
638
639fn write_overlay_extent(prop: &str, extent: &panes::OverlayExtent, css: &mut String) {
640 match extent.value {
641 ExtentValue::Fixed(v) => {
642 let _ = write!(css, " {prop}: {v}px;");
643 }
644 ExtentValue::Percent(pct) => {
645 let _ = write!(css, " {prop}: {pct}%;");
646 }
647 ExtentValue::Full => {
648 let _ = write!(css, " {prop}: 100%;");
649 }
650 }
651 if let Some(min) = extent.min {
652 let _ = write!(css, " min-{prop}: {min}px;");
653 }
654 if let Some(max) = extent.max {
655 let _ = write!(css, " max-{prop}: {max}px;");
656 }
657}