lib/gui/src/properties/text.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const std = @import("std");
2 const gui = @import("gui");
3 const hypothesis = @import("hypothesis");
4
5 const Allocator = std.mem.Allocator;
6 const Atlas = gui.paint.text.Atlas;
7 const BitmapGlyph = gui.paint.text.BitmapGlyph;
8
9 const codepoints = [_]u21{ 'a', 'b', 0x00e9, 0x754c, 0x1f642 };
10 const max_codepoints = 24;
11 const max_text_bytes = max_codepoints * 4;
12 const property_atlas_cache_limits = gui.paint.TextAtlasCacheLimits{
13 .measure_entries = 64,
14 .measure_payload_bytes = 16 * 1024,
15 .shape_entries = 64,
16 .shape_payload_bytes = 64 * 1024,
17 };
18
19 fn settings() hypothesis.Settings {
20 const base = hypothesis.Settings{
21 .max_examples = 400,
22 .max_choices = 4096,
23 .max_shrinks = 10_000,
24 .target_examples = 400,
25 };
26 return base
27 .withSeed(0x6775_6954_6578_7401)
28 .withDatabase("zig-out/hypothesis-failures/gui-text");
29 }
30
31 fn drawInt(data: *hypothesis.ConjectureData, min: usize, max: usize, shrink_towards: usize) !usize {
32 return @intCast(try data.drawInteger(min, max, shrink_towards));
33 }
34
35 fn bitmapAtlas(allocator: Allocator) !Atlas {
36 const glyphs = [_]BitmapGlyph{
37 .{ .codepoint = 'a', .rows = &.{0xff} },
38 .{ .codepoint = 'b', .rows = &.{0xff} },
39 .{ .codepoint = 0x00e9, .rows = &.{0xff} },
40 .{ .codepoint = 0x754c, .rows = &.{0xff} },
41 .{ .codepoint = 0x1f642, .rows = &.{0xff} },
42 };
43 return Atlas.initFromBitmapGlyphs(
44 allocator,
45 &glyphs,
46 .{ .width = 8, .height = 1, .stride = 1 },
47 16,
48 property_atlas_cache_limits,
49 );
50 }
51
52 const CaretRoundTripProperty = struct {
53 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
54 var atlas = try bitmapAtlas(allocator);
55 defer atlas.deinit();
56 const count = try drawInt(data, 0, max_codepoints, 8);
57 var text_buffer: [max_text_bytes]u8 = undefined;
58 var boundaries: [max_codepoints + 1]usize = undefined;
59 boundaries[0] = 0;
60 var text_len: usize = 0;
61 for (0..count) |index| {
62 const codepoint = codepoints[try drawInt(data, 0, codepoints.len - 1, 0)];
63 var encoded: [4]u8 = undefined;
64 const encoded_len = try std.unicode.utf8Encode(codepoint, &encoded);
65 @memcpy(text_buffer[text_len .. text_len + encoded_len], encoded[0..encoded_len]);
66 text_len += encoded_len;
67 boundaries[index + 1] = text_len;
68 }
69 const text = text_buffer[0..text_len];
70
71 for (boundaries[0 .. count + 1]) |boundary| {
72 const advance = try atlas.advanceForByteOffset(text, boundary);
73 const hit = try atlas.hitTestAdvance(text, advance);
74 try std.testing.expectEqual(boundary, hit.byte_offset);
75 try std.testing.expectApproxEqAbs(advance, hit.advance, 0.001);
76 }
77 }
78 };
79
80 const SelectionPartitionProperty = struct {
81 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
82 const glyphs = [_]BitmapGlyph{
83 .{ .codepoint = 'A', .rows = &.{0xff} },
84 .{ .codepoint = ' ', .rows = &.{0x00} },
85 };
86 var atlas = try Atlas.initFromBitmapGlyphs(
87 allocator,
88 &glyphs,
89 .{ .width = 8, .height = 1, .stride = 1 },
90 16,
91 property_atlas_cache_limits,
92 );
93 defer atlas.deinit();
94 const content = "AA AA AA AA";
95 const first = try drawInt(data, 0, content.len, 0);
96 const second = try drawInt(data, 0, content.len, content.len);
97 const wrap_cells = try drawInt(data, 2, 5, 3);
98 const wrap_width: f32 = @floatFromInt(wrap_cells * 8);
99 const child = [_]gui.model.UiNode{.{
100 .widget_id = 2,
101 .kind = .text_input,
102 .text = .{
103 .content = content,
104 .point_size = 16,
105 .line_height = 1,
106 .wrap_width = @floatCast(wrap_width),
107 },
108 .text_selection = .{
109 .selection_active = true,
110 .selection_anchor_byte_offset = first,
111 .selection_focus_byte_offset = second,
112 },
113 .paint = .{ .foreground = .{ .r = 12, .g = 24, .b = 36, .a = 255 } },
114 .size = .{ .width = wrap_width, .height = 96 },
115 }};
116 const surface = gui.model.UiSurfaceTree{
117 .available_size = .{ .width = wrap_width, .height = 96 },
118 .root = .{ .widget_id = 1, .children = &child },
119 };
120 var workspace = gui.frame.Workspace.init(allocator);
121 defer workspace.deinit();
122 const frame = try workspace.buildSurface(&surface, .{});
123 var commands = gui.paint.CommandBuffer.init(allocator);
124 defer commands.deinit();
125 const entries = [_]gui.paint.text.AtlasSet.Entry{.{
126 .face = 0,
127 .image_index = 0,
128 .atlas = &atlas,
129 }};
130 const atlases = gui.paint.text.AtlasSet{ .entries = &entries };
131 try gui.paint.text.appendFrameCommands(&commands, frame, &atlases, 1, 0);
132
133 var selected_width: f32 = 0;
134 for (commands.items()) |command| {
135 if (command.kind == .fill and command.color.a == 64) {
136 selected_width += command.rect.width;
137 }
138 }
139 const selected_bytes = @max(first, second) - @min(first, second);
140 const expected_width: f32 = @floatFromInt(selected_bytes * 8);
141 try std.testing.expectApproxEqAbs(expected_width, selected_width, 0.001);
142 }
143 };
144
145 const TextRunValidationProperty = struct {
146 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
147 _ = allocator;
148 const count = try drawInt(data, 1, max_codepoints, 8);
149 var text_buffer: [max_text_bytes]u8 = undefined;
150 var boundaries: [max_codepoints + 1]usize = undefined;
151 boundaries[0] = 0;
152 var text_len: usize = 0;
153 for (0..count) |index| {
154 const codepoint = codepoints[try drawInt(data, 0, codepoints.len - 1, 0)];
155 var encoded: [4]u8 = undefined;
156 const encoded_len = try std.unicode.utf8Encode(codepoint, &encoded);
157 @memcpy(text_buffer[text_len .. text_len + encoded_len], encoded[0..encoded_len]);
158 text_len += encoded_len;
159 boundaries[index + 1] = text_len;
160 }
161 const content = text_buffer[0..text_len];
162 const start_index = try drawInt(data, 0, count - 1, 0);
163 const end_index = try drawInt(data, start_index + 1, count, count);
164 const valid = gui.model.UiTextRun{
165 .byte_start = boundaries[start_index],
166 .byte_end = boundaries[end_index],
167 };
168 try gui.model.validateTextRuns(content, &.{valid});
169
170 var empty = valid;
171 empty.byte_end = empty.byte_start;
172 try std.testing.expectError(
173 error.InvalidTextRun,
174 gui.model.validateTextRuns(content, &.{empty}),
175 );
176 var past_end = valid;
177 past_end.byte_end = content.len + 1;
178 try std.testing.expectError(
179 error.InvalidTextRun,
180 gui.model.validateTextRuns(content, &.{past_end}),
181 );
182 try std.testing.expectError(
183 error.InvalidTextRun,
184 gui.model.validateTextRuns(content, &.{ valid, valid }),
185 );
186 if (count > 1) {
187 const reversed = [_]gui.model.UiTextRun{
188 .{ .byte_start = boundaries[1], .byte_end = boundaries[2] },
189 .{ .byte_start = boundaries[0], .byte_end = boundaries[1] },
190 };
191 try std.testing.expectError(
192 error.InvalidTextRun,
193 gui.model.validateTextRuns(content, &reversed),
194 );
195 }
196 for (0..count) |index| {
197 if (boundaries[index + 1] - boundaries[index] <= 1) continue;
198 const split = gui.model.UiTextRun{
199 .byte_start = boundaries[index],
200 .byte_end = boundaries[index] + 1,
201 };
202 try std.testing.expectError(
203 error.InvalidTextRun,
204 gui.model.validateTextRuns(content, &.{split}),
205 );
206 break;
207 }
208 }
209 };
210
211 const StyledGeometryProperty = struct {
212 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
213 var atlas = try bitmapAtlas(allocator);
214 defer atlas.deinit();
215 const count = try drawInt(data, 1, max_codepoints, 8);
216 var text_buffer: [max_text_bytes]u8 = undefined;
217 var boundaries: [max_codepoints + 1]usize = undefined;
218 boundaries[0] = 0;
219 var text_len: usize = 0;
220 for (0..count) |index| {
221 const codepoint = codepoints[try drawInt(data, 0, codepoints.len - 1, 0)];
222 var encoded: [4]u8 = undefined;
223 const encoded_len = try std.unicode.utf8Encode(codepoint, &encoded);
224 @memcpy(text_buffer[text_len .. text_len + encoded_len], encoded[0..encoded_len]);
225 text_len += encoded_len;
226 boundaries[index + 1] = text_len;
227 }
228 const content = text_buffer[0..text_len];
229 const start_index = try drawInt(data, 0, count - 1, 0);
230 const end_index = try drawInt(data, start_index + 1, count, count);
231 const wrap_cells = try drawInt(data, 1, count, @min(count, 4));
232 const wrap_width: f32 = @floatFromInt(wrap_cells * 8);
233 const runs = [_]gui.model.UiTextRun{.{
234 .byte_start = boundaries[start_index],
235 .byte_end = boundaries[end_index],
236 .foreground = .{ .r = 220, .g = 30, .b = 20, .a = 255 },
237 .background = .{ .r = 10, .g = 20, .b = 40, .a = 160 },
238 .underline = true,
239 .strikethrough = end_index - start_index > 1,
240 }};
241 const plain = gui.model.UiText{
242 .content = content,
243 .point_size = 16,
244 .line_height = 1,
245 .wrap_width = wrap_width,
246 };
247 const styled = gui.model.UiText{
248 .content = content,
249 .runs = &runs,
250 .point_size = 16,
251 .line_height = 1,
252 .wrap_width = wrap_width,
253 };
254 try gui.model.validateTextRuns(content, &runs);
255 try std.testing.expectEqual(
256 try gui.paint.text.measure(&atlas, plain),
257 try gui.paint.text.measure(&atlas, styled),
258 );
259 for ([_]usize{ boundaries[start_index], boundaries[end_index] }) |boundary| {
260 const advance = try atlas.advanceForByteOffset(content, boundary);
261 const hit = try atlas.hitTestAdvance(content, advance);
262 try std.testing.expectEqual(boundary, hit.byte_offset);
263 try std.testing.expectApproxEqAbs(advance, hit.advance, 0.001);
264 }
265
266 const selection = gui.model.UiTextSelection{
267 .cursor_visible = true,
268 .cursor_byte_offset = boundaries[end_index],
269 .selection_active = true,
270 .selection_anchor_byte_offset = boundaries[start_index],
271 .selection_focus_byte_offset = boundaries[end_index],
272 };
273 const nodes = [_]gui.model.UiNode{
274 .{
275 .widget_id = 2,
276 .kind = .text_input,
277 .text = plain,
278 .text_selection = selection,
279 .size = .{ .width = wrap_width, .height = max_codepoints * 16 },
280 },
281 .{
282 .widget_id = 2,
283 .kind = .text_input,
284 .text = styled,
285 .text_selection = selection,
286 .size = .{ .width = wrap_width, .height = max_codepoints * 16 },
287 },
288 };
289 var workspaces = [_]gui.frame.Workspace{
290 gui.frame.Workspace.init(allocator),
291 gui.frame.Workspace.init(allocator),
292 };
293 defer for (&workspaces) |*workspace| workspace.deinit();
294 var buffers = [_]gui.paint.CommandBuffer{
295 gui.paint.CommandBuffer.init(allocator),
296 gui.paint.CommandBuffer.init(allocator),
297 };
298 defer for (&buffers) |*buffer| buffer.deinit();
299 const entries = [_]gui.paint.text.AtlasSet.Entry{.{
300 .face = 0,
301 .image_index = 0,
302 .atlas = &atlas,
303 }};
304 const atlases = gui.paint.text.AtlasSet{ .entries = &entries };
305 const device_scale: f32 = @floatFromInt(try drawInt(data, 1, 2, 1));
306 var widget_geometry: [2]gui.model.WidgetFrame = undefined;
307 for (0..nodes.len) |index| {
308 const surface = gui.model.UiSurfaceTree{
309 .available_size = .{ .width = wrap_width, .height = max_codepoints * 16 },
310 .root = .{ .widget_id = 1, .children = nodes[index .. index + 1] },
311 };
312 const frame = try workspaces[index].buildSurface(&surface, .{});
313 widget_geometry[index] = frame.widgets[1];
314 try gui.paint.text.appendFrameCommands(&buffers[index], frame, &atlases, device_scale, 0);
315 }
316 try std.testing.expectEqual(widget_geometry[0].rect, widget_geometry[1].rect);
317 try std.testing.expectEqual(widget_geometry[0].visible_rect, widget_geometry[1].visible_rect);
318 try std.testing.expectEqual(widget_geometry[0].constraints, widget_geometry[1].constraints);
319 try std.testing.expectEqual(widget_geometry[0].content_size, widget_geometry[1].content_size);
320
321 var plain_index: usize = 0;
322 var styled_index: usize = 0;
323 while (true) {
324 while (plain_index < buffers[0].items().len and buffers[0].items()[plain_index].kind != .glyph) {
325 plain_index += 1;
326 }
327 while (styled_index < buffers[1].items().len and buffers[1].items()[styled_index].kind != .glyph) {
328 styled_index += 1;
329 }
330 const plain_done = plain_index == buffers[0].items().len;
331 const styled_done = styled_index == buffers[1].items().len;
332 try std.testing.expectEqual(plain_done, styled_done);
333 if (plain_done) break;
334 const plain_glyph = buffers[0].items()[plain_index];
335 const styled_glyph = buffers[1].items()[styled_index];
336 try std.testing.expectEqual(plain_glyph.rect, styled_glyph.rect);
337 try std.testing.expectEqual(plain_glyph.clip, styled_glyph.clip);
338 try std.testing.expectEqual(plain_glyph.source, styled_glyph.source);
339 plain_index += 1;
340 styled_index += 1;
341 }
342
343 var selection_rects: [2][max_codepoints]gui.layout.Rect = undefined;
344 var selection_counts: [2]usize = @splat(0);
345 for (&buffers, 0..) |*buffer, buffer_index| {
346 for (buffer.items()) |item| {
347 if (item.kind != .fill or item.color.a != 64) continue;
348 selection_rects[buffer_index][selection_counts[buffer_index]] = item.rect;
349 selection_counts[buffer_index] += 1;
350 }
351 }
352 try std.testing.expectEqual(selection_counts[0], selection_counts[1]);
353 try std.testing.expectEqualSlices(
354 gui.layout.Rect,
355 selection_rects[0][0..selection_counts[0]],
356 selection_rects[1][0..selection_counts[1]],
357 );
358 const plain_caret = buffers[0].items()[buffers[0].items().len - 1];
359 const styled_caret = buffers[1].items()[buffers[1].items().len - 1];
360 try std.testing.expectEqual(gui.paint.command.Kind.fill, plain_caret.kind);
361 try std.testing.expectEqual(gui.paint.command.Kind.fill, styled_caret.kind);
362 try std.testing.expectEqual(plain_caret.rect, styled_caret.rect);
363 }
364 };
365
366 const MixedMetricGeometryProperty = struct {
367 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
368 var base_atlas = try bitmapAtlas(allocator);
369 defer base_atlas.deinit();
370 var large_atlas = try bitmapAtlas(allocator);
371 defer large_atlas.deinit();
372 const count = try drawInt(data, 2, max_codepoints, 8);
373 var text_buffer: [max_text_bytes]u8 = undefined;
374 var boundaries: [max_codepoints + 1]usize = undefined;
375 boundaries[0] = 0;
376 var text_len: usize = 0;
377 for (0..count) |index| {
378 const codepoint = codepoints[try drawInt(data, 0, codepoints.len - 1, 0)];
379 var encoded: [4]u8 = undefined;
380 const encoded_len = try std.unicode.utf8Encode(codepoint, &encoded);
381 @memcpy(text_buffer[text_len .. text_len + encoded_len], encoded[0..encoded_len]);
382 text_len += encoded_len;
383 boundaries[index + 1] = text_len;
384 }
385 const style_start = try drawInt(data, 0, count - 1, 0);
386 const style_end = try drawInt(data, style_start + 1, count, count);
387 const styles = [_]gui.model.UiTextStyle{.{
388 .font_asset_id = 2,
389 .point_size = 24,
390 }};
391 const runs = [_]gui.model.UiTextRun{.{
392 .byte_start = boundaries[style_start],
393 .byte_end = boundaries[style_end],
394 .style_slot = 1,
395 .background = .{ .r = 8, .g = 16, .b = 32, .a = 160 },
396 .underline = true,
397 }};
398 const wrap_cells = try drawInt(data, 2, 8, 4);
399 const wrap_width: f32 = @floatFromInt(wrap_cells * 8);
400 const text = gui.model.UiText{
401 .content = text_buffer[0..text_len],
402 .runs = &runs,
403 .styles = &styles,
404 .font_asset_id = 1,
405 .point_size = 16,
406 .line_height = 1,
407 .wrap_width = wrap_width,
408 };
409 try gui.model.validateText(text);
410 const entries = [_]gui.paint.text.AtlasSet.Entry{
411 .{ .face = 1, .image_index = 3, .atlas = &base_atlas },
412 .{ .face = 2, .image_index = 9, .atlas = &large_atlas },
413 };
414 const atlases = gui.paint.text.AtlasSet{ .entries = &entries };
415 for (boundaries[0 .. count + 1]) |boundary| {
416 const advance = try gui.paint.text.textAdvanceForByteOffset(
417 &atlases,
418 text,
419 boundary,
420 );
421 const hit = try gui.paint.text.textHitTestAdvance(&atlases, text, advance);
422 try std.testing.expectEqual(boundary, hit.byte_offset);
423 try std.testing.expectApproxEqAbs(advance, hit.advance, 0.001);
424 }
425 const first = try drawInt(data, 0, count, 0);
426 const second = try drawInt(data, 0, count, count);
427 const child = [_]gui.model.UiNode{.{
428 .widget_id = 2,
429 .kind = .text_input,
430 .text = text,
431 .text_selection = .{
432 .selection_active = true,
433 .selection_anchor_byte_offset = boundaries[first],
434 .selection_focus_byte_offset = boundaries[second],
435 },
436 .size = .{ .width = wrap_width, .height = max_codepoints * 24 },
437 }};
438 const surface = gui.model.UiSurfaceTree{
439 .available_size = .{ .width = wrap_width, .height = max_codepoints * 24 },
440 .root = .{ .widget_id = 1, .children = &child },
441 };
442 var workspace = gui.frame.Workspace.init(allocator);
443 defer workspace.deinit();
444 const frame = try workspace.buildSurface(&surface, .{
445 .resolvers = gui.paint.text.frameResolvers(&atlases),
446 });
447 var commands = gui.paint.CommandBuffer.init(allocator);
448 defer commands.deinit();
449 const device_scale: f32 = @floatFromInt(try drawInt(data, 1, 2, 1));
450 try gui.paint.text.appendFrameCommands(
451 &commands,
452 frame,
453 &atlases,
454 device_scale,
455 0,
456 );
457 var selected_width: f32 = 0;
458 for (commands.items()) |item| {
459 if (item.kind == .fill and item.color.a == 64) selected_width += item.rect.width;
460 }
461 const selected_start = @min(first, second);
462 const selected_end = @max(first, second);
463 var expected_width: f32 = 0;
464 for (selected_start..selected_end) |index| {
465 expected_width += if (index >= style_start and index < style_end) 12 else 8;
466 }
467 try std.testing.expectApproxEqAbs(
468 expected_width * device_scale,
469 selected_width,
470 0.001,
471 );
472 }
473 };
474
475 const MultilineCaretRoundTripProperty = struct {
476 pub fn property(data: *hypothesis.ConjectureData, allocator: Allocator) !void {
477 var atlas = try bitmapAtlas(allocator);
478 defer atlas.deinit();
479 const count = try drawInt(data, 0, max_codepoints, 8);
480 var text_buffer: [max_text_bytes]u8 = undefined;
481 var boundaries: [max_codepoints + 1]usize = undefined;
482 boundaries[0] = 0;
483 var text_len: usize = 0;
484 for (0..count) |index| {
485 const token = try drawInt(data, 0, codepoints.len, 0);
486 if (token == codepoints.len) {
487 text_buffer[text_len] = '\n';
488 text_len += 1;
489 } else {
490 var encoded: [4]u8 = undefined;
491 const encoded_len = try std.unicode.utf8Encode(
492 codepoints[token],
493 &encoded,
494 );
495 @memcpy(
496 text_buffer[text_len .. text_len + encoded_len],
497 encoded[0..encoded_len],
498 );
499 text_len += encoded_len;
500 }
501 boundaries[index + 1] = text_len;
502 }
503 const wrap_cells = try drawInt(data, 1, 6, 3);
504 const wrap_width: f32 = @floatFromInt(wrap_cells * 8);
505 const horizontal: gui.model.UiTextAlign = @fromBackingInt(@intCast(
506 try drawInt(data, 0, 2, 0),
507 ));
508 const vertical: gui.model.UiTextAlign = @fromBackingInt(@intCast(
509 try drawInt(data, 0, 2, 0),
510 ));
511 const text = gui.model.UiText{
512 .content = text_buffer[0..text_len],
513 .point_size = 16,
514 .line_height = 1,
515 .wrap_width = wrap_width,
516 .horizontal_align = horizontal,
517 .vertical_align = vertical,
518 };
519 const box = gui.layout.Size{
520 .width = wrap_width + @as(
521 f32,
522 @floatFromInt(try drawInt(data, 0, 6, 2) * 8),
523 ),
524 .height = @floatFromInt((count + 4) * 16),
525 };
526 const entries = [_]gui.paint.text.AtlasSet.Entry{.{
527 .face = 0,
528 .image_index = 0,
529 .atlas = &atlas,
530 }};
531 const atlases = gui.paint.text.AtlasSet{ .entries = &entries };
532 const boundary = boundaries[try drawInt(data, 0, count, 0)];
533
534 const downstream = try gui.paint.text.textCaretGeometry(
535 &atlases,
536 text,
537 box,
538 boundary,
539 .downstream,
540 );
541 const downstream_hit = try gui.paint.text.textHitTestPoint(
542 &atlases,
543 text,
544 box,
545 .{
546 .x = downstream.x,
547 .y = downstream.y + downstream.height * 0.5,
548 },
549 );
550 try std.testing.expectEqual(boundary, downstream_hit.byte_offset);
551
552 const upstream = try gui.paint.text.textCaretGeometry(
553 &atlases,
554 text,
555 box,
556 boundary,
557 .upstream,
558 );
559 const upstream_hit = try gui.paint.text.textHitTestPoint(
560 &atlases,
561 text,
562 box,
563 .{
564 .x = upstream.x,
565 .y = upstream.y + upstream.height * 0.5,
566 },
567 );
568 try std.testing.expectEqual(boundary, upstream_hit.byte_offset);
569 if (upstream.line_index != downstream.line_index) {
570 try std.testing.expectEqual(
571 gui.paint.text.TextCaretAffinity.upstream,
572 upstream_hit.affinity,
573 );
574 }
575
576 const first = try gui.paint.text.textHitTestPoint(
577 &atlases,
578 text,
579 box,
580 .{ .x = -1000, .y = -1000 },
581 );
582 try std.testing.expectEqual(@as(usize, 0), first.byte_offset);
583 try std.testing.expectEqual(@as(usize, 0), first.line_index);
584 const last = try gui.paint.text.textHitTestPoint(
585 &atlases,
586 text,
587 box,
588 .{ .x = 10000, .y = 10000 },
589 );
590 try std.testing.expectEqual(text.content.len, last.byte_offset);
591 }
592 };
593
594 test "property: cached caret and hit queries round trip at grapheme boundaries" {
595 try hypothesis.checkNamed(
596 CaretRoundTripProperty,
597 "gui-text-caret-round-trip",
598 settings(),
599 );
600 }
601
602 test "property: wrapped visual lines partition selection width" {
603 var property_settings = settings();
604 property_settings.max_examples = 200;
605 property_settings.target_examples = 200;
606 try hypothesis.checkNamed(
607 SelectionPartitionProperty,
608 "gui-text-selection-partition",
609 property_settings,
610 );
611 }
612
613 test "property: text run validation rejects malformed generated ranges" {
614 var property_settings = settings();
615 property_settings.max_examples = 200;
616 property_settings.target_examples = 200;
617 try hypothesis.checkNamed(
618 TextRunValidationProperty,
619 "gui-text-run-validation",
620 property_settings,
621 );
622 }
623
624 test "property: text run styles preserve wrapped caret hit and selection geometry" {
625 var property_settings = settings();
626 property_settings.max_examples = 200;
627 property_settings.target_examples = 200;
628 try hypothesis.checkNamed(
629 StyledGeometryProperty,
630 "gui-text-run-geometry",
631 property_settings,
632 );
633 }
634
635 test "property: mixed metric runs preserve caret hits and wrapped selection partitions" {
636 var property_settings = settings();
637 property_settings.max_examples = 150;
638 property_settings.target_examples = 150;
639 try hypothesis.checkNamed(
640 MixedMetricGeometryProperty,
641 "gui-text-mixed-metric-geometry",
642 property_settings,
643 );
644 }
645
646 test "property: multiline caret geometry round trips hard and soft boundaries" {
647 var property_settings = settings();
648 property_settings.max_examples = 250;
649 property_settings.target_examples = 250;
650 try hypothesis.checkNamed(
651 MultilineCaretRoundTripProperty,
652 "gui-text-multiline-caret-round-trip",
653 property_settings,
654 );
655 }