1use alloc::boxed::Box;
34use alloc::vec;
35use alloc::vec::Vec;
36
37use crate::invalidation::InvalidationList;
38use crate::widget::Rect;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62#[non_exhaustive]
63pub enum Dimension {
64 Px(i32),
66 Pct(u16),
68 Content,
70}
71
72impl Default for Dimension {
73 fn default() -> Self {
76 Dimension::Px(0)
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
93#[non_exhaustive]
94pub enum FlexFlow {
95 #[default]
97 Row,
98 Column,
100 RowWrap,
102 RowReverse,
104 RowWrapReverse,
106 ColumnWrap,
108 ColumnReverse,
110 ColumnWrapReverse,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126#[non_exhaustive]
127pub enum FlexAlign {
128 #[default]
130 Start,
131 End,
133 Center,
135 SpaceEvenly,
137 SpaceAround,
140 SpaceBetween,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct FlexConfig {
156 pub flow: FlexFlow,
158 pub main_align: FlexAlign,
160 pub cross_align: FlexAlign,
162 pub track_cross_align: FlexAlign,
164 pub gap_main: i32,
166 pub gap_cross: i32,
168}
169
170impl Default for FlexConfig {
171 fn default() -> Self {
172 Self {
173 flow: FlexFlow::Row,
174 main_align: FlexAlign::Start,
175 cross_align: FlexAlign::Start,
176 track_cross_align: FlexAlign::Start,
177 gap_main: 0,
178 gap_cross: 0,
179 }
180 }
181}
182
183#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194#[non_exhaustive]
195pub enum GridTrack {
196 Px(i32),
198 Fr(u8),
203 Content,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
218#[non_exhaustive]
219pub enum GridAlign {
220 Start,
222 Center,
224 End,
226 #[default]
228 Stretch,
229 SpaceEvenly,
231 SpaceAround,
233 SpaceBetween,
235}
236
237#[derive(Debug, Clone, PartialEq, Eq)]
251pub struct GridConfig {
252 pub col_tracks: Vec<GridTrack>,
254 pub row_tracks: Vec<GridTrack>,
256 pub col_gap: i32,
258 pub row_gap: i32,
260 pub col_align: GridAlign,
262 pub row_align: GridAlign,
264}
265
266impl Default for GridConfig {
267 fn default() -> Self {
268 Self {
269 col_tracks: Vec::new(),
270 row_tracks: Vec::new(),
271 col_gap: 0,
272 row_gap: 0,
273 col_align: GridAlign::Stretch,
274 row_align: GridAlign::Stretch,
275 }
276 }
277}
278
279#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct ItemHints {
290 pub width: Dimension,
294 pub height: Dimension,
298
299 pub flex_grow: u8,
310 pub self_align: Option<FlexAlign>,
314
315 pub col_pos: u16,
318 pub col_span: u16,
320 pub row_pos: u16,
322 pub row_span: u16,
324 pub col_align: GridAlign,
326 pub row_align: GridAlign,
328
329 pub min_width: Option<i32>,
332 pub max_width: Option<i32>,
334 pub min_height: Option<i32>,
336 pub max_height: Option<i32>,
338}
339
340impl Default for ItemHints {
341 fn default() -> Self {
342 Self {
343 width: Dimension::Px(0),
344 height: Dimension::Px(0),
345 flex_grow: 0,
346 self_align: None,
347 col_pos: 0,
348 col_span: 1,
349 row_pos: 0,
350 row_span: 1,
351 col_align: GridAlign::Stretch,
352 row_align: GridAlign::Stretch,
353 min_width: None,
354 max_width: None,
355 min_height: None,
356 max_height: None,
357 }
358 }
359}
360
361#[derive(Debug, Clone, PartialEq, Eq)]
367pub enum EngineConfig {
368 Flex(FlexConfig),
370 Grid(GridConfig),
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Default)]
385pub enum LayoutRole {
386 #[default]
389 None,
390 Container(EngineConfig),
392 Item(ItemHints),
394}
395
396#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct LayoutState {
410 pub role: LayoutRole,
412 pub computed: Option<Rect>,
416 pub layout_dirty: bool,
419}
420
421impl Default for LayoutState {
422 fn default() -> Self {
423 Self {
424 role: LayoutRole::None,
425 computed: None,
426 layout_dirty: true,
427 }
428 }
429}
430
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
450pub struct LayoutStyle {
451 pub padding_top: i32,
453 pub padding_bottom: i32,
455 pub padding_left: i32,
457 pub padding_right: i32,
459 pub margin_top: i32,
461 pub margin_bottom: i32,
463 pub margin_left: i32,
465 pub margin_right: i32,
467 pub gap_row: i32,
469 pub gap_col: i32,
471}
472
473pub fn resolve_layout_style(
482 style_state: Option<&crate::style_cascade::StyleState>,
483 part: crate::style_cascade::Part,
484 states: crate::object::ObjectStates,
485) -> LayoutStyle {
486 let Some(ss) = style_state else {
487 return LayoutStyle::default();
488 };
489
490 let mut out = LayoutStyle::default();
493
494 macro_rules! apply_patch {
496 ($patch:expr) => {{
497 let p: &crate::style_cascade::StylePatch = $patch;
498 if out.padding_top == 0 {
499 if let Some(v) = p.padding_top {
500 out.padding_top = v;
501 }
502 }
503 if out.padding_bottom == 0 {
504 if let Some(v) = p.padding_bottom {
505 out.padding_bottom = v;
506 }
507 }
508 if out.padding_left == 0 {
509 if let Some(v) = p.padding_left {
510 out.padding_left = v;
511 }
512 }
513 if out.padding_right == 0 {
514 if let Some(v) = p.padding_right {
515 out.padding_right = v;
516 }
517 }
518 if out.margin_top == 0 {
519 if let Some(v) = p.margin_top {
520 out.margin_top = v;
521 }
522 }
523 if out.margin_bottom == 0 {
524 if let Some(v) = p.margin_bottom {
525 out.margin_bottom = v;
526 }
527 }
528 if out.margin_left == 0 {
529 if let Some(v) = p.margin_left {
530 out.margin_left = v;
531 }
532 }
533 if out.margin_right == 0 {
534 if let Some(v) = p.margin_right {
535 out.margin_right = v;
536 }
537 }
538 if out.gap_row == 0 {
539 if let Some(v) = p.gap_row {
540 out.gap_row = v;
541 }
542 }
543 if out.gap_col == 0 {
544 if let Some(v) = p.gap_col {
545 out.gap_col = v;
546 }
547 }
548 }};
549 }
550
551 for (selector, patch) in ss.local_entries().iter().rev() {
554 if selector.matches(part, states) {
555 apply_patch!(patch);
556 }
557 }
558
559 for (selector, patch) in ss.added_entries().iter().rev() {
562 if selector.matches(part, states) {
563 apply_patch!(*patch);
564 }
565 }
566
567 out
568}
569
570fn resolve_dimension(
583 dim: Dimension,
584 parent_content: i32,
585 content_size: i32,
586 min: Option<i32>,
587 max: Option<i32>,
588) -> i32 {
589 let raw = match dim {
590 Dimension::Px(n) => n,
591 Dimension::Pct(p) => (parent_content * p as i32) / 100,
592 Dimension::Content => content_size,
593 };
594 clamp_size(raw, min, max)
595}
596
597fn clamp_size(size: i32, min: Option<i32>, max: Option<i32>) -> i32 {
599 let mut s = size;
600 if let Some(lo) = min {
601 s = s.max(lo);
602 }
603 if let Some(hi) = max {
604 s = s.min(hi);
605 }
606 s
607}
608
609pub fn run_layout<const N: usize>(
633 root: &mut crate::object::ObjectNode,
634 planner: &mut InvalidationList<N>,
635) {
636 let root_bounds = root.effective_bounds();
639 run_layout_node(root, root_bounds, planner);
640}
641
642fn run_layout_node<const N: usize>(
647 node: &mut crate::object::ObjectNode,
648 _parent_bounds: Rect,
649 planner: &mut InvalidationList<N>,
650) {
651 let is_dirty_container = node
653 .layout
654 .as_ref()
655 .map(|ls| ls.layout_dirty && matches!(ls.role, LayoutRole::Container(_)))
656 .unwrap_or(false);
657
658 if is_dirty_container {
659 let engine = match node.layout.as_ref().unwrap().role.clone() {
661 LayoutRole::Container(cfg) => cfg,
662 _ => unreachable!(),
663 };
664
665 let layout_style = resolve_layout_style(
667 node.style.as_deref(),
668 crate::style_cascade::Part::MAIN,
669 node.meta().states(),
670 );
671
672 let container_bounds = node.effective_bounds();
674
675 let content_x = container_bounds.x + layout_style.padding_left;
677 let content_y = container_bounds.y + layout_style.padding_top;
678 let content_w =
679 (container_bounds.width - layout_style.padding_left - layout_style.padding_right)
680 .max(0);
681 let content_h =
682 (container_bounds.height - layout_style.padding_top - layout_style.padding_bottom)
683 .max(0);
684 let content_area = Rect {
685 x: content_x,
686 y: content_y,
687 width: content_w,
688 height: content_h,
689 };
690
691 match engine {
692 EngineConfig::Flex(cfg) => {
693 run_flex(node, content_area, &cfg, &layout_style, planner);
694 }
695 EngineConfig::Grid(cfg) => {
696 run_grid(node, content_area, &cfg, &layout_style, planner);
697 }
698 }
699
700 if let Some(ls) = node.layout.as_deref_mut() {
702 ls.layout_dirty = false;
703 }
704
705 use crate::object::ObjectEvent;
707 node.invoke_handlers_for(&ObjectEvent::LayoutChanged);
708 } else {
709 let children_count = node.children().len();
712 for i in 0..children_count {
713 let child_bounds = node.children()[i].effective_bounds();
714 run_layout_node(&mut node.children_mut()[i], child_bounds, planner);
716 }
717 }
718}
719
720fn run_flex<const N: usize>(
726 container: &mut crate::object::ObjectNode,
727 content_area: Rect,
728 cfg: &FlexConfig,
729 layout_style: &LayoutStyle,
730 planner: &mut InvalidationList<N>,
731) {
732 use FlexFlow::*;
733
734 let is_row_flow = matches!(cfg.flow, Row | RowWrap | RowReverse | RowWrapReverse);
735 let is_reverse = matches!(
736 cfg.flow,
737 RowReverse | RowWrapReverse | ColumnReverse | ColumnWrapReverse
738 );
739 let _is_wrap = matches!(
740 cfg.flow,
741 RowWrap | RowWrapReverse | ColumnWrap | ColumnWrapReverse
742 );
743
744 let gap_main = if layout_style.gap_col != 0 && is_row_flow {
746 layout_style.gap_col
747 } else if layout_style.gap_row != 0 && !is_row_flow {
748 layout_style.gap_row
749 } else {
750 cfg.gap_main
751 };
752 let _gap_cross = if layout_style.gap_row != 0 && is_row_flow {
753 layout_style.gap_row
754 } else if layout_style.gap_col != 0 && !is_row_flow {
755 layout_style.gap_col
756 } else {
757 cfg.gap_cross
758 };
759
760 let n = container.children().len();
762 let mut sizes: Vec<(i32, i32)> = Vec::with_capacity(n); let mut grows: Vec<u8> = Vec::with_capacity(n);
764 let mut self_aligns: Vec<Option<FlexAlign>> = Vec::with_capacity(n);
765
766 for i in 0..n {
767 let child = &container.children()[i];
768 let (hints, intrinsic) = child_hints_and_bounds(child);
769
770 let main_dim = if is_row_flow {
771 hints.width
772 } else {
773 hints.height
774 };
775 let cross_dim = if is_row_flow {
776 hints.height
777 } else {
778 hints.width
779 };
780
781 let parent_main = if is_row_flow {
782 content_area.width
783 } else {
784 content_area.height
785 };
786 let parent_cross = if is_row_flow {
787 content_area.height
788 } else {
789 content_area.width
790 };
791
792 let intrinsic_main = if is_row_flow {
793 intrinsic.width
794 } else {
795 intrinsic.height
796 };
797 let intrinsic_cross = if is_row_flow {
798 intrinsic.height
799 } else {
800 intrinsic.width
801 };
802
803 let main = resolve_main_dim(main_dim, parent_main, intrinsic_main, &hints);
804 let cross = resolve_cross_dim(cross_dim, parent_cross, intrinsic_cross, &hints);
805
806 sizes.push((main, cross));
807 grows.push(hints.flex_grow);
808 self_aligns.push(hints.self_align);
809 }
810
811 let gaps_total = if n > 1 { gap_main * (n as i32 - 1) } else { 0 };
813 let fixed_main: i32 = sizes.iter().map(|&(m, _)| m).sum();
814 let parent_main = if is_row_flow {
815 content_area.width
816 } else {
817 content_area.height
818 };
819 let free = (parent_main - fixed_main - gaps_total).max(0);
820
821 let total_grow: u32 = grows.iter().map(|&g| g as u32).sum();
823 if total_grow > 0 && free > 0 {
824 let mut distributed = 0i32;
825 for i in 0..n {
826 if grows[i] > 0 {
827 let share = free * grows[i] as i32 / total_grow as i32;
828 sizes[i].0 += share;
829 distributed += share;
830 }
831 }
832 let remainder = free - distributed;
834 if remainder > 0
835 && let Some(last_grow_idx) = grows.iter().rposition(|&g| g > 0)
836 {
837 sizes[last_grow_idx].0 += remainder;
838 }
839 }
840
841 let total_main_used: i32 = sizes.iter().map(|&(m, _)| m).sum::<i32>() + gaps_total;
843 let start_offset = main_align_offset(
844 cfg.main_align,
845 parent_main,
846 total_main_used,
847 n as i32,
848 gap_main,
849 );
850
851 let parent_cross = if is_row_flow {
853 content_area.height
854 } else {
855 content_area.width
856 };
857 let indices: Vec<usize> = if is_reverse {
858 (0..n).rev().collect()
859 } else {
860 (0..n).collect()
861 };
862
863 let mut main_cursor = start_offset;
864 for (seq, &child_idx) in indices.iter().enumerate() {
865 let (item_main, item_cross_intrinsic) = sizes[child_idx];
866
867 let effective_cross_align = self_aligns[child_idx].unwrap_or(cfg.cross_align);
869 let item_cross = match effective_cross_align {
870 FlexAlign::Start
871 | FlexAlign::End
872 | FlexAlign::Center
873 | FlexAlign::SpaceEvenly
874 | FlexAlign::SpaceAround
875 | FlexAlign::SpaceBetween => item_cross_intrinsic,
876 };
877 let cross_offset = cross_align_offset(effective_cross_align, parent_cross, item_cross);
878
879 let (x, y, w, h) = if is_row_flow {
880 (
881 content_area.x + main_cursor,
882 content_area.y + cross_offset,
883 item_main,
884 item_cross,
885 )
886 } else {
887 (
888 content_area.x + cross_offset,
889 content_area.y + main_cursor,
890 item_cross,
891 item_main,
892 )
893 };
894
895 let new_rect = Rect {
896 x,
897 y,
898 width: w,
899 height: h,
900 };
901 write_computed(&mut container.children_mut()[child_idx], new_rect, planner);
902
903 main_cursor += item_main;
905 if seq < n.saturating_sub(1) {
906 main_cursor += gap_main;
907 }
908 }
909}
910
911fn main_align_offset(
913 align: FlexAlign,
914 parent_size: i32,
915 total_used: i32,
916 item_count: i32,
917 gap: i32,
918) -> i32 {
919 let free = (parent_size - total_used).max(0);
920 match align {
921 FlexAlign::Start => 0,
922 FlexAlign::End => free,
923 FlexAlign::Center => free / 2,
924 FlexAlign::SpaceEvenly => {
925 if item_count > 0 {
926 free / (item_count + 1)
927 } else {
928 0
929 }
930 }
931 FlexAlign::SpaceAround => {
932 if item_count > 0 {
933 free / (2 * item_count)
934 } else {
935 0
936 }
937 }
938 FlexAlign::SpaceBetween => {
939 let _ = gap; 0
944 }
945 }
946}
947
948fn cross_align_offset(align: FlexAlign, parent_cross: i32, item_cross: i32) -> i32 {
950 let free = (parent_cross - item_cross).max(0);
951 match align {
952 FlexAlign::Start
953 | FlexAlign::SpaceEvenly
954 | FlexAlign::SpaceAround
955 | FlexAlign::SpaceBetween => 0,
956 FlexAlign::End => free,
957 FlexAlign::Center => free / 2,
958 }
959}
960
961fn resolve_main_dim(dim: Dimension, parent_main: i32, intrinsic: i32, hints: &ItemHints) -> i32 {
963 let (min, max) = intrinsic_min_max_for_dim(dim, hints, true);
964 match dim {
965 Dimension::Px(0) => clamp_size(intrinsic, min, max),
966 _ => resolve_dimension(dim, parent_main, intrinsic, min, max),
967 }
968}
969
970fn resolve_cross_dim(dim: Dimension, parent_cross: i32, intrinsic: i32, hints: &ItemHints) -> i32 {
972 let (min, max) = intrinsic_min_max_for_dim(dim, hints, false);
973 match dim {
974 Dimension::Px(0) => clamp_size(intrinsic, min, max),
975 _ => resolve_dimension(dim, parent_cross, intrinsic, min, max),
976 }
977}
978
979fn intrinsic_min_max_for_dim(
981 _dim: Dimension,
982 hints: &ItemHints,
983 is_width: bool,
984) -> (Option<i32>, Option<i32>) {
985 if is_width {
986 (hints.min_width, hints.max_width)
987 } else {
988 (hints.min_height, hints.max_height)
989 }
990}
991
992fn run_grid<const N: usize>(
998 container: &mut crate::object::ObjectNode,
999 content_area: Rect,
1000 cfg: &GridConfig,
1001 layout_style: &LayoutStyle,
1002 planner: &mut InvalidationList<N>,
1003) {
1004 let n_cols = cfg.col_tracks.len();
1005 let n_rows = cfg.row_tracks.len();
1006
1007 if n_cols == 0 || n_rows == 0 {
1008 return;
1009 }
1010
1011 let col_gap = if layout_style.gap_col != 0 {
1013 layout_style.gap_col
1014 } else {
1015 cfg.col_gap
1016 };
1017 let row_gap = if layout_style.gap_row != 0 {
1018 layout_style.gap_row
1019 } else {
1020 cfg.row_gap
1021 };
1022
1023 let mut col_sizes: Vec<i32> = vec![0i32; n_cols];
1025 let mut row_sizes: Vec<i32> = vec![0i32; n_rows];
1026
1027 for (i, track) in cfg.col_tracks.iter().enumerate() {
1028 if let GridTrack::Px(px) = track {
1029 col_sizes[i] = (*px).max(0);
1030 }
1031 }
1032 for (i, track) in cfg.row_tracks.iter().enumerate() {
1033 if let GridTrack::Px(px) = track {
1034 row_sizes[i] = (*px).max(0);
1035 }
1036 }
1037
1038 let n_children = container.children().len();
1041 for (track_idx, col_size) in col_sizes.iter_mut().enumerate() {
1042 if !matches!(cfg.col_tracks[track_idx], GridTrack::Content) {
1043 continue;
1044 }
1045 let mut max_w = 0i32;
1046 for ci in 0..n_children {
1047 let child = &container.children()[ci];
1048 let (hints, intrinsic) = child_hints_and_bounds(child);
1049 let col = hints.col_pos as usize;
1050 let span = hints.col_span.max(1) as usize;
1051 if col <= track_idx && track_idx < col + span {
1052 let w = resolve_main_dim(hints.width, content_area.width, intrinsic.width, &hints);
1053 if w > max_w {
1054 max_w = w;
1055 }
1056 }
1057 }
1058 *col_size = max_w;
1059 }
1060 for (track_idx, row_size) in row_sizes.iter_mut().enumerate() {
1061 if !matches!(cfg.row_tracks[track_idx], GridTrack::Content) {
1062 continue;
1063 }
1064 let mut max_h = 0i32;
1065 for ci in 0..n_children {
1066 let child = &container.children()[ci];
1067 let (hints, intrinsic) = child_hints_and_bounds(child);
1068 let row = hints.row_pos as usize;
1069 let span = hints.row_span.max(1) as usize;
1070 if row <= track_idx && track_idx < row + span {
1071 let h =
1072 resolve_cross_dim(hints.height, content_area.height, intrinsic.height, &hints);
1073 if h > max_h {
1074 max_h = h;
1075 }
1076 }
1077 }
1078 *row_size = max_h;
1079 }
1080
1081 let gaps_col_total = if n_cols > 1 {
1083 col_gap * (n_cols as i32 - 1)
1084 } else {
1085 0
1086 };
1087 let gaps_row_total = if n_rows > 1 {
1088 row_gap * (n_rows as i32 - 1)
1089 } else {
1090 0
1091 };
1092
1093 let fixed_col_total: i32 = cfg
1094 .col_tracks
1095 .iter()
1096 .zip(&col_sizes)
1097 .filter(|(t, _)| !matches!(t, GridTrack::Fr(_)))
1098 .map(|(_, &s)| s)
1099 .sum();
1100 let fixed_row_total: i32 = cfg
1101 .row_tracks
1102 .iter()
1103 .zip(&row_sizes)
1104 .filter(|(t, _)| !matches!(t, GridTrack::Fr(_)))
1105 .map(|(_, &s)| s)
1106 .sum();
1107
1108 let free_col = (content_area.width - fixed_col_total - gaps_col_total).max(0);
1109 let free_row = (content_area.height - fixed_row_total - gaps_row_total).max(0);
1110
1111 let total_col_fr: u32 = cfg
1112 .col_tracks
1113 .iter()
1114 .filter_map(|t| {
1115 if let GridTrack::Fr(f) = t {
1116 Some(*f as u32)
1117 } else {
1118 None
1119 }
1120 })
1121 .sum();
1122 let total_row_fr: u32 = cfg
1123 .row_tracks
1124 .iter()
1125 .filter_map(|t| {
1126 if let GridTrack::Fr(f) = t {
1127 Some(*f as u32)
1128 } else {
1129 None
1130 }
1131 })
1132 .sum();
1133
1134 if total_col_fr > 0 {
1135 let mut distributed = 0i32;
1136 let mut last_fr_idx = None;
1137 for (i, track) in cfg.col_tracks.iter().enumerate() {
1138 if let GridTrack::Fr(f) = track {
1139 let share = free_col * (*f as i32) / total_col_fr as i32;
1140 col_sizes[i] = share;
1141 distributed += share;
1142 last_fr_idx = Some(i);
1143 }
1144 }
1145 if let Some(idx) = last_fr_idx {
1146 col_sizes[idx] += free_col - distributed;
1147 }
1148 }
1149
1150 if total_row_fr > 0 {
1151 let mut distributed = 0i32;
1152 let mut last_fr_idx = None;
1153 for (i, track) in cfg.row_tracks.iter().enumerate() {
1154 if let GridTrack::Fr(f) = track {
1155 let share = free_row * (*f as i32) / total_row_fr as i32;
1156 row_sizes[i] = share;
1157 distributed += share;
1158 last_fr_idx = Some(i);
1159 }
1160 }
1161 if let Some(idx) = last_fr_idx {
1162 row_sizes[idx] += free_row - distributed;
1163 }
1164 }
1165
1166 let mut col_offsets: Vec<i32> = Vec::with_capacity(n_cols);
1168 let mut acc = 0i32;
1169 for (i, &s) in col_sizes.iter().enumerate() {
1170 col_offsets.push(acc);
1171 acc += s;
1172 if i < n_cols - 1 {
1173 acc += col_gap;
1174 }
1175 }
1176 let mut row_offsets: Vec<i32> = Vec::with_capacity(n_rows);
1177 acc = 0;
1178 for (i, &s) in row_sizes.iter().enumerate() {
1179 row_offsets.push(acc);
1180 acc += s;
1181 if i < n_rows - 1 {
1182 acc += row_gap;
1183 }
1184 }
1185
1186 for ci in 0..n_children {
1188 let child = &container.children()[ci];
1189 let (hints, intrinsic) = child_hints_and_bounds(child);
1190
1191 let col = (hints.col_pos as usize).min(n_cols.saturating_sub(1));
1192 let row = (hints.row_pos as usize).min(n_rows.saturating_sub(1));
1193 let col_span = (hints.col_span.max(1) as usize).min(n_cols - col);
1194 let row_span = (hints.row_span.max(1) as usize).min(n_rows - row);
1195
1196 let cell_x = content_area.x + col_offsets[col];
1198 let cell_y = content_area.y + row_offsets[row];
1199 let cell_w: i32 =
1200 col_sizes[col..col + col_span].iter().sum::<i32>() + col_gap * (col_span as i32 - 1);
1201 let cell_h: i32 =
1202 row_sizes[row..row + row_span].iter().sum::<i32>() + row_gap * (row_span as i32 - 1);
1203
1204 let item_w = match hints.col_align {
1206 GridAlign::Stretch => cell_w,
1207 _ => {
1208 let w = resolve_main_dim(hints.width, cell_w, intrinsic.width, &hints);
1209 w.min(cell_w)
1210 }
1211 };
1212 let item_h = match hints.row_align {
1213 GridAlign::Stretch => cell_h,
1214 _ => {
1215 let h = resolve_cross_dim(hints.height, cell_h, intrinsic.height, &hints);
1216 h.min(cell_h)
1217 }
1218 };
1219
1220 let item_x = cell_x + grid_align_offset(hints.col_align, cell_w, item_w);
1222 let item_y = cell_y + grid_align_offset(hints.row_align, cell_h, item_h);
1223
1224 let new_rect = Rect {
1225 x: item_x,
1226 y: item_y,
1227 width: item_w,
1228 height: item_h,
1229 };
1230 write_computed(&mut container.children_mut()[ci], new_rect, planner);
1231 }
1232}
1233
1234fn grid_align_offset(align: GridAlign, cell_size: i32, item_size: i32) -> i32 {
1236 let free = (cell_size - item_size).max(0);
1237 match align {
1238 GridAlign::Start
1239 | GridAlign::Stretch
1240 | GridAlign::SpaceEvenly
1241 | GridAlign::SpaceAround
1242 | GridAlign::SpaceBetween => 0,
1243 GridAlign::Center => free / 2,
1244 GridAlign::End => free,
1245 }
1246}
1247
1248fn child_hints_and_bounds(child: &crate::object::ObjectNode) -> (ItemHints, Rect) {
1257 let intrinsic = child.effective_bounds();
1258 let hints = if let Some(ls) = child.layout.as_deref() {
1259 if let LayoutRole::Item(ref h) = ls.role {
1260 let mut h = h.clone();
1261 if h.width == Dimension::Px(0) {
1263 h.width = Dimension::Px(intrinsic.width);
1264 }
1265 if h.height == Dimension::Px(0) {
1266 h.height = Dimension::Px(intrinsic.height);
1267 }
1268 h
1269 } else {
1270 default_hints_for(intrinsic)
1271 }
1272 } else {
1273 default_hints_for(intrinsic)
1274 };
1275 (hints, intrinsic)
1276}
1277
1278fn default_hints_for(intrinsic: Rect) -> ItemHints {
1280 ItemHints {
1281 width: Dimension::Px(intrinsic.width),
1282 height: Dimension::Px(intrinsic.height),
1283 ..ItemHints::default()
1284 }
1285}
1286
1287fn write_computed<const N: usize>(
1290 child: &mut crate::object::ObjectNode,
1291 new_rect: Rect,
1292 planner: &mut InvalidationList<N>,
1293) {
1294 use crate::object::ObjectEvent;
1295
1296 let old_rect = child.effective_bounds();
1298
1299 child.widget().borrow_mut().set_bounds(new_rect);
1301
1302 let slot = child
1304 .layout
1305 .get_or_insert_with(|| Box::new(LayoutState::default()));
1306 slot.computed = Some(new_rect);
1307
1308 if old_rect != new_rect {
1310 planner.push(old_rect.union(new_rect));
1311 child.invoke_handlers_for(&ObjectEvent::SizeChanged);
1313 }
1314}
1315
1316#[cfg(test)]
1321mod tests {
1322 use alloc::rc::Rc;
1323 use alloc::vec;
1324 use core::cell::RefCell;
1325
1326 use super::*;
1327 use crate::event::Event;
1328 use crate::invalidation::InvalidationList;
1329 use crate::object::{ObjectEvent, ObjectNode};
1330 use crate::renderer::Renderer;
1331 use crate::widget::{Color, Widget};
1332
1333 struct ResizeWidget {
1339 bounds: Rect,
1340 set_bounds_log: Vec<Rect>,
1341 }
1342
1343 impl ResizeWidget {
1344 fn new(bounds: Rect) -> Rc<RefCell<Self>> {
1345 Rc::new(RefCell::new(Self {
1346 bounds,
1347 set_bounds_log: Vec::new(),
1348 }))
1349 }
1350 }
1351
1352 impl Widget for ResizeWidget {
1353 fn bounds(&self) -> Rect {
1354 self.bounds
1355 }
1356 fn draw(&self, _r: &mut dyn Renderer) {}
1357 fn handle_event(&mut self, _e: &Event) -> bool {
1358 false
1359 }
1360 fn set_bounds(&mut self, b: Rect) {
1361 self.bounds = b;
1362 self.set_bounds_log.push(b);
1363 }
1364 }
1365
1366 struct StaticWidget {
1368 bounds: Rect,
1369 }
1370 impl StaticWidget {
1371 fn new(x: i32, y: i32, w: i32, h: i32) -> Rc<RefCell<Self>> {
1372 Rc::new(RefCell::new(Self {
1373 bounds: Rect {
1374 x,
1375 y,
1376 width: w,
1377 height: h,
1378 },
1379 }))
1380 }
1381 }
1382 impl Widget for StaticWidget {
1383 fn bounds(&self) -> Rect {
1384 self.bounds
1385 }
1386 fn draw(&self, _r: &mut dyn Renderer) {}
1387 fn handle_event(&mut self, _e: &Event) -> bool {
1388 false
1389 }
1390 }
1392
1393 #[allow(dead_code)]
1394 struct NoopRenderer;
1395 impl Renderer for NoopRenderer {
1396 fn fill_rect(&mut self, _: Rect, _: Color) {}
1397 fn draw_text(&mut self, _: (i32, i32), _: &str, _: Color) {}
1398 }
1399
1400 fn make_node(x: i32, y: i32, w: i32, h: i32) -> ObjectNode {
1401 ObjectNode::new(StaticWidget::new(x, y, w, h))
1402 }
1403
1404 #[test]
1409 fn set_bounds_default_noop() {
1410 let widget = StaticWidget::new(0, 0, 100, 50);
1412 let original = widget.borrow().bounds();
1413 widget.borrow_mut().set_bounds(Rect {
1414 x: 10,
1415 y: 20,
1416 width: 30,
1417 height: 40,
1418 });
1419 assert_eq!(widget.borrow().bounds(), original);
1421 }
1422
1423 #[test]
1424 fn set_bounds_override_adopts_rect() {
1425 let widget = ResizeWidget::new(Rect {
1426 x: 0,
1427 y: 0,
1428 width: 50,
1429 height: 50,
1430 });
1431 let new_bounds = Rect {
1432 x: 10,
1433 y: 20,
1434 width: 80,
1435 height: 60,
1436 };
1437 widget.borrow_mut().set_bounds(new_bounds);
1438 assert_eq!(widget.borrow().bounds(), new_bounds);
1439 assert_eq!(widget.borrow().set_bounds_log, vec![new_bounds]);
1440 }
1441
1442 #[test]
1447 fn effective_bounds_without_layout_returns_intrinsic() {
1448 let node = make_node(5, 10, 100, 200);
1449 let eb = node.effective_bounds();
1450 assert_eq!(
1451 eb,
1452 Rect {
1453 x: 5,
1454 y: 10,
1455 width: 100,
1456 height: 200
1457 }
1458 );
1459 }
1460
1461 #[test]
1462 fn effective_bounds_with_computed_returns_computed() {
1463 let mut node = make_node(5, 10, 100, 200);
1464 node.layout = Some(Box::new(LayoutState {
1465 computed: Some(Rect {
1466 x: 100,
1467 y: 150,
1468 width: 80,
1469 height: 60,
1470 }),
1471 ..LayoutState::default()
1472 }));
1473 let eb = node.effective_bounds();
1474 assert_eq!(
1475 eb,
1476 Rect {
1477 x: 100,
1478 y: 150,
1479 width: 80,
1480 height: 60
1481 }
1482 );
1483 }
1484
1485 #[test]
1490 fn hit_test_uses_effective_bounds() {
1491 let mut node = make_node(0, 0, 50, 50);
1492 node.meta_mut()
1493 .flags_mut()
1494 .insert(crate::object::ObjectFlags::CLICKABLE);
1495 node.layout = Some(Box::new(LayoutState {
1497 computed: Some(Rect {
1498 x: 100,
1499 y: 100,
1500 width: 50,
1501 height: 50,
1502 }),
1503 ..LayoutState::default()
1504 }));
1505 assert!(node.hit_test(10, 10).is_none());
1507 assert!(node.hit_test(110, 110).is_some());
1509 }
1510
1511 #[test]
1516 fn dimension_px_resolves_to_n() {
1517 assert_eq!(resolve_dimension(Dimension::Px(42), 100, 0, None, None), 42);
1518 }
1519
1520 #[test]
1521 fn dimension_pct_resolves_against_parent_content() {
1522 assert_eq!(
1524 resolve_dimension(Dimension::Pct(50), 200, 0, None, None),
1525 100
1526 );
1527 }
1528
1529 #[test]
1530 fn dimension_content_uses_content_size() {
1531 assert_eq!(
1532 resolve_dimension(Dimension::Content, 200, 73, None, None),
1533 73
1534 );
1535 }
1536
1537 #[test]
1538 fn dimension_min_max_clamped() {
1539 assert_eq!(
1540 resolve_dimension(Dimension::Px(5), 100, 0, Some(10), None),
1541 10
1542 );
1543 assert_eq!(
1544 resolve_dimension(Dimension::Px(200), 100, 0, None, Some(50)),
1545 50
1546 );
1547 }
1548
1549 #[test]
1554 fn flex_row_places_children_in_order_with_gap() {
1555 let container_w = StaticWidget::new(0, 0, 200, 50);
1557 let mut container = ObjectNode::new(container_w);
1558 container.set_layout_flex(FlexConfig {
1559 flow: FlexFlow::Row,
1560 gap_main: 10,
1561 ..FlexConfig::default()
1562 });
1563
1564 container.append_child(make_node(0, 0, 60, 50));
1566 container.append_child(make_node(0, 0, 60, 50));
1567
1568 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1570 x: 0,
1571 y: 0,
1572 width: 200,
1573 height: 50,
1574 });
1575
1576 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1577 run_layout(&mut container, &mut planner);
1578
1579 let c0 = container.children()[0].effective_bounds();
1580 let c1 = container.children()[1].effective_bounds();
1581
1582 assert_eq!(c0.x, 0);
1584 assert_eq!(c0.width, 60);
1585 assert_eq!(c1.x, 70);
1586 assert_eq!(c1.width, 60);
1587 }
1588
1589 #[test]
1590 fn flex_column_places_children_vertically() {
1591 let container_w = StaticWidget::new(0, 0, 50, 200);
1592 let mut container = ObjectNode::new(container_w);
1593 container.set_layout_flex(FlexConfig {
1594 flow: FlexFlow::Column,
1595 gap_main: 5,
1596 ..FlexConfig::default()
1597 });
1598 container.append_child(make_node(0, 0, 50, 40));
1599 container.append_child(make_node(0, 0, 50, 40));
1600 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1601 x: 0,
1602 y: 0,
1603 width: 50,
1604 height: 200,
1605 });
1606
1607 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1608 run_layout(&mut container, &mut planner);
1609
1610 let c0 = container.children()[0].effective_bounds();
1611 let c1 = container.children()[1].effective_bounds();
1612 assert_eq!(c0.y, 0);
1613 assert_eq!(c0.height, 40);
1614 assert_eq!(c1.y, 45); assert_eq!(c1.height, 40);
1616 }
1617
1618 #[test]
1623 fn flex_grow_distributes_remaining_space() {
1624 let container_w = StaticWidget::new(0, 0, 200, 50);
1625 let mut container = ObjectNode::new(container_w);
1626 container.set_layout_flex(FlexConfig {
1627 flow: FlexFlow::Row,
1628 gap_main: 0,
1629 ..FlexConfig::default()
1630 });
1631
1632 container.append_child(make_node(0, 0, 40, 50));
1634 let mut child1 = make_node(0, 0, 0, 50);
1636 child1.set_item_hints(ItemHints {
1637 flex_grow: 1,
1638 width: Dimension::Px(0),
1639 height: Dimension::Px(50),
1640 ..ItemHints::default()
1641 });
1642 container.append_child(child1);
1643
1644 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1645 x: 0,
1646 y: 0,
1647 width: 200,
1648 height: 50,
1649 });
1650
1651 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1652 run_layout(&mut container, &mut planner);
1653
1654 let c0 = container.children()[0].effective_bounds();
1655 let c1 = container.children()[1].effective_bounds();
1656 assert_eq!(c0.width, 40);
1657 assert_eq!(c1.x, 40);
1658 assert_eq!(c1.width, 160);
1659 }
1660
1661 #[test]
1666 fn grid_px_tracks_and_explicit_placement() {
1667 let container_w = StaticWidget::new(0, 0, 200, 100);
1669 let mut container = ObjectNode::new(container_w);
1670 container.set_layout_grid(GridConfig {
1671 col_tracks: vec![GridTrack::Px(100), GridTrack::Px(100)],
1672 row_tracks: vec![GridTrack::Px(50), GridTrack::Px(50)],
1673 ..GridConfig::default()
1674 });
1675 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1676 x: 0,
1677 y: 0,
1678 width: 200,
1679 height: 100,
1680 });
1681
1682 let mut child = make_node(0, 0, 80, 40);
1684 child.set_item_hints(ItemHints {
1685 col_pos: 1,
1686 row_pos: 0,
1687 col_align: GridAlign::Stretch,
1688 row_align: GridAlign::Stretch,
1689 ..ItemHints::default()
1690 });
1691 container.append_child(child);
1692
1693 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1694 run_layout(&mut container, &mut planner);
1695
1696 let c = container.children()[0].effective_bounds();
1697 assert_eq!(c.x, 100);
1699 assert_eq!(c.y, 0);
1700 assert_eq!(c.width, 100);
1701 assert_eq!(c.height, 50);
1702 }
1703
1704 #[test]
1705 fn grid_fr_tracks_distribute_remaining_space() {
1706 let container_w = StaticWidget::new(0, 0, 300, 100);
1708 let mut container = ObjectNode::new(container_w);
1709 container.set_layout_grid(GridConfig {
1710 col_tracks: vec![GridTrack::Fr(1), GridTrack::Fr(2)],
1711 row_tracks: vec![GridTrack::Px(100)],
1712 ..GridConfig::default()
1713 });
1714 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1715 x: 0,
1716 y: 0,
1717 width: 300,
1718 height: 100,
1719 });
1720
1721 let mut child0 = make_node(0, 0, 10, 10);
1722 child0.set_item_hints(ItemHints {
1723 col_pos: 0,
1724 row_pos: 0,
1725 col_align: GridAlign::Stretch,
1726 row_align: GridAlign::Stretch,
1727 ..ItemHints::default()
1728 });
1729 let mut child1 = make_node(0, 0, 10, 10);
1730 child1.set_item_hints(ItemHints {
1731 col_pos: 1,
1732 row_pos: 0,
1733 col_align: GridAlign::Stretch,
1734 row_align: GridAlign::Stretch,
1735 ..ItemHints::default()
1736 });
1737 container.append_child(child0);
1738 container.append_child(child1);
1739
1740 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1741 run_layout(&mut container, &mut planner);
1742
1743 let c0 = container.children()[0].effective_bounds();
1744 let c1 = container.children()[1].effective_bounds();
1745 assert_eq!(c0.width, 100);
1747 assert_eq!(c1.x, 100);
1748 assert_eq!(c1.width, 200);
1749 }
1750
1751 #[test]
1752 fn grid_span_covers_multiple_tracks() {
1753 let container_w = StaticWidget::new(0, 0, 150, 50);
1755 let mut container = ObjectNode::new(container_w);
1756 container.set_layout_grid(GridConfig {
1757 col_tracks: vec![GridTrack::Px(50), GridTrack::Px(50), GridTrack::Px(50)],
1758 row_tracks: vec![GridTrack::Px(50)],
1759 ..GridConfig::default()
1760 });
1761 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1762 x: 0,
1763 y: 0,
1764 width: 150,
1765 height: 50,
1766 });
1767
1768 let mut child = make_node(0, 0, 10, 10);
1769 child.set_item_hints(ItemHints {
1770 col_pos: 0,
1771 col_span: 2,
1772 row_pos: 0,
1773 col_align: GridAlign::Stretch,
1774 row_align: GridAlign::Stretch,
1775 ..ItemHints::default()
1776 });
1777 container.append_child(child);
1778
1779 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1780 run_layout(&mut container, &mut planner);
1781
1782 let c = container.children()[0].effective_bounds();
1783 assert_eq!(c.width, 100);
1784 assert_eq!(c.height, 50);
1785 }
1786
1787 #[test]
1792 fn layout_pass_pushes_old_union_new_to_planner() {
1793 let container_w = StaticWidget::new(0, 0, 200, 50);
1794 let mut container = ObjectNode::new(container_w);
1795 container.set_layout_flex(FlexConfig::default());
1796 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1797 x: 0,
1798 y: 0,
1799 width: 200,
1800 height: 50,
1801 });
1802
1803 container.append_child(make_node(0, 0, 50, 50));
1805
1806 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1807 run_layout(&mut container, &mut planner);
1808
1809 let plan = planner.plan();
1811 let _ = plan;
1816 }
1817
1818 #[test]
1819 fn layout_pass_pushes_dirty_rect_on_move() {
1820 let container_w = StaticWidget::new(0, 0, 200, 50);
1821 let mut container = ObjectNode::new(container_w);
1822 container.set_layout_flex(FlexConfig {
1823 flow: FlexFlow::Row,
1824 gap_main: 0,
1825 ..FlexConfig::default()
1826 });
1827 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1828 x: 0,
1829 y: 0,
1830 width: 200,
1831 height: 50,
1832 });
1833
1834 container.append_child(make_node(0, 0, 80, 50));
1836 container.append_child(make_node(0, 0, 60, 50));
1838
1839 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1840 run_layout(&mut container, &mut planner);
1841
1842 let rects_pushed = planner.plan();
1844 assert!(
1846 !matches!(rects_pushed, crate::invalidation::PresentPlan::None),
1847 "expected dirty rects to be pushed for displaced child"
1848 );
1849 }
1850
1851 #[test]
1856 fn size_changed_dispatched_on_resize() {
1857 let container_w = StaticWidget::new(0, 0, 200, 50);
1858 let mut container = ObjectNode::new(container_w);
1859 container.set_layout_flex(FlexConfig::default());
1860 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1861 x: 0,
1862 y: 0,
1863 width: 200,
1864 height: 50,
1865 });
1866
1867 container.append_child(make_node(99, 99, 50, 50));
1870
1871 use alloc::rc::Rc;
1872 use core::cell::Cell;
1873 let fired = Rc::new(Cell::new(false));
1874 let fired2 = fired.clone();
1875 container.children_mut()[0].add_target_handler(move |ev, _ctx| {
1876 if matches!(ev, ObjectEvent::SizeChanged) {
1877 fired2.set(true);
1878 }
1879 false
1880 });
1881
1882 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1883 run_layout(&mut container, &mut planner);
1884
1885 assert!(fired.get(), "SizeChanged should fire when child moves");
1886 }
1887
1888 #[test]
1889 fn layout_changed_dispatched_to_container() {
1890 let container_w = StaticWidget::new(0, 0, 100, 100);
1891 let mut container = ObjectNode::new(container_w);
1892 container.set_layout_flex(FlexConfig::default());
1893 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1894 x: 0,
1895 y: 0,
1896 width: 100,
1897 height: 100,
1898 });
1899
1900 use alloc::rc::Rc;
1901 use core::cell::Cell;
1902 let fired = Rc::new(Cell::new(false));
1903 let fired2 = fired.clone();
1904 container.add_target_handler(move |ev, _ctx| {
1905 if matches!(ev, ObjectEvent::LayoutChanged) {
1906 fired2.set(true);
1907 }
1908 false
1909 });
1910
1911 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1912 run_layout(&mut container, &mut planner);
1913 assert!(fired.get(), "LayoutChanged should fire on the container");
1914 }
1915
1916 #[test]
1921 fn dirty_flag_cleared_after_layout() {
1922 let container_w = StaticWidget::new(0, 0, 100, 50);
1923 let mut container = ObjectNode::new(container_w);
1924 container.set_layout_flex(FlexConfig::default());
1925 container.layout.as_deref_mut().unwrap().computed = Some(Rect {
1926 x: 0,
1927 y: 0,
1928 width: 100,
1929 height: 50,
1930 });
1931
1932 assert!(container.layout.as_ref().unwrap().layout_dirty);
1933 let mut planner: InvalidationList<16> = InvalidationList::with_size(8192, 8192);
1934 run_layout(&mut container, &mut planner);
1935 assert!(!container.layout.as_ref().unwrap().layout_dirty);
1936 }
1937
1938 #[test]
1939 fn set_layout_flex_marks_dirty() {
1940 let mut node = make_node(0, 0, 100, 50);
1941 node.set_layout_flex(FlexConfig::default());
1942 assert!(node.layout.as_ref().unwrap().layout_dirty);
1943 }
1944
1945 #[test]
1946 fn set_item_hints_marks_dirty() {
1947 let mut node = make_node(0, 0, 50, 50);
1948 node.set_item_hints(ItemHints::default());
1949 assert!(node.layout.as_ref().unwrap().layout_dirty);
1950 }
1951
1952 #[test]
1957 fn layout_style_defaults_to_zero() {
1958 let ls = resolve_layout_style(
1959 None,
1960 crate::style_cascade::Part::MAIN,
1961 crate::object::ObjectStates::DEFAULT,
1962 );
1963 assert_eq!(ls, LayoutStyle::default());
1964 }
1965
1966 #[test]
1967 fn layout_style_reads_padding_from_cascade() {
1968 let patch = crate::style_cascade::StylePatch {
1969 padding_top: Some(8),
1970 padding_left: Some(4),
1971 ..crate::style_cascade::StylePatch::new()
1972 };
1973 let mut style_state = crate::style_cascade::StyleState::new();
1974 crate::style_cascade::push_local(
1975 &mut style_state,
1976 patch,
1977 crate::style_cascade::Selector::part(crate::style_cascade::Part::MAIN),
1978 );
1979 let ls = resolve_layout_style(
1980 Some(&style_state),
1981 crate::style_cascade::Part::MAIN,
1982 crate::object::ObjectStates::DEFAULT,
1983 );
1984 assert_eq!(ls.padding_top, 8);
1985 assert_eq!(ls.padding_left, 4);
1986 assert_eq!(ls.padding_bottom, 0);
1987 }
1988
1989 #[test]
1990 fn core_style_struct_unchanged() {
1991 let s = crate::style::Style::default();
1993 let _ = s.bg_color;
1994 let _ = s.border_color;
1995 let _ = s.border_width;
1996 let _ = s.alpha;
1997 let _ = s.radius;
1998 }
1999}