1use taffy::prelude::*;
10use taffy::geometry::Point;
11
12#[derive(Clone, Copy, Debug)]
14pub struct Rgba {
15 pub r: f32,
16 pub g: f32,
17 pub b: f32,
18 pub a: f32,
19}
20
21impl Rgba {
22 pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
23 Self { r, g, b, a }
24 }
25}
26
27#[derive(Clone, Copy, Debug, Default)]
29pub struct Sides {
30 pub top: f32,
31 pub right: f32,
32 pub bottom: f32,
33 pub left: f32,
34}
35
36impl Sides {
37 pub const fn uniform(v: f32) -> Self {
38 Self {
39 top: v,
40 right: v,
41 bottom: v,
42 left: v,
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq)]
51pub enum Len {
52 Px(f32),
53 Pct(f32),
54 Vw(f32),
55 Vh(f32),
56}
57
58#[derive(Clone, Copy, Debug)]
60pub enum Track {
61 Px(f32),
62 Fr(f32),
63 Auto,
64 MinMax(TrackSide, TrackSide),
68}
69
70#[derive(Clone, Copy, Debug)]
74pub enum TrackSide {
75 Px(f32),
76 Fr(f32),
77 Auto,
78}
79
80#[derive(Clone, Copy, Debug, Default)]
83pub enum Axis {
84 #[default]
85 Row,
86 Column,
87}
88
89#[derive(Clone, Copy, Debug)]
91pub enum Justify {
92 Start,
93 Center,
94 End,
95 SpaceBetween,
96 SpaceAround,
97}
98
99#[derive(Clone, Copy, Debug)]
101pub enum Align {
102 Start,
103 Center,
104 End,
105 Stretch,
106}
107
108#[derive(Clone, Copy, Debug, Default, PartialEq)]
110pub enum TextAlign {
111 #[default]
112 Start,
113 Center,
114 End,
115 Justify,
116}
117
118#[derive(Clone, Copy, Debug, Default, PartialEq)]
121pub enum TextWrap {
122 #[default]
123 Normal,
124 BreakWord,
126 Anywhere,
128}
129
130#[derive(Clone, Copy, Debug, Default, PartialEq)]
133pub enum Display {
134 #[default]
135 Block,
136 Inline,
139 Flex,
140 Grid,
141 None,
143}
144
145#[derive(Clone, Copy, Debug, Default, PartialEq)]
147pub enum Overflow {
148 #[default]
149 Visible,
150 Clip,
152 Scroll,
155}
156
157#[derive(Clone, Copy, Debug, Default, PartialEq)]
161pub enum Cursor {
162 #[default]
163 Default,
164 Pointer,
166}
167
168#[derive(Clone, Copy, Debug, Default, PartialEq)]
172pub enum Position {
173 #[default]
174 Relative,
175 Absolute,
176}
177
178pub type Corners = [f32; 4];
181
182pub type Transform = [f32; 6];
186
187#[derive(Clone, Copy, Debug, Default, PartialEq)]
189pub enum GridFlow {
190 #[default]
191 Row,
192 Column,
193 RowDense,
194 ColumnDense,
195}
196
197#[derive(Clone, Copy, Debug, Default, PartialEq)]
199pub enum GridPlace {
200 #[default]
202 Auto,
203 Line(i16),
205 Span(u16),
207}
208
209#[derive(Clone, Debug)]
211pub enum Background {
212 Color(Rgba),
213 Gradient(Gradient),
214 Image(String),
217}
218
219#[derive(Clone, Debug)]
221pub struct Gradient {
222 pub kind: GradientKind,
223 pub stops: Vec<(Rgba, f32)>,
225}
226
227#[derive(Clone, Copy, Debug)]
228pub enum GradientKind {
229 Linear { angle: f32 },
232 Radial,
234}
235
236#[derive(Clone, Copy, Debug)]
238pub struct BoxShadow {
239 pub dx: f32,
240 pub dy: f32,
241 pub blur: f32,
242 pub spread: f32,
243 pub color: Rgba,
244 pub inset: bool,
246}
247
248#[derive(Clone, Debug)]
250pub struct Style {
251 pub display: Display,
252 pub width: Option<Len>,
253 pub height: Option<Len>,
254 pub min_width: Option<Len>,
255 pub max_width: Option<Len>,
256 pub min_height: Option<Len>,
257 pub max_height: Option<Len>,
258 pub grid_columns: Vec<Track>,
259 pub grid_rows: Vec<Track>,
260 pub grid_column: (GridPlace, GridPlace),
262 pub grid_row: (GridPlace, GridPlace),
263 pub grid_auto_flow: GridFlow,
265 pub grid_auto_rows: Vec<Track>,
266 pub grid_auto_columns: Vec<Track>,
267 pub grow: f32,
268 pub shrink: f32,
272 pub basis: Option<Len>,
274 pub wrap: bool,
276 pub opacity: f32,
278 pub text_wrap: TextWrap,
280 pub padding: Sides,
281 pub margin: Sides,
282 pub border: Sides,
283 pub border_color: Option<Rgba>,
284 pub gap: f32,
285 pub row_gap: Option<f32>,
288 pub column_gap: Option<f32>,
289 pub axis: Axis,
290 pub justify: Option<Justify>,
291 pub align: Option<Align>,
292 pub align_self: Option<Align>,
295 pub justify_self: Option<Align>,
296 pub justify_items: Option<Align>,
298 pub align_content: Option<Justify>,
299 pub overflow: Overflow,
300 pub background: Option<Background>,
301 pub radius: Corners,
303 pub box_shadow: Option<BoxShadow>,
305 pub transform: Option<Transform>,
308 pub cursor: Cursor,
310 pub position: Position,
313 pub inset: [Option<Len>; 4],
314 pub aspect_ratio: Option<f32>,
316}
317
318impl Default for Style {
319 fn default() -> Self {
320 Self {
321 display: Display::Block,
322 width: None,
323 height: None,
324 min_width: None,
325 max_width: None,
326 min_height: None,
327 max_height: None,
328 grid_columns: Vec::new(),
329 grid_rows: Vec::new(),
330 grid_column: (GridPlace::Auto, GridPlace::Auto),
331 grid_row: (GridPlace::Auto, GridPlace::Auto),
332 grid_auto_flow: GridFlow::Row,
333 grid_auto_rows: Vec::new(),
334 grid_auto_columns: Vec::new(),
335 grow: 0.0,
336 shrink: 1.0,
337 basis: None,
338 wrap: false,
339 opacity: 1.0,
340 text_wrap: TextWrap::Normal,
341 padding: Sides::default(),
342 margin: Sides::default(),
343 border: Sides::default(),
344 border_color: None,
345 gap: 0.0,
346 row_gap: None,
347 column_gap: None,
348 axis: Axis::Row,
349 justify: None,
350 align: None,
351 align_self: None,
352 justify_self: None,
353 justify_items: None,
354 align_content: None,
355 overflow: Overflow::Visible,
356 background: None,
357 radius: [0.0; 4],
358 box_shadow: None,
359 transform: None,
360 cursor: Cursor::Default,
361 position: Position::Relative,
362 inset: [None; 4],
363 aspect_ratio: None,
364 }
365 }
366}
367
368#[derive(Clone, Debug)]
372pub struct ImageContent {
373 pub src: String,
374 pub intrinsic: (f32, f32),
375}
376
377#[derive(Clone, Debug)]
379pub struct TextContent {
380 pub text: String,
381 pub font_size: f32,
382 pub weight: u16,
383 pub color: Rgba,
384 pub align: TextAlign,
385 pub wrap: TextWrap,
386 pub font_family: Option<String>,
389 pub letter_spacing: Option<f32>,
391 pub word_spacing: Option<f32>,
392 pub line_height: Option<f32>,
394 pub italic: bool,
396 pub underline: bool,
398 pub strikethrough: bool,
399 pub nowrap: bool,
401 pub caret: Option<usize>,
403 pub selection: Option<(usize, usize)>,
407 pub preedit: Option<(usize, usize)>,
417}
418
419#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
427pub enum AccessRole {
428 #[default]
430 None,
431 Label,
433 Heading,
434 Button,
435 CheckBox,
436 RadioButton,
437 TextInput,
438 MultilineTextInput,
440 ComboBox,
442 Image,
443 ScrollView,
445 Group,
447}
448
449impl AccessRole {
450 pub fn is_meaningful(self) -> bool {
452 self != Self::None
453 }
454}
455
456#[derive(Clone, Debug, Default, PartialEq)]
460pub struct Access {
461 pub role: AccessRole,
462 pub label: Option<String>,
465 pub placeholder: Option<String>,
470 pub value: Option<String>,
472 pub checked: Option<bool>,
474}
475
476impl Access {
477 pub fn name(&self) -> Option<&str> {
479 self.label.as_deref().or(self.placeholder.as_deref())
480 }
481}
482
483#[derive(Clone, Debug)]
486pub struct Node {
487 pub style: Style,
488 pub text: Option<TextContent>,
489 pub image: Option<ImageContent>,
491 pub tick: Option<Rgba>,
495 pub children: Vec<Node>,
496 pub on_tap: Option<String>,
497 pub model: Option<String>,
499 pub multiline: bool,
501 pub options: Option<Vec<String>>,
503 pub hidden: bool,
505 pub id: Option<String>,
507 pub label_for: Option<String>,
511 pub focus_model: Option<String>,
515 pub state_path: Option<Vec<usize>>,
520 pub access: Access,
522}
523
524impl Node {
525 pub fn new(style: Style) -> Self {
526 Self {
527 style,
528 text: None,
529 image: None,
530 tick: None,
531 children: Vec::new(),
532 on_tap: None,
533 model: None,
534 multiline: false,
535 options: None,
536 hidden: false,
537 id: None,
538 label_for: None,
539 focus_model: None,
540 state_path: None,
541 access: Access::default(),
542 }
543 }
544
545 pub fn text(style: Style, text: TextContent) -> Self {
546 Self {
547 style,
548 text: Some(text),
549 image: None,
550 tick: None,
551 children: Vec::new(),
552 on_tap: None,
553 model: None,
554 multiline: false,
555 options: None,
556 hidden: false,
557 id: None,
558 label_for: None,
559 focus_model: None,
560 state_path: None,
561 access: Access::default(),
562 }
563 }
564
565 pub fn image(style: Style, image: ImageContent) -> Self {
566 Self {
567 style,
568 text: None,
569 image: Some(image),
570 tick: None,
571 children: Vec::new(),
572 on_tap: None,
573 model: None,
574 multiline: false,
575 options: None,
576 hidden: false,
577 id: None,
578 label_for: None,
579 focus_model: None,
580 state_path: None,
581 access: Access::default(),
582 }
583 }
584
585 pub fn with(mut self, child: Node) -> Self {
586 self.children.push(child);
587 self
588 }
589}
590
591#[derive(Clone, Debug)]
594pub struct PaintRect {
595 pub x: f32,
596 pub y: f32,
597 pub width: f32,
598 pub height: f32,
599 pub background: Option<Background>,
600 pub radius: Corners,
601 pub border_width: f32,
603 pub border_color: Option<Rgba>,
604}
605
606#[derive(Clone, Debug)]
608pub struct PaintText {
609 pub x: f32,
610 pub y: f32,
611 pub width: f32,
612 pub height: f32,
613 pub content: TextContent,
614}
615
616#[derive(Clone, Copy, Debug)]
618pub struct PaintTick {
619 pub x: f32,
620 pub y: f32,
621 pub width: f32,
622 pub height: f32,
623 pub color: Rgba,
624}
625
626#[derive(Clone, Debug)]
628pub struct PaintImage {
629 pub x: f32,
630 pub y: f32,
631 pub width: f32,
632 pub height: f32,
633 pub content: ImageContent,
634}
635
636#[derive(Clone, Debug)]
638pub enum Paint {
639 Rect(PaintRect),
640 Text(PaintText),
641 Image(PaintImage),
642 Tick(PaintTick),
643 Shadow {
646 x: f32,
647 y: f32,
648 width: f32,
649 height: f32,
650 radius: f32,
651 blur: f32,
652 color: Rgba,
653 },
654 PushClip {
656 x: f32,
657 y: f32,
658 width: f32,
659 height: f32,
660 radius: Corners,
661 },
662 PopClip,
664 PushTransform(Transform),
667 PopTransform,
669 PushOpacity {
672 alpha: f32,
673 width: f32,
674 height: f32,
675 },
676 PopOpacity,
678}
679
680#[derive(Clone, Copy, Debug, Default, PartialEq)]
683pub struct Offset {
684 pub x: f32,
685 pub y: f32,
686}
687
688impl Offset {
689 pub fn clamp_to(self, max: Offset) -> Offset {
690 Offset {
691 x: self.x.clamp(0.0, max.x),
692 y: self.y.clamp(0.0, max.y),
693 }
694 }
695}
696
697#[derive(Clone, Debug)]
700pub struct ScrollRegion {
701 pub id: usize,
702 pub x: f32,
703 pub y: f32,
704 pub width: f32,
705 pub height: f32,
706 pub content_width: f32,
708 pub content_height: f32,
709 pub max: Offset,
711}
712
713impl ScrollRegion {
714 pub fn contains(&self, px: f32, py: f32) -> bool {
715 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
716 }
717
718 pub fn scrollable(&self) -> bool {
720 self.max.x > 0.0 || self.max.y > 0.0
721 }
722}
723
724#[derive(Clone, Debug)]
726pub struct HitRegion {
727 pub x: f32,
728 pub y: f32,
729 pub width: f32,
730 pub height: f32,
731 pub on_tap: String,
732 pub cursor: Cursor,
736}
737
738impl HitRegion {
739 pub fn contains(&self, px: f32, py: f32) -> bool {
740 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
741 }
742}
743
744#[derive(Clone, Debug)]
747pub struct FocusRegion {
748 pub x: f32,
749 pub y: f32,
750 pub width: f32,
751 pub height: f32,
752 pub model: String,
753 pub text: Option<PaintText>,
756 pub multiline: bool,
758 pub scroll_id: Option<usize>,
761}
762
763impl FocusRegion {
764 pub fn contains(&self, px: f32, py: f32) -> bool {
765 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
766 }
767}
768
769#[derive(Clone, Debug)]
772pub struct SelectRegion {
773 pub x: f32,
774 pub y: f32,
775 pub width: f32,
776 pub height: f32,
777 pub model: String,
778 pub options: Vec<String>,
779}
780
781impl SelectRegion {
782 pub fn contains(&self, px: f32, py: f32) -> bool {
783 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
784 }
785}
786
787#[derive(Clone, Debug)]
792pub struct StateRegion {
793 pub x: f32,
794 pub y: f32,
795 pub width: f32,
796 pub height: f32,
797 pub path: Vec<usize>,
800}
801
802impl StateRegion {
803 pub fn contains(&self, px: f32, py: f32) -> bool {
804 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
805 }
806}
807
808#[derive(Clone, Debug)]
816pub struct AccessNode {
817 pub x: f32,
818 pub y: f32,
819 pub width: f32,
820 pub height: f32,
821 pub access: Access,
822 pub model: Option<String>,
825}
826
827#[derive(Clone, Debug)]
830pub struct FocusItem {
831 pub x: f32,
832 pub y: f32,
833 pub width: f32,
834 pub height: f32,
835 pub kind: FocusKind,
836}
837
838impl FocusItem {
839 pub fn contains(&self, px: f32, py: f32) -> bool {
840 px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height
841 }
842}
843
844#[derive(Clone, Debug)]
845pub enum FocusKind {
846 Text { model: String, multiline: bool, text: Option<PaintText> },
848 Activate { on_tap: String },
850 Select { model: String, options: Vec<String> },
852}
853
854#[derive(Clone, Debug, Default)]
857pub struct Layout {
858 pub paints: Vec<Paint>,
859 pub hits: Vec<HitRegion>,
860 pub focuses: Vec<FocusRegion>,
861 pub selects: Vec<SelectRegion>,
862 pub focusables: Vec<FocusItem>,
864 pub scrolls: Vec<ScrollRegion>,
865 pub states: Vec<StateRegion>,
867 pub access: Vec<AccessNode>,
869}
870
871pub type Measure<'a> = dyn FnMut(&TextContent, Option<f32>) -> (f32, f32) + 'a;
877
878enum PaintKind {
880 Box {
881 bg: Option<Background>,
882 radius: Corners,
883 border_width: f32,
884 border_color: Option<Rgba>,
885 clip: bool,
886 shadow: Option<BoxShadow>,
887 },
888 Text(TextContent),
889 Image(ImageContent),
890 Tick(Rgba),
891}
892
893fn to_dim(l: Len, vp: (f32, f32)) -> Dimension {
894 match l {
895 Len::Px(v) => length(v),
896 Len::Pct(p) => percent(p),
897 Len::Vw(v) => length(vp.0 * v / 100.0),
898 Len::Vh(v) => length(vp.1 * v / 100.0),
899 }
900}
901
902fn to_placement(p: GridPlace) -> GridPlacement {
903 match p {
904 GridPlace::Auto => auto(),
905 GridPlace::Line(i) => line(i),
906 GridPlace::Span(n) => span(n),
907 }
908}
909
910fn to_track(t: Track) -> TrackSizingFunction {
911 match t {
912 Track::Px(v) => length(v),
913 Track::Fr(f) => fr(f),
914 Track::Auto => auto(),
915 Track::MinMax(lo, hi) => minmax(
916 match lo {
918 TrackSide::Px(v) => length(v),
919 TrackSide::Fr(_) | TrackSide::Auto => auto(),
920 },
921 match hi {
922 TrackSide::Px(v) => length(v),
923 TrackSide::Fr(f) => fr(f),
924 TrackSide::Auto => auto(),
925 },
926 ),
927 }
928}
929
930fn to_auto_track(t: Track) -> taffy::NonRepeatedTrackSizingFunction {
933 match t {
934 Track::Px(v) => length(v),
935 Track::Fr(f) => fr(f),
936 Track::Auto => auto(),
937 Track::MinMax(lo, hi) => minmax(
938 match lo {
939 TrackSide::Px(v) => length(v),
940 TrackSide::Fr(_) | TrackSide::Auto => auto(),
941 },
942 match hi {
943 TrackSide::Px(v) => length(v),
944 TrackSide::Fr(f) => fr(f),
945 TrackSide::Auto => auto(),
946 },
947 ),
948 }
949}
950
951fn to_taffy(style: &Style, vp: (f32, f32)) -> taffy::Style {
953 taffy::Style {
954 display: match style.display {
955 Display::Block | Display::Inline => taffy::Display::Block,
958 Display::Flex => taffy::Display::Flex,
959 Display::Grid => taffy::Display::Grid,
960 Display::None => taffy::Display::None,
961 },
962 grid_template_columns: style.grid_columns.iter().copied().map(to_track).collect(),
963 grid_template_rows: style.grid_rows.iter().copied().map(to_track).collect(),
964 grid_column: Line {
965 start: to_placement(style.grid_column.0),
966 end: to_placement(style.grid_column.1),
967 },
968 grid_row: Line {
969 start: to_placement(style.grid_row.0),
970 end: to_placement(style.grid_row.1),
971 },
972 grid_auto_flow: match style.grid_auto_flow {
973 GridFlow::Row => taffy::GridAutoFlow::Row,
974 GridFlow::Column => taffy::GridAutoFlow::Column,
975 GridFlow::RowDense => taffy::GridAutoFlow::RowDense,
976 GridFlow::ColumnDense => taffy::GridAutoFlow::ColumnDense,
977 },
978 grid_auto_rows: style.grid_auto_rows.iter().copied().map(to_auto_track).collect(),
979 grid_auto_columns: style.grid_auto_columns.iter().copied().map(to_auto_track).collect(),
980 flex_direction: match style.axis {
981 Axis::Column => FlexDirection::Column,
982 Axis::Row => FlexDirection::Row,
983 },
984 justify_content: style.justify.map(|j| match j {
985 Justify::Start => JustifyContent::FlexStart,
986 Justify::Center => JustifyContent::Center,
987 Justify::End => JustifyContent::FlexEnd,
988 Justify::SpaceBetween => JustifyContent::SpaceBetween,
989 Justify::SpaceAround => JustifyContent::SpaceAround,
990 }),
991 align_items: style
994 .align
995 .map(to_align_items)
996 .or(if style.display == Display::Flex {
997 Some(AlignItems::FlexStart)
998 } else {
999 None
1000 }),
1001 align_self: style.align_self.map(to_align_items),
1002 justify_self: style.justify_self.map(to_align_items),
1003 justify_items: style.justify_items.map(to_align_items),
1004 align_content: style.align_content.map(to_align_content),
1005 position: match style.position {
1006 Position::Relative => taffy::Position::Relative,
1007 Position::Absolute => taffy::Position::Absolute,
1008 },
1009 inset: Rect {
1010 left: to_inset(style.inset[3], vp),
1011 right: to_inset(style.inset[1], vp),
1012 top: to_inset(style.inset[0], vp),
1013 bottom: to_inset(style.inset[2], vp),
1014 },
1015 aspect_ratio: style.aspect_ratio,
1016 overflow: match style.overflow {
1020 Overflow::Scroll => Point {
1021 x: taffy::Overflow::Scroll,
1022 y: taffy::Overflow::Scroll,
1023 },
1024 _ => Point {
1025 x: taffy::Overflow::Visible,
1026 y: taffy::Overflow::Visible,
1027 },
1028 },
1029 flex_grow: style.grow,
1030 flex_shrink: style.shrink,
1031 flex_basis: style.basis.map(|l| to_dim(l, vp)).unwrap_or(auto()),
1032 flex_wrap: if style.wrap {
1033 FlexWrap::Wrap
1034 } else {
1035 FlexWrap::NoWrap
1036 },
1037 size: Size {
1038 width: match style.width {
1049 Some(Len::Pct(_)) if style.wrap && style.max_width.is_some() => auto(),
1050 Some(l) => to_dim(l, vp),
1051 None => auto(),
1052 },
1053 height: style.height.map(|l| to_dim(l, vp)).unwrap_or(auto()),
1054 },
1055 min_size: Size {
1056 width: style.min_width.map(|l| to_dim(l, vp)).unwrap_or(auto()),
1057 height: style.min_height.map(|l| to_dim(l, vp)).unwrap_or(auto()),
1058 },
1059 max_size: Size {
1060 width: match (style.max_width, style.width) {
1066 (Some(l), _) => to_dim(l, vp),
1067 (None, None) if style.shrink != 0.0 => percent(1.0),
1070 (None, _) => auto(),
1071 },
1072 height: style.max_height.map(|l| to_dim(l, vp)).unwrap_or(auto()),
1073 },
1074 padding: Rect {
1075 left: length(style.padding.left),
1076 right: length(style.padding.right),
1077 top: length(style.padding.top),
1078 bottom: length(style.padding.bottom),
1079 },
1080 margin: Rect {
1081 left: length(style.margin.left),
1082 right: length(style.margin.right),
1083 top: length(style.margin.top),
1084 bottom: length(style.margin.bottom),
1085 },
1086 border: Rect {
1087 left: length(style.border.left),
1088 right: length(style.border.right),
1089 top: length(style.border.top),
1090 bottom: length(style.border.bottom),
1091 },
1092 gap: Size {
1095 width: length(style.column_gap.unwrap_or(style.gap)),
1096 height: length(style.row_gap.unwrap_or(style.gap)),
1097 },
1098 ..Default::default()
1099 }
1100}
1101
1102fn to_align_items(a: Align) -> AlignItems {
1103 match a {
1104 Align::Start => AlignItems::FlexStart,
1105 Align::Center => AlignItems::Center,
1106 Align::End => AlignItems::FlexEnd,
1107 Align::Stretch => AlignItems::Stretch,
1108 }
1109}
1110
1111fn to_align_content(j: Justify) -> AlignContent {
1112 match j {
1113 Justify::Start => AlignContent::FlexStart,
1114 Justify::Center => AlignContent::Center,
1115 Justify::End => AlignContent::FlexEnd,
1116 Justify::SpaceBetween => AlignContent::SpaceBetween,
1117 Justify::SpaceAround => AlignContent::SpaceAround,
1118 }
1119}
1120
1121fn to_inset(l: Option<Len>, vp: (f32, f32)) -> LengthPercentageAuto {
1122 match l {
1123 None => auto(),
1124 Some(Len::Px(v)) => length(v),
1125 Some(Len::Pct(p)) => percent(p),
1126 Some(Len::Vw(v)) => length(vp.0 * v / 100.0),
1127 Some(Len::Vh(v)) => length(vp.1 * v / 100.0),
1128 }
1129}
1130
1131struct Bound {
1134 id: NodeId,
1135 model: String,
1136 multiline: bool,
1137 options: Option<Vec<String>>,
1138}
1139
1140#[allow(clippy::too_many_arguments)]
1141fn build(
1142 tree: &mut TaffyTree<TextContent>,
1143 node: &Node,
1144 paint: &mut Vec<(NodeId, PaintKind)>,
1145 handlers: &mut Vec<(NodeId, String, Cursor)>,
1146 models: &mut Vec<Bound>,
1147 focus_labels: &mut Vec<(NodeId, String)>,
1148 hidden: &mut Vec<NodeId>,
1149 opacities: &mut Vec<(NodeId, f32)>,
1150 scrolls: &mut Vec<NodeId>,
1151 transforms: &mut Vec<(NodeId, Transform)>,
1152 states: &mut Vec<(NodeId, Vec<usize>)>,
1153 access: &mut Vec<(NodeId, Access, Option<String>)>,
1154 vp: (f32, f32),
1155) -> NodeId {
1156 let id = if let Some(tc) = &node.text {
1157 let id = tree
1160 .new_leaf_with_context(to_taffy(&node.style, vp), tc.clone())
1161 .expect("taffy text leaf");
1162 paint.push((
1165 id,
1166 PaintKind::Box {
1167 bg: node.style.background.clone(),
1168 radius: node.style.radius,
1169 border_width: node.style.border.top,
1170 border_color: node.style.border_color,
1171 clip: node.style.overflow != Overflow::Visible,
1172 shadow: node.style.box_shadow,
1173 },
1174 ));
1175 paint.push((id, PaintKind::Text(tc.clone())));
1176 id
1177 } else if let Some(color) = node.tick {
1178 let id = tree.new_leaf(to_taffy(&node.style, vp)).expect("taffy tick");
1179 paint.push((id, PaintKind::Tick(color)));
1180 id
1181 } else if let Some(ic) = &node.image {
1182 let mut ts = to_taffy(&node.style, vp);
1185 if node.style.width.is_none() {
1186 ts.size.width = length(ic.intrinsic.0);
1187 }
1188 if node.style.height.is_none() {
1189 ts.size.height = length(ic.intrinsic.1);
1190 }
1191 let id = tree.new_leaf(ts).expect("taffy image leaf");
1192 paint.push((
1193 id,
1194 PaintKind::Box {
1195 bg: node.style.background.clone(),
1196 radius: node.style.radius,
1197 border_width: node.style.border.top,
1198 border_color: node.style.border_color,
1199 clip: node.style.overflow != Overflow::Visible,
1200 shadow: node.style.box_shadow,
1201 },
1202 ));
1203 paint.push((id, PaintKind::Image(ic.clone())));
1204 id
1205 } else {
1206 let children: Vec<NodeId> = node
1207 .children
1208 .iter()
1209 .map(|c| build(tree, c, paint, handlers, models, focus_labels, hidden, opacities, scrolls, transforms, states, access, vp))
1210 .collect();
1211 let id = if children.is_empty() {
1212 tree.new_leaf(to_taffy(&node.style, vp)).expect("taffy leaf")
1213 } else {
1214 tree.new_with_children(to_taffy(&node.style, vp), &children)
1215 .expect("taffy node")
1216 };
1217 paint.push((
1218 id,
1219 PaintKind::Box {
1220 bg: node.style.background.clone(),
1221 radius: node.style.radius,
1222 border_width: node.style.border.top,
1224 border_color: node.style.border_color,
1225 clip: node.style.overflow != Overflow::Visible,
1226 shadow: node.style.box_shadow,
1227 },
1228 ));
1229 id
1230 };
1231 if let Some(handler) = &node.on_tap {
1232 handlers.push((id, handler.clone(), node.style.cursor));
1233 }
1234 if let Some(model) = &node.model {
1235 models.push(Bound {
1236 id,
1237 model: model.clone(),
1238 multiline: node.multiline,
1239 options: node.options.clone(),
1240 });
1241 }
1242 if let Some(fm) = &node.focus_model {
1243 focus_labels.push((id, fm.clone()));
1244 }
1245 if node.hidden {
1246 hidden.push(id);
1247 }
1248 if node.style.opacity < 1.0 {
1249 opacities.push((id, node.style.opacity.max(0.0)));
1250 }
1251 if let Some(tf) = node.style.transform {
1252 transforms.push((id, tf));
1253 }
1254 if node.style.overflow == Overflow::Scroll {
1255 scrolls.push(id);
1256 }
1257 if let Some(path) = &node.state_path {
1258 states.push((id, path.clone()));
1259 }
1260 if node.access.role.is_meaningful() {
1261 access.push((id, node.access.clone(), node.model.clone()));
1262 }
1263 id
1264}
1265
1266#[allow(clippy::too_many_arguments)]
1267fn collect(
1268 tree: &TaffyTree<TextContent>,
1269 id: NodeId,
1270 origin_x: f32,
1271 origin_y: f32,
1272 paint: &[(NodeId, PaintKind)],
1273 handlers: &[(NodeId, String, Cursor)],
1274 models: &[Bound],
1275 focus_labels: &[(NodeId, String)],
1276 hidden: &[NodeId],
1277 opacities: &[(NodeId, f32)],
1278 scrolls: &[NodeId],
1279 transforms: &[(NodeId, Transform)],
1280 states: &[(NodeId, Vec<usize>)],
1281 access: &[(NodeId, Access, Option<String>)],
1282 offsets: &[Offset],
1283 vp: (f32, f32),
1284 out: &mut Layout,
1285) {
1286 let layout = tree.layout(id).expect("layout");
1287 let x = origin_x + layout.location.x;
1288 let y = origin_y + layout.location.y;
1289
1290 if hidden.contains(&id) {
1293 return;
1294 }
1295
1296 let alpha = opacities
1299 .iter()
1300 .find(|(nid, _)| *nid == id)
1301 .map(|(_, a)| *a)
1302 .unwrap_or(1.0);
1303 if alpha < 1.0 {
1304 out.paints.push(Paint::PushOpacity {
1305 alpha,
1306 width: vp.0,
1307 height: vp.1,
1308 });
1309 }
1310
1311 let transform = transforms.iter().find(|(nid, _)| *nid == id).map(|(_, m)| *m);
1315 if let Some(m) = transform {
1316 let (ox, oy) = (x + layout.size.width / 2.0, y + layout.size.height / 2.0);
1317 out.paints.push(Paint::PushTransform(centre_transform(m, ox, oy)));
1318 }
1319
1320 let mut clip = false;
1321 let mut clip_radius = [0.0; 4];
1322 for (_, kind) in paint.iter().filter(|(nid, _)| *nid == id) {
1325 match kind {
1326 PaintKind::Box {
1327 bg,
1328 radius,
1329 border_width,
1330 border_color,
1331 clip: c,
1332 shadow,
1333 } => {
1334 clip = *c;
1335 clip_radius = *radius;
1336 if let Some(sh) = shadow.filter(|s| !s.inset) {
1339 out.paints.push(Paint::Shadow {
1340 x: x + sh.dx - sh.spread,
1341 y: y + sh.dy - sh.spread,
1342 width: layout.size.width + 2.0 * sh.spread,
1343 height: layout.size.height + 2.0 * sh.spread,
1344 radius: radius.iter().copied().fold(0.0, f32::max),
1347 blur: sh.blur,
1348 color: sh.color,
1349 });
1350 }
1351 let has_border = *border_width > 0.0 && border_color.is_some();
1352 if bg.is_some() || has_border {
1353 out.paints.push(Paint::Rect(PaintRect {
1354 x,
1355 y,
1356 width: layout.size.width,
1357 height: layout.size.height,
1358 background: bg.clone(),
1359 radius: *radius,
1360 border_width: *border_width,
1361 border_color: *border_color,
1362 }));
1363 }
1364 }
1365 PaintKind::Text(tc) => out.paints.push(Paint::Text(PaintText {
1366 x,
1367 y,
1368 width: layout.size.width,
1369 height: layout.size.height,
1370 content: tc.clone(),
1371 })),
1372 PaintKind::Tick(color) => out.paints.push(Paint::Tick(PaintTick {
1373 x,
1374 y,
1375 width: layout.size.width,
1376 height: layout.size.height,
1377 color: *color,
1378 })),
1379 PaintKind::Image(ic) => out.paints.push(Paint::Image(PaintImage {
1380 x,
1381 y,
1382 width: layout.size.width,
1383 height: layout.size.height,
1384 content: ic.clone(),
1385 })),
1386 }
1387 }
1388
1389 if let Some((_, model)) = focus_labels.iter().find(|(nid, _)| *nid == id) {
1392 out.focuses.push(FocusRegion {
1393 x,
1394 y,
1395 width: layout.size.width,
1396 height: layout.size.height,
1397 model: model.clone(),
1398 text: None,
1399 multiline: false,
1400 scroll_id: None,
1401 });
1402 }
1403
1404 if let Some((_, node_access, model)) = access.iter().find(|(nid, ..)| *nid == id) {
1408 out.access.push(AccessNode {
1409 x,
1410 y,
1411 width: layout.size.width,
1412 height: layout.size.height,
1413 access: node_access.clone(),
1414 model: model.clone(),
1415 });
1416 }
1417
1418 if let Some((_, path)) = states.iter().find(|(nid, _)| *nid == id) {
1421 out.states.push(StateRegion {
1422 x,
1423 y,
1424 width: layout.size.width,
1425 height: layout.size.height,
1426 path: path.clone(),
1427 });
1428 }
1429
1430 if let Some((_, handler, cursor)) = handlers.iter().find(|(nid, ..)| *nid == id) {
1431 out.hits.push(HitRegion {
1432 x,
1433 y,
1434 width: layout.size.width,
1435 height: layout.size.height,
1436 on_tap: handler.clone(),
1437 cursor: *cursor,
1438 });
1439 }
1440
1441 let (fw, fh) = (layout.size.width, layout.size.height);
1442 if let Some(bound) = models.iter().find(|b| b.id == id) {
1443 if let Some(options) = &bound.options {
1444 out.selects.push(SelectRegion {
1446 x,
1447 y,
1448 width: fw,
1449 height: fh,
1450 model: bound.model.clone(),
1451 options: options.clone(),
1452 });
1453 out.focusables.push(FocusItem {
1454 x,
1455 y,
1456 width: fw,
1457 height: fh,
1458 kind: FocusKind::Select { model: bound.model.clone(), options: options.clone() },
1459 });
1460 } else {
1461 let text = tree
1464 .children(id)
1465 .ok()
1466 .and_then(|kids| kids.first().copied())
1467 .and_then(|kid| {
1468 let child = tree.layout(kid).ok()?;
1469 let content = paint.iter().find_map(|(nid, k)| match k {
1470 PaintKind::Text(tc) if *nid == kid => Some(tc.clone()),
1471 _ => None,
1472 })?;
1473 Some(PaintText {
1474 x: x + child.location.x,
1475 y: y + child.location.y,
1476 width: child.size.width,
1477 height: child.size.height,
1478 content,
1479 })
1480 });
1481 out.focuses.push(FocusRegion {
1482 x,
1483 y,
1484 width: fw,
1485 height: fh,
1486 model: bound.model.clone(),
1487 text: text.clone(),
1488 multiline: bound.multiline,
1489 scroll_id: scrolls.contains(&id).then(|| out.scrolls.len()),
1492 });
1493 out.focusables.push(FocusItem {
1494 x,
1495 y,
1496 width: fw,
1497 height: fh,
1498 kind: FocusKind::Text { model: bound.model.clone(), multiline: bound.multiline, text },
1499 });
1500 }
1501 } else if let Some((_, handler, _)) = handlers.iter().find(|(nid, ..)| *nid == id) {
1502 out.focusables.push(FocusItem {
1505 x,
1506 y,
1507 width: fw,
1508 height: fh,
1509 kind: FocusKind::Activate { on_tap: handler.clone() },
1510 });
1511 }
1512
1513 if clip {
1515 out.paints.push(Paint::PushClip {
1516 x,
1517 y,
1518 width: layout.size.width,
1519 height: layout.size.height,
1520 radius: clip_radius,
1521 });
1522 }
1523
1524 let mut shift = Offset::default();
1527 if scrolls.contains(&id) {
1528 let sid = out.scrolls.len();
1529 let max = Offset {
1530 x: (layout.content_size.width - layout.size.width).max(0.0),
1531 y: (layout.content_size.height - layout.size.height).max(0.0),
1532 };
1533 shift = offsets.get(sid).copied().unwrap_or_default().clamp_to(max);
1534 out.scrolls.push(ScrollRegion {
1535 id: sid,
1536 x,
1537 y,
1538 width: layout.size.width,
1539 height: layout.size.height,
1540 content_width: layout.content_size.width,
1541 content_height: layout.content_size.height,
1542 max,
1543 });
1544 }
1545
1546 for child in tree.children(id).expect("children") {
1547 collect(
1548 tree,
1549 child,
1550 x - shift.x,
1551 y - shift.y,
1552 paint,
1553 handlers,
1554 models,
1555 focus_labels,
1556 hidden,
1557 opacities,
1558 scrolls,
1559 transforms,
1560 states,
1561 access,
1562 offsets,
1563 vp,
1564 out,
1565 );
1566 }
1567 if clip {
1568 out.paints.push(Paint::PopClip);
1569 }
1570 if transform.is_some() {
1571 out.paints.push(Paint::PopTransform);
1572 }
1573 if alpha < 1.0 {
1574 out.paints.push(Paint::PopOpacity);
1575 }
1576}
1577
1578fn centre_transform(m: Transform, ox: f32, oy: f32) -> Transform {
1581 let [a, b, c, d, e, f] = m;
1582 [
1583 a,
1584 b,
1585 c,
1586 d,
1587 e + ox - a * ox - c * oy,
1588 f + oy - b * ox - d * oy,
1589 ]
1590}
1591
1592pub fn layout(root: &Node, avail_w: f32, avail_h: f32, measure: &mut Measure) -> Layout {
1595 layout_scrolled(root, avail_w, avail_h, &[], measure)
1596}
1597
1598pub fn layout_scrolled(
1601 root: &Node,
1602 avail_w: f32,
1603 avail_h: f32,
1604 offsets: &[Offset],
1605 measure: &mut Measure,
1606) -> Layout {
1607 let mut tree: TaffyTree<TextContent> = TaffyTree::new();
1608 tree.disable_rounding();
1612 let mut paint = Vec::new();
1613 let mut handlers = Vec::new();
1614 let mut models = Vec::new();
1615 let mut focus_labels = Vec::new();
1616 let mut hidden = Vec::new();
1617 let mut opacities = Vec::new();
1618 let mut scrolls = Vec::new();
1619 let mut transforms = Vec::new();
1620 let mut states = Vec::new();
1621 let mut access = Vec::new();
1622 let vp = (avail_w, avail_h);
1623 let root_id = build(
1624 &mut tree,
1625 root,
1626 &mut paint,
1627 &mut handlers,
1628 &mut models,
1629 &mut focus_labels,
1630 &mut hidden,
1631 &mut opacities,
1632 &mut scrolls,
1633 &mut transforms,
1634 &mut states,
1635 &mut access,
1636 vp,
1637 );
1638
1639 let mut root_style = to_taffy(&root.style, vp);
1641 root_style.size = Size {
1642 width: length(avail_w),
1643 height: length(avail_h),
1644 };
1645 tree.set_style(root_id, root_style).expect("set root style");
1646
1647 tree.compute_layout_with_measure(
1648 root_id,
1649 Size {
1650 width: AvailableSpace::Definite(avail_w),
1651 height: AvailableSpace::Definite(avail_h),
1652 },
1653 |known, available, _id, ctx, _style| {
1654 if let (Some(w), Some(h)) = (known.width, known.height) {
1655 return Size { width: w, height: h };
1656 }
1657 match ctx {
1658 Some(tc) => {
1659 let max = known.width.or(match available.width {
1662 AvailableSpace::Definite(w) => Some(w),
1663 _ => None,
1664 });
1665 let (w, h) = measure(tc, max);
1666 Size {
1667 width: known.width.unwrap_or(w),
1668 height: known.height.unwrap_or(h),
1669 }
1670 }
1671 None => Size {
1672 width: 0.0,
1673 height: 0.0,
1674 },
1675 }
1676 },
1677 )
1678 .expect("compute layout");
1679
1680 let mut out = Layout::default();
1681 collect(
1682 &tree, root_id, 0.0, 0.0, &paint, &handlers, &models, &focus_labels, &hidden, &opacities,
1683 &scrolls, &transforms, &states, &access, offsets, vp, &mut out,
1684 );
1685 out
1686}