Another project
1
fork

Configure Feed

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

feat(ui): layout tree, paint plan

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

+295
+153
crates/bone-ui/src/layout/paint.rs
··· 1 + use super::axis::Axis; 2 + use super::dock::PanelId; 3 + use super::engine::{NodeKind, SolvedLayout, SolvedNode}; 4 + use super::geometry::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 5 + use super::scroll::ScrollAxes; 6 + use crate::theme::{Color, ElevationLevel, StrokeWidth, Theme}; 7 + use crate::widget_id::WidgetId; 8 + 9 + #[derive(Clone, Debug, PartialEq)] 10 + pub enum PaintCommand { 11 + PushClip { 12 + rect: LayoutRect, 13 + }, 14 + PopClip, 15 + Surface { 16 + rect: LayoutRect, 17 + elevation: ElevationLevel, 18 + }, 19 + Divider { 20 + rect: LayoutRect, 21 + axis: Axis, 22 + color: Color, 23 + }, 24 + TabStrip { 25 + rect: LayoutRect, 26 + active: PanelId, 27 + tabs: Vec<PanelId>, 28 + }, 29 + Translate { 30 + delta: LayoutPos, 31 + }, 32 + Untranslate, 33 + PanelSlot { 34 + rect: LayoutRect, 35 + id: PanelId, 36 + }, 37 + LeafSlot { 38 + rect: LayoutRect, 39 + id: WidgetId, 40 + }, 41 + ScrollContent { 42 + rect: LayoutRect, 43 + id: WidgetId, 44 + axes: ScrollAxes, 45 + content_size: LayoutSize, 46 + }, 47 + } 48 + 49 + #[derive(Clone, Debug, PartialEq, Default)] 50 + pub struct PaintPlan { 51 + pub commands: Vec<PaintCommand>, 52 + } 53 + 54 + #[must_use] 55 + pub fn paint_plan(solved: &SolvedLayout, theme: &Theme) -> PaintPlan { 56 + PaintPlan { 57 + commands: walk(solved, solved.root_node(), theme), 58 + } 59 + } 60 + 61 + fn walk(layout: &SolvedLayout, node: &SolvedNode, theme: &Theme) -> Vec<PaintCommand> { 62 + match &node.kind { 63 + NodeKind::Pass => walk_children(layout, node, theme), 64 + NodeKind::Leaf(id) => vec![PaintCommand::LeafSlot { 65 + rect: node.rect, 66 + id: *id, 67 + }], 68 + NodeKind::ScrollRegion { id, axes, offset } => { 69 + core::iter::once(PaintCommand::PushClip { rect: node.rect }) 70 + .chain(core::iter::once(PaintCommand::ScrollContent { 71 + rect: node.rect, 72 + id: *id, 73 + axes: *axes, 74 + content_size: node.content_size, 75 + })) 76 + .chain(core::iter::once(PaintCommand::Translate { 77 + delta: LayoutPos::new( 78 + LayoutPx::saturating(-offset.x.value()), 79 + LayoutPx::saturating(-offset.y.value()), 80 + ), 81 + })) 82 + .chain(walk_children(layout, node, theme)) 83 + .chain(core::iter::once(PaintCommand::Untranslate)) 84 + .chain(core::iter::once(PaintCommand::PopClip)) 85 + .collect() 86 + } 87 + NodeKind::Splitter { axis, .. } | NodeKind::DockSplit { axis, .. } => { 88 + walk_children(layout, node, theme) 89 + .into_iter() 90 + .chain(divider_command(layout, node, *axis, theme)) 91 + .collect() 92 + } 93 + NodeKind::DockHost { .. } => core::iter::once(PaintCommand::Surface { 94 + rect: node.rect, 95 + elevation: theme.elevation.level0, 96 + }) 97 + .chain(walk_children(layout, node, theme)) 98 + .collect(), 99 + NodeKind::DockTabStrip { active, tabs } => vec![PaintCommand::TabStrip { 100 + rect: node.rect, 101 + active: *active, 102 + tabs: tabs.clone(), 103 + }], 104 + NodeKind::DockPanel { id } => core::iter::once(PaintCommand::Surface { 105 + rect: node.rect, 106 + elevation: theme.elevation.level1, 107 + }) 108 + .chain(core::iter::once(PaintCommand::PanelSlot { 109 + rect: node.rect, 110 + id: *id, 111 + })) 112 + .chain(walk_children(layout, node, theme)) 113 + .collect(), 114 + } 115 + } 116 + 117 + fn walk_children(layout: &SolvedLayout, node: &SolvedNode, theme: &Theme) -> Vec<PaintCommand> { 118 + node.children 119 + .iter() 120 + .flat_map(|c| walk(layout, layout.node(*c), theme)) 121 + .collect() 122 + } 123 + 124 + fn divider_command( 125 + layout: &SolvedLayout, 126 + node: &SolvedNode, 127 + axis: Axis, 128 + theme: &Theme, 129 + ) -> Option<PaintCommand> { 130 + if node.children.len() != 2 { 131 + return None; 132 + } 133 + let first = layout.node(node.children[0]); 134 + Some(PaintCommand::Divider { 135 + rect: divider_between(axis, first.rect, node.rect), 136 + axis, 137 + color: theme.colors.neutral.step(crate::theme::Step12::BORDER), 138 + }) 139 + } 140 + 141 + fn divider_between(axis: Axis, first: LayoutRect, parent: LayoutRect) -> LayoutRect { 142 + let thickness = LayoutPx::new(StrokeWidth::HAIRLINE.value_px()); 143 + match axis { 144 + Axis::Horizontal => LayoutRect::new( 145 + LayoutPos::new(first.max_x(), parent.min_y()), 146 + LayoutSize::new(thickness, parent.size.height), 147 + ), 148 + Axis::Vertical => LayoutRect::new( 149 + LayoutPos::new(parent.min_x(), first.max_y()), 150 + LayoutSize::new(parent.size.width, thickness), 151 + ), 152 + } 153 + }
+142
crates/bone-ui/src/layout/primitives.rs
··· 1 + use super::axis::{Axis, CrossAxisAlign, MainAxisJustify}; 2 + use super::dock::{DockState, PanelId, SplitFraction}; 3 + use super::geometry::EdgeInsets; 4 + use super::scroll::ScrollAxes; 5 + use super::track::{FlexWeight, GridSpan, GridTrack}; 6 + use crate::theme::Spacing; 7 + use crate::widget_id::WidgetId; 8 + 9 + #[derive(Clone, Debug, PartialEq)] 10 + pub struct GridChild { 11 + pub span: GridSpan, 12 + pub child: Layout, 13 + } 14 + 15 + #[derive(Clone, Debug, PartialEq)] 16 + pub struct DockPanel { 17 + pub id: PanelId, 18 + pub child: Layout, 19 + } 20 + 21 + #[derive(Clone, Debug, PartialEq)] 22 + pub enum Layout { 23 + Row { 24 + gap: Spacing, 25 + justify: MainAxisJustify, 26 + cross: CrossAxisAlign, 27 + children: Vec<Layout>, 28 + }, 29 + Column { 30 + gap: Spacing, 31 + justify: MainAxisJustify, 32 + cross: CrossAxisAlign, 33 + children: Vec<Layout>, 34 + }, 35 + Stack { 36 + children: Vec<Layout>, 37 + }, 38 + Grid { 39 + columns: Vec<GridTrack>, 40 + rows: Vec<GridTrack>, 41 + column_gap: Spacing, 42 + row_gap: Spacing, 43 + children: Vec<GridChild>, 44 + }, 45 + Inset { 46 + padding: EdgeInsets, 47 + child: Box<Layout>, 48 + }, 49 + Spacer { 50 + weight: FlexWeight, 51 + }, 52 + Gap { 53 + size: Spacing, 54 + }, 55 + ScrollRegion { 56 + id: WidgetId, 57 + axes: ScrollAxes, 58 + child: Box<Layout>, 59 + }, 60 + Splitter { 61 + id: WidgetId, 62 + axis: Axis, 63 + default_fraction: SplitFraction, 64 + a: Box<Layout>, 65 + b: Box<Layout>, 66 + }, 67 + DockHost { 68 + id: WidgetId, 69 + state: DockState, 70 + panels: Vec<DockPanel>, 71 + tab_strip_height: Spacing, 72 + }, 73 + Leaf { 74 + id: WidgetId, 75 + }, 76 + } 77 + 78 + impl Layout { 79 + #[must_use] 80 + pub fn inset(padding: EdgeInsets, child: Layout) -> Self { 81 + Self::Inset { 82 + padding, 83 + child: Box::new(child), 84 + } 85 + } 86 + 87 + #[must_use] 88 + pub fn spacer() -> Self { 89 + Self::weighted_spacer(FlexWeight::ONE) 90 + } 91 + 92 + #[must_use] 93 + pub const fn weighted_spacer(weight: FlexWeight) -> Self { 94 + Self::Spacer { weight } 95 + } 96 + 97 + #[must_use] 98 + pub const fn leaf(id: WidgetId) -> Self { 99 + Self::Leaf { id } 100 + } 101 + 102 + #[must_use] 103 + pub fn scroll_region(id: WidgetId, axes: ScrollAxes, child: Layout) -> Self { 104 + Self::ScrollRegion { 105 + id, 106 + axes, 107 + child: Box::new(child), 108 + } 109 + } 110 + 111 + #[must_use] 112 + pub fn splitter( 113 + id: WidgetId, 114 + axis: Axis, 115 + default_fraction: SplitFraction, 116 + a: Layout, 117 + b: Layout, 118 + ) -> Self { 119 + Self::Splitter { 120 + id, 121 + axis, 122 + default_fraction, 123 + a: Box::new(a), 124 + b: Box::new(b), 125 + } 126 + } 127 + 128 + #[must_use] 129 + pub fn dock_host( 130 + id: WidgetId, 131 + state: DockState, 132 + panels: Vec<DockPanel>, 133 + tab_strip_height: Spacing, 134 + ) -> Self { 135 + Self::DockHost { 136 + id, 137 + state, 138 + panels, 139 + tab_strip_height, 140 + } 141 + } 142 + }