Skip to main content

lumen_core/
traits.rs

1//! The backend roles, plus the [`Bindable`] trait declaring a component as a property-bus participant.
2//!
3//! Each role is a trait a caller drives without naming an implementation. A [`WindowBackend`] runs the app and
4//! drives a [`Renderer`] and an [`A11yBackend`] through their traits; a [`LayoutEngine`] installs the systems that
5//! solve layout. Implementations register themselves into the registry of their kind (see [`crate::backends`]), and
6//! the launch picks from there.
7
8use crate::app::App;
9use crate::property_store::PropertyValue;
10use crate::window::WindowOptions;
11use bevy_ecs::component::Component;
12use bevy_ecs::world::World;
13use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
14use std::any::Any;
15use std::sync::Arc;
16use thiserror::Error;
17
18/// A live OS window a [`Renderer`] presents into.
19///
20/// The window backend owns the window and shares it as an
21/// `Arc<dyn RenderTarget>`. The renderer keeps that handle for as long as
22/// it holds a surface, so the window outlives every GPU object bound to
23/// it. The two handle traits are the platform-neutral vocabulary every
24/// desktop graphics API already speaks.
25pub trait RenderTarget: HasWindowHandle + HasDisplayHandle + Send + Sync + 'static {
26    /// Drawable size in physical pixels.
27    fn physical_size(&self) -> (u32, u32);
28}
29
30/// Where a [`Renderer`] puts its frames.
31#[derive(Clone)]
32pub enum FrameTarget {
33    /// An OS window: each frame is presented on screen.
34    Window(Arc<dyn RenderTarget>),
35    /// An image of `width` x `height` physical pixels that never reaches a
36    /// screen. Headless runs, screenshots, and tests read it back through
37    /// [`crate::render_world::SurfaceCapture`].
38    Offscreen {
39        /// Width in physical pixels.
40        width: u32,
41        /// Height in physical pixels.
42        height: u32,
43    },
44}
45
46impl std::fmt::Debug for FrameTarget {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            Self::Window(window) => f
50                .debug_tuple("Window")
51                .field(&window.physical_size())
52                .finish(),
53            Self::Offscreen { width, height } => f
54                .debug_struct("Offscreen")
55                .field("width", width)
56                .field("height", height)
57                .finish(),
58        }
59    }
60}
61
62/// Why a renderer could not bind to its target or put a frame on it.
63#[derive(Debug, Error)]
64pub enum RenderError {
65    /// Binding the renderer to its target failed: no adapter, no device,
66    /// or no usable surface format.
67    #[error("renderer init failed: {0}")]
68    Init(String),
69    /// Encoding, submitting, or presenting the frame failed.
70    #[error("present failed: {0}")]
71    Present(String),
72    /// A call that needs a target arrived before [`Renderer::attach`].
73    #[error("renderer is not attached to a target")]
74    Detached,
75}
76
77/// What the caller knows about the frame it is asking for.
78#[derive(Clone, Copy, Debug, Default)]
79pub struct FrameRequest {
80    /// The tick reported that render-relevant state changed. A clear flag
81    /// means nothing in the world moved, so the last presented frame is
82    /// still correct.
83    pub dirty: bool,
84    /// The target was just recreated (resize, DPI change), so whatever
85    /// the renderer had buffered is gone and the frame must be redrawn in
86    /// full even when the scene is unchanged.
87    pub force_full: bool,
88}
89
90/// A render backend's renderer: everything between the retained scene and
91/// the pixels.
92///
93/// The renderer owns scene assembly, encoding caches, damage tracking, and
94/// whatever its target needs (a swap chain, a window buffer, an offscreen
95/// image). No graphics-API type crosses the boundary, so a caller compiles
96/// without naming one. Frames are driven from the render world, which
97/// already holds the retained scene, the viewport, the text shaper, and the
98/// screenshot channel.
99///
100/// Construction does no work. [`Self::attach`] binds the renderer to a
101/// [`FrameTarget`]: a window backend attaches a window once it exists, and
102/// a headless launch attaches an offscreen image and hands the renderer to
103/// [`crate::render_backend::install_offscreen`], whose render-world system
104/// drives it each frame. A pending [`crate::render_world::SurfaceCapture`]
105/// request is answered from the frame [`Self::present`] puts up, on either
106/// kind of target.
107pub trait Renderer: Send + 'static {
108    /// Bind to `target`. Calling it again rebinds the renderer. A
109    /// [`RenderError::Init`] failure is what lets a launch try the next
110    /// backend.
111    fn attach(&mut self, target: FrameTarget) -> Result<(), RenderError>;
112
113    /// Reconfigure for a new physical size. Returns `true` when the size
114    /// actually changed, so callers can drop the relayout and repaint a
115    /// duplicate resize event would otherwise force.
116    fn resize(&mut self, width: u32, height: u32) -> bool;
117
118    /// Whether [`Self::present`] would put anything new up. The renderer
119    /// answers, because only it knows what its buffers still hold and how
120    /// precisely it can compare this scene against the last one it painted.
121    fn wants_present(&mut self, render_world: &mut World, request: FrameRequest) -> bool;
122
123    /// Paint, submit, and present one frame from the render world.
124    fn present(&mut self, render_world: &mut World) -> Result<(), RenderError>;
125
126    /// Release the target and everything behind it. A window backend calls
127    /// this while the platform connection is still alive, because tearing a
128    /// surface down after the display connection closes crashes some
129    /// drivers.
130    fn detach(&mut self);
131}
132
133/// A boxed renderer is a renderer, so a launch can hand the one the
134/// registry built to anything generic over [`Renderer`].
135impl<R: Renderer + ?Sized> Renderer for Box<R> {
136    fn attach(&mut self, target: FrameTarget) -> Result<(), RenderError> {
137        (**self).attach(target)
138    }
139
140    fn resize(&mut self, width: u32, height: u32) -> bool {
141        (**self).resize(width, height)
142    }
143
144    fn wants_present(&mut self, render_world: &mut World, request: FrameRequest) -> bool {
145        (**self).wants_present(render_world, request)
146    }
147
148    fn present(&mut self, render_world: &mut World) -> Result<(), RenderError> {
149        (**self).present(render_world)
150    }
151
152    fn detach(&mut self) {
153        (**self).detach();
154    }
155}
156
157/// A layout engine: what turns the styled tree into absolute boxes.
158///
159/// The engine contributes systems rather than being called per node or per
160/// tick. Layout is change-driven (style, children, text, and image changes
161/// mark entities dirty) and its state is usually not `Send`, so the work
162/// belongs in ordinary systems in [`crate::tick::TickStage::LayoutSync`] the
163/// scheduler runs, with change detection, like every other system. The
164/// launch dispatches through this trait once, when it installs the engine
165/// it selected from [`crate::layout_backend::LayoutBackends`].
166///
167/// The systems that write each entity's [`crate::components::Transform`]
168/// go in [`crate::layout_backend::LayoutSolve`], so code that needs this
169/// tick's boxes orders itself after that set without naming an engine.
170pub trait LayoutEngine: 'static {
171    /// Insert the engine's state and add its systems to `app`.
172    fn install(self: Box<Self>, app: &mut App);
173}
174
175/// Why a window backend could not run the app.
176#[derive(Debug, Error)]
177pub enum WindowError {
178    /// The platform event loop could not be created.
179    #[error("event loop creation failed: {0}")]
180    EventLoop(String),
181    /// The event loop ended with an error.
182    #[error("event loop run error: {0}")]
183    Run(String),
184    /// The backend was handed no renderer to present with.
185    #[error("no renderer to present with")]
186    NoRenderer,
187}
188
189/// A window backend: the OS window, the event loop, and the translation of
190/// platform events into the app's messages.
191///
192/// [`Self::run`] takes the built app and drives it until the window closes:
193/// it opens the window from [`WindowOptions`], attaches the first of
194/// `renderers` that binds to it, ticks the app on input and on the redraw
195/// requests in [`crate::window_backend::RedrawScheduler`], and presents
196/// through [`Renderer`]. The launch dispatches through this trait once, for
197/// the whole run, so nothing per event goes through a vtable it would not
198/// otherwise.
199pub trait WindowBackend: 'static {
200    /// Run `app` in a window. Blocks until the window closes.
201    ///
202    /// `renderers` are tried in order; one that fails to bind with
203    /// [`RenderError::Init`] gives way to the next. `a11y` builds the
204    /// accessibility bridge once the window exists, or is `None` for a run
205    /// without one.
206    fn run(
207        self: Box<Self>,
208        app: App,
209        options: WindowOptions,
210        renderers: Vec<Box<dyn Renderer>>,
211        a11y: Option<A11yBridgeFactory>,
212    ) -> Result<(), WindowError>;
213}
214
215/// Builds the accessibility bridge for a window once the window exists.
216///
217/// What a bridge needs from the window differs between window backends
218/// (a platform window handle, an event-loop handle, a way to wake the loop),
219/// so the factory is opaque here. Each window backend documents the factory
220/// type it accepts; [`Self::downcast`] recovers it, and a backend handed a
221/// factory built for another one runs without accessibility rather than
222/// failing.
223pub struct A11yBridgeFactory(Box<dyn Any>);
224
225impl A11yBridgeFactory {
226    /// Wrap `factory`, of the type the target window backend documents.
227    pub fn new<F: Any>(factory: F) -> Self {
228        Self(Box::new(factory))
229    }
230
231    /// The factory as `F`, or `self` back when it was built as another type.
232    pub fn downcast<F: Any>(self) -> Result<F, Self> {
233        self.0.downcast::<F>().map(|f| *f).map_err(Self)
234    }
235}
236
237impl std::fmt::Debug for A11yBridgeFactory {
238    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239        f.debug_tuple("A11yBridgeFactory").finish()
240    }
241}
242
243/// The bridge between the ECS world and the platform accessibility API.
244///
245/// The world-side half (walking the tree, translating roles and states)
246/// runs as a system in [`crate::tick::TickStage::A11ySync`] and leaves a
247/// pending update behind; the three methods here are what a window
248/// backend calls to keep the platform in step with it. Assistive
249/// technologies deliver their requests on their own threads, so an
250/// implementation queues them and applies the queue in [`Self::pump`], on
251/// the main thread, before the tick that reacts to them.
252pub trait A11yBackend: 'static {
253    /// Feed a platform window event to the bridge, before the window
254    /// backend handles it. The event is the window backend's own type; an
255    /// implementation downcasts it and ignores what it does not know.
256    fn window_event(&mut self, event: &dyn Any);
257
258    /// Apply queued assistive-technology requests (focus, click, value
259    /// changes, scroll-into-view) to the world.
260    fn pump(&mut self, world: &mut World);
261
262    /// Publish the tree update the A11ySync stage built, if one is
263    /// pending and an assistive technology is listening.
264    fn publish(&mut self, world: &mut World);
265}
266
267// The async seam carries value types (boxed futures, the service resources
268// that hold the selected backend), so it lives in [`crate::task`]. Re-exported
269// here because backends implement it alongside the traits above.
270pub use crate::task::{Spawn, Timer};
271
272/// Declares that a [`Component`] participates in the entity-property bus exposed by [`crate::property_store::PropertyStore`].
273///
274/// The intent is to collapse the `BindText` / `BindChecked` / `BindValue` zoo onto a single, type-erased property
275/// pipeline. The trait defines the shape; there is no registration call on [`crate::app::App`] yet, so implementing
276/// it does not wire anything up, and no component in the workspace implements it yet. The shape it is designed
277/// for is [`crate::components::TextContent`] (`NAME = "text"`, `Value = Arc<str>`).
278pub trait Bindable: Component {
279    /// Bus name for this component. Markup `bind-<NAME>="signal"` wires `PropertyKey::Entity(e, NAME)` to `PropertyKey::Global("signal")`.
280    const NAME: &'static str;
281
282    /// Typed value carried over the bus. Must round-trip through [`PropertyValue`].
283    type Value: Into<PropertyValue> + From<PropertyValue>;
284
285    /// Reads the component into its bus value.
286    fn read(&self) -> Self::Value;
287
288    /// Writes a bus value into the component.
289    fn write(&mut self, v: Self::Value);
290}
291
292#[cfg(test)]
293mod tests {
294    use super::{A11yBridgeFactory, FrameRequest, FrameTarget, RenderError, WindowError};
295
296    /// The default frame request asks for nothing. A window backend fills
297    /// it in from what the tick reported, so a default that leaned the
298    /// other way would repaint every frame of an idle app.
299    #[test]
300    fn the_default_frame_request_asks_for_nothing() {
301        let request = FrameRequest::default();
302        assert!(!request.dirty);
303        assert!(!request.force_full);
304    }
305
306    /// Render errors are printed to a person whose app just failed to
307    /// start or failed to draw, so each one has to say which of the two
308    /// happened and carry the backend's own reason.
309    #[test]
310    fn render_errors_say_what_failed() {
311        assert_eq!(
312            RenderError::Init("no adapter".into()).to_string(),
313            "renderer init failed: no adapter",
314        );
315        assert_eq!(
316            RenderError::Present("device lost".into()).to_string(),
317            "present failed: device lost",
318        );
319        assert_eq!(
320            RenderError::Detached.to_string(),
321            "renderer is not attached to a target",
322        );
323    }
324
325    /// An offscreen target prints its size, for the message a launch
326    /// writes when it cannot bind one.
327    #[test]
328    fn an_offscreen_target_prints_its_size() {
329        let target = FrameTarget::Offscreen {
330            width: 320,
331            height: 200,
332        };
333        let printed = format!("{target:?}");
334        assert!(
335            printed.contains("320") && printed.contains("200"),
336            "{printed}"
337        );
338    }
339
340    /// A window backend recovers the factory type it documents, and gets
341    /// the factory back untouched when it was built as something else.
342    #[test]
343    fn an_a11y_factory_downcasts_to_the_type_it_was_built_as() {
344        type Factory = fn() -> u8;
345        fn build() -> u8 {
346            7
347        }
348        let factory = A11yBridgeFactory::new(build as Factory);
349        let factory = factory.downcast::<String>().expect_err("not a String");
350        let build = factory
351            .downcast::<Factory>()
352            .expect("the type it was built as");
353        assert_eq!(build(), 7);
354    }
355
356    /// Window errors reach a person whose app did not open, through the
357    /// launch's `window: ...` message.
358    #[test]
359    fn window_errors_say_what_failed() {
360        assert_eq!(
361            WindowError::EventLoop("no display".into()).to_string(),
362            "event loop creation failed: no display",
363        );
364        assert_eq!(
365            WindowError::NoRenderer.to_string(),
366            "no renderer to present with",
367        );
368    }
369}