this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

vxfw: Add ScrollBars widget

This widget is intended to wrap a ScrollView widget and show vertical
and horizontal scroll bars as indicators of scroll position in the
ScrollView. It's recommended to provide the estimated content sizes
with as much accuracy as possible for the best user experience and
performance.

If estimated content sizes are not provided the scroll bar sizes and
positions will be estimated using the size of child arrays. This is not
perfect and will cause inconsistencies if the child widgets aren't all
the exact same heights.

authored by

Kristófer R and committed by
Tim Culverhouse
46cdcca2 ee6c47c5

+633
+632
src/vxfw/ScrollBars.zig
··· 1 + const std = @import("std"); 2 + const vaxis = @import("../main.zig"); 3 + const vxfw = @import("vxfw.zig"); 4 + 5 + const Allocator = std.mem.Allocator; 6 + 7 + const ScrollBars = @This(); 8 + 9 + /// The ScrollBars widget must contain a ScrollView widget. The scroll bars drawn will be for the 10 + /// scroll view contained in the ScrollBars widget. 11 + scroll_view: vxfw.ScrollView, 12 + /// If `true` a horizontal scroll bar will be drawn. Set to `false` to hide the horizontal scroll 13 + /// bar. Defaults to `true`. 14 + draw_horizontal_scrollbar: bool = true, 15 + /// If `true` a vertical scroll bar will be drawn. Set to `false` to hide the vertical scroll bar. 16 + /// Defaults to `true`. 17 + draw_vertical_scrollbar: bool = true, 18 + /// The estimated height of all the content in the ScrollView. When provided this height will be 19 + /// used to calculate the size of the scrollbar's thumb. If this is not provided the widget will 20 + /// make a best effort estimate of the size of the thumb using the number of elements rendered at 21 + /// any given time. This will cause inconsistent thumb sizes - and possibly inconsistent 22 + /// positioning - if different elements in the ScrollView have different heights. For the best user 23 + /// experience, providing this estimate is strongly recommended. 24 + /// 25 + /// Note that this doesn't necessarily have to be an accurate estimate and the tolerance for larger 26 + /// views is quite forgiving, especially if you overshoot the estimate. 27 + estimated_content_height: ?u32 = null, 28 + /// The estimated width of all the content in the ScrollView. When provided this width will be used 29 + /// to calculate the size of the scrollbar's thumb. If this is not provided the widget will make a 30 + /// best effort estimate of the size of the thumb using the width of the elements rendered at any 31 + /// given time. This will cause inconsistent thumb sizes - and possibly inconsistent positioning - 32 + /// if different elements in the ScrollView have different widths. For the best user experience, 33 + /// providing this estimate is strongly recommended. 34 + /// 35 + /// Note that this doesn't necessarily have to be 36 + /// an accurate estimate and the tolerance for larger views is quite forgiving, especially if you 37 + /// overshoot the estimate. 38 + estimated_content_width: ?u32 = null, 39 + /// The cell drawn for the vertical scroll thumb. Replace this to customize the scroll thumb. Must 40 + /// have a 1 column width. 41 + vertical_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "▐", .width = 1 } }, 42 + /// The cell drawn for the vertical scroll thumb while it's being hovered. Replace this to customize 43 + /// the scroll thumb. Must have a 1 column width. 44 + vertical_scrollbar_hover_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "█", .width = 1 } }, 45 + /// The cell drawn for the vertical scroll thumb while it's being dragged by the mouse. Replace this 46 + /// to customize the scroll thumb. Must have a 1 column width. 47 + vertical_scrollbar_drag_thumb: vaxis.Cell = .{ 48 + .char = .{ .grapheme = "█", .width = 1 }, 49 + .style = .{ .fg = .{ .index = 4 } }, 50 + }, 51 + /// The cell drawn for the vertical scroll thumb. Replace this to customize the scroll thumb. Must 52 + /// have a 1 column width. 53 + horizontal_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "▃", .width = 1 } }, 54 + /// The cell drawn for the horizontal scroll thumb while it's being hovered. Replace this to 55 + /// customize the scroll thumb. Must have a 1 column width. 56 + horizontal_scrollbar_hover_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "█", .width = 1 } }, 57 + /// The cell drawn for the horizontal scroll thumb while it's being dragged by the mouse. Replace 58 + /// this to customize the scroll thumb. Must have a 1 column width. 59 + horizontal_scrollbar_drag_thumb: vaxis.Cell = .{ 60 + .char = .{ .grapheme = "█", .width = 1 }, 61 + .style = .{ .fg = .{ .index = 4 } }, 62 + }, 63 + 64 + /// You should not change this variable, treat it as private to the implementation. Used to track 65 + /// the size of the widget so we can locate scroll bars for mouse interaction. 66 + last_frame_size: vxfw.Size = .{ .width = 0, .height = 0 }, 67 + /// You should not change this variable, treat it as private to the implementation. Used to track 68 + /// the width of the content so we map horizontal scroll thumb position to view position. 69 + last_frame_max_content_width: u32 = 0, 70 + /// You should not change this variable, treat it as private to the implementation. Used to track 71 + /// the position of the mouse relative to the scroll thumb for mouse interaction. 72 + mouse_offset_into_thumb: u8 = 0, 73 + 74 + /// You should not change this variable, treat it as private to the implementation. Used to track 75 + /// the position of the scroll thumb for mouse interaction. 76 + vertical_thumb_top_row: u32 = 0, 77 + /// You should not change this variable, treat it as private to the implementation. Used to track 78 + /// the position of the scroll thumb for mouse interaction. 79 + vertical_thumb_bottom_row: u32 = 0, 80 + /// You should not change this variable, treat it as private to the implementation. Used to track 81 + /// whether the scroll thumb is hovered or not so we can set the right hover style for the thumb. 82 + is_hovering_vertical_thumb: bool = false, 83 + /// You should not change this variable, treat it as private to the implementation. Used to track 84 + /// whether the thumb is currently being dragged, which is important to allowing the mouse to leave 85 + /// the scroll thumb while it's being dragged. 86 + is_dragging_vertical_thumb: bool = false, 87 + 88 + /// You should not change this variable, treat it as private to the implementation. Used to track 89 + /// the position of the scroll thumb for mouse interaction. 90 + horizontal_thumb_start_col: u32 = 0, 91 + /// You should not change this variable, treat it as private to the implementation. Used to track 92 + /// the position of the scroll thumb for mouse interaction. 93 + horizontal_thumb_end_col: u32 = 0, 94 + /// You should not change this variable, treat it as private to the implementation. Used to track 95 + /// whether the scroll thumb is hovered or not so we can set the right hover style for the thumb. 96 + is_hovering_horizontal_thumb: bool = false, 97 + /// You should not change this variable, treat it as private to the implementation. Used to track 98 + /// whether the thumb is currently being dragged, which is important to allowing the mouse to leave 99 + /// the scroll thumb while it's being dragged. 100 + is_dragging_horizontal_thumb: bool = false, 101 + 102 + pub fn widget(self: *const ScrollBars) vxfw.Widget { 103 + return .{ 104 + .userdata = @constCast(self), 105 + .eventHandler = typeErasedEventHandler, 106 + .captureHandler = typeErasedCaptureHandler, 107 + .drawFn = typeErasedDrawFn, 108 + }; 109 + } 110 + 111 + fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 112 + const self: *ScrollBars = @ptrCast(@alignCast(ptr)); 113 + return self.handleEvent(ctx, event); 114 + } 115 + fn typeErasedCaptureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 116 + const self: *ScrollBars = @ptrCast(@alignCast(ptr)); 117 + return self.handleCapture(ctx, event); 118 + } 119 + 120 + fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 121 + const self: *ScrollBars = @ptrCast(@alignCast(ptr)); 122 + return self.draw(ctx); 123 + } 124 + 125 + pub fn handleCapture(self: *ScrollBars, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 126 + switch (event) { 127 + .mouse => |mouse| { 128 + if (self.is_dragging_vertical_thumb) { 129 + // Stop dragging the thumb when the mouse is released. 130 + if (mouse.type == .release and 131 + mouse.button == .left and 132 + self.is_dragging_vertical_thumb) 133 + { 134 + // If we just let the scroll thumb go after dragging we need to make sure we 135 + // redraw so the right style is immediately applied to the thumb. 136 + if (self.is_dragging_vertical_thumb) { 137 + self.is_dragging_vertical_thumb = false; 138 + ctx.redraw = true; 139 + } 140 + 141 + const is_mouse_over_vertical_thumb = 142 + mouse.col == self.last_frame_size.width -| 1 and 143 + mouse.row >= self.vertical_thumb_top_row and 144 + mouse.row < self.vertical_thumb_bottom_row; 145 + 146 + // If we're not hovering the scroll bar after letting it go, we should trigger a 147 + // redraw so it goes back to its narrow, non-active, state immediately. 148 + if (!is_mouse_over_vertical_thumb) { 149 + self.is_hovering_vertical_thumb = false; 150 + ctx.redraw = true; 151 + } 152 + 153 + // No need to redraw yet, but we must consume the event so ending the drag 154 + // action doesn't trigger some other event handler. 155 + return ctx.consumeEvent(); 156 + } 157 + 158 + // Process dragging the vertical thumb. 159 + if (mouse.type == .drag) { 160 + // Make sure we consume the event if we're currently dragging the mouse so other 161 + // events aren't sent in the mean time. 162 + ctx.consumeEvent(); 163 + 164 + // New scroll thumb position. 165 + const new_thumb_top = mouse.row -| self.mouse_offset_into_thumb; 166 + 167 + // If the new thumb position is at the top we know we've scrolled to the top of 168 + // the scroll view. 169 + if (new_thumb_top == 0) { 170 + self.scroll_view.scroll.top = 0; 171 + return ctx.consumeAndRedraw(); 172 + } 173 + 174 + const new_thumb_top_f: f32 = @floatFromInt(new_thumb_top); 175 + const widget_height_f: f32 = @floatFromInt(self.last_frame_size.height); 176 + const total_num_children_f: f32 = count: { 177 + if (self.scroll_view.item_count) |c| break :count @floatFromInt(c); 178 + 179 + switch (self.scroll_view.children) { 180 + .slice => |slice| break :count @floatFromInt(slice.len), 181 + .builder => |builder| { 182 + var counter: usize = 0; 183 + while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_| 184 + counter += 1; 185 + 186 + break :count @floatFromInt(counter); 187 + }, 188 + } 189 + }; 190 + 191 + const new_top_child_idx_f = 192 + new_thumb_top_f * 193 + total_num_children_f / widget_height_f; 194 + self.scroll_view.scroll.top = @intFromFloat(new_top_child_idx_f); 195 + 196 + return ctx.consumeAndRedraw(); 197 + } 198 + } 199 + 200 + if (self.is_dragging_horizontal_thumb) { 201 + // Stop dragging the thumb when the mouse is released. 202 + if (mouse.type == .release and 203 + mouse.button == .left and 204 + self.is_dragging_horizontal_thumb) 205 + { 206 + // If we just let the scroll thumb go after dragging we need to make sure we 207 + // redraw so the right style is immediately applied to the thumb. 208 + if (self.is_dragging_horizontal_thumb) { 209 + self.is_dragging_horizontal_thumb = false; 210 + ctx.redraw = true; 211 + } 212 + 213 + const is_mouse_over_horizontal_thumb = 214 + mouse.row == self.last_frame_size.height -| 1 and 215 + mouse.col >= self.horizontal_thumb_start_col and 216 + mouse.col < self.horizontal_thumb_end_col; 217 + 218 + // If we're not hovering the scroll bar after letting it go, we should trigger a 219 + // redraw so it goes back to its narrow, non-active, state immediately. 220 + if (!is_mouse_over_horizontal_thumb) { 221 + self.is_hovering_horizontal_thumb = false; 222 + ctx.redraw = true; 223 + } 224 + 225 + // No need to redraw yet, but we must consume the event so ending the drag 226 + // action doesn't trigger some other event handler. 227 + return ctx.consumeEvent(); 228 + } 229 + 230 + // Process dragging the horizontal thumb. 231 + if (mouse.type == .drag) { 232 + // Make sure we consume the event if we're currently dragging the mouse so other 233 + // events aren't sent in the mean time. 234 + ctx.consumeEvent(); 235 + 236 + // New scroll thumb position. 237 + const new_thumb_col_start = mouse.col -| self.mouse_offset_into_thumb; 238 + 239 + // If the new thumb position is at the horizontal beginning of the current view 240 + // we know we've scrolled to the beginning of the scroll view. 241 + if (new_thumb_col_start == 0) { 242 + self.scroll_view.scroll.left = 0; 243 + return ctx.consumeAndRedraw(); 244 + } 245 + 246 + const new_thumb_col_start_f: f32 = @floatFromInt(new_thumb_col_start); 247 + const widget_width_f: f32 = @floatFromInt(self.last_frame_size.width); 248 + 249 + const max_content_width_f: f32 = 250 + @floatFromInt(self.last_frame_max_content_width); 251 + 252 + const new_view_col_start_f = 253 + new_thumb_col_start_f * max_content_width_f / widget_width_f; 254 + const new_view_col_start: u32 = @intFromFloat(@ceil(new_view_col_start_f)); 255 + 256 + self.scroll_view.scroll.left = 257 + @min(new_view_col_start, self.last_frame_max_content_width); 258 + 259 + return ctx.consumeAndRedraw(); 260 + } 261 + } 262 + }, 263 + else => {}, 264 + } 265 + } 266 + 267 + pub fn handleEvent(self: *ScrollBars, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void { 268 + switch (event) { 269 + .mouse => |mouse| { 270 + // 1. Process vertical scroll thumb hover. 271 + 272 + const is_mouse_over_vertical_thumb = 273 + mouse.col == self.last_frame_size.width -| 1 and 274 + mouse.row >= self.vertical_thumb_top_row and 275 + mouse.row < self.vertical_thumb_bottom_row; 276 + 277 + // Make sure we only update the state and redraw when it's necessary. 278 + if (!self.is_hovering_vertical_thumb and is_mouse_over_vertical_thumb) { 279 + self.is_hovering_vertical_thumb = true; 280 + ctx.redraw = true; 281 + } else if (self.is_hovering_vertical_thumb and !is_mouse_over_vertical_thumb) { 282 + self.is_hovering_vertical_thumb = false; 283 + ctx.redraw = true; 284 + } 285 + 286 + const did_start_dragging_vertical_thumb = is_mouse_over_vertical_thumb and 287 + mouse.type == .press and mouse.button == .left; 288 + 289 + if (did_start_dragging_vertical_thumb) { 290 + self.is_dragging_vertical_thumb = true; 291 + self.mouse_offset_into_thumb = @intCast(mouse.row -| self.vertical_thumb_top_row); 292 + 293 + // No need to redraw yet, but we must consume the event. 294 + return ctx.consumeEvent(); 295 + } 296 + 297 + // 2. Process horizontal scroll thumb hover. 298 + 299 + const is_mouse_over_horizontal_thumb = 300 + mouse.row == self.last_frame_size.height -| 1 and 301 + mouse.col >= self.horizontal_thumb_start_col and 302 + mouse.col < self.horizontal_thumb_end_col; 303 + 304 + // Make sure we only update the state and redraw when it's necessary. 305 + if (!self.is_hovering_horizontal_thumb and is_mouse_over_horizontal_thumb) { 306 + self.is_hovering_horizontal_thumb = true; 307 + ctx.redraw = true; 308 + } else if (self.is_hovering_horizontal_thumb and !is_mouse_over_horizontal_thumb) { 309 + self.is_hovering_horizontal_thumb = false; 310 + ctx.redraw = true; 311 + } 312 + 313 + const did_start_dragging_horizontal_thumb = is_mouse_over_horizontal_thumb and 314 + mouse.type == .press and mouse.button == .left; 315 + 316 + if (did_start_dragging_horizontal_thumb) { 317 + self.is_dragging_horizontal_thumb = true; 318 + self.mouse_offset_into_thumb = @intCast( 319 + mouse.col -| self.horizontal_thumb_start_col, 320 + ); 321 + 322 + // No need to redraw yet, but we must consume the event. 323 + return ctx.consumeEvent(); 324 + } 325 + }, 326 + .mouse_leave => self.is_dragging_vertical_thumb = false, 327 + else => {}, 328 + } 329 + } 330 + 331 + pub fn draw(self: *ScrollBars, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 332 + var children = std.ArrayList(vxfw.SubSurface).init(ctx.arena); 333 + 334 + // 1. If we're not drawing the scrollbars we can just draw the ScrollView directly. 335 + 336 + if (!self.draw_vertical_scrollbar and !self.draw_horizontal_scrollbar) { 337 + try children.append(.{ 338 + .origin = .{ .row = 0, .col = 0 }, 339 + .surface = try self.scroll_view.draw(ctx), 340 + }); 341 + 342 + return .{ 343 + .size = ctx.max.size(), 344 + .widget = self.widget(), 345 + .buffer = &.{}, 346 + .children = children.items, 347 + }; 348 + } 349 + 350 + // 2. Otherwise we can draw the scrollbars. 351 + 352 + const max = ctx.max.size(); 353 + self.last_frame_size = max; 354 + 355 + // 3. Draw the scroll view itself. 356 + 357 + const scroll_view_surface = try self.scroll_view.draw(ctx.withConstraints( 358 + ctx.min, 359 + .{ 360 + // We make sure to make room for the scrollbars if required. 361 + .width = max.width -| @intFromBool(self.draw_vertical_scrollbar), 362 + .height = max.height -| @intFromBool(self.draw_horizontal_scrollbar), 363 + }, 364 + )); 365 + 366 + try children.append(.{ 367 + .origin = .{ .row = 0, .col = 0 }, 368 + .surface = scroll_view_surface, 369 + }); 370 + 371 + // 4. Draw the vertical scroll bar. 372 + 373 + if (self.draw_vertical_scrollbar) vertical: { 374 + // If we can't scroll, then there's no need to draw the scroll bar. 375 + if (self.scroll_view.scroll.top == 0 and !self.scroll_view.scroll.has_more_vertical) 376 + break :vertical; 377 + 378 + // To draw the vertical scrollbar we need to know how big the scroll bar thumb should be. 379 + // If we've been provided with an estimated height we use that to figure out how big the 380 + // thumb should be, otherwise we estimate the size based on how many of the children were 381 + // actually drawn in the ScrollView. 382 + 383 + const widget_height_f: f32 = @floatFromInt(scroll_view_surface.size.height); 384 + const total_num_children_f: f32 = count: { 385 + if (self.scroll_view.item_count) |c| break :count @floatFromInt(c); 386 + 387 + switch (self.scroll_view.children) { 388 + .slice => |slice| break :count @floatFromInt(slice.len), 389 + .builder => |builder| { 390 + var counter: usize = 0; 391 + while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_| 392 + counter += 1; 393 + 394 + break :count @floatFromInt(counter); 395 + }, 396 + } 397 + }; 398 + 399 + const thumb_height: u16 = height: { 400 + // If we know the height, we can use the height of the current view to determine the 401 + // size of the thumb. 402 + if (self.estimated_content_height) |h| { 403 + const content_height_f: f32 = @floatFromInt(h); 404 + 405 + const thumb_height_f = widget_height_f * widget_height_f / content_height_f; 406 + break :height @intFromFloat(@max(thumb_height_f, 1)); 407 + } 408 + 409 + // Otherwise we estimate the size of the thumb based on the number of child elements 410 + // drawn in the scroll view, and the number of total child elements. 411 + 412 + const num_children_rendered_f: f32 = @floatFromInt(scroll_view_surface.children.len); 413 + 414 + const thumb_height_f = widget_height_f * num_children_rendered_f / total_num_children_f; 415 + break :height @intFromFloat(@max(thumb_height_f, 1)); 416 + }; 417 + 418 + // We also need to know the position of the thumb in the scroll bar. To find that we use the 419 + // index of the top-most child widget rendered in the ScrollView. 420 + 421 + const thumb_top: u32 = if (self.scroll_view.scroll.top == 0) 422 + 0 423 + else if (self.scroll_view.scroll.has_more_vertical) pos: { 424 + const top_child_idx_f: f32 = @floatFromInt(self.scroll_view.scroll.top); 425 + const thumb_top_f = widget_height_f * top_child_idx_f / total_num_children_f; 426 + 427 + break :pos @intFromFloat(thumb_top_f); 428 + } else max.height -| thumb_height; 429 + 430 + // Once we know the thumb height and its position we can draw the scroll bar. 431 + 432 + const scroll_bar = try vxfw.Surface.init( 433 + ctx.arena, 434 + self.widget(), 435 + .{ 436 + .width = 1, 437 + // We make sure to make room for the horizontal scroll bar if it's being drawn. 438 + .height = max.height -| @intFromBool(self.draw_horizontal_scrollbar), 439 + }, 440 + ); 441 + 442 + const thumb_end_row = thumb_top + thumb_height; 443 + for (thumb_top..thumb_end_row) |row| { 444 + scroll_bar.writeCell( 445 + 0, 446 + @intCast(row), 447 + if (self.is_dragging_vertical_thumb) 448 + self.vertical_scrollbar_drag_thumb 449 + else if (self.is_hovering_vertical_thumb) 450 + self.vertical_scrollbar_hover_thumb 451 + else 452 + self.vertical_scrollbar_thumb, 453 + ); 454 + } 455 + 456 + self.vertical_thumb_top_row = thumb_top; 457 + self.vertical_thumb_bottom_row = thumb_end_row; 458 + 459 + try children.append(.{ 460 + .origin = .{ .row = 0, .col = max.width -| 1 }, 461 + .surface = scroll_bar, 462 + }); 463 + } 464 + 465 + // 5. Draw the horizontal scroll bar. 466 + 467 + const is_horizontally_scrolled = self.scroll_view.scroll.left > 0; 468 + const has_more_horizontal_content = self.scroll_view.scroll.has_more_horizontal; 469 + 470 + const should_draw_scrollbar = is_horizontally_scrolled or has_more_horizontal_content; 471 + 472 + if (self.draw_horizontal_scrollbar and should_draw_scrollbar) { 473 + const scroll_bar = try vxfw.Surface.init( 474 + ctx.arena, 475 + self.widget(), 476 + .{ .width = max.width, .height = 1 }, 477 + ); 478 + 479 + const widget_width_f: f32 = @floatFromInt(max.width); 480 + 481 + const max_content_width: u32 = width: { 482 + if (self.estimated_content_width) |w| break :width w; 483 + 484 + var max_content_width: u32 = 0; 485 + for (scroll_view_surface.children) |child| { 486 + max_content_width = @max(max_content_width, child.surface.size.width); 487 + } 488 + break :width max_content_width; 489 + }; 490 + const max_content_width_f: f32 = 491 + if (self.scroll_view.scroll.left + max.width > max_content_width) 492 + // If we've managed to overscroll horizontally for whatever reason - for example if the 493 + // content changes - we make sure the scroll thumb doesn't disappear by increasing the 494 + // max content width to match the current overscrolled position. 495 + @floatFromInt(self.scroll_view.scroll.left + max.width) 496 + else 497 + @floatFromInt(max_content_width); 498 + 499 + self.last_frame_max_content_width = max_content_width; 500 + 501 + const thumb_width_f: f32 = widget_width_f * widget_width_f / max_content_width_f; 502 + const thumb_width: u32 = @intFromFloat(@max(thumb_width_f, 1)); 503 + 504 + const view_start_col_f: f32 = @floatFromInt(self.scroll_view.scroll.left); 505 + const thumb_start_f = view_start_col_f * widget_width_f / max_content_width_f; 506 + 507 + const thumb_start: u32 = @intFromFloat(thumb_start_f); 508 + const thumb_end = thumb_start + thumb_width; 509 + for (thumb_start..thumb_end) |col| { 510 + scroll_bar.writeCell( 511 + @intCast(col), 512 + 0, 513 + if (self.is_dragging_horizontal_thumb) 514 + self.horizontal_scrollbar_drag_thumb 515 + else if (self.is_hovering_horizontal_thumb) 516 + self.horizontal_scrollbar_hover_thumb 517 + else 518 + self.horizontal_scrollbar_thumb, 519 + ); 520 + } 521 + self.horizontal_thumb_start_col = thumb_start; 522 + self.horizontal_thumb_end_col = thumb_end; 523 + try children.append(.{ 524 + .origin = .{ .row = max.height -| 1, .col = 0 }, 525 + .surface = scroll_bar, 526 + }); 527 + } 528 + 529 + return .{ 530 + .size = ctx.max.size(), 531 + .widget = self.widget(), 532 + .buffer = &.{}, 533 + .children = children.items, 534 + }; 535 + } 536 + 537 + test ScrollBars { 538 + // Create child widgets 539 + const Text = @import("Text.zig"); 540 + const abc: Text = .{ .text = "abc\n def\n ghi" }; 541 + const def: Text = .{ .text = "def" }; 542 + const ghi: Text = .{ .text = "ghi" }; 543 + const jklmno: Text = .{ .text = "jkl\n mno" }; 544 + // 545 + // 0 |abc| 546 + // 1 | d|ef 547 + // 2 | g|hi 548 + // 3 |def| 549 + // 4 ghi 550 + // 5 jkl 551 + // 6 mno 552 + 553 + // Create the scroll view 554 + const ScrollView = @import("ScrollView.zig"); 555 + const scroll_view: ScrollView = .{ 556 + .wheel_scroll = 1, // Set wheel scroll to one 557 + .children = .{ .slice = &.{ 558 + abc.widget(), 559 + def.widget(), 560 + ghi.widget(), 561 + jklmno.widget(), 562 + } }, 563 + }; 564 + 565 + // Create the scroll bars. 566 + var scroll_bars: ScrollBars = .{ 567 + .scroll_view = scroll_view, 568 + .estimated_content_height = 7, 569 + .estimated_content_width = 5, 570 + }; 571 + 572 + // Boiler plate draw context 573 + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 574 + defer arena.deinit(); 575 + const ucd = try vaxis.Unicode.init(arena.allocator()); 576 + vxfw.DrawContext.init(&ucd, .unicode); 577 + 578 + const scroll_widget = scroll_bars.widget(); 579 + const draw_ctx: vxfw.DrawContext = .{ 580 + .arena = arena.allocator(), 581 + .min = .{}, 582 + .max = .{ .width = 3, .height = 4 }, 583 + .cell_size = .{ .width = 10, .height = 20 }, 584 + }; 585 + 586 + var surface = try scroll_widget.draw(draw_ctx); 587 + // Scroll bars should have 3 children: both scrollbars and the scroll view. 588 + try std.testing.expectEqual(3, surface.children.len); 589 + 590 + // Hide only the horizontal scroll bar. 591 + scroll_bars.draw_horizontal_scrollbar = false; 592 + surface = try scroll_widget.draw(draw_ctx); 593 + // Scroll bars should have 2 children: vertical scroll bar and the scroll view. 594 + try std.testing.expectEqual(2, surface.children.len); 595 + 596 + // Hide only the vertical scroll bar. 597 + scroll_bars.draw_horizontal_scrollbar = true; 598 + scroll_bars.draw_vertical_scrollbar = false; 599 + surface = try scroll_widget.draw(draw_ctx); 600 + // Scroll bars should have 2 children: vertical scroll bar and the scroll view. 601 + try std.testing.expectEqual(2, surface.children.len); 602 + 603 + // Hide both scroll bars. 604 + scroll_bars.draw_horizontal_scrollbar = false; 605 + surface = try scroll_widget.draw(draw_ctx); 606 + // Scroll bars should have 1 child: the scroll view. 607 + try std.testing.expectEqual(1, surface.children.len); 608 + 609 + // Re-enable scroll bars. 610 + scroll_bars.draw_horizontal_scrollbar = true; 611 + scroll_bars.draw_vertical_scrollbar = true; 612 + 613 + // Even though the estimated size is smaller than the draw area, we still render the scroll 614 + // bars if the scroll view knows we haven't rendered everything. 615 + scroll_bars.estimated_content_height = 2; 616 + scroll_bars.estimated_content_width = 1; 617 + surface = try scroll_widget.draw(draw_ctx); 618 + // Scroll bars should have 3 children: both scrollbars and the scroll view. 619 + try std.testing.expectEqual(3, surface.children.len); 620 + 621 + // The scroll view should be able to tell whether the scroll bars need to be rendered or not 622 + // even if estimated content sizes aren't provided. 623 + scroll_bars.estimated_content_height = null; 624 + scroll_bars.estimated_content_width = null; 625 + surface = try scroll_widget.draw(draw_ctx); 626 + // Scroll bars should have 3 children: both scrollbars and the scroll view. 627 + try std.testing.expectEqual(3, surface.children.len); 628 + } 629 + 630 + test "refAllDecls" { 631 + std.testing.refAllDecls(@This()); 632 + }
+1
src/vxfw/vxfw.zig
··· 20 20 pub const Padding = @import("Padding.zig"); 21 21 pub const RichText = @import("RichText.zig"); 22 22 pub const ScrollView = @import("ScrollView.zig"); 23 + pub const ScrollBars = @import("ScrollBars.zig"); 23 24 pub const SizedBox = @import("SizedBox.zig"); 24 25 pub const SplitView = @import("SplitView.zig"); 25 26 pub const Spinner = @import("Spinner.zig");