Another project
0

Configure Feed

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

at main 24 kB View raw
1use bone_document::{DimensionValue, SketchDimension}; 2use bone_render::Camera2; 3use bone_types::{Angle, Length, Point2}; 4use bone_ui::frame::FrameCtx; 5use bone_ui::input::{NamedKey, PointerButton}; 6use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize}; 7use bone_ui::widgets::{ 8 Button, ButtonState, ButtonVariant, MemoryClipboard, ParsedInput, ParsedInputResponse, TakeKey, 9 TextInputState, WidgetPaint, show_button, show_parsed_input, take_key, 10}; 11use bone_ui::{WidgetId, WidgetKey}; 12use uom::si::angle::degree; 13use uom::si::length::millimeter; 14 15use crate::sketch_mode::PendingDimension; 16use crate::smart_dimension; 17use crate::strings; 18 19const EDITOR_WIDTH_PX: f32 = 140.0; 20const EDITOR_HEIGHT_PX: f32 = 24.0; 21const EDITOR_OFFSET_X_PX: f32 = 12.0; 22const EDITOR_OFFSET_Y_PX: f32 = -12.0; 23const SWAP_BUTTON_WIDTH_PX: f32 = 64.0; 24const SWAP_BUTTON_GAP_PX: f32 = 4.0; 25 26#[derive(Default)] 27pub struct DimensionEditorState { 28 pub text: TextInputState, 29 pub clipboard: MemoryClipboard, 30 pub last_seeded_for: Option<SketchDimension>, 31 pub focus_request_pending: bool, 32} 33 34impl DimensionEditorState { 35 pub fn reset_for(&mut self, proto: SketchDimension) { 36 self.text = TextInputState::from_text(format_initial_value(proto.value())); 37 self.last_seeded_for = Some(proto); 38 self.focus_request_pending = true; 39 } 40 41 pub fn close(&mut self) { 42 self.text = TextInputState::default(); 43 self.last_seeded_for = None; 44 self.focus_request_pending = false; 45 } 46} 47 48#[derive(Clone, Debug, PartialEq)] 49pub enum DimensionEditorAction { 50 Idle, 51 Commit(DimensionValue), 52 Cancel, 53 Swap(SketchDimension), 54} 55 56pub struct DimensionEditorOutcome { 57 pub paints: Vec<WidgetPaint>, 58 pub action: DimensionEditorAction, 59 pub bounds: LayoutRect, 60 pub claimed_pointer: bool, 61} 62 63pub fn render( 64 ctx: &mut FrameCtx<'_>, 65 pending: PendingDimension, 66 anchor: Point2, 67 camera: &Camera2, 68 viewport_rect: LayoutRect, 69 state: &mut DimensionEditorState, 70) -> DimensionEditorOutcome { 71 let editor_id = WidgetId::ROOT.child(WidgetKey::new("dim.editor")); 72 let radius_id = editor_id.child(WidgetKey::new("swap.radius")); 73 let diameter_id = editor_id.child(WidgetKey::new("swap.diameter")); 74 let editor_rect = editor_rect(viewport_rect, camera, anchor); 75 let mut paints = Vec::new(); 76 if state.last_seeded_for != Some(pending.proto) { 77 state.reset_for(pending.proto); 78 } 79 if state.focus_request_pending { 80 ctx.request_focus(editor_id); 81 state.focus_request_pending = false; 82 } 83 let placeholder = smart_dimension::placeholder_for(pending.proto.value()); 84 let parsed = match pending.proto.value() { 85 DimensionValue::Length(_) => render_length(ctx, editor_id, editor_rect, placeholder, state), 86 DimensionValue::Angle(_) => render_angle(ctx, editor_id, editor_rect, placeholder, state), 87 }; 88 paints.extend(parsed.paint); 89 let effective_proto = merge_typed_value(pending.proto, parsed.value); 90 let swap = render_swap(ctx, &effective_proto, editor_rect, radius_id, diameter_id); 91 paints.extend(swap.paints); 92 let canceled = take_key(ctx.input, &[TakeKey::named(NamedKey::Escape)]).is_some(); 93 let bounds = match swap.bounds { 94 Some(swap_rect) => editor_rect.union(swap_rect), 95 None => editor_rect, 96 }; 97 let pressed_outside = primary_press_outside(ctx, bounds); 98 let action = if canceled { 99 DimensionEditorAction::Cancel 100 } else if let Some(swapped) = swap.action { 101 DimensionEditorAction::Swap(swapped) 102 } else if let Some(value) = parsed.committed { 103 DimensionEditorAction::Commit(value) 104 } else if pressed_outside { 105 match parsed.value { 106 Some(value) => DimensionEditorAction::Commit(value), 107 None => DimensionEditorAction::Cancel, 108 } 109 } else { 110 DimensionEditorAction::Idle 111 }; 112 DimensionEditorOutcome { 113 paints, 114 action, 115 bounds, 116 claimed_pointer: pressed_outside, 117 } 118} 119 120#[must_use] 121fn merge_typed_value(proto: SketchDimension, typed: Option<DimensionValue>) -> SketchDimension { 122 typed 123 .and_then(|v| proto.with_value(v).ok()) 124 .unwrap_or(proto) 125} 126 127fn primary_press_outside(ctx: &FrameCtx<'_>, bounds: LayoutRect) -> bool { 128 if !ctx.input.buttons_pressed.contains(PointerButton::Primary) { 129 return false; 130 } 131 let Some(p) = ctx.input.pointer.as_ref() else { 132 return false; 133 }; 134 !bounds.contains(p.position) 135} 136 137struct ParsedOutcome { 138 paint: Vec<WidgetPaint>, 139 value: Option<DimensionValue>, 140 committed: Option<DimensionValue>, 141} 142 143fn render_length( 144 ctx: &mut FrameCtx<'_>, 145 id: WidgetId, 146 rect: LayoutRect, 147 placeholder: bone_ui::strings::StringKey, 148 state: &mut DimensionEditorState, 149) -> ParsedOutcome { 150 let widget = ParsedInput::<Length>::new(id, rect, placeholder, &mut state.text); 151 let ParsedInputResponse { 152 value, 153 committed, 154 paint, 155 .. 156 } = show_parsed_input(ctx, widget, &mut state.clipboard); 157 ParsedOutcome { 158 paint, 159 value: value.map(DimensionValue::Length), 160 committed: committed.map(DimensionValue::Length), 161 } 162} 163 164fn render_angle( 165 ctx: &mut FrameCtx<'_>, 166 id: WidgetId, 167 rect: LayoutRect, 168 placeholder: bone_ui::strings::StringKey, 169 state: &mut DimensionEditorState, 170) -> ParsedOutcome { 171 let widget = ParsedInput::<Angle>::new(id, rect, placeholder, &mut state.text); 172 let ParsedInputResponse { 173 value, 174 committed, 175 paint, 176 .. 177 } = show_parsed_input(ctx, widget, &mut state.clipboard); 178 ParsedOutcome { 179 paint, 180 value: value.map(DimensionValue::Angle), 181 committed: committed.map(DimensionValue::Angle), 182 } 183} 184 185struct SwapOutcome { 186 paints: Vec<WidgetPaint>, 187 action: Option<SketchDimension>, 188 bounds: Option<LayoutRect>, 189} 190 191fn render_swap( 192 ctx: &mut FrameCtx<'_>, 193 proto: &SketchDimension, 194 editor_rect: LayoutRect, 195 radius_id: WidgetId, 196 diameter_id: WidgetId, 197) -> SwapOutcome { 198 let (is_radius, is_diameter) = match proto { 199 SketchDimension::Radius { .. } => (true, false), 200 SketchDimension::Diameter { .. } => (false, true), 201 SketchDimension::Linear { .. } | SketchDimension::Angular { .. } => { 202 return SwapOutcome { 203 paints: Vec::new(), 204 action: None, 205 bounds: None, 206 }; 207 } 208 }; 209 let radius_rect = swap_button_rect(editor_rect, 0); 210 let diameter_rect = swap_button_rect(editor_rect, 1); 211 let radius_btn = Button::new( 212 radius_id, 213 radius_rect, 214 strings::TOOL_RADIUS, 215 if is_radius { 216 ButtonVariant::Primary 217 } else { 218 ButtonVariant::Secondary 219 }, 220 ) 221 .with_state(if is_radius { 222 ButtonState::Disabled 223 } else { 224 ButtonState::Idle 225 }); 226 let diameter_btn = Button::new( 227 diameter_id, 228 diameter_rect, 229 strings::TOOL_DIAMETER, 230 if is_diameter { 231 ButtonVariant::Primary 232 } else { 233 ButtonVariant::Secondary 234 }, 235 ) 236 .with_state(if is_diameter { 237 ButtonState::Disabled 238 } else { 239 ButtonState::Idle 240 }); 241 let radius_resp = show_button(ctx, radius_btn); 242 let diameter_resp = show_button(ctx, diameter_btn); 243 let mut paints = Vec::new(); 244 paints.extend(radius_resp.paint); 245 paints.extend(diameter_resp.paint); 246 let action = if radius_resp.activated || diameter_resp.activated { 247 smart_dimension::swap_radius_diameter(*proto) 248 } else { 249 None 250 }; 251 SwapOutcome { 252 paints, 253 action, 254 bounds: Some(radius_rect.union(diameter_rect)), 255 } 256} 257 258fn editor_rect(viewport_rect: LayoutRect, camera: &Camera2, anchor: Point2) -> LayoutRect { 259 let LayoutPos { x: ax, y: ay } = world_to_screen(camera, anchor); 260 let raw_x = ax.value() + EDITOR_OFFSET_X_PX; 261 let raw_y = ay.value() + EDITOR_OFFSET_Y_PX; 262 let total_width = EDITOR_WIDTH_PX + 2.0 * (SWAP_BUTTON_WIDTH_PX + SWAP_BUTTON_GAP_PX); 263 let min_x = viewport_rect.min_x().value(); 264 let min_y = viewport_rect.min_y().value(); 265 let max_x = (min_x + viewport_rect.size.width.value() - total_width).max(min_x); 266 let max_y = (min_y + viewport_rect.size.height.value() - EDITOR_HEIGHT_PX).max(min_y); 267 let x = raw_x.clamp(min_x, max_x); 268 let y = raw_y.clamp(min_y, max_y); 269 LayoutRect::new( 270 LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)), 271 LayoutSize::new( 272 LayoutPx::new(EDITOR_WIDTH_PX), 273 LayoutPx::new(EDITOR_HEIGHT_PX), 274 ), 275 ) 276} 277 278fn swap_button_rect(editor_rect: LayoutRect, index: u32) -> LayoutRect { 279 #[allow( 280 clippy::cast_precision_loss, 281 reason = "swap-button index is 0 or 1; f32 round-trip is exact" 282 )] 283 let offset = editor_rect.size.width.value() 284 + SWAP_BUTTON_GAP_PX 285 + (index as f32) * (SWAP_BUTTON_WIDTH_PX + SWAP_BUTTON_GAP_PX); 286 LayoutRect::new( 287 LayoutPos::new( 288 LayoutPx::new(editor_rect.min_x().value() + offset), 289 editor_rect.origin.y, 290 ), 291 LayoutSize::new(LayoutPx::new(SWAP_BUTTON_WIDTH_PX), editor_rect.size.height), 292 ) 293} 294 295#[allow( 296 clippy::cast_possible_truncation, 297 reason = "world-to-screen px collapses f64 to LayoutPx (f32) at the sub-pixel limit" 298)] 299fn world_to_screen(camera: &Camera2, world: Point2) -> LayoutPos { 300 let extent = camera.extent(); 301 let w = f64::from(extent.width().value()); 302 let h = f64::from(extent.height().value()); 303 let zoom = camera.zoom().value(); 304 let (pan_x, pan_y) = camera.pan_mm().coords_mm(); 305 let (wx, wy) = world.coords_mm(); 306 let cursor_x = w * 0.5 + (wx - pan_x) * zoom; 307 let cursor_y = h * 0.5 - (wy - pan_y) * zoom; 308 LayoutPos::new( 309 LayoutPx::saturating(cursor_x as f32), 310 LayoutPx::saturating(cursor_y as f32), 311 ) 312} 313 314fn format_initial_value(value: DimensionValue) -> String { 315 match value { 316 DimensionValue::Length(l) => format!("{:.3}", l.get::<millimeter>()), 317 DimensionValue::Angle(a) => format!("{:.3}", a.get::<degree>()), 318 } 319} 320 321#[cfg(test)] 322mod tests { 323 use super::*; 324 use bone_document::{DimensionKind, SketchDimension}; 325 use bone_render::{PixelsPerMm, ViewportExtent, ViewportPx}; 326 use bone_types::{Length, SketchEntityId}; 327 use uom::si::length::millimeter; 328 329 fn sample_radius() -> SketchDimension { 330 SketchDimension::Radius { 331 target: SketchEntityId::default(), 332 value: Length::new::<millimeter>(3.0), 333 kind: DimensionKind::Driving, 334 } 335 } 336 337 fn sample_camera() -> Camera2 { 338 let extent = ViewportExtent::new(ViewportPx::new(800), ViewportPx::new(600)); 339 Camera2::new(extent).with_zoom(PixelsPerMm::new(10.0)) 340 } 341 342 #[test] 343 fn merge_typed_value_returns_proto_when_no_typed_input() { 344 let proto = sample_radius(); 345 assert_eq!(merge_typed_value(proto, None), proto); 346 } 347 348 #[test] 349 fn merge_typed_value_overrides_proto_value_when_typed_matches() { 350 let proto = sample_radius(); 351 let typed = DimensionValue::Length(Length::new::<millimeter>(5.0)); 352 let merged = merge_typed_value(proto, Some(typed)); 353 let SketchDimension::Radius { value, .. } = merged else { 354 panic!("expected Radius"); 355 }; 356 assert!((value.get::<millimeter>() - 5.0).abs() < 1e-9); 357 } 358 359 #[test] 360 fn merge_typed_value_falls_back_when_typed_kind_mismatches() { 361 let proto = sample_radius(); 362 let bad = DimensionValue::Angle(bone_types::Angle::new::<uom::si::angle::radian>(1.0)); 363 assert_eq!(merge_typed_value(proto, Some(bad)), proto); 364 } 365 366 #[test] 367 fn editor_state_reset_seeds_text_from_value() { 368 let mut state = DimensionEditorState::default(); 369 state.reset_for(sample_radius()); 370 assert_eq!(state.text.text, "3.000"); 371 assert_eq!(state.last_seeded_for, Some(sample_radius())); 372 } 373 374 #[test] 375 fn editor_state_close_drops_seeded_proto() { 376 let mut state = DimensionEditorState::default(); 377 state.reset_for(sample_radius()); 378 state.close(); 379 assert_eq!(state.last_seeded_for, None); 380 assert_eq!(state.text.text, ""); 381 } 382 383 #[test] 384 fn second_frame_keeps_seeded_text_after_first_render() { 385 use bone_ui::a11y::AccessTreeBuilder; 386 use bone_ui::focus::FocusManager; 387 use bone_ui::hit_test::{HitFrame, HitState, resolve}; 388 use bone_ui::hotkey::HotkeyTable; 389 use bone_ui::input::{FrameInstant, InputSnapshot}; 390 use bone_ui::strings::StringTable; 391 use bone_ui::theme::Theme; 392 use bone_ui::widgets::{LabelText, WidgetPaint}; 393 use std::sync::Arc; 394 let mut state = DimensionEditorState::default(); 395 let pending = PendingDimension { 396 proto: sample_radius(), 397 anchor: Point2::origin(), 398 }; 399 let camera = sample_camera(); 400 let viewport = LayoutRect::new( 401 LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)), 402 LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)), 403 ); 404 let theme = Arc::new(Theme::light()); 405 let table = HotkeyTable::new(); 406 let mut focus = FocusManager::new(); 407 let mut hit_state = HitState::new(); 408 let strings = StringTable::empty(); 409 let run_one = 410 |state: &mut DimensionEditorState, focus: &mut FocusManager, prev_hit: &HitState| { 411 let mut hits = HitFrame::new(); 412 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 413 let mut shaper = bone_text::Shaper::new(); 414 let mut a11y = AccessTreeBuilder::new(); 415 let outcome = { 416 let mut ctx = FrameCtx::new( 417 Arc::clone(&theme), 418 &mut input, 419 focus, 420 &table, 421 strings, 422 &mut hits, 423 prev_hit, 424 &mut a11y, 425 &mut shaper, 426 ); 427 render(&mut ctx, pending, pending.anchor, &camera, viewport, state) 428 }; 429 let next_hit = resolve(prev_hit, &hits, &input, focus.focused()); 430 (outcome, next_hit) 431 }; 432 let (_, next_hit) = run_one(&mut state, &mut focus, &hit_state); 433 hit_state = next_hit; 434 let (outcome2, _) = run_one(&mut state, &mut focus, &hit_state); 435 let any_label = outcome2.paints.iter().any(|p| { 436 matches!( 437 p, 438 WidgetPaint::Label { text: LabelText::Owned(t), .. } if t == "3.000", 439 ) 440 }); 441 assert!(any_label, "second frame must still paint the seeded value"); 442 } 443 444 #[test] 445 fn render_emits_surface_and_label_for_seeded_state() { 446 use bone_ui::a11y::AccessTreeBuilder; 447 use bone_ui::focus::FocusManager; 448 use bone_ui::hit_test::{HitFrame, HitState}; 449 use bone_ui::hotkey::HotkeyTable; 450 use bone_ui::input::{FrameInstant, InputSnapshot}; 451 use bone_ui::strings::StringTable; 452 use bone_ui::theme::Theme; 453 use bone_ui::widgets::{LabelText, WidgetPaint}; 454 use std::sync::Arc; 455 let mut state = DimensionEditorState::default(); 456 let pending = PendingDimension { 457 proto: sample_radius(), 458 anchor: Point2::origin(), 459 }; 460 let camera = sample_camera(); 461 let viewport = LayoutRect::new( 462 LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)), 463 LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)), 464 ); 465 let theme = Arc::new(Theme::light()); 466 let table = HotkeyTable::new(); 467 let mut focus = FocusManager::new(); 468 let mut hits = HitFrame::new(); 469 let prev = HitState::new(); 470 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 471 let mut shaper = bone_text::Shaper::new(); 472 let mut a11y = AccessTreeBuilder::new(); 473 let mut ctx = FrameCtx::new( 474 theme, 475 &mut input, 476 &mut focus, 477 &table, 478 StringTable::empty(), 479 &mut hits, 480 &prev, 481 &mut a11y, 482 &mut shaper, 483 ); 484 let outcome = render( 485 &mut ctx, 486 pending, 487 pending.anchor, 488 &camera, 489 viewport, 490 &mut state, 491 ); 492 let any_label_with_text = outcome.paints.iter().any(|p| { 493 matches!( 494 p, 495 WidgetPaint::Label { text: LabelText::Owned(t), .. } if t == "3.000", 496 ) 497 }); 498 assert!(any_label_with_text, "popup must paint the seeded value"); 499 let any_surface = outcome 500 .paints 501 .iter() 502 .any(|p| matches!(p, WidgetPaint::Surface { .. })); 503 assert!(any_surface, "popup must paint a surface fill"); 504 } 505 506 #[test] 507 fn world_to_screen_at_pan_origin_is_window_center() { 508 let camera = sample_camera(); 509 let p = world_to_screen(&camera, Point2::origin()); 510 assert!((p.x.value() - 400.0).abs() < 0.1); 511 assert!((p.y.value() - 300.0).abs() < 0.1); 512 } 513 514 #[test] 515 fn world_to_screen_inverts_y() { 516 let camera = sample_camera(); 517 let above = world_to_screen(&camera, Point2::from_mm(0.0, 5.0)); 518 let below = world_to_screen(&camera, Point2::from_mm(0.0, -5.0)); 519 assert!(above.y.value() < below.y.value()); 520 } 521 522 #[test] 523 fn editor_rect_does_not_panic_when_viewport_is_narrower_than_editor() { 524 let viewport = LayoutRect::new( 525 LayoutPos::new(LayoutPx::new(250.0), LayoutPx::new(40.0)), 526 LayoutSize::new(LayoutPx::new(120.0), LayoutPx::new(60.0)), 527 ); 528 let camera = sample_camera(); 529 let rect = editor_rect(viewport, &camera, Point2::origin()); 530 assert!(rect.min_x().value() >= viewport.min_x().value()); 531 assert!(rect.min_y().value() >= viewport.min_y().value()); 532 } 533 534 #[test] 535 fn editor_rect_clamps_inside_viewport_when_anchor_is_far_right() { 536 let viewport = LayoutRect::new( 537 LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)), 538 LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)), 539 ); 540 let camera = sample_camera(); 541 let anchor = Point2::from_mm(100.0, 0.0); 542 let rect = editor_rect(viewport, &camera, anchor); 543 let total_width = EDITOR_WIDTH_PX + 2.0 * (SWAP_BUTTON_WIDTH_PX + SWAP_BUTTON_GAP_PX); 544 assert!(rect.max_x().value() <= viewport.max_x().value()); 545 assert!(rect.min_x().value() + total_width <= viewport.max_x().value() + 1e-3); 546 } 547 548 #[test] 549 fn swap_button_rects_sit_to_right_of_editor() { 550 let editor = LayoutRect::new( 551 LayoutPos::new(LayoutPx::new(50.0), LayoutPx::new(60.0)), 552 LayoutSize::new(LayoutPx::new(140.0), LayoutPx::new(24.0)), 553 ); 554 let radius = swap_button_rect(editor, 0); 555 let diameter = swap_button_rect(editor, 1); 556 assert!(radius.min_x().value() > editor.max_x().value()); 557 assert!(diameter.min_x().value() > radius.max_x().value()); 558 assert!((radius.origin.y.value() - editor.origin.y.value()).abs() < 1e-9); 559 } 560 561 #[test] 562 fn primary_press_outside_bounds_commits_current_value() { 563 use bone_ui::a11y::AccessTreeBuilder; 564 use bone_ui::focus::FocusManager; 565 use bone_ui::hit_test::{HitFrame, HitState}; 566 use bone_ui::hotkey::HotkeyTable; 567 use bone_ui::input::{ 568 FrameInstant, InputSnapshot, PointerButton, PointerButtonMask, PointerSample, 569 }; 570 use bone_ui::strings::StringTable; 571 use bone_ui::theme::Theme; 572 use std::sync::Arc; 573 let mut state = DimensionEditorState::default(); 574 state.reset_for(sample_radius()); 575 let pending = PendingDimension { 576 proto: sample_radius(), 577 anchor: Point2::origin(), 578 }; 579 let camera = sample_camera(); 580 let viewport = LayoutRect::new( 581 LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)), 582 LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)), 583 ); 584 let theme = Arc::new(Theme::light()); 585 let table = HotkeyTable::new(); 586 let mut focus = FocusManager::new(); 587 let mut hits = HitFrame::new(); 588 let prev = HitState::new(); 589 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 590 input.pointer = Some(PointerSample::new(LayoutPos::new( 591 LayoutPx::new(700.0), 592 LayoutPx::new(500.0), 593 ))); 594 input.buttons_pressed = PointerButtonMask::just(PointerButton::Primary); 595 let mut shaper = bone_text::Shaper::new(); 596 let mut a11y = AccessTreeBuilder::new(); 597 let outcome = { 598 let mut ctx = FrameCtx::new( 599 theme, 600 &mut input, 601 &mut focus, 602 &table, 603 StringTable::empty(), 604 &mut hits, 605 &prev, 606 &mut a11y, 607 &mut shaper, 608 ); 609 render( 610 &mut ctx, 611 pending, 612 pending.anchor, 613 &camera, 614 viewport, 615 &mut state, 616 ) 617 }; 618 match outcome.action { 619 DimensionEditorAction::Commit(_) => {} 620 other => panic!("expected Commit, got {other:?}"), 621 } 622 } 623 624 #[test] 625 fn primary_press_inside_editor_does_not_commit() { 626 use bone_ui::a11y::AccessTreeBuilder; 627 use bone_ui::focus::FocusManager; 628 use bone_ui::hit_test::{HitFrame, HitState}; 629 use bone_ui::hotkey::HotkeyTable; 630 use bone_ui::input::{ 631 FrameInstant, InputSnapshot, PointerButton, PointerButtonMask, PointerSample, 632 }; 633 use bone_ui::strings::StringTable; 634 use bone_ui::theme::Theme; 635 use std::sync::Arc; 636 let mut state = DimensionEditorState::default(); 637 state.reset_for(sample_radius()); 638 let pending = PendingDimension { 639 proto: sample_radius(), 640 anchor: Point2::origin(), 641 }; 642 let camera = sample_camera(); 643 let viewport = LayoutRect::new( 644 LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)), 645 LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)), 646 ); 647 let bounds_probe = editor_rect(viewport, &camera, pending.anchor); 648 let inside_x = bounds_probe.min_x().value() + 4.0; 649 let inside_y = bounds_probe.min_y().value() + 4.0; 650 651 let theme = Arc::new(Theme::light()); 652 let table = HotkeyTable::new(); 653 let mut focus = FocusManager::new(); 654 let mut hits = HitFrame::new(); 655 let prev = HitState::new(); 656 let mut input = InputSnapshot::idle(FrameInstant::ZERO); 657 input.pointer = Some(PointerSample::new(LayoutPos::new( 658 LayoutPx::new(inside_x), 659 LayoutPx::new(inside_y), 660 ))); 661 input.buttons_pressed = PointerButtonMask::just(PointerButton::Primary); 662 let mut shaper = bone_text::Shaper::new(); 663 let mut a11y = AccessTreeBuilder::new(); 664 let outcome = { 665 let mut ctx = FrameCtx::new( 666 theme, 667 &mut input, 668 &mut focus, 669 &table, 670 StringTable::empty(), 671 &mut hits, 672 &prev, 673 &mut a11y, 674 &mut shaper, 675 ); 676 render( 677 &mut ctx, 678 pending, 679 pending.anchor, 680 &camera, 681 viewport, 682 &mut state, 683 ) 684 }; 685 let action = &outcome.action; 686 assert!( 687 !matches!(action, DimensionEditorAction::Commit(_)), 688 "press inside editor must not commit, got {action:?}", 689 ); 690 } 691}