Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_gizmos/src/stroke_text.rs
9356 views
1
//! This module draws text gizmos using a stroke font.
2
3
use crate::simplex_stroke_font::*;
4
use crate::{gizmos::GizmoBuffer, prelude::GizmoConfigGroup};
5
use bevy_color::Color;
6
use bevy_math::{vec2, Isometry2d, Isometry3d, Vec2};
7
use core::ops::Range;
8
9
/// A stroke font containing glyphs for the 95 printable ASCII codes.
10
pub struct StrokeFont<'a> {
11
/// Baseline-to-baseline line height ratio.
12
pub line_height: f32,
13
/// Full glyph height (cap + descender) in font units.
14
pub height: f32,
15
/// Cap height in font units.
16
pub cap_height: f32,
17
/// Advance used for unsupported glyphs.
18
pub advance: i8,
19
/// Raw glyph point positions.
20
pub positions: &'a [[i8; 2]],
21
/// Stroke ranges into `positions`.
22
pub strokes: &'a [Range<usize>],
23
/// Glyph advances and stroke ranges.
24
pub glyphs: &'a [(i8, Range<usize>); 95],
25
}
26
27
impl<'a> StrokeFont<'a> {
28
/// Builds a `StrokeTextLayout` for `text` at the requested `font_size`.
29
pub fn layout(&'a self, text: &'a str, font_size: f32) -> StrokeTextLayout<'a> {
30
let scale = font_size / SIMPLEX_CAP_HEIGHT;
31
let glyph_height = SIMPLEX_HEIGHT * scale;
32
let line_height = LINE_HEIGHT * glyph_height;
33
let margin_top = line_height - glyph_height;
34
let space_advance = SIMPLEX_GLYPHS[0].0 as f32 * scale;
35
StrokeTextLayout {
36
font: self,
37
scale,
38
line_height,
39
margin_top,
40
space_advance,
41
text,
42
}
43
}
44
45
fn get_glyph_index(&self, c: char) -> Option<usize> {
46
let code = c as u32;
47
if (0x20..=0x7E).contains(&code) {
48
Some(code as usize - 0x20)
49
} else {
50
None
51
}
52
}
53
54
/// Get the advance and stroke point ranges for a glyph.
55
pub fn get_glyph(&self, c: char) -> Option<(i8, Range<usize>)> {
56
Some(self.glyphs[self.get_glyph_index(c)?].clone())
57
}
58
59
/// Get the advance for a glyph.
60
pub fn get_glyph_advance(&self, c: char) -> Option<i8> {
61
Some(self.glyphs[self.get_glyph_index(c)?].0)
62
}
63
}
64
65
/// Stroke text layout
66
pub struct StrokeTextLayout<'a> {
67
/// The unscaled font
68
font: &'a StrokeFont<'a>,
69
/// The text
70
text: &'a str,
71
/// Scale applied to the raw glyph positions.
72
scale: f32,
73
/// Height of each line of text.
74
line_height: f32,
75
/// Space between top of line and cap height.
76
margin_top: f32,
77
/// Width of a space.
78
space_advance: f32,
79
}
80
81
impl<'a> StrokeTextLayout<'a> {
82
/// Computes the width and height of a text layout with this font and
83
/// the given text.
84
///
85
/// Returns the layout size in pixels.
86
pub fn measure(&self) -> Vec2 {
87
let mut layout_size = vec2(0., self.line_height);
88
89
let mut line_width = 0.;
90
for c in self.text.chars() {
91
if c == '\n' {
92
layout_size.x = layout_size.x.max(line_width);
93
line_width = 0.;
94
layout_size.y += self.line_height;
95
continue;
96
}
97
98
line_width += self
99
.font
100
.get_glyph_advance(c)
101
.map(|advance| advance as f32 * self.scale)
102
.unwrap_or(self.space_advance);
103
}
104
105
layout_size.x = layout_size.x.max(line_width);
106
layout_size
107
}
108
109
/// Returns an iterator over the font strokes for this text layout,
110
/// grouped into polylines of `Vec2` points.
111
pub fn render(&'a self) -> impl Iterator<Item = impl Iterator<Item = Vec2>> + 'a {
112
let mut chars = self.text.chars();
113
let mut x = 0.0;
114
let mut y = -self.margin_top;
115
let mut current_strokes: Range<usize> = 0..0;
116
let mut current_x = 0.0;
117
118
core::iter::from_fn(move || loop {
119
if !current_strokes.is_empty() {
120
for stroke_index in current_strokes.by_ref() {
121
let stroke = self.font.strokes[stroke_index].clone();
122
if stroke.len() < 2 {
123
continue;
124
}
125
126
return Some(stroke.map(move |index| {
127
let [p, q] = self.font.positions[index];
128
Vec2::new(
129
current_x + self.scale * p as f32,
130
y - self.scale * (self.font.cap_height - q as f32),
131
)
132
}));
133
}
134
}
135
136
let c = chars.next()?;
137
if c == '\n' {
138
x = 0.0;
139
y -= self.line_height;
140
continue;
141
}
142
143
let Some((advance, strokes)) = self.font.get_glyph(c) else {
144
x += self.space_advance;
145
continue;
146
};
147
current_strokes = strokes;
148
current_x = x;
149
150
x += advance as f32 * self.scale;
151
})
152
}
153
}
154
155
impl<Config, Clear> GizmoBuffer<Config, Clear>
156
where
157
Config: GizmoConfigGroup,
158
Clear: 'static + Send + Sync,
159
{
160
/// Draw text using a stroke font with the given isometry applied.
161
///
162
/// Only ASCII characters in the range 32–126 are supported.
163
///
164
/// # Arguments
165
///
166
/// - `isometry`: defines the translation and rotation of the text.
167
/// - `text`: the text to be drawn.
168
/// - `size`: the size of the text in pixels.
169
/// - `anchor`: normalized anchor point relative to the text bounds,
170
/// where `(0, 0)` is centered, `(-0.5, 0.5)` is top-left,
171
/// and `(0.5, -0.5)` is bottom-right.
172
/// - `color`: the color of the text.
173
///
174
/// # Example
175
/// ```
176
/// # use bevy_gizmos::prelude::*;
177
/// # use bevy_math::prelude::*;
178
/// # use bevy_color::Color;
179
/// fn system(mut gizmos: Gizmos) {
180
/// gizmos.text(Isometry3d::IDENTITY, "text gizmo", 25., Vec2::ZERO, Color::WHITE);
181
/// }
182
/// # bevy_ecs::system::assert_is_system(system);
183
/// ```
184
pub fn text(
185
&mut self,
186
isometry: impl Into<Isometry3d>,
187
text: &str,
188
font_size: f32,
189
anchor: Vec2,
190
color: impl Into<Color>,
191
) {
192
let isometry: Isometry3d = isometry.into();
193
let color = color.into();
194
let layout = SIMPLEX_STROKE_FONT.layout(text, font_size);
195
let layout_anchor = layout.measure() * (vec2(-0.5, 0.5) - anchor);
196
for points in layout.render() {
197
self.linestrip(
198
points.map(|point| isometry * (layout_anchor + point).extend(0.)),
199
color,
200
);
201
}
202
}
203
204
/// Draw text using a stroke font in 2d with the given isometry applied.
205
///
206
/// Only ASCII characters in the range 32–126 are supported.
207
///
208
/// # Arguments
209
///
210
/// - `isometry`: defines the translation and rotation of the text.
211
/// - `text`: the text to be drawn.
212
/// - `size`: the size of the text.
213
/// - `anchor`: normalized anchor point relative to the text bounds,
214
/// where `(0., 0.)` is centered, `(-0.5, 0.5)` is top-left,
215
/// and `(0.5, -0.5)` is bottom-right.
216
/// - `color`: the color of the text.
217
///
218
/// # Example
219
/// ```
220
/// # use bevy_gizmos::prelude::*;
221
/// # use bevy_math::prelude::*;
222
/// # use bevy_color::Color;
223
/// fn system(mut gizmos: Gizmos) {
224
/// gizmos.text_2d(Isometry2d::IDENTITY, "2D text gizmo", 25., Vec2::ZERO, Color::WHITE);
225
/// }
226
/// # bevy_ecs::system::assert_is_system(system);
227
/// ```
228
pub fn text_2d(
229
&mut self,
230
isometry: impl Into<Isometry2d>,
231
text: &str,
232
font_size: f32,
233
anchor: Vec2,
234
color: impl Into<Color>,
235
) {
236
let isometry: Isometry2d = isometry.into();
237
let color = color.into();
238
let layout = SIMPLEX_STROKE_FONT.layout(text, font_size);
239
let layout_anchor = layout.measure() * (vec2(-0.5, 0.5) - anchor);
240
for points in layout.render() {
241
self.linestrip_2d(
242
points.map(|point| isometry * (layout_anchor + point)),
243
color,
244
);
245
}
246
}
247
}
248
249