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