1use crate::protocol::ProtocolError;
12use crate::schema::{Paint, TypeRole};
13use std::rc::Rc;
14
15pub const COMMAND_LEN: usize = 36;
17
18pub const TAG_LINE: u16 = 1;
19pub const TAG_RECT: u16 = 2;
20pub const TAG_ROUND_RECT: u16 = 3;
21pub const TAG_CIRCLE: u16 = 4;
22pub const TAG_ARC: u16 = 5;
23pub const TAG_POLYLINE_REF: u16 = 6;
24pub const TAG_TEXT_AT: u16 = 7;
25
26#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum DrawFieldType {
29 Float,
31 U32,
33 Role(&'static str),
35}
36
37#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38pub struct DrawFieldSchema {
39 pub name: &'static str,
40 pub ty: DrawFieldType,
41 pub word: u8,
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub struct DrawCommandSchema {
47 pub name: &'static str,
48 pub tag: u16,
49 pub fields: &'static [DrawFieldSchema],
50}
51
52const fn float(name: &'static str, word: u8) -> DrawFieldSchema {
53 DrawFieldSchema {
54 name,
55 ty: DrawFieldType::Float,
56 word,
57 }
58}
59
60const fn unsigned(name: &'static str, word: u8) -> DrawFieldSchema {
61 DrawFieldSchema {
62 name,
63 ty: DrawFieldType::U32,
64 word,
65 }
66}
67
68pub const DRAW_COMMAND_SCHEMA: &[DrawCommandSchema] = &[
70 DrawCommandSchema {
71 name: "Line",
72 tag: TAG_LINE,
73 fields: &[
74 float("x1", 0),
75 float("y1", 1),
76 float("x2", 2),
77 float("y2", 3),
78 float("strokeWidth", 4),
79 ],
80 },
81 DrawCommandSchema {
82 name: "Rect",
83 tag: TAG_RECT,
84 fields: &[
85 float("x", 0),
86 float("y", 1),
87 float("width", 2),
88 float("height", 3),
89 float("strokeWidth", 4),
91 ],
92 },
93 DrawCommandSchema {
94 name: "RoundRect",
95 tag: TAG_ROUND_RECT,
96 fields: &[
97 float("x", 0),
98 float("y", 1),
99 float("width", 2),
100 float("height", 3),
101 float("radius", 4),
102 float("strokeWidth", 5),
103 ],
104 },
105 DrawCommandSchema {
106 name: "Circle",
107 tag: TAG_CIRCLE,
108 fields: &[
109 float("centerX", 0),
110 float("centerY", 1),
111 float("radius", 2),
112 float("strokeWidth", 3),
113 ],
114 },
115 DrawCommandSchema {
116 name: "Arc",
117 tag: TAG_ARC,
118 fields: &[
119 float("centerX", 0),
120 float("centerY", 1),
121 float("radius", 2),
122 float("startDegrees", 3),
123 float("sweepDegrees", 4),
124 float("strokeWidth", 5),
125 ],
126 },
127 DrawCommandSchema {
128 name: "PolylineRef",
129 tag: TAG_POLYLINE_REF,
130 fields: &[unsigned("assetId", 0), float("strokeWidth", 1)],
131 },
132 DrawCommandSchema {
133 name: "TextAt",
134 tag: TAG_TEXT_AT,
135 fields: &[
136 unsigned("textOffset", 0),
137 unsigned("textLength", 1),
138 float("x", 2),
139 float("y", 3),
140 DrawFieldSchema {
141 name: "typeRole",
142 ty: DrawFieldType::Role("TypeRole"),
143 word: 4,
144 },
145 ],
146 },
147];
148
149#[derive(Clone, Copy, Debug, PartialEq)]
151pub enum DrawCommand {
152 Line {
153 paint: Paint,
154 x1: f32,
155 y1: f32,
156 x2: f32,
157 y2: f32,
158 stroke_width: f32,
159 },
160 Rect {
162 paint: Paint,
163 x: f32,
164 y: f32,
165 width: f32,
166 height: f32,
167 stroke_width: f32,
168 },
169 RoundRect {
170 paint: Paint,
171 x: f32,
172 y: f32,
173 width: f32,
174 height: f32,
175 radius: f32,
176 stroke_width: f32,
177 },
178 Circle {
179 paint: Paint,
180 center_x: f32,
181 center_y: f32,
182 radius: f32,
183 stroke_width: f32,
184 },
185 Arc {
186 paint: Paint,
187 center_x: f32,
188 center_y: f32,
189 radius: f32,
190 start_degrees: f32,
191 sweep_degrees: f32,
192 stroke_width: f32,
193 },
194 PolylineRef {
196 paint: Paint,
197 asset_id: u32,
198 stroke_width: f32,
199 },
200 TextAt {
203 paint: Paint,
204 text_offset: u32,
205 text_length: u32,
206 x: f32,
207 y: f32,
208 type_role: TypeRole,
209 },
210}
211
212impl DrawCommand {
213 pub const fn tag(&self) -> u16 {
214 match self {
215 Self::Line { .. } => TAG_LINE,
216 Self::Rect { .. } => TAG_RECT,
217 Self::RoundRect { .. } => TAG_ROUND_RECT,
218 Self::Circle { .. } => TAG_CIRCLE,
219 Self::Arc { .. } => TAG_ARC,
220 Self::PolylineRef { .. } => TAG_POLYLINE_REF,
221 Self::TextAt { .. } => TAG_TEXT_AT,
222 }
223 }
224
225 pub const fn paint(&self) -> Paint {
226 match *self {
227 Self::Line { paint, .. }
228 | Self::Rect { paint, .. }
229 | Self::RoundRect { paint, .. }
230 | Self::Circle { paint, .. }
231 | Self::Arc { paint, .. }
232 | Self::PolylineRef { paint, .. }
233 | Self::TextAt { paint, .. } => paint,
234 }
235 }
236
237 fn words(&self) -> [u32; 6] {
239 let mut words = [0_u32; 6];
240 match *self {
241 Self::Line {
242 x1,
243 y1,
244 x2,
245 y2,
246 stroke_width,
247 ..
248 } => {
249 words[0] = x1.to_bits();
250 words[1] = y1.to_bits();
251 words[2] = x2.to_bits();
252 words[3] = y2.to_bits();
253 words[4] = stroke_width.to_bits();
254 }
255 Self::Rect {
256 x,
257 y,
258 width,
259 height,
260 stroke_width,
261 ..
262 } => {
263 words[0] = x.to_bits();
264 words[1] = y.to_bits();
265 words[2] = width.to_bits();
266 words[3] = height.to_bits();
267 words[4] = stroke_width.to_bits();
268 }
269 Self::RoundRect {
270 x,
271 y,
272 width,
273 height,
274 radius,
275 stroke_width,
276 ..
277 } => {
278 words[0] = x.to_bits();
279 words[1] = y.to_bits();
280 words[2] = width.to_bits();
281 words[3] = height.to_bits();
282 words[4] = radius.to_bits();
283 words[5] = stroke_width.to_bits();
284 }
285 Self::Circle {
286 center_x,
287 center_y,
288 radius,
289 stroke_width,
290 ..
291 } => {
292 words[0] = center_x.to_bits();
293 words[1] = center_y.to_bits();
294 words[2] = radius.to_bits();
295 words[3] = stroke_width.to_bits();
296 }
297 Self::Arc {
298 center_x,
299 center_y,
300 radius,
301 start_degrees,
302 sweep_degrees,
303 stroke_width,
304 ..
305 } => {
306 words[0] = center_x.to_bits();
307 words[1] = center_y.to_bits();
308 words[2] = radius.to_bits();
309 words[3] = start_degrees.to_bits();
310 words[4] = sweep_degrees.to_bits();
311 words[5] = stroke_width.to_bits();
312 }
313 Self::PolylineRef {
314 asset_id,
315 stroke_width,
316 ..
317 } => {
318 words[0] = asset_id;
319 words[1] = stroke_width.to_bits();
320 }
321 Self::TextAt {
322 text_offset,
323 text_length,
324 x,
325 y,
326 type_role,
327 ..
328 } => {
329 words[0] = text_offset;
330 words[1] = text_length;
331 words[2] = x.to_bits();
332 words[3] = y.to_bits();
333 words[4] = u32::from(type_role as u16);
334 }
335 }
336 words
337 }
338
339 fn write_to(&self, output: &mut Vec<u8>) {
340 output.extend_from_slice(&self.tag().to_le_bytes());
341 output.extend_from_slice(&(COMMAND_LEN as u16).to_le_bytes());
342 output.extend_from_slice(&self.paint().to_bits().to_le_bytes());
343 for word in self.words() {
344 output.extend_from_slice(&word.to_le_bytes());
345 }
346 }
347
348 fn read_from(record: &[u8]) -> Result<Self, ProtocolError> {
349 let tag = u16::from_le_bytes([record[0], record[1]]);
350 let length = u16::from_le_bytes([record[2], record[3]]);
351 if usize::from(length) != COMMAND_LEN {
352 return Err(ProtocolError::InvalidRecordLength);
353 }
354 let paint_bits = u64::from_le_bytes(record[4..12].try_into().expect("eight bytes"));
355 let paint = Paint::from_bits(paint_bits).ok_or(ProtocolError::InvalidValueKind(0))?;
356 let word = |index: usize| {
357 let start = 12 + index * 4;
358 u32::from_le_bytes(
359 record[start..start + 4]
360 .try_into()
361 .expect("four bytes inside a 36 byte record"),
362 )
363 };
364 let real = |index: usize| f32::from_bits(word(index));
365 Ok(match tag {
366 TAG_LINE => Self::Line {
367 paint,
368 x1: real(0),
369 y1: real(1),
370 x2: real(2),
371 y2: real(3),
372 stroke_width: real(4),
373 },
374 TAG_RECT => Self::Rect {
375 paint,
376 x: real(0),
377 y: real(1),
378 width: real(2),
379 height: real(3),
380 stroke_width: real(4),
381 },
382 TAG_ROUND_RECT => Self::RoundRect {
383 paint,
384 x: real(0),
385 y: real(1),
386 width: real(2),
387 height: real(3),
388 radius: real(4),
389 stroke_width: real(5),
390 },
391 TAG_CIRCLE => Self::Circle {
392 paint,
393 center_x: real(0),
394 center_y: real(1),
395 radius: real(2),
396 stroke_width: real(3),
397 },
398 TAG_ARC => Self::Arc {
399 paint,
400 center_x: real(0),
401 center_y: real(1),
402 radius: real(2),
403 start_degrees: real(3),
404 sweep_degrees: real(4),
405 stroke_width: real(5),
406 },
407 TAG_POLYLINE_REF => Self::PolylineRef {
408 paint,
409 asset_id: word(0),
410 stroke_width: real(1),
411 },
412 TAG_TEXT_AT => {
413 let raw_role = u16::try_from(word(4))
414 .map_err(|_| ProtocolError::InvalidValueKind(u16::MAX))?;
415 Self::TextAt {
416 paint,
417 text_offset: word(0),
418 text_length: word(1),
419 x: real(2),
420 y: real(3),
421 type_role: TypeRole::try_from(raw_role)
422 .map_err(|()| ProtocolError::InvalidValueKind(raw_role))?,
423 }
424 }
425 other => return Err(ProtocolError::InvalidTag(other)),
426 })
427 }
428}
429
430#[derive(Clone, Default, PartialEq)]
435pub struct DrawList {
436 bytes: Option<Rc<[u8]>>,
437 view_box: Option<(f32, f32)>,
438}
439
440impl Eq for DrawList {}
441
442impl std::fmt::Debug for DrawList {
443 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 formatter
445 .debug_struct("DrawList")
446 .field("bytes", &self.as_bytes().len())
447 .field("view_box", &self.view_box)
448 .finish()
449 }
450}
451
452impl DrawList {
453 pub fn builder() -> DrawListBuilder {
454 DrawListBuilder::default()
455 }
456
457 pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
459 Self {
460 bytes: Some(bytes.into()),
461 view_box: None,
462 }
463 }
464
465 pub fn view_box(&self) -> Option<(f32, f32)> {
468 self.view_box
469 }
470
471 pub fn as_bytes(&self) -> &[u8] {
472 self.bytes.as_deref().unwrap_or(&[])
473 }
474
475 pub fn is_empty(&self) -> bool {
476 self.as_bytes().is_empty()
477 }
478
479 pub fn decode(&self) -> Result<Vec<DrawCommand>, ProtocolError> {
481 decode_commands(self.as_bytes())
482 }
483
484 pub fn text_at(&self, offset: u32, length: u32) -> Result<&str, ProtocolError> {
486 let bytes = self.as_bytes();
487 let start = usize::try_from(offset).map_err(|_| ProtocolError::LengthOverflow)?;
488 let len = usize::try_from(length).map_err(|_| ProtocolError::LengthOverflow)?;
489 let end = start
490 .checked_add(len)
491 .ok_or(ProtocolError::InvalidStringRange)?;
492 let slice = bytes
493 .get(start..end)
494 .ok_or(ProtocolError::InvalidStringRange)?;
495 std::str::from_utf8(slice).map_err(|_| ProtocolError::InvalidUtf8)
496 }
497}
498
499pub fn decode_commands(bytes: &[u8]) -> Result<Vec<DrawCommand>, ProtocolError> {
501 let mut commands = Vec::new();
502 let mut position = 0;
503 while position + COMMAND_LEN <= bytes.len() {
504 let record = &bytes[position..position + COMMAND_LEN];
505 let Ok(command) = DrawCommand::read_from(record) else {
509 break;
510 };
511 commands.push(command);
512 position += COMMAND_LEN;
513 }
514 Ok(commands)
515}
516
517#[derive(Debug, Default)]
520pub struct DrawListBuilder {
521 records: Vec<u8>,
522 text: Vec<u8>,
523 view_box: Option<(f32, f32)>,
524}
525
526impl DrawListBuilder {
527 pub fn with_capacity(commands: usize, text_bytes: usize) -> Self {
528 Self {
529 records: Vec::with_capacity(commands * COMMAND_LEN),
530 text: Vec::with_capacity(text_bytes),
531 view_box: None,
532 }
533 }
534
535 pub fn view_box(mut self, width: f32, height: f32) -> Self {
538 self.view_box = Some((width, height));
539 self
540 }
541
542 pub fn fractional(self) -> Self {
545 self.view_box(1.0, 1.0)
546 }
547
548 pub fn push(mut self, command: DrawCommand) -> Self {
551 command.write_to(&mut self.records);
552 self
553 }
554
555 pub fn line(self, paint: Paint, x1: f32, y1: f32, x2: f32, y2: f32, stroke_width: f32) -> Self {
556 self.push(DrawCommand::Line {
557 paint,
558 x1,
559 y1,
560 x2,
561 y2,
562 stroke_width,
563 })
564 }
565
566 pub fn rect(
568 self,
569 paint: Paint,
570 x: f32,
571 y: f32,
572 width: f32,
573 height: f32,
574 stroke_width: f32,
575 ) -> Self {
576 self.push(DrawCommand::Rect {
577 paint,
578 x,
579 y,
580 width,
581 height,
582 stroke_width,
583 })
584 }
585
586 #[allow(clippy::too_many_arguments)]
587 pub fn round_rect(
588 self,
589 paint: Paint,
590 x: f32,
591 y: f32,
592 width: f32,
593 height: f32,
594 radius: f32,
595 stroke_width: f32,
596 ) -> Self {
597 self.push(DrawCommand::RoundRect {
598 paint,
599 x,
600 y,
601 width,
602 height,
603 radius,
604 stroke_width,
605 })
606 }
607
608 pub fn circle(
609 self,
610 paint: Paint,
611 center_x: f32,
612 center_y: f32,
613 radius: f32,
614 stroke_width: f32,
615 ) -> Self {
616 self.push(DrawCommand::Circle {
617 paint,
618 center_x,
619 center_y,
620 radius,
621 stroke_width,
622 })
623 }
624
625 #[allow(clippy::too_many_arguments)]
626 pub fn arc(
627 self,
628 paint: Paint,
629 center_x: f32,
630 center_y: f32,
631 radius: f32,
632 start_degrees: f32,
633 sweep_degrees: f32,
634 stroke_width: f32,
635 ) -> Self {
636 self.push(DrawCommand::Arc {
637 paint,
638 center_x,
639 center_y,
640 radius,
641 start_degrees,
642 sweep_degrees,
643 stroke_width,
644 })
645 }
646
647 pub fn polyline_ref(self, paint: Paint, asset_id: u32, stroke_width: f32) -> Self {
650 self.push(DrawCommand::PolylineRef {
651 paint,
652 asset_id,
653 stroke_width,
654 })
655 }
656
657 pub fn text_at(
658 mut self,
659 paint: Paint,
660 text: &str,
661 x: f32,
662 y: f32,
663 type_role: TypeRole,
664 ) -> Self {
665 let offset = self.text.len() as u32;
668 self.text.extend_from_slice(text.as_bytes());
669 self.push(DrawCommand::TextAt {
670 paint,
671 text_offset: offset,
672 text_length: text.len() as u32,
673 x,
674 y,
675 type_role,
676 })
677 }
678
679 pub fn build(mut self) -> DrawList {
680 let records_len = self.records.len() as u32;
681 let mut position = 0;
682 while position + COMMAND_LEN <= self.records.len() {
683 if u16::from_le_bytes([self.records[position], self.records[position + 1]])
684 == TAG_TEXT_AT
685 {
686 let field = position + 12;
687 let relative = u32::from_le_bytes(
688 self.records[field..field + 4]
689 .try_into()
690 .expect("four bytes inside a 36 byte record"),
691 );
692 self.records[field..field + 4]
693 .copy_from_slice(&relative.saturating_add(records_len).to_le_bytes());
694 }
695 position += COMMAND_LEN;
696 }
697 self.records.append(&mut self.text);
698 let mut list = DrawList::from_bytes(self.records);
699 list.view_box = self.view_box;
700 list
701 }
702}
703
704#[cfg(test)]
705mod tests {
706 use super::*;
707 use crate::schema::{Color, ColorRole};
708
709 #[test]
710 fn fr17_every_command_round_trips_in_a_fixed_length_record() {
711 let list = DrawList::builder()
712 .line(Paint::Role(ColorRole::Primary), 1.0, 2.0, 3.0, 4.0, 1.5)
713 .rect(
714 Paint::Literal(Color::rgb(0x1b1b1f)),
715 0.0,
716 0.0,
717 8.0,
718 9.0,
719 0.0,
720 )
721 .round_rect(
722 Paint::Role(ColorRole::Surface),
723 1.0,
724 2.0,
725 3.0,
726 4.0,
727 5.0,
728 6.0,
729 )
730 .circle(Paint::Role(ColorRole::Error), 4.0, 5.0, 6.0, 0.5)
731 .arc(
732 Paint::Role(ColorRole::Outline),
733 1.0,
734 2.0,
735 3.0,
736 0.0,
737 90.0,
738 2.0,
739 )
740 .polyline_ref(Paint::Role(ColorRole::Secondary), 42, 3.0)
741 .text_at(
742 Paint::Role(ColorRole::OnSurface),
743 "한글",
744 7.0,
745 8.0,
746 TypeRole::Label,
747 )
748 .build();
749
750 let commands = list.decode().unwrap();
751
752 assert_eq!(commands.len(), DRAW_COMMAND_SCHEMA.len());
753 assert_eq!(
754 list.as_bytes().len(),
755 DRAW_COMMAND_SCHEMA.len() * COMMAND_LEN + "한글".len(),
756 );
757 for (command, schema) in commands.iter().zip(DRAW_COMMAND_SCHEMA) {
758 assert_eq!(command.tag(), schema.tag, "{} is out of order", schema.name);
759 }
760 }
761
762 #[test]
763 fn fr17_text_offsets_are_relative_to_the_list_itself() {
764 let list = DrawList::builder()
765 .circle(Paint::Role(ColorRole::Primary), 0.0, 0.0, 1.0, 0.0)
766 .text_at(
767 Paint::Role(ColorRole::OnSurface),
768 "hi",
769 1.0,
770 2.0,
771 TypeRole::Body,
772 )
773 .text_at(
774 Paint::Role(ColorRole::OnSurface),
775 "there",
776 3.0,
777 4.0,
778 TypeRole::Body,
779 )
780 .build();
781
782 let texts: Vec<_> = list
783 .decode()
784 .unwrap()
785 .into_iter()
786 .filter_map(|command| match command {
787 DrawCommand::TextAt {
788 text_offset,
789 text_length,
790 ..
791 } => Some(list.text_at(text_offset, text_length).unwrap().to_owned()),
792 _ => None,
793 })
794 .collect();
795
796 assert_eq!(texts, ["hi", "there"]);
797 }
798
799 #[test]
800 fn fr17_paint_carries_a_color_role_into_the_canvas() {
801 let list = DrawList::builder()
802 .line(Paint::Role(ColorRole::Primary), 0.0, 0.0, 1.0, 1.0, 1.0)
803 .build();
804
805 assert_eq!(
806 list.decode().unwrap()[0].paint(),
807 Paint::Role(ColorRole::Primary),
808 );
809 }
810
811 #[test]
812 fn fr17_two_identical_lists_compare_equal() {
813 let build = || {
814 DrawList::builder()
815 .rect(Paint::Role(ColorRole::Surface), 0.0, 0.0, 4.0, 4.0, 0.0)
816 .build()
817 };
818
819 assert_eq!(build(), build());
820 }
821}