Another project
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}