1use std::collections::{HashMap, HashSet};
15
16use macroquad::prelude::*;
17use shuyi_assets::font::{AtlasRect, FALLBACK_GLYPH, FontAtlasDef};
18use shuyi_assets::image::{ImageId, ImageTable};
19use shuyi_assets::lighting::Light;
20use shuyi_ui::frame::Rgb;
21use shuyi_ui::frame::console::{Layout, layout};
22use shuyi_ui::frame::glyph_buffer::{Backdrop, GlyphBuffer, Item, Layer, Paint, Sprite};
23use shuyi_ui::frame::painted::{PaintedSprite, PaintedTable};
24
25use crate::painted::PaintedTextures;
26use crate::pictures::{Fresh, Remake, src_fits};
27
28const MISSING_COLOR: Color = MAGENTA;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36enum Recolour {
37 Silhouette,
40 Dimmed,
42 Shaded(Light),
44}
45
46#[derive(Clone, Copy, PartialEq, Eq)]
48enum Fills {
49 NotClear,
51 All,
53}
54
55pub struct Renderer {
57 texture: Texture2D,
58 atlas: FontAtlasDef,
59 clear: Rgb,
61 warned: HashSet<char>,
63 images: HashMap<ImageId, Texture2D>,
65 recoloured: HashMap<(ImageId, Recolour), Option<Texture2D>>,
68 warned_images: HashSet<ImageId>,
70 sources: HashMap<ImageId, &'static [u8]>,
73 remade: HashMap<ImageId, (u16, u16)>,
76 painted: PaintedTextures<Texture2D>,
78}
79
80impl Renderer {
81 pub fn new(
86 atlas: FontAtlasDef,
87 png: &[u8],
88 clear: Rgb,
89 images: &ImageTable,
90 bytes: fn(&str) -> Option<&'static [u8]>,
91 ) -> Result<Self, String> {
92 Self::with_files(atlas, png, clear, images, bytes)
93 }
94
95 pub(crate) fn with_files(
97 atlas: FontAtlasDef,
98 png: &[u8],
99 clear: Rgb,
100 images: &ImageTable,
101 bytes: impl Fn(&str) -> Option<&'static [u8]>,
102 ) -> Result<Self, String> {
103 let texture = upload(png).map_err(|e| format!("font atlas image: {e}"))?;
104 let mut textures = HashMap::new();
105 let mut sources = HashMap::new();
106 for id in images.ids() {
107 let source = bytes(id.path()).unwrap_or_default();
108 match upload(source) {
109 Ok(texture) => {
110 textures.insert(id, texture);
111 sources.insert(id, source);
112 }
113 Err(e) => {
114 warn!("image {}: {}", id.path(), e);
115 }
116 }
117 }
118 Ok(Self {
119 texture,
120 atlas,
121 clear,
122 warned: HashSet::new(),
123 images: textures,
124 recoloured: HashMap::new(),
125 warned_images: HashSet::new(),
126 sources,
127 remade: HashMap::new(),
128 painted: PaintedTextures::new(),
129 })
130 }
131
132 pub(crate) fn remake(&mut self, fresh: Vec<Fresh<Image>>, font: &FontAtlasDef) {
136 for Fresh {
137 remake,
138 decoded,
139 bytes,
140 } in fresh
141 {
142 let texture = texture_of(&decoded);
143 match remake {
144 Remake::Font => {
145 self.texture = texture;
146 self.atlas = font.clone();
147 self.warned.clear();
148 }
149 Remake::Image(id) => {
150 self.images.insert(id, texture);
151 self.sources.insert(id, bytes);
152 self.remade.insert(id, (decoded.width, decoded.height));
153 self.recoloured.retain(|(image, _), _| *image != id);
154 self.warned_images.remove(&id);
155 }
156 }
157 }
158 }
159
160 pub fn set_clear(&mut self, clear: Rgb) {
163 self.clear = clear;
164 }
165
166 pub fn draw(&mut self, buf: &GlyphBuffer) {
177 let dpi = screen_dpi_scale();
178 let window_h = (screen_height() * dpi).round();
181 let fit = layout(buf.spec(), (screen_width() * dpi).round(), window_h);
182 #[allow(clippy::cast_precision_loss)] let scale = fit.scale as f32 / dpi;
184 let (offset_x, offset_y) = (fit.offset_x / dpi, fit.offset_y / dpi);
185 clear_background(BLACK);
186 draw_rectangle(
187 offset_x,
188 offset_y,
189 f32::from(buf.width()) * f32::from(buf.spec().cell_w) * scale,
190 f32::from(buf.height()) * f32::from(buf.spec().cell_h) * scale,
191 color(self.clear),
192 );
193 let pictures = buf.pictures();
194 if let Some(backdrop) = buf.backdrop() {
195 self.draw_backdrop(backdrop, pictures, &fit, window_h);
196 }
197 let fills = if buf.backdrop().is_some() {
199 Fills::All
200 } else {
201 Fills::NotClear
202 };
203 self.draw_layers((buf, pictures), (offset_x, offset_y), scale, fills);
204 self.painted.end_frame();
205 }
206
207 #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] fn draw_backdrop(
219 &mut self,
220 backdrop: &Backdrop,
221 pictures: &PaintedTable,
222 fit: &Layout,
223 window_h: f32,
224 ) {
225 let scale = fit.scale as f32;
226 let clip = backdrop.clip_px();
227 let (x, y) = (
228 fit.offset_x + clip.x as f32 * scale,
229 fit.offset_y + clip.y as f32 * scale,
230 );
231 let (w, h) = (clip.w as f32 * scale, clip.h as f32 * scale);
232 set_camera(&Camera2D {
233 target: vec2(w / 2.0, h / 2.0),
234 zoom: vec2(2.0 / w, 2.0 / h),
235 viewport: Some((x as i32, (window_h - y - h) as i32, w as i32, h as i32)),
236 ..Default::default()
237 });
238 let (dx, dy) = backdrop.scene_offset(fit.scale);
239 let scene_scale = f32::from(backdrop.zoom()) * scale;
240 self.draw_layers(
241 (backdrop.scene(), pictures),
242 (dx as f32, dy as f32),
243 scene_scale,
244 Fills::NotClear,
245 );
246 set_default_camera();
247 }
248
249 fn draw_layers(
254 &mut self,
255 (buf, pictures): (&GlyphBuffer, &PaintedTable),
256 origin: (f32, f32),
257 scale: f32,
258 fills: Fills,
259 ) {
260 let (offset_x, offset_y) = origin;
261 let cell_w = f32::from(buf.spec().cell_w) * scale;
262 let cell_h = f32::from(buf.spec().cell_h) * scale;
263 let cells = || {
264 (0..i32::from(buf.height())).flat_map(move |y| {
265 (0..i32::from(buf.width())).filter_map(move |x| {
266 #[allow(clippy::cast_precision_loss)] let at = (offset_x + x as f32 * cell_w, offset_y + y as f32 * cell_h);
268 buf.get(x, y).map(|cell| (at, cell))
269 })
270 })
271 };
272 for ((px, py), cell) in cells() {
273 if !cell.see_through && (fills == Fills::All || cell.bg != self.clear) {
274 draw_rectangle(px, py, cell_w, cell_h, color(cell.bg));
275 }
276 }
277 self.draw_items((buf, pictures), Layer::Under, origin, scale);
278 for (x, y, cell, wide) in buf.glyphs() {
279 if cell.glyph == ' ' {
280 continue;
281 }
282 let set = buf.widths().wide_font();
283 let Some((rect, fg, cells)) = self.atlas_cell(cell.glyph, cell.fg, wide, set) else {
284 continue;
285 };
286 #[allow(clippy::cast_precision_loss)] let (px, py) = (offset_x + x as f32 * cell_w, offset_y + y as f32 * cell_h);
288 draw_texture_ex(
289 &self.texture,
290 px,
291 py,
292 fg,
293 DrawTextureParams {
294 dest_size: Some(vec2(cell_w * cells, cell_h)),
295 source: Some(source_rect(rect)),
296 ..Default::default()
297 },
298 );
299 }
300 self.draw_items((buf, pictures), Layer::Over, origin, scale);
301 }
302
303 fn draw_items(
307 &mut self,
308 (buf, pictures): (&GlyphBuffer, &PaintedTable),
309 layer: Layer,
310 origin: (f32, f32),
311 scale: f32,
312 ) {
313 for item in buf.items().iter().filter(|item| item.layer() == layer) {
314 let visible = item.visible();
315 #[allow(clippy::cast_precision_loss)] let (x, y, size) = (
317 origin.0 + visible.x as f32 * scale,
318 origin.1 + visible.y as f32 * scale,
319 vec2(visible.w as f32, visible.h as f32) * scale,
320 );
321 match item {
322 Item::Rect(rect) => draw_rectangle(x, y, size.x, size.y, color(rect.color)),
323 Item::Sprite(sprite) => self.draw_sprite(sprite, x, y, size),
324 Item::Painted(painted) => self.draw_painted(painted, pictures, x, y, size),
325 #[allow(clippy::cast_precision_loss)] Item::Gizmo(gizmo) => gizmo.for_each_run(|row, x0, x1| {
329 draw_rectangle(
330 origin.0 + x0 as f32 * scale,
331 origin.1 + row as f32 * scale,
332 (x1 - x0) as f32 * scale,
333 scale,
334 color(gizmo.color),
335 );
336 }),
337 _ => {}
339 }
340 }
341 }
342
343 fn draw_sprite(&mut self, sprite: &Sprite, x: f32, y: f32, size: Vec2) {
348 if let Some(&picture) = self.remade.get(&sprite.image)
349 && !src_fits(sprite.src, picture)
350 {
351 if self.warned_images.insert(sprite.image) {
352 warn!(
353 "image {} is {} by {} now: a sprite's source is past its edge",
354 sprite.image.path(),
355 picture.0,
356 picture.1
357 );
358 }
359 draw_rectangle(x, y, size.x, size.y, MISSING_COLOR);
360 return;
361 }
362 let (recolour, tint) = match sprite.paint {
363 Paint::Image => (None, Rgb::new(255, 255, 255)),
364 Paint::Solid(color) => (Some(Recolour::Silhouette), color),
365 Paint::Dimmed => (Some(Recolour::Dimmed), Rgb::new(255, 255, 255)),
366 Paint::Shaded(light) => (Some(Recolour::Shaded(light)), Rgb::new(255, 255, 255)),
367 };
368 let source = self.sources.get(&sprite.image).copied();
369 let texture = match recolour {
370 None => self.images.get(&sprite.image),
371 Some(recolour) => self
372 .recoloured
373 .entry((sprite.image, recolour))
374 .or_insert_with(|| recoloured(source?, recolour))
375 .as_ref(),
376 };
377 let Some(texture) = texture else {
378 if self.warned_images.insert(sprite.image) {
379 warn!("image {} has no texture", sprite.image.path());
380 }
381 draw_rectangle(x, y, size.x, size.y, MISSING_COLOR);
382 return;
383 };
384 let [sx, sy, sw, sh] = sprite.clipped_src();
385 let tint = tint.scale(f32::from(sprite.brightness) / 255.0);
387 draw_texture_ex(
388 texture,
389 x,
390 y,
391 Color::from_rgba(tint.r, tint.g, tint.b, sprite.opacity),
392 DrawTextureParams {
393 dest_size: Some(size),
394 source: Some(Rect::new(sx, sy, sw, sh)),
395 flip_x: sprite.flip_x,
396 ..Default::default()
397 },
398 );
399 }
400
401 fn draw_painted(
406 &mut self,
407 painted: &PaintedSprite,
408 pictures: &PaintedTable,
409 x: f32,
410 y: f32,
411 size: Vec2,
412 ) {
413 let Some(picture) = pictures.get(painted.picture) else {
414 draw_rectangle(x, y, size.x, size.y, MISSING_COLOR);
415 return;
416 };
417 let (width, height) = picture.size();
418 if width == 0 || height == 0 {
419 return;
420 }
421 let make = |rgba: &[u8]| {
422 let texture = Texture2D::from_rgba8(width, height, rgba);
423 texture.set_filter(FilterMode::Nearest);
424 texture
425 };
426 let update = |texture: &Texture2D, rgba: &[u8]| {
427 texture.update_from_bytes(width.into(), height.into(), rgba);
428 };
429 let texture = self.painted.texture(painted.picture, picture, make, update);
430 let [sx, sy, sw, sh] = painted.clipped_src();
431 let bright = painted.brightness;
432 draw_texture_ex(
433 texture,
434 x,
435 y,
436 Color::from_rgba(bright, bright, bright, 255),
437 DrawTextureParams {
438 dest_size: Some(size),
439 source: Some(Rect::new(sx, sy, sw, sh)),
440 ..Default::default()
441 },
442 );
443 }
444
445 fn atlas_cell(
452 &mut self,
453 glyph: char,
454 fg: Rgb,
455 wide: bool,
456 set: Option<&str>,
457 ) -> Option<(AtlasRect, Color, f32)> {
458 if let Some(rect) = self.atlas.wide_rect_in(set, glyph).filter(|_| wide) {
459 return Some((rect, color(fg), 2.0));
460 }
461 if let Some(rect) = self.atlas.glyph_rect(glyph) {
462 return Some((rect, color(fg), 1.0));
463 }
464 if self.warned.insert(glyph) {
465 warn!(
466 "glyph {:?} (U+{:04X}) not in font atlas",
467 glyph,
468 u32::from(glyph)
469 );
470 }
471 Some((self.atlas.glyph_rect(FALLBACK_GLYPH)?, MISSING_COLOR, 1.0))
472 }
473}
474
475pub(crate) fn decode(png: &[u8]) -> Result<Image, String> {
477 Image::from_file_with_format(png, Some(ImageFormat::Png)).map_err(|e| e.to_string())
478}
479
480fn texture_of(image: &Image) -> Texture2D {
482 let texture = Texture2D::from_image(image);
483 texture.set_filter(FilterMode::Nearest);
484 texture
485}
486
487pub(crate) fn upload(png: &[u8]) -> Result<Texture2D, String> {
489 decode(png).map(|image| texture_of(&image))
490}
491
492fn recoloured(png: &[u8], recolour: Recolour) -> Option<Texture2D> {
495 let mut picture = Image::from_file_with_format(png, Some(ImageFormat::Png)).ok()?;
496 for pixel in picture.bytes.as_chunks_mut::<4>().0 {
497 let own = Rgb::new(pixel[0], pixel[1], pixel[2]);
498 let rgb = match recolour {
499 Recolour::Silhouette => Rgb::new(255, 255, 255),
500 Recolour::Dimmed => Paint::Dimmed.apply(own),
501 Recolour::Shaded(light) => Paint::Shaded(light).apply(own),
502 };
503 pixel[..3].copy_from_slice(&[rgb.r, rgb.g, rgb.b]);
504 }
505 let texture = Texture2D::from_image(&picture);
506 texture.set_filter(FilterMode::Nearest);
507 Some(texture)
508}
509
510pub(crate) fn color(c: Rgb) -> Color {
512 Color::from_rgba(c.r, c.g, c.b, 255)
513}
514
515#[allow(clippy::cast_precision_loss)] fn source_rect(r: AtlasRect) -> Rect {
517 Rect::new(r.x as f32, r.y as f32, r.w as f32, r.h as f32)
518}