Another project
0

Configure Feed

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

at main 7.5 kB View raw
1use bone_types::IconId; 2use bone_ui::a11y::{AccessNode, Role}; 3use bone_ui::frame::{FrameCtx, InteractDeclaration}; 4use bone_ui::hit_test::Sense; 5use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 6use bone_ui::strings::StringKey; 7use bone_ui::theme::{Border, Step12, StrokeWidth}; 8use bone_ui::widgets::{IconTint, WidgetPaint}; 9use bone_ui::{WidgetId, WidgetKey}; 10 11use crate::shell::MenuAction; 12use crate::strings; 13 14const BUTTON_PX: f32 = 22.0; 15const ICON_PX: f32 = 16.0; 16const BUTTON_GAP: f32 = 2.0; 17const GROUP_GAP: f32 = 8.0; 18const STRIP_PAD: f32 = 4.0; 19const STRIP_TOP_INSET: f32 = 8.0; 20 21#[derive(Copy, Clone)] 22struct HeadsUpTool { 23 key: &'static str, 24 icon: IconId, 25 label: StringKey, 26 action: Option<MenuAction>, 27} 28 29const GROUPS: [&[HeadsUpTool]; 3] = [ 30 &[ 31 HeadsUpTool { 32 key: "zoom_fit", 33 icon: IconId::ZoomToFit, 34 label: strings::HEADS_UP_ZOOM_FIT, 35 action: Some(MenuAction::ZoomFit), 36 }, 37 HeadsUpTool { 38 key: "zoom_area", 39 icon: IconId::ZoomToArea, 40 label: strings::HEADS_UP_ZOOM_AREA, 41 action: None, 42 }, 43 HeadsUpTool { 44 key: "previous_view", 45 icon: IconId::PreviousView, 46 label: strings::HEADS_UP_PREVIOUS_VIEW, 47 action: None, 48 }, 49 HeadsUpTool { 50 key: "section_view", 51 icon: IconId::SectionView, 52 label: strings::HEADS_UP_SECTION_VIEW, 53 action: None, 54 }, 55 ], 56 &[ 57 HeadsUpTool { 58 key: "view_orientation", 59 icon: IconId::ViewOrientation, 60 label: strings::HEADS_UP_VIEW_ORIENTATION, 61 action: None, 62 }, 63 HeadsUpTool { 64 key: "display_style", 65 icon: IconId::DisplayStyle, 66 label: strings::HEADS_UP_DISPLAY_STYLE, 67 action: None, 68 }, 69 HeadsUpTool { 70 key: "hide_show", 71 icon: IconId::HideShowItems, 72 label: strings::HEADS_UP_HIDE_SHOW, 73 action: None, 74 }, 75 ], 76 &[ 77 HeadsUpTool { 78 key: "edit_appearance", 79 icon: IconId::EditAppearance, 80 label: strings::HEADS_UP_EDIT_APPEARANCE, 81 action: None, 82 }, 83 HeadsUpTool { 84 key: "view_settings", 85 icon: IconId::ViewSettings, 86 label: strings::HEADS_UP_VIEW_SETTINGS, 87 action: None, 88 }, 89 ], 90]; 91 92enum StripItem { 93 Separator, 94 Tool(HeadsUpTool), 95} 96 97fn strip_items() -> Vec<StripItem> { 98 GROUPS 99 .iter() 100 .enumerate() 101 .flat_map(|(index, group)| { 102 (index > 0) 103 .then_some(StripItem::Separator) 104 .into_iter() 105 .chain(group.iter().copied().map(StripItem::Tool)) 106 }) 107 .collect() 108} 109 110fn strip_width() -> f32 { 111 let buttons: usize = GROUPS.iter().map(|g| g.len()).sum(); 112 let inner_gaps: usize = GROUPS.iter().map(|g| g.len().saturating_sub(1)).sum(); 113 let separators = GROUPS.len().saturating_sub(1); 114 #[allow( 115 clippy::cast_precision_loss, 116 reason = "heads-up tool counts fit the f32 mantissa" 117 )] 118 let body = 119 buttons as f32 * BUTTON_PX + inner_gaps as f32 * BUTTON_GAP + separators as f32 * GROUP_GAP; 120 2.0 * STRIP_PAD + body 121} 122 123struct StripStyle { 124 hover_fill: bone_ui::theme::Color, 125 separator: bone_ui::theme::Color, 126 radius: bone_ui::theme::Radius, 127} 128 129#[must_use] 130pub fn render_heads_up_toolbar( 131 ctx: &mut FrameCtx<'_>, 132 viewport: LayoutRect, 133 base: WidgetId, 134 paints: &mut Vec<WidgetPaint>, 135) -> Option<MenuAction> { 136 let width = strip_width(); 137 let height = 2.0 * STRIP_PAD + BUTTON_PX; 138 if viewport.size.width.value() < width + 2.0 * STRIP_TOP_INSET 139 || viewport.size.height.value() < height + 2.0 * STRIP_TOP_INSET 140 { 141 return None; 142 } 143 let left = viewport.min_x().value() + (viewport.size.width.value() - width) / 2.0; 144 let top = viewport.min_y().value() + STRIP_TOP_INSET; 145 let strip_rect = LayoutRect::new( 146 LayoutPos::new(LayoutPx::new(left), LayoutPx::new(top)), 147 LayoutSize::new(LayoutPx::new(width), LayoutPx::new(height)), 148 ); 149 ctx.a11y.push( 150 base, 151 strip_rect, 152 AccessNode::new(Role::Toolbar).with_label(strings::HEADS_UP_BAR), 153 ); 154 let theme = ctx.theme(); 155 let style = StripStyle { 156 hover_fill: theme.colors.neutral.step(Step12::HOVER_BG), 157 separator: theme.colors.neutral.step(Step12::SUBTLE_BORDER), 158 radius: theme.radius.sm, 159 }; 160 paints.push(WidgetPaint::Surface { 161 rect: strip_rect, 162 fill: theme.colors.surface(theme.elevation.level2.surface), 163 border: Some(Border { 164 width: StrokeWidth::HAIRLINE, 165 color: style.separator, 166 }), 167 radius: style.radius, 168 elevation: Some(theme.elevation.level2), 169 }); 170 let row_top = top + STRIP_PAD; 171 let (_, clicked) = strip_items().iter().fold( 172 (left + STRIP_PAD, None::<MenuAction>), 173 |(x, clicked), item| match item { 174 StripItem::Separator => { 175 paints.push(WidgetPaint::Surface { 176 rect: LayoutRect::new( 177 LayoutPos::new(LayoutPx::new(x + GROUP_GAP / 2.0), LayoutPx::new(row_top)), 178 LayoutSize::new( 179 LayoutPx::new(StrokeWidth::HAIRLINE.value_px()), 180 LayoutPx::new(BUTTON_PX), 181 ), 182 ), 183 fill: style.separator, 184 border: None, 185 radius: ctx.theme().radius.none, 186 elevation: None, 187 }); 188 (x + GROUP_GAP, clicked) 189 } 190 StripItem::Tool(tool) => { 191 let next = clicked.or_else(|| { 192 draw_tool(ctx, base, tool, x, row_top, &style, paints) 193 .then_some(tool.action) 194 .flatten() 195 }); 196 (x + BUTTON_PX + BUTTON_GAP, next) 197 } 198 }, 199 ); 200 clicked 201} 202 203fn draw_tool( 204 ctx: &mut FrameCtx<'_>, 205 base: WidgetId, 206 tool: &HeadsUpTool, 207 x: f32, 208 row_top: f32, 209 style: &StripStyle, 210 paints: &mut Vec<WidgetPaint>, 211) -> bool { 212 let button_rect = LayoutRect::new( 213 LayoutPos::new(LayoutPx::new(x), LayoutPx::new(row_top)), 214 LayoutSize::new(LayoutPx::new(BUTTON_PX), LayoutPx::new(BUTTON_PX)), 215 ); 216 let interaction = ctx.interact( 217 InteractDeclaration::new( 218 base.child(WidgetKey::new(tool.key)), 219 button_rect, 220 Sense::INTERACTIVE, 221 ) 222 .a11y(AccessNode::new(Role::Button).with_label(tool.label)), 223 ); 224 if interaction.hover() || interaction.pressed() { 225 paints.push(WidgetPaint::Surface { 226 rect: button_rect, 227 fill: style.hover_fill, 228 border: None, 229 radius: style.radius, 230 elevation: None, 231 }); 232 } 233 let inset = (BUTTON_PX - ICON_PX) / 2.0; 234 paints.push(WidgetPaint::Icon { 235 rect: LayoutRect::new( 236 LayoutPos::new(LayoutPx::new(x + inset), LayoutPx::new(row_top + inset)), 237 LayoutSize::new(LayoutPx::new(ICON_PX), LayoutPx::new(ICON_PX)), 238 ), 239 icon: tool.icon, 240 tint: IconTint::Normal, 241 }); 242 interaction.click() 243}