Skip to main content

panes_css/
emit.rs

1use std::fmt::Write as _;
2
3// `fmt::Write` for `String` is infallible. `let _ =` discards the unused `Result`.
4
5use panes::compiler::Axis;
6use panes::{Constraints, Layout, LayoutTree, Node, NodeId};
7
8/// Mutable state threaded through recursive CSS emission.
9struct EmitCtx {
10    css: String,
11    counter: u32,
12}
13
14/// Emit a CSS string from a `Layout` tree.
15///
16/// The browser acts as the layout solver via flexbox properties.
17/// Panels use `[data-pane="kind"]` selectors, containers use
18/// `[data-pane-node="N"]`, and the root uses `[data-pane-root]`.
19pub 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}