Skip to main content

lumen_core/
layout_backend.rs

1//! The layout engines a binary carries, how a launch installs one, and the
2//! system set that marks where layout is solved.
3//!
4//! Each engine registers a [`LayoutBackend`] into the app's
5//! [`LayoutBackends`] from its own crate, and the launch installs the
6//! highest-priority one with [`install_layout`]. The core and the runtime
7//! name no engine.
8
9use crate::app::App;
10use crate::backends::{BackendEntry, Backends, missing, registered};
11use crate::traits::LayoutEngine;
12use bevy_ecs::schedule::SystemSet;
13
14/// One layout engine: its name, its rank, and how to make it.
15#[derive(Clone, Copy, Debug)]
16pub struct LayoutBackend {
17    /// The name the engine is registered under.
18    pub name: &'static str,
19    /// Install order preference: the highest-priority engine is the one a
20    /// launch installs.
21    pub priority: i32,
22    /// The engine, not yet installed. Construction does no work;
23    /// [`LayoutEngine::install`] does.
24    pub engine: fn() -> Box<dyn LayoutEngine>,
25}
26
27impl BackendEntry for LayoutBackend {
28    const KIND: &'static str = "layout";
29
30    fn name(&self) -> &'static str {
31        self.name
32    }
33
34    fn priority(&self) -> i32 {
35        self.priority
36    }
37}
38
39/// Main-world registry of the layout engines this binary carries. An
40/// engine's capability install adds itself with
41/// [`crate::backends::register_backend`].
42pub type LayoutBackends = Backends<LayoutBackend>;
43
44/// The systems in [`crate::tick::TickStage::LayoutSync`] that write each
45/// entity's [`crate::components::Transform`] for this tick.
46///
47/// A system that reads this tick's boxes (keeping a caret in view, placing
48/// an overlay) runs `.after(LayoutSolve)` and works under any engine.
49#[derive(SystemSet, Clone, Copy, Debug, Hash, PartialEq, Eq)]
50pub struct LayoutSolve;
51
52/// Install the highest-priority layout engine registered into `app`, and
53/// return its name. An error says the build carries none.
54pub fn install_layout(app: &mut App) -> Result<&'static str, String> {
55    let backend = registered::<LayoutBackend>(app)
56        .preferred()
57        .ok_or_else(missing::<LayoutBackend>)?;
58    (backend.engine)().install(app);
59    Ok(backend.name)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use crate::backends::register_backend;
66    use crate::components::Transform;
67    use crate::tick::TickStage;
68    use bevy_ecs::prelude::*;
69
70    /// Moves every box one pixel right per tick, in [`LayoutSolve`].
71    struct Nudge;
72
73    fn nudge(mut boxes: Query<&mut Transform>) {
74        for mut t in &mut boxes {
75            t.absolute.x += 1.0;
76        }
77    }
78
79    impl LayoutEngine for Nudge {
80        fn install(self: Box<Self>, app: &mut App) {
81            app.add_systems(TickStage::LayoutSync, nudge.in_set(LayoutSolve));
82        }
83    }
84
85    fn nudge_engine() -> Box<dyn LayoutEngine> {
86        Box::new(Nudge)
87    }
88
89    /// Records the x a box had when it ran.
90    #[derive(Resource, Default)]
91    struct Seen(Vec<f32>);
92
93    fn after_solve(boxes: Query<&Transform>, mut seen: ResMut<Seen>) {
94        seen.0.extend(boxes.iter().map(|t| t.absolute.x));
95    }
96
97    /// A build with no engine says so instead of laying nothing out.
98    #[test]
99    fn a_build_without_an_engine_says_so() {
100        let mut app = App::new();
101        assert_eq!(
102            install_layout(&mut app).expect_err("none registered"),
103            "this build carries no layout backend"
104        );
105    }
106
107    /// The launch installs the engine the registry ranks first, and a
108    /// system ordered after [`LayoutSolve`] sees the boxes it wrote this
109    /// tick.
110    #[test]
111    fn the_preferred_engine_installs_and_its_solve_runs_first() {
112        let mut app = App::new();
113        register_backend(
114            &mut app,
115            LayoutBackend {
116                name: "nudge",
117                priority: 1,
118                engine: nudge_engine,
119            },
120        );
121        app.world.init_resource::<Seen>();
122        app.add_systems(TickStage::LayoutSync, after_solve.after(LayoutSolve));
123        assert_eq!(install_layout(&mut app), Ok("nudge"));
124        app.world.spawn(Transform::default());
125
126        app.tick();
127        app.tick();
128        assert_eq!(app.world.resource::<Seen>().0, [1.0, 2.0]);
129    }
130}