Extract click/drag selection handling into SelectionGesture (#12830) master
Refactor terminal text selection into a reusable `SelectionGesture` state machine. Most importantly, this means our click+drag logic around selection is now fully unit tested! And we found bugs! And fixed them! The large line increase in this diff is mainly comments + tests. I've wanted to do this forever so we can unit test this, but I was kicked in the butt to do it recently because reimplementing selection logic in libghostty consumers turns out to be complex and error prone and we have a perfectly battle tested logic machine here so why not extract it? Behavioral changes from main surfaced via unit testing: - Dragging now drags by output across semantic output blocks when the initial press was an output selection. This matches the behavior of dragging continuing whatever the initial selection logic was. - Selection autoscroll now stops when the click anchor is invalidated by a screen change (e.g. primary to alt) - Deep press (macOS force touch) now selects the word at the original press location and consumes the active drag gesture, preventing later movement from dragging or autoscrolling that selection. This matches built-in macOS apps. - Mouse release records whether the gesture moved away from the pressed cell, so link and prompt clicks are skipped after a drag while normal clicks still activate them. Example usage: ```zig var gesture: terminal.SelectionGesture = .init; defer gesture.deinit(t); const press_selection = try gesture.press(t, .{ .time = try std.time.Instant.now(), .pin = press_pin, .xpos = mouse_x, .ypos = mouse_y, .max_distance = cell_width, .repeat_interval = mouse_interval, .word_boundary_codepoints = selection_word_chars, .behaviors = &.{ .cell, .word, .output }, }); try t.screens.active.select(press_selection); if (gesture.drag(t, drag_event)) |drag_selection| { try t.screens.active.select(drag_selection); } gesture.release(t, .{ .pin = release_pin }); ```