╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2669 try root.input(allocator, key, root_focus); │ │2670 }, │ │2671 .mouse => |mouse| { │ │2672 - if (mouse.action == .press and mouse.action.press == .left) { │ │2672 + if (mouse.button == .left) { │ │2673 if (cellPageLink(root, mouse.x, mouse.y, session.data)) |route| return session.navigate(route);│ │2674 if (root_focus.hitTest(mouse.x, mouse.y)) |hit| { │ │2675 const focus_id = hit.id; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │2704 .event => |event| switch (event) { │ │2705 .background => |rgb| session.terminal_background = rgb, │ │2706 .resize => try root.input(allocator, key, root_focus), │ │2707 + }, │ │2708 + // a scroll tick goes to the pane under the pointer, focusing it first │ │2709 + .scroll_up, .scroll_down => |position| { │ │2710 + if (scrollUnder(root_focus, position)) |scroll_id| { │ │2711 + if (!focusInside(root_focus, scroll_id)) root_focus.setFocus(scroll_id);│ │2712 + } │ │2713 + try root.input(allocator, key, root_focus); │ │2714 }, │ │2715 else => try root.input(allocator, key, root_focus), │ │2716 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2721 if (inPageLink(root_focus, focus_id, session.data)) |route| session.data.current_page = route; │ │2722 } │ │2723 │ │2724 +// the innermost vertically scrolling widget laid out under `position` │ │2725 +fn scrollUnder(root_focus: *const Focus, position: xitui.input.Position) ?usize { │ │2726 + var found: ?usize = null; │ │2727 + var found_area: usize = std.math.maxInt(usize); │ │2728 + var iter = root_focus.children.iterator(); │ │2729 + while (iter.next()) |entry| { │ │2730 + const info = entry.value_ptr.focus.scroll orelse continue; │ │2731 + if (info.direction == .horiz) continue; │ │2732 + const r = entry.value_ptr.rect; │ │2733 + if (position.x < r.x or position.y < r.y or position.x >= r.x + r.size.width or position.y >= r.y + r.size.height) continue;│ │2734 + // a nested scroll lies within its parent, so the smallest is innermost │ │2735 + const area = r.size.width * r.size.height; │ │2736 + if (area < found_area) { │ │2737 + found = entry.key_ptr.*; │ │2738 + found_area = area; │ │2739 + } │ │2740 + } │ │2741 + return found; │ │2742 +} │ │2743 + │ │2744 +// whether the focused widget is `id` or lies under it │ │2745 +fn focusInside(root_focus: *const Focus, id: usize) bool { │ │2746 + var current = root_focus.grandchild_id orelse return false; │ │2747 + while (true) { │ │2748 + if (current == id) return true; │ │2749 + const child = root_focus.children.get(current) orelse return false; │ │2750 + current = child.parent_id; │ │2751 + } │ │2752 +} │ │2753 + │ │2754 // a link to bytes the server serves directly rather than to a page route, so │ │2755 // it never resolves to a RoutablePage │ │2756 pub const raw_link_prefix = "ax:"; │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯