tessera_ui_basic_components/pipelines/
text.rs

1mod command;
2
3use std::sync::OnceLock;
4
5use glyphon::fontdb;
6use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
7use tessera_ui::{Color, DrawablePipeline, PxPosition, PxSize, wgpu};
8
9pub use command::{TextCommand, TextConstraint};
10
11/// It costs a lot to create a glyphon font system, so we use a static one
12/// to share it every where and avoid creating it multiple times.
13static FONT_SYSTEM: OnceLock<RwLock<glyphon::FontSystem>> = OnceLock::new();
14
15#[cfg(target_os = "android")]
16fn init_font_system() -> RwLock<glyphon::FontSystem> {
17    let mut font_system = glyphon::FontSystem::new();
18
19    font_system.db_mut().load_fonts_dir("/system/fonts");
20    font_system.db_mut().set_sans_serif_family("Roboto");
21    font_system.db_mut().set_serif_family("Noto Serif");
22    font_system.db_mut().set_monospace_family("Droid Sans Mono");
23    font_system.db_mut().set_cursive_family("Dancing Script");
24    font_system.db_mut().set_fantasy_family("Dancing Script");
25
26    RwLock::new(font_system)
27}
28
29#[cfg(not(target_os = "android"))]
30fn init_font_system() -> RwLock<glyphon::FontSystem> {
31    RwLock::new(glyphon::FontSystem::new())
32}
33
34/// It costs a lot to create a glyphon font system, so we use a static one
35/// to share it every where and avoid creating it multiple times.
36/// This function returns a read lock of the font system.
37pub fn read_font_system() -> RwLockReadGuard<'static, glyphon::FontSystem> {
38    FONT_SYSTEM.get_or_init(init_font_system).read()
39}
40
41/// It costs a lot to create a glyphon font system, so we use a static one
42/// to share it every where and avoid creating it multiple times.
43/// This function returns a write lock of the font system.
44pub fn write_font_system() -> RwLockWriteGuard<'static, glyphon::FontSystem> {
45    FONT_SYSTEM.get_or_init(init_font_system).write()
46}
47
48/// A text renderer
49pub struct GlyphonTextRender {
50    /// Glyphon font atlas, a heavy-weight, shared resource.
51    atlas: glyphon::TextAtlas,
52    /// Glyphon cache, a heavy-weight, shared resource.
53    #[allow(unused)]
54    cache: glyphon::Cache,
55    /// Glyphon viewport, holds screen-size related buffers.
56    viewport: glyphon::Viewport,
57    /// Glyphon swash cache, a CPU-side cache for glyph rasterization.
58    swash_cache: glyphon::SwashCache,
59    /// The multisample state, needed for creating temporary renderers.
60    msaa: wgpu::MultisampleState,
61}
62
63impl GlyphonTextRender {
64    /// Create a new text renderer pipeline.
65    pub fn new(
66        gpu: &wgpu::Device,
67        queue: &wgpu::Queue,
68        config: &wgpu::SurfaceConfiguration,
69        sample_count: u32,
70    ) -> Self {
71        let cache = glyphon::Cache::new(gpu);
72        let atlas = glyphon::TextAtlas::new(gpu, queue, &cache, config.format);
73        let viewport = glyphon::Viewport::new(gpu, &cache);
74        let swash_cache = glyphon::SwashCache::new();
75        let msaa = wgpu::MultisampleState {
76            count: sample_count,
77            mask: !0,
78            alpha_to_coverage_enabled: false,
79        };
80
81        Self {
82            atlas,
83            cache,
84            viewport,
85            swash_cache,
86            msaa,
87        }
88    }
89}
90
91#[allow(unused_variables)]
92impl DrawablePipeline<TextCommand> for GlyphonTextRender {
93    fn draw(
94        &mut self,
95        gpu: &wgpu::Device,
96        gpu_queue: &wgpu::Queue,
97        config: &wgpu::SurfaceConfiguration,
98        render_pass: &mut wgpu::RenderPass<'_>,
99        command: &TextCommand,
100        size: PxSize,
101        start_pos: PxPosition,
102        _scene_texture_view: &wgpu::TextureView,
103    ) {
104        // Create a new, temporary TextRenderer for each draw call.
105        // This is necessary to avoid state conflicts when rendering multiple
106        // text elements interleaved with other components. It correctly
107        // isolates the `prepare` call for each text block.
108        let mut text_renderer = glyphon::TextRenderer::new(&mut self.atlas, gpu, self.msaa, None);
109
110        self.viewport.update(
111            gpu_queue,
112            glyphon::Resolution {
113                width: config.width,
114                height: config.height,
115            },
116        );
117
118        let text_areas = std::iter::once(command.data.text_area(start_pos));
119
120        text_renderer
121            .prepare(
122                gpu,
123                gpu_queue,
124                &mut write_font_system(),
125                &mut self.atlas,
126                &self.viewport,
127                text_areas,
128                &mut self.swash_cache,
129            )
130            .unwrap();
131
132        text_renderer
133            .render(&self.atlas, &self.viewport, render_pass)
134            .unwrap();
135    }
136}
137
138#[derive(Debug, Clone)]
139pub struct TextData {
140    /// glyphon text buffer
141    text_buffer: glyphon::Buffer,
142    /// text area size
143    pub size: [u32; 2],
144}
145
146impl TextData {
147    /// Prepare all text datas before rendering
148    /// returns the text data buffer
149    /// Notice that we must specify the text position
150    /// before rendering its return value
151    pub fn new(
152        text: String,
153        color: Color,
154        size: f32,
155        line_height: f32,
156        constraint: TextConstraint,
157    ) -> TextData {
158        // Create text buffer
159        let mut text_buffer = glyphon::Buffer::new(
160            &mut write_font_system(),
161            glyphon::Metrics::new(size, line_height),
162        );
163        let color = glyphon::Color::rgba(
164            (color.r * 255.0) as u8,
165            (color.g * 255.0) as u8,
166            (color.b * 255.0) as u8,
167            (color.a * 255.0) as u8,
168        );
169        text_buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
170        text_buffer.set_size(
171            &mut write_font_system(),
172            constraint.max_width,
173            constraint.max_height,
174        );
175        text_buffer.set_text(
176            &mut write_font_system(),
177            &text,
178            &glyphon::Attrs::new()
179                .family(fontdb::Family::SansSerif)
180                .color(color),
181            glyphon::Shaping::Advanced,
182        );
183        text_buffer.shape_until_scroll(&mut write_font_system(), false);
184        // Calculate text bounds
185        // Get the layout runs
186        let mut run_width: f32 = 0.0;
187        // Calculate the line height based on the number of lines
188        let line_height =
189            text_buffer.layout_runs().count() as f32 * text_buffer.metrics().line_height;
190        for run in text_buffer.layout_runs() {
191            // Take the max. width of all lines.
192            run_width = run_width.max(run.line_w);
193        }
194        // build text data
195        Self {
196            text_buffer,
197            size: [run_width as u32, line_height as u32],
198        }
199    }
200
201    pub fn from_buffer(text_buffer: glyphon::Buffer) -> Self {
202        // Calculate text bounds
203        // Get the layout runs
204        let mut run_width: f32 = 0.0;
205        // Calculate the line height based on the number of lines
206        let line_height =
207            text_buffer.layout_runs().count() as f32 * text_buffer.metrics().line_height;
208        for run in text_buffer.layout_runs() {
209            // Take the max. width of all lines.
210            run_width = run_width.max(run.line_w);
211        }
212        // build text data
213        Self {
214            text_buffer,
215            size: [run_width as u32, line_height as u32],
216        }
217    }
218
219    /// Get the glyphon text area from the text data
220    fn text_area(&'_ self, start_pos: PxPosition) -> glyphon::TextArea<'_> {
221        let bounds = glyphon::TextBounds {
222            left: start_pos.x.raw(),
223            top: start_pos.y.raw(),
224            right: start_pos.x.raw() + self.size[0] as i32,
225            bottom: start_pos.y.raw() + self.size[1] as i32,
226        };
227        glyphon::TextArea {
228            buffer: &self.text_buffer,
229            left: start_pos.x.to_f32(),
230            top: start_pos.y.to_f32(),
231            scale: 1.0,
232            bounds,
233            default_color: glyphon::Color::rgb(0, 0, 0), // Black by default
234            custom_glyphs: &[],
235        }
236    }
237}