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(
661 mut self,
662 paint: Paint,
663 text: &str,
664 x: f32,
665 y: f32,
666 type_role: TypeRole,
667 ) -> Self {
668 let offset = self.text.len() as u32;
671 self.text.extend_from_slice(text.as_bytes());
672 self.push(DrawCommand::TextAt {
673 paint,
674 text_offset: offset,
675 text_length: text.len() as u32,
676 x,
677 y,
678 type_role,
679 })
680 }
681
682 pub fn build(mut self) -> DrawList {
683 let records_len = self.records.len() as u32;
684 let mut position = 0;
685 while position + COMMAND_LEN <= self.records.len() {
686 if u16::from_le_bytes([self.records[position], self.records[position + 1]])
687 == TAG_TEXT_AT
688 {
689 let field = position + 12;
690 let relative = u32::from_le_bytes(
691 self.records[field..field + 4]
692 .try_into()
693 .expect("four bytes inside a 36 byte record"),
694 );
695 self.records[field..field + 4]
696 .copy_from_slice(&relative.saturating_add(records_len).to_le_bytes());
697 }
698 position += COMMAND_LEN;
699 }
700 self.records.append(&mut self.text);
701 let mut list = DrawList::from_bytes(self.records);
702 list.view_box = self.view_box;
703 list
704 }
705}
706
707#[cfg(test)]
708mod tests {
709 use super::*;
710 use crate::schema::{Color, ColorRole};
711
712 #[test]
713 fn fr17_every_command_round_trips_in_a_fixed_length_record() {
714 let list = DrawList::builder()
715 .line(Paint::Role(ColorRole::Primary), 1.0, 2.0, 3.0, 4.0, 1.5)
716 .rect(
717 Paint::Literal(Color::rgb(0x1b1b1f)),
718 0.0,
719 0.0,
720 8.0,
721 9.0,
722 0.0,
723 )
724 .round_rect(
725 Paint::Role(ColorRole::Surface),
726 1.0,
727 2.0,
728 3.0,
729 4.0,
730 5.0,
731 6.0,
732 )
733 .circle(Paint::Role(ColorRole::Error), 4.0, 5.0, 6.0, 0.5)
734 .arc(
735 Paint::Role(ColorRole::Outline),
736 1.0,
737 2.0,
738 3.0,
739 0.0,
740 90.0,
741 2.0,
742 )
743 .polyline_ref(Paint::Role(ColorRole::Secondary), 42, 3.0)
744 .text_at(
745 Paint::Role(ColorRole::OnSurface),
746 "한글",
747 7.0,
748 8.0,
749 TypeRole::Label,
750 )
751 .build();
752
753 let commands = list.decode().unwrap();
754
755 assert_eq!(commands.len(), DRAW_COMMAND_SCHEMA.len());
756 assert_eq!(
757 list.as_bytes().len(),
758 DRAW_COMMAND_SCHEMA.len() * COMMAND_LEN + "한글".len(),
759 );
760 for (command, schema) in commands.iter().zip(DRAW_COMMAND_SCHEMA) {
761 assert_eq!(command.tag(), schema.tag, "{} is out of order", schema.name);
762 }
763 }
764
765 #[test]
766 fn fr17_text_offsets_are_relative_to_the_list_itself() {
767 let list = DrawList::builder()
768 .circle(Paint::Role(ColorRole::Primary), 0.0, 0.0, 1.0, 0.0)
769 .text_at(
770 Paint::Role(ColorRole::OnSurface),
771 "hi",
772 1.0,
773 2.0,
774 TypeRole::Body,
775 )
776 .text_at(
777 Paint::Role(ColorRole::OnSurface),
778 "there",
779 3.0,
780 4.0,
781 TypeRole::Body,
782 )
783 .build();
784
785 let texts: Vec<_> = list
786 .decode()
787 .unwrap()
788 .into_iter()
789 .filter_map(|command| match command {
790 DrawCommand::TextAt {
791 text_offset,
792 text_length,
793 ..
794 } => Some(list.text_at(text_offset, text_length).unwrap().to_owned()),
795 _ => None,
796 })
797 .collect();
798
799 assert_eq!(texts, ["hi", "there"]);
800 }
801
802 #[test]
803 fn fr17_paint_carries_a_color_role_into_the_canvas() {
804 let list = DrawList::builder()
805 .line(Paint::Role(ColorRole::Primary), 0.0, 0.0, 1.0, 1.0, 1.0)
806 .build();
807
808 assert_eq!(
809 list.decode().unwrap()[0].paint(),
810 Paint::Role(ColorRole::Primary),
811 );
812 }
813
814 #[test]
815 fn fr17_two_identical_lists_compare_equal() {
816 let build = || {
817 DrawList::builder()
818 .rect(Paint::Role(ColorRole::Surface), 0.0, 0.0, 4.0, 4.0, 0.0)
819 .build()
820 };
821
822 assert_eq!(build(), build());
823 }
824}