Skip to main content

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}