feat: improve scrollbar behavior

Clicking and dragging inside the scrollbar grip will now preserve the mouse click offset
and not jump to the center position on click.

Clicking outside the scrollbar grip will now jump as though the center of the grip was
dragged to the click position.

This is more inline with typical GUI scrollbar behaviors.
This commit is contained in:
CJ van den Berg 2025-12-17 14:34:50 +01:00
parent 84225983b7
commit 8745cd05d2
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9

View file

@ -13,6 +13,7 @@ plane: Plane,
pos_scrn: u32 = 0, pos_scrn: u32 = 0,
view_scrn: u32 = 8, view_scrn: u32 = 8,
size_scrn: u32 = 8, size_scrn: u32 = 8,
mouse_pos_scrn_offset: u32 = 0,
pos_virt: u32 = 0, pos_virt: u32 = 0,
view_virt: u32 = 1, view_virt: u32 = 1,
@ -67,14 +68,16 @@ pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) { if (try m.match(.{ "B", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) {
self.active = true; self.active = true;
self.move_to(y, ypx); self.update_max_ypx(ypx);
self.click_at(y, ypx);
return true; return true;
} else if (try m.match(.{ "B", input.event.release, @intFromEnum(input.mouse.BUTTON1), tp.more })) { } else if (try m.match(.{ "B", input.event.release, @intFromEnum(input.mouse.BUTTON1), tp.more })) {
self.active = false; self.active = false;
return true; return true;
} else if (try m.match(.{ "D", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) { } else if (try m.match(.{ "D", input.event.press, @intFromEnum(input.mouse.BUTTON1), tp.any, tp.any, tp.extract(&y), tp.any, tp.extract(&ypx) })) {
self.active = true; self.active = true;
self.move_to(y, ypx); self.update_max_ypx(ypx);
self.drag_to(y, ypx);
return true; return true;
} else if (try m.match(.{ "B", input.event.release, @intFromEnum(input.mouse.BUTTON1), tp.more })) { } else if (try m.match(.{ "B", input.event.release, @intFromEnum(input.mouse.BUTTON1), tp.more })) {
self.active = false; self.active = false;
@ -89,33 +92,63 @@ pub fn receive(self: *Self, _: tp.pid_ref, m: tp.message) error{Exit}!bool {
return false; return false;
} }
fn move_to(self: *Self, y_: i32, ypx_: i32) void { fn update_max_ypx(self: *Self, ypx_: i32) void {
self.max_ypx = @max(self.max_ypx, ypx_); self.max_ypx = @max(self.max_ypx, ypx_);
}
fn y_coord_to_pos_scrn(self: *const Self, y_: i32, ypx_: i32) u32 {
const max_ypx: f64 = @floatFromInt(self.max_ypx); const max_ypx: f64 = @floatFromInt(self.max_ypx);
const y: f64 = @floatFromInt(y_); const y: f64 = @floatFromInt(y_);
const ypx: f64 = @floatFromInt(ypx_); const ypx: f64 = @floatFromInt(ypx_);
const plane_y: f64 = @floatFromInt(self.plane.abs_y()); const plane_y: f64 = @floatFromInt(self.plane.abs_y());
const size_scrn: f64 = @floatFromInt(self.size_scrn);
const view_scrn: f64 = @floatFromInt(self.view_scrn);
const ratio = max_ypx / eighths_c; const ratio = max_ypx / eighths_c;
const pos_scrn: f64 = ((y - plane_y) * eighths_c) + (ypx / ratio) - (view_scrn / 2); const pos_scrn_ = ((y - plane_y) * eighths_c) + (ypx / ratio);
const max_pos_scrn = size_scrn - view_scrn; const pos_scrn: i32 = @intFromFloat(pos_scrn_);
const pos_scrn_clamped = @min(@max(0, pos_scrn), max_pos_scrn); return @max(0, pos_scrn);
const pos_virt = self.pos_scrn_to_virt(@intFromFloat(pos_scrn_clamped));
self.set(self.size_virt, self.view_virt, pos_virt);
_ = self.event_sink.msg(.{ "scroll_to", pos_virt }) catch {};
} }
fn pos_scrn_to_virt(self: Self, pos_scrn_: u32) u32 { fn clamp_pos_scrn(self: *const Self, pos_scrn: u32) u32 {
const max_pos_scrn = self.size_scrn -| self.view_scrn;
return @min(pos_scrn, max_pos_scrn);
}
fn pos_scrn_to_virt(self: *const Self, pos_scrn_: u32) u32 {
const pos_scrn: f64 = @floatFromInt(self.clamp_pos_scrn(pos_scrn_));
const size_virt: f64 = @floatFromInt(self.size_virt); const size_virt: f64 = @floatFromInt(self.size_virt);
const size_scrn: f64 = @floatFromInt(self.plane.dim_y() * eighths_c); const size_scrn: f64 = @floatFromInt(self.plane.dim_y() * eighths_c);
const pos_scrn: f64 = @floatFromInt(pos_scrn_);
const ratio = size_virt / size_scrn; const ratio = size_virt / size_scrn;
return @intFromFloat(pos_scrn * ratio); return @intFromFloat(pos_scrn * ratio);
} }
fn is_pos_scrn_in_bar(self: *const Self, pos_scrn: u32) bool {
return pos_scrn > self.pos_scrn and pos_scrn <= self.pos_scrn + self.view_scrn;
}
fn click_at(self: *Self, y: i32, ypx: i32) void {
const pos_scrn = self.y_coord_to_pos_scrn(y, ypx);
if (self.is_pos_scrn_in_bar(pos_scrn)) {
self.mouse_pos_scrn_offset = pos_scrn -| self.pos_scrn;
@import("std").log.debug("click: {d}:{d}", .{ pos_scrn, self.mouse_pos_scrn_offset });
} else {
self.mouse_pos_scrn_offset = self.view_scrn / 2;
@import("std").log.debug("click off: {d}:{d}", .{ pos_scrn, self.mouse_pos_scrn_offset });
self.move_to(self.pos_scrn_to_virt(pos_scrn -| self.mouse_pos_scrn_offset));
}
}
fn drag_to(self: *Self, y: i32, ypx: i32) void {
const pos_scrn = self.y_coord_to_pos_scrn(y, ypx) -| self.mouse_pos_scrn_offset;
const pos_virt = self.pos_scrn_to_virt(pos_scrn);
@import("std").log.debug("drag_to: {d:}:{d}:{d}", .{ pos_scrn, pos_virt, self.mouse_pos_scrn_offset });
self.move_to(pos_virt);
}
fn move_to(self: *Self, pos_virt: u32) void {
self.set(self.size_virt, self.view_virt, pos_virt);
_ = self.event_sink.msg(.{ "scroll_to", pos_virt }) catch {};
}
pub fn render(self: *Self, theme: *const Widget.Theme) bool { pub fn render(self: *Self, theme: *const Widget.Theme) bool {
const style = if (self.style_factory) |f| const style = if (self.style_factory) |f|
f(self, theme) f(self, theme)
@ -130,7 +163,7 @@ pub fn render(self: *Self, theme: *const Widget.Theme) bool {
self.plane.set_base_style(style); self.plane.set_base_style(style);
self.plane.erase(); self.plane.erase();
if (!(tui.config().scrollbar_auto_hide and self.size_scrn == self.view_scrn)) if (!(tui.config().scrollbar_auto_hide and self.size_scrn == self.view_scrn))
smooth_bar_at(&self.plane, @intCast(self.pos_scrn), @intCast(self.view_scrn)) catch {}; smooth_bar_at(&self.plane, self.pos_scrn, self.view_scrn) catch {};
return false; return false;
} }
@ -156,19 +189,19 @@ pub fn set(self: *Self, size_virt_: u32, view_virt_: u32, pos_virt_: u32) void {
const eighths_b = [_][]const u8{ "", "", "", "", "", "", "", "" }; const eighths_b = [_][]const u8{ "", "", "", "", "", "", "", "" };
const eighths_t = [_][]const u8{ " ", "", "🮂", "🮃", "", "🮄", "🮅", "🮆" }; const eighths_t = [_][]const u8{ " ", "", "🮂", "🮃", "", "🮄", "🮅", "🮆" };
const eighths_c: i32 = @intCast(eighths_b.len); const eighths_c: u32 = eighths_b.len;
fn smooth_bar_at(plane: *Plane, pos_: i32, size_: i32) !void { fn smooth_bar_at(plane: *Plane, pos_: u32, size_: u32) !void {
const height: i32 = @intCast(plane.dim_y()); const height: u32 = plane.dim_y();
var size = @max(size_, 8); var size = @max(size_, 8);
const pos: i32 = @min(height * eighths_c - size, pos_); const pos = @min(height * eighths_c - size, pos_);
var pos_y = @as(c_int, @intCast(@divFloor(pos, eighths_c))); var pos_y: c_int = @intCast(@divFloor(pos, eighths_c));
const blk = @mod(pos, eighths_c); const blk = @mod(pos, eighths_c);
const b = eighths_b[@intCast(blk)]; const b = eighths_b[blk];
plane.erase(); plane.erase();
plane.cursor_move_yx(pos_y, 0) catch return; plane.cursor_move_yx(pos_y, 0) catch return;
_ = try plane.putstr(@ptrCast(b)); _ = try plane.putstr(@ptrCast(b));
size -= @as(u16, @intCast(eighths_c)) - @as(u16, @intCast(blk)); size -= eighths_c - blk;
while (size >= 8) { while (size >= 8) {
pos_y += 1; pos_y += 1;
size -= 8; size -= 8;