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, Eq, PartialEq)]
435pub struct DrawList {
436 bytes: Option<Rc<[u8]>>,
437}
438
439impl std::fmt::Debug for DrawList {
440 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
441 formatter
442 .debug_struct("DrawList")
443 .field("bytes", &self.as_bytes().len())
444 .finish()
445 }
446}
447
448impl DrawList {
449 pub fn builder() -> DrawListBuilder {
450 DrawListBuilder::default()
451 }
452
453 pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
455 Self {
456 bytes: Some(bytes.into()),
457 }
458 }
459
460 pub fn as_bytes(&self) -> &[u8] {
461 self.bytes.as_deref().unwrap_or(&[])
462 }
463
464 pub fn is_empty(&self) -> bool {
465 self.as_bytes().is_empty()
466 }
467
468 pub fn decode(&self) -> Result<Vec<DrawCommand>, ProtocolError> {
470 decode_commands(self.as_bytes())
471 }
472
473 pub fn text_at(&self, offset: u32, length: u32) -> Result<&str, ProtocolError> {
475 let bytes = self.as_bytes();
476 let start = usize::try_from(offset).map_err(|_| ProtocolError::LengthOverflow)?;
477 let len = usize::try_from(length).map_err(|_| ProtocolError::LengthOverflow)?;
478 let end = start
479 .checked_add(len)
480 .ok_or(ProtocolError::InvalidStringRange)?;
481 let slice = bytes
482 .get(start..end)
483 .ok_or(ProtocolError::InvalidStringRange)?;
484 std::str::from_utf8(slice).map_err(|_| ProtocolError::InvalidUtf8)
485 }
486}
487
488pub fn decode_commands(bytes: &[u8]) -> Result<Vec<DrawCommand>, ProtocolError> {
490 let mut commands = Vec::new();
491 let mut position = 0;
492 while position + COMMAND_LEN <= bytes.len() {
493 let record = &bytes[position..position + COMMAND_LEN];
494 let Ok(command) = DrawCommand::read_from(record) else {
498 break;
499 };
500 commands.push(command);
501 position += COMMAND_LEN;
502 }
503 Ok(commands)
504}
505
506#[derive(Debug, Default)]
509pub struct DrawListBuilder {
510 records: Vec<u8>,
511 text: Vec<u8>,
512}
513
514impl DrawListBuilder {
515 pub fn with_capacity(commands: usize, text_bytes: usize) -> Self {
516 Self {
517 records: Vec::with_capacity(commands * COMMAND_LEN),
518 text: Vec::with_capacity(text_bytes),
519 }
520 }
521
522 pub fn push(mut self, command: DrawCommand) -> Self {
525 command.write_to(&mut self.records);
526 self
527 }
528
529 pub fn line(self, paint: Paint, x1: f32, y1: f32, x2: f32, y2: f32, stroke_width: f32) -> Self {
530 self.push(DrawCommand::Line {
531 paint,
532 x1,
533 y1,
534 x2,
535 y2,
536 stroke_width,
537 })
538 }
539
540 pub fn rect(
542 self,
543 paint: Paint,
544 x: f32,
545 y: f32,
546 width: f32,
547 height: f32,
548 stroke_width: f32,
549 ) -> Self {
550 self.push(DrawCommand::Rect {
551 paint,
552 x,
553 y,
554 width,
555 height,
556 stroke_width,
557 })
558 }
559
560 #[allow(clippy::too_many_arguments)]
561 pub fn round_rect(
562 self,
563 paint: Paint,
564 x: f32,
565 y: f32,
566 width: f32,
567 height: f32,
568 radius: f32,
569 stroke_width: f32,
570 ) -> Self {
571 self.push(DrawCommand::RoundRect {
572 paint,
573 x,
574 y,
575 width,
576 height,
577 radius,
578 stroke_width,
579 })
580 }
581
582 pub fn circle(
583 self,
584 paint: Paint,
585 center_x: f32,
586 center_y: f32,
587 radius: f32,
588 stroke_width: f32,
589 ) -> Self {
590 self.push(DrawCommand::Circle {
591 paint,
592 center_x,
593 center_y,
594 radius,
595 stroke_width,
596 })
597 }
598
599 #[allow(clippy::too_many_arguments)]
600 pub fn arc(
601 self,
602 paint: Paint,
603 center_x: f32,
604 center_y: f32,
605 radius: f32,
606 start_degrees: f32,
607 sweep_degrees: f32,
608 stroke_width: f32,
609 ) -> Self {
610 self.push(DrawCommand::Arc {
611 paint,
612 center_x,
613 center_y,
614 radius,
615 start_degrees,
616 sweep_degrees,
617 stroke_width,
618 })
619 }
620
621 pub fn polyline_ref(self, paint: Paint, asset_id: u32, stroke_width: f32) -> Self {
624 self.push(DrawCommand::PolylineRef {
625 paint,
626 asset_id,
627 stroke_width,
628 })
629 }
630
631 pub fn text_at(
632 mut self,
633 paint: Paint,
634 text: &str,
635 x: f32,
636 y: f32,
637 type_role: TypeRole,
638 ) -> Self {
639 let offset = self.text.len() as u32;
642 self.text.extend_from_slice(text.as_bytes());
643 self.push(DrawCommand::TextAt {
644 paint,
645 text_offset: offset,
646 text_length: text.len() as u32,
647 x,
648 y,
649 type_role,
650 })
651 }
652
653 pub fn build(mut self) -> DrawList {
654 let records_len = self.records.len() as u32;
655 let mut position = 0;
656 while position + COMMAND_LEN <= self.records.len() {
657 if u16::from_le_bytes([self.records[position], self.records[position + 1]])
658 == TAG_TEXT_AT
659 {
660 let field = position + 12;
661 let relative = u32::from_le_bytes(
662 self.records[field..field + 4]
663 .try_into()
664 .expect("four bytes inside a 36 byte record"),
665 );
666 self.records[field..field + 4]
667 .copy_from_slice(&relative.saturating_add(records_len).to_le_bytes());
668 }
669 position += COMMAND_LEN;
670 }
671 self.records.append(&mut self.text);
672 DrawList::from_bytes(self.records)
673 }
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use crate::schema::{Color, ColorRole};
680
681 #[test]
682 fn fr17_every_command_round_trips_in_a_fixed_length_record() {
683 let list = DrawList::builder()
684 .line(Paint::Role(ColorRole::Primary), 1.0, 2.0, 3.0, 4.0, 1.5)
685 .rect(
686 Paint::Literal(Color::rgb(0x1b1b1f)),
687 0.0,
688 0.0,
689 8.0,
690 9.0,
691 0.0,
692 )
693 .round_rect(
694 Paint::Role(ColorRole::Surface),
695 1.0,
696 2.0,
697 3.0,
698 4.0,
699 5.0,
700 6.0,
701 )
702 .circle(Paint::Role(ColorRole::Error), 4.0, 5.0, 6.0, 0.5)
703 .arc(
704 Paint::Role(ColorRole::Outline),
705 1.0,
706 2.0,
707 3.0,
708 0.0,
709 90.0,
710 2.0,
711 )
712 .polyline_ref(Paint::Role(ColorRole::Secondary), 42, 3.0)
713 .text_at(
714 Paint::Role(ColorRole::OnSurface),
715 "한글",
716 7.0,
717 8.0,
718 TypeRole::Label,
719 )
720 .build();
721
722 let commands = list.decode().unwrap();
723
724 assert_eq!(commands.len(), DRAW_COMMAND_SCHEMA.len());
725 assert_eq!(
726 list.as_bytes().len(),
727 DRAW_COMMAND_SCHEMA.len() * COMMAND_LEN + "한글".len(),
728 );
729 for (command, schema) in commands.iter().zip(DRAW_COMMAND_SCHEMA) {
730 assert_eq!(command.tag(), schema.tag, "{} is out of order", schema.name);
731 }
732 }
733
734 #[test]
735 fn fr17_text_offsets_are_relative_to_the_list_itself() {
736 let list = DrawList::builder()
737 .circle(Paint::Role(ColorRole::Primary), 0.0, 0.0, 1.0, 0.0)
738 .text_at(
739 Paint::Role(ColorRole::OnSurface),
740 "hi",
741 1.0,
742 2.0,
743 TypeRole::Body,
744 )
745 .text_at(
746 Paint::Role(ColorRole::OnSurface),
747 "there",
748 3.0,
749 4.0,
750 TypeRole::Body,
751 )
752 .build();
753
754 let texts: Vec<_> = list
755 .decode()
756 .unwrap()
757 .into_iter()
758 .filter_map(|command| match command {
759 DrawCommand::TextAt {
760 text_offset,
761 text_length,
762 ..
763 } => Some(list.text_at(text_offset, text_length).unwrap().to_owned()),
764 _ => None,
765 })
766 .collect();
767
768 assert_eq!(texts, ["hi", "there"]);
769 }
770
771 #[test]
772 fn fr17_paint_carries_a_color_role_into_the_canvas() {
773 let list = DrawList::builder()
774 .line(Paint::Role(ColorRole::Primary), 0.0, 0.0, 1.0, 1.0, 1.0)
775 .build();
776
777 assert_eq!(
778 list.decode().unwrap()[0].paint(),
779 Paint::Role(ColorRole::Primary),
780 );
781 }
782
783 #[test]
784 fn fr17_two_identical_lists_compare_equal() {
785 let build = || {
786 DrawList::builder()
787 .rect(Paint::Role(ColorRole::Surface), 0.0, 0.0, 4.0, 4.0, 0.0)
788 .build()
789 };
790
791 assert_eq!(build(), build());
792 }
793}