1use blockworx_doc::hash::AssetHash;
11use blockworx_geom::{Align2, Angle, Pos2, Rect, Vec2};
12use blockworx_paint::{Color, DrawOp, Font, record::Stroke};
13use egui::{
14 CornerRadius, Shape, StrokeKind,
15 epaint::{CircleShape, RectShape, TextShape},
16};
17
18use crate::{convert::IntoEgui as _, image::ImageRegistry};
19
20const MAX_RASTER: u32 = 2048;
23
24pub fn replay(draw_list: &[DrawOp], painter: &egui::Painter, images: &ImageRegistry) -> Vec<Shape> {
28 let mut shapes = Vec::with_capacity(draw_list.len());
29 for op in draw_list {
30 match op {
31 DrawOp::Rect {
32 rect,
33 rounding,
34 fill,
35 stroke,
36 } => shapes.push(Shape::Rect(RectShape::new(
37 rect.egui(),
38 CornerRadius::same(rounding.round().min(255.0) as u8),
39 fill.egui(),
40 stroke_of(*stroke),
41 StrokeKind::Middle,
42 ))),
43 DrawOp::LineSegment { points, stroke } => shapes.push(Shape::line_segment(
44 [points[0].egui(), points[1].egui()],
45 stroke_of(*stroke),
46 )),
47 DrawOp::Line { points, stroke } => shapes.push(Shape::line(
48 points.iter().map(|p| p.egui()).collect(),
49 stroke_of(*stroke),
50 )),
51 DrawOp::Circle {
52 center,
53 radius,
54 fill,
55 stroke,
56 } => shapes.push(Shape::Circle(CircleShape {
57 center: center.egui(),
58 radius: *radius,
59 fill: fill.egui(),
60 stroke: stroke_of(*stroke),
61 })),
62 DrawOp::ConvexPolygon {
63 points,
64 fill,
65 stroke,
66 } => shapes.push(Shape::convex_polygon(
67 points.iter().map(|p| p.egui()).collect(),
68 fill.egui(),
69 stroke_of(*stroke),
70 )),
71 DrawOp::Text {
72 rect,
73 text,
74 font,
75 color,
76 ..
77 } => rows(
78 painter,
79 *rect,
80 Run {
81 text,
82 font,
83 color: *color,
84 },
85 f32::INFINITY,
86 &mut shapes,
87 ),
88 DrawOp::TextWrapped {
89 rect,
90 text,
91 font,
92 color,
93 max_width,
94 ..
95 } => rows(
96 painter,
97 *rect,
98 Run {
99 text,
100 font,
101 color: *color,
102 },
103 *max_width,
104 &mut shapes,
105 ),
106 DrawOp::RotatedText {
107 pos,
108 anchor,
109 text,
110 font,
111 color,
112 angle,
113 } => shapes.push(rotated(
114 painter,
115 *pos,
116 *anchor,
117 Run {
118 text,
119 font,
120 color: *color,
121 },
122 *angle,
123 )),
124 DrawOp::Image { rect, hash } => shapes.extend(image(painter, images, *rect, *hash)),
125 }
126 }
127 shapes
128}
129
130fn stroke_of(stroke: Stroke) -> egui::Stroke {
131 egui::Stroke::new(stroke.width, stroke.color.egui())
132}
133
134#[derive(Clone, Copy)]
136struct Run<'a> {
137 text: &'a str,
138 font: &'a Font,
139 color: Color,
140}
141
142fn rows(painter: &egui::Painter, rect: Rect, run: Run<'_>, wrap: f32, shapes: &mut Vec<Shape>) {
152 let font = run.font.egui();
153 let color = run.color.egui();
154 let galley = painter.layout(run.text.to_owned(), font.clone(), color, wrap);
156 let Some(metrics) = galley.rows.iter().flat_map(|r| r.glyphs.iter()).next() else {
160 return;
161 };
162 let (ascent, line_height) = (metrics.font_ascent, metrics.line_height);
163 for (row, placed) in galley.rows.iter().enumerate() {
164 let line: String = placed.glyphs.iter().map(|g| g.chr).collect();
167 if line.is_empty() {
168 continue; }
170 let line_galley = painter.layout_no_wrap(line, font.clone(), color);
171 let snapped = line_galley
174 .rows
175 .first()
176 .and_then(|r| r.glyphs.first().map(|g| r.pos.y + g.pos.y))
177 .unwrap_or(0.0);
178 let baseline = rect.min.y + ascent + row as f32 * line_height;
179 shapes.push(Shape::galley(
180 Pos2::new(rect.min.x, baseline - snapped).egui(),
181 line_galley,
182 color,
183 ));
184 }
185}
186
187fn rotated(
191 painter: &egui::Painter,
192 pos: Pos2,
193 anchor: Align2,
194 run: Run<'_>,
195 angle: Angle,
196) -> Shape {
197 let color = run.color.egui();
198 let galley = painter.layout_no_wrap(run.text.to_owned(), run.font.egui(), color);
199 let correction = galley
200 .rows
201 .first()
202 .and_then(|r| {
203 r.glyphs
204 .first()
205 .map(|g| g.font_ascent - (r.pos.y + g.pos.y))
206 })
207 .unwrap_or(0.0);
208 TextShape::new((pos + Vec2::new(0.0, correction)).egui(), galley, color)
209 .with_angle_and_anchor(angle.into(), anchor.egui())
210 .into()
211}
212
213fn image(
219 painter: &egui::Painter,
220 images: &ImageRegistry,
221 rect: Rect,
222 hash: AssetHash,
223) -> Option<Shape> {
224 let ctx = painter.ctx();
225 let Some(uri) = images.uri(hash) else {
226 tracing::warn!("image {hash} was painted before its bytes were registered");
227 return None;
228 };
229 if rect.width() <= 0.0 || rect.height() <= 0.0 {
230 return None;
231 }
232 let hint = egui::load::SizeHint::Size {
235 width: (rect.width().ceil() as u32).clamp(1, MAX_RASTER),
236 height: (rect.height().ceil() as u32).clamp(1, MAX_RASTER),
237 maintain_aspect_ratio: true,
238 };
239 match ctx.try_load_texture(uri, egui::TextureOptions::LINEAR, hint) {
240 Ok(egui::load::TexturePoll::Ready { texture }) => {
241 let uv = Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0));
242 let untinted = Color::WHITE.egui();
245 Some(Shape::image(texture.id, rect.egui(), uv.egui(), untinted))
246 }
247 Ok(egui::load::TexturePoll::Pending { .. }) => {
249 ctx.request_repaint();
250 None
251 }
252 Err(_) => None,
254 }
255}