use rs_grid_core::{
    commands::GridCommand,
    scrollbar::{HScrollbarGeom, ScrollbarGeom},
};
use wasm_bindgen::{JsCast, prelude::Closure};
use web_sys::{MouseEvent, ResizeObserver, WheelEvent};

use super::{
    ActiveDrag, GridCanvas, HThumbDrag, ThumbDrag,
    context_menu::remove_ctx_menu, dom_helpers::document,
};

impl GridCanvas {
    pub(super) fn attach_resize_observer(&self) {
        let gc = self.clone();

        let cb = Closure::<dyn FnMut(js_sys::Array)>::new(
            move |_entries: js_sys::Array| {
                let canvas = &gc.0.canvas;
                let win = web_sys::window().expect("no window");
                let dpr = win.device_pixel_ratio();

                let css_w = canvas.client_width() as f64;
                let css_h = canvas.client_height() as f64;

                if css_w <= 0.0 || css_h <= 0.0 {
                    return;
                }

                let new_w = (css_w * dpr) as u32;
                let new_h = (css_h * dpr) as u32;

                // Only reset the canvas when physical dimensions
                // actually change.  set_width/set_height wipe all
                // pixels to transparent, which can produce a
                // visible flash if the browser paints between the
                // clear and the subsequent render() call.
                if canvas.width() != new_w || canvas.height() != new_h {
                    canvas.set_width(new_w);
                    canvas.set_height(new_h);
                }

                gc.0.builder.borrow_mut().dpr = dpr;

                gc.dispatch(GridCommand::Resize {
                    width: css_w,
                    height: css_h,
                });
            },
        );

        let observer = ResizeObserver::new(cb.as_ref().unchecked_ref())
            .expect("ResizeObserver::new");
        observer.observe(&self.0.canvas);

        *self.0._resize_closure.borrow_mut() = Some(cb);
        *self.0._resize_observer.borrow_mut() = Some(observer);
    }

    pub(super) fn attach_listeners(&self) {
        self.attach_wheel();
        self.attach_mousedown();
        self.attach_mouseleave();
        self.attach_dblclick();
        self.attach_contextmenu();
        self.attach_mousemove();
        self.attach_mouseup();
        self.attach_keydown();
        self.attach_copy();
        self.attach_cut();
        self.attach_paste();
    }

    fn attach_wheel(&self) {
        let gc = self.clone();
        let cb = Closure::<dyn FnMut(_)>::new(move |evt: WheelEvent| {
            evt.prevent_default();

            // Close any active cell editor on scroll.
            gc.cancel_and_close_edit();

            // Convert delta to logical pixels regardless of unit.
            let (dx, dy) = {
                let state = gc.0.state.borrow();
                match evt.delta_mode() {
                    // DOM_DELTA_LINE — Firefox mouse wheel (3 lines/notch)
                    1 => {
                        let lh = state.model.row_height;
                        (evt.delta_x() * lh, evt.delta_y() * lh)
                    }
                    // DOM_DELTA_PAGE
                    2 => {
                        let h = state.viewport.height;
                        (evt.delta_x() * h, evt.delta_y() * h)
                    }
                    // DOM_DELTA_PIXEL (0) — Chrome/Safari (trackpad + wheel)
                    _ => (evt.delta_x(), evt.delta_y()),
                }
            };

            if evt.delta_mode() == 0 {
                // Pixel mode = trackpad: the OS already supplies
                // inertia — scroll directly, no software momentum.
                gc.dispatch(GridCommand::ScrollBy { dx, dy });
            } else {
                // Line / page mode = mouse wheel: accumulate velocity
                // so render_immediate() can apply a smooth deceleration.
                const MAX_V: f64 = 250.0;
                gc.0.scroll_vx
                    .set((gc.0.scroll_vx.get() + dx).clamp(-MAX_V, MAX_V));
                gc.0.scroll_vy
                    .set((gc.0.scroll_vy.get() + dy).clamp(-MAX_V, MAX_V));
                gc.render();
            }
        });
        let f: js_sys::Function =
            cb.as_ref().unchecked_ref::<js_sys::Function>().clone();
        // Browsers default wheel to passive — we must
        // opt out so preventDefault() actually works.
        let opts = web_sys::AddEventListenerOptions::new();
        opts.set_passive(false);
        self.0
            .canvas
            .add_event_listener_with_callback_and_add_event_listener_options(
                "wheel", &f, &opts,
            )
            .expect("add wheel listener");
        self.0
            .canvas_listeners
            .borrow_mut()
            .push(("wheel".to_string(), f));
        self.0.closures.borrow_mut().push(Box::new(cb));
    }

    fn attach_mouseleave(&self) {
        let gc = self.clone();
        let cb = Closure::<dyn FnMut(_)>::new(move |_: MouseEvent| {
            if gc.0.state.borrow().hovered_row.is_some() {
                gc.dispatch(GridCommand::SetHoveredRow(None));
            }
            if gc.0.hovered_menu_col.borrow().is_some() {
                *gc.0.hovered_menu_col.borrow_mut() = None;
                gc.render();
            }
        });
        let f: js_sys::Function =
            cb.as_ref().unchecked_ref::<js_sys::Function>().clone();
        self.0
            .canvas
            .add_event_listener_with_callback("mouseleave", &f)
            .expect("add mouseleave listener");
        self.0
            .canvas_listeners
            .borrow_mut()
            .push(("mouseleave".to_string(), f));
        self.0.closures.borrow_mut().push(Box::new(cb));
    }

    fn attach_dblclick(&self) {
        let gc = self.clone();
        let cb = Closure::<dyn FnMut(_)>::new(move |evt: MouseEvent| {
            let (x, y) = gc.canvas_xy(&evt);
            // Double-click on column separator → auto-fit width
            if let Some(col_idx) = gc.hit_col_resize_separator(x, y) {
                let (cw, hcw, cp, hrr) = gc.autofit_params();
                gc.dispatch(GridCommand::AutoFitColumn {
                    col_idx,
                    char_width: cw,
                    header_char_width: hcw,
                    cell_padding: cp,
                    header_right_reserve: hrr,
                });
                return;
            }
            let coord = gc.0.state.borrow().hit_test(x, y);
            if let Some(coord) = coord {
                let col_key =
                    gc.0.state.borrow().model.columns[coord.col].key.clone();
                gc.dispatch(GridCommand::StartEdit {
                    row: coord.row,
                    col_key,
                });
                gc.show_edit_input();
            }
        });
        let f: js_sys::Function =
            cb.as_ref().unchecked_ref::<js_sys::Function>().clone();
        self.0
            .canvas
            .add_event_listener_with_callback("dblclick", &f)
            .expect("add dblclick listener");
        self.0
            .canvas_listeners
            .borrow_mut()
            .push(("dblclick".to_string(), f));
        self.0.closures.borrow_mut().push(Box::new(cb));
    }

    fn attach_mousedown(&self) {
        let gc = self.clone();
        let cb = Closure::<dyn FnMut(_)>::new(move |evt: MouseEvent| {
            // Stop any active scroll momentum so a click always
            // lands on the row the user sees, not one that is
            // still drifting.
            gc.0.scroll_vx.set(0.0);
            gc.0.scroll_vy.set(0.0);

            // Claim focus so keydown / clipboard handlers know
            // this grid is the active target.
            let _ = gc.0.canvas.focus();

            // Right-click is handled by contextmenu event
            if evt.button() == 2 {
                return;
            }

            // Middle-click → browser-style autoscroll
            if evt.button() == 1 {
                evt.prevent_default();
                if matches!(*gc.0.drag.borrow(), Some(ActiveDrag::Pan { .. })) {
                    gc.stop_pan();
                    return;
                }
                let cx = evt.client_x() as f64;
                let cy = evt.client_y() as f64;
                *gc.0.pan_mouse.borrow_mut() = (cx, cy);
                *gc.0.drag.borrow_mut() = Some(ActiveDrag::Pan {
                    origin_x: cx,
                    origin_y: cy,
                });
                let _ =
                    gc.0.canvas
                        .style()
                        .set_property("cursor", GridCanvas::pan_cursor());
                let gc2 = gc.clone();
                let cb2 = Closure::<dyn FnMut()>::new(move || {
                    let (ox, oy) = match *gc2.0.drag.borrow() {
                        Some(ActiveDrag::Pan { origin_x, origin_y }) => {
                            (origin_x, origin_y)
                        }
                        _ => return,
                    };
                    let (mx, my) = *gc2.0.pan_mouse.borrow();
                    const DEADZONE: f64 = 8.0;
                    const SPEED: f64 = 0.04;
                    let raw_dx = mx - ox;
                    let raw_dy = my - oy;
                    let dx = if raw_dx.abs() > DEADZONE {
                        (raw_dx.abs() - DEADZONE)
                            * raw_dx.signum()
                            * SPEED
                            * 16.0
                    } else {
                        0.0
                    };
                    let dy = if raw_dy.abs() > DEADZONE {
                        (raw_dy.abs() - DEADZONE)
                            * raw_dy.signum()
                            * SPEED
                            * 16.0
                    } else {
                        0.0
                    };
                    if dx != 0.0 || dy != 0.0 {
                        gc2.dispatch(GridCommand::ScrollBy { dx, dy });
                    }
                });
                let win2 = web_sys::window().expect("no window");
                let id = win2
                    .set_interval_with_callback_and_timeout_and_arguments_0(
                        cb2.as_ref().unchecked_ref(),
                        16,
                    )
                    .expect("setInterval");
                *gc.0.scroll_interval.borrow_mut() = Some(id);
                gc.0.scroll_closures.borrow_mut().push(Box::new(cb2));
                return;
            }

            let (x, y) = gc.canvas_xy(&evt);

            // Close any open context menu
            remove_ctx_menu();

            // ── scrollbar interaction ─────────────────────
            if let Some(sb) = gc.scrollbar() {
                if sb.hit_up_arrow(x, y) {
                    let row_h = gc.0.state.borrow().model.row_height;
                    gc.dispatch(GridCommand::ScrollBy {
                        dx: 0.0,
                        dy: -row_h,
                    });
                    gc.start_scroll_repeat(-row_h);
                    return;
                }
                if sb.hit_down_arrow(x, y) {
                    let row_h = gc.0.state.borrow().model.row_height;
                    gc.dispatch(GridCommand::ScrollBy { dx: 0.0, dy: row_h });
                    gc.start_scroll_repeat(row_h);
                    return;
                }
                if sb.hit_thumb(x, y) {
                    *gc.0.drag.borrow_mut() =
                        Some(ActiveDrag::Thumb(ThumbDrag {
                            start_client_y: evt.client_y() as f64,
                            start_scroll_y: gc
                                .0
                                .state
                                .borrow()
                                .viewport
                                .scroll_y,
                        }));
                    return;
                }
                if sb.hit_track(x, y) {
                    let going_down = y > sb.thumb_y + sb.thumb_h * 0.5;
                    gc.start_track_scroll_repeat(y, going_down);
                    return;
                }
            }

            // ── horizontal scrollbar interaction ──────────
            if let Some(hsb) = gc.hscrollbar() {
                if hsb.hit_left_arrow(x, y) {
                    let col_w =
                        gc.0.state
                            .borrow()
                            .model
                            .columns
                            .first()
                            .map_or(40.0, |c| c.width);
                    gc.dispatch(GridCommand::ScrollBy {
                        dx: -col_w,
                        dy: 0.0,
                    });
                    gc.start_scroll_repeat_x(-col_w);
                    return;
                }
                if hsb.hit_right_arrow(x, y) {
                    let col_w =
                        gc.0.state
                            .borrow()
                            .model
                            .columns
                            .first()
                            .map_or(40.0, |c| c.width);
                    gc.dispatch(GridCommand::ScrollBy { dx: col_w, dy: 0.0 });
                    gc.start_scroll_repeat_x(col_w);
                    return;
                }
                if hsb.hit_thumb(x, y) {
                    *gc.0.drag.borrow_mut() =
                        Some(ActiveDrag::HThumb(HThumbDrag {
                            start_client_x: evt.client_x() as f64,
                            start_scroll_x: gc
                                .0
                                .state
                                .borrow()
                                .viewport
                                .scroll_x,
                        }));
                    return;
                }
                if hsb.hit_track(x, y) {
                    let going_right = x > hsb.thumb_x + hsb.thumb_w * 0.5;
                    gc.start_htrack_scroll_repeat(x, going_right);
                    return;
                }
            }

            // ── row header selection ──────────────────────
            let row = gc.0.state.borrow().hit_test_row_header(x, y);
            if let Some(row) = row {
                if evt.shift_key() {
                    gc.dispatch(GridCommand::ExtendRowSelection(row));
                } else {
                    gc.dispatch(GridCommand::SelectRow(row));
                }
                *gc.0.drag.borrow_mut() = Some(ActiveDrag::Row);
                return;
            }

            // ── column resize separator ───────────────────
            if let Some(col_idx) = gc.hit_col_resize_separator(x, y) {
                let state = gc.0.state.borrow();
                let start_width = state.model.columns[col_idx].width;
                let start_flex = state.model.columns[col_idx].flex;
                drop(state);
                *gc.0.drag.borrow_mut() = Some(ActiveDrag::ColumnResize {
                    col_idx,
                    start_client_x: evt.client_x() as f64,
                    start_width,
                    start_flex,
                });
                gc.set_cursor("w-resize");
                return;
            }

            // ── column header ─────────────────────────────
            let col = gc.0.state.borrow().hit_test_col_header(x, y);
            if let Some(col) = col {
                // Three-dot icon at right edge → open header menu.
                if gc.hit_header_menu_icon(x, y).is_some() {
                    let (ax, ay) = gc.menu_icon_anchor(col);
                    let rect = gc.0.canvas.get_bounding_client_rect();
                    gc.show_col_header_menu(
                        col,
                        (rect.left() + ax) as i32,
                        (rect.top() + ay) as i32,
                    );
                    return;
                }
                // Shift+Click is reserved for multi-column sort (not yet
                // implemented). Treat it identically to a plain click for
                // now so it doesn't accidentally trigger a cell-range
                // extension into the column — behaviour that AG Grid and
                // most grids do not exhibit.
                *gc.0.drag.borrow_mut() = Some(ActiveDrag::ColClick {
                    col_idx: col,
                    start_client_x: evt.client_x() as f64,
                });
                return;
            }

            // ── cell button hit-test ──────────────────────
            //
            // Check button zones from the last rendered frame
            // before falling through to cell selection.
            // button_id and col index are cloned before
            // releasing the borrow of last_frame.
            {
                let hit =
                    gc.0.last_frame
                        .borrow()
                        .as_ref()
                        .and_then(|f| f.hit_button(x, y))
                        .map(|z| (z.row, z.col, z.button_id.clone()));
                if let Some((row, col_idx, btn_id)) = hit {
                    let col_key =
                        gc.0.state
                            .borrow()
                            .model
                            .columns
                            .get(col_idx)
                            .map(|c| c.key.clone());
                    if let Some(key) = col_key {
                        let cb = gc.0.on_cell_button_click.borrow().clone();
                        if let Some(cb) = cb {
                            cb(row, &key, &btn_id);
                        }
                    }
                    return;
                }
            }

            // ── cell selection ────────────────────────────
            let coord = gc.0.state.borrow().hit_test(x, y);
            if let Some(coord) = coord {
                if evt.shift_key() {
                    gc.dispatch(GridCommand::ExtendSelection(coord));
                } else {
                    gc.dispatch(GridCommand::SelectCell(coord));
                }
                *gc.0.drag.borrow_mut() = Some(ActiveDrag::Cell);
            }
        });
        let f: js_sys::Function =
            cb.as_ref().unchecked_ref::<js_sys::Function>().clone();
        self.0
            .canvas
            .add_event_listener_with_callback("mousedown", &f)
            .expect("add mousedown listener");
        self.0
            .canvas_listeners
            .borrow_mut()
            .push(("mousedown".to_string(), f));
        self.0.closures.borrow_mut().push(Box::new(cb));
    }

    fn attach_mousemove(&self) {
        let gc = self.clone();
        let cb = Closure::<dyn FnMut(_)>::new(move |evt: MouseEvent| {
            let drag = gc.0.drag.borrow();
            match *drag {
                Some(ActiveDrag::Thumb(ref ds)) => {
                    let dy = evt.client_y() as f64 - ds.start_client_y;
                    let start_scroll_y = ds.start_scroll_y;
                    drop(drag);

                    let new_scroll_y = {
                        let s = gc.0.state.borrow();
                        let track_w =
                            gc.0.builder.borrow().theme.scrollbar_width;
                        // Recompute from the drag-start scroll so the
                        // start-thumb offset is stable under non-linear
                        // mappings (e.g. logarithmic scale).
                        if let Some(sb) = ScrollbarGeom::compute(
                            start_scroll_y,
                            s.viewport.width,
                            s.viewport.height,
                            s.model.header_height,
                            s.model.total_height(),
                            track_w,
                        ) {
                            sb.drag_to_scroll(
                                start_scroll_y,
                                dy,
                                s.model.total_height(),
                                s.viewport.height,
                                s.model.header_height,
                            )
                        } else {
                            return;
                        }
                    };

                    let current_x = gc.0.state.borrow().viewport.scroll_x;
                    gc.dispatch(GridCommand::ScrollTo {
                        x: current_x,
                        y: new_scroll_y,
                    });
                }
                Some(ActiveDrag::HThumb(ref ds)) => {
                    let dx = evt.client_x() as f64 - ds.start_client_x;
                    let start_scroll_x = ds.start_scroll_x;
                    drop(drag);
                    let new_scroll_x = {
                        let s = gc.0.state.borrow();
                        let track_h =
                            gc.0.builder.borrow().theme.scrollbar_width;
                        let vsb_w = if ScrollbarGeom::compute(
                            s.viewport.scroll_y,
                            s.viewport.width,
                            s.viewport.height,
                            s.model.header_height,
                            s.model.total_height(),
                            track_h,
                        )
                        .is_some()
                        {
                            track_h
                        } else {
                            0.0
                        };
                        // Recompute from drag-start scroll for a stable
                        // start-thumb offset under non-linear mappings.
                        if let Some(hsb) = HScrollbarGeom::compute(
                            start_scroll_x,
                            s.viewport.width,
                            s.viewport.height,
                            s.model.row_number_width,
                            s.model.total_width(),
                            vsb_w,
                            track_h,
                        ) {
                            hsb.drag_to_scroll(
                                start_scroll_x,
                                dx,
                                s.model.total_width(),
                                s.viewport.width,
                                s.model.row_number_width,
                                vsb_w,
                            )
                        } else {
                            return;
                        }
                    };
                    let current_y = gc.0.state.borrow().viewport.scroll_y;
                    gc.dispatch(GridCommand::ScrollTo {
                        x: new_scroll_x,
                        y: current_y,
                    });
                }
                Some(ActiveDrag::Cell) => {
                    drop(drag);
                    let (x, y) = gc.canvas_xy(&evt);
                    // Keep last position for the auto-scroll interval.
                    gc.0.drag_last_pos.set((x, y));
                    // Extend selection to the cell under the cursor.
                    // Use an explicit `let` so the immutable borrow from
                    // hit_test() is dropped before dispatch() borrows mutably.
                    let coord = gc.0.state.borrow().hit_test(x, y);
                    if let Some(coord) = coord {
                        gc.dispatch(GridCommand::ExtendSelection(coord));
                    }
                    // Start/stop edge auto-scroll based on cursor proximity
                    // to the viewport edges.
                    gc.update_cell_drag_scroll(x, y);
                }
                Some(ActiveDrag::Row) => {
                    drop(drag);
                    let (x, y) = gc.canvas_xy(&evt);
                    let row = gc.0.state.borrow().hit_test_row_header(x, y);
                    if let Some(row) = row {
                        gc.dispatch(GridCommand::ExtendRowSelection(row));
                    }
                }
                Some(ActiveDrag::ColClick {
                    col_idx,
                    start_client_x,
                }) => {
                    let (ci, scx) = (col_idx, start_client_x);
                    drop(drag);
                    let dx = evt.client_x() as f64 - scx;
                    let reorder_enabled =
                        gc.0.state.borrow().model.column_reorderable;
                    if dx.abs() > 5.0 && reorder_enabled {
                        let (vx, vy) = gc.canvas_xy(&evt);
                        // Seed animation from real column positions
                        // so the lerp starts from the actual layout.
                        {
                            let state = gc.0.state.borrow();
                            let mut cum = 0.0_f64;
                            let init: Vec<f64> = state
                                .model
                                .columns
                                .iter()
                                .map(|c| {
                                    let off = cum;
                                    cum += c.width;
                                    off
                                })
                                .collect();
                            drop(state);
                            *gc.0.drag_col_offsets.borrow_mut() = init;
                        }
                        *gc.0.drag.borrow_mut() =
                            Some(ActiveDrag::ColumnDrag {
                                col_idx: ci,
                                current_vx: vx,
                                current_vy: vy,
                            });
                        gc.set_cursor("grabbing");
                        gc.render();
                    }
                }
                Some(ActiveDrag::ColumnDrag { col_idx, .. }) => {
                    let ci = col_idx;
                    drop(drag);
                    let (vx, vy) = gc.canvas_xy(&evt);
                    *gc.0.drag.borrow_mut() = Some(ActiveDrag::ColumnDrag {
                        col_idx: ci,
                        current_vx: vx,
                        current_vy: vy,
                    });
                    gc.set_cursor("grabbing");
                    gc.render();
                }
                Some(ActiveDrag::Pan { .. }) => {
                    drop(drag);
                    *gc.0.pan_mouse.borrow_mut() =
                        (evt.client_x() as f64, evt.client_y() as f64);
                }
                Some(ActiveDrag::ColumnResize {
                    col_idx,
                    start_client_x,
                    start_width,
                    ..
                }) => {
                    let (ci, scx, sw) = (col_idx, start_client_x, start_width);
                    drop(drag);
                    let dx = evt.client_x() as f64 - scx;
                    let min_w = gc.header_min_col_width();
                    gc.dispatch(GridCommand::ResizeColumn {
                        col_idx: ci,
                        new_width: (sw + dx).max(min_w),
                    });
                }
                None => {
                    drop(drag);
                    // Skip hover while an edit overlay or the
                    // context menu is open.
                    if gc.0.edit_input.borrow().is_some() {
                        return;
                    }
                    if super::dom_helpers::document()
                        .get_element_by_id("rs-grid-ctx-menu")
                        .is_some()
                    {
                        return;
                    }
                    let (x, y) = gc.canvas_xy(&evt);
                    if gc.hit_header_menu_icon(x, y).is_some() {
                        gc.set_cursor("pointer");
                    } else if gc.hit_col_resize_separator(x, y).is_some() {
                        gc.set_cursor("w-resize");
                    } else {
                        gc.set_cursor("default");
                    }
                    let new_row = gc.row_at(x, y);
                    if gc.0.state.borrow().hovered_row != new_row {
                        gc.dispatch(GridCommand::SetHoveredRow(new_row));
                    }
                    // Track which column's menu icon is hovered.
                    let new_menu = gc.hit_header_menu_icon(x, y);
                    if *gc.0.hovered_menu_col.borrow() != new_menu {
                        *gc.0.hovered_menu_col.borrow_mut() = new_menu;
                        gc.render();
                    }
                }
            }
        });
        let f: js_sys::Function =
            cb.as_ref().unchecked_ref::<js_sys::Function>().clone();
        document()
            .add_event_listener_with_callback("mousemove", &f)
            .expect("add mousemove listener");
        self.0
            .doc_listeners
            .borrow_mut()
            .push(("mousemove".to_string(), f));
        self.0.closures.borrow_mut().push(Box::new(cb));
    }

    fn attach_mouseup(&self) {
        let gc = self.clone();
        let cb = Closure::<dyn FnMut(_)>::new(move |evt: MouseEvent| {
            // Middle-button release stops pan.
            if evt.button() == 1 {
                if matches!(*gc.0.drag.borrow(), Some(ActiveDrag::Pan { .. })) {
                    gc.stop_pan();
                }
                return;
            }
            gc.stop_scroll_repeat();
            gc.stop_cell_drag_scroll();

            let finished_drag = gc.0.drag.borrow_mut().take();
            match finished_drag {
                Some(ActiveDrag::ColClick { col_idx, .. }) => {
                    let key =
                        gc.0.state
                            .borrow()
                            .model
                            .columns
                            .get(col_idx)
                            .map(|c| c.key.clone());
                    if let Some(key) = key {
                        gc.dispatch(GridCommand::ToggleSort { col_key: key });
                    }
                }
                Some(ActiveDrag::ColumnDrag {
                    col_idx,
                    current_vx,
                    ..
                }) => {
                    gc.0.drag_col_offsets.borrow_mut().clear();
                    gc.set_cursor("default");
                    let insert = gc.insertion_index(current_vx);
                    let to = if insert > col_idx { insert - 1 } else { insert };
                    if to != col_idx {
                        gc.dispatch(GridCommand::MoveColumn {
                            from_idx: col_idx,
                            to_idx: to,
                        });
                    } else {
                        // No move — just redraw to clear
                        // the drag preview.
                        gc.render();
                    }
                }
                Some(ActiveDrag::ColumnResize {
                    col_idx,
                    start_width,
                    start_flex,
                    ..
                }) => {
                    gc.dispatch(GridCommand::CommitColumnResize {
                        col_idx,
                        old_width: start_width,
                        old_flex: start_flex,
                    });
                    gc.set_cursor("default");
                }
                _ => {}
            }
        });
        let f: js_sys::Function =
            cb.as_ref().unchecked_ref::<js_sys::Function>().clone();
        document()
            .add_event_listener_with_callback("mouseup", &f)
            .expect("add mouseup listener");
        self.0
            .doc_listeners
            .borrow_mut()
            .push(("mouseup".to_string(), f));
        self.0.closures.borrow_mut().push(Box::new(cb));
    }
}
