@@ -3350,3 +3350,247 @@ test "hairline borders snap to whole device columns with smooth arcs" {
33503350 const corner = surface .pixelRgba8 (5 , 4 )[3 ];
33513351 try std .testing .expect (corner > 0 and corner < 255 );
33523352}
3353+
3354+ test "single-line fields clip and horizontally scroll an overflowing value" {
3355+ const long_text = "a value far too long for a narrow single-line field to show at once" ;
3356+ const field = Widget {
3357+ .id = 7 ,
3358+ .kind = .text_field ,
3359+ .frame = geometry .RectF .init (10 , 12 , 120 , 32 ),
3360+ .text = long_text ,
3361+ .text_selection = TextSelection .collapsed (long_text .len ),
3362+ .state = .{ .focused = true },
3363+ .semantics = .{ .label = "Name" },
3364+ // The retained offset channel: an oversized write clamps to the
3365+ // farthest the value can scroll, exactly like the textarea's
3366+ // vertical offset does.
3367+ .value = 100000 ,
3368+ };
3369+ const max_offset = support .textInputMaxHorizontalScrollOffsetForWidget (field , .{});
3370+ try std .testing .expect (max_offset > 0 );
3371+ try std .testing .expectEqual (max_offset , support .clampedTextInputHorizontalScrollOffsetForWidget (field , .{}, 100000 ));
3372+
3373+ var zero_offset_field = field ;
3374+ zero_offset_field .value = 0 ;
3375+
3376+ var commands : [8 ]CanvasCommand = undefined ;
3377+ var builder = Builder .init (& commands );
3378+ try emitWidgetTree (& builder , field , .{});
3379+ const display_list = builder .displayList ();
3380+ var zero_commands : [8 ]CanvasCommand = undefined ;
3381+ var zero_builder = Builder .init (& zero_commands );
3382+ try emitWidgetTree (& zero_builder , zero_offset_field , .{});
3383+ const zero_display_list = zero_builder .displayList ();
3384+
3385+ // Fill, border, offset focus ring, clip, text, caret, pop.
3386+ try std .testing .expectEqual (@as (usize , 7 ), display_list .commandCount ());
3387+ const viewport = textInputViewportForWidget (field , .{}).? ;
3388+ switch (display_list .commands [3 ]) {
3389+ .push_clip = > | clip | {
3390+ try std .testing .expectEqual (widgetPartId (7 , 16 ), clip .id );
3391+ try expectRectApprox (viewport , clip .rect );
3392+ },
3393+ else = > return error .TestUnexpectedResult ,
3394+ }
3395+ // The draw-text origin shifts left by exactly the clamped offset.
3396+ const scrolled_origin = switch (display_list .commands [4 ]) {
3397+ .draw_text = > | text | text .origin ,
3398+ else = > return error .TestUnexpectedResult ,
3399+ };
3400+ const resting_origin = switch (zero_display_list .commands [4 ]) {
3401+ .draw_text = > | text | text .origin ,
3402+ else = > return error .TestUnexpectedResult ,
3403+ };
3404+ try std .testing .expectApproxEqAbs (resting_origin .x - max_offset , scrolled_origin .x , 0.001 );
3405+ try std .testing .expectEqual (resting_origin .y , scrolled_origin .y );
3406+ // The end-of-value caret rides the same origin, landing inside the
3407+ // clip instead of past the field's border.
3408+ switch (display_list .commands [5 ]) {
3409+ .fill_rect = > | caret | {
3410+ try std .testing .expect (caret .rect .x >= viewport .x - 0.001 );
3411+ try std .testing .expect (caret .rect .maxX () <= viewport .maxX () + 0.001 );
3412+ },
3413+ else = > return error .TestUnexpectedResult ,
3414+ }
3415+ try std .testing .expectEqual (CanvasCommand .pop_clip , display_list .commands [6 ]);
3416+ }
3417+
3418+ test "the caret keep-visible offset scrolls to the caret and returns home" {
3419+ const long_text = "a value far too long for a narrow single-line field to show at once" ;
3420+ var field = Widget {
3421+ .id = 7 ,
3422+ .kind = .text_field ,
3423+ .frame = geometry .RectF .init (10 , 12 , 120 , 32 ),
3424+ .text = long_text ,
3425+ .text_selection = TextSelection .collapsed (long_text .len ),
3426+ .state = .{ .focused = true },
3427+ .semantics = .{ .label = "Name" },
3428+ };
3429+ const max_offset = support .textInputMaxHorizontalScrollOffsetForWidget (field , .{});
3430+ try std .testing .expect (max_offset > 0 );
3431+
3432+ // Caret at the end, unscrolled: the recompute scrolls forward far
3433+ // enough that the caret sits inside the visible span.
3434+ const end_offset = support .textInputCaretVisibleScrollOffsetForWidget (field , .{}, 0 );
3435+ try std .testing .expect (end_offset > 0 );
3436+ try std .testing .expect (end_offset <= max_offset + 0.001 );
3437+ field .value = end_offset ;
3438+ const viewport = textInputViewportForWidget (field , .{}).? ;
3439+ const end_geometry = textGeometryForWidget (field , .{});
3440+ const end_caret = end_geometry .caret_bounds .? ;
3441+ try std .testing .expect (end_caret .x >= viewport .x - 0.001 );
3442+ try std .testing .expect (end_caret .maxX () <= viewport .maxX () + 0.001 );
3443+
3444+ // Home: caret back at byte zero scrolls all the way back — the field
3445+ // never shows trailing emptiness while text could fill it.
3446+ field .text_selection = TextSelection .collapsed (0 );
3447+ try std .testing .expectEqual (@as (f32 , 0 ), support .textInputCaretVisibleScrollOffsetForWidget (field , .{}, end_offset ));
3448+
3449+ // A value that fits never scrolls and never adjusts.
3450+ var short = field ;
3451+ short .text = "short" ;
3452+ short .text_selection = TextSelection .collapsed (5 );
3453+ try std .testing .expectEqual (@as (f32 , 0 ), support .textInputMaxHorizontalScrollOffsetForWidget (short , .{}));
3454+ try std .testing .expectEqual (@as (f32 , 0 ), support .textInputCaretVisibleScrollOffsetForWidget (short , .{}, 25 ));
3455+ }
3456+
3457+ test "scrolled single-line selection rects shift with the text origin" {
3458+ const long_text = "a value far too long for a narrow single-line field to show at once" ;
3459+ const scrolled = Widget {
3460+ .id = 7 ,
3461+ .kind = .text_field ,
3462+ .frame = geometry .RectF .init (10 , 12 , 120 , 32 ),
3463+ .text = long_text ,
3464+ .text_selection = .{ .anchor = 2 , .focus = 9 },
3465+ .state = .{ .focused = true },
3466+ .semantics = .{ .label = "Name" },
3467+ .value = 20 ,
3468+ };
3469+ var resting = scrolled ;
3470+ resting .value = 0 ;
3471+
3472+ var commands : [10 ]CanvasCommand = undefined ;
3473+ var builder = Builder .init (& commands );
3474+ try emitWidgetTree (& builder , scrolled , .{});
3475+ const display_list = builder .displayList ();
3476+ var resting_commands : [10 ]CanvasCommand = undefined ;
3477+ var resting_builder = Builder .init (& resting_commands );
3478+ try emitWidgetTree (& resting_builder , resting , .{});
3479+ const resting_display_list = resting_builder .displayList ();
3480+
3481+ // Fill, border, focus ring, clip, selection rect, text, selected
3482+ // glyphs, pop — the same shape at both offsets.
3483+ try std .testing .expectEqual (display_list .commandCount (), resting_display_list .commandCount ());
3484+ const scrolled_selection = switch (display_list .commands [4 ]) {
3485+ .fill_rect = > | rect | rect .rect ,
3486+ else = > return error .TestUnexpectedResult ,
3487+ };
3488+ const resting_selection = switch (resting_display_list .commands [4 ]) {
3489+ .fill_rect = > | rect | rect .rect ,
3490+ else = > return error .TestUnexpectedResult ,
3491+ };
3492+ const scrolled_text = switch (display_list .commands [5 ]) {
3493+ .draw_text = > | text | text .origin ,
3494+ else = > return error .TestUnexpectedResult ,
3495+ };
3496+ const resting_text = switch (resting_display_list .commands [5 ]) {
3497+ .draw_text = > | text | text .origin ,
3498+ else = > return error .TestUnexpectedResult ,
3499+ };
3500+ // Selection geometry and the draw-text origin move together, by
3501+ // exactly the scroll offset.
3502+ try std .testing .expectApproxEqAbs (@as (f32 , 20 ), resting_selection .x - scrolled_selection .x , 0.001 );
3503+ try std .testing .expectApproxEqAbs (@as (f32 , 20 ), resting_text .x - scrolled_text .x , 0.001 );
3504+ try std .testing .expectApproxEqAbs (resting_selection .width , scrolled_selection .width , 0.001 );
3505+ }
3506+
3507+ test "short single-line values emit no clip and an unshifted origin" {
3508+ const field = Widget {
3509+ .id = 7 ,
3510+ .kind = .text_field ,
3511+ .frame = geometry .RectF .init (10 , 12 , 120 , 32 ),
3512+ .text = "short" ,
3513+ .text_selection = TextSelection .collapsed (5 ),
3514+ .state = .{ .focused = true },
3515+ .semantics = .{ .label = "Name" },
3516+ // A stale offset on a value that fits clamps to zero: fitting
3517+ // fields render exactly as they did before fields scrolled.
3518+ .value = 40 ,
3519+ };
3520+ var commands : [8 ]CanvasCommand = undefined ;
3521+ var builder = Builder .init (& commands );
3522+ try emitWidgetTree (& builder , field , .{});
3523+ const display_list = builder .displayList ();
3524+ // Fill, border, offset focus ring, text, caret — no clip pair.
3525+ try std .testing .expectEqual (@as (usize , 5 ), display_list .commandCount ());
3526+ for (display_list .commands ) | command | {
3527+ try std .testing .expect (command != .push_clip and command != .pop_clip );
3528+ }
3529+ var zero = field ;
3530+ zero .value = 0 ;
3531+ var zero_commands : [8 ]CanvasCommand = undefined ;
3532+ var zero_builder = Builder .init (& zero_commands );
3533+ try emitWidgetTree (& zero_builder , zero , .{});
3534+ const zero_display_list = zero_builder .displayList ();
3535+ switch (display_list .commands [3 ]) {
3536+ .draw_text = > | text | {
3537+ try std .testing .expectEqual (switch (zero_display_list .commands [3 ]) {
3538+ .draw_text = > | zero_text | zero_text .origin .x ,
3539+ else = > return error .TestUnexpectedResult ,
3540+ }, text .origin .x );
3541+ },
3542+ else = > return error .TestUnexpectedResult ,
3543+ }
3544+ }
3545+
3546+ test "search fields clip an overflowing value and keep chrome outside the clip" {
3547+ const long_text = "an overflowing search query that runs past the narrow field" ;
3548+ const field = Widget {
3549+ .id = 9 ,
3550+ .kind = .search_field ,
3551+ .frame = geometry .RectF .init (10 , 12 , 140 , 32 ),
3552+ .text = long_text ,
3553+ .text_selection = TextSelection .collapsed (long_text .len ),
3554+ .state = .{ .focused = true },
3555+ .semantics = .{ .label = "Search" },
3556+ .value = 100000 ,
3557+ };
3558+ try std .testing .expect (support .textInputMaxHorizontalScrollOffsetForWidget (field , .{}) > 0 );
3559+
3560+ var commands : [24 ]CanvasCommand = undefined ;
3561+ var builder = Builder .init (& commands );
3562+ try emitWidgetTree (& builder , field , .{});
3563+ const display_list = builder .displayList ();
3564+
3565+ var clip_index : ? usize = null ;
3566+ var pop_index : ? usize = null ;
3567+ var text_index : ? usize = null ;
3568+ var caret_index : ? usize = null ;
3569+ var clear_transform_index : ? usize = null ;
3570+ for (display_list .commands , 0.. ) | command , index | {
3571+ switch (command ) {
3572+ .push_clip = > | clip | {
3573+ try std .testing .expectEqual (widgetPartId (9 , 7 ), clip .id );
3574+ try expectRectApprox (textInputViewportForWidget (field , .{}).? , clip .rect );
3575+ clip_index = index ;
3576+ },
3577+ .pop_clip = > pop_index = index ,
3578+ .draw_text = > | text | {
3579+ if (text .id == widgetPartId (9 , 9 )) text_index = index ;
3580+ },
3581+ .fill_rect = > | caret | {
3582+ if (caret .id == widgetPartId (9 , 11 )) caret_index = index ;
3583+ },
3584+ .transform = > {
3585+ if (clear_transform_index == null and pop_index != null ) clear_transform_index = index ;
3586+ },
3587+ else = > {},
3588+ }
3589+ }
3590+ // Text and caret sit inside the clip pair; the trailing clear
3591+ // affordance draws after the pop, outside it.
3592+ try std .testing .expect (clip_index .? < text_index .? );
3593+ try std .testing .expect (text_index .? < caret_index .? );
3594+ try std .testing .expect (caret_index .? < pop_index .? );
3595+ try std .testing .expect (pop_index .? < clear_transform_index .? );
3596+ }
0 commit comments