Another project
1
fork

Configure Feed

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

feat(ui): layout newtypes and taffy dep

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

+420 -48
+19
Cargo.lock
··· 293 293 "palette", 294 294 "ron", 295 295 "serde", 296 + "taffy", 296 297 "thiserror 2.0.18", 297 298 "uom", 298 299 ] ··· 1023 1024 dependencies = [ 1024 1025 "bitflags 2.11.1", 1025 1026 ] 1027 + 1028 + [[package]] 1029 + name = "grid" 1030 + version = "1.0.1" 1031 + source = "registry+https://github.com/rust-lang/crates.io-index" 1032 + checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" 1026 1033 1027 1034 [[package]] 1028 1035 name = "half" ··· 2633 2640 "libc", 2634 2641 "thiserror 1.0.69", 2635 2642 "walkdir", 2643 + ] 2644 + 2645 + [[package]] 2646 + name = "taffy" 2647 + version = "0.10.1" 2648 + source = "registry+https://github.com/rust-lang/crates.io-index" 2649 + checksum = "aea22054047c16c3f34d3ac473a2170be1424b1115b2a3adcf28cfb067c88859" 2650 + dependencies = [ 2651 + "arrayvec", 2652 + "grid", 2653 + "serde", 2654 + "slotmap", 2636 2655 ] 2637 2656 2638 2657 [[package]]
+1
Cargo.toml
··· 49 49 serde = { version = "1", default-features = false, features = ["std", "derive", "rc"] } 50 50 slotmap = { version = "1", default-features = false, features = ["std", "serde"] } 51 51 swash = { version = "0.2", default-features = false, features = ["std", "scale"] } 52 + taffy = { version = "0.10", default-features = false, features = ["std", "flexbox", "grid", "content_size", "serde", "taffy_tree"] } 52 53 tempfile = "3" 53 54 thiserror = "2" 54 55 tracing = "0.1"
+1 -4
crates/bone-render/src/pipelines/glyph.rs
··· 282 282 } 283 283 } 284 284 285 - fn upload_atlas( 286 - device: &wgpu::Device, 287 - queue: &wgpu::Queue, 288 - ) -> (wgpu::TextureView, wgpu::Sampler) { 285 + fn upload_atlas(device: &wgpu::Device, queue: &wgpu::Queue) -> (wgpu::TextureView, wgpu::Sampler) { 289 286 let Ok((extent, rgba)) = decode_png(COMMITTED_ATLAS) else { 290 287 panic!("committed relation_glyphs.png failed to decode; repo state is broken"); 291 288 };
+13 -19
crates/bone-render/src/pipelines/text.rs
··· 3 3 use bone_types::SketchDimensionId; 4 4 use lyon_tessellation::{ 5 5 BuffersBuilder, FillOptions, FillTessellator, FillVertex, VertexBuffers, 6 - path::{ 7 - Path as LyonPath, 8 - builder::WithSvg, 9 - math::Point as LyonPoint, 10 - path::BuilderImpl, 11 - }, 6 + path::{Path as LyonPath, builder::WithSvg, math::Point as LyonPoint, path::BuilderImpl}, 12 7 }; 13 8 use swash::{ 14 9 FontRef, ··· 400 395 ) -> LyonPath { 401 396 placements 402 397 .iter() 403 - .fold(LyonPath::svg_builder(), |mut builder, (glyph_id, origin, _)| { 404 - let mut outline = Outline::new(); 405 - if scaler.scale_outline_into(*glyph_id, &mut outline) { 406 - let mut adapter = LyonAdapter::new(&mut builder, offset_x + origin, offset_y); 407 - outline.path().copy_to(&mut adapter); 408 - } 409 - builder 410 - }) 398 + .fold( 399 + LyonPath::svg_builder(), 400 + |mut builder, (glyph_id, origin, _)| { 401 + let mut outline = Outline::new(); 402 + if scaler.scale_outline_into(*glyph_id, &mut outline) { 403 + let mut adapter = LyonAdapter::new(&mut builder, offset_x + origin, offset_y); 404 + outline.path().copy_to(&mut adapter); 405 + } 406 + builder 407 + }, 408 + ) 411 409 .build() 412 410 } 413 411 ··· 455 453 self 456 454 } 457 455 458 - fn quad_to( 459 - &mut self, 460 - control: impl Into<ZenoPoint>, 461 - to: impl Into<ZenoPoint>, 462 - ) -> &mut Self { 456 + fn quad_to(&mut self, control: impl Into<ZenoPoint>, to: impl Into<ZenoPoint>) -> &mut Self { 463 457 let c = control.into(); 464 458 let p = to.into(); 465 459 self.current = p;
+3 -13
crates/bone-render/src/scene.rs
··· 395 395 ) 396 396 } 397 397 398 - fn push_relation( 399 - mut self, 400 - sketch: &Sketch, 401 - id: SketchRelationId, 402 - ) -> Result<Self, PickIdError> { 398 + fn push_relation(mut self, sketch: &Sketch, id: SketchRelationId) -> Result<Self, PickIdError> { 403 399 let Some(rel) = sketch.relations().get(id).copied() else { 404 400 return Ok(self); 405 401 }; ··· 507 503 fn entity_anchor(sketch: &Sketch, id: SketchEntityId) -> Option<(Point2, Vec2)> { 508 504 let e = sketch.entities().get(id)?; 509 505 Some(match *e { 510 - SketchEntity::Point(p) => ( 511 - p.at(), 512 - Vec2::from_mm(FRAC_1_SQRT_2, FRAC_1_SQRT_2), 513 - ), 506 + SketchEntity::Point(p) => (p.at(), Vec2::from_mm(FRAC_1_SQRT_2, FRAC_1_SQRT_2)), 514 507 SketchEntity::Line(l) => line_anchor(sketch, l), 515 508 SketchEntity::Arc(a) => arc_anchor(sketch, a), 516 509 SketchEntity::Circle(c) => circle_anchor(sketch, c), ··· 537 530 fn circle_anchor(sketch: &Sketch, circle: CircleData) -> (Point2, Vec2) { 538 531 let (cx, cy) = point_position(sketch, circle.center()).coords_mm(); 539 532 let r_mm = circle.radius().get::<millimeter>(); 540 - ( 541 - Point2::from_mm(cx + r_mm, cy), 542 - Vec2::from_mm(1.0, 0.0), 543 - ) 533 + (Point2::from_mm(cx + r_mm, cy), Vec2::from_mm(1.0, 0.0)) 544 534 } 545 535 546 536 fn dimension_anchor(sketch: &Sketch, dim: SketchDimension) -> Option<Point2> {
+2 -2
crates/bone-render/tests/relations.rs
··· 2 2 3 3 use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity, SketchRelation}; 4 4 use bone_render::{ 5 - Camera2, OffscreenContext, PixelDiff, PixelDiffThreshold, PixelsPerMm, RenderError, 6 - RelationGlyphKind, SketchRenderer, SketchScene, Style, ViewportExtent, ViewportPx, decode_png, 5 + Camera2, OffscreenContext, PixelDiff, PixelDiffThreshold, PixelsPerMm, RelationGlyphKind, 6 + RenderError, SketchRenderer, SketchScene, Style, ViewportExtent, ViewportPx, decode_png, 7 7 encode_png, 8 8 }; 9 9 use bone_types::{Length, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3};
+1
crates/bone-ui/Cargo.toml
··· 8 8 [dependencies] 9 9 palette = { workspace = true } 10 10 serde = { workspace = true } 11 + taffy = { workspace = true } 11 12 thiserror = { workspace = true } 12 13 uom = { workspace = true } 13 14
+24
crates/bone-ui/src/layout/axis.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 4 + pub enum Axis { 5 + Horizontal, 6 + Vertical, 7 + } 8 + 9 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 10 + pub enum MainAxisJustify { 11 + Start, 12 + Center, 13 + End, 14 + SpaceBetween, 15 + SpaceAround, 16 + } 17 + 18 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 19 + pub enum CrossAxisAlign { 20 + Start, 21 + Center, 22 + End, 23 + Stretch, 24 + }
+160
crates/bone-ui/src/layout/geometry.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Debug, thiserror::Error, PartialEq, Eq)] 4 + pub enum LayoutPxError { 5 + #[error("LayoutPx must be finite")] 6 + NotFinite, 7 + } 8 + 9 + #[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Serialize, Deserialize)] 10 + #[serde(try_from = "f32", into = "f32")] 11 + pub struct LayoutPx(f32); 12 + 13 + impl LayoutPx { 14 + pub const ZERO: Self = Self(0.0); 15 + 16 + #[must_use] 17 + pub const fn new(value: f32) -> Self { 18 + assert!(value.is_finite(), "LayoutPx must be finite"); 19 + Self(value) 20 + } 21 + 22 + #[must_use] 23 + pub fn saturating(value: f32) -> Self { 24 + if value.is_finite() { 25 + Self(value) 26 + } else { 27 + Self::ZERO 28 + } 29 + } 30 + 31 + #[must_use] 32 + pub fn saturating_nonneg(value: f32) -> Self { 33 + if value.is_finite() { 34 + Self(value.max(0.0)) 35 + } else { 36 + Self::ZERO 37 + } 38 + } 39 + 40 + #[must_use] 41 + pub const fn value(self) -> f32 { 42 + self.0 43 + } 44 + } 45 + 46 + impl Default for LayoutPx { 47 + fn default() -> Self { 48 + Self::ZERO 49 + } 50 + } 51 + 52 + impl TryFrom<f32> for LayoutPx { 53 + type Error = LayoutPxError; 54 + 55 + fn try_from(value: f32) -> Result<Self, Self::Error> { 56 + if value.is_finite() { 57 + Ok(Self(value)) 58 + } else { 59 + Err(LayoutPxError::NotFinite) 60 + } 61 + } 62 + } 63 + 64 + impl From<LayoutPx> for f32 { 65 + fn from(value: LayoutPx) -> Self { 66 + value.0 67 + } 68 + } 69 + 70 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 71 + pub struct LayoutPos { 72 + pub x: LayoutPx, 73 + pub y: LayoutPx, 74 + } 75 + 76 + impl LayoutPos { 77 + pub const ORIGIN: Self = Self { 78 + x: LayoutPx::ZERO, 79 + y: LayoutPx::ZERO, 80 + }; 81 + 82 + #[must_use] 83 + pub const fn new(x: LayoutPx, y: LayoutPx) -> Self { 84 + Self { x, y } 85 + } 86 + } 87 + 88 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 89 + pub struct LayoutSize { 90 + pub width: LayoutPx, 91 + pub height: LayoutPx, 92 + } 93 + 94 + impl LayoutSize { 95 + #[must_use] 96 + pub const fn new(width: LayoutPx, height: LayoutPx) -> Self { 97 + assert!( 98 + width.value() >= 0.0, 99 + "LayoutSize width must be non-negative" 100 + ); 101 + assert!( 102 + height.value() >= 0.0, 103 + "LayoutSize height must be non-negative" 104 + ); 105 + Self { width, height } 106 + } 107 + } 108 + 109 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 110 + pub struct LayoutRect { 111 + pub origin: LayoutPos, 112 + pub size: LayoutSize, 113 + } 114 + 115 + impl LayoutRect { 116 + #[must_use] 117 + pub const fn new(origin: LayoutPos, size: LayoutSize) -> Self { 118 + Self { origin, size } 119 + } 120 + 121 + #[must_use] 122 + pub fn min_x(self) -> LayoutPx { 123 + self.origin.x 124 + } 125 + 126 + #[must_use] 127 + pub fn min_y(self) -> LayoutPx { 128 + self.origin.y 129 + } 130 + 131 + #[must_use] 132 + pub fn max_x(self) -> LayoutPx { 133 + LayoutPx::saturating(self.origin.x.value() + self.size.width.value()) 134 + } 135 + 136 + #[must_use] 137 + pub fn max_y(self) -> LayoutPx { 138 + LayoutPx::saturating(self.origin.y.value() + self.size.height.value()) 139 + } 140 + } 141 + 142 + #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] 143 + pub struct EdgeInsets { 144 + pub left: crate::theme::Spacing, 145 + pub right: crate::theme::Spacing, 146 + pub top: crate::theme::Spacing, 147 + pub bottom: crate::theme::Spacing, 148 + } 149 + 150 + impl EdgeInsets { 151 + #[must_use] 152 + pub const fn all(value: crate::theme::Spacing) -> Self { 153 + Self { 154 + left: value, 155 + right: value, 156 + top: value, 157 + bottom: value, 158 + } 159 + } 160 + }
+26
crates/bone-ui/src/layout/mod.rs
··· 1 + mod axis; 2 + mod dock; 3 + mod engine; 4 + mod geometry; 5 + mod paint; 6 + mod primitives; 7 + mod retained; 8 + mod scroll; 9 + mod splitter; 10 + #[cfg(test)] 11 + mod tests; 12 + mod track; 13 + 14 + pub use axis::{Axis, CrossAxisAlign, MainAxisJustify}; 15 + pub use dock::{ 16 + DockNode, DockState, DockStateError, FloatingSurface, PanelId, SplitFraction, 17 + SplitFractionError, TabIndex, 18 + }; 19 + pub use engine::{LayoutError, NodeKind, SolvedIndex, SolvedLayout, SolvedNode, measure}; 20 + pub use geometry::{EdgeInsets, LayoutPos, LayoutPx, LayoutPxError, LayoutRect, LayoutSize}; 21 + pub use paint::{PaintCommand, PaintPlan, paint_plan}; 22 + pub use primitives::{DockPanel, GridChild, Layout}; 23 + pub use retained::{RetainedLayout, ScrollOffset}; 24 + pub use scroll::{ScrollAxes, clamp_scroll}; 25 + pub use splitter::{SplitterMove, SplitterStep, apply_keyboard_move, fraction_from_drag}; 26 + pub use track::{FlexWeight, GridLine, GridLineRef, GridSpan, GridTrack, TrackName, TrackSize};
+143
crates/bone-ui/src/layout/track.rs
··· 1 + use core::num::NonZeroU16; 2 + 3 + use serde::{Deserialize, Serialize}; 4 + 5 + use crate::theme::Spacing; 6 + 7 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 8 + #[serde(transparent)] 9 + pub struct FlexWeight(NonZeroU16); 10 + 11 + impl FlexWeight { 12 + pub const ONE: Self = Self(NonZeroU16::MIN); 13 + 14 + #[must_use] 15 + pub const fn new(weight: NonZeroU16) -> Self { 16 + Self(weight) 17 + } 18 + 19 + #[must_use] 20 + pub fn as_f32(self) -> f32 { 21 + f32::from(self.0.get()) 22 + } 23 + } 24 + 25 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 26 + #[serde(transparent)] 27 + pub struct GridLine(NonZeroU16); 28 + 29 + impl GridLine { 30 + #[must_use] 31 + pub const fn new(line: NonZeroU16) -> Self { 32 + Self(line) 33 + } 34 + 35 + #[must_use] 36 + pub const fn get(self) -> NonZeroU16 { 37 + self.0 38 + } 39 + 40 + #[must_use] 41 + pub const fn taffy_index(self) -> i16 { 42 + let n = self.0.get(); 43 + if n > i16::MAX as u16 { 44 + i16::MAX 45 + } else { 46 + n.cast_signed() 47 + } 48 + } 49 + } 50 + 51 + #[derive(Copy, Clone, Debug, PartialEq)] 52 + pub enum TrackSize { 53 + Fixed(Spacing), 54 + Auto, 55 + Flex(FlexWeight), 56 + } 57 + 58 + impl TrackSize { 59 + pub const FLEX_1: Self = Self::Flex(FlexWeight::ONE); 60 + } 61 + 62 + #[derive(Clone, Debug, PartialEq, Eq, Hash)] 63 + pub struct TrackName(&'static str); 64 + 65 + impl TrackName { 66 + #[must_use] 67 + pub const fn new(name: &'static str) -> Self { 68 + Self(name) 69 + } 70 + 71 + #[must_use] 72 + pub const fn as_str(&self) -> &'static str { 73 + self.0 74 + } 75 + } 76 + 77 + #[derive(Clone, Debug, PartialEq)] 78 + pub struct GridTrack { 79 + pub name: Option<TrackName>, 80 + pub size: TrackSize, 81 + } 82 + 83 + impl GridTrack { 84 + #[must_use] 85 + pub const fn unnamed(size: TrackSize) -> Self { 86 + Self { name: None, size } 87 + } 88 + 89 + #[must_use] 90 + pub const fn named(name: TrackName, size: TrackSize) -> Self { 91 + Self { 92 + name: Some(name), 93 + size, 94 + } 95 + } 96 + } 97 + 98 + #[derive(Clone, Debug, PartialEq, Eq, Hash)] 99 + pub enum GridLineRef { 100 + Line(GridLine), 101 + Name(TrackName), 102 + } 103 + 104 + impl From<GridLine> for GridLineRef { 105 + fn from(line: GridLine) -> Self { 106 + Self::Line(line) 107 + } 108 + } 109 + 110 + impl From<TrackName> for GridLineRef { 111 + fn from(name: TrackName) -> Self { 112 + Self::Name(name) 113 + } 114 + } 115 + 116 + #[derive(Clone, Debug, PartialEq, Eq, Hash)] 117 + pub struct GridSpan { 118 + pub column_start: GridLineRef, 119 + pub column_end: GridLineRef, 120 + pub row_start: GridLineRef, 121 + pub row_end: GridLineRef, 122 + } 123 + 124 + impl GridSpan { 125 + #[must_use] 126 + pub fn rect( 127 + column_start: GridLine, 128 + column_end: GridLine, 129 + row_start: GridLine, 130 + row_end: GridLine, 131 + ) -> Option<Self> { 132 + if column_end.get() <= column_start.get() || row_end.get() <= row_start.get() { 133 + return None; 134 + } 135 + Some(Self { 136 + column_start: GridLineRef::Line(column_start), 137 + column_end: GridLineRef::Line(column_end), 138 + row_start: GridLineRef::Line(row_start), 139 + row_end: GridLineRef::Line(row_end), 140 + }) 141 + } 142 + } 143 +
+5 -2
crates/bone-ui/src/lib.rs
··· 1 + pub mod layout; 1 2 pub mod theme; 3 + mod widget_id; 2 4 3 5 pub use theme::{ 4 6 BlurRadius, Border, CadColors, Color, ColorError, Colors, Easing, ElevationLevel, 5 7 ElevationScale, FontFace, FontSize, FontWeight, LetterSpacing, LineHeight, Motion, MotionToken, 6 - Radius, RadiusScale, Scale12, Shadow, ShadowOffset, Spacing, SpacingScale, Step12, 7 - StrokeWidth, SurfaceLevel, Theme, ThemeMode, Typography, TypographyRole, 8 + Radius, RadiusScale, Scale12, Shadow, ShadowOffset, Spacing, SpacingScale, Step12, StrokeWidth, 9 + SurfaceLevel, Theme, ThemeMode, Typography, TypographyRole, 8 10 }; 11 + pub use widget_id::WidgetId;
+8 -8
crates/bone-ui/src/theme/color.rs
··· 66 66 pub const TRANSPARENT: Self = Self([0.0, 0.0, 0.0, 0.0]); 67 67 68 68 #[must_use] 69 - pub(in crate::theme) const fn from_linear_rgba_premul( 70 - r: f32, 71 - g: f32, 72 - b: f32, 73 - a: f32, 74 - ) -> Self { 69 + pub(in crate::theme) const fn from_linear_rgba_premul(r: f32, g: f32, b: f32, a: f32) -> Self { 75 70 Self([r, g, b, a]) 76 71 } 77 72 ··· 608 603 let scale = Scale12::from_oklch_curve( 609 604 230.0, 610 605 &[0.05; 12], 611 - &[0.10, 0.18, 0.26, 0.34, 0.42, 0.50, 0.58, 0.66, 0.74, 0.82, 0.90, 0.98], 606 + &[ 607 + 0.10, 0.18, 0.26, 0.34, 0.42, 0.50, 0.58, 0.66, 0.74, 0.82, 0.90, 0.98, 608 + ], 612 609 ); 613 610 let steps = scale.steps(); 614 611 let lums: Vec<f32> = steps.iter().map(|c| c.relative_luminance()).collect(); 615 612 let monotone = lums.windows(2).all(|w| w[0] <= w[1]); 616 - assert!(monotone, "lightness curve should produce non-decreasing luminance"); 613 + assert!( 614 + monotone, 615 + "lightness curve should produce non-decreasing luminance" 616 + ); 617 617 } 618 618 619 619 #[test]
+14
crates/bone-ui/src/widget_id.rs
··· 1 + use core::num::NonZeroU64; 2 + 3 + use serde::{Deserialize, Serialize}; 4 + 5 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 6 + #[serde(transparent)] 7 + pub struct WidgetId(NonZeroU64); 8 + 9 + impl WidgetId { 10 + #[must_use] 11 + pub const fn from_raw(raw: NonZeroU64) -> Self { 12 + Self(raw) 13 + } 14 + }