Another project
1
fork

Configure Feed

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

feat(ui): dock state, retained scroll + split

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

+387
+223
crates/bone-ui/src/layout/dock.rs
··· 1 + use core::num::NonZeroU32; 2 + use std::collections::BTreeSet; 3 + 4 + use serde::{Deserialize, Serialize}; 5 + 6 + use super::axis::Axis; 7 + use super::geometry::{LayoutPos, LayoutSize}; 8 + 9 + #[derive(Debug, thiserror::Error, PartialEq, Eq)] 10 + pub enum SplitFractionError { 11 + #[error("split fraction must be finite")] 12 + NotFinite, 13 + #[error("split fraction must lie in [0.0, 1.0]")] 14 + OutOfRange, 15 + } 16 + 17 + #[derive(Debug, thiserror::Error, PartialEq, Eq)] 18 + pub enum DockStateError { 19 + #[error("solidworks default requires distinct panel ids; {0} appears more than once")] 20 + DuplicatePanelId(PanelId), 21 + } 22 + 23 + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] 24 + #[serde(try_from = "f32", into = "f32")] 25 + pub struct SplitFraction(f32); 26 + 27 + impl SplitFraction { 28 + pub const HALF: Self = Self::clamped(0.5); 29 + 30 + pub fn new(value: f32) -> Result<Self, SplitFractionError> { 31 + if !value.is_finite() { 32 + return Err(SplitFractionError::NotFinite); 33 + } 34 + if !(0.0..=1.0).contains(&value) { 35 + return Err(SplitFractionError::OutOfRange); 36 + } 37 + Ok(Self(value)) 38 + } 39 + 40 + #[must_use] 41 + pub const fn clamped(value: f32) -> Self { 42 + if !value.is_finite() { 43 + return Self(0.5); 44 + } 45 + let bounded = if value < 0.0 { 46 + 0.0 47 + } else if value > 1.0 { 48 + 1.0 49 + } else { 50 + value 51 + }; 52 + Self(bounded) 53 + } 54 + 55 + #[must_use] 56 + pub const fn value(self) -> f32 { 57 + self.0 58 + } 59 + } 60 + 61 + impl TryFrom<f32> for SplitFraction { 62 + type Error = SplitFractionError; 63 + fn try_from(value: f32) -> Result<Self, Self::Error> { 64 + Self::new(value) 65 + } 66 + } 67 + 68 + impl From<SplitFraction> for f32 { 69 + fn from(s: SplitFraction) -> Self { 70 + s.0 71 + } 72 + } 73 + 74 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 75 + #[serde(transparent)] 76 + pub struct TabIndex(u16); 77 + 78 + impl TabIndex { 79 + pub const ZERO: Self = Self(0); 80 + 81 + #[must_use] 82 + pub const fn new(index: u16) -> Self { 83 + Self(index) 84 + } 85 + 86 + #[must_use] 87 + pub const fn get(self) -> u16 { 88 + self.0 89 + } 90 + } 91 + 92 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 93 + #[serde(transparent)] 94 + pub struct PanelId(NonZeroU32); 95 + 96 + impl PanelId { 97 + #[must_use] 98 + pub const fn new(id: NonZeroU32) -> Self { 99 + Self(id) 100 + } 101 + 102 + #[must_use] 103 + pub const fn get(self) -> NonZeroU32 { 104 + self.0 105 + } 106 + } 107 + 108 + impl core::fmt::Display for PanelId { 109 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 110 + write!(f, "PanelId({})", self.0.get()) 111 + } 112 + } 113 + 114 + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 115 + pub enum DockNode { 116 + Split { 117 + axis: Axis, 118 + fraction: SplitFraction, 119 + a: Box<DockNode>, 120 + b: Box<DockNode>, 121 + }, 122 + Tabs { 123 + tabs: Vec<PanelId>, 124 + active: TabIndex, 125 + }, 126 + } 127 + 128 + impl DockNode { 129 + #[must_use] 130 + pub fn tabs(panels: Vec<PanelId>) -> Self { 131 + Self::Tabs { 132 + tabs: panels, 133 + active: TabIndex::ZERO, 134 + } 135 + } 136 + 137 + #[must_use] 138 + pub fn split(axis: Axis, fraction: SplitFraction, a: Self, b: Self) -> Self { 139 + Self::Split { 140 + axis, 141 + fraction, 142 + a: Box::new(a), 143 + b: Box::new(b), 144 + } 145 + } 146 + 147 + #[must_use] 148 + pub fn panel_ids(&self) -> Vec<PanelId> { 149 + match self { 150 + Self::Tabs { tabs, .. } => tabs.clone(), 151 + Self::Split { a, b, .. } => a.panel_ids().into_iter().chain(b.panel_ids()).collect(), 152 + } 153 + } 154 + } 155 + 156 + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 157 + pub struct FloatingSurface { 158 + pub root: DockNode, 159 + pub origin: LayoutPos, 160 + pub size: LayoutSize, 161 + } 162 + 163 + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 164 + pub struct DockState { 165 + pub main: DockNode, 166 + pub floating: Vec<FloatingSurface>, 167 + } 168 + 169 + impl DockState { 170 + #[must_use] 171 + pub fn new(main: DockNode) -> Self { 172 + Self { 173 + main, 174 + floating: Vec::new(), 175 + } 176 + } 177 + 178 + pub fn solidworks_default( 179 + feature_tree: PanelId, 180 + property_pane: PanelId, 181 + ribbon: PanelId, 182 + viewport: PanelId, 183 + status: PanelId, 184 + ) -> Result<Self, DockStateError> { 185 + const FEATURE_TREE_RATIO: SplitFraction = SplitFraction::clamped(0.22); 186 + const VIEWPORT_RATIO: SplitFraction = SplitFraction::clamped(0.78); 187 + const RIBBON_RATIO: SplitFraction = SplitFraction::clamped(0.10); 188 + const STATUS_RATIO: SplitFraction = SplitFraction::clamped(0.97); 189 + [feature_tree, property_pane, ribbon, viewport, status] 190 + .into_iter() 191 + .try_fold(BTreeSet::<PanelId>::new(), |mut seen, id| { 192 + if seen.insert(id) { 193 + Ok(seen) 194 + } else { 195 + Err(DockStateError::DuplicatePanelId(id)) 196 + } 197 + })?; 198 + let center = DockNode::split( 199 + Axis::Horizontal, 200 + FEATURE_TREE_RATIO, 201 + DockNode::tabs(vec![feature_tree]), 202 + DockNode::split( 203 + Axis::Horizontal, 204 + VIEWPORT_RATIO, 205 + DockNode::tabs(vec![viewport]), 206 + DockNode::tabs(vec![property_pane]), 207 + ), 208 + ); 209 + let with_ribbon = DockNode::split( 210 + Axis::Vertical, 211 + RIBBON_RATIO, 212 + DockNode::tabs(vec![ribbon]), 213 + center, 214 + ); 215 + let main = DockNode::split( 216 + Axis::Vertical, 217 + STATUS_RATIO, 218 + with_ribbon, 219 + DockNode::tabs(vec![status]), 220 + ); 221 + Ok(Self::new(main)) 222 + } 223 + }
+51
crates/bone-ui/src/layout/retained.rs
··· 1 + use std::collections::BTreeMap; 2 + 3 + use serde::{Deserialize, Serialize}; 4 + 5 + use super::dock::SplitFraction; 6 + use super::geometry::LayoutPx; 7 + use crate::widget_id::WidgetId; 8 + 9 + #[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize)] 10 + pub struct ScrollOffset { 11 + pub x: LayoutPx, 12 + pub y: LayoutPx, 13 + } 14 + 15 + impl ScrollOffset { 16 + pub const ZERO: Self = Self { 17 + x: LayoutPx::ZERO, 18 + y: LayoutPx::ZERO, 19 + }; 20 + 21 + #[must_use] 22 + pub const fn new(x: LayoutPx, y: LayoutPx) -> Self { 23 + Self { x, y } 24 + } 25 + } 26 + 27 + #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] 28 + pub struct RetainedLayout { 29 + pub scroll: BTreeMap<WidgetId, ScrollOffset>, 30 + pub split: BTreeMap<WidgetId, SplitFraction>, 31 + } 32 + 33 + impl RetainedLayout { 34 + #[must_use] 35 + pub fn scroll_for(&self, id: WidgetId) -> ScrollOffset { 36 + self.scroll.get(&id).copied().unwrap_or(ScrollOffset::ZERO) 37 + } 38 + 39 + #[must_use] 40 + pub fn split_for(&self, id: WidgetId, default: SplitFraction) -> SplitFraction { 41 + self.split.get(&id).copied().unwrap_or(default) 42 + } 43 + 44 + pub fn set_scroll(&mut self, id: WidgetId, offset: ScrollOffset) { 45 + self.scroll.insert(id, offset); 46 + } 47 + 48 + pub fn set_split(&mut self, id: WidgetId, fraction: SplitFraction) { 49 + self.split.insert(id, fraction); 50 + } 51 + }
+59
crates/bone-ui/src/layout/scroll.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + use super::geometry::{LayoutPx, LayoutSize}; 4 + use super::retained::ScrollOffset; 5 + 6 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 7 + pub enum ScrollAxes { 8 + Horizontal, 9 + Vertical, 10 + Both, 11 + } 12 + 13 + impl ScrollAxes { 14 + #[must_use] 15 + pub const fn allows_horizontal(self) -> bool { 16 + matches!(self, Self::Horizontal | Self::Both) 17 + } 18 + 19 + #[must_use] 20 + pub const fn allows_vertical(self) -> bool { 21 + matches!(self, Self::Vertical | Self::Both) 22 + } 23 + } 24 + 25 + #[must_use] 26 + pub fn clamp_scroll( 27 + requested: ScrollOffset, 28 + viewport: LayoutSize, 29 + content: LayoutSize, 30 + axes: ScrollAxes, 31 + ) -> ScrollOffset { 32 + ScrollOffset { 33 + x: clamp_axis( 34 + requested.x, 35 + viewport.width, 36 + content.width, 37 + axes.allows_horizontal(), 38 + ), 39 + y: clamp_axis( 40 + requested.y, 41 + viewport.height, 42 + content.height, 43 + axes.allows_vertical(), 44 + ), 45 + } 46 + } 47 + 48 + fn clamp_axis( 49 + requested: LayoutPx, 50 + viewport: LayoutPx, 51 + content: LayoutPx, 52 + allowed: bool, 53 + ) -> LayoutPx { 54 + if !allowed { 55 + return LayoutPx::ZERO; 56 + } 57 + let max = (content.value() - viewport.value()).max(0.0); 58 + LayoutPx::new(requested.value().clamp(0.0, max)) 59 + }
+54
crates/bone-ui/src/layout/splitter.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + use super::dock::SplitFraction; 4 + use super::geometry::LayoutPx; 5 + 6 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 7 + pub enum SplitterStep { 8 + Coarse, 9 + Fine, 10 + } 11 + 12 + impl SplitterStep { 13 + #[must_use] 14 + pub const fn fraction_step(self) -> f32 { 15 + match self { 16 + Self::Fine => 0.005, 17 + Self::Coarse => 0.05, 18 + } 19 + } 20 + } 21 + 22 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 23 + pub enum SplitterMove { 24 + DecreaseA(SplitterStep), 25 + IncreaseA(SplitterStep), 26 + Min, 27 + Max, 28 + Reset, 29 + } 30 + 31 + #[must_use] 32 + pub fn apply_keyboard_move( 33 + current: SplitFraction, 34 + default: SplitFraction, 35 + mv: SplitterMove, 36 + ) -> SplitFraction { 37 + let next = match mv { 38 + SplitterMove::DecreaseA(step) => current.value() - step.fraction_step(), 39 + SplitterMove::IncreaseA(step) => current.value() + step.fraction_step(), 40 + SplitterMove::Min => 0.0, 41 + SplitterMove::Max => 1.0, 42 + SplitterMove::Reset => default.value(), 43 + }; 44 + SplitFraction::clamped(next) 45 + } 46 + 47 + #[must_use] 48 + pub fn fraction_from_drag(total: LayoutPx, handle: LayoutPx) -> SplitFraction { 49 + let total_v = total.value(); 50 + if total_v <= 0.0 { 51 + return SplitFraction::HALF; 52 + } 53 + SplitFraction::clamped(handle.value() / total_v) 54 + }