1use cosmic_text::SwashImage;
34
35#[derive(Debug, Clone, Copy, PartialEq, Default)]
37pub enum TextRenderMode {
38 #[default]
40 Bitmap,
41 SDF {
43 spread: f32,
47 },
48}
49
50impl TextRenderMode {
51 pub fn is_sdf(&self) -> bool {
53 matches!(self, Self::SDF { .. })
54 }
55
56 pub fn spread(&self) -> f32 {
58 match self {
59 Self::SDF { spread } => *spread,
60 Self::Bitmap => 0.0,
61 }
62 }
63}
64
65pub fn generate_sdf(source: &SwashImage, spread: f32) -> Vec<u8> {
84 let width = source.placement.width as usize;
85 let height = source.placement.height as usize;
86
87 if width == 0 || height == 0 {
88 return Vec::new();
89 }
90
91 let mut output = vec![0u8; width * height];
92 let threshold = 128u8;
93
94 for y in 0..height {
96 for x in 0..width {
97 let idx = y * width + x;
98 let value = source.data[idx];
99
100 let inside = value >= threshold;
102
103 let mut min_dist = spread;
105
106 let search_radius = (spread.ceil() as i32) + 1;
108
109 for dy in -search_radius..=search_radius {
110 for dx in -search_radius..=search_radius {
111 let nx = x as i32 + dx;
112 let ny = y as i32 + dy;
113
114 if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
116 continue;
117 }
118
119 let nidx = ny as usize * width + nx as usize;
120 let neighbor_value = source.data[nidx];
121 let neighbor_inside = neighbor_value >= threshold;
122
123 if inside != neighbor_inside {
125 let dist = ((dx * dx + dy * dy) as f32).sqrt();
126 min_dist = min_dist.min(dist);
127 }
128 }
129 }
130
131 let normalized = (min_dist / spread).clamp(0.0, 1.0);
133
134 let sdf_value = if inside {
138 127.0 + normalized * 128.0
139 } else {
140 127.0 - normalized * 127.0
141 };
142
143 output[idx] = sdf_value.clamp(0.0, 255.0) as u8;
144 }
145 }
146
147 output
148}
149
150pub fn generate_sdf_smooth(source: &SwashImage, spread: f32) -> Vec<u8> {
154 let width = source.placement.width as usize;
155 let height = source.placement.height as usize;
156
157 if width == 0 || height == 0 {
158 return Vec::new();
159 }
160
161 let mut output = vec![0u8; width * height];
162
163 for y in 0..height {
165 for x in 0..width {
166 let idx = y * width + x;
167
168 let source_value = bilinear_sample(source, x as f32, y as f32);
170 let threshold = 0.5f32;
171 let inside = source_value >= threshold;
172
173 let mut min_dist = spread;
175 let search_radius = (spread.ceil() as i32) + 1;
176
177 for dy in -search_radius..=search_radius {
178 for dx in -search_radius..=search_radius {
179 let nx = x as i32 + dx;
180 let ny = y as i32 + dy;
181
182 if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
183 continue;
184 }
185
186 let neighbor_value = bilinear_sample(source, nx as f32, ny as f32);
187 let neighbor_inside = neighbor_value >= threshold;
188
189 if inside != neighbor_inside {
190 let dist = ((dx * dx + dy * dy) as f32).sqrt();
191 min_dist = min_dist.min(dist);
192 }
193 }
194 }
195
196 let normalized = (min_dist / spread).clamp(0.0, 1.0);
197 let sdf_value = if inside {
198 127.0 + normalized * 128.0
199 } else {
200 127.0 - normalized * 127.0
201 };
202
203 output[idx] = sdf_value.clamp(0.0, 255.0) as u8;
204 }
205 }
206
207 output
208}
209
210fn bilinear_sample(image: &SwashImage, x: f32, y: f32) -> f32 {
212 let width = image.placement.width as usize;
213 let height = image.placement.height as usize;
214
215 let x0 = x.floor() as i32;
216 let y0 = y.floor() as i32;
217 let x1 = (x0 + 1).min(width as i32 - 1);
218 let y1 = (y0 + 1).min(height as i32 - 1);
219
220 let fx = x - x0 as f32;
221 let fy = y - y0 as f32;
222
223 let sample = |ix: i32, iy: i32| -> f32 {
225 if ix < 0 || iy < 0 || ix >= width as i32 || iy >= height as i32 {
226 0.0
227 } else {
228 let idx = iy as usize * width + ix as usize;
229 image.data[idx] as f32 / 255.0
230 }
231 };
232
233 let v00 = sample(x0, y0);
234 let v10 = sample(x1, y0);
235 let v01 = sample(x0, y1);
236 let v11 = sample(x1, y1);
237
238 let v0 = v00 * (1.0 - fx) + v10 * fx;
240 let v1 = v01 * (1.0 - fx) + v11 * fx;
241 v0 * (1.0 - fy) + v1 * fy
242}
243
244#[derive(Debug, Clone)]
246pub struct SdfConfig {
247 pub mode: TextRenderMode,
249 pub edge_softness: f32,
252 pub outline_width: f32,
254 pub smooth: bool,
256}
257
258impl Default for SdfConfig {
259 fn default() -> Self {
260 Self {
261 mode: TextRenderMode::Bitmap,
262 edge_softness: 0.05,
263 outline_width: 0.0,
264 smooth: false,
265 }
266 }
267}
268
269impl SdfConfig {
270 pub fn new() -> Self {
272 Self::default()
273 }
274
275 pub fn with_sdf(mut self, spread: f32) -> Self {
277 self.mode = TextRenderMode::SDF { spread };
278 self
279 }
280
281 pub fn edge_softness(mut self, softness: f32) -> Self {
283 self.edge_softness = softness.clamp(0.0, 1.0);
284 self
285 }
286
287 pub fn outline_width(mut self, width: f32) -> Self {
289 self.outline_width = width.max(0.0);
290 self
291 }
292
293 pub fn smooth(mut self, enable: bool) -> Self {
295 self.smooth = enable;
296 self
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 #[test]
305 fn test_render_mode_default() {
306 let mode = TextRenderMode::default();
307 assert!(!mode.is_sdf());
308 assert_eq!(mode.spread(), 0.0);
309 }
310
311 #[test]
312 fn test_render_mode_sdf() {
313 let mode = TextRenderMode::SDF { spread: 4.0 };
314 assert!(mode.is_sdf());
315 assert_eq!(mode.spread(), 4.0);
316 }
317
318 #[test]
319 fn test_render_mode_bitmap() {
320 let mode = TextRenderMode::Bitmap;
321 assert!(!mode.is_sdf());
322 assert_eq!(mode.spread(), 0.0);
323 }
324
325 #[test]
326 fn test_sdf_config_default() {
327 let config = SdfConfig::default();
328 assert!(!config.mode.is_sdf());
329 assert_eq!(config.edge_softness, 0.05);
330 assert_eq!(config.outline_width, 0.0);
331 assert!(!config.smooth);
332 }
333
334 #[test]
335 fn test_sdf_config_builder() {
336 let config = SdfConfig::new()
337 .with_sdf(6.0)
338 .edge_softness(0.1)
339 .outline_width(2.0)
340 .smooth(true);
341
342 assert!(config.mode.is_sdf());
343 assert_eq!(config.mode.spread(), 6.0);
344 assert_eq!(config.edge_softness, 0.1);
345 assert_eq!(config.outline_width, 2.0);
346 assert!(config.smooth);
347 }
348
349 #[test]
350 fn test_sdf_config_edge_softness_clamp() {
351 let config = SdfConfig::new().edge_softness(2.0); assert_eq!(config.edge_softness, 1.0);
354 }
355
356 #[test]
357 fn test_sdf_config_outline_width_clamp() {
358 let config = SdfConfig::new().outline_width(-5.0); assert_eq!(config.outline_width, 0.0);
361 }
362
363 }