Skip to main content

khora_data/flow/
ui.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! `UiFlow` — projects UI ECS data + text layout into a per-frame
16//! [`UiScene`](crate::ui::UiScene) published into the
17//! [`LaneBus`](khora_core::lane::LaneBus).
18//!
19//! `Flow::project` drives both stages — node extraction from the ECS and
20//! text layout via the `TextRenderer` service — because both are pure
21//! per-frame derivations of the world. Atlas allocation is **not** here:
22//! it requires GPU device access (lane territory) and produces structural
23//! GPU side effects, so the UI agent owns it and exposes the resulting
24//! `UiAtlasMap` to its lane through `LaneContext`.
25
26use std::sync::{Arc, RwLock};
27
28use khora_core::asset::font::Font;
29use khora_core::renderer::api::text::TextRenderer;
30use khora_core::renderer::GraphicsDevice;
31use khora_core::Runtime;
32
33use crate::assets::Assets;
34use crate::ecs::{SemanticDomain, World};
35use crate::flow::{Flow, Selection};
36use crate::register_flow;
37use crate::ui::components::{UiBorder, UiColor, UiImage, UiText, UiTransform};
38use crate::ui::{ExtractedUiNode, ExtractedUiText, UiScene};
39
40/// UI presentation Flow.
41///
42/// Deliberately **uncached** (no `cache_key` override): the projection
43/// depends on the surface size (runtime state) and on font assets that can
44/// hot-reload without any change signal the key could fold in, so a cached
45/// `UiScene` could go stale. It re-projects every tick.
46#[derive(Default)]
47pub struct UiFlow;
48
49impl Flow for UiFlow {
50    type View = UiScene;
51
52    const DOMAIN: SemanticDomain = SemanticDomain::Ui;
53    const NAME: &'static str = "ui";
54
55    fn project(&self, world: &World, _sel: &Selection, runtime: &Runtime) -> Self::View {
56        let surface_size = runtime
57            .backends
58            .get::<Arc<dyn GraphicsDevice>>()
59            .map(|d| d.get_surface_size())
60            .unwrap_or((0, 0));
61
62        let mut scene = UiScene {
63            surface_size,
64            ..Default::default()
65        };
66
67        extract_nodes(world, &mut scene);
68
69        if let (Some(text_renderer), Some(fonts_lock)) = (
70            runtime.backends.get::<Arc<dyn TextRenderer>>(),
71            runtime.resources.get::<Arc<RwLock<Assets<Font>>>>(),
72        ) {
73            if let Ok(fonts) = fonts_lock.read() {
74                layout_texts(world, text_renderer.as_ref(), &fonts, &mut scene);
75            }
76        }
77
78        scene.nodes.sort_by_key(|n| n.z_index);
79        scene.texts.sort_by_key(|t| t.z_index);
80        scene
81    }
82}
83
84register_flow!(UiFlow);
85
86fn extract_nodes(world: &World, scene: &mut UiScene) {
87    let query = world.query::<(
88        &UiTransform,
89        Option<&UiColor>,
90        Option<&UiBorder>,
91        Option<&UiImage>,
92    )>();
93    for (transform, color, border, image) in query {
94        scene.nodes.push(ExtractedUiNode {
95            pos: transform.pos,
96            size: transform.size,
97            color: color.copied(),
98            border: border.copied(),
99            image: image.copied(),
100            z_index: transform.z_index,
101        });
102    }
103}
104
105fn layout_texts(
106    world: &World,
107    text_renderer: &dyn TextRenderer,
108    fonts: &Assets<Font>,
109    scene: &mut UiScene,
110) {
111    for (transform, text) in world.query::<(&UiTransform, &UiText)>() {
112        if let Some(font_handle) = fonts.get(&text.font) {
113            let layout =
114                text_renderer.layout_text(&text.content, font_handle, text.font, text.size, None);
115            scene.texts.push(ExtractedUiText {
116                pos: transform.pos,
117                layout: std::sync::Arc::from(layout),
118                color: text.color,
119                z_index: transform.z_index,
120            });
121        }
122    }
123}