Another project
0

Configure Feed

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

feat(app): sketch tools + strings + chrome wiring in main

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

author
Lewis
date (May 8, 2026, 12:13 PM +0300) commit 2b4af203 parent b629525e change-id xxwtszzp
+668 -64
+33
crates/bone-app/src/chrome.rs
··· 1 + use bone_render::ChromeInstance; 2 + use bone_ui::layout::LayoutRect; 3 + use bone_ui::theme::{Color, Theme}; 4 + use bone_ui::widgets::{PaintPrim, WidgetPaint, lower_paint}; 5 + 6 + #[must_use] 7 + pub fn paint_to_instances(theme: &Theme, paints: &[WidgetPaint]) -> Vec<ChromeInstance> { 8 + paints 9 + .iter() 10 + .map(|p| prim_to_instance(&lower_paint(theme, p))) 11 + .collect() 12 + } 13 + 14 + fn prim_to_instance(prim: &PaintPrim) -> ChromeInstance { 15 + let (border_color, thickness_px) = prim 16 + .border 17 + .map_or((Color::TRANSPARENT, 0.0), |b| (b.color, b.width.value_px())); 18 + ChromeInstance::new( 19 + rect_to_xywh(prim.rect), 20 + prim.fill.linear_rgba_premul(), 21 + border_color.linear_rgba_premul(), 22 + [thickness_px, prim.radius.value_px()], 23 + ) 24 + } 25 + 26 + fn rect_to_xywh(rect: LayoutRect) -> [f32; 4] { 27 + [ 28 + rect.origin.x.value(), 29 + rect.origin.y.value(), 30 + rect.size.width.value(), 31 + rect.size.height.value(), 32 + ] 33 + }
+365 -64
crates/bone-app/src/main.rs
··· 1 1 use std::path::{Path, PathBuf}; 2 2 use std::sync::Arc; 3 3 4 - use bone_document::{EditOutcome, Sketch, SketchEdit, SketchEntity}; 4 + use bone_document::{Document, EditOutcome, Sketch, SketchEdit, SketchEntity}; 5 5 use bone_render::{ 6 - Camera2, PixelsPerMm, SketchRenderer, SketchScene, Style, SurfaceContext, ViewportExtent, 7 - ViewportPx, 6 + Camera2, ChromeInstance, ChromePipeline, PixelsPerMm, SketchRenderer, SketchScene, Style, 7 + SurfaceContext, ViewportExtent, ViewportPx, 8 8 }; 9 9 use bone_types::{ 10 - Length, Point2, Point3, SketchEntityId, SketchPlaneBasis, Tolerance, UnitVec3, Vec2, 10 + DocumentId, Length, Point2, Point3, SketchEntityId, SketchId, SketchPlaneBasis, Tolerance, 11 + UnitVec3, Vec2, 11 12 }; 13 + use bone_ui::a11y::AccessTreeBuilder; 14 + use bone_ui::focus::FocusManager; 15 + use bone_ui::frame::FrameCtx; 12 16 use bone_ui::gallery::{GALLERY_CANVAS, GalleryState, render}; 17 + use bone_ui::hit_test::{HitFrame, HitState, resolve}; 18 + use bone_ui::hotkey::HotkeyTable; 19 + use bone_ui::input::{ 20 + FrameInstant, InputSnapshot, KeyChar, KeyCode as UiKeyCode, KeyEvent as UiKeyEvent, 21 + ModifierMask, NamedKey, PointerButton, PointerButtonMask, PointerSample, 22 + }; 23 + use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 13 24 use bone_ui::raster::{PngError, encode_png, rasterize}; 25 + use bone_ui::strings::StringTable; 14 26 use bone_ui::theme::{Theme, ThemeMode}; 15 27 use tracing_subscriber::EnvFilter; 16 28 use uom::si::length::millimeter; ··· 22 34 keyboard::{KeyCode, ModifiersState, PhysicalKey}, 23 35 window::{Window, WindowId}, 24 36 }; 37 + 38 + mod chrome; 39 + mod shell; 40 + mod sketch_mode; 41 + mod strings; 42 + 43 + use sketch_mode::Mode; 25 44 26 45 #[derive(Debug, thiserror::Error)] 27 46 enum AppError { ··· 65 84 struct RenderState { 66 85 surface: SurfaceContext, 67 86 renderer: SketchRenderer, 87 + chrome_pipeline: ChromePipeline, 68 88 scene: SketchScene, 69 89 camera: Camera2, 70 90 style: Style, 91 + theme: Arc<Theme>, 92 + shell: shell::Shell, 93 + document: Document, 94 + sketch_id: SketchId, 95 + mode: Mode, 96 + focus: FocusManager, 97 + hit_state: HitState, 98 + hotkeys: HotkeyTable, 99 + strings: StringTable, 100 + viewport_rect: LayoutRect, 71 101 } 72 102 73 103 #[derive(Default)] 74 104 struct InputState { 75 105 cursor_px: Option<PhysicalPosition<f64>>, 76 - left_down: bool, 77 - middle_down: bool, 106 + left_pan: bool, 107 + middle_pan: bool, 78 108 modifiers: ModifiersState, 109 + pending_pressed: PointerButtonMask, 110 + pending_released: PointerButtonMask, 111 + pending_keys: Vec<UiKeyEvent>, 112 + pending_text: String, 79 113 } 80 114 81 115 impl InputState { 82 116 fn panning(&self) -> bool { 83 - self.middle_down || (self.left_down && self.modifiers.shift_key()) 117 + self.middle_pan || (self.left_pan && self.modifiers.shift_key()) 84 118 } 85 119 86 120 fn pan_step_px(&self) -> f64 { ··· 90 124 PAN_STEP_PX 91 125 } 92 126 } 127 + 128 + fn modifier_mask(&self) -> ModifierMask { 129 + let bits = [ 130 + (self.modifiers.control_key(), ModifierMask::CTRL), 131 + (self.modifiers.shift_key(), ModifierMask::SHIFT), 132 + (self.modifiers.alt_key(), ModifierMask::ALT), 133 + (self.modifiers.super_key(), ModifierMask::META), 134 + ]; 135 + bits.iter() 136 + .filter(|(active, _)| *active) 137 + .fold(ModifierMask::NONE, |acc, (_, m)| acc.union(*m)) 138 + } 139 + 140 + fn pointer_sample(&self) -> Option<PointerSample> { 141 + self.cursor_px 142 + .map(physical_to_layout_pos) 143 + .map(PointerSample::new) 144 + } 145 + 146 + fn cursor_in(&self, rect: LayoutRect) -> bool { 147 + self.cursor_px 148 + .map(physical_to_layout_pos) 149 + .is_some_and(|p| rect.contains(p)) 150 + } 151 + 152 + fn drain_snapshot(&mut self) -> InputSnapshot { 153 + let mut snap = InputSnapshot::idle(FrameInstant::ZERO); 154 + snap.pointer = self.pointer_sample(); 155 + snap.buttons_pressed = 156 + core::mem::replace(&mut self.pending_pressed, PointerButtonMask::EMPTY); 157 + snap.buttons_released = 158 + core::mem::replace(&mut self.pending_released, PointerButtonMask::EMPTY); 159 + snap.keys_pressed = core::mem::take(&mut self.pending_keys); 160 + snap.text_committed = core::mem::take(&mut self.pending_text); 161 + snap.modifiers = self.modifier_mask(); 162 + snap 163 + } 164 + 165 + fn forget_pan_state(&mut self) { 166 + self.cursor_px = None; 167 + self.left_pan = false; 168 + self.middle_pan = false; 169 + self.modifiers = ModifiersState::empty(); 170 + } 171 + } 172 + 173 + #[allow( 174 + clippy::cast_possible_truncation, 175 + reason = "winit logical px (f64) collapses to LayoutPx (f32) at the sub-pixel limit" 176 + )] 177 + fn physical_to_layout_pos(p: PhysicalPosition<f64>) -> LayoutPos { 178 + LayoutPos::new( 179 + LayoutPx::saturating(p.x as f32), 180 + LayoutPx::saturating(p.y as f32), 181 + ) 93 182 } 94 183 95 184 struct App { ··· 169 258 add_arc(sketch, arc_center, arc_start, arc_end) 170 259 } 171 260 261 + fn initial_document(sketch: Sketch) -> (Document, SketchId) { 262 + let mut document = Document::new(DocumentId::default(), "Untitled".to_owned()); 263 + let sketch_id = SketchId::default(); 264 + document.insert_sketch(sketch_id, "Sketch1".to_owned(), sketch); 265 + (document, sketch_id) 266 + } 267 + 172 268 fn viewport_extent(size: PhysicalSize<u32>) -> ViewportExtent { 173 269 ViewportExtent::new( 174 270 ViewportPx::new(size.width.max(1)), ··· 176 272 ) 177 273 } 178 274 275 + #[allow( 276 + clippy::cast_precision_loss, 277 + reason = "viewport pixel counts at any realistic display size fit f32 mantissa" 278 + )] 279 + fn layout_size_from_extent(extent: ViewportExtent) -> LayoutSize { 280 + LayoutSize::new( 281 + LayoutPx::new(extent.width().value() as f32), 282 + LayoutPx::new(extent.height().value() as f32), 283 + ) 284 + } 285 + 286 + fn empty_rect() -> LayoutRect { 287 + LayoutRect::new(LayoutPos::ORIGIN, LayoutSize::ZERO) 288 + } 289 + 179 290 fn zoom_factor(delta: MouseScrollDelta) -> f64 { 180 291 match delta { 181 292 MouseScrollDelta::LineDelta(_, y) => ZOOM_STEP_PER_LINE.powf(f64::from(y)), ··· 196 307 let w = f64::from(extent.width().value()); 197 308 let h = f64::from(extent.height().value()); 198 309 let (pan_x, pan_y) = camera.pan_mm().coords_mm(); 199 - let (cursor_x, cursor_y) = match cursor { 200 - Some(c) => (c.x, c.y), 201 - None => (w * 0.5, h * 0.5), 202 - }; 310 + let (cursor_x, cursor_y) = cursor.map_or((w * 0.5, h * 0.5), |c| (c.x, c.y)); 203 311 let horizontal_px = cursor_x - w * 0.5; 204 312 let vertical_px = h * 0.5 - cursor_y; 205 313 let world_x = pan_x + horizontal_px / zoom_before; ··· 220 328 )) 221 329 } 222 330 223 - fn zoom_fit(camera: Camera2, scene: &SketchScene) -> Camera2 { 331 + fn zoom_fit(camera: Camera2, scene: &SketchScene, viewport: LayoutRect) -> Camera2 { 224 332 let Some(aabb) = scene.aabb() else { 225 333 return camera; 226 334 }; 227 335 let (mnx, mny) = aabb.min().coords_mm(); 228 336 let (mxx, mxy) = aabb.max().coords_mm(); 229 - let center = Vec2::from_mm((mnx + mxx) * 0.5, (mny + mxy) * 0.5); 337 + let center_x = (mnx + mxx) * 0.5; 338 + let center_y = (mny + mxy) * 0.5; 230 339 let world_w = mxx - mnx; 231 340 let world_h = mxy - mny; 232 - if world_w <= 0.0 && world_h <= 0.0 { 233 - return camera.with_pan(center); 341 + let v_w = f64::from(viewport.size.width.value()); 342 + let v_h = f64::from(viewport.size.height.value()); 343 + if v_w <= 0.0 || v_h <= 0.0 { 344 + return camera; 234 345 } 235 - let extent = camera.extent(); 236 - let w_px = f64::from(extent.width().value()); 237 - let h_px = f64::from(extent.height().value()); 238 346 let axis_zoom = |pixels: f64, world: f64| { 239 347 if world > 0.0 { 240 348 pixels / world ··· 242 350 f64::INFINITY 243 351 } 244 352 }; 245 - let raw_zoom = axis_zoom(w_px, world_w).min(axis_zoom(h_px, world_h)) * ZOOM_FIT_MARGIN; 246 - let clamped = raw_zoom.clamp(ZOOM_MIN, ZOOM_MAX); 247 - camera.with_zoom(PixelsPerMm::new(clamped)).with_pan(center) 353 + let raw_zoom = axis_zoom(v_w, world_w).min(axis_zoom(v_h, world_h)) * ZOOM_FIT_MARGIN; 354 + let zoom = raw_zoom.clamp(ZOOM_MIN, ZOOM_MAX); 355 + let pan = pan_centering((center_x, center_y), camera.extent(), viewport, zoom); 356 + camera.with_zoom(PixelsPerMm::new(zoom)).with_pan(pan) 357 + } 358 + 359 + fn pan_centering( 360 + target_world: (f64, f64), 361 + window: ViewportExtent, 362 + viewport: LayoutRect, 363 + zoom: f64, 364 + ) -> Vec2 { 365 + let v_w = f64::from(viewport.size.width.value()); 366 + let v_h = f64::from(viewport.size.height.value()); 367 + let viewport_center_x = f64::from(viewport.origin.x.value()) + v_w * 0.5; 368 + let viewport_center_y = f64::from(viewport.origin.y.value()) + v_h * 0.5; 369 + let window_center_x = f64::from(window.width().value()) * 0.5; 370 + let window_center_y = f64::from(window.height().value()) * 0.5; 371 + let pan_x = target_world.0 - (viewport_center_x - window_center_x) / zoom; 372 + let pan_y = target_world.1 + (viewport_center_y - window_center_y) / zoom; 373 + Vec2::from_mm(pan_x, pan_y) 248 374 } 249 375 250 376 enum KeyAction { ··· 268 394 KeyCode::ArrowRight => Some(pan_by_px(camera, -step, 0.0)), 269 395 KeyCode::ArrowUp => Some(pan_by_px(camera, 0.0, step)), 270 396 KeyCode::ArrowDown => Some(pan_by_px(camera, 0.0, -step)), 271 - KeyCode::KeyF => Some(zoom_fit(camera, &state.scene)), 397 + KeyCode::KeyF => Some(zoom_fit(camera, &state.scene, state.viewport_rect)), 272 398 KeyCode::KeyZ => Some(zoom_about( 273 399 camera, 274 400 input.cursor_px, ··· 308 434 } 309 435 }; 310 436 let renderer = SketchRenderer::new(surface.gpu(), surface.color_format()); 437 + let chrome_pipeline = ChromePipeline::new(surface.gpu(), surface.color_format()); 311 438 let sketch = default_sketch(); 312 439 let scene = match SketchScene::extract(&sketch) { 313 440 Ok(s) => s, ··· 319 446 }; 320 447 let camera = Camera2::new(extent).with_zoom(PixelsPerMm::new(INITIAL_ZOOM_PX_PER_MM)); 321 448 let style = Style::default(); 449 + let theme = Arc::new(Theme::light()); 450 + let shell = match shell::Shell::new() { 451 + Ok(s) => s, 452 + Err(e) => { 453 + tracing::error!(error = %e, "Shell::new failed"); 454 + event_loop.exit(); 455 + return; 456 + } 457 + }; 458 + let (document, sketch_id) = initial_document(sketch); 459 + let strings = strings::make_strings(bone_ui::strings::Locale::EnGb); 460 + let viewport_rect = empty_rect(); 322 461 window.request_redraw(); 323 462 self.window = Some(window); 324 463 self.render = Some(RenderState { 325 464 surface, 326 465 renderer, 466 + chrome_pipeline, 327 467 scene, 328 468 camera, 329 469 style, 470 + theme, 471 + shell, 472 + document, 473 + sketch_id, 474 + mode: Mode::Idle, 475 + focus: FocusManager::new(), 476 + hit_state: HitState::new(), 477 + hotkeys: HotkeyTable::new(), 478 + strings, 479 + viewport_rect, 330 480 }); 331 481 } 332 482 483 + #[allow( 484 + clippy::too_many_lines, 485 + reason = "winit event dispatch is a flat match by event variant; splitting obscures the dispatch table" 486 + )] 333 487 fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { 334 488 let (Some(state), Some(window)) = (self.render.as_mut(), self.window.as_ref()) else { 335 489 return; ··· 340 494 let extent = viewport_extent(size); 341 495 state.surface.resize(extent); 342 496 state.camera = state.camera.with_extent(extent); 497 + state.viewport_rect = empty_rect(); 343 498 window.request_redraw(); 344 499 } 345 - WindowEvent::RedrawRequested => { 346 - let surface = &mut state.surface; 347 - let renderer = &mut state.renderer; 348 - let scene = &state.scene; 349 - let camera = state.camera; 350 - let style = &state.style; 351 - renderer.prepare(scene, style); 352 - surface.render( 353 - |encoder, color, pick| { 354 - renderer.encode_passes(encoder, color, pick, scene, camera, style); 355 - }, 356 - || window.pre_present_notify(), 357 - ); 358 - } 359 - WindowEvent::Focused(false) => { 360 - self.input.cursor_px = None; 361 - self.input.left_down = false; 362 - self.input.middle_down = false; 363 - self.input.modifiers = ModifiersState::empty(); 364 - } 500 + WindowEvent::RedrawRequested => render_frame(state, window, &mut self.input), 501 + WindowEvent::Focused(false) => self.input.forget_pan_state(), 365 502 WindowEvent::ModifiersChanged(mods) => { 366 503 self.input.modifiers = mods.state(); 367 504 } ··· 372 509 && let Some(p) = prev 373 510 { 374 511 state.camera = pan_by_px(state.camera, position.x - p.x, position.y - p.y); 375 - window.request_redraw(); 376 512 } 513 + window.request_redraw(); 377 514 } 378 515 WindowEvent::CursorLeft { .. } => { 379 516 self.input.cursor_px = None; ··· 383 520 button: MouseButton::Left, 384 521 .. 385 522 } => { 386 - self.input.left_down = btn_state == ElementState::Pressed; 523 + if btn_state == ElementState::Pressed { 524 + self.input.left_pan = self.input.cursor_in(state.viewport_rect); 525 + self.input.pending_pressed = 526 + self.input.pending_pressed.with(PointerButton::Primary); 527 + } else { 528 + self.input.left_pan = false; 529 + self.input.pending_released = 530 + self.input.pending_released.with(PointerButton::Primary); 531 + } 532 + window.request_redraw(); 533 + } 534 + WindowEvent::MouseInput { 535 + state: btn_state, 536 + button: MouseButton::Right, 537 + .. 538 + } => { 539 + if btn_state == ElementState::Pressed { 540 + self.input.pending_pressed = 541 + self.input.pending_pressed.with(PointerButton::Secondary); 542 + } else { 543 + self.input.pending_released = 544 + self.input.pending_released.with(PointerButton::Secondary); 545 + } 546 + window.request_redraw(); 387 547 } 388 548 WindowEvent::MouseInput { 389 549 state: btn_state, 390 550 button: MouseButton::Middle, 391 551 .. 392 552 } => { 393 - self.input.middle_down = btn_state == ElementState::Pressed; 553 + if btn_state == ElementState::Pressed { 554 + self.input.middle_pan = self.input.cursor_in(state.viewport_rect); 555 + self.input.pending_pressed = 556 + self.input.pending_pressed.with(PointerButton::Middle); 557 + } else { 558 + self.input.middle_pan = false; 559 + self.input.pending_released = 560 + self.input.pending_released.with(PointerButton::Middle); 561 + } 394 562 } 395 - WindowEvent::MouseWheel { delta, .. } => { 563 + WindowEvent::MouseWheel { delta, .. } if self.input.cursor_in(state.viewport_rect) => { 396 564 state.camera = zoom_about(state.camera, self.input.cursor_px, zoom_factor(delta)); 397 565 window.request_redraw(); 398 566 } ··· 404 572 .. 405 573 }, 406 574 .. 407 - } => match keyboard_action(code, &self.input, state) { 408 - Some(KeyAction::Exit) => event_loop.exit(), 409 - Some(KeyAction::Camera(next)) => { 410 - state.camera = next; 411 - window.request_redraw(); 575 + } => { 576 + if let Some(named) = keycode_to_named(code) { 577 + self.input.pending_keys.push(UiKeyEvent::new( 578 + UiKeyCode::Named(named), 579 + self.input.modifier_mask(), 580 + )); 581 + } else if let Some(c) = keycode_to_char(code) { 582 + self.input.pending_keys.push(UiKeyEvent::new( 583 + UiKeyCode::Char(KeyChar::from_char(c)), 584 + self.input.modifier_mask(), 585 + )); 586 + } 587 + if code == KeyCode::Escape { 588 + state.mode = Mode::Idle; 589 + } 590 + match keyboard_action(code, &self.input, state) { 591 + Some(KeyAction::Exit) => event_loop.exit(), 592 + Some(KeyAction::Camera(next)) => { 593 + state.camera = next; 594 + } 595 + None => {} 412 596 } 413 - None => {} 414 - }, 597 + window.request_redraw(); 598 + } 415 599 _ => {} 416 600 } 417 601 } 418 602 } 419 603 420 - enum Mode { 604 + #[allow( 605 + clippy::cast_precision_loss, 606 + reason = "viewport pixel counts at any realistic display size fit f32 mantissa" 607 + )] 608 + fn render_frame(state: &mut RenderState, window: &Window, input_state: &mut InputState) { 609 + let extent = state.surface.extent(); 610 + let layout_size = layout_size_from_extent(extent); 611 + let theme = Arc::clone(&state.theme); 612 + let mut input = input_state.drain_snapshot(); 613 + let mut hits = HitFrame::new(); 614 + let mut a11y = AccessTreeBuilder::new(); 615 + let frame = { 616 + let mut ctx = FrameCtx::new( 617 + theme, 618 + &mut input, 619 + &mut state.focus, 620 + &state.hotkeys, 621 + &state.strings, 622 + &mut hits, 623 + &state.hit_state, 624 + &mut a11y, 625 + ); 626 + state 627 + .shell 628 + .render(&mut ctx, &state.document, &state.mode, layout_size) 629 + }; 630 + state.viewport_rect = frame.viewport_rect; 631 + state.hit_state = resolve(&state.hit_state, &hits, &input, state.focus.focused()); 632 + if let Some(tool) = frame.activated_tool { 633 + let mode = core::mem::take(&mut state.mode); 634 + state.mode = match mode { 635 + Mode::Idle => Mode::enter_sketch(state.sketch_id).arm_tool(tool), 636 + sketching @ Mode::Sketch { .. } => sketching.arm_tool(tool), 637 + }; 638 + } 639 + let chrome_instances: Vec<ChromeInstance> = 640 + chrome::paint_to_instances(&state.theme, &frame.paints); 641 + let viewport_px = [ 642 + extent.width().value() as f32, 643 + extent.height().value() as f32, 644 + ]; 645 + let surface = &mut state.surface; 646 + let renderer = &mut state.renderer; 647 + let chrome_pipeline = &mut state.chrome_pipeline; 648 + let scene = &state.scene; 649 + let camera = state.camera; 650 + let style = &state.style; 651 + renderer.prepare(scene, style); 652 + surface.render( 653 + |encoder, color, pick| { 654 + renderer.encode_passes(encoder, color, pick, scene, camera, style); 655 + chrome_pipeline.draw(encoder, color, viewport_px, &chrome_instances); 656 + }, 657 + || window.pre_present_notify(), 658 + ); 659 + } 660 + 661 + enum RunMode { 421 662 Window, 422 663 Gallery(PathBuf), 423 664 } 424 665 425 - fn parse_mode(args: impl IntoIterator<Item = String>) -> Result<Mode, AppError> { 666 + fn parse_mode(args: impl IntoIterator<Item = String>) -> Result<RunMode, AppError> { 426 667 let collected: Vec<String> = args.into_iter().skip(1).collect(); 427 - parse_args(&collected, Mode::Window) 668 + parse_args(&collected, RunMode::Window) 428 669 } 429 670 430 - fn parse_args(args: &[String], acc: Mode) -> Result<Mode, AppError> { 671 + fn parse_args(args: &[String], acc: RunMode) -> Result<RunMode, AppError> { 431 672 match args { 432 673 [] => Ok(acc), 433 674 [first, second, rest @ ..] if first == "--gallery" => { 434 - parse_args(rest, Mode::Gallery(PathBuf::from(second))) 675 + parse_args(rest, RunMode::Gallery(PathBuf::from(second))) 435 676 } 436 677 [only] if only == "--gallery" => Err(AppError::MissingArg { 437 678 flag: "--gallery <out-dir>", ··· 454 695 let mut state = GalleryState::new(); 455 696 let paint = render(Arc::new(theme.clone()), &mut state); 456 697 let rgba = rasterize(&theme, &paint, GALLERY_CANVAS); 457 - let png = encode_png(&rgba, GALLERY_CANVAS).map_err(|source| AppError::GalleryEncode { 458 - mode, 459 - source, 460 - })?; 698 + let png = encode_png(&rgba, GALLERY_CANVAS) 699 + .map_err(|source| AppError::GalleryEncode { mode, source })?; 461 700 let target = out_dir.join(format!("gallery_{mode}.png")); 462 701 std::fs::write(&target, &png).map_err(|source| AppError::GalleryWrite { 463 702 path: target.clone(), ··· 476 715 ) 477 716 .init(); 478 717 match parse_mode(std::env::args())? { 479 - Mode::Gallery(dir) => emit_gallery(&dir), 480 - Mode::Window => run_window(), 718 + RunMode::Gallery(dir) => emit_gallery(&dir), 719 + RunMode::Window => run_window(), 720 + } 721 + } 722 + 723 + fn keycode_to_named(code: KeyCode) -> Option<NamedKey> { 724 + match code { 725 + KeyCode::Tab => Some(NamedKey::Tab), 726 + KeyCode::Enter | KeyCode::NumpadEnter => Some(NamedKey::Enter), 727 + KeyCode::Escape => Some(NamedKey::Escape), 728 + KeyCode::Backspace => Some(NamedKey::Backspace), 729 + KeyCode::Delete => Some(NamedKey::Delete), 730 + KeyCode::Space => Some(NamedKey::Space), 731 + KeyCode::ArrowUp => Some(NamedKey::ArrowUp), 732 + KeyCode::ArrowDown => Some(NamedKey::ArrowDown), 733 + KeyCode::ArrowLeft => Some(NamedKey::ArrowLeft), 734 + KeyCode::ArrowRight => Some(NamedKey::ArrowRight), 735 + KeyCode::Home => Some(NamedKey::Home), 736 + KeyCode::End => Some(NamedKey::End), 737 + KeyCode::PageUp => Some(NamedKey::PageUp), 738 + KeyCode::PageDown => Some(NamedKey::PageDown), 739 + _ => None, 740 + } 741 + } 742 + 743 + fn keycode_to_char(code: KeyCode) -> Option<char> { 744 + match code { 745 + KeyCode::KeyA => Some('a'), 746 + KeyCode::KeyB => Some('b'), 747 + KeyCode::KeyC => Some('c'), 748 + KeyCode::KeyD => Some('d'), 749 + KeyCode::KeyE => Some('e'), 750 + KeyCode::KeyF => Some('f'), 751 + KeyCode::KeyG => Some('g'), 752 + KeyCode::KeyH => Some('h'), 753 + KeyCode::KeyI => Some('i'), 754 + KeyCode::KeyJ => Some('j'), 755 + KeyCode::KeyK => Some('k'), 756 + KeyCode::KeyL => Some('l'), 757 + KeyCode::KeyM => Some('m'), 758 + KeyCode::KeyN => Some('n'), 759 + KeyCode::KeyO => Some('o'), 760 + KeyCode::KeyP => Some('p'), 761 + KeyCode::KeyQ => Some('q'), 762 + KeyCode::KeyR => Some('r'), 763 + KeyCode::KeyS => Some('s'), 764 + KeyCode::KeyT => Some('t'), 765 + KeyCode::KeyU => Some('u'), 766 + KeyCode::KeyV => Some('v'), 767 + KeyCode::KeyW => Some('w'), 768 + KeyCode::KeyX => Some('x'), 769 + KeyCode::KeyY => Some('y'), 770 + KeyCode::KeyZ => Some('z'), 771 + KeyCode::Digit0 => Some('0'), 772 + KeyCode::Digit1 => Some('1'), 773 + KeyCode::Digit2 => Some('2'), 774 + KeyCode::Digit3 => Some('3'), 775 + KeyCode::Digit4 => Some('4'), 776 + KeyCode::Digit5 => Some('5'), 777 + KeyCode::Digit6 => Some('6'), 778 + KeyCode::Digit7 => Some('7'), 779 + KeyCode::Digit8 => Some('8'), 780 + KeyCode::Digit9 => Some('9'), 781 + _ => None, 481 782 } 482 783 } 483 784
+114
crates/bone-app/src/sketch_mode.rs
··· 1 + use bone_types::SketchId; 2 + 3 + #[derive(Copy, Clone, Debug, PartialEq, Eq)] 4 + pub enum SketchTool { 5 + Point, 6 + Line, 7 + CenterpointArc, 8 + TangentArc, 9 + ThreePointArc, 10 + Circle, 11 + PerimeterCircle, 12 + CornerRectangle, 13 + CenterRectangle, 14 + ThreePointCornerRectangle, 15 + ThreePointCenterRectangle, 16 + Parallelogram, 17 + SmartDimension, 18 + } 19 + 20 + impl SketchTool { 21 + pub const ENTITIES: &'static [Self] = &[ 22 + Self::Point, 23 + Self::Line, 24 + Self::CenterpointArc, 25 + Self::TangentArc, 26 + Self::ThreePointArc, 27 + Self::Circle, 28 + Self::PerimeterCircle, 29 + Self::CornerRectangle, 30 + Self::CenterRectangle, 31 + Self::ThreePointCornerRectangle, 32 + Self::ThreePointCenterRectangle, 33 + Self::Parallelogram, 34 + ]; 35 + } 36 + 37 + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 38 + pub struct SketchSession { 39 + pub tool: Option<SketchTool>, 40 + } 41 + 42 + #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] 43 + pub enum Mode { 44 + #[default] 45 + Idle, 46 + Sketch { 47 + sketch_id: SketchId, 48 + session: SketchSession, 49 + }, 50 + } 51 + 52 + impl Mode { 53 + #[must_use] 54 + pub const fn enter_sketch(sketch_id: SketchId) -> Self { 55 + Self::Sketch { 56 + sketch_id, 57 + session: SketchSession { tool: None }, 58 + } 59 + } 60 + 61 + #[must_use] 62 + pub fn arm_tool(self, kind: SketchTool) -> Self { 63 + match self { 64 + Self::Sketch { 65 + sketch_id, 66 + mut session, 67 + } => { 68 + session.tool = Some(kind); 69 + Self::Sketch { sketch_id, session } 70 + } 71 + Self::Idle => Self::Idle, 72 + } 73 + } 74 + } 75 + 76 + #[cfg(test)] 77 + mod tests { 78 + use super::{Mode, SketchSession, SketchTool}; 79 + use bone_types::SketchId; 80 + 81 + #[test] 82 + fn arm_tool_in_sketch_records_kind() { 83 + let mode = Mode::enter_sketch(SketchId::default()).arm_tool(SketchTool::Line); 84 + let Mode::Sketch { session, .. } = mode else { 85 + panic!("expected sketch mode"); 86 + }; 87 + assert_eq!(session.tool, Some(SketchTool::Line)); 88 + } 89 + 90 + #[test] 91 + fn arm_tool_in_idle_is_noop() { 92 + assert_eq!(Mode::Idle.arm_tool(SketchTool::Line), Mode::Idle); 93 + } 94 + 95 + #[test] 96 + fn default_mode_is_idle() { 97 + assert_eq!(Mode::default(), Mode::Idle); 98 + } 99 + 100 + #[test] 101 + fn entities_table_excludes_smart_dimension() { 102 + assert!(!SketchTool::ENTITIES.contains(&SketchTool::SmartDimension)); 103 + assert_eq!( 104 + SketchTool::ENTITIES.len(), 105 + 12, 106 + "ADR 0008 day-1 entity tools" 107 + ); 108 + } 109 + 110 + #[test] 111 + fn fresh_session_has_no_tool() { 112 + assert_eq!(SketchSession::default().tool, None); 113 + } 114 + }
+156
crates/bone-app/src/strings.rs
··· 1 + use bone_ui::strings::{Locale, StringKey, StringTable}; 2 + 3 + pub const APP_TITLE: StringKey = StringKey::new("app.title"); 4 + 5 + pub const RIBBON_LABEL: StringKey = StringKey::new("shell.ribbon"); 6 + pub const RIBBON_TAB_SKETCH: StringKey = StringKey::new("shell.ribbon.tab.sketch"); 7 + pub const RIBBON_GROUP_ENTITIES: StringKey = StringKey::new("shell.ribbon.group.entities"); 8 + pub const RIBBON_GROUP_RELATIONS: StringKey = StringKey::new("shell.ribbon.group.relations"); 9 + pub const RIBBON_GROUP_DIMENSIONS: StringKey = StringKey::new("shell.ribbon.group.dimensions"); 10 + 11 + pub const TOOL_POINT: StringKey = StringKey::new("tool.point"); 12 + pub const TOOL_LINE: StringKey = StringKey::new("tool.line"); 13 + pub const TOOL_CENTERPOINT_ARC: StringKey = StringKey::new("tool.centerpoint_arc"); 14 + pub const TOOL_TANGENT_ARC: StringKey = StringKey::new("tool.tangent_arc"); 15 + pub const TOOL_THREE_POINT_ARC: StringKey = StringKey::new("tool.three_point_arc"); 16 + pub const TOOL_CIRCLE: StringKey = StringKey::new("tool.circle"); 17 + pub const TOOL_PERIMETER_CIRCLE: StringKey = StringKey::new("tool.perimeter_circle"); 18 + pub const TOOL_CORNER_RECTANGLE: StringKey = StringKey::new("tool.corner_rectangle"); 19 + pub const TOOL_CENTER_RECTANGLE: StringKey = StringKey::new("tool.center_rectangle"); 20 + pub const TOOL_THREE_POINT_CORNER_RECTANGLE: StringKey = 21 + StringKey::new("tool.three_point_corner_rectangle"); 22 + pub const TOOL_THREE_POINT_CENTER_RECTANGLE: StringKey = 23 + StringKey::new("tool.three_point_center_rectangle"); 24 + pub const TOOL_PARALLELOGRAM: StringKey = StringKey::new("tool.parallelogram"); 25 + 26 + pub const TOOL_COINCIDENT: StringKey = StringKey::new("tool.coincident"); 27 + pub const TOOL_HORIZONTAL: StringKey = StringKey::new("tool.horizontal"); 28 + pub const TOOL_VERTICAL: StringKey = StringKey::new("tool.vertical"); 29 + pub const TOOL_PARALLEL: StringKey = StringKey::new("tool.parallel"); 30 + pub const TOOL_PERPENDICULAR: StringKey = StringKey::new("tool.perpendicular"); 31 + pub const TOOL_TANGENT: StringKey = StringKey::new("tool.tangent"); 32 + pub const TOOL_EQUAL: StringKey = StringKey::new("tool.equal"); 33 + pub const TOOL_CONCENTRIC: StringKey = StringKey::new("tool.concentric"); 34 + pub const TOOL_FIX: StringKey = StringKey::new("tool.fix"); 35 + 36 + pub const TOOL_SMART_DIMENSION: StringKey = StringKey::new("tool.smart_dimension"); 37 + 38 + pub const FEATURE_TREE_LABEL: StringKey = StringKey::new("shell.feature_tree"); 39 + pub const FEATURE_ORIGIN: StringKey = StringKey::new("feature.origin"); 40 + pub const FEATURE_PLANE_XY: StringKey = StringKey::new("feature.plane.xy"); 41 + pub const FEATURE_PLANE_YZ: StringKey = StringKey::new("feature.plane.yz"); 42 + pub const FEATURE_PLANE_ZX: StringKey = StringKey::new("feature.plane.zx"); 43 + pub const FEATURE_SKETCH_DEFAULT: StringKey = StringKey::new("feature.sketch.default"); 44 + 45 + pub const PROPERTY_PANE_LABEL: StringKey = StringKey::new("shell.property_pane"); 46 + 47 + pub const STATUS_BAR_LABEL: StringKey = StringKey::new("shell.status_bar"); 48 + pub const STATUS_READY: StringKey = StringKey::new("status.ready"); 49 + pub const STATUS_SKETCH_ACTIVE: StringKey = StringKey::new("status.sketch_active"); 50 + 51 + #[must_use] 52 + pub fn make_strings(locale: Locale) -> StringTable { 53 + let mut table = StringTable::for_locale(locale); 54 + let entries: &[(StringKey, &str)] = match locale { 55 + Locale::EnGb => EN_GB, 56 + Locale::ArXb => AR_XB, 57 + }; 58 + entries.iter().for_each(|(key, value)| { 59 + table.insert(*key, (*value).to_owned()); 60 + }); 61 + table 62 + } 63 + 64 + const EN_GB: &[(StringKey, &str)] = &[ 65 + (APP_TITLE, "Bone"), 66 + (RIBBON_LABEL, "Ribbon"), 67 + (RIBBON_TAB_SKETCH, "Sketch"), 68 + (RIBBON_GROUP_ENTITIES, "Entities"), 69 + (RIBBON_GROUP_RELATIONS, "Relations"), 70 + (RIBBON_GROUP_DIMENSIONS, "Dimensions"), 71 + (TOOL_POINT, "Point"), 72 + (TOOL_LINE, "Line"), 73 + (TOOL_CENTERPOINT_ARC, "Centerpoint Arc"), 74 + (TOOL_TANGENT_ARC, "Tangent Arc"), 75 + (TOOL_THREE_POINT_ARC, "3 Point Arc"), 76 + (TOOL_CIRCLE, "Circle"), 77 + (TOOL_PERIMETER_CIRCLE, "Perimeter Circle"), 78 + (TOOL_CORNER_RECTANGLE, "Corner Rectangle"), 79 + (TOOL_CENTER_RECTANGLE, "Center Rectangle"), 80 + ( 81 + TOOL_THREE_POINT_CORNER_RECTANGLE, 82 + "3 Point Corner Rectangle", 83 + ), 84 + ( 85 + TOOL_THREE_POINT_CENTER_RECTANGLE, 86 + "3 Point Center Rectangle", 87 + ), 88 + (TOOL_PARALLELOGRAM, "Parallelogram"), 89 + (TOOL_COINCIDENT, "Coincident"), 90 + (TOOL_HORIZONTAL, "Horizontal"), 91 + (TOOL_VERTICAL, "Vertical"), 92 + (TOOL_PARALLEL, "Parallel"), 93 + (TOOL_PERPENDICULAR, "Perpendicular"), 94 + (TOOL_TANGENT, "Tangent"), 95 + (TOOL_EQUAL, "Equal"), 96 + (TOOL_CONCENTRIC, "Concentric"), 97 + (TOOL_FIX, "Fix"), 98 + (TOOL_SMART_DIMENSION, "Smart Dimension"), 99 + (FEATURE_TREE_LABEL, "Feature Tree"), 100 + (FEATURE_ORIGIN, "Origin"), 101 + (FEATURE_PLANE_XY, "Front Plane"), 102 + (FEATURE_PLANE_YZ, "Right Plane"), 103 + (FEATURE_PLANE_ZX, "Top Plane"), 104 + (FEATURE_SKETCH_DEFAULT, "Sketch"), 105 + (PROPERTY_PANE_LABEL, "Property Pane"), 106 + (STATUS_BAR_LABEL, "Status Bar"), 107 + (STATUS_READY, "Ready"), 108 + (STATUS_SKETCH_ACTIVE, "Editing Sketch"), 109 + ]; 110 + 111 + const AR_XB: &[(StringKey, &str)] = &[ 112 + (APP_TITLE, "[!! Bône !!]"), 113 + (RIBBON_LABEL, "[!! ʁibbon !!]"), 114 + (RIBBON_TAB_SKETCH, "[!! Skêtch !!]"), 115 + (RIBBON_GROUP_ENTITIES, "[!! Entîtîes !!]"), 116 + (RIBBON_GROUP_RELATIONS, "[!! Relâtions !!]"), 117 + (RIBBON_GROUP_DIMENSIONS, "[!! Dîmensions !!]"), 118 + (TOOL_POINT, "[!! Pôint !!]"), 119 + (TOOL_LINE, "[!! Lîne !!]"), 120 + (TOOL_CENTERPOINT_ARC, "[!! Cêntrepoint Arc !!]"), 121 + (TOOL_TANGENT_ARC, "[!! Tângent Arc !!]"), 122 + (TOOL_THREE_POINT_ARC, "[!! 3 Pôint Arc !!]"), 123 + (TOOL_CIRCLE, "[!! Cîrcle !!]"), 124 + (TOOL_PERIMETER_CIRCLE, "[!! Perîmeter Cîrcle !!]"), 125 + (TOOL_CORNER_RECTANGLE, "[!! Côrner Rectàngle !!]"), 126 + (TOOL_CENTER_RECTANGLE, "[!! Cênter Rectàngle !!]"), 127 + ( 128 + TOOL_THREE_POINT_CORNER_RECTANGLE, 129 + "[!! 3 Pôint Côrner Rectàngle !!]", 130 + ), 131 + ( 132 + TOOL_THREE_POINT_CENTER_RECTANGLE, 133 + "[!! 3 Pôint Cênter Rectàngle !!]", 134 + ), 135 + (TOOL_PARALLELOGRAM, "[!! Parâllelôgram !!]"), 136 + (TOOL_COINCIDENT, "[!! Coincîdent !!]"), 137 + (TOOL_HORIZONTAL, "[!! Horîzontal !!]"), 138 + (TOOL_VERTICAL, "[!! Vêrtical !!]"), 139 + (TOOL_PARALLEL, "[!! Parâllel !!]"), 140 + (TOOL_PERPENDICULAR, "[!! Perpendîcular !!]"), 141 + (TOOL_TANGENT, "[!! Tângent !!]"), 142 + (TOOL_EQUAL, "[!! Équal !!]"), 143 + (TOOL_CONCENTRIC, "[!! Concêntric !!]"), 144 + (TOOL_FIX, "[!! Fîx !!]"), 145 + (TOOL_SMART_DIMENSION, "[!! Smârt Dimensiôn !!]"), 146 + (FEATURE_TREE_LABEL, "[!! Featûre Tree !!]"), 147 + (FEATURE_ORIGIN, "[!! Orîgin !!]"), 148 + (FEATURE_PLANE_XY, "[!! Front Plàne !!]"), 149 + (FEATURE_PLANE_YZ, "[!! Rîght Plàne !!]"), 150 + (FEATURE_PLANE_ZX, "[!! Tôp Plàne !!]"), 151 + (FEATURE_SKETCH_DEFAULT, "[!! Skêtch !!]"), 152 + (PROPERTY_PANE_LABEL, "[!! Propérty Pâne !!]"), 153 + (STATUS_BAR_LABEL, "[!! Statûs Bar !!]"), 154 + (STATUS_READY, "[!! Réady !!]"), 155 + (STATUS_SKETCH_ACTIVE, "[!! Edîting Skêtch !!]"), 156 + ];