use bone_document::{DimensionValue, SketchDimension};
use bone_render::Camera2;
use bone_types::{Angle, Length, Point2};
use bone_ui::frame::FrameCtx;
use bone_ui::input::{NamedKey, PointerButton};
use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use bone_ui::widgets::{
    Button, ButtonState, ButtonVariant, MemoryClipboard, ParsedInput, ParsedInputResponse, TakeKey,
    TextInputState, WidgetPaint, show_button, show_parsed_input, take_key,
};
use bone_ui::{WidgetId, WidgetKey};
use uom::si::angle::degree;
use uom::si::length::millimeter;

use crate::sketch_mode::PendingDimension;
use crate::smart_dimension;
use crate::strings;

const EDITOR_WIDTH_PX: f32 = 140.0;
const EDITOR_HEIGHT_PX: f32 = 24.0;
const EDITOR_OFFSET_X_PX: f32 = 12.0;
const EDITOR_OFFSET_Y_PX: f32 = -12.0;
const SWAP_BUTTON_WIDTH_PX: f32 = 64.0;
const SWAP_BUTTON_GAP_PX: f32 = 4.0;

#[derive(Default)]
pub struct DimensionEditorState {
    pub text: TextInputState,
    pub clipboard: MemoryClipboard,
    pub last_seeded_for: Option<SketchDimension>,
    pub focus_request_pending: bool,
}

impl DimensionEditorState {
    pub fn reset_for(&mut self, proto: SketchDimension) {
        self.text = TextInputState::from_text(format_initial_value(proto.value()));
        self.last_seeded_for = Some(proto);
        self.focus_request_pending = true;
    }

    pub fn close(&mut self) {
        self.text = TextInputState::default();
        self.last_seeded_for = None;
        self.focus_request_pending = false;
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum DimensionEditorAction {
    Idle,
    Commit(DimensionValue),
    Cancel,
    Swap(SketchDimension),
}

pub struct DimensionEditorOutcome {
    pub paints: Vec<WidgetPaint>,
    pub action: DimensionEditorAction,
    pub bounds: LayoutRect,
    pub claimed_pointer: bool,
}

pub fn render(
    ctx: &mut FrameCtx<'_>,
    pending: PendingDimension,
    anchor: Point2,
    camera: &Camera2,
    viewport_rect: LayoutRect,
    state: &mut DimensionEditorState,
) -> DimensionEditorOutcome {
    let editor_id = WidgetId::ROOT.child(WidgetKey::new("dim.editor"));
    let radius_id = editor_id.child(WidgetKey::new("swap.radius"));
    let diameter_id = editor_id.child(WidgetKey::new("swap.diameter"));
    let editor_rect = editor_rect(viewport_rect, camera, anchor);
    let mut paints = Vec::new();
    if state.last_seeded_for != Some(pending.proto) {
        state.reset_for(pending.proto);
    }
    if state.focus_request_pending {
        ctx.request_focus(editor_id);
        state.focus_request_pending = false;
    }
    let placeholder = smart_dimension::placeholder_for(pending.proto.value());
    let parsed = match pending.proto.value() {
        DimensionValue::Length(_) => render_length(ctx, editor_id, editor_rect, placeholder, state),
        DimensionValue::Angle(_) => render_angle(ctx, editor_id, editor_rect, placeholder, state),
    };
    paints.extend(parsed.paint);
    let effective_proto = merge_typed_value(pending.proto, parsed.value);
    let swap = render_swap(ctx, &effective_proto, editor_rect, radius_id, diameter_id);
    paints.extend(swap.paints);
    let canceled = take_key(ctx.input, &[TakeKey::named(NamedKey::Escape)]).is_some();
    let bounds = match swap.bounds {
        Some(swap_rect) => editor_rect.union(swap_rect),
        None => editor_rect,
    };
    let pressed_outside = primary_press_outside(ctx, bounds);
    let action = if canceled {
        DimensionEditorAction::Cancel
    } else if let Some(swapped) = swap.action {
        DimensionEditorAction::Swap(swapped)
    } else if let Some(value) = parsed.committed {
        DimensionEditorAction::Commit(value)
    } else if pressed_outside {
        match parsed.value {
            Some(value) => DimensionEditorAction::Commit(value),
            None => DimensionEditorAction::Cancel,
        }
    } else {
        DimensionEditorAction::Idle
    };
    DimensionEditorOutcome {
        paints,
        action,
        bounds,
        claimed_pointer: pressed_outside,
    }
}

#[must_use]
fn merge_typed_value(proto: SketchDimension, typed: Option<DimensionValue>) -> SketchDimension {
    typed
        .and_then(|v| proto.with_value(v).ok())
        .unwrap_or(proto)
}

fn primary_press_outside(ctx: &FrameCtx<'_>, bounds: LayoutRect) -> bool {
    if !ctx.input.buttons_pressed.contains(PointerButton::Primary) {
        return false;
    }
    let Some(p) = ctx.input.pointer.as_ref() else {
        return false;
    };
    !bounds.contains(p.position)
}

struct ParsedOutcome {
    paint: Vec<WidgetPaint>,
    value: Option<DimensionValue>,
    committed: Option<DimensionValue>,
}

fn render_length(
    ctx: &mut FrameCtx<'_>,
    id: WidgetId,
    rect: LayoutRect,
    placeholder: bone_ui::strings::StringKey,
    state: &mut DimensionEditorState,
) -> ParsedOutcome {
    let widget = ParsedInput::<Length>::new(id, rect, placeholder, &mut state.text);
    let ParsedInputResponse {
        value,
        committed,
        paint,
        ..
    } = show_parsed_input(ctx, widget, &mut state.clipboard);
    ParsedOutcome {
        paint,
        value: value.map(DimensionValue::Length),
        committed: committed.map(DimensionValue::Length),
    }
}

fn render_angle(
    ctx: &mut FrameCtx<'_>,
    id: WidgetId,
    rect: LayoutRect,
    placeholder: bone_ui::strings::StringKey,
    state: &mut DimensionEditorState,
) -> ParsedOutcome {
    let widget = ParsedInput::<Angle>::new(id, rect, placeholder, &mut state.text);
    let ParsedInputResponse {
        value,
        committed,
        paint,
        ..
    } = show_parsed_input(ctx, widget, &mut state.clipboard);
    ParsedOutcome {
        paint,
        value: value.map(DimensionValue::Angle),
        committed: committed.map(DimensionValue::Angle),
    }
}

struct SwapOutcome {
    paints: Vec<WidgetPaint>,
    action: Option<SketchDimension>,
    bounds: Option<LayoutRect>,
}

fn render_swap(
    ctx: &mut FrameCtx<'_>,
    proto: &SketchDimension,
    editor_rect: LayoutRect,
    radius_id: WidgetId,
    diameter_id: WidgetId,
) -> SwapOutcome {
    let (is_radius, is_diameter) = match proto {
        SketchDimension::Radius { .. } => (true, false),
        SketchDimension::Diameter { .. } => (false, true),
        SketchDimension::Linear { .. } | SketchDimension::Angular { .. } => {
            return SwapOutcome {
                paints: Vec::new(),
                action: None,
                bounds: None,
            };
        }
    };
    let radius_rect = swap_button_rect(editor_rect, 0);
    let diameter_rect = swap_button_rect(editor_rect, 1);
    let radius_btn = Button::new(
        radius_id,
        radius_rect,
        strings::TOOL_RADIUS,
        if is_radius {
            ButtonVariant::Primary
        } else {
            ButtonVariant::Secondary
        },
    )
    .with_state(if is_radius {
        ButtonState::Disabled
    } else {
        ButtonState::Idle
    });
    let diameter_btn = Button::new(
        diameter_id,
        diameter_rect,
        strings::TOOL_DIAMETER,
        if is_diameter {
            ButtonVariant::Primary
        } else {
            ButtonVariant::Secondary
        },
    )
    .with_state(if is_diameter {
        ButtonState::Disabled
    } else {
        ButtonState::Idle
    });
    let radius_resp = show_button(ctx, radius_btn);
    let diameter_resp = show_button(ctx, diameter_btn);
    let mut paints = Vec::new();
    paints.extend(radius_resp.paint);
    paints.extend(diameter_resp.paint);
    let action = if radius_resp.activated || diameter_resp.activated {
        smart_dimension::swap_radius_diameter(*proto)
    } else {
        None
    };
    SwapOutcome {
        paints,
        action,
        bounds: Some(radius_rect.union(diameter_rect)),
    }
}

fn editor_rect(viewport_rect: LayoutRect, camera: &Camera2, anchor: Point2) -> LayoutRect {
    let LayoutPos { x: ax, y: ay } = world_to_screen(camera, anchor);
    let raw_x = ax.value() + EDITOR_OFFSET_X_PX;
    let raw_y = ay.value() + EDITOR_OFFSET_Y_PX;
    let total_width = EDITOR_WIDTH_PX + 2.0 * (SWAP_BUTTON_WIDTH_PX + SWAP_BUTTON_GAP_PX);
    let min_x = viewport_rect.min_x().value();
    let min_y = viewport_rect.min_y().value();
    let max_x = (min_x + viewport_rect.size.width.value() - total_width).max(min_x);
    let max_y = (min_y + viewport_rect.size.height.value() - EDITOR_HEIGHT_PX).max(min_y);
    let x = raw_x.clamp(min_x, max_x);
    let y = raw_y.clamp(min_y, max_y);
    LayoutRect::new(
        LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)),
        LayoutSize::new(
            LayoutPx::new(EDITOR_WIDTH_PX),
            LayoutPx::new(EDITOR_HEIGHT_PX),
        ),
    )
}

fn swap_button_rect(editor_rect: LayoutRect, index: u32) -> LayoutRect {
    #[allow(
        clippy::cast_precision_loss,
        reason = "swap-button index is 0 or 1; f32 round-trip is exact"
    )]
    let offset = editor_rect.size.width.value()
        + SWAP_BUTTON_GAP_PX
        + (index as f32) * (SWAP_BUTTON_WIDTH_PX + SWAP_BUTTON_GAP_PX);
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(editor_rect.min_x().value() + offset),
            editor_rect.origin.y,
        ),
        LayoutSize::new(LayoutPx::new(SWAP_BUTTON_WIDTH_PX), editor_rect.size.height),
    )
}

#[allow(
    clippy::cast_possible_truncation,
    reason = "world-to-screen px collapses f64 to LayoutPx (f32) at the sub-pixel limit"
)]
fn world_to_screen(camera: &Camera2, world: Point2) -> LayoutPos {
    let extent = camera.extent();
    let w = f64::from(extent.width().value());
    let h = f64::from(extent.height().value());
    let zoom = camera.zoom().value();
    let (pan_x, pan_y) = camera.pan_mm().coords_mm();
    let (wx, wy) = world.coords_mm();
    let cursor_x = w * 0.5 + (wx - pan_x) * zoom;
    let cursor_y = h * 0.5 - (wy - pan_y) * zoom;
    LayoutPos::new(
        LayoutPx::saturating(cursor_x as f32),
        LayoutPx::saturating(cursor_y as f32),
    )
}

fn format_initial_value(value: DimensionValue) -> String {
    match value {
        DimensionValue::Length(l) => format!("{:.3}", l.get::<millimeter>()),
        DimensionValue::Angle(a) => format!("{:.3}", a.get::<degree>()),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_document::{DimensionKind, SketchDimension};
    use bone_render::{PixelsPerMm, ViewportExtent, ViewportPx};
    use bone_types::{Length, SketchEntityId};
    use uom::si::length::millimeter;

    fn sample_radius() -> SketchDimension {
        SketchDimension::Radius {
            target: SketchEntityId::default(),
            value: Length::new::<millimeter>(3.0),
            kind: DimensionKind::Driving,
        }
    }

    fn sample_camera() -> Camera2 {
        let extent = ViewportExtent::new(ViewportPx::new(800), ViewportPx::new(600));
        Camera2::new(extent).with_zoom(PixelsPerMm::new(10.0))
    }

    #[test]
    fn merge_typed_value_returns_proto_when_no_typed_input() {
        let proto = sample_radius();
        assert_eq!(merge_typed_value(proto, None), proto);
    }

    #[test]
    fn merge_typed_value_overrides_proto_value_when_typed_matches() {
        let proto = sample_radius();
        let typed = DimensionValue::Length(Length::new::<millimeter>(5.0));
        let merged = merge_typed_value(proto, Some(typed));
        let SketchDimension::Radius { value, .. } = merged else {
            panic!("expected Radius");
        };
        assert!((value.get::<millimeter>() - 5.0).abs() < 1e-9);
    }

    #[test]
    fn merge_typed_value_falls_back_when_typed_kind_mismatches() {
        let proto = sample_radius();
        let bad = DimensionValue::Angle(bone_types::Angle::new::<uom::si::angle::radian>(1.0));
        assert_eq!(merge_typed_value(proto, Some(bad)), proto);
    }

    #[test]
    fn editor_state_reset_seeds_text_from_value() {
        let mut state = DimensionEditorState::default();
        state.reset_for(sample_radius());
        assert_eq!(state.text.text, "3.000");
        assert_eq!(state.last_seeded_for, Some(sample_radius()));
    }

    #[test]
    fn editor_state_close_drops_seeded_proto() {
        let mut state = DimensionEditorState::default();
        state.reset_for(sample_radius());
        state.close();
        assert_eq!(state.last_seeded_for, None);
        assert_eq!(state.text.text, "");
    }

    #[test]
    fn second_frame_keeps_seeded_text_after_first_render() {
        use bone_ui::a11y::AccessTreeBuilder;
        use bone_ui::focus::FocusManager;
        use bone_ui::hit_test::{HitFrame, HitState, resolve};
        use bone_ui::hotkey::HotkeyTable;
        use bone_ui::input::{FrameInstant, InputSnapshot};
        use bone_ui::strings::StringTable;
        use bone_ui::theme::Theme;
        use bone_ui::widgets::{LabelText, WidgetPaint};
        use std::sync::Arc;
        let mut state = DimensionEditorState::default();
        let pending = PendingDimension {
            proto: sample_radius(),
            anchor: Point2::origin(),
        };
        let camera = sample_camera();
        let viewport = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
            LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)),
        );
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hit_state = HitState::new();
        let strings = StringTable::empty();
        let run_one =
            |state: &mut DimensionEditorState, focus: &mut FocusManager, prev_hit: &HitState| {
                let mut hits = HitFrame::new();
                let mut input = InputSnapshot::idle(FrameInstant::ZERO);
                let mut shaper = bone_text::Shaper::new();
                let mut a11y = AccessTreeBuilder::new();
                let outcome = {
                    let mut ctx = FrameCtx::new(
                        Arc::clone(&theme),
                        &mut input,
                        focus,
                        &table,
                        strings,
                        &mut hits,
                        prev_hit,
                        &mut a11y,
                        &mut shaper,
                    );
                    render(&mut ctx, pending, pending.anchor, &camera, viewport, state)
                };
                let next_hit = resolve(prev_hit, &hits, &input, focus.focused());
                (outcome, next_hit)
            };
        let (_, next_hit) = run_one(&mut state, &mut focus, &hit_state);
        hit_state = next_hit;
        let (outcome2, _) = run_one(&mut state, &mut focus, &hit_state);
        let any_label = outcome2.paints.iter().any(|p| {
            matches!(
                p,
                WidgetPaint::Label { text: LabelText::Owned(t), .. } if t == "3.000",
            )
        });
        assert!(any_label, "second frame must still paint the seeded value");
    }

    #[test]
    fn render_emits_surface_and_label_for_seeded_state() {
        use bone_ui::a11y::AccessTreeBuilder;
        use bone_ui::focus::FocusManager;
        use bone_ui::hit_test::{HitFrame, HitState};
        use bone_ui::hotkey::HotkeyTable;
        use bone_ui::input::{FrameInstant, InputSnapshot};
        use bone_ui::strings::StringTable;
        use bone_ui::theme::Theme;
        use bone_ui::widgets::{LabelText, WidgetPaint};
        use std::sync::Arc;
        let mut state = DimensionEditorState::default();
        let pending = PendingDimension {
            proto: sample_radius(),
            anchor: Point2::origin(),
        };
        let camera = sample_camera();
        let viewport = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
            LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)),
        );
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let outcome = render(
            &mut ctx,
            pending,
            pending.anchor,
            &camera,
            viewport,
            &mut state,
        );
        let any_label_with_text = outcome.paints.iter().any(|p| {
            matches!(
                p,
                WidgetPaint::Label { text: LabelText::Owned(t), .. } if t == "3.000",
            )
        });
        assert!(any_label_with_text, "popup must paint the seeded value");
        let any_surface = outcome
            .paints
            .iter()
            .any(|p| matches!(p, WidgetPaint::Surface { .. }));
        assert!(any_surface, "popup must paint a surface fill");
    }

    #[test]
    fn world_to_screen_at_pan_origin_is_window_center() {
        let camera = sample_camera();
        let p = world_to_screen(&camera, Point2::origin());
        assert!((p.x.value() - 400.0).abs() < 0.1);
        assert!((p.y.value() - 300.0).abs() < 0.1);
    }

    #[test]
    fn world_to_screen_inverts_y() {
        let camera = sample_camera();
        let above = world_to_screen(&camera, Point2::from_mm(0.0, 5.0));
        let below = world_to_screen(&camera, Point2::from_mm(0.0, -5.0));
        assert!(above.y.value() < below.y.value());
    }

    #[test]
    fn editor_rect_does_not_panic_when_viewport_is_narrower_than_editor() {
        let viewport = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(250.0), LayoutPx::new(40.0)),
            LayoutSize::new(LayoutPx::new(120.0), LayoutPx::new(60.0)),
        );
        let camera = sample_camera();
        let rect = editor_rect(viewport, &camera, Point2::origin());
        assert!(rect.min_x().value() >= viewport.min_x().value());
        assert!(rect.min_y().value() >= viewport.min_y().value());
    }

    #[test]
    fn editor_rect_clamps_inside_viewport_when_anchor_is_far_right() {
        let viewport = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
            LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)),
        );
        let camera = sample_camera();
        let anchor = Point2::from_mm(100.0, 0.0);
        let rect = editor_rect(viewport, &camera, anchor);
        let total_width = EDITOR_WIDTH_PX + 2.0 * (SWAP_BUTTON_WIDTH_PX + SWAP_BUTTON_GAP_PX);
        assert!(rect.max_x().value() <= viewport.max_x().value());
        assert!(rect.min_x().value() + total_width <= viewport.max_x().value() + 1e-3);
    }

    #[test]
    fn swap_button_rects_sit_to_right_of_editor() {
        let editor = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(50.0), LayoutPx::new(60.0)),
            LayoutSize::new(LayoutPx::new(140.0), LayoutPx::new(24.0)),
        );
        let radius = swap_button_rect(editor, 0);
        let diameter = swap_button_rect(editor, 1);
        assert!(radius.min_x().value() > editor.max_x().value());
        assert!(diameter.min_x().value() > radius.max_x().value());
        assert!((radius.origin.y.value() - editor.origin.y.value()).abs() < 1e-9);
    }

    #[test]
    fn primary_press_outside_bounds_commits_current_value() {
        use bone_ui::a11y::AccessTreeBuilder;
        use bone_ui::focus::FocusManager;
        use bone_ui::hit_test::{HitFrame, HitState};
        use bone_ui::hotkey::HotkeyTable;
        use bone_ui::input::{
            FrameInstant, InputSnapshot, PointerButton, PointerButtonMask, PointerSample,
        };
        use bone_ui::strings::StringTable;
        use bone_ui::theme::Theme;
        use std::sync::Arc;
        let mut state = DimensionEditorState::default();
        state.reset_for(sample_radius());
        let pending = PendingDimension {
            proto: sample_radius(),
            anchor: Point2::origin(),
        };
        let camera = sample_camera();
        let viewport = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
            LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)),
        );
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(700.0),
            LayoutPx::new(500.0),
        )));
        input.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let outcome = {
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            render(
                &mut ctx,
                pending,
                pending.anchor,
                &camera,
                viewport,
                &mut state,
            )
        };
        match outcome.action {
            DimensionEditorAction::Commit(_) => {}
            other => panic!("expected Commit, got {other:?}"),
        }
    }

    #[test]
    fn primary_press_inside_editor_does_not_commit() {
        use bone_ui::a11y::AccessTreeBuilder;
        use bone_ui::focus::FocusManager;
        use bone_ui::hit_test::{HitFrame, HitState};
        use bone_ui::hotkey::HotkeyTable;
        use bone_ui::input::{
            FrameInstant, InputSnapshot, PointerButton, PointerButtonMask, PointerSample,
        };
        use bone_ui::strings::StringTable;
        use bone_ui::theme::Theme;
        use std::sync::Arc;
        let mut state = DimensionEditorState::default();
        state.reset_for(sample_radius());
        let pending = PendingDimension {
            proto: sample_radius(),
            anchor: Point2::origin(),
        };
        let camera = sample_camera();
        let viewport = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(0.0), LayoutPx::new(0.0)),
            LayoutSize::new(LayoutPx::new(800.0), LayoutPx::new(600.0)),
        );
        let bounds_probe = editor_rect(viewport, &camera, pending.anchor);
        let inside_x = bounds_probe.min_x().value() + 4.0;
        let inside_y = bounds_probe.min_y().value() + 4.0;

        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(inside_x),
            LayoutPx::new(inside_y),
        )));
        input.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let outcome = {
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            render(
                &mut ctx,
                pending,
                pending.anchor,
                &camera,
                viewport,
                &mut state,
            )
        };
        let action = &outcome.action;
        assert!(
            !matches!(action, DimensionEditorAction::Commit(_)),
            "press inside editor must not commit, got {action:?}",
        );
    }
}
