Skip to main content

nightshade_api/
text.rs

1//! Screen text anchored to a corner or the center, and 3d labels.
2
3use crate::runner::UI_ROOT_NAME;
4use nightshade::ecs::ui::state::UiStateTrait;
5use nightshade::ecs::ui::units::UiValue;
6use nightshade::prelude::*;
7
8/// Where screen text sits, anchored to a window corner or the center.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ScreenAnchor {
11    TopLeft,
12    TopRight,
13    BottomLeft,
14    BottomRight,
15    Center,
16}
17
18/// Spawns screen text at the given anchor. It renders as a retained UI overlay
19/// that stays fixed to the window, on top of the scene. Update the content with
20/// [`set_text`], restyle it with [`set_text_color`] and [`set_text_size`].
21pub fn spawn_text(world: &mut World, text: &str, anchor: ScreenAnchor) -> Entity {
22    let root = ui_root(world);
23    let (position, anchor_kind, alignment) = anchor_window(anchor);
24    let entity = {
25        let mut tree = UiTreeBuilder::from_parent(world, root);
26        tree.add_node()
27            .window(position, Ab(vec2(960.0, 96.0)), anchor_kind)
28            .with_text(text, 18.0)
29            .with_text_alignment(alignment, VerticalAlignment::Middle)
30            .with_text_outline(Vec4::new(0.0, 0.0, 0.0, 1.0), 0.15)
31            .color_raw::<UiBase>(Vec4::new(1.0, 1.0, 1.0, 1.0))
32            .without_pointer_events()
33            .entity()
34    };
35    ui_mark_render_dirty(world);
36    entity
37}
38
39/// Spawns 3d text at `position` that always faces the camera.
40pub fn spawn_label(world: &mut World, text: &str, position: Vec3) -> Entity {
41    spawn_3d_billboard_text_with_properties(
42        world,
43        text,
44        position,
45        TextProperties {
46            font_size: 24.0,
47            alignment: TextAlignment::Center,
48            vertical_alignment: VerticalAlignment::Middle,
49            ..Default::default()
50        },
51    )
52}
53
54/// Sets a text entity's color as linear RGBA. Works on screen text and 3d
55/// labels alike.
56pub fn set_text_color(world: &mut World, entity: Entity, color: [f32; 4]) {
57    let rgba = Vec4::new(color[0], color[1], color[2], color[3]);
58    if is_screen_text(world, entity) {
59        if let Some(node_color) = world.ui.get_ui_node_color_mut(entity) {
60            node_color.colors[UiBase::INDEX] = Some(rgba);
61        }
62        ui_mark_render_dirty(world);
63        return;
64    }
65    if let Some(component) = world.core.get_text_mut(entity) {
66        component.set_color(rgba);
67    }
68}
69
70/// Sets a text entity's font size. The same scale [`spawn_text`] and
71/// [`spawn_label`] start from, so 18 to 48 reads well.
72pub fn set_text_size(world: &mut World, entity: Entity, size: f32) {
73    if is_screen_text(world, entity) {
74        if let Some(UiNodeContent::Text {
75            font_size_override, ..
76        }) = world.ui.get_ui_node_content_mut(entity)
77        {
78            *font_size_override = Some(size);
79        }
80        ui_mark_render_dirty(world);
81        return;
82    }
83    if let Some(component) = world.core.get_text_mut(entity) {
84        component.set_font_size(size);
85    }
86}
87
88/// Replaces the content of a text entity. Skips all work when the text is
89/// unchanged, so calling it every frame with a formatted string is fine.
90pub fn set_text(world: &mut World, entity: Entity, text: &str) {
91    if let Some(slot) = screen_text_slot(world, entity) {
92        if world.resources.text.cache.get_text(slot) == Some(text) {
93            return;
94        }
95        world.resources.text.cache.set_text(slot, text);
96        ui_mark_render_dirty(world);
97        return;
98    }
99    let Some(text_index) = world
100        .core
101        .get_text(entity)
102        .map(|component| component.text_index)
103    else {
104        return;
105    };
106    if world.resources.text.cache.get_text(text_index) == Some(text) {
107        return;
108    }
109    world.resources.text.cache.set_text(text_index, text);
110    if let Some(component) = world.core.get_text_mut(entity) {
111        component.dirty = true;
112    }
113}
114
115fn ui_root(world: &mut World) -> Entity {
116    if let Some(&root) = world.resources.entities.names.get(UI_ROOT_NAME)
117        && world.ui.get_ui_layout_root(root).is_some()
118    {
119        return root;
120    }
121    let root = UiTreeBuilder::new(world).finish();
122    world
123        .resources
124        .entities
125        .names
126        .insert(UI_ROOT_NAME.to_string(), root);
127    root
128}
129
130fn is_screen_text(world: &World, entity: Entity) -> bool {
131    matches!(
132        world.ui.get_ui_node_content(entity),
133        Some(UiNodeContent::Text { .. })
134    )
135}
136
137fn screen_text_slot(world: &World, entity: Entity) -> Option<usize> {
138    match world.ui.get_ui_node_content(entity) {
139        Some(UiNodeContent::Text { text_slot, .. }) => Some(*text_slot),
140        _ => None,
141    }
142}
143
144fn anchor_window(anchor: ScreenAnchor) -> (UiValue<Vec2>, Anchor, TextAlignment) {
145    match anchor {
146        ScreenAnchor::TopLeft => (
147            Ab(vec2(20.0, 16.0)).into(),
148            Anchor::TopLeft,
149            TextAlignment::Left,
150        ),
151        ScreenAnchor::TopRight => (
152            Rl(vec2(100.0, 0.0)) + Ab(vec2(-20.0, 16.0)),
153            Anchor::TopRight,
154            TextAlignment::Right,
155        ),
156        ScreenAnchor::BottomLeft => (
157            Rl(vec2(0.0, 100.0)) + Ab(vec2(20.0, -16.0)),
158            Anchor::BottomLeft,
159            TextAlignment::Left,
160        ),
161        ScreenAnchor::BottomRight => (
162            Rl(vec2(100.0, 100.0)) + Ab(vec2(-20.0, -16.0)),
163            Anchor::BottomRight,
164            TextAlignment::Right,
165        ),
166        ScreenAnchor::Center => (
167            Rl(vec2(50.0, 50.0)).into(),
168            Anchor::Center,
169            TextAlignment::Center,
170        ),
171    }
172}