this repo has no description
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}