const std = @import("std");
const dvui = @import("../dvui.zig");

const Event = dvui.Event;
const Options = dvui.Options;
const Point = dvui.Point;
const Rect = dvui.Rect;
const RectScale = dvui.RectScale;
const ScrollInfo = dvui.ScrollInfo;
const Size = dvui.Size;
const Widget = dvui.Widget;
const WidgetData = dvui.WidgetData;

const ScrollContainerWidget = @This();

var scroll_current: ?*ScrollContainerWidget = null;

pub fn current() ?*ScrollContainerWidget {
    return scroll_current;
}

pub fn scrollSet(scroll: ?*ScrollContainerWidget) ?*ScrollContainerWidget {
    const ret = scroll_current;
    scroll_current = scroll;
    return ret;
}

pub var defaults: Options = .{
    .name = "ScrollContainer",
    // most of the time ScrollContainer is used inside ScrollArea which
    // overrides these
    .background = true,
    .min_size_content = .{ .w = 5, .h = 5 },
    .style = .window,
};

pub const InitOptions = struct {
    scroll_area: ?*dvui.ScrollAreaWidget,
    frame_viewport: ?Point = null,
    lock_visible: bool = false,
    event_rect: ?Rect.Physical = null,
    process_events_after: bool = true,

    /// If non null scrollContainer will write the total amount of scrolling
    /// that came from user events.  Many events are processed in deinit(),
    /// so run that before using this value.
    user_scroll: ?*dvui.Point = null,
};

wd: WidgetData,
si: *ScrollInfo,
init_opts: InitOptions,
last_focus: dvui.Id,
parentScroll: ?*ScrollContainerWidget = null,
subwindowId: dvui.Id,

// si.viewport.x/y might be updated in the middle of a frame, this prevents
// those visual artifacts
frame_viewport: Point = Point{},

prevClip: Rect.Physical = .{},

nextVirtualSize: Size = Size{},
seen_expanded_child: bool = false,

lock_visible: bool = false,
first_visible_id: dvui.Id = .zero,
first_visible_offset: Point = Point{}, // offset of top left of first visible widget from viewport
first_visible_viewport: Point = Point{}, // viewport from previous frame

seen_scroll_drag: bool = false,

finger_down: bool = false,

/// It's expected to call this when `self` is `undefined`
pub fn init(self: *ScrollContainerWidget, src: std.builtin.SourceLocation, io_scroll_info: *ScrollInfo, init_options: InitOptions, opts: Options) void {
    const options = defaults.override(opts);
    self.* = .{
        .wd = WidgetData.init(src, .{ .scroll_when_focused = false }, options),
        .si = io_scroll_info,
        .init_opts = init_options,
        .last_focus = dvui.lastFocusedIdInFrame(),
        .subwindowId = dvui.subwindowCurrentId(),
    };

    if (self.init_opts.user_scroll) |us| us.* = .{};

    if (dvui.dataGet(null, self.data().id, "_finger_down", bool)) |down| self.finger_down = down;

    const crect = self.data().contentRect();
    self.si.viewport.w = crect.w;
    self.si.viewport.h = crect.h;

    self.data().register();

    if (self.init_opts.scroll_area) |sa| {
        sa.setContainerRect(crect);
    }

    switch (self.si.horizontal) {
        .none => self.si.virtual_size.w = crect.w,
        .auto => {},
        .given => {},
    }
    switch (self.si.vertical) {
        .none => self.si.virtual_size.h = crect.h,
        .auto => {},
        .given => {},
    }

    self.data().borderAndBackground(.{});

    self.prevClip = dvui.clip(self.data().contentRectScale().r);

    self.frame_viewport = self.init_opts.frame_viewport orelse self.si.viewport.topLeft();

    self.lock_visible = self.init_opts.lock_visible;
    if (self.lock_visible) {
        self.first_visible_id = dvui.dataGet(null, self.data().id, "_fv_id", dvui.Id) orelse blk: {
            dvui.log.debug("ScrollContainerWidget forcing lock_visible false due to not having a first visible widget\n", .{});
            self.lock_visible = false;
            break :blk .zero;
        };
        self.first_visible_offset = dvui.dataGet(null, self.data().id, "_fv_offset", Point) orelse .{};
        self.first_visible_viewport = dvui.dataGet(null, self.data().id, "_fv_viewport", Point) orelse .{};
    }

    dvui.parentSet(self.widget());
    self.parentScroll = scrollSet(self);
}

pub fn matchEvent(self: *ScrollContainerWidget, e: *Event) bool {
    // track finger press/release even if it doesn't happen in our rect
    if (e.evt == .mouse and e.evt.mouse.action == .press and e.evt.mouse.button.touch()) {
        self.finger_down = true;
    } else if (e.evt == .mouse and e.evt.mouse.action == .release and e.evt.mouse.button.touch()) {
        self.finger_down = false;
    }

    return dvui.eventMatch(e, .{ .id = self.data().id, .r = self.init_opts.event_rect orelse self.data().borderRectScale().r });
}

pub fn processEvents(self: *ScrollContainerWidget) void {
    const evts = dvui.events();
    for (evts) |*e| {
        if (!self.matchEvent(e))
            continue;

        self.processEvent(e);
    }

    // might have changed from events
    self.frame_viewport = self.init_opts.frame_viewport orelse self.si.viewport.topLeft();
}

pub const processVelocity = @compileError("no longer needed, processVelocityAfter called automatically in deinit");

pub fn processVelocityAfter(self: *ScrollContainerWidget) void {
    // velocity is only for touch currently

    // damp the current velocity
    // exponential decay: v *= damping^secs_since
    // tweak the damping so we brake harder as the velocity slows down
    if (!self.finger_down) {
        const before = self.si.viewport.topLeft();
        {
            const damping = 0.0001 + @min(1.0, @abs(self.si.velocity.x) / 50.0) * (0.7 - 0.0001);
            self.si.velocity.x *= @exp(@log(damping) * dvui.secondsSinceLastFrame());
            if (@abs(self.si.velocity.x) > 1) {
                //std.debug.print("vel x {d}\n", .{self.si.velocity.x});
                self.si.viewport.x += self.si.velocity.x * 50 * dvui.secondsSinceLastFrame();
                dvui.refresh(null, @src(), self.data().id);
            } else {
                self.si.velocity.x = 0;
            }
        }

        {
            const damping = 0.0001 + @min(1.0, @abs(self.si.velocity.y) / 50.0) * (0.7 - 0.0001);
            self.si.velocity.y *= @exp(@log(damping) * dvui.secondsSinceLastFrame());
            if (@abs(self.si.velocity.y) > 1) {
                //std.debug.print("vel y {d}\n", .{self.si.velocity.y});
                self.si.viewport.y += self.si.velocity.y * 50 * dvui.secondsSinceLastFrame();
                dvui.refresh(null, @src(), self.data().id);
            } else {
                self.si.velocity.y = 0;
            }
        }

        if (self.init_opts.user_scroll) |us| us.* = us.*.plus(self.si.viewport.topLeft().diff(before));
    }

    // bounce back if we went too far and finger is up
    // bounce back does not count for user_scroll
    {
        const max_scroll = self.si.scrollMax(.horizontal);
        if (self.si.viewport.x < 0) {
            self.si.velocity.x = 0;
            self.si.viewport.x = @max(-20, self.si.viewport.x);
            if (!self.finger_down) {
                self.si.viewport.x = @min(0, @max(-20, self.si.viewport.x + 250 * dvui.secondsSinceLastFrame()));
                dvui.refresh(null, @src(), self.data().id);
            }
        } else if (self.si.viewport.x > max_scroll) {
            self.si.velocity.x = 0;
            self.si.viewport.x = @min(max_scroll + 20, self.si.viewport.x);
            if (!self.finger_down) {
                self.si.viewport.x = @max(max_scroll, @min(max_scroll + 20, self.si.viewport.x - 250 * dvui.secondsSinceLastFrame()));
                dvui.refresh(null, @src(), self.data().id);
            }
        }
    }

    {
        const max_scroll = self.si.scrollMax(.vertical);

        if (self.si.viewport.y < 0) {
            self.si.velocity.y = 0;
            self.si.viewport.y = @max(-20, self.si.viewport.y);
            if (!self.finger_down) {
                self.si.viewport.y = @min(0, @max(-20, self.si.viewport.y + 250 * dvui.secondsSinceLastFrame()));
                dvui.refresh(null, @src(), self.data().id);
            }
        } else if (self.si.viewport.y > max_scroll) {
            self.si.velocity.y = 0;
            self.si.viewport.y = @min(max_scroll + 20, self.si.viewport.y);
            if (!self.finger_down) {
                self.si.viewport.y = @max(max_scroll, @min(max_scroll + 20, self.si.viewport.y - 250 * dvui.secondsSinceLastFrame()));
                dvui.refresh(null, @src(), self.data().id);
            }
        }
    }
}

pub fn widget(self: *ScrollContainerWidget) Widget {
    return Widget.init(self, data, rectFor, screenRectScale, minSizeForChild);
}

pub fn data(self: *ScrollContainerWidget) *WidgetData {
    return self.wd.validate();
}

pub fn rectFor(self: *ScrollContainerWidget, id: dvui.Id, min_size: Size, e: Options.Expand, g: Options.Gravity) Rect {
    // we stack widgets vertically, so only vertical expansion is a problem
    if (self.seen_expanded_child) {
        // Having one expanded child makes sense - could be taking the rest of
        // the given space or filling the visible space (if bigger than the min
        // virtual size).  But that should be the last (usually only) child
        // that asks for space.
        //
        // We saw an expanded child and gave it the rest of the space, and now
        // another child has asked for space, which shouldn't happen.  Visually
        // the new child might not appear at all or appear on top of the
        // expanded child.
        //
        // If you are reading this, make sure that children of scrollArea() are
        // not expanded in the scrollArea's layout direction, or that only the
        // last child is.
        dvui.log.err("{s}:{d} got child {x} after expanded child", .{ @src().file, @src().line, id });
        var iter = dvui.parentGet().data().iterator();
        while (iter.next()) |wd| {
            dvui.log.debug("  {s}:{d} {s} {x}", .{
                wd.src.file,
                wd.src.line,
                wd.options.name orelse "???",
                wd.id,
            });
            dvui.Debug.errorOutline(wd.rectScale().r);
        }
    } else if (e.isVertical()) {
        self.seen_expanded_child = true;
    }

    const y = self.nextVirtualSize.h;

    const h = switch (self.si.vertical) {
        // no scrolling, you only get the visible space
        .none => self.si.viewport.h - y,

        // you get the space you need or more if there is extra visible space
        // and you are expanded
        .auto => @max(self.si.viewport.h - y, min_size.h),

        // you get the given space
        .given => self.si.virtual_size.h - y,
    };

    // widgets always get the full horizontal space depending on scroll mode
    const maxw = switch (self.si.horizontal) {
        // no scrolling, you only get the visible space
        .none => self.si.viewport.w,

        // you get the space you need or more if there is extra visible space
        // and you are expanded
        .auto => @max(self.si.viewport.w, min_size.w),

        // you get the given space
        .given => self.si.virtual_size.w,
    };

    const rect = Rect{ .x = 0, .y = y, .w = maxw, .h = h };
    const ret = dvui.placeIn(rect, min_size, e, g);

    if (self.lock_visible and self.first_visible_id == id) {
        self.first_visible_id = .zero;
        self.lock_visible = false;

        const scroll_since_last_frame = self.frame_viewport.diff(self.first_visible_viewport);

        self.frame_viewport.x = 0;
        self.frame_viewport.y = y + self.first_visible_offset.y + scroll_since_last_frame.y;
        self.si.viewport.x = self.frame_viewport.x;
        self.si.viewport.y = self.frame_viewport.y;
    }

    if (!self.lock_visible and self.first_visible_id == .zero and self.frame_viewport.y < (ret.y + ret.h)) {
        self.first_visible_id = id;
        self.first_visible_offset = Point.diff(self.frame_viewport, ret.topLeft());

        // record where the viewport was, so that if we do lock_visible next
        // frame we can tell how much scrolling happened
        self.first_visible_viewport = self.frame_viewport;
    }

    return ret;
}

pub fn pointFromPhysical(self: *ScrollContainerWidget, p: Point.Physical) Point {
    return self.data().contentRectScale().pointFromPhysical(p).plus(self.frame_viewport);
}

pub fn screenRectScale(self: *ScrollContainerWidget, rect: Rect) RectScale {
    var r = rect;
    if (self.lock_visible) {
        // haven't found the first visible yet, put offscreen
        r.x = 10000;
        r.y = 10000;
    } else {
        r.y -= self.frame_viewport.y;
        r.x -= self.frame_viewport.x;
    }

    return self.data().contentRectScale().rectToRectScale(r);
}

pub fn minSizeForChild(self: *ScrollContainerWidget, s: Size) void {
    self.nextVirtualSize.h += s.h;
    self.nextVirtualSize.w = @max(self.nextVirtualSize.w, s.w);
}

pub fn processEvent(self: *ScrollContainerWidget, e: *Event) void {
    switch (e.evt) {
        .mouse => |me| {
            if (me.action == .press and me.button.touch()) {
                // stop any current scrolling
                if (self.si.velocity.x != 0 or self.si.velocity.y != 0) {
                    // if we were scrolling, then eat the finger press so it
                    // doesn't do anything other than stop the scroll
                    e.handle(@src(), self.data());

                    self.si.velocity.x = 0;
                    self.si.velocity.y = 0;
                }
            }
        },
        else => {},
    }
}

pub fn processScrollDrag(
    self: *ScrollContainerWidget,
    sd: dvui.ScrollDragOptions,
) void {
    const rs = self.data().contentRectScale();
    var scrolly: f32 = 0;
    if (sd.mouse_pt.y <= rs.r.y and // want to scroll up
        sd.screen_rect.y < rs.r.y and // scrolling would show more of child
        self.si.viewport.y > 0) // can scroll up
    {
        scrolly = if (!self.seen_scroll_drag) -200 * dvui.secondsSinceLastFrame() else -5;
    }

    if (sd.mouse_pt.y >= (rs.r.y + rs.r.h) and
        (sd.screen_rect.y + sd.screen_rect.h) > (rs.r.y + rs.r.h) and
        self.si.viewport.y < self.si.scrollMax(.vertical))
    {
        scrolly = if (!self.seen_scroll_drag) 200 * dvui.secondsSinceLastFrame() else 5;
    }

    var scrollx: f32 = 0;
    if (sd.mouse_pt.x <= rs.r.x and // want to scroll left
        sd.screen_rect.x < rs.r.x and // scrolling would show more of child
        self.si.viewport.x > 0) // can scroll left
    {
        scrollx = if (!self.seen_scroll_drag) -200 * dvui.secondsSinceLastFrame() else -5;
    }

    if (sd.mouse_pt.x >= (rs.r.x + rs.r.w) and
        (sd.screen_rect.x + sd.screen_rect.w) > (rs.r.x + rs.r.w) and
        self.si.viewport.x < self.si.scrollMax(.horizontal))
    {
        scrollx = if (!self.seen_scroll_drag) 200 * dvui.secondsSinceLastFrame() else 5;
    }

    var propagate = true;

    const before = self.si.viewport.topLeft();

    if (scrolly != 0 or scrollx != 0) {
        propagate = false;
        if (scrolly != 0) {
            self.si.scrollByOffset(.vertical, scrolly);
        }
        if (scrollx != 0) {
            self.si.scrollByOffset(.horizontal, scrollx);
        }

        dvui.refresh(null, @src(), self.data().id);

        // if we are scrolling, then we need a motion event next
        // frame so that the child widget can adjust selection
        dvui.currentWindow().inject_motion_event = true;
    }

    if (self.init_opts.user_scroll) |us| us.* = us.*.plus(self.si.viewport.topLeft().diff(before));

    self.seen_scroll_drag = true;

    if (propagate) {
        if (self.parentScroll) |parent| {
            if (parent.subwindowId == self.subwindowId) {
                parent.processScrollDrag(sd);
            }
        }
    }
}

pub fn processScrollTo(
    self: *ScrollContainerWidget,
    st: dvui.ScrollToOptions,
) void {
    const before = self.si.viewport.topLeft();

    const rs = self.data().contentRectScale();

    // We will propagate if the whole thing is inside our rect.  Think a large
    // grid (has internal scroll area) inside larger page scroll area.  The
    // grid's scroll area isn't doing anything, but grid cells can be
    // scrolled into view on the page.
    const propagate = rs.r.intersect(st.screen_rect).equals(st.screen_rect);

    if (self.si.vertical != .none) {
        const ypx = rs.r.y - st.screen_rect.y; // how far to the top
        const ypx2 = (st.screen_rect.y + st.screen_rect.h) - (rs.r.y + rs.r.h); // to the bottom

        const yup = @min(ypx, -ypx2);
        if (yup > 0) {
            self.si.viewport.y = self.si.viewport.y - (yup / rs.s);
            if (!st.over_scroll) {
                self.si.scrollToOffset(.vertical, self.si.viewport.y);
            }
            dvui.refresh(null, @src(), self.data().id);
        }

        const ydown = @min(ypx2, -ypx);
        if (ydown > 0) {
            self.si.viewport.y = self.si.viewport.y + (ydown / rs.s);
            if (!st.over_scroll) {
                self.si.scrollToOffset(.vertical, self.si.viewport.y);
            }
            dvui.refresh(null, @src(), self.data().id);
        }
    }

    if (self.si.horizontal != .none) {
        const xpx = rs.r.x - st.screen_rect.x; // how far left
        const xpx2 = (st.screen_rect.x + st.screen_rect.w) - (rs.r.x + rs.r.w); // how far right

        const xleft = @min(xpx, -xpx2);
        if (xleft > 0) {
            self.si.viewport.x = self.si.viewport.x - (xleft / rs.s);
            if (!st.over_scroll) {
                self.si.scrollToOffset(.horizontal, self.si.viewport.x);
            }
            dvui.refresh(null, @src(), self.data().id);
        }

        const xright = @min(xpx2, -xpx);
        if (xright > 0) {
            self.si.viewport.x = self.si.viewport.x + (xright / rs.s);
            if (!st.over_scroll) {
                self.si.scrollToOffset(.horizontal, self.si.viewport.x);
            }
            dvui.refresh(null, @src(), self.data().id);
        }
    }

    if (self.init_opts.user_scroll) |us| us.* = us.*.plus(self.si.viewport.topLeft().diff(before));

    if (propagate) {
        if (self.parentScroll) |parent| {
            if (parent.subwindowId == self.subwindowId) {
                var opts = st;
                // over scroll only makes sense for the inner-most one, because
                // you only do it assuming that the inner-most scroll area
                // virtual size is about to get bigger
                opts.over_scroll = false;
                parent.processScrollTo(opts);
            }
        }
    }
}

pub fn processMotionScroll(self: *ScrollContainerWidget, motion: dvui.Point.Physical) void {
    const rs = self.data().borderRectScale();

    var have_parent: bool = false;
    if (self.parentScroll) |parent| {
        if (parent.subwindowId == self.subwindowId) {
            have_parent = true;
        }
    }

    // We propagate (instead of not handling the motion event) because we have
    // capture.
    //
    // Whether to propagate out to any containing scroll
    // containers. Propagate unless we did the whole scroll
    // in the main direction of movement.
    //
    // This helps prevent spurious propogation from a text
    // entry box where you are trying to scroll vertically
    // but the motion event has a small amount of
    // horizontal.
    var propagate: bool = false;

    if (self.si.vertical != .none) {
        if (!have_parent or
            (motion.y > 0 and self.si.viewport.y > 0) or
            (motion.y < 0 and self.si.viewport.y < self.si.scrollMax(.vertical)))
        {
            // can scroll in that direction or no parent, so scroll anyway (might bump past the limit)
            self.si.viewport.y -= motion.y / rs.s;
            if (self.init_opts.user_scroll) |us| us.*.y -= motion.y / rs.s;
            self.si.velocity.y = -motion.y / rs.s;
            dvui.refresh(null, @src(), self.data().id);
        } else if (@abs(motion.y) > @abs(motion.x)) {
            propagate = true;
        }
    }

    if (self.si.horizontal != .none) {
        if (!have_parent or
            (motion.x > 0 and self.si.viewport.x > 0) or
            (motion.x < 0 and self.si.viewport.x < self.si.scrollMax(.horizontal)))
        {
            self.si.viewport.x -= motion.x / rs.s;
            if (self.init_opts.user_scroll) |us| us.*.x -= motion.x / rs.s;
            self.si.velocity.x = -motion.x / rs.s;
            dvui.refresh(null, @src(), self.data().id);
        } else if (@abs(motion.x) > @abs(motion.y)) {
            propagate = true;
        }
    }

    if (propagate) {
        if (self.parentScroll) |parent| {
            if (parent.subwindowId == self.subwindowId) {
                parent.processMotionScroll(motion);
            }
        }
    }
}

pub fn processEventsAfter(self: *ScrollContainerWidget) void {
    const focus_id = dvui.lastFocusedIdInFrameSince(self.last_focus);

    const evts = dvui.events();
    for (evts) |*e| {
        if (dvui.captured(self.wd.id) and e.evt == .mouse and e.evt.mouse.action == .release and e.evt.mouse.button.touch()) {
            // release our capture even if this event was already handled, see
            // comment in the .motion handling
            dvui.captureMouse(null, e.num);
            dvui.dragEnd();
            if (!e.handled) e.handle(@src(), self.data());
        }

        if (!dvui.eventMatch(e, .{ .id = self.data().id, .focus_id = focus_id, .r = self.init_opts.event_rect orelse self.data().borderRectScale().r }))
            continue;

        self.processEventAfter(e);
    }
}

pub fn processEventAfter(self: *ScrollContainerWidget, e: *Event) void {
    switch (e.evt) {
        .mouse => |me| {
            if (me.action == .focus) {
                e.handle(@src(), self.data());
                // focus so that we can receive keyboard input
                dvui.focusWidget(self.data().id, null, e.num);
            } else if (me.action == .wheel_x) {
                if (self.si.scrollMax(.horizontal) > 0) {
                    if ((me.action.wheel_x > 0 and self.si.viewport.x <= 0) or (me.action.wheel_x < 0 and self.si.viewport.x >= self.si.scrollMax(.horizontal))) {
                        // propagate the scroll event because we are already maxxed out
                    } else {
                        e.handle(@src(), self.data());
                        self.si.scrollByOffset(.horizontal, -me.action.wheel_x);
                        if (self.init_opts.user_scroll) |us| us.*.x -= me.action.wheel_x;
                        dvui.refresh(null, @src(), self.data().id);
                    }
                }
            } else if (me.action == .wheel_y) {
                if (self.si.scrollMax(.vertical) > 0) {
                    if ((me.action.wheel_y > 0 and self.si.viewport.y <= 0) or (me.action.wheel_y < 0 and self.si.viewport.y >= self.si.scrollMax(.vertical))) {
                        // propagate the scroll event because we are already maxxed out
                    } else {
                        e.handle(@src(), self.data());
                        self.si.scrollByOffset(.vertical, -me.action.wheel_y);
                        if (self.init_opts.user_scroll) |us| us.*.y -= me.action.wheel_y;
                        dvui.refresh(null, @src(), self.data().id);
                    }
                }
            } else if (me.action == .press and me.button.touch()) {
                // don't let this event go through to floating window
                // which would capture the mouse preventing scrolling
                e.handle(@src(), self.data());
                dvui.captureMouse(self.data(), e.num);
            } else if (me.action == .release and dvui.captured(self.data().id)) {
                e.handle(@src(), self.data());
                dvui.captureMouse(null, e.num);
                dvui.dragEnd();
            } else if (me.action == .motion and me.button.touch()) {
                e.handle(@src(), self.data());

                // Need to capture here because it's common for the touch
                // down to happen on top of a different widget.  Example is
                // a touch down on a button, which captures.  Then when the
                // drag starts the button gives up capture, so we get here,
                // never having seen the touch down.
                //
                // We will give up capture on any touch release even if
                // already handled (see comment at top of loop):
                // * finger down/motion/up in same frame in textLayout
                // * textLayout gives up capture on the motion event
                // * textLayout processes the finger up
                // * we capture here on the motion event
                // * we would never see the finger up (already processed)
                dvui.captureMouse(self.data(), e.num);

                self.processMotionScroll(me.action.motion);
            }
        },
        .key => |ke| {
            if (ke.code == .up and (ke.action == .down or ke.action == .repeat)) {
                e.handle(@src(), self.data());
                if (self.si.vertical != .none) {
                    self.si.scrollByOffset(.vertical, -10);
                    if (self.init_opts.user_scroll) |us| us.*.y -= 10;
                }
                dvui.refresh(null, @src(), self.data().id);
            } else if (ke.code == .down and (ke.action == .down or ke.action == .repeat)) {
                e.handle(@src(), self.data());
                if (self.si.vertical != .none) {
                    self.si.scrollByOffset(.vertical, 10);
                    if (self.init_opts.user_scroll) |us| us.*.y += 10;
                }
                dvui.refresh(null, @src(), self.data().id);
            } else if (ke.code == .left and (ke.action == .down or ke.action == .repeat)) {
                e.handle(@src(), self.data());
                if (self.si.horizontal != .none) {
                    self.si.scrollByOffset(.horizontal, -10);
                    if (self.init_opts.user_scroll) |us| us.*.x -= 10;
                }
                dvui.refresh(null, @src(), self.data().id);
            } else if (ke.code == .right and (ke.action == .down or ke.action == .repeat)) {
                e.handle(@src(), self.data());
                if (self.si.horizontal != .none) {
                    self.si.scrollByOffset(.horizontal, 10);
                    if (self.init_opts.user_scroll) |us| us.*.x += 10;
                }
                dvui.refresh(null, @src(), self.data().id);
            } else if (ke.code == .page_up and (ke.action == .down or ke.action == .repeat)) {
                e.handle(@src(), self.data());
                const before = self.si.viewport.topLeft();
                self.si.scrollPageUp(.vertical);
                if (self.init_opts.user_scroll) |us| us.* = us.*.plus(self.si.viewport.topLeft().diff(before));
                dvui.refresh(null, @src(), self.data().id);
            } else if (ke.code == .page_down and (ke.action == .down or ke.action == .repeat)) {
                e.handle(@src(), self.data());
                const before = self.si.viewport.topLeft();
                self.si.scrollPageDown(.vertical);
                if (self.init_opts.user_scroll) |us| us.* = us.*.plus(self.si.viewport.topLeft().diff(before));
                dvui.refresh(null, @src(), self.data().id);
            }
        },
        else => {},
    }
}

pub fn deinit(self: *ScrollContainerWidget) void {
    defer if (dvui.widgetIsAllocated(self)) dvui.widgetFree(self);
    defer self.* = undefined;

    // need to reset clip before processEventsAfter, event_rect could be
    // outside clip, and mouse events only match inside the clip
    dvui.clipSet(self.prevClip);

    if (self.init_opts.process_events_after) {
        self.processEventsAfter();
        self.processVelocityAfter();
    }

    dvui.dataSet(null, self.data().id, "_fv_id", self.first_visible_id);
    dvui.dataSet(null, self.data().id, "_fv_offset", self.first_visible_offset);
    dvui.dataSet(null, self.data().id, "_fv_viewport", self.first_visible_viewport);
    dvui.dataSet(null, self.data().id, "_finger_down", self.finger_down);

    const padded = self.data().options.padSize(self.nextVirtualSize);
    switch (self.si.horizontal) {
        .none => self.data().min_size.w = padded.w,
        .auto => {
            self.data().min_size.w = padded.w;
            if (self.nextVirtualSize.w != self.si.virtual_size.w) {
                self.si.virtual_size.w = self.nextVirtualSize.w;
                dvui.refresh(null, @src(), self.data().id);
            }
        },
        .given => {},
    }

    switch (self.si.vertical) {
        .none => self.data().min_size.h = padded.h,
        .auto => {
            self.data().min_size.h = padded.h;
            if (self.nextVirtualSize.h != self.si.virtual_size.h) {
                self.si.virtual_size.h = self.nextVirtualSize.h;
                dvui.refresh(null, @src(), self.data().id);
            }
        },
        .given => {},
    }

    self.data().minSizeSetAndRefresh();
    self.data().minSizeReportToParent();
    _ = scrollSet(self.parentScroll);
    dvui.parentReset(self.data().id, self.data().parent);
}

test {
    _ = InitOptions;
    _ = current;
    _ = scrollSet;
    _ = init;
    _ = matchEvent;
    _ = processEvents;
    _ = processVelocityAfter;
    _ = widget;
    _ = data;
    _ = rectFor;
    _ = pointFromPhysical;
    _ = screenRectScale;
    _ = minSizeForChild;
    _ = processEvent;
    _ = processScrollDrag;
    _ = processScrollTo;
    _ = processMotionScroll;
    _ = processEventsAfter;
    _ = processEventAfter;
    _ = deinit;
}
