winged_rust/layout.rs
1//! Reusable page layouts.
2//!
3//! Ports `Winged-Swift/Sources/WingedSwift/templates/Layout.swift` — the only protocol in
4//! the entire Swift library.
5
6use crate::core::Node;
7use crate::elements::div;
8
9/// Wraps content in a consistent structure: a header, a footer, navigation.
10///
11/// # A footgun that does not exist here
12///
13/// Winged-Swift's docs spend considerable space warning that `HTMLTag` is a reference type,
14/// so reusing one instance in two places silently shares the node — which is why its
15/// documented component pattern is "a function that returns a fresh tag each call". In Rust
16/// [`Node`] is a value type, so reuse copies. Write components however you like.
17///
18/// # Examples
19/// ```
20/// use winged_rust::prelude::*;
21/// use winged_rust::Layout;
22///
23/// struct Blog { site_title: String }
24///
25/// impl Layout for Blog {
26/// fn render(&self, content: Node) -> Node {
27/// body()
28/// .child(header().child(h1().text(&self.site_title)))
29/// .child(main_tag().child(content))
30/// .child(footer().child(p().text("© 2026")))
31/// .into()
32/// }
33/// }
34///
35/// let page = Blog { site_title: "RideKeeper".into() }.render(p().text("Hello").into());
36/// assert!(page.render().contains("<h1>RideKeeper</h1>"));
37/// ```
38pub trait Layout {
39 /// Wraps a single node in the layout.
40 fn render(&self, content: Node) -> Node;
41
42 /// Wraps several nodes, grouping them in a `<div>` first.
43 ///
44 /// Matches the Swift extension, which also introduces a `Div`. Use
45 /// [`Node::fragment`] and [`Layout::render`] if you would rather not have the wrapper.
46 fn render_many(&self, contents: Vec<Node>) -> Node {
47 self.render(div().children_from(contents).into())
48 }
49}
50
51#[cfg(test)]
52mod tests {
53 use super::*;
54 use crate::core::Render;
55 use crate::elements::{body, h1, main_tag, p};
56
57 struct Minimal;
58
59 impl Layout for Minimal {
60 fn render(&self, content: Node) -> Node {
61 body()
62 .child(h1().text("Site"))
63 .child(main_tag().child(content))
64 .into()
65 }
66 }
67
68 /// Ports `LayoutTests.wrapsASingleTag`.
69 #[test]
70 fn a_layout_wraps_a_single_node() {
71 let page = Minimal.render(p().text("body").into());
72 assert_eq!(
73 page.render(),
74 "<body><h1>Site</h1><main><p>body</p></main></body>"
75 );
76 }
77
78 /// Ports `LayoutTests.wrapsSeveralTagsInAContainer`.
79 #[test]
80 fn render_many_groups_the_contents_in_a_div() {
81 let page = Minimal.render_many(vec![p().text("a").into(), p().text("b").into()]);
82 assert_eq!(
83 page.render(),
84 "<body><h1>Site</h1><main><div><p>a</p><p>b</p></div></main></body>"
85 );
86 }
87
88 /// Ports `LayoutTests.anEmptyContentListStillProducesAPage`.
89 #[test]
90 fn render_many_with_no_contents_still_produces_the_wrapper() {
91 let page = Minimal.render_many(vec![]);
92 assert!(page.render().contains("<div></div>"));
93 }
94}