grafo/
text.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
//! Text rendering for the Grafo library.
//!
//! This module provides functionality to render text using the `glyphon` crate.
//!
//! # Examples
//!
//! Rendering text with specific layout:
//!
//! ```rust
//! use grafo::{TextAlignment, TextLayout};
//! use grafo::Color;
//! use grafo::MathRect;
//!
//! // Define the text layout
//! let layout = TextLayout {
//!     font_size: 16.0,
//!     line_height: 20.0,
//!     color: Color::rgb(255, 255, 255), // White text
//!     area: MathRect {
//!         min: (0.0, 0.0).into(),
//!         max: (200.0, 50.0).into(),
//!     },
//!     horizontal_alignment: TextAlignment::Center,
//!     vertical_alignment: TextAlignment::Center,
//! };
//!
//! // Usage of layout in rendering functions (pseudo-code)
//! // renderer.render_text("Hello, World!", layout);
//! ```

use crate::renderer::MathRect;
use crate::Color;
use glyphon::cosmic_text::Align;
use glyphon::{Attrs, Family, FontSystem, Metrics, Shaping, SwashCache, TextAtlas, TextRenderer};
use glyphon::{Buffer as TextBuffer, Color as TextColor, TextArea, TextBounds};
use wgpu::{Device, MultisampleState};

/// Specifies the alignment of text within its layout area.
///
/// # Variants
///
/// - `Start`: Align text to the start (left or right, depending on language).
/// - `End`: Align text to the end.
/// - `Center`: Center-align the text.
///
/// # Examples
///
/// ```rust
/// use grafo::TextAlignment;
///
/// let align_start = TextAlignment::Start;
/// let align_end = TextAlignment::End;
/// let align_center = TextAlignment::Center;
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum TextAlignment {
    /// Align text to the start (left or right, depending on language).
    Start,
    /// Align text to the end.
    End,
    /// Center-align the text.
    Center,
}

/// Defines the layout parameters for rendering text.
///
/// # Fields
///
/// - `font_size`: The size of the font in pixels.
/// - `line_height`: The height of each line of text.
/// - `color`: The color of the text.
/// - `area`: The rectangular area within which the text is rendered.
/// - `horizontal_alignment`: The horizontal alignment of the text.
/// - `vertical_alignment`: The vertical alignment of the text.
///
/// # Examples
///
/// ```rust
/// use grafo::{TextAlignment, TextLayout};
/// use grafo::Color;
/// use grafo::MathRect;
///
/// let layout = TextLayout {
///     font_size: 16.0,
///     line_height: 20.0,
///     color: Color::rgb(255, 255, 255), // White text
///     area: MathRect {
///         min: (0.0, 0.0).into(),
///         max: (200.0, 50.0).into(),
///     },
///     horizontal_alignment: TextAlignment::Center,
///     vertical_alignment: TextAlignment::Center,
/// };
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextLayout {
    /// The size of the font in pixels.
    pub font_size: f32,
    /// The height of each line of text.
    pub line_height: f32,
    /// The color of the text.
    pub color: Color,
    /// The rectangular area within which the text is rendered.
    pub area: MathRect,
    /// The horizontal alignment of the text.
    pub horizontal_alignment: TextAlignment,
    /// The vertical alignment of the text.
    pub vertical_alignment: TextAlignment,
}

/// Internal wrapper for `glyphon::TextRenderer` and related components.
///
/// This struct manages the text renderer, atlas, font system, and swash cache.
pub(crate) struct TextRendererWrapper {
    pub(crate) text_renderer: TextRenderer,
    pub(crate) atlas: TextAtlas,
    pub(crate) font_system: FontSystem,
    pub(crate) swash_cache: SwashCache,
}

impl TextRendererWrapper {
    /// Creates a new `TextRendererWrapper`.
    ///
    /// # Parameters
    ///
    /// - `device`: The WGPU device.
    /// - `queue`: The WGPU queue.
    /// - `swapchain_format`: The format of the swapchain texture.
    /// - `depth_stencil_state`: Optional depth stencil state.
    pub fn new(
        device: &Device,
        queue: &wgpu::Queue,
        swapchain_format: wgpu::TextureFormat,
        depth_stencil_state: Option<wgpu::DepthStencilState>,
        glyphon_cache: &glyphon::Cache,
    ) -> Self {
        let font_system = FontSystem::new();
        let swash_cache = SwashCache::new();
        let mut atlas = TextAtlas::new(device, queue, glyphon_cache, swapchain_format);
        let text_renderer = TextRenderer::new(
            &mut atlas,
            device,
            MultisampleState::default(),
            depth_stencil_state,
        );

        Self {
            text_renderer,
            atlas,
            font_system,
            swash_cache,
        }
    }
}

#[derive(Debug)]
pub(crate) struct TextDrawData {
    /// The text buffer containing glyph information.
    pub(crate) text_buffer: TextBuffer,
    /// The area within which the text is rendered.
    pub(crate) area: MathRect,
    /// The vertical alignment of the text.
    #[allow(unused)]
    pub(crate) vertical_alignment: TextAlignment,
    /// The top position of the text within the layout area.
    pub(crate) top: f32,
    /// The color of the text.
    pub(crate) color: Color,
}

impl TextDrawData {
    pub fn new(
        text: &str,
        layout: impl Into<TextLayout>,
        clip_to_shape: Option<usize>,
        scale_factor: f32,
        font_system: &mut FontSystem,
        font_family: Family,
    ) -> Self {
        let layout = layout.into();

        let mut buffer = TextBuffer::new(
            font_system,
            Metrics::new(layout.font_size, layout.line_height),
        );

        let text_area_size = layout.area.size();

        buffer.set_size(
            font_system,
            Some(text_area_size.width),
            Some(text_area_size.height),
        );

        // TODO: it's set text that causes performance issues
        buffer.set_text(
            font_system,
            text,
            Attrs::new()
                .family(font_family)
                .metadata(clip_to_shape.unwrap_or(0)),
            Shaping::Advanced,
        );

        let align = match layout.horizontal_alignment {
            // None is equal to start of the line - left or right, depending on the language
            TextAlignment::Start => None,
            TextAlignment::End => Some(Align::End),
            TextAlignment::Center => Some(Align::Center),
        };

        for line in buffer.lines.iter_mut() {
            line.set_align(align);
        }

        let area = layout.area;

        let mut min_y = f32::INFINITY;
        let mut max_y = f32::NEG_INFINITY;

        buffer.shape_until_scroll(font_system, false);

        for layout_run in buffer.layout_runs() {
            for glyph in layout_run.glyphs.iter() {
                let physical_glyph = glyph.physical((0.0, 0.0), scale_factor);
                min_y = min_y.min(physical_glyph.y as f32 + layout_run.line_y);
                max_y = max_y.max(physical_glyph.y as f32 + layout_run.line_y);
            }
        }

        // TODO: that should be a line height
        let buffer_height = if max_y > min_y {
            max_y - min_y + layout.font_size
        } else {
            layout.font_size // for a single line
        };

        let top = match layout.vertical_alignment {
            TextAlignment::Start => area.min.y,
            TextAlignment::End => area.max.y - buffer_height,
            TextAlignment::Center => area.min.y + (area.height() - buffer_height) / 2.0,
        };

        // println!("Text {} is clipped to shape {}", text, clip_to_shape.unwrap_or(0));

        TextDrawData {
            top,
            text_buffer: buffer,
            area: layout.area,
            vertical_alignment: layout.vertical_alignment,
            color: layout.color,
        }
    }
    pub fn to_text_area(&self, scale_factor: f32) -> TextArea {
        let area = self.area;
        let top = self.top;

        TextArea {
            buffer: &self.text_buffer,
            left: area.min.x * scale_factor,
            top: top * scale_factor,
            scale: scale_factor,
            bounds: TextBounds {
                left: (area.min.x * scale_factor) as i32,
                top: (area.min.y * scale_factor) as i32,
                right: (area.max.x * scale_factor) as i32,
                bottom: (area.max.y * scale_factor) as i32,
            },
            default_color: TextColor::rgba(
                self.color.0[1],
                self.color.0[2],
                self.color.0[3],
                self.color.0[0],
            ),
            custom_glyphs: &[],
        }
    }
}