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
50pub struct Labels {
51 font_system: FontSystem,
52 swash: SwashCache,
53 atlas: TextAtlas,
54 viewport: Viewport,
55 renderer: TextRenderer,
56 shaped: HashMap<u32, Buffer>,
57 placed: Vec<PlacedLabel>,
58}
59
60impl Labels {
61 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, format: wgpu::TextureFormat) -> Self {
62 let cache = Cache::new(device);
63 let mut atlas = TextAtlas::new(device, queue, &cache, format);
64 let viewport = Viewport::new(device, &cache);
65 let renderer = TextRenderer::new(&mut atlas, device, wgpu::MultisampleState::default(), None);
66 let mut font_system = FontSystem::new();
67 let db = font_system.db_mut();
68 db.load_font_data(FALLBACK_FONT.to_vec());
69 let family = db.faces().next().map(|face| face.families[0].0.clone());
70 if let Some(family) = family {
71 db.set_sans_serif_family(family.clone());
72 db.set_serif_family(family.clone());
73 db.set_monospace_family(family.clone());
74 db.set_cursive_family(family.clone());
75 db.set_fantasy_family(family);
76 }
77 Self {
78 font_system,
79 swash: SwashCache::new(),
80 atlas,
81 viewport,
82 renderer,
83 shaped: HashMap::new(),
84 placed: Vec::new(),
85 }
86 }
87
88 fn project(cam: &Camera, world: [f32; 2]) -> (f32, f32) {
90 let relx = (world[0] - cam.center[0]) * cam.zoom;
91 let rely = (world[1] - cam.center[1]) * cam.zoom;
92 let sx = cam.viewport[0] * 0.5 + relx;
93 let sy = cam.viewport[1] * 0.5 - rely; (sx, sy)
95 }
96
97 pub fn set_labels(&mut self, labels: &[LabelDraw]) {
100 if self.shaped.len() > LABEL_CACHE_CAP {
101 self.shaped.clear();
102 }
103 self.placed.clear();
104 let Self { shaped, font_system, placed, .. } = self;
105 for l in labels {
106 shaped.entry(l.key).or_insert_with(|| {
107 let mut buf = Buffer::new(font_system, Metrics::new(14.0, 18.0));
108 buf.set_text(font_system, l.text, Attrs::new().family(Family::SansSerif), Shaping::Advanced);
109 buf.shape_until_scroll(font_system, false);
110 buf
111 });
112 placed.push(PlacedLabel { key: l.key, pos: l.pos, opacity: l.opacity, color: l.color, anchor: l.anchor });
113 }
114 }
115
116 pub fn prepare(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, cam: &Camera) -> Result<(), String> {
117 self.viewport.update(queue, Resolution { width: cam.viewport[0] as u32, height: cam.viewport[1] as u32 });
118 let areas: Vec<TextArea> = self.placed.iter().filter_map(|p| {
119 let buf = self.shaped.get(&p.key)?;
123 let (sx, sy) = Self::project(cam, p.pos);
124 let left = match p.anchor {
125 LabelAnchor::NodeRight { radius } => sx + radius * cam.zoom + 4.0,
129 LabelAnchor::Center => {
133 let w = buf.layout_runs().map(|r| r.line_w).fold(0.0_f32, f32::max);
134 sx - w * 0.5
135 }
136 };
137 let [r, g, b] = p.color;
138 Some(TextArea {
139 buffer: buf,
140 left,
141 top: sy - 9.0,
142 scale: 1.0,
143 bounds: TextBounds { left: 0, top: 0, right: cam.viewport[0] as i32, bottom: cam.viewport[1] as i32 },
144 default_color: Color::rgba(
145 (r * 255.0) as u8,
146 (g * 255.0) as u8,
147 (b * 255.0) as u8,
148 (cam.fade * p.opacity * 255.0) as u8,
149 ),
150 custom_glyphs: &[],
151 })
152 }).collect();
153 self.renderer.prepare(device, queue, &mut self.font_system, &mut self.atlas, &self.viewport, areas, &mut self.swash).map_err(|e| e.to_string())?;
154 Ok(())
155 }
156
157 pub fn draw<'a>(&'a self, rpass: &mut wgpu::RenderPass<'a>) -> Result<(), String> {
158 self.renderer.render(&self.atlas, &self.viewport, rpass).map_err(|e| e.to_string())?;
159 Ok(())
160 }
161}