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