Another project
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): widget_id child mixer, LayoutOffset

Lewis: May this revision serve well! <lu5a@proton.me>

+201 -72
+36
crates/bone-ui/src/layout/geometry.rs
··· 106 106 } 107 107 } 108 108 109 + #[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)] 110 + pub struct LayoutOffset { 111 + pub dx: LayoutPx, 112 + pub dy: LayoutPx, 113 + } 114 + 115 + impl LayoutOffset { 116 + pub const ZERO: Self = Self { 117 + dx: LayoutPx::ZERO, 118 + dy: LayoutPx::ZERO, 119 + }; 120 + 121 + #[must_use] 122 + pub const fn new(dx: LayoutPx, dy: LayoutPx) -> Self { 123 + Self { dx, dy } 124 + } 125 + 126 + #[must_use] 127 + pub fn between(from: LayoutPos, to: LayoutPos) -> Self { 128 + Self { 129 + dx: LayoutPx::saturating(to.x.value() - from.x.value()), 130 + dy: LayoutPx::saturating(to.y.value() - from.y.value()), 131 + } 132 + } 133 + } 134 + 109 135 #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 110 136 pub struct LayoutRect { 111 137 pub origin: LayoutPos, ··· 136 162 #[must_use] 137 163 pub fn max_y(self) -> LayoutPx { 138 164 LayoutPx::saturating(self.origin.y.value() + self.size.height.value()) 165 + } 166 + 167 + #[must_use] 168 + pub fn contains(self, pos: LayoutPos) -> bool { 169 + let x = pos.x.value(); 170 + let y = pos.y.value(); 171 + x >= self.min_x().value() 172 + && x < self.max_x().value() 173 + && y >= self.min_y().value() 174 + && y < self.max_y().value() 139 175 } 140 176 } 141 177
+3 -1
crates/bone-ui/src/layout/mod.rs
··· 17 17 SplitFractionError, TabIndex, 18 18 }; 19 19 pub use engine::{LayoutError, NodeKind, SolvedIndex, SolvedLayout, SolvedNode, measure}; 20 - pub use geometry::{EdgeInsets, LayoutPos, LayoutPx, LayoutPxError, LayoutRect, LayoutSize}; 20 + pub use geometry::{ 21 + EdgeInsets, LayoutOffset, LayoutPos, LayoutPx, LayoutPxError, LayoutRect, LayoutSize, 22 + }; 21 23 pub use paint::{PaintCommand, PaintPlan, paint_plan}; 22 24 pub use primitives::{DockPanel, GridChild, Layout}; 23 25 pub use retained::{RetainedLayout, ScrollOffset};
+48 -70
crates/bone-ui/src/layout/tests.rs
··· 16 16 use crate::widget_id::WidgetId; 17 17 18 18 fn wid(n: u64) -> WidgetId { 19 - WidgetId::from_raw(NonZeroU64::new(n).unwrap_or(NonZeroU64::MIN)) 19 + let Some(nz) = NonZeroU64::new(n) else { 20 + panic!("test widget id must be non-zero"); 21 + }; 22 + WidgetId::from_raw(nz) 20 23 } 21 24 22 25 fn pid(n: u32) -> PanelId { 23 - PanelId::new(NonZeroU32::new(n).unwrap_or(NonZeroU32::MIN)) 26 + let Some(nz) = NonZeroU32::new(n) else { 27 + panic!("test panel id must be non-zero"); 28 + }; 29 + PanelId::new(nz) 24 30 } 25 31 26 32 fn gl(n: u16) -> GridLine { 27 - GridLine::new(NonZeroU16::new(n).unwrap_or(NonZeroU16::MIN)) 33 + let Some(nz) = NonZeroU16::new(n) else { 34 + panic!("test grid line must be non-zero"); 35 + }; 36 + GridLine::new(nz) 28 37 } 29 38 30 39 fn fw(n: u16) -> FlexWeight { 31 - FlexWeight::new(NonZeroU16::new(n).unwrap_or(NonZeroU16::MIN)) 40 + let Some(nz) = NonZeroU16::new(n) else { 41 + panic!("test flex weight must be non-zero"); 42 + }; 43 + FlexWeight::new(nz) 32 44 } 33 45 34 46 fn size(w: f32, h: f32) -> LayoutSize { ··· 124 136 125 137 #[test] 126 138 fn grid_spans_two_columns() { 127 - let Some(span) = GridSpan::rect( 128 - gl(1), 129 - gl(3), 130 - gl(1), 131 - gl(2), 132 - ) else { 139 + let Some(span) = GridSpan::rect(gl(1), gl(3), gl(1), gl(2)) else { 133 140 panic!("valid span"); 134 141 }; 135 142 let layout = Layout::Grid { ··· 431 438 432 439 #[test] 433 440 fn dock_solidworks_default_panel_set_is_complete() { 434 - let panels = ( 435 - pid(1), 436 - pid(2), 437 - pid(3), 438 - pid(4), 439 - pid(5), 440 - ); 441 + let panels = (pid(1), pid(2), pid(3), pid(4), pid(5)); 441 442 let Ok(dock) = DockState::solidworks_default(panels.0, panels.1, panels.2, panels.3, panels.4) 442 443 else { 443 444 panic!("solidworks_default rejected distinct ids"); ··· 650 651 651 652 #[test] 652 653 fn grid_span_rect_rejects_non_increasing_lines() { 653 - assert!( 654 - GridSpan::rect( 655 - gl(2), 656 - gl(1), 657 - gl(1), 658 - gl(2) 659 - ) 660 - .is_none() 661 - ); 662 - assert!( 663 - GridSpan::rect( 664 - gl(1), 665 - gl(1), 666 - gl(1), 667 - gl(2) 668 - ) 669 - .is_none() 670 - ); 671 - assert!( 672 - GridSpan::rect( 673 - gl(1), 674 - gl(2), 675 - gl(2), 676 - gl(2) 677 - ) 678 - .is_none() 679 - ); 680 - assert!( 681 - GridSpan::rect( 682 - gl(1), 683 - gl(2), 684 - gl(1), 685 - gl(2) 686 - ) 687 - .is_some() 688 - ); 654 + assert!(GridSpan::rect(gl(2), gl(1), gl(1), gl(2)).is_none()); 655 + assert!(GridSpan::rect(gl(1), gl(1), gl(1), gl(2)).is_none()); 656 + assert!(GridSpan::rect(gl(1), gl(2), gl(2), gl(2)).is_none()); 657 + assert!(GridSpan::rect(gl(1), gl(2), gl(1), gl(2)).is_some()); 689 658 } 690 659 691 660 #[test] ··· 934 903 assert_eq!(pops, 2); 935 904 assert_eq!(translates, 2); 936 905 assert_eq!(untranslates, 2); 937 - let depth = plan 938 - .commands 939 - .iter() 940 - .try_fold(0i32, |d, c| match c { 941 - PaintCommand::PushClip { .. } | PaintCommand::Translate { .. } => Some(d + 1), 942 - PaintCommand::PopClip | PaintCommand::Untranslate => { 943 - if d <= 0 { 944 - None 945 - } else { 946 - Some(d - 1) 947 - } 906 + let depth = plan.commands.iter().try_fold(0i32, |d, c| match c { 907 + PaintCommand::PushClip { .. } | PaintCommand::Translate { .. } => Some(d + 1), 908 + PaintCommand::PopClip | PaintCommand::Untranslate => { 909 + if d <= 0 { 910 + None 911 + } else { 912 + Some(d - 1) 948 913 } 949 - _ => Some(d), 950 - }); 914 + } 915 + _ => Some(d), 916 + }); 951 917 assert_eq!(depth, Some(0), "push/pop pairs must nest correctly"); 952 918 } 953 919 ··· 1051 1017 }], 1052 1018 }; 1053 1019 let result = measure(&layout, size(100.0, 30.0), &RetainedLayout::default()); 1054 - assert!(matches!(result, Err(LayoutError::GridColumnSpanNotIncreasing))); 1020 + assert!(matches!( 1021 + result, 1022 + Err(LayoutError::GridColumnSpanNotIncreasing) 1023 + )); 1055 1024 } 1056 1025 1057 1026 #[test] ··· 1139 1108 fn scroll_region_clamps_retained_offset_against_content() { 1140 1109 let id = wid(50); 1141 1110 let mut retained = RetainedLayout::default(); 1142 - retained.set_scroll(id, ScrollOffset::new(LayoutPx::ZERO, LayoutPx::new(10_000.0))); 1111 + retained.set_scroll( 1112 + id, 1113 + ScrollOffset::new(LayoutPx::ZERO, LayoutPx::new(10_000.0)), 1114 + ); 1143 1115 let inner = Layout::Column { 1144 1116 gap: sp(0.0), 1145 1117 justify: MainAxisJustify::Start, ··· 1196 1168 tab_strip_height: sp(24.0), 1197 1169 }; 1198 1170 let result = measure(&layout, size(200.0, 200.0), &RetainedLayout::default()); 1199 - assert!(matches!(result, Err(LayoutError::UnsupportedFloatingSurfaces))); 1171 + assert!(matches!( 1172 + result, 1173 + Err(LayoutError::UnsupportedFloatingSurfaces) 1174 + )); 1200 1175 } 1201 1176 1202 1177 #[test] ··· 1205 1180 gap: sp(0.0), 1206 1181 justify: MainAxisJustify::End, 1207 1182 cross: CrossAxisAlign::Center, 1208 - children: vec![Layout::Gap { size: sp(20.0) }, Layout::Gap { size: sp(20.0) }], 1183 + children: vec![ 1184 + Layout::Gap { size: sp(20.0) }, 1185 + Layout::Gap { size: sp(20.0) }, 1186 + ], 1209 1187 }; 1210 1188 let solved = solve(&layout, size(120.0, 10.0), &RetainedLayout::default()); 1211 1189 let kids = &solved.root_node().children;
-1
crates/bone-ui/src/layout/track.rs
··· 140 140 }) 141 141 } 142 142 } 143 -
+114
crates/bone-ui/src/widget_id.rs
··· 6 6 #[serde(transparent)] 7 7 pub struct WidgetId(NonZeroU64); 8 8 9 + const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; 10 + const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; 11 + 9 12 impl WidgetId { 13 + pub const ROOT: Self = match NonZeroU64::new(0xB0FE_B0FE_B0FE_B0FE) { 14 + Some(n) => Self(n), 15 + None => panic!("ROOT seed must be non-zero"), 16 + }; 17 + 10 18 #[must_use] 11 19 pub const fn from_raw(raw: NonZeroU64) -> Self { 12 20 Self(raw) 21 + } 22 + 23 + #[must_use] 24 + pub const fn raw(self) -> NonZeroU64 { 25 + self.0 26 + } 27 + 28 + #[must_use] 29 + pub fn child(self, key: WidgetKey) -> Self { 30 + self.mix(key.as_str(), 0) 31 + } 32 + 33 + #[must_use] 34 + pub fn child_indexed(self, key: WidgetKey, index: u64) -> Self { 35 + self.mix(key.as_str(), index) 36 + } 37 + 38 + fn mix(self, key: &str, index: u64) -> Self { 39 + let parent = self.0.get().to_le_bytes(); 40 + let suffix = index.to_le_bytes(); 41 + let raw = parent 42 + .iter() 43 + .chain(key.as_bytes().iter()) 44 + .chain(suffix.iter()) 45 + .fold(FNV_OFFSET, |h, b| { 46 + (h ^ u64::from(*b)).wrapping_mul(FNV_PRIME) 47 + }) 48 + | 1; 49 + match NonZeroU64::new(raw) { 50 + Some(n) => Self(n), 51 + None => panic!("`| 1` forces non-zero"), 52 + } 53 + } 54 + } 55 + 56 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 57 + #[serde(transparent)] 58 + pub struct WidgetKey(&'static str); 59 + 60 + impl WidgetKey { 61 + #[must_use] 62 + pub const fn new(s: &'static str) -> Self { 63 + Self(s) 64 + } 65 + 66 + #[must_use] 67 + pub const fn as_str(self) -> &'static str { 68 + self.0 69 + } 70 + } 71 + 72 + #[cfg(test)] 73 + mod tests { 74 + use super::{WidgetId, WidgetKey}; 75 + 76 + const PANEL: WidgetKey = WidgetKey::new("panel"); 77 + const ITEM: WidgetKey = WidgetKey::new("item"); 78 + 79 + #[test] 80 + fn root_is_stable() { 81 + assert_eq!(WidgetId::ROOT, WidgetId::ROOT); 82 + } 83 + 84 + #[test] 85 + fn child_is_deterministic() { 86 + let a = WidgetId::ROOT.child(PANEL); 87 + let b = WidgetId::ROOT.child(PANEL); 88 + assert_eq!(a, b); 89 + } 90 + 91 + #[test] 92 + fn child_distinguishes_keys() { 93 + let a = WidgetId::ROOT.child(PANEL); 94 + let b = WidgetId::ROOT.child(ITEM); 95 + assert_ne!(a, b); 96 + } 97 + 98 + #[test] 99 + fn child_indexed_distinguishes_indices() { 100 + let a = WidgetId::ROOT.child_indexed(ITEM, 0); 101 + let b = WidgetId::ROOT.child_indexed(ITEM, 1); 102 + assert_ne!(a, b); 103 + } 104 + 105 + #[test] 106 + fn child_path_depends_on_parent() { 107 + let a = WidgetId::ROOT.child(PANEL).child(ITEM); 108 + let b = WidgetId::ROOT.child(ITEM); 109 + assert_ne!(a, b); 110 + } 111 + 112 + #[test] 113 + fn child_zero_indexed_matches_child_unindexed() { 114 + let a = WidgetId::ROOT.child(PANEL); 115 + let b = WidgetId::ROOT.child_indexed(PANEL, 0); 116 + assert_eq!(a, b); 117 + } 118 + 119 + #[test] 120 + fn child_value_is_pinned_to_fnv() { 121 + let id = WidgetId::ROOT.child(PANEL); 122 + let raw = id.raw().get(); 123 + assert_eq!(raw & 1, 1, "low bit must be forced on"); 124 + assert_eq!(raw, 0x185e_529f_9def_b5c5, "FNV-1a hash must stay frozen"); 125 + let other = WidgetId::ROOT.child(ITEM).raw().get(); 126 + assert_ne!(raw, other); 13 127 } 14 128 }