1use core::time::Duration;
14use std::{
15 cell::{Cell, RefCell},
16 collections::BTreeMap,
17 rc::Rc,
18};
19
20use blockworx_doc::{block_model::Asset, hash::AssetHash};
21use blockworx_geom::{Align2, Angle, Pos2, Rect, Vec2, WorldPx};
22
23use crate::{
24 AnimKey, Animator, Canvas, Color, Cursor, Easing, EditText, Font, Palette, PaletteStroke,
25 PointerKind, Renderer, Swatch, Tick, Vantage, text::Layout,
26};
27
28#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
31pub struct Stroke {
32 pub width: f32,
33 pub color: Color,
34}
35
36impl Stroke {
37 pub const NONE: Self = Self {
39 width: 0.0,
40 color: Color::TRANSPARENT,
41 };
42}
43
44#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
48pub enum DrawOp {
49 Rect {
50 rect: Rect,
51 rounding: f32,
52 fill: Color,
53 stroke: Stroke,
54 },
55 LineSegment {
56 points: [Pos2; 2],
57 stroke: Stroke,
58 },
59 Line {
60 points: Vec<Pos2>,
61 stroke: Stroke,
62 },
63 Circle {
64 center: Pos2,
65 radius: f32,
66 fill: Color,
67 stroke: Stroke,
68 },
69 ConvexPolygon {
70 points: Vec<Pos2>,
71 fill: Color,
72 stroke: Stroke,
73 },
74 Text {
75 rect: Rect,
76 anchor: Align2,
77 text: String,
78 font: Font,
79 color: Color,
80 },
81 RotatedText {
82 pos: Pos2,
83 anchor: Align2,
84 text: String,
85 font: Font,
86 color: Color,
87 angle: Angle,
88 },
89 TextWrapped {
90 rect: Rect,
91 anchor: Align2,
92 text: String,
93 font: Font,
94 color: Color,
95 max_width: f32,
96 },
97 Image {
101 rect: Rect,
102 hash: AssetHash,
103 },
104}
105
106pub type DrawList = Vec<DrawOp>;
108
109pub struct Recorded {
111 pub draw_list: DrawList,
112 pub assets: BTreeMap<AssetHash, Asset>,
116 pub cursor: Option<Cursor>,
117 pub edit_text: Option<EditText>,
118 pub repaint: Option<Duration>,
120}
121
122pub struct Recording<L: crate::TextLayout> {
127 palette: Palette,
128 layout: L,
129 vantage: Vantage,
130 viewport: Rect,
131 easing: Rc<RefCell<Easing>>,
132 tick: Tick,
133 pointer: Option<Pos2>,
134 pointer_kind: PointerKind,
135 draw_list: RefCell<DrawList>,
136 assets: RefCell<BTreeMap<AssetHash, Asset>>,
137 cursor: Option<Cursor>,
138 edit_text: Option<EditText>,
139 repaint: Cell<Option<Duration>>,
140}
141
142pub struct Frame<L: crate::TextLayout> {
146 pub palette: Palette,
147 pub layout: L,
148 pub vantage: Vantage,
149 pub viewport: Rect,
150 pub easing: Rc<RefCell<Easing>>,
151 pub tick: Tick,
152 pub pointer: Option<Pos2>,
154 pub pointer_kind: PointerKind,
155}
156
157impl<L: crate::TextLayout> Recording<L> {
158 pub fn new(frame: Frame<L>) -> Self {
159 let Frame {
160 palette,
161 layout,
162 vantage,
163 viewport,
164 easing,
165 tick,
166 pointer,
167 pointer_kind,
168 } = frame;
169 Self {
170 palette,
171 layout,
172 vantage,
173 viewport,
174 easing,
175 tick,
176 pointer,
177 pointer_kind,
178 draw_list: RefCell::new(DrawList::new()),
179 assets: RefCell::new(BTreeMap::new()),
180 cursor: None,
181 edit_text: None,
182 repaint: Cell::new(None),
183 }
184 }
185
186 pub fn finish(self) -> Recorded {
187 Recorded {
188 draw_list: self.draw_list.into_inner(),
189 assets: self.assets.into_inner(),
190 cursor: self.cursor,
191 edit_text: self.edit_text,
192 repaint: self.repaint.get(),
193 }
194 }
195
196 pub fn take_edit_text(&mut self) -> Option<EditText> {
200 self.edit_text.take()
201 }
202
203 fn record(&self, op: DrawOp) {
204 self.draw_list.borrow_mut().push(op);
205 }
206
207 fn w2s(&self, world: Pos2) -> Pos2 {
208 self.vantage.world_to_screen(self.viewport.min, world)
209 }
210
211 fn color(&self, swatch: impl Into<Swatch>) -> Color {
212 self.palette.resolve(swatch.into())
213 }
214
215 fn ink(&self, stroke: impl Into<PaletteStroke>) -> Stroke {
216 let stroke = stroke.into();
217 Stroke {
218 width: self.vantage.remap_len(stroke.width),
219 color: self.color(stroke.color),
220 }
221 }
222
223 fn measure(&self, text: &str, font: &Font, wrap: WorldPx) -> Vec2 {
227 self.laid(text, font, wrap) / self.vantage.zoom.get()
228 }
229
230 fn laid(&self, text: &str, font: &Font, wrap: WorldPx) -> Vec2 {
232 self.laid_out(text, font, wrap).size
233 }
234
235 fn laid_out(&self, text: &str, font: &Font, wrap: WorldPx) -> Layout {
237 let scaled = self.vantage.remap_font(font);
238 let wrap = WorldPx::new(self.vantage.remap_len(wrap));
239 self.layout.layout(text, &scaled, wrap)
240 }
241
242 #[expect(clippy::too_many_arguments)]
248 fn write(
249 &self,
250 pos: Pos2,
251 anchor: Align2,
252 text: &str,
253 font: &Font,
254 color: Color,
255 max_width: WorldPx,
256 ) -> Rect {
257 let rect = anchor.anchor_size(self.w2s(pos), self.laid(text, font, max_width));
258 let font = self.vantage.remap_font(font);
259 self.record(if max_width == WorldPx::UNBOUNDED {
260 DrawOp::Text {
261 rect,
262 anchor,
263 text: text.to_owned(),
264 font,
265 color,
266 }
267 } else {
268 DrawOp::TextWrapped {
269 rect,
270 anchor,
271 text: text.to_owned(),
272 font,
273 color,
274 max_width: self.vantage.remap_len(max_width),
275 }
276 });
277 rect
278 }
279}
280
281impl<L: crate::TextLayout> Renderer for Recording<L> {
282 fn rect(
283 &self,
284 rect: Rect,
285 rounding: WorldPx,
286 fill: impl Into<Swatch>,
287 stroke: impl Into<PaletteStroke>,
288 ) {
289 self.record(DrawOp::Rect {
290 rect: self.vantage.remap_rect(self.viewport.min, rect),
291 rounding: self.vantage.remap_len(rounding),
292 fill: self.color(fill),
293 stroke: self.ink(stroke),
294 });
295 }
296
297 fn line_segment(&self, points: [Pos2; 2], stroke: impl Into<PaletteStroke>) {
298 self.record(DrawOp::LineSegment {
299 points: [self.w2s(points[0]), self.w2s(points[1])],
300 stroke: self.ink(stroke),
301 });
302 }
303
304 fn line(&self, points: Vec<Pos2>, stroke: impl Into<PaletteStroke>) {
305 self.record(DrawOp::Line {
306 points: points.into_iter().map(|p| self.w2s(p)).collect(),
307 stroke: self.ink(stroke),
308 });
309 }
310
311 fn circle(
312 &self,
313 center: Pos2,
314 radius: WorldPx,
315 fill: impl Into<Swatch>,
316 stroke: impl Into<PaletteStroke>,
317 ) {
318 self.record(DrawOp::Circle {
319 center: self.w2s(center),
320 radius: self.vantage.remap_len(radius),
321 fill: self.color(fill),
322 stroke: self.ink(stroke),
323 });
324 }
325
326 fn add_convex_polygon(
327 &self,
328 points: Vec<Pos2>,
329 fill: impl Into<Swatch>,
330 stroke: impl Into<PaletteStroke>,
331 ) {
332 self.record(DrawOp::ConvexPolygon {
333 points: points.into_iter().map(|p| self.w2s(p)).collect(),
334 fill: self.color(fill),
335 stroke: self.ink(stroke),
336 });
337 }
338
339 fn text(
340 &self,
341 pos: Pos2,
342 anchor: Align2,
343 text: impl ToString,
344 font: &Font,
345 color: impl Into<Swatch>,
346 ) -> Rect {
347 let color = self.color(color);
348 self.write(
349 pos,
350 anchor,
351 &text.to_string(),
352 font,
353 color,
354 WorldPx::UNBOUNDED,
355 )
356 }
357
358 fn text_wrapped(
359 &self,
360 pos: Pos2,
361 anchor: Align2,
362 text: impl ToString,
363 font: &Font,
364 color: impl Into<Swatch>,
365 max_width: WorldPx,
366 ) -> Rect {
367 let color = self.color(color);
368 self.write(pos, anchor, &text.to_string(), font, color, max_width)
369 }
370
371 fn rotated_text(
372 &self,
373 pos: Pos2,
374 anchor: Align2,
375 text: impl ToString,
376 font: &Font,
377 color: impl Into<Swatch>,
378 angle: Angle,
379 ) {
380 let color = self.color(color);
381 self.record(DrawOp::RotatedText {
382 pos: self.w2s(pos),
383 anchor,
384 text: text.to_string(),
385 font: self.vantage.remap_font(font),
386 color,
387 angle,
388 });
389 }
390
391 fn text_size(&self, text: impl ToString, font: &Font) -> Vec2 {
392 self.measure(&text.to_string(), font, WorldPx::UNBOUNDED)
393 }
394
395 fn text_size_wrapped(&self, text: impl ToString, font: &Font, max_width: WorldPx) -> Vec2 {
396 self.measure(&text.to_string(), font, max_width)
397 }
398
399 fn text_layout(&self, text: &str, font: &Font, max_width: WorldPx) -> Layout {
400 self.laid_out(text, font, max_width)
401 .scaled_down(self.vantage.zoom)
402 }
403
404 fn draw_image(&self, rect: Rect, image: &Asset) {
405 let hash = image.hash();
406 self.assets
407 .borrow_mut()
408 .entry(hash)
409 .or_insert_with(|| image.clone());
410 self.record(DrawOp::Image {
411 rect: self.vantage.remap_rect(self.viewport.min, rect),
412 hash,
413 });
414 }
415
416 fn image_intrinsic_size(&self, image: &Asset) -> Option<Vec2> {
417 crate::image::image_intrinsic_size(image).ok()
418 }
419
420 fn animator(&self) -> Option<&dyn Animator> {
421 Some(self)
422 }
423
424 fn visible_world_bounds(&self) -> Option<Rect> {
425 Some(self.vantage.visible(self.viewport))
426 }
427}
428
429impl<L: crate::TextLayout> Animator for Recording<L> {
430 fn animate(&self, key: AnimKey, goal: f32, over: Duration) -> f32 {
431 let animated = self.easing.borrow_mut().animate(self.tick, key, goal, over);
432 if animated.in_progress {
433 self.request_repaint();
434 }
435 animated.value
436 }
437
438 fn pointer_world(&self) -> Option<Pos2> {
439 self.pointer
440 }
441}
442
443impl<L: crate::TextLayout> Canvas for Recording<L> {
444 fn set_cursor(&mut self, cursor: Cursor) {
445 self.cursor = Some(cursor);
446 }
447
448 fn cursor(&self) -> Option<Cursor> {
449 self.cursor
450 }
451
452 fn set_edit_text(&mut self, edit: EditText) {
453 self.edit_text = Some(edit.fitted(&*self));
454 }
455
456 fn remap_rect(&self, world: Rect) -> Rect {
457 self.vantage.remap_rect(self.viewport.min, world)
458 }
459
460 fn request_repaint(&self) {
461 self.request_repaint_after(Duration::ZERO);
462 }
463
464 fn request_repaint_after(&self, after: Duration) {
465 self.repaint.set(Some(
466 self.repaint
467 .get()
468 .map_or(after, |soonest| soonest.min(after)),
469 ));
470 }
471
472 fn now(&self) -> Duration {
473 self.tick.now()
474 }
475
476 fn pointer_kind(&self) -> PointerKind {
477 self.pointer_kind
478 }
479}