Another project
0

Configure Feed

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

feat(app): sketch status badge & ribbon overflow

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

author
Lewis
date (May 21, 2026, 10:39 PM +0300) commit 1ce6215b parent 7da78504 change-id ryoprxko
+537 -43
+3 -3
crates/bone-app/src/hotkeys.rs
··· 306 306 if inner.scope != HotkeyScope::Sketch { 307 307 return Ok(()); 308 308 } 309 - let outer = bindings.iter().find(|other| { 310 - other.scope == HotkeyScope::Global && other.chord == inner.chord 311 - }); 309 + let outer = bindings 310 + .iter() 311 + .find(|other| other.scope == HotkeyScope::Global && other.chord == inner.chord); 312 312 match outer { 313 313 None => Ok(()), 314 314 Some(other) => Err(HotkeyTableError::Conflict {
+8 -12
crates/bone-app/src/main.rs
··· 55 55 mod sketch_mode; 56 56 mod smart_dimension; 57 57 mod snap; 58 + mod status_badge; 58 59 mod strings; 59 60 mod tools; 60 61 ··· 159 160 || state.pending_overwrite.is_some() 160 161 || state.pending_discard.is_some() 161 162 || state.shortcut_bar.is_some() 163 + || state.shell.state.ribbon_overflow_open.values().any(|v| *v) 162 164 } 163 165 164 166 #[derive(Copy, Clone, Debug, PartialEq, Eq)] ··· 1708 1710 .try_for_each(|id| builder.mirror_entity(*id, axis))?; 1709 1711 builder.entity_map.insert(axis_id, axis_id); 1710 1712 builder.sketch = symmetric_relations_for_pairs(builder.sketch, &builder.point_map, axis_id)?; 1711 - builder.sketch = 1712 - copy_relations(builder.sketch, source_ids, axis_id, &builder.entity_map)?; 1713 - builder.sketch = 1714 - copy_dimensions(builder.sketch, source_ids, axis_id, &builder.entity_map)?; 1713 + builder.sketch = copy_relations(builder.sketch, source_ids, axis_id, &builder.entity_map)?; 1714 + builder.sketch = copy_dimensions(builder.sketch, source_ids, axis_id, &builder.entity_map)?; 1715 1715 Ok(builder.sketch) 1716 1716 } 1717 1717 ··· 4047 4047 let (sketch, off_a) = tools::add_point(sketch, Point2::from_mm(-2.0, 3.0)); 4048 4048 let (sketch, off_b) = tools::add_point(sketch, Point2::from_mm(2.0, 3.0)); 4049 4049 let (sketch, off_line) = tools::add_line(sketch, off_a, off_b, false); 4050 - let Ok((sketch, _)) = 4051 - sketch.apply(SketchEdit::AddRelation(SketchRelation::Parallel( 4052 - off_line, axis_line, 4053 - ))) 4054 - else { 4050 + let Ok((sketch, _)) = sketch.apply(SketchEdit::AddRelation(SketchRelation::Parallel( 4051 + off_line, axis_line, 4052 + ))) else { 4055 4053 panic!("seed Parallel(off_line, axis_line) must apply"); 4056 4054 }; 4057 4055 let axis_geom = ··· 4064 4062 let parallel_count = mirrored 4065 4063 .relations() 4066 4064 .iter() 4067 - .filter( 4068 - |(_, r)| matches!(r, SketchRelation::Parallel(_, b) if *b == axis_line), 4069 - ) 4065 + .filter(|(_, r)| matches!(r, SketchRelation::Parallel(_, b) if *b == axis_line)) 4070 4066 .count(); 4071 4067 assert_eq!( 4072 4068 parallel_count, 2,
+1 -2
crates/bone-app/src/settings.rs
··· 53 53 #[cfg(target_os = "windows")] 54 54 fn config_base_dir() -> Option<PathBuf> { 55 55 std::env::var_os("APPDATA").map(PathBuf::from).or_else(|| { 56 - std::env::var_os("USERPROFILE") 57 - .map(|p| PathBuf::from(p).join("AppData").join("Roaming")) 56 + std::env::var_os("USERPROFILE").map(|p| PathBuf::from(p).join("AppData").join("Roaming")) 58 57 }) 59 58 } 60 59
+173 -26
crates/bone-app/src/shell.rs
··· 4 4 5 5 use bone_document::{ 6 6 DimensionKind, DimensionValue, Document, Sketch, SketchDimension, SketchEntity, SketchRelation, 7 + SketchStatusReport, SketchVersion, 7 8 }; 8 9 use bone_types::{Length, Point2, SketchDimensionId, SketchEntityId, SketchId}; 9 10 use bone_ui::a11y::{AccessNode, Role}; ··· 19 20 use bone_ui::widgets::GlyphMark; 20 21 use bone_ui::widgets::{ 21 22 AngleEditor, Clipboard, Dialog, DialogButton, HotkeyCapture, HotkeyCaptureState, LabelText, 22 - LengthEditor, MemoryClipboard, MenuBar, MenuBarEntry, MenuBarState, MenuItem, PropertyCell, 23 - PropertyEditor, PropertyGrid, PropertyRow, RenameCommit, Ribbon, RibbonGroup, RibbonIconSize, 24 - RibbonTab, Slider, SliderRange, SliderStep, StatusAlign, StatusBar, StatusItem, Tab, Tabs, 25 - TabsOrientation, ToolbarItem, TreeNode, TreeView, TreeViewState, WidgetPaint, show_dialog, 26 - show_hotkey_capture, show_menu_bar, show_property_grid, show_ribbon, show_slider, 23 + LengthEditor, MemoryClipboard, MenuBar, MenuBarEntry, MenuBarState, MenuItem, PanelState, 24 + PropertyCell, PropertyEditor, PropertyGrid, PropertyRow, RenameCommit, Ribbon, RibbonGroup, 25 + RibbonIconSize, RibbonTab, Slider, SliderRange, SliderStep, StatusAlign, StatusBar, StatusItem, 26 + Tab, Tabs, TabsOrientation, ToolbarItem, TreeNode, TreeView, TreeViewState, WidgetPaint, 27 + show_dialog, show_hotkey_capture, show_menu_bar, show_property_grid, show_ribbon, show_slider, 27 28 show_status_bar, show_tabs, show_tree_view, 28 29 }; 29 30 use bone_ui::{WidgetId, WidgetKey}; ··· 37 38 use crate::sketch_mode::PendingDimension; 38 39 use crate::sketch_mode::{Mode, Plane, SketchTool}; 39 40 use crate::smart_dimension; 41 + use crate::status_badge::{ 42 + render_status_panel, status_badge_widget_id, status_color, status_label_key, 43 + status_panel_widget_id, 44 + }; 40 45 use crate::strings; 41 46 42 47 const RIBBON_GROUP_PADDING_PX: f32 = 8.0; ··· 46 51 const STATUS_MODE_WIDTH: LayoutPx = LayoutPx::new(220.0); 47 52 const STATUS_UNITS_WIDTH: LayoutPx = LayoutPx::new(80.0); 48 53 const STATUS_COORDS_WIDTH: LayoutPx = LayoutPx::new(180.0); 54 + const STATUS_STATUS_WIDTH: LayoutPx = LayoutPx::new(180.0); 49 55 50 56 #[derive(Copy, Clone, Debug, PartialEq, Eq)] 51 57 struct ShellPanels { ··· 239 245 } 240 246 241 247 #[derive(Default)] 248 + #[allow( 249 + clippy::struct_excessive_bools, 250 + reason = "shell aggregates independent dialog and panel toggles" 251 + )] 242 252 pub struct ShellState { 243 253 pub feature_tree: TreeViewState, 244 254 pub clipboard: MemoryClipboard, ··· 249 259 pub hotkey_capture: BTreeMap<bone_ui::hotkey::ActionId, HotkeyCaptureState>, 250 260 pub left_pane: LeftPane, 251 261 last_left_pane_interesting: bool, 262 + pub status_panel_open: bool, 263 + pub status_panel: PanelState, 264 + status_cache: Option<(SketchVersion, SketchStatusReport)>, 265 + pub ribbon_overflow_open: BTreeMap<WidgetId, bool>, 252 266 } 253 267 254 268 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] ··· 421 435 selection: entity_ids, 422 436 }, 423 437 &mut paints, 438 + &mut popover_paints, 439 + &mut self.state.ribbon_overflow_open, 424 440 ); 425 441 let active_tool = match mode { 426 442 Mode::Sketch { session, .. } => session.tool, ··· 465 481 &mut paints, 466 482 ); 467 483 render_doc_tabs(ctx, doc_tabs_rect, &self.ids, &mut paints); 468 - render_status_bar( 484 + let status_report: Option<&SketchStatusReport> = if let Some(s) = active_sketch { 485 + let v = s.version(); 486 + if self 487 + .state 488 + .status_cache 489 + .as_ref() 490 + .is_none_or(|(cv, _)| *cv != v) 491 + { 492 + self.state.status_cache = Some((v, s.status())); 493 + } 494 + self.state.status_cache.as_ref().map(|(_, r)| r) 495 + } else { 496 + self.state.status_cache = None; 497 + None 498 + }; 499 + let status_badge_id = status_badge_widget_id(self.ids.status_bar); 500 + let status_activated = render_status_bar( 469 501 ctx, 470 502 status_rect, 471 503 self.ids.status_bar, 472 504 mode, 473 505 document, 474 506 cursor_world, 507 + status_report, 508 + status_badge_id, 475 509 &mut paints, 476 510 ); 511 + if status_activated { 512 + self.state.status_panel_open = !self.state.status_panel_open; 513 + } 514 + if status_report.is_none_or(|r| r.offending().is_empty()) { 515 + self.state.status_panel_open = false; 516 + } 517 + if self.state.status_panel_open { 518 + if let (Some(report), Some(sketch)) = (status_report, active_sketch) { 519 + render_status_panel( 520 + ctx, 521 + status_panel_widget_id(self.ids.status_bar), 522 + &mut self.state.status_panel, 523 + status_rect, 524 + report, 525 + sketch, 526 + &mut popover_paints, 527 + ); 528 + } else { 529 + self.state.status_panel_open = false; 530 + } 531 + } 477 532 let confirm = 478 533 render_confirm_corner(ctx, viewport_rect, &self.ids, mode.is_sketch(), &mut paints); 479 534 let confirm_action = confirm; ··· 1196 1251 ctx: &mut FrameCtx<'_>, 1197 1252 inputs: RibbonInputs<'_>, 1198 1253 paints: &mut Vec<WidgetPaint>, 1254 + popover_paints: &mut Vec<WidgetPaint>, 1255 + overflow_open: &mut BTreeMap<WidgetId, bool>, 1199 1256 ) -> Option<WidgetId> { 1200 1257 let RibbonInputs { 1201 1258 rect, ··· 1243 1300 .map(|item| size_item(item, small_min)) 1244 1301 .collect(); 1245 1302 let tab_id = ribbon.child(WidgetKey::new("tab.sketch")); 1303 + let groups = build_sketch_groups( 1304 + ribbon, 1305 + entity_items, 1306 + relation_items, 1307 + dimension_items, 1308 + large_min, 1309 + small_min, 1310 + overflow_open, 1311 + ); 1312 + let placeholder_tab = |key: &'static str, label: StringKey| { 1313 + RibbonTab::new(ribbon.child(WidgetKey::new(key)), label, Vec::new()).disabled(true) 1314 + }; 1315 + let tabs = [ 1316 + placeholder_tab("tab.features", strings::RIBBON_TAB_FEATURES), 1317 + RibbonTab::new(tab_id, strings::RIBBON_TAB_SKETCH, groups), 1318 + placeholder_tab("tab.surfaces", strings::RIBBON_TAB_SURFACES), 1319 + placeholder_tab("tab.evaluate", strings::RIBBON_TAB_EVALUATE), 1320 + ]; 1321 + let pointer_pressed = !ctx.input.buttons_pressed.is_empty(); 1322 + let response = show_ribbon( 1323 + ctx, 1324 + Ribbon::new(ribbon, rect, strings::RIBBON_LABEL, &tabs, tab_id), 1325 + ); 1326 + process_ribbon_response( 1327 + response, 1328 + paints, 1329 + popover_paints, 1330 + overflow_open, 1331 + pointer_pressed, 1332 + ) 1333 + } 1334 + 1335 + fn build_sketch_groups( 1336 + ribbon: WidgetId, 1337 + entity_items: Vec<ToolbarItem>, 1338 + relation_items: Vec<ToolbarItem>, 1339 + dimension_items: Vec<ToolbarItem>, 1340 + large_min: LayoutPx, 1341 + small_min: LayoutPx, 1342 + overflow_open: &BTreeMap<WidgetId, bool>, 1343 + ) -> Vec<RibbonGroup> { 1246 1344 let dimensions_preferred = group_width_for(&dimension_items, large_min); 1247 - let groups: Vec<RibbonGroup> = vec![ 1345 + let entities_id = ribbon.child(WidgetKey::new("group.entities")); 1346 + let relations_id = ribbon.child(WidgetKey::new("group.relations")); 1347 + let dimensions_id = ribbon.child(WidgetKey::new("group.dimensions")); 1348 + let open_of = |id: WidgetId| overflow_open.get(&id).copied().unwrap_or(false); 1349 + vec![ 1248 1350 RibbonGroup { 1249 - id: ribbon.child(WidgetKey::new("group.entities")), 1351 + id: entities_id, 1250 1352 label: strings::RIBBON_GROUP_ENTITIES, 1251 1353 min_width: group_min_width(large_min, entity_items.len()), 1252 1354 width: group_width_for(&entity_items, large_min), 1253 1355 items: entity_items, 1254 1356 icon_size: RibbonIconSize::Large, 1357 + overflow_open: open_of(entities_id), 1358 + overflow_label: Some(strings::TOOLBAR_OVERFLOW), 1255 1359 }, 1256 1360 RibbonGroup { 1257 - id: ribbon.child(WidgetKey::new("group.relations")), 1361 + id: relations_id, 1258 1362 label: strings::RIBBON_GROUP_RELATIONS, 1259 1363 min_width: group_min_width(small_min, relation_items.len()), 1260 1364 width: group_width_for(&relation_items, small_min), 1261 1365 items: relation_items, 1262 1366 icon_size: RibbonIconSize::Small, 1367 + overflow_open: open_of(relations_id), 1368 + overflow_label: Some(strings::TOOLBAR_OVERFLOW), 1263 1369 }, 1264 1370 RibbonGroup { 1265 - id: ribbon.child(WidgetKey::new("group.dimensions")), 1371 + id: dimensions_id, 1266 1372 label: strings::RIBBON_GROUP_DIMENSIONS, 1267 1373 min_width: dimensions_preferred, 1268 1374 width: dimensions_preferred, 1269 1375 items: dimension_items, 1270 1376 icon_size: RibbonIconSize::Large, 1377 + overflow_open: open_of(dimensions_id), 1378 + overflow_label: Some(strings::TOOLBAR_OVERFLOW), 1271 1379 }, 1272 - ]; 1273 - let placeholder_tab = |key: &'static str, label: StringKey| { 1274 - RibbonTab::new(ribbon.child(WidgetKey::new(key)), label, Vec::new()).disabled(true) 1275 - }; 1276 - let tabs = [ 1277 - placeholder_tab("tab.features", strings::RIBBON_TAB_FEATURES), 1278 - RibbonTab::new(tab_id, strings::RIBBON_TAB_SKETCH, groups), 1279 - placeholder_tab("tab.surfaces", strings::RIBBON_TAB_SURFACES), 1280 - placeholder_tab("tab.evaluate", strings::RIBBON_TAB_EVALUATE), 1281 - ]; 1282 - let response = show_ribbon( 1283 - ctx, 1284 - Ribbon::new(ribbon, rect, strings::RIBBON_LABEL, &tabs, tab_id), 1285 - ); 1380 + ] 1381 + } 1382 + 1383 + fn process_ribbon_response( 1384 + response: bone_ui::widgets::RibbonResponse, 1385 + paints: &mut Vec<WidgetPaint>, 1386 + popover_paints: &mut Vec<WidgetPaint>, 1387 + overflow_open: &mut BTreeMap<WidgetId, bool>, 1388 + pointer_pressed: bool, 1389 + ) -> Option<WidgetId> { 1286 1390 paints.extend(response.paint); 1391 + popover_paints.extend(response.popover_paint); 1392 + response.overflow_toggled.iter().for_each(|id| { 1393 + let entry = overflow_open.entry(*id).or_insert(false); 1394 + *entry = !*entry; 1395 + }); 1396 + if let Some(toggled_id) = response.overflow_toggled.first().copied() 1397 + && overflow_open.get(&toggled_id).copied().unwrap_or(false) 1398 + { 1399 + overflow_open 1400 + .iter_mut() 1401 + .filter(|(k, _)| **k != toggled_id) 1402 + .for_each(|(_, v)| *v = false); 1403 + } 1404 + let any_open = overflow_open.values().any(|v| *v); 1405 + let activated_anything = response.activated_tool.is_some(); 1406 + let outside_click = pointer_pressed 1407 + && any_open 1408 + && response.overflow_toggled.is_empty() 1409 + && !response.popup_consumed_click; 1410 + if activated_anything || outside_click { 1411 + overflow_open.values_mut().for_each(|v| *v = false); 1412 + } 1287 1413 response.activated_tool 1288 1414 } 1289 1415 ··· 1918 2044 paints.extend(response.paint); 1919 2045 } 1920 2046 2047 + #[allow( 2048 + clippy::too_many_arguments, 2049 + reason = "status bar bundles mode + cursor + status diagnostics in one render pass" 2050 + )] 1921 2051 fn render_status_bar( 1922 2052 ctx: &mut FrameCtx<'_>, 1923 2053 rect: LayoutRect, ··· 1925 2055 mode: &Mode, 1926 2056 document: &Document, 1927 2057 cursor_world: Option<Point2>, 2058 + status_report: Option<&SketchStatusReport>, 2059 + status_badge_id: WidgetId, 1928 2060 paints: &mut Vec<WidgetPaint>, 1929 - ) { 2061 + ) -> bool { 1930 2062 if rect.size.width.value() <= 0.0 || rect.size.height.value() <= 0.0 { 1931 - return; 2063 + return false; 1932 2064 } 1933 2065 let mode_label = mode_status_label(ctx.strings, mode, document); 1934 2066 let mode_item = StatusItem::with_text( ··· 1957 2089 }) 1958 2090 }) 1959 2091 .flatten(); 2092 + let status_item = status_report.map(|report| { 2093 + let has_panel_content = !report.offending().is_empty(); 2094 + StatusItem::new( 2095 + status_badge_id, 2096 + status_label_key(report.status()), 2097 + StatusAlign::End, 2098 + STATUS_STATUS_WIDTH, 2099 + ) 2100 + .interactive(has_panel_content) 2101 + .badge(status_color(report.status(), &ctx.theme().cad)) 2102 + }); 1960 2103 let mut items: Vec<StatusItem> = vec![mode_item]; 1961 2104 if let Some(coords) = coords_item { 1962 2105 items.push(coords); 1963 2106 } 2107 + if let Some(status) = status_item { 2108 + items.push(status); 2109 + } 1964 2110 items.push(units_item); 1965 2111 let response = show_status_bar( 1966 2112 ctx, 1967 2113 StatusBar::new(id, rect, strings::STATUS_BAR_LABEL, &items), 1968 2114 ); 1969 2115 paints.extend(response.paint); 2116 + response.activated == Some(status_badge_id) 1970 2117 } 1971 2118 1972 2119 fn mode_status_label(strings_table: &StringTable, mode: &Mode, document: &Document) -> LabelText {
+295
crates/bone-app/src/status_badge.rs
··· 1 + use bone_document::{Sketch, SketchEntityKind, SketchRelation, SketchStatusReport}; 2 + use bone_types::{SketchDimensionId, SketchEntityId, SketchItemId, SketchRelationId, SketchStatus}; 3 + use bone_ui::frame::FrameCtx; 4 + use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 5 + use bone_ui::strings::{StringKey, StringTable}; 6 + use bone_ui::theme::{CadColors, Color}; 7 + use bone_ui::widgets::{ 8 + LabelText, Panel, PanelState, PanelTitlebar, PanelVariant, WidgetPaint, show_panel, 9 + }; 10 + use bone_ui::{WidgetId, WidgetKey}; 11 + 12 + use crate::strings; 13 + 14 + const STATUS_PANEL_WIDTH_PX: f32 = 280.0; 15 + const STATUS_PANEL_TITLE_HEIGHT_PX: f32 = 28.0; 16 + const STATUS_PANEL_ROW_HEIGHT_PX: f32 = 22.0; 17 + const STATUS_PANEL_PADDING_PX: f32 = 8.0; 18 + const STATUS_PANEL_MAX_ROWS: usize = 12; 19 + const STATUS_PANEL_GAP_PX: f32 = 6.0; 20 + 21 + #[must_use] 22 + pub fn status_label_key(status: SketchStatus) -> StringKey { 23 + match status { 24 + SketchStatus::UnderDefined => strings::STATUS_SKETCH_UNDER_DEFINED, 25 + SketchStatus::FullyDefined => strings::STATUS_SKETCH_FULLY_DEFINED, 26 + SketchStatus::OverDefined => strings::STATUS_SKETCH_OVER_DEFINED, 27 + SketchStatus::NoSolutionFound => strings::STATUS_SKETCH_NO_SOLUTION, 28 + SketchStatus::InvalidSolutionFound => strings::STATUS_SKETCH_INVALID, 29 + SketchStatus::Dangling => strings::STATUS_SKETCH_DANGLING, 30 + } 31 + } 32 + 33 + #[must_use] 34 + pub fn status_color(status: SketchStatus, cad: &CadColors) -> Color { 35 + match status { 36 + SketchStatus::UnderDefined => cad.sketch_under_defined, 37 + SketchStatus::FullyDefined => cad.sketch_fully_defined, 38 + SketchStatus::OverDefined 39 + | SketchStatus::NoSolutionFound 40 + | SketchStatus::InvalidSolutionFound => cad.sketch_over_defined, 41 + SketchStatus::Dangling => cad.sketch_dangling, 42 + } 43 + } 44 + 45 + #[must_use] 46 + pub fn status_panel_rect(status_bar: LayoutRect, row_count: usize) -> LayoutRect { 47 + let rows = row_count.clamp(1, STATUS_PANEL_MAX_ROWS); 48 + #[allow( 49 + clippy::cast_precision_loss, 50 + reason = "row count is bounded by STATUS_PANEL_MAX_ROWS" 51 + )] 52 + let body_height = rows as f32 * STATUS_PANEL_ROW_HEIGHT_PX + 2.0 * STATUS_PANEL_PADDING_PX; 53 + let panel_height = STATUS_PANEL_TITLE_HEIGHT_PX + body_height; 54 + let panel_width = STATUS_PANEL_WIDTH_PX; 55 + let bar_right = status_bar.origin.x.value() + status_bar.size.width.value(); 56 + let origin_x = (bar_right - panel_width).max(status_bar.origin.x.value()); 57 + let origin_y = status_bar.origin.y.value() - panel_height - STATUS_PANEL_GAP_PX; 58 + LayoutRect::new( 59 + LayoutPos::new(LayoutPx::new(origin_x), LayoutPx::new(origin_y.max(0.0))), 60 + LayoutSize::new(LayoutPx::new(panel_width), LayoutPx::new(panel_height)), 61 + ) 62 + } 63 + 64 + pub fn render_status_panel( 65 + ctx: &mut FrameCtx<'_>, 66 + panel_id: WidgetId, 67 + panel_state: &mut PanelState, 68 + status_bar_rect: LayoutRect, 69 + report: &SketchStatusReport, 70 + sketch: &Sketch, 71 + paints: &mut Vec<WidgetPaint>, 72 + ) { 73 + let lines = compose_panel_lines(report, sketch, ctx.strings); 74 + let rect = status_panel_rect(status_bar_rect, lines.len()); 75 + let response = show_panel( 76 + ctx, 77 + Panel::new(panel_id, rect, panel_state) 78 + .variant(PanelVariant::Card) 79 + .titlebar(PanelTitlebar { 80 + label: strings::STATUS_PANEL_TITLE, 81 + height: LayoutPx::new(STATUS_PANEL_TITLE_HEIGHT_PX), 82 + collapsible: false, 83 + }), 84 + ); 85 + paints.extend(response.paint); 86 + let Some(body) = response.body_rect else { 87 + return; 88 + }; 89 + lines.into_iter().enumerate().for_each(|(i, line)| { 90 + #[allow( 91 + clippy::cast_precision_loss, 92 + reason = "row index bounded by STATUS_PANEL_MAX_ROWS" 93 + )] 94 + let y = body.origin.y.value() 95 + + STATUS_PANEL_PADDING_PX 96 + + (i as f32) * STATUS_PANEL_ROW_HEIGHT_PX; 97 + paints.push(WidgetPaint::Label { 98 + rect: LayoutRect::new( 99 + LayoutPos::new( 100 + LayoutPx::new(body.origin.x.value() + STATUS_PANEL_PADDING_PX), 101 + LayoutPx::new(y), 102 + ), 103 + LayoutSize::new( 104 + LayoutPx::saturating_nonneg( 105 + body.size.width.value() - 2.0 * STATUS_PANEL_PADDING_PX, 106 + ), 107 + LayoutPx::new(STATUS_PANEL_ROW_HEIGHT_PX), 108 + ), 109 + ), 110 + text: line, 111 + color: ctx.theme().colors.text_primary(), 112 + role: ctx.theme().typography.body, 113 + }); 114 + }); 115 + } 116 + 117 + fn compose_panel_lines( 118 + report: &SketchStatusReport, 119 + sketch: &Sketch, 120 + strings_table: &StringTable, 121 + ) -> Vec<LabelText> { 122 + let total = report.offending().len(); 123 + if total == 0 { 124 + return vec![LabelText::Key(strings::STATUS_PANEL_EMPTY)]; 125 + } 126 + if total <= STATUS_PANEL_MAX_ROWS { 127 + return report 128 + .offending() 129 + .iter() 130 + .map(|item| offending_label(*item, sketch, strings_table)) 131 + .collect(); 132 + } 133 + let visible = STATUS_PANEL_MAX_ROWS - 1; 134 + let rest = total - visible; 135 + let head = report 136 + .offending() 137 + .iter() 138 + .take(visible) 139 + .map(|item| offending_label(*item, sketch, strings_table)); 140 + let more_template = strings_table.resolve(strings::STATUS_PANEL_MORE); 141 + let more_line = LabelText::Owned(more_template.replace("{n}", &rest.to_string())); 142 + head.chain(core::iter::once(more_line)).collect() 143 + } 144 + 145 + fn offending_label(item: SketchItemId, sketch: &Sketch, strings_table: &StringTable) -> LabelText { 146 + let (kind_key, ordinal) = match item { 147 + SketchItemId::Relation(id) => ( 148 + sketch 149 + .relations() 150 + .get(id) 151 + .map(|rel| relation_label_key(*rel)), 152 + relation_ordinal(sketch, id), 153 + ), 154 + SketchItemId::Dimension(id) => ( 155 + sketch 156 + .dimensions() 157 + .get(id) 158 + .map(|dim| dimension_label_key(*dim)), 159 + dimension_ordinal(sketch, id), 160 + ), 161 + SketchItemId::Entity(id) => ( 162 + sketch 163 + .entities() 164 + .get(id) 165 + .map(|entity| entity_label_key(entity.kind())), 166 + entity_ordinal(sketch, id), 167 + ), 168 + }; 169 + let kind_str = strings_table.resolve(kind_key.unwrap_or(strings::STATUS_PANEL_KIND_UNKNOWN)); 170 + match ordinal { 171 + Some(o) => LabelText::Owned(format!("{kind_str} #{}", o + 1)), 172 + None => LabelText::Owned(kind_str.to_owned()), 173 + } 174 + } 175 + 176 + fn relation_ordinal(sketch: &Sketch, id: SketchRelationId) -> Option<usize> { 177 + sketch.relation_order().iter().position(|x| *x == id) 178 + } 179 + 180 + fn dimension_ordinal(sketch: &Sketch, id: SketchDimensionId) -> Option<usize> { 181 + sketch.dimension_order().iter().position(|x| *x == id) 182 + } 183 + 184 + fn entity_ordinal(sketch: &Sketch, id: SketchEntityId) -> Option<usize> { 185 + sketch.entity_order().iter().position(|x| *x == id) 186 + } 187 + 188 + fn relation_label_key(rel: SketchRelation) -> StringKey { 189 + match rel { 190 + SketchRelation::Coincident(_, _) => strings::TOOL_COINCIDENT, 191 + SketchRelation::Horizontal(_) => strings::TOOL_HORIZONTAL, 192 + SketchRelation::Vertical(_) => strings::TOOL_VERTICAL, 193 + SketchRelation::Parallel(_, _) => strings::TOOL_PARALLEL, 194 + SketchRelation::Perpendicular(_, _) => strings::TOOL_PERPENDICULAR, 195 + SketchRelation::Tangent(_, _) => strings::TOOL_TANGENT, 196 + SketchRelation::Equal(_, _) => strings::TOOL_EQUAL, 197 + SketchRelation::Concentric(_, _) => strings::TOOL_CONCENTRIC, 198 + SketchRelation::Midpoint { .. } => strings::TOOL_MIDPOINT, 199 + SketchRelation::Symmetric { .. } => strings::TOOL_SYMMETRIC, 200 + SketchRelation::Fix(_) => strings::TOOL_FIX, 201 + } 202 + } 203 + 204 + fn dimension_label_key(dim: bone_document::SketchDimension) -> StringKey { 205 + use bone_document::SketchDimension as Dim; 206 + match dim { 207 + Dim::Linear { .. } => strings::STATUS_PANEL_KIND_LINEAR, 208 + Dim::Angular { .. } => strings::STATUS_PANEL_KIND_ANGULAR, 209 + Dim::Radius { .. } => strings::STATUS_PANEL_KIND_RADIUS, 210 + Dim::Diameter { .. } => strings::STATUS_PANEL_KIND_DIAMETER, 211 + } 212 + } 213 + 214 + fn entity_label_key(kind: SketchEntityKind) -> StringKey { 215 + match kind { 216 + SketchEntityKind::Point => strings::STATUS_PANEL_KIND_POINT, 217 + SketchEntityKind::Line => strings::STATUS_PANEL_KIND_LINE, 218 + SketchEntityKind::Arc => strings::STATUS_PANEL_KIND_ARC, 219 + SketchEntityKind::Circle => strings::STATUS_PANEL_KIND_CIRCLE, 220 + } 221 + } 222 + 223 + #[must_use] 224 + pub fn status_badge_widget_id(parent: WidgetId) -> WidgetId { 225 + parent.child(WidgetKey::new("status.badge")) 226 + } 227 + 228 + #[must_use] 229 + pub fn status_panel_widget_id(parent: WidgetId) -> WidgetId { 230 + parent.child(WidgetKey::new("status.panel")) 231 + } 232 + 233 + #[cfg(test)] 234 + mod tests { 235 + use super::{SketchStatus, status_color, status_label_key}; 236 + use crate::strings; 237 + use bone_ui::theme::Theme; 238 + 239 + #[test] 240 + fn each_status_maps_to_expected_label_key() { 241 + assert_eq!( 242 + status_label_key(SketchStatus::UnderDefined), 243 + strings::STATUS_SKETCH_UNDER_DEFINED 244 + ); 245 + assert_eq!( 246 + status_label_key(SketchStatus::FullyDefined), 247 + strings::STATUS_SKETCH_FULLY_DEFINED 248 + ); 249 + assert_eq!( 250 + status_label_key(SketchStatus::OverDefined), 251 + strings::STATUS_SKETCH_OVER_DEFINED 252 + ); 253 + assert_eq!( 254 + status_label_key(SketchStatus::NoSolutionFound), 255 + strings::STATUS_SKETCH_NO_SOLUTION 256 + ); 257 + assert_eq!( 258 + status_label_key(SketchStatus::InvalidSolutionFound), 259 + strings::STATUS_SKETCH_INVALID 260 + ); 261 + assert_eq!( 262 + status_label_key(SketchStatus::Dangling), 263 + strings::STATUS_SKETCH_DANGLING 264 + ); 265 + } 266 + 267 + #[test] 268 + fn over_no_solution_and_invalid_share_red_token() { 269 + let cad = Theme::light().cad; 270 + assert_eq!( 271 + status_color(SketchStatus::OverDefined, &cad), 272 + cad.sketch_over_defined 273 + ); 274 + assert_eq!( 275 + status_color(SketchStatus::NoSolutionFound, &cad), 276 + cad.sketch_over_defined 277 + ); 278 + assert_eq!( 279 + status_color(SketchStatus::InvalidSolutionFound, &cad), 280 + cad.sketch_over_defined 281 + ); 282 + assert_eq!( 283 + status_color(SketchStatus::Dangling, &cad), 284 + cad.sketch_dangling 285 + ); 286 + assert_eq!( 287 + status_color(SketchStatus::UnderDefined, &cad), 288 + cad.sketch_under_defined 289 + ); 290 + assert_eq!( 291 + status_color(SketchStatus::FullyDefined, &cad), 292 + cad.sketch_fully_defined 293 + ); 294 + } 295 + }
+57
crates/bone-app/src/strings.rs
··· 89 89 pub const STATUS_READY: StringKey = StringKey::new("status.ready"); 90 90 pub const STATUS_SKETCH_ACTIVE: StringKey = StringKey::new("status.sketch_active"); 91 91 pub const STATUS_UNITS_MM: StringKey = StringKey::new("status.units.mm"); 92 + pub const STATUS_SKETCH_UNDER_DEFINED: StringKey = StringKey::new("status.sketch.under_defined"); 93 + pub const STATUS_SKETCH_FULLY_DEFINED: StringKey = StringKey::new("status.sketch.fully_defined"); 94 + pub const STATUS_SKETCH_OVER_DEFINED: StringKey = StringKey::new("status.sketch.over_defined"); 95 + pub const STATUS_SKETCH_NO_SOLUTION: StringKey = StringKey::new("status.sketch.no_solution"); 96 + pub const STATUS_SKETCH_INVALID: StringKey = StringKey::new("status.sketch.invalid"); 97 + pub const STATUS_SKETCH_DANGLING: StringKey = StringKey::new("status.sketch.dangling"); 98 + pub const STATUS_PANEL_TITLE: StringKey = StringKey::new("status.panel.title"); 99 + pub const STATUS_PANEL_EMPTY: StringKey = StringKey::new("status.panel.empty"); 100 + pub const STATUS_PANEL_KIND_POINT: StringKey = StringKey::new("status.panel.kind.point"); 101 + pub const STATUS_PANEL_KIND_LINE: StringKey = StringKey::new("status.panel.kind.line"); 102 + pub const STATUS_PANEL_KIND_ARC: StringKey = StringKey::new("status.panel.kind.arc"); 103 + pub const STATUS_PANEL_KIND_CIRCLE: StringKey = StringKey::new("status.panel.kind.circle"); 104 + pub const STATUS_PANEL_KIND_LINEAR: StringKey = StringKey::new("status.panel.kind.linear"); 105 + pub const STATUS_PANEL_KIND_ANGULAR: StringKey = StringKey::new("status.panel.kind.angular"); 106 + pub const STATUS_PANEL_KIND_RADIUS: StringKey = StringKey::new("status.panel.kind.radius"); 107 + pub const STATUS_PANEL_KIND_DIAMETER: StringKey = StringKey::new("status.panel.kind.diameter"); 108 + pub const STATUS_PANEL_KIND_UNKNOWN: StringKey = StringKey::new("status.panel.kind.unknown"); 109 + pub const STATUS_PANEL_MORE: StringKey = StringKey::new("status.panel.more"); 110 + pub const TOOLBAR_OVERFLOW: StringKey = StringKey::new("toolbar.overflow"); 92 111 93 112 pub const MENU_BAR_LABEL: StringKey = StringKey::new("shell.menu_bar"); 94 113 pub const MENU_FILE: StringKey = StringKey::new("menu.file"); ··· 303 322 (STATUS_READY, "Ready"), 304 323 (STATUS_SKETCH_ACTIVE, "Editing"), 305 324 (STATUS_UNITS_MM, "MMGS"), 325 + (STATUS_SKETCH_UNDER_DEFINED, "Under Defined"), 326 + (STATUS_SKETCH_FULLY_DEFINED, "Fully Defined"), 327 + (STATUS_SKETCH_OVER_DEFINED, "Over Defined"), 328 + (STATUS_SKETCH_NO_SOLUTION, "No Solution Found"), 329 + (STATUS_SKETCH_INVALID, "Invalid Solution Found"), 330 + (STATUS_SKETCH_DANGLING, "Dangling"), 331 + (STATUS_PANEL_TITLE, "Sketch Diagnostics"), 332 + (STATUS_PANEL_EMPTY, "No conflicting constraints."), 333 + (STATUS_PANEL_KIND_POINT, "Point"), 334 + (STATUS_PANEL_KIND_LINE, "Line"), 335 + (STATUS_PANEL_KIND_ARC, "Arc"), 336 + (STATUS_PANEL_KIND_CIRCLE, "Circle"), 337 + (STATUS_PANEL_KIND_LINEAR, "Linear Dimension"), 338 + (STATUS_PANEL_KIND_ANGULAR, "Angular Dimension"), 339 + (STATUS_PANEL_KIND_RADIUS, "Radius Dimension"), 340 + (STATUS_PANEL_KIND_DIAMETER, "Diameter Dimension"), 341 + (STATUS_PANEL_KIND_UNKNOWN, "Unknown"), 342 + (STATUS_PANEL_MORE, "+ {n} more"), 343 + (TOOLBAR_OVERFLOW, "More tools"), 306 344 (MENU_BAR_LABEL, "Menu Bar"), 307 345 (MENU_FILE, "File"), 308 346 (MENU_EDIT, "Edit"), ··· 521 559 (STATUS_READY, "[!! Réady !!]"), 522 560 (STATUS_SKETCH_ACTIVE, "[!! Edîting !!]"), 523 561 (STATUS_UNITS_MM, "[!! MMGS !!]"), 562 + (STATUS_SKETCH_UNDER_DEFINED, "[!! Ûnder Defîned !!]"), 563 + (STATUS_SKETCH_FULLY_DEFINED, "[!! Fûlly Defîned !!]"), 564 + (STATUS_SKETCH_OVER_DEFINED, "[!! Ôver Defîned !!]"), 565 + (STATUS_SKETCH_NO_SOLUTION, "[!! Nô Solûtion Foûnd !!]"), 566 + (STATUS_SKETCH_INVALID, "[!! Invâlid Solûtion Foûnd !!]"), 567 + (STATUS_SKETCH_DANGLING, "[!! Dânglîng !!]"), 568 + (STATUS_PANEL_TITLE, "[!! Skêtch Diâgnostics !!]"), 569 + (STATUS_PANEL_EMPTY, "[!! Nô conflîcting constrâints !!]"), 570 + (STATUS_PANEL_KIND_POINT, "[!! Pôint !!]"), 571 + (STATUS_PANEL_KIND_LINE, "[!! Lîne !!]"), 572 + (STATUS_PANEL_KIND_ARC, "[!! Ârc !!]"), 573 + (STATUS_PANEL_KIND_CIRCLE, "[!! Cîrcle !!]"), 574 + (STATUS_PANEL_KIND_LINEAR, "[!! Lîneâr Dimênsion !!]"), 575 + (STATUS_PANEL_KIND_ANGULAR, "[!! Ângulâr Dimênsion !!]"), 576 + (STATUS_PANEL_KIND_RADIUS, "[!! Râdius Dimênsion !!]"), 577 + (STATUS_PANEL_KIND_DIAMETER, "[!! Diâmeter Dimênsion !!]"), 578 + (STATUS_PANEL_KIND_UNKNOWN, "[!! Ûnknôwn !!]"), 579 + (STATUS_PANEL_MORE, "[!! + {n} môre !!]"), 580 + (TOOLBAR_OVERFLOW, "[!! Môre tôols !!]"), 524 581 (MENU_BAR_LABEL, "[!! Mênu Bâr !!]"), 525 582 (MENU_FILE, "[!! Fîle !!]"), 526 583 (MENU_EDIT, "[!! Édit !!]"),