1use super::semantics::{ActionEntry, Semantics};
2use super::widget_id::WidgetNodeId;
3use crate::NodeId;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
9pub enum Op {
10 Structural(StructuralOp),
11 Layout(LayoutOp),
12 Paint(PaintOp),
13 Semantics(Semantics),
14}
15
16impl std::hash::Hash for Op {
17 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
18 match self {
19 Self::Structural(s) => {
20 0.hash(state);
21 s.hash(state);
22 }
23 Self::Layout(l) => {
24 1.hash(state);
25 l.hash(state);
26 }
27 Self::Paint(p) => {
28 2.hash(state);
29 p.hash(state);
30 }
31 Self::Semantics(s) => {
32 3.hash(state);
33 s.hash(state);
34 }
35 }
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
40pub enum StructuralOp {
41 Group { stable_hash: u64 },
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
45pub struct CompositeScalar {
46 pub base: f32,
47 pub animation_target: Option<WidgetNodeId>,
48}
49
50impl CompositeScalar {
51 pub fn new(base: f32) -> Self {
52 Self {
53 base,
54 animation_target: None,
55 }
56 }
57
58 pub fn animated(mut self, target: WidgetNodeId) -> Self {
59 self.animation_target = Some(target);
60 self
61 }
62}
63
64impl std::hash::Hash for CompositeScalar {
65 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
66 self.base.to_bits().hash(state);
67 self.animation_target.hash(state);
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash, Default)]
72pub struct CompositeStyle {
73 pub opacity: Option<CompositeScalar>,
74 pub translate_x: Option<CompositeScalar>,
75 pub translate_y: Option<CompositeScalar>,
76 pub scale: Option<CompositeScalar>,
77 pub rotation: Option<CompositeScalar>,
78 pub clip_to_bounds: bool,
79 pub repaint_boundary: bool,
80}
81
82pub type LayoutUnit = f32;
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
85pub enum TextAlign {
86 Left,
87 Right,
88 Center,
89 Justify,
90 #[default]
91 Start,
92 End,
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
96pub enum TextOverflow {
97 Clip,
98 Ellipsis,
99 Fade,
100 #[default]
101 Visible,
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
105pub enum TextDirection {
106 #[default]
107 Auto,
108 Ltr,
109 Rtl,
110}
111
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
113pub enum TextWidthBasis {
114 #[default]
115 Parent,
116 LongestLine,
117}
118
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, Default)]
120pub enum MouseCursor {
121 #[default]
122 Basic,
123 Pointer,
124 Text,
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
128pub struct TextHeightBehavior {
129 pub apply_height_to_first_ascent: bool,
130 pub apply_height_to_last_descent: bool,
131}
132
133impl Default for TextHeightBehavior {
134 fn default() -> Self {
135 Self {
136 apply_height_to_first_ascent: true,
137 apply_height_to_last_descent: true,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
143pub struct TextParagraphStyle {
144 pub text_align: TextAlign,
145 pub max_lines: Option<usize>,
146 pub overflow: TextOverflow,
147 #[serde(default)]
148 pub text_direction: TextDirection,
149 #[serde(default)]
150 pub text_width_basis: TextWidthBasis,
151 #[serde(default)]
152 pub strut_line_height: Option<LayoutUnit>,
153 #[serde(default)]
154 pub text_height_behavior: TextHeightBehavior,
155}
156
157impl PartialEq for TextParagraphStyle {
158 fn eq(&self, other: &Self) -> bool {
159 self.text_align == other.text_align
160 && self.max_lines == other.max_lines
161 && self.overflow == other.overflow
162 && self.text_direction == other.text_direction
163 && self.text_width_basis == other.text_width_basis
164 && self.strut_line_height.map(f32::to_bits) == other.strut_line_height.map(f32::to_bits)
165 && self.text_height_behavior == other.text_height_behavior
166 }
167}
168
169impl Eq for TextParagraphStyle {}
170
171impl std::hash::Hash for TextParagraphStyle {
172 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
173 self.text_align.hash(state);
174 self.max_lines.hash(state);
175 self.overflow.hash(state);
176 self.text_direction.hash(state);
177 self.text_width_basis.hash(state);
178 self.strut_line_height.map(f32::to_bits).hash(state);
179 self.text_height_behavior.hash(state);
180 }
181}
182
183const TEXT_PARAGRAPH_ALIGN_BITS: u32 = 0b111;
184const TEXT_PARAGRAPH_OVERFLOW_BITS: u32 = 0b111 << 3;
185const TEXT_PARAGRAPH_MAX_LINES_SHIFT: u32 = 6;
186const TEXT_PARAGRAPH_SENTINEL: u32 = 1;
187const TEXT_PARAGRAPH_MAX_ENCODED_LINES: usize = ((1 << 24) - 1) >> TEXT_PARAGRAPH_MAX_LINES_SHIFT;
188
189const fn text_align_code(align: TextAlign) -> u32 {
190 match align {
191 TextAlign::Start => 0,
192 TextAlign::Left => 1,
193 TextAlign::Center => 2,
194 TextAlign::Right => 3,
195 TextAlign::End => 4,
196 TextAlign::Justify => 5,
197 }
198}
199
200const fn text_overflow_code(overflow: TextOverflow) -> u32 {
201 match overflow {
202 TextOverflow::Visible => 0,
203 TextOverflow::Clip => 1,
204 TextOverflow::Ellipsis => 2,
205 TextOverflow::Fade => 3,
206 }
207}
208
209const fn decode_text_align(code: u32) -> TextAlign {
210 match code {
211 1 => TextAlign::Left,
212 2 => TextAlign::Center,
213 3 => TextAlign::Right,
214 4 => TextAlign::End,
215 5 => TextAlign::Justify,
216 _ => TextAlign::Start,
217 }
218}
219
220const fn decode_text_overflow(code: u32) -> TextOverflow {
221 match code {
222 1 => TextOverflow::Clip,
223 2 => TextOverflow::Ellipsis,
224 3 => TextOverflow::Fade,
225 _ => TextOverflow::Visible,
226 }
227}
228
229pub fn encode_text_paragraph_style(style: TextParagraphStyle) -> Option<LayoutUnit> {
230 if style == TextParagraphStyle::default() {
231 return None;
232 }
233 if style.text_direction != TextDirection::Auto
234 || style.text_width_basis != TextWidthBasis::Parent
235 || style.strut_line_height.is_some()
236 || style.text_height_behavior != TextHeightBehavior::default()
237 {
238 return None;
239 }
240
241 let max_lines = style
242 .max_lines
243 .unwrap_or(0)
244 .min(TEXT_PARAGRAPH_MAX_ENCODED_LINES) as u32;
245 let encoded = TEXT_PARAGRAPH_SENTINEL
246 + text_align_code(style.text_align)
247 + (text_overflow_code(style.overflow) << 3)
248 + (max_lines << TEXT_PARAGRAPH_MAX_LINES_SHIFT);
249
250 Some(-(encoded as LayoutUnit))
251}
252
253pub fn decode_text_paragraph_style(
254 encoded_width: Option<LayoutUnit>,
255) -> Option<TextParagraphStyle> {
256 let encoded_width = encoded_width?;
257 if !encoded_width.is_finite() || encoded_width >= 0.0 {
258 return None;
259 }
260
261 let raw = (-encoded_width).round();
262 if raw < TEXT_PARAGRAPH_SENTINEL as f32 {
263 return None;
264 }
265
266 let bits = raw as u32 - TEXT_PARAGRAPH_SENTINEL;
267 let text_align = decode_text_align(bits & TEXT_PARAGRAPH_ALIGN_BITS);
268 let overflow = decode_text_overflow((bits & TEXT_PARAGRAPH_OVERFLOW_BITS) >> 3);
269 let max_lines = match bits >> TEXT_PARAGRAPH_MAX_LINES_SHIFT {
270 0 => None,
271 lines => Some(lines as usize),
272 };
273
274 Some(TextParagraphStyle {
275 text_align,
276 max_lines,
277 overflow,
278 text_direction: TextDirection::Auto,
279 text_width_basis: TextWidthBasis::Parent,
280 strut_line_height: None,
281 text_height_behavior: TextHeightBehavior::default(),
282 })
283}
284
285#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
286pub enum FlexDirection {
287 Row,
288 Column,
289}
290
291impl Default for FlexDirection {
292 fn default() -> Self {
293 FlexDirection::Row
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
298pub enum EmbedKind {
299 Video,
300 Web,
301 Custom(Vec<u8>),
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
305pub enum GridTrack {
306 Points(LayoutUnit),
307 Percent(f32),
308 Fr(f32),
309 Auto,
310 MinContent,
311 MaxContent,
312}
313
314impl std::hash::Hash for GridTrack {
315 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
316 match self {
317 Self::Points(u) => {
318 0.hash(state);
319 u.to_bits().hash(state);
320 }
321 Self::Percent(f) => {
322 1.hash(state);
323 f.to_bits().hash(state);
324 }
325 Self::Fr(f) => {
326 2.hash(state);
327 f.to_bits().hash(state);
328 }
329 Self::Auto => {
330 3.hash(state);
331 }
332 Self::MinContent => {
333 4.hash(state);
334 }
335 Self::MaxContent => {
336 5.hash(state);
337 }
338 }
339 }
340}
341
342#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
343pub enum GridPlacement {
344 Auto,
345 Line(i16),
346 Span(u16),
347}
348
349impl Default for GridPlacement {
350 fn default() -> Self {
351 Self::Auto
352 }
353}
354
355#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
356pub enum FlexWrap {
357 NoWrap,
358 Wrap,
359 WrapReverse,
360}
361
362impl Default for FlexWrap {
363 fn default() -> Self {
364 FlexWrap::NoWrap
365 }
366}
367
368#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
369pub enum AlignItems {
370 Start,
371 End,
372 Center,
373 Stretch,
374 Baseline,
375}
376
377impl Default for AlignItems {
378 fn default() -> Self {
379 AlignItems::Stretch
380 }
381}
382
383#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
384pub enum JustifyContent {
385 Start,
386 End,
387 Center,
388 SpaceBetween,
389 SpaceAround,
390 SpaceEvenly,
391}
392
393impl Default for JustifyContent {
394 fn default() -> Self {
395 JustifyContent::Start
396 }
397}
398
399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
400pub enum LayoutOp {
401 Box {
402 width: Option<LayoutUnit>,
403 height: Option<LayoutUnit>,
404 min_width: Option<LayoutUnit>,
405 max_width: Option<LayoutUnit>,
406 min_height: Option<LayoutUnit>,
407 max_height: Option<LayoutUnit>,
408 padding: [LayoutUnit; 4],
409 flex_grow: LayoutUnit,
410 flex_shrink: LayoutUnit,
411 aspect_ratio: Option<f32>,
412 },
413 Flex {
414 direction: FlexDirection,
415 wrap: FlexWrap,
416 flex_grow: LayoutUnit,
417 flex_shrink: LayoutUnit,
418 padding: [LayoutUnit; 4],
419 gap: Option<LayoutUnit>,
420 align_items: AlignItems,
421 justify_content: JustifyContent,
422 },
423 Grid {
424 columns: Vec<GridTrack>,
425 rows: Vec<GridTrack>,
426 column_gap: Option<LayoutUnit>,
427 row_gap: Option<LayoutUnit>,
428 padding: [LayoutUnit; 4],
429 },
430 GridItem {
431 row_start: GridPlacement,
432 row_end: GridPlacement,
433 col_start: GridPlacement,
434 col_end: GridPlacement,
435 },
436 Scroll {
437 direction: FlexDirection,
438 show_scrollbar: bool,
439 width: Option<LayoutUnit>,
440 height: Option<LayoutUnit>,
441 min_width: Option<LayoutUnit>,
442 max_width: Option<LayoutUnit>,
443 min_height: Option<LayoutUnit>,
444 max_height: Option<LayoutUnit>,
445 padding: [LayoutUnit; 4],
446 flex_grow: LayoutUnit,
447 flex_shrink: LayoutUnit,
448 },
449 Embed {
450 kind: EmbedKind,
451 widget_id: WidgetNodeId,
452 width: Option<LayoutUnit>,
453 height: Option<LayoutUnit>,
454 },
455 AbsoluteFill,
456 Positioned {
457 left: Option<LayoutUnit>,
458 top: Option<LayoutUnit>,
459 right: Option<LayoutUnit>,
460 bottom: Option<LayoutUnit>,
461 width: Option<LayoutUnit>,
462 height: Option<LayoutUnit>,
463 },
464 ZStack,
465 Align,
466 Flyout {
467 anchor: NodeId,
468 content: NodeId,
469 },
470 Transform {
471 transform: [f32; 16],
472 },
473 Clip {
474 path: Option<String>,
475 },
476}
477
478impl std::hash::Hash for LayoutOp {
479 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
480 let hash_unit = |u: LayoutUnit, h: &mut H| u.to_bits().hash(h);
481 let hash_opt_unit = |u: Option<LayoutUnit>, h: &mut H| u.map(|v| v.to_bits()).hash(h);
482 let hash_units = |us: [LayoutUnit; 4], h: &mut H| {
483 for u in us {
484 u.to_bits().hash(h);
485 }
486 };
487
488 match self {
489 Self::Box {
490 width,
491 height,
492 min_width,
493 max_width,
494 min_height,
495 max_height,
496 padding,
497 flex_grow,
498 flex_shrink,
499 aspect_ratio,
500 } => {
501 0.hash(state);
502 hash_opt_unit(*width, state);
503 hash_opt_unit(*height, state);
504 hash_opt_unit(*min_width, state);
505 hash_opt_unit(*max_width, state);
506 hash_opt_unit(*min_height, state);
507 hash_opt_unit(*max_height, state);
508 hash_units(*padding, state);
509 hash_unit(*flex_grow, state);
510 hash_unit(*flex_shrink, state);
511 aspect_ratio.map(|f| f.to_bits()).hash(state);
512 }
513 Self::Flex {
514 direction,
515 wrap,
516 flex_grow,
517 flex_shrink,
518 padding,
519 gap,
520 align_items,
521 justify_content,
522 } => {
523 1.hash(state);
524 direction.hash(state);
525 wrap.hash(state);
526 hash_unit(*flex_grow, state);
527 hash_unit(*flex_shrink, state);
528 hash_units(*padding, state);
529 hash_opt_unit(*gap, state);
530 align_items.hash(state);
531 justify_content.hash(state);
532 }
533 Self::Grid {
534 columns,
535 rows,
536 column_gap,
537 row_gap,
538 padding,
539 } => {
540 2.hash(state);
541 columns.hash(state);
542 rows.hash(state);
543 hash_opt_unit(*column_gap, state);
544 hash_opt_unit(*row_gap, state);
545 hash_units(*padding, state);
546 }
547 Self::GridItem {
548 row_start,
549 row_end,
550 col_start,
551 col_end,
552 } => {
553 3.hash(state);
554 row_start.hash(state);
555 row_end.hash(state);
556 col_start.hash(state);
557 col_end.hash(state);
558 }
559 Self::Scroll {
560 direction,
561 show_scrollbar,
562 width,
563 height,
564 min_width,
565 max_width,
566 min_height,
567 max_height,
568 padding,
569 flex_grow,
570 flex_shrink,
571 } => {
572 4.hash(state);
573 direction.hash(state);
574 show_scrollbar.hash(state);
575 hash_opt_unit(*width, state);
576 hash_opt_unit(*height, state);
577 hash_opt_unit(*min_width, state);
578 hash_opt_unit(*max_width, state);
579 hash_opt_unit(*min_height, state);
580 hash_opt_unit(*max_height, state);
581 hash_units(*padding, state);
582 hash_unit(*flex_grow, state);
583 hash_unit(*flex_shrink, state);
584 }
585 Self::Embed {
586 kind,
587 widget_id,
588 width,
589 height,
590 } => {
591 5.hash(state);
592 kind.hash(state);
593 widget_id.hash(state);
594 hash_opt_unit(*width, state);
595 hash_opt_unit(*height, state);
596 }
597 Self::AbsoluteFill => {
598 6.hash(state);
599 }
600 Self::Positioned {
601 left,
602 top,
603 right,
604 bottom,
605 width,
606 height,
607 } => {
608 7.hash(state);
609 hash_opt_unit(*left, state);
610 hash_opt_unit(*top, state);
611 hash_opt_unit(*right, state);
612 hash_opt_unit(*bottom, state);
613 hash_opt_unit(*width, state);
614 hash_opt_unit(*height, state);
615 }
616 Self::ZStack => {
617 8.hash(state);
618 }
619 Self::Align => {
620 9.hash(state);
621 }
622 Self::Flyout { anchor, content } => {
623 10.hash(state);
624 anchor.hash(state);
625 content.hash(state);
626 }
627 Self::Transform { transform } => {
628 11.hash(state);
629 for v in transform {
630 v.to_bits().hash(state);
631 }
632 }
633 Self::Clip { path } => {
634 12.hash(state);
635 path.hash(state);
636 }
637 }
638 }
639}
640
641#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
642pub struct Color {
643 pub r: u8,
644 pub g: u8,
645 pub b: u8,
646 pub a: u8,
647}
648
649impl Color {
650 pub const BLACK: Self = Self {
651 r: 0,
652 g: 0,
653 b: 0,
654 a: 255,
655 };
656 pub const WHITE: Self = Self {
657 r: 255,
658 g: 255,
659 b: 255,
660 a: 255,
661 };
662 pub const RED: Self = Self {
663 r: 255,
664 g: 0,
665 b: 0,
666 a: 255,
667 };
668 pub const GREEN: Self = Self {
669 r: 0,
670 g: 255,
671 b: 0,
672 a: 255,
673 };
674 pub const BLUE: Self = Self {
675 r: 0,
676 g: 0,
677 b: 255,
678 a: 255,
679 };
680
681 pub fn with_alpha(mut self, a: u8) -> Self {
682 self.a = a;
683 self
684 }
685}
686
687#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
688pub enum Fill {
689 Solid(Color),
690 LinearGradient {
691 start: (f32, f32),
692 end: (f32, f32),
693 stops: Vec<(f32, Color)>,
694 },
695 RadialGradient {
696 center: (f32, f32),
697 radius: f32,
698 stops: Vec<(f32, Color)>,
699 },
700}
701
702impl std::hash::Hash for Fill {
703 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
704 match self {
705 Self::Solid(c) => {
706 0.hash(state);
707 c.hash(state);
708 }
709 Self::LinearGradient { start, end, stops } => {
710 1.hash(state);
711 start.0.to_bits().hash(state);
712 start.1.to_bits().hash(state);
713 end.0.to_bits().hash(state);
714 end.1.to_bits().hash(state);
715 for (off, c) in stops {
716 off.to_bits().hash(state);
717 c.hash(state);
718 }
719 }
720 Self::RadialGradient {
721 center,
722 radius,
723 stops,
724 } => {
725 2.hash(state);
726 center.0.to_bits().hash(state);
727 center.1.to_bits().hash(state);
728 radius.to_bits().hash(state);
729 for (off, c) in stops {
730 off.to_bits().hash(state);
731 c.hash(state);
732 }
733 }
734 }
735 }
736}
737
738#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
739pub enum LineCap {
740 Butt,
741 Round,
742 Square,
743}
744
745#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
746pub enum LineJoin {
747 Miter,
748 Round,
749 Bevel,
750}
751
752#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
753pub struct Stroke {
754 pub fill: Fill,
755 pub width: LayoutUnit,
756 pub dash_array: Option<Vec<f32>>,
757 pub line_cap: LineCap,
758 pub line_join: LineJoin,
759}
760
761impl std::hash::Hash for Stroke {
762 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
763 self.fill.hash(state);
764 self.width.to_bits().hash(state);
765 if let Some(da) = &self.dash_array {
766 1.hash(state);
767 for d in da {
768 d.to_bits().hash(state);
769 }
770 } else {
771 0.hash(state);
772 }
773 self.line_cap.hash(state);
774 self.line_join.hash(state);
775 }
776}
777
778#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
779pub struct BoxShadow {
780 pub color: Color,
781 pub blur_radius: LayoutUnit,
782 pub offset: (LayoutUnit, LayoutUnit),
783}
784
785impl std::hash::Hash for BoxShadow {
786 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
787 self.color.hash(state);
788 self.blur_radius.to_bits().hash(state);
789 self.offset.0.to_bits().hash(state);
790 self.offset.1.to_bits().hash(state);
791 }
792}
793
794#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Hash)]
795pub enum ImageFit {
796 Contain,
797 Cover,
798 Fill,
799 None,
800}
801
802#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
803pub struct TextStyle {
804 pub font_size: LayoutUnit,
805 pub color: Color,
806 pub underline: bool,
807 #[serde(default)]
808 pub font_family: Option<String>,
809 #[serde(default)]
810 pub locale: Option<String>,
811 #[serde(default = "text_weight_default")]
812 pub font_weight: u16,
813 #[serde(default)]
814 pub font_style: FontStyle,
815 #[serde(default)]
816 pub line_height: Option<LayoutUnit>,
817 #[serde(default)]
818 pub letter_spacing: LayoutUnit,
819 pub background_color: Option<Color>,
821}
822
823impl std::hash::Hash for TextStyle {
824 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
825 self.font_size.to_bits().hash(state);
826 self.color.hash(state);
827 self.underline.hash(state);
828 self.font_family.hash(state);
829 self.locale.hash(state);
830 self.font_weight.hash(state);
831 self.font_style.hash(state);
832 self.line_height.map(f32::to_bits).hash(state);
833 self.letter_spacing.to_bits().hash(state);
834 self.background_color.hash(state);
835 }
836}
837
838#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
839pub enum FontStyle {
840 #[default]
841 Normal,
842 Italic,
843}
844
845const fn text_weight_default() -> u16 {
846 400
847}
848
849#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Hash)]
850pub struct TextRun {
851 pub text: String,
852 pub style: TextStyle,
853}
854
855#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
856pub struct RichTextAnnotation {
857 pub range: std::ops::Range<usize>,
858 #[serde(default)]
859 pub semantics_label: Option<String>,
860 #[serde(default)]
861 pub semantics_identifier: Option<String>,
862 #[serde(default)]
863 pub spell_out: Option<bool>,
864 #[serde(default)]
865 pub mouse_cursor: Option<MouseCursor>,
866 #[serde(default)]
867 pub actions: Vec<ActionEntry>,
868}
869
870pub const INLINE_WIDGET_MARKER_PREFIX: &str = "__fission_inline_widget__:";
871
872#[derive(Debug, Clone, Copy, PartialEq)]
873pub struct InlineWidgetMarker {
874 pub id: u64,
875 pub width: LayoutUnit,
876 pub height: LayoutUnit,
877}
878
879pub fn encode_inline_widget_marker(id: u64, width: LayoutUnit, height: LayoutUnit) -> String {
880 format!("{INLINE_WIDGET_MARKER_PREFIX}{id}:{width}:{height}")
881}
882
883pub fn decode_inline_widget_marker(family: Option<&str>) -> Option<InlineWidgetMarker> {
884 let family = family?;
885 let encoded = family.strip_prefix(INLINE_WIDGET_MARKER_PREFIX)?;
886 let mut parts = encoded.split(':');
887 let id = parts.next()?.parse().ok()?;
888 let width = parts.next()?.parse().ok()?;
889 let height = parts.next()?.parse().ok()?;
890 if parts.next().is_some() {
891 return None;
892 }
893 Some(InlineWidgetMarker { id, width, height })
894}
895
896const fn text_wrap_default() -> bool {
897 true
898}
899
900#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
901pub enum PaintOp {
902 DrawRect {
903 fill: Option<Fill>,
904 stroke: Option<Stroke>,
905 corner_radius: LayoutUnit,
906 shadow: Option<BoxShadow>,
907 },
908 DrawText {
909 text: String,
910 size: LayoutUnit,
911 color: Color,
912 underline: bool,
913 #[serde(default = "text_wrap_default")]
914 wrap: bool,
915 caret_index: Option<usize>,
916 #[serde(default)]
917 caret_color: Option<Color>,
918 #[serde(default)]
919 caret_width: Option<LayoutUnit>,
920 #[serde(default)]
921 caret_height: Option<LayoutUnit>,
922 #[serde(default)]
923 caret_radius: Option<LayoutUnit>,
924 #[serde(default)]
925 paragraph_style: Option<TextParagraphStyle>,
926 },
927 DrawRichText {
928 runs: Vec<TextRun>,
929 #[serde(default = "text_wrap_default")]
930 wrap: bool,
931 caret_index: Option<usize>,
932 #[serde(default)]
933 caret_color: Option<Color>,
934 #[serde(default)]
935 caret_width: Option<LayoutUnit>,
936 #[serde(default)]
937 caret_height: Option<LayoutUnit>,
938 #[serde(default)]
939 caret_radius: Option<LayoutUnit>,
940 #[serde(default)]
941 paragraph_style: Option<TextParagraphStyle>,
942 },
943 DrawImage {
944 source: String,
945 fit: ImageFit,
946 },
947 DrawPath {
948 path: String,
949 fill: Option<Fill>,
950 stroke: Option<Stroke>,
951 },
952 DrawSvg {
953 content: String,
954 fill: Option<Fill>,
955 stroke: Option<Stroke>,
956 },
957}
958
959impl std::hash::Hash for PaintOp {
960 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
961 match self {
962 Self::DrawRect {
963 fill,
964 stroke,
965 corner_radius,
966 shadow,
967 } => {
968 0.hash(state);
969 fill.hash(state);
970 stroke.hash(state);
971 corner_radius.to_bits().hash(state);
972 shadow.hash(state);
973 }
974 Self::DrawText {
975 text,
976 size,
977 color,
978 underline,
979 wrap,
980 caret_index,
981 caret_color,
982 caret_width,
983 caret_height,
984 caret_radius,
985 paragraph_style,
986 } => {
987 1.hash(state);
988 text.hash(state);
989 size.to_bits().hash(state);
990 color.hash(state);
991 underline.hash(state);
992 wrap.hash(state);
993 caret_index.hash(state);
994 caret_color.hash(state);
995 caret_width.map(|w| w.to_bits()).hash(state);
996 caret_height.map(|h| h.to_bits()).hash(state);
997 caret_radius.map(|r| r.to_bits()).hash(state);
998 paragraph_style.hash(state);
999 }
1000 Self::DrawRichText {
1001 runs,
1002 wrap,
1003 caret_index,
1004 caret_color,
1005 caret_width,
1006 caret_height,
1007 caret_radius,
1008 paragraph_style,
1009 } => {
1010 2.hash(state);
1011 runs.hash(state);
1012 wrap.hash(state);
1013 caret_index.hash(state);
1014 caret_color.hash(state);
1015 caret_width.map(|w| w.to_bits()).hash(state);
1016 caret_height.map(|h| h.to_bits()).hash(state);
1017 caret_radius.map(|r| r.to_bits()).hash(state);
1018 paragraph_style.hash(state);
1019 }
1020 Self::DrawImage { source, fit } => {
1021 3.hash(state);
1022 source.hash(state);
1023 fit.hash(state);
1024 }
1025 Self::DrawPath { path, fill, stroke } => {
1026 4.hash(state);
1027 path.hash(state);
1028 fill.hash(state);
1029 stroke.hash(state);
1030 }
1031 Self::DrawSvg {
1032 content,
1033 fill,
1034 stroke,
1035 } => {
1036 5.hash(state);
1037 content.hash(state);
1038 fill.hash(state);
1039 stroke.hash(state);
1040 }
1041 }
1042 }
1043}
1044
1045#[cfg(test)]
1046mod tests {
1047 use super::{
1048 decode_inline_widget_marker, decode_text_paragraph_style, encode_inline_widget_marker,
1049 encode_text_paragraph_style, InlineWidgetMarker, TextAlign, TextDirection,
1050 TextHeightBehavior, TextOverflow, TextParagraphStyle, TextWidthBasis,
1051 TEXT_PARAGRAPH_MAX_ENCODED_LINES,
1052 };
1053
1054 #[test]
1055 fn paragraph_style_round_trips_alignment_overflow_and_line_cap() {
1056 let style = TextParagraphStyle {
1057 text_align: TextAlign::Justify,
1058 max_lines: Some(3),
1059 overflow: TextOverflow::Fade,
1060 text_direction: TextDirection::Auto,
1061 text_width_basis: TextWidthBasis::Parent,
1062 strut_line_height: None,
1063 text_height_behavior: TextHeightBehavior::default(),
1064 };
1065
1066 let encoded = encode_text_paragraph_style(style);
1067 assert_eq!(decode_text_paragraph_style(encoded), Some(style));
1068 }
1069
1070 #[test]
1071 fn paragraph_style_clamps_line_count_to_precise_encoding_budget() {
1072 let encoded = encode_text_paragraph_style(TextParagraphStyle {
1073 text_align: TextAlign::End,
1074 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES + 99),
1075 overflow: TextOverflow::Ellipsis,
1076 text_direction: TextDirection::Auto,
1077 text_width_basis: TextWidthBasis::Parent,
1078 strut_line_height: None,
1079 text_height_behavior: TextHeightBehavior::default(),
1080 });
1081
1082 assert_eq!(
1083 decode_text_paragraph_style(encoded),
1084 Some(TextParagraphStyle {
1085 text_align: TextAlign::End,
1086 max_lines: Some(TEXT_PARAGRAPH_MAX_ENCODED_LINES),
1087 overflow: TextOverflow::Ellipsis,
1088 text_direction: TextDirection::Auto,
1089 text_width_basis: TextWidthBasis::Parent,
1090 strut_line_height: None,
1091 text_height_behavior: TextHeightBehavior::default(),
1092 })
1093 );
1094 }
1095
1096 #[test]
1097 fn paragraph_style_compact_encoding_rejects_extended_fields() {
1098 assert_eq!(
1099 encode_text_paragraph_style(TextParagraphStyle {
1100 text_align: TextAlign::Start,
1101 max_lines: Some(2),
1102 overflow: TextOverflow::Visible,
1103 text_direction: TextDirection::Rtl,
1104 text_width_basis: TextWidthBasis::LongestLine,
1105 strut_line_height: Some(24.0),
1106 text_height_behavior: TextHeightBehavior {
1107 apply_height_to_first_ascent: false,
1108 apply_height_to_last_descent: true,
1109 },
1110 }),
1111 None
1112 );
1113 }
1114
1115 #[test]
1116 fn inline_widget_marker_round_trips() {
1117 let encoded = encode_inline_widget_marker(7, 24.5, 12.0);
1118 assert_eq!(
1119 decode_inline_widget_marker(Some(encoded.as_str())),
1120 Some(InlineWidgetMarker {
1121 id: 7,
1122 width: 24.5,
1123 height: 12.0,
1124 })
1125 );
1126 }
1127}