1use std::collections::HashMap;
2
3use glyphon::{Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping,
4 SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport};
5use crate::Camera;
6
7static FALLBACK_FONT: &[u8] = include_bytes!("../assets/Inter-Bold.ttf");
15
16#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum LabelAnchor {
19 NodeRight { radius: f32 },
22 Center,
25}
26
27pub struct LabelDraw<'a> {
30 pub key: u32,
31 pub text: &'a str,
32 pub pos: [f32; 2],
33 pub opacity: f32,
34 pub color: [f32; 3],
37 pub anchor: LabelAnchor,
38}
39
40#[derive(Clone, Copy)]
42struct PlacedLabel { key: u32, pos: [f32; 2], opacity: f32, color: [f32; 3], anchor: LabelAnchor }
43
44const LABEL_CACHE_CAP: usize = 2048;
49
50const FONT_SIZE_CSS_PX: f32 = 14.0;
55const LINE_HEIGHT_CSS_PX: f32 = 18.0;
56const LABEL_GAP_CSS_PX: f32 = 4.0;
58
59pub struct Labels {
60 font_system: FontSystem,
61 swash: SwashCache,
62 atlas: TextAtlas,
63 viewport: Viewport,
64 renderer: TextRenderer,
65 shaped: HashMap<u32, Buffer>,
66 placed: Vec<PlacedLabel>,
67 shaped_at_ratio: f32,
71}
72
73impl Labels {
74 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
75 let cache = Cache::new(device);
76 let mut atlas = TextAtlas::new(device, queue, &cache, format);
77 let viewport = Viewport::new(device, &cache);
78 let renderer = TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
79 let mut font_system = FontSystem::new();
80 let db = font_system.db_mut();
81 db.load_font_data(FALLBACK_FONT.to_vec());
82 let family = db.faces().next().map(|face| face.families[0].0.clone());
83 if let Some(family) = family {
84 db.set_sans_serif_family(family.clone());
85 db.set_serif_family(family.clone());
86 db.set_monospace_family(family.clone());
87 db.set_cursive_family(family.clone());
88 db.set_fantasy_family(family);
89 }
90 Self {
91 font_system,
92 swash: SwashCache::new(),
93 atlas,
94 viewport,
95 renderer,
96 shaped: HashMap::new(),
97 placed: Vec::new(),
98 shaped_at_ratio: 1.0,
99 }
100 }
101
102 fn project(cam: &Camera, world: [f32; 2]) -> (f32, f32) {
104 let relx = (world[0] - cam.center[0]) * cam.zoom;
105 let rely = (world[1] - cam.center[1]) * cam.zoom;
106 let sx = cam.viewport[0] * 0.5 + relx;
107 let sy = cam.viewport[1] * 0.5 - rely; (sx, sy)
109 }
110
111 pub fn set_labels(&mut self, labels: &[LabelDraw], pixel_ratio: f32) {
114 if self.shaped_at_ratio != pixel_ratio {
116 self.shaped.clear();
117 self.shaped_at_ratio = pixel_ratio;
118 }
119 if self.shaped.len() > LABEL_CACHE_CAP {
120 self.shaped.clear();
121 }
122 self.placed.clear();
123 let metrics = Metrics::new(FONT_SIZE_CSS_PX * pixel_ratio, LINE_HEIGHT_CSS_PX * pixel_ratio);
124 let Self { shaped, font_system, placed, .. } = self;
125 for l in labels {
126 shaped.entry(l.key).or_insert_with(|| {
127 let mut buf = Buffer::new(font_system, metrics);
128 buf.set_text(font_system, l.text, Attrs::new().family(Family::SansSerif), Shaping::Advanced);
129 buf.shape_until_scroll(font_system, false);
130 buf
131 });
132 placed.push(PlacedLabel { key: l.key, pos: l.pos, opacity: l.opacity, color: l.color, anchor: l.anchor });
133 }
134 }
135
136 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, cam: &Camera) -> Result<(), String> {
137 self.viewport.update(queue, Resolution { width: cam.viewport[0] as u32, height: cam.viewport[1] as u32 });
138 let areas: Vec<TextArea> = self.placed.iter().filter_map(|p| {
139 let buf = self.shaped.get(&p.key)?;
143 let (sx, sy) = Self::project(cam, p.pos);
144 let left = match p.anchor {
145 LabelAnchor::NodeRight { radius } => sx + radius * cam.zoom + cam.css_px(LABEL_GAP_CSS_PX),
149 LabelAnchor::Center => {
153 let w = buf.layout_runs().map(|r| r.line_w).fold(0.0_f32, f32::max);
154 sx - w * 0.5
155 }
156 };
157 let [r, g, b] = p.color;
158 Some(TextArea {
159 buffer: buf,
160 left,
161 top: sy - cam.css_px(LINE_HEIGHT_CSS_PX * 0.5),
164 scale: 1.0,
165 bounds: TextBounds { left: 0, top: 0, right: cam.viewport[0] as i32, bottom: cam.viewport[1] as i32 },
166 default_color: Color::rgba(
167 (r * 255.0) as u8,
168 (g * 255.0) as u8,
169 (b * 255.0) as u8,
170 (cam.fade * p.opacity * 255.0) as u8,
171 ),
172 custom_glyphs: &[],
173 })
174 }).collect();
175 self.renderer.prepare(device, queue, &mut self.font_system, &mut self.atlas, &self.viewport, areas, &mut self.swash).map_err(|e| e.to_string())?;
176 Ok(())
177 }
178
179 pub fn draw<'a>(&'a self, rpass: &mut wgpu::RenderPass<'a>) -> Result<(), String> {
180 self.renderer.render(&self.atlas, &self.viewport, rpass).map_err(|e| e.to_string())?;
181 Ok(())
182 }
183}