@@ -212,6 +212,41 @@ pub fn ensureScroll(self: *ListView) void {
212212 }
213213}
214214
215+ fn knownItemCount (self : * ListView ) ? u32 {
216+ if (self .item_count ) | count | return count ;
217+ switch (self .children ) {
218+ .slice = > | slice | {
219+ self .item_count = @intCast (slice .len );
220+ return self .item_count ;
221+ },
222+ .builder = > return null ,
223+ }
224+ }
225+
226+ /// Move the cursor directly to an item and start drawing from that item.
227+ ///
228+ /// This is useful for large jumps where walking from the current scroll position to the cursor
229+ /// would require building every child between the two positions. If the item count is known, `idx`
230+ /// is clamped to the last item.
231+ pub fn jumpToItem (self : * ListView , idx : u32 ) void {
232+ const cursor = if (self .knownItemCount ()) | count |
233+ if (count == 0 ) 0 else @min (idx , count - 1 )
234+ else
235+ idx ;
236+
237+ self .cursor = cursor ;
238+ self .scroll = .{ .top = cursor };
239+ }
240+
241+ /// Scroll directly to the bottom when the item count is known.
242+ ///
243+ /// This preserves the cursor. For builder-backed lists without `item_count`, the bottom is not
244+ /// known, so this does nothing.
245+ pub fn scrollToBottom (self : * ListView ) void {
246+ const count = self .knownItemCount () orelse return ;
247+ self .scroll = if (count == 0 ) .{} else .{ .top = count - 1 };
248+ }
249+
215250/// Inserts children until add_height is < 0
216251fn insertChildren (
217252 self : * ListView ,
@@ -672,6 +707,141 @@ test ListView {
672707 try std .testing .expectEqual (3 , list_view .cursor );
673708}
674709
710+ test "ListView: jumpToItem avoids walking intermediate children" {
711+ const Text = @import ("Text.zig" );
712+ const text : Text = .{ .text = "item" };
713+
714+ const CountingBuilder = struct {
715+ len : usize ,
716+ widget : vxfw.Widget ,
717+ calls : * usize ,
718+
719+ fn build (ptr : * const anyopaque , idx : usize , _ : usize ) ? vxfw.Widget {
720+ const self : * const @This () = @ptrCast (@alignCast (ptr ));
721+ self .calls .* += 1 ;
722+ if (idx >= self .len ) return null ;
723+ return self .widget ;
724+ }
725+ };
726+
727+ var calls : usize = 0 ;
728+ const builder : CountingBuilder = .{
729+ .len = 1000 ,
730+ .widget = text .widget (),
731+ .calls = & calls ,
732+ };
733+ var list_view : ListView = .{
734+ .item_count = @intCast (builder .len ),
735+ .children = .{ .builder = .{
736+ .userdata = & builder ,
737+ .buildFn = CountingBuilder .build ,
738+ } },
739+ };
740+
741+ var arena = std .heap .ArenaAllocator .init (std .testing .allocator );
742+ defer arena .deinit ();
743+ vxfw .DrawContext .init (.unicode );
744+
745+ const draw_ctx : vxfw.DrawContext = .{
746+ .arena = arena .allocator (),
747+ .min = .{},
748+ .max = .{ .width = 16 , .height = 4 },
749+ .cell_size = .{ .width = 10 , .height = 20 },
750+ };
751+
752+ list_view .jumpToItem (999 );
753+ const surface = try list_view .widget ().draw (draw_ctx );
754+
755+ try std .testing .expectEqual (999 , list_view .cursor );
756+ try std .testing .expectEqual (996 , list_view .scroll .top );
757+ try std .testing .expectEqual (0 , list_view .scroll .offset );
758+ try std .testing .expectEqual (4 , surface .children .len );
759+ try std .testing .expect (calls < 10 );
760+ }
761+
762+ test "ListView: jumpToItem clamps to item count" {
763+ var list_view : ListView = .{
764+ .item_count = 10 ,
765+ .children = .{ .slice = &.{} },
766+ };
767+
768+ list_view .jumpToItem (100 );
769+
770+ try std .testing .expectEqual (9 , list_view .cursor );
771+ try std .testing .expectEqual (9 , list_view .scroll .top );
772+ try std .testing .expectEqual (0 , list_view .scroll .offset );
773+ }
774+
775+ test "ListView: scrollToBottom avoids walking intermediate children" {
776+ const Text = @import ("Text.zig" );
777+ const text : Text = .{ .text = "item" };
778+
779+ const CountingBuilder = struct {
780+ len : usize ,
781+ widget : vxfw.Widget ,
782+ calls : * usize ,
783+
784+ fn build (ptr : * const anyopaque , idx : usize , _ : usize ) ? vxfw.Widget {
785+ const self : * const @This () = @ptrCast (@alignCast (ptr ));
786+ self .calls .* += 1 ;
787+ if (idx >= self .len ) return null ;
788+ return self .widget ;
789+ }
790+ };
791+
792+ var calls : usize = 0 ;
793+ const builder : CountingBuilder = .{
794+ .len = 1000 ,
795+ .widget = text .widget (),
796+ .calls = & calls ,
797+ };
798+ var list_view : ListView = .{
799+ .item_count = @intCast (builder .len ),
800+ .children = .{ .builder = .{
801+ .userdata = & builder ,
802+ .buildFn = CountingBuilder .build ,
803+ } },
804+ };
805+
806+ var arena = std .heap .ArenaAllocator .init (std .testing .allocator );
807+ defer arena .deinit ();
808+ vxfw .DrawContext .init (.unicode );
809+
810+ const draw_ctx : vxfw.DrawContext = .{
811+ .arena = arena .allocator (),
812+ .min = .{},
813+ .max = .{ .width = 16 , .height = 4 },
814+ .cell_size = .{ .width = 10 , .height = 20 },
815+ };
816+
817+ list_view .scrollToBottom ();
818+ const surface = try list_view .widget ().draw (draw_ctx );
819+
820+ try std .testing .expectEqual (0 , list_view .cursor );
821+ try std .testing .expectEqual (996 , list_view .scroll .top );
822+ try std .testing .expectEqual (0 , list_view .scroll .offset );
823+ try std .testing .expectEqual (4 , surface .children .len );
824+ try std .testing .expect (calls < 10 );
825+ }
826+
827+ test "ListView: scrollToBottom gets count from slice" {
828+ const Text = @import ("Text.zig" );
829+ const zero : Text = .{ .text = "0" };
830+ const one : Text = .{ .text = "1" };
831+ const two : Text = .{ .text = "2" };
832+
833+ var list_view : ListView = .{
834+ .children = .{ .slice = &.{ zero .widget (), one .widget (), two .widget () } },
835+ };
836+
837+ list_view .scrollToBottom ();
838+
839+ try std .testing .expectEqual (0 , list_view .cursor );
840+ try std .testing .expectEqual (2 , list_view .scroll .top );
841+ try std .testing .expectEqual (0 , list_view .scroll .offset );
842+ try std .testing .expectEqual (3 , list_view .item_count );
843+ }
844+
675845// @reykjalin found an issue on mac with ghostty where the scroll up and scroll down were uneven.
676846// Ghostty has high precision scrolling and sends a lot of wheel events for each tick
677847test "ListView: uneven scroll" {
0 commit comments