-
Notifications
You must be signed in to change notification settings - Fork 46
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
scroll: move scrollbars into separate widget
- Loading branch information
Showing
4 changed files
with
239 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
const std = @import("std"); | ||
const vaxis = @import("../main.zig"); | ||
const vxfw = @import("vxfw.zig"); | ||
|
||
const Allocator = std.mem.Allocator; | ||
|
||
const ScrollBars = @This(); | ||
|
||
const vertical_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "▐", .width = 1 } }; | ||
const horizontal_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "▃", .width = 1 } }; | ||
|
||
/// The ScrollBars widget must contain a ScrollView widget. The scroll bars drawn will be for the | ||
/// scroll view contained in the ScrollBars widget. | ||
scroll_view: vxfw.ScrollView, | ||
/// If `true` a horizontal scroll bar will be drawn. Set to `false` to hide the horizontal scroll | ||
/// bar. Defaults to `true`. | ||
draw_horizontal_scrollbar: bool = true, | ||
/// If `true` a vertical scroll bar will be drawn. Set to `false` to hide the vertical scroll bar. | ||
/// Defaults to `true`. | ||
draw_vertical_scrollbar: bool = true, | ||
/// The estimated height of all the content in the ScrollView. When provided this height will be | ||
/// used to calculate the size of the scrollbar's thumb. If this is not provided the widget will | ||
/// make a best effort estimate of the size of the thumb using the number of elements rendered at | ||
/// any given time. This will cause inconsistent thumb sizes - and possibly inconsistent | ||
/// positioning - if different elements in the ScrollView have different heights. For the best user | ||
/// experience, providing this estimate is strongly recommended. | ||
/// | ||
/// Note that this doesn't necessarily have to be an accurate estimate and the tolerance for larger | ||
/// views is quite forgiving, especially if you overshoot the estimate. | ||
estimated_content_height: ?u32 = null, | ||
|
||
pub fn widget(self: *const ScrollBars) vxfw.Widget { | ||
return .{ | ||
.userdata = @constCast(self), | ||
.eventHandler = typeErasedEventHandler, | ||
.drawFn = typeErasedDrawFn, | ||
}; | ||
} | ||
|
||
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { | ||
const self: *ScrollBars = @ptrCast(@alignCast(ptr)); | ||
return self.handleEvent(ctx, event); | ||
} | ||
|
||
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { | ||
const self: *ScrollBars = @ptrCast(@alignCast(ptr)); | ||
return self.draw(ctx); | ||
} | ||
|
||
pub fn handleEvent(_: *ScrollBars, _: *vxfw.EventContext, _: vxfw.Event) anyerror!void {} | ||
|
||
pub fn draw(self: *ScrollBars, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { | ||
var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); | ||
|
||
// 1. If we're not drawing the scrollbars we can just draw the ScrollView directly. | ||
|
||
if (!self.draw_vertical_scrollbar and !self.draw_horizontal_scrollbar) { | ||
try children.append(.{ | ||
.origin = .{ .row = 0, .col = 0 }, | ||
.surface = try self.scroll_view.draw(ctx), | ||
}); | ||
|
||
return .{ | ||
.size = ctx.max.size(), | ||
.widget = self.widget(), | ||
.buffer = &.{}, | ||
.children = children.items, | ||
}; | ||
} | ||
|
||
// 2. Otherwise we can draw the scrollbars. | ||
|
||
const max = ctx.max.size(); | ||
|
||
// 3. Draw the scroll view itself. | ||
|
||
const scroll_view_surface = try self.scroll_view.draw(ctx.withConstraints( | ||
ctx.min, | ||
.{ | ||
// We make sure to make room for the scrollbars if required. | ||
.width = max.width -| @intFromBool(self.draw_vertical_scrollbar), | ||
.height = max.height -| @intFromBool(self.draw_horizontal_scrollbar), | ||
}, | ||
)); | ||
|
||
try children.append(.{ | ||
.origin = .{ .row = 0, .col = 0 }, | ||
.surface = scroll_view_surface, | ||
}); | ||
|
||
// 4. Draw the vertical scroll bar. | ||
|
||
if (self.draw_vertical_scrollbar) { | ||
// To draw the vertical scrollbar we need to know how big the scroll bar thumb should be. | ||
// If we've been provided with an estimated height we use that to figure out how big the | ||
// thumb should be, otherwise we estimate the size based on how many of the children were | ||
// actually drawn in the ScrollView. | ||
|
||
const widget_height_f: f32 = @floatFromInt(scroll_view_surface.size.height); | ||
const total_num_children_f: f32 = count: { | ||
if (self.scroll_view.item_count) |c| break :count @floatFromInt(c); | ||
|
||
switch (self.scroll_view.children) { | ||
.slice => |slice| break :count @floatFromInt(slice.len), | ||
.builder => |builder| { | ||
var counter: usize = 0; | ||
while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_| | ||
counter += 1; | ||
|
||
break :count @floatFromInt(counter); | ||
}, | ||
} | ||
}; | ||
|
||
const thumb_height: u16 = height: { | ||
// If we know the height, we can use the height of the current view to determine the | ||
// size of the thumb. | ||
if (self.estimated_content_height) |h| { | ||
const content_height_f: f32 = @floatFromInt(h); | ||
|
||
const thumb_height_f = widget_height_f * widget_height_f / content_height_f; | ||
break :height @intFromFloat(@max(thumb_height_f, 1)); | ||
} | ||
|
||
// Otherwise we estimate the size of the thumb based on the number of child elements | ||
// drawn in the scroll view, and the number of total child elements. | ||
|
||
const num_children_rendered_f: f32 = @floatFromInt(scroll_view_surface.children.len); | ||
|
||
const thumb_height_f = widget_height_f * num_children_rendered_f / total_num_children_f; | ||
break :height @intFromFloat(@max(thumb_height_f, 1)); | ||
}; | ||
|
||
// We also need to know the position of the thumb in the scroll bar. To find that we use the | ||
// index of the top-most child widget rendered in the ScrollView. | ||
|
||
const thumb_top: u32 = if (self.scroll_view.scroll.top == 0) | ||
0 | ||
else if (self.scroll_view.scroll.has_more) pos: { | ||
const top_child_idx_f: f32 = @floatFromInt(self.scroll_view.scroll.top); | ||
const thumb_top_f = widget_height_f * top_child_idx_f / total_num_children_f; | ||
|
||
break :pos @intFromFloat(thumb_top_f); | ||
} else max.height -| thumb_height; | ||
|
||
// Once we know the thumb height and its position we can draw the scroll bar. | ||
|
||
const scroll_bar = try vxfw.Surface.init( | ||
ctx.arena, | ||
self.widget(), | ||
.{ | ||
.width = 1, | ||
// We make sure to make room for the horizontal scroll bar if it's being drawn. | ||
.height = max.height -| @intFromBool(self.draw_horizontal_scrollbar), | ||
}, | ||
); | ||
|
||
const thumb_end_row = thumb_top + thumb_height; | ||
for (thumb_top..thumb_end_row) |row| { | ||
scroll_bar.writeCell( | ||
0, | ||
@intCast(row), | ||
vertical_scrollbar_thumb, | ||
); | ||
} | ||
|
||
try children.append(.{ | ||
.origin = .{ .row = 0, .col = max.width -| 1 }, | ||
.surface = scroll_bar, | ||
}); | ||
} | ||
|
||
// 5. TODO: Draw the horizontal scroll bar. | ||
|
||
// if (self.draw_horizontal_scrollbar) { | ||
// const scroll_bar = try vxfw.Surface.init( | ||
// ctx.arena, | ||
// self.widget(), | ||
// .{ .width = max.width, .height = 1 }, | ||
// ); | ||
// for (0..max.width / 2) |col| { | ||
// scroll_bar.writeCell( | ||
// @intCast(col), | ||
// 0, | ||
// horizontal_scrollbar_thumb, | ||
// ); | ||
// } | ||
// try children.append(.{ | ||
// .origin = .{ .row = max.height -| 1, .col = 0 }, | ||
// .surface = scroll_bar, | ||
// }); | ||
// } | ||
|
||
return .{ | ||
.size = ctx.max.size(), | ||
.widget = self.widget(), | ||
.buffer = &.{}, | ||
.children = children.items, | ||
}; | ||
} | ||
|
||
test "refAllDecls" { | ||
std.testing.refAllDecls(@This()); | ||
} |
Oops, something went wrong.