this repo has no description
13
fork

Configure Feed

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

at main 632 lines 28 kB view raw
1const std = @import("std"); 2const vaxis = @import("../main.zig"); 3const vxfw = @import("vxfw.zig"); 4 5const Allocator = std.mem.Allocator; 6 7const 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. 11scroll_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`. 14draw_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`. 17draw_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. 27estimated_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. 38estimated_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. 41vertical_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. 44vertical_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. 47vertical_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. 53horizontal_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. 56horizontal_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. 59horizontal_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. 66last_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. 69last_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. 72mouse_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. 76vertical_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. 79vertical_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. 82is_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. 86is_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. 90horizontal_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. 93horizontal_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. 96is_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. 100is_dragging_horizontal_thumb: bool = false, 101 102pub 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 111fn 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} 115fn 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 120fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 121 const self: *ScrollBars = @ptrCast(@alignCast(ptr)); 122 return self.draw(ctx); 123} 124 125pub 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 267pub 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 const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col); 272 const mouse_row: u16 = if (mouse.row < 0) 0 else @intCast(mouse.row); 273 const is_mouse_over_vertical_thumb = 274 mouse_col == self.last_frame_size.width -| 1 and 275 mouse_row >= self.vertical_thumb_top_row and 276 mouse_row < self.vertical_thumb_bottom_row; 277 278 // Make sure we only update the state and redraw when it's necessary. 279 if (!self.is_hovering_vertical_thumb and is_mouse_over_vertical_thumb) { 280 self.is_hovering_vertical_thumb = true; 281 ctx.redraw = true; 282 } else if (self.is_hovering_vertical_thumb and !is_mouse_over_vertical_thumb) { 283 self.is_hovering_vertical_thumb = false; 284 ctx.redraw = true; 285 } 286 287 const did_start_dragging_vertical_thumb = is_mouse_over_vertical_thumb and 288 mouse.type == .press and mouse.button == .left; 289 290 if (did_start_dragging_vertical_thumb) { 291 self.is_dragging_vertical_thumb = true; 292 self.mouse_offset_into_thumb = @intCast(mouse_row -| self.vertical_thumb_top_row); 293 294 // No need to redraw yet, but we must consume the event. 295 return ctx.consumeEvent(); 296 } 297 298 // 2. Process horizontal scroll thumb hover. 299 300 const is_mouse_over_horizontal_thumb = 301 mouse_row == self.last_frame_size.height -| 1 and 302 mouse_col >= self.horizontal_thumb_start_col and 303 mouse_col < self.horizontal_thumb_end_col; 304 305 // Make sure we only update the state and redraw when it's necessary. 306 if (!self.is_hovering_horizontal_thumb and is_mouse_over_horizontal_thumb) { 307 self.is_hovering_horizontal_thumb = true; 308 ctx.redraw = true; 309 } else if (self.is_hovering_horizontal_thumb and !is_mouse_over_horizontal_thumb) { 310 self.is_hovering_horizontal_thumb = false; 311 ctx.redraw = true; 312 } 313 314 const did_start_dragging_horizontal_thumb = is_mouse_over_horizontal_thumb and 315 mouse.type == .press and mouse.button == .left; 316 317 if (did_start_dragging_horizontal_thumb) { 318 self.is_dragging_horizontal_thumb = true; 319 self.mouse_offset_into_thumb = @intCast( 320 mouse_col -| self.horizontal_thumb_start_col, 321 ); 322 323 // No need to redraw yet, but we must consume the event. 324 return ctx.consumeEvent(); 325 } 326 }, 327 .mouse_leave => self.is_dragging_vertical_thumb = false, 328 else => {}, 329 } 330} 331 332pub fn draw(self: *ScrollBars, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface { 333 var children: std.ArrayList(vxfw.SubSurface) = .empty; 334 335 // 1. If we're not drawing the scrollbars we can just draw the ScrollView directly. 336 337 if (!self.draw_vertical_scrollbar and !self.draw_horizontal_scrollbar) { 338 try children.append(ctx.arena, .{ 339 .origin = .{ .row = 0, .col = 0 }, 340 .surface = try self.scroll_view.draw(ctx), 341 }); 342 343 return .{ 344 .size = ctx.max.size(), 345 .widget = self.widget(), 346 .buffer = &.{}, 347 .children = children.items, 348 }; 349 } 350 351 // 2. Otherwise we can draw the scrollbars. 352 353 const max = ctx.max.size(); 354 self.last_frame_size = max; 355 356 // 3. Draw the scroll view itself. 357 358 const scroll_view_surface = try self.scroll_view.draw(ctx.withConstraints( 359 ctx.min, 360 .{ 361 // We make sure to make room for the scrollbars if required. 362 .width = max.width -| @intFromBool(self.draw_vertical_scrollbar), 363 .height = max.height -| @intFromBool(self.draw_horizontal_scrollbar), 364 }, 365 )); 366 367 try children.append(ctx.arena, .{ 368 .origin = .{ .row = 0, .col = 0 }, 369 .surface = scroll_view_surface, 370 }); 371 372 // 4. Draw the vertical scroll bar. 373 374 if (self.draw_vertical_scrollbar) vertical: { 375 // If we can't scroll, then there's no need to draw the scroll bar. 376 if (self.scroll_view.scroll.top == 0 and !self.scroll_view.scroll.has_more_vertical) 377 break :vertical; 378 379 // To draw the vertical scrollbar we need to know how big the scroll bar thumb should be. 380 // If we've been provided with an estimated height we use that to figure out how big the 381 // thumb should be, otherwise we estimate the size based on how many of the children were 382 // actually drawn in the ScrollView. 383 384 const widget_height_f: f32 = @floatFromInt(scroll_view_surface.size.height); 385 const total_num_children_f: f32 = count: { 386 if (self.scroll_view.item_count) |c| break :count @floatFromInt(c); 387 388 switch (self.scroll_view.children) { 389 .slice => |slice| break :count @floatFromInt(slice.len), 390 .builder => |builder| { 391 var counter: usize = 0; 392 while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_| 393 counter += 1; 394 395 break :count @floatFromInt(counter); 396 }, 397 } 398 }; 399 400 const thumb_height: u16 = height: { 401 // If we know the height, we can use the height of the current view to determine the 402 // size of the thumb. 403 if (self.estimated_content_height) |h| { 404 const content_height_f: f32 = @floatFromInt(h); 405 406 const thumb_height_f = widget_height_f * widget_height_f / content_height_f; 407 break :height @intFromFloat(@max(thumb_height_f, 1)); 408 } 409 410 // Otherwise we estimate the size of the thumb based on the number of child elements 411 // drawn in the scroll view, and the number of total child elements. 412 413 const num_children_rendered_f: f32 = @floatFromInt(scroll_view_surface.children.len); 414 415 const thumb_height_f = widget_height_f * num_children_rendered_f / total_num_children_f; 416 break :height @intFromFloat(@max(thumb_height_f, 1)); 417 }; 418 419 // We also need to know the position of the thumb in the scroll bar. To find that we use the 420 // index of the top-most child widget rendered in the ScrollView. 421 422 const thumb_top: u32 = if (self.scroll_view.scroll.top == 0) 423 0 424 else if (self.scroll_view.scroll.has_more_vertical) pos: { 425 const top_child_idx_f: f32 = @floatFromInt(self.scroll_view.scroll.top); 426 const thumb_top_f = widget_height_f * top_child_idx_f / total_num_children_f; 427 428 break :pos @intFromFloat(thumb_top_f); 429 } else max.height -| thumb_height; 430 431 // Once we know the thumb height and its position we can draw the scroll bar. 432 433 const scroll_bar = try vxfw.Surface.init( 434 ctx.arena, 435 self.widget(), 436 .{ 437 .width = 1, 438 // We make sure to make room for the horizontal scroll bar if it's being drawn. 439 .height = max.height -| @intFromBool(self.draw_horizontal_scrollbar), 440 }, 441 ); 442 443 const thumb_end_row = thumb_top + thumb_height; 444 for (thumb_top..thumb_end_row) |row| { 445 scroll_bar.writeCell( 446 0, 447 @intCast(row), 448 if (self.is_dragging_vertical_thumb) 449 self.vertical_scrollbar_drag_thumb 450 else if (self.is_hovering_vertical_thumb) 451 self.vertical_scrollbar_hover_thumb 452 else 453 self.vertical_scrollbar_thumb, 454 ); 455 } 456 457 self.vertical_thumb_top_row = thumb_top; 458 self.vertical_thumb_bottom_row = thumb_end_row; 459 460 try children.append(ctx.arena, .{ 461 .origin = .{ .row = 0, .col = max.width -| 1 }, 462 .surface = scroll_bar, 463 }); 464 } 465 466 // 5. Draw the horizontal scroll bar. 467 468 const is_horizontally_scrolled = self.scroll_view.scroll.left > 0; 469 const has_more_horizontal_content = self.scroll_view.scroll.has_more_horizontal; 470 471 const should_draw_scrollbar = is_horizontally_scrolled or has_more_horizontal_content; 472 473 if (self.draw_horizontal_scrollbar and should_draw_scrollbar) { 474 const scroll_bar = try vxfw.Surface.init( 475 ctx.arena, 476 self.widget(), 477 .{ .width = max.width, .height = 1 }, 478 ); 479 480 const widget_width_f: f32 = @floatFromInt(max.width); 481 482 const max_content_width: u32 = width: { 483 if (self.estimated_content_width) |w| break :width w; 484 485 var max_content_width: u32 = 0; 486 for (scroll_view_surface.children) |child| { 487 max_content_width = @max(max_content_width, child.surface.size.width); 488 } 489 break :width max_content_width; 490 }; 491 const max_content_width_f: f32 = 492 if (self.scroll_view.scroll.left + max.width > max_content_width) 493 // If we've managed to overscroll horizontally for whatever reason - for example if the 494 // content changes - we make sure the scroll thumb doesn't disappear by increasing the 495 // max content width to match the current overscrolled position. 496 @floatFromInt(self.scroll_view.scroll.left + max.width) 497 else 498 @floatFromInt(max_content_width); 499 500 self.last_frame_max_content_width = max_content_width; 501 502 const thumb_width_f: f32 = widget_width_f * widget_width_f / max_content_width_f; 503 const thumb_width: u32 = @intFromFloat(@max(thumb_width_f, 1)); 504 505 const view_start_col_f: f32 = @floatFromInt(self.scroll_view.scroll.left); 506 const thumb_start_f = view_start_col_f * widget_width_f / max_content_width_f; 507 508 const thumb_start: u32 = @intFromFloat(thumb_start_f); 509 const thumb_end = thumb_start + thumb_width; 510 for (thumb_start..thumb_end) |col| { 511 scroll_bar.writeCell( 512 @intCast(col), 513 0, 514 if (self.is_dragging_horizontal_thumb) 515 self.horizontal_scrollbar_drag_thumb 516 else if (self.is_hovering_horizontal_thumb) 517 self.horizontal_scrollbar_hover_thumb 518 else 519 self.horizontal_scrollbar_thumb, 520 ); 521 } 522 self.horizontal_thumb_start_col = thumb_start; 523 self.horizontal_thumb_end_col = thumb_end; 524 try children.append(ctx.arena, .{ 525 .origin = .{ .row = max.height -| 1, .col = 0 }, 526 .surface = scroll_bar, 527 }); 528 } 529 530 return .{ 531 .size = ctx.max.size(), 532 .widget = self.widget(), 533 .buffer = &.{}, 534 .children = children.items, 535 }; 536} 537 538test ScrollBars { 539 // Create child widgets 540 const Text = @import("Text.zig"); 541 const abc: Text = .{ .text = "abc\n def\n ghi" }; 542 const def: Text = .{ .text = "def" }; 543 const ghi: Text = .{ .text = "ghi" }; 544 const jklmno: Text = .{ .text = "jkl\n mno" }; 545 // 546 // 0 |abc| 547 // 1 | d|ef 548 // 2 | g|hi 549 // 3 |def| 550 // 4 ghi 551 // 5 jkl 552 // 6 mno 553 554 // Create the scroll view 555 const ScrollView = @import("ScrollView.zig"); 556 const scroll_view: ScrollView = .{ 557 .wheel_scroll = 1, // Set wheel scroll to one 558 .children = .{ .slice = &.{ 559 abc.widget(), 560 def.widget(), 561 ghi.widget(), 562 jklmno.widget(), 563 } }, 564 }; 565 566 // Create the scroll bars. 567 var scroll_bars: ScrollBars = .{ 568 .scroll_view = scroll_view, 569 .estimated_content_height = 7, 570 .estimated_content_width = 5, 571 }; 572 573 // Boiler plate draw context 574 var arena = std.heap.ArenaAllocator.init(std.testing.allocator); 575 defer arena.deinit(); 576 vxfw.DrawContext.init(.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 630test "refAllDecls" { 631 std.testing.refAllDecls(@This()); 632}