wisp/scene/flex_text.rs
1//! `FlexText` — late-pass textured-quad scene node.
2//!
3//! Structurally identical to [`Sprite`](crate::scene::Sprite): a
4//! textured quad with anchor + tint sampled from its container's
5//! transform. The difference is *when* the renderer paints it.
6//!
7//! Wisp's render-bucket order is **sprite → graphics → text**.
8//! Charts emit their bars / gridlines / axis lines as
9//! [`Graphics`](crate::scene::Graphics) primitives, so anything in
10//! the sprite pass renders *under* them. That's correct for
11//! background imagery but wrong for axis tick labels, legends, KPI
12//! big numbers — any text that has to read on top of the chart.
13//!
14//! `FlexText` participates in a **fourth render pass** that runs
15//! after [`Text`](crate::scene::Text) (the bitmap-font pipeline),
16//! using the same instanced textured-quad shader as
17//! [`Sprite`](crate::scene::Sprite). The intended producer is the
18//! flexible-text path: rasterise via
19//! [`crate::text::TextTexturePipeline::render`], call
20//! [`crate::texture::render_texture::RenderTexture::as_texture`],
21//! drop the resulting [`crate::texture::Texture`] into a `FlexText`.
22//!
23//! `FlexText` is *purposefully* not auto-named "Label" or "Overlay"
24//! — call it what it is. Charts can compose it for axis labels,
25//! callers can compose it for any "render this textured quad on top
26//! of graphics" use case.
27
28use glam::Vec2;
29
30use crate::color::Color;
31use crate::scene::container::Container;
32use crate::texture::Texture;
33
34/// Late-pass textured quad. Same data shape as
35/// [`Sprite`](crate::scene::Sprite); the render pipeline puts it in
36/// the fourth render pass so it paints after every
37/// [`Graphics`](crate::scene::Graphics) primitive.
38#[derive(Debug, Clone)]
39pub struct FlexText {
40 /// Scene-graph state (transform, alpha, visible, blend mode, parent / children).
41 pub container: Container,
42 /// GPU texture sampled in the fragment shader. Usually the
43 /// output of
44 /// [`crate::text::TextTexturePipeline::render`] +
45 /// [`crate::texture::render_texture::RenderTexture::as_texture`].
46 pub texture: Texture,
47 /// Normalized anchor in `[0, 1]²`. `0,0` = top-left, `0.5, 0.5`
48 /// = centre.
49 pub anchor: Vec2,
50 /// Multiplied with the sampled texel. Defaults to white so the
51 /// glyph colour baked into the source texture wins.
52 pub tint: Color,
53}
54
55impl FlexText {
56 /// Construct a `FlexText` from `texture` with default container,
57 /// top-left anchor, and white tint.
58 #[must_use]
59 pub fn from_texture(texture: Texture) -> Self {
60 Self {
61 container: Container::default(),
62 texture,
63 anchor: Vec2::ZERO,
64 tint: Color::WHITE,
65 }
66 }
67
68 /// Builder: set the anchor.
69 #[must_use]
70 pub fn with_anchor(mut self, anchor: Vec2) -> Self {
71 self.anchor = anchor;
72 self
73 }
74
75 /// Builder: set the tint.
76 #[must_use]
77 pub fn with_tint(mut self, tint: Color) -> Self {
78 self.tint = tint;
79 self
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::application::{AppConfig, Application};
87
88 fn boot_texture() -> Texture {
89 let app = pollster::block_on(Application::new(AppConfig::default())).expect("init");
90 let bytes = vec![255u8; 4 * 4 * 4];
91 Texture::from_rgba(&app, 4, 4, &bytes)
92 }
93
94 #[test]
95 fn from_texture_defaults() {
96 let texture = boot_texture();
97 let node = FlexText::from_texture(texture);
98 assert_eq!(node.anchor, Vec2::ZERO);
99 assert_eq!(node.tint, Color::WHITE);
100 }
101
102 #[test]
103 fn builder_with_anchor() {
104 let texture = boot_texture();
105 let node = FlexText::from_texture(texture).with_anchor(Vec2::splat(0.5));
106 assert_eq!(node.anchor, Vec2::splat(0.5));
107 }
108
109 #[test]
110 fn builder_with_tint() {
111 let texture = boot_texture();
112 let node = FlexText::from_texture(texture).with_tint(Color::RED);
113 assert_eq!(node.tint, Color::RED);
114 }
115}