//! Gesture Recognition System //! //! Recognizes complex gestures from raw input events. //! Supports tap, double-tap, long-press, drag, and swipe gestures. const std = @import("std"); const Input = @import("input.zig"); const Layout = @import("layout.zig"); /// Gesture types pub const GestureType = enum { /// No gesture detected none, /// Single tap tap, /// Double tap double_tap, /// Long press (hold) long_press, /// Drag gesture (press and move) drag, /// Swipe gesture (quick movement in direction) swipe_left, swipe_right, swipe_up, swipe_down, /// Pinch (two-finger zoom) - for future touch support pinch, /// Rotate (two-finger rotation) - for future touch support rotate, }; /// Gesture phase pub const GesturePhase = enum { /// Gesture not started none, /// Gesture may be starting possible, /// Gesture recognized and in progress began, /// Gesture position/value changed changed, /// Gesture ended normally ended, /// Gesture was cancelled cancelled, }; /// Swipe direction pub const SwipeDirection = enum { left, right, up, down, }; /// Gesture configuration pub const Config = struct { /// Double tap maximum time between taps (ms) double_tap_time_ms: u32 = 300, /// Long press minimum hold time (ms) long_press_time_ms: u32 = 500, /// Minimum distance for swipe detection swipe_min_distance: f32 = 50.0, /// Minimum velocity for swipe (pixels/second) swipe_min_velocity: f32 = 200.0, /// Maximum distance for tap (to distinguish from drag) tap_max_distance: f32 = 10.0, /// Drag threshold distance drag_threshold: f32 = 5.0, }; /// Gesture result pub const Result = struct { /// Detected gesture type gesture_type: GestureType = .none, /// Current phase phase: GesturePhase = .none, /// Start position start_pos: struct { x: i32, y: i32 } = .{ .x = 0, .y = 0 }, /// Current position current_pos: struct { x: i32, y: i32 } = .{ .x = 0, .y = 0 }, /// Delta from start delta: struct { x: i32, y: i32 } = .{ .x = 0, .y = 0 }, /// Velocity (pixels/second) velocity: struct { x: f32, y: f32 } = .{ .x = 0, .y = 0 }, /// Duration in milliseconds duration_ms: u32 = 0, /// Tap count (for multi-tap) tap_count: u8 = 0, /// Check if gesture is active pub fn isActive(self: *const Result) bool { return self.phase == .began or self.phase == .changed; } /// Check if gesture ended pub fn ended(self: *const Result) bool { return self.phase == .ended; } /// Get swipe direction if swipe gesture pub fn swipeDirection(self: *const Result) ?SwipeDirection { return switch (self.gesture_type) { .swipe_left => .left, .swipe_right => .right, .swipe_up => .up, .swipe_down => .down, else => null, }; } }; /// Gesture recognizer state pub const Recognizer = struct { /// Configuration config: Config = .{}, /// Current gesture result result: Result = .{}, // Internal state is_pressed: bool = false, press_start_time: i64 = 0, press_start_pos: struct { x: i32, y: i32 } = .{ .x = 0, .y = 0 }, last_pos: struct { x: i32, y: i32 } = .{ .x = 0, .y = 0 }, last_tap_time: i64 = 0, last_tap_pos: struct { x: i32, y: i32 } = .{ .x = 0, .y = 0 }, tap_count: u8 = 0, is_dragging: bool = false, long_press_fired: bool = false, // Velocity tracking velocity_samples: [5]struct { x: i32, y: i32, time: i64 } = undefined, velocity_sample_count: u8 = 0, velocity_sample_index: u8 = 0, const Self = @This(); pub fn init(config: Config) Self { return .{ .config = config }; } /// Update recognizer with current input state and time pub fn update(self: *Self, input: *const Input.InputState, current_time_ms: i64) Result { const mouse = input.mousePos(); // Reset result self.result = .{}; // Handle mouse press if (input.mousePressed(.left)) { self.handlePress(mouse.x, mouse.y, current_time_ms); } // Handle mouse release if (input.mouseReleased(.left)) { self.handleRelease(mouse.x, mouse.y, current_time_ms); } // Handle movement while pressed if (self.is_pressed) { self.handleMove(mouse.x, mouse.y, current_time_ms); } return self.result; } fn handlePress(self: *Self, x: i32, y: i32, time: i64) void { self.is_pressed = true; self.press_start_time = time; self.press_start_pos = .{ .x = x, .y = y }; self.last_pos = .{ .x = x, .y = y }; self.is_dragging = false; self.long_press_fired = false; // Reset velocity tracking self.velocity_sample_count = 0; self.velocity_sample_index = 0; // Check for double tap potential const time_since_last_tap = time - self.last_tap_time; const dist_from_last_tap = distance( @floatFromInt(x), @floatFromInt(y), @floatFromInt(self.last_tap_pos.x), @floatFromInt(self.last_tap_pos.y), ); if (time_since_last_tap < self.config.double_tap_time_ms and dist_from_last_tap < self.config.tap_max_distance) { self.tap_count += 1; } else { self.tap_count = 1; } self.result.phase = .possible; self.result.start_pos = .{ .x = x, .y = y }; self.result.current_pos = .{ .x = x, .y = y }; } fn handleRelease(self: *Self, x: i32, y: i32, time: i64) void { if (!self.is_pressed) return; self.is_pressed = false; const duration = time - self.press_start_time; const dist = distance( @floatFromInt(x), @floatFromInt(y), @floatFromInt(self.press_start_pos.x), @floatFromInt(self.press_start_pos.y), ); // Calculate velocity const vel = self.calculateVelocity(); self.result.current_pos = .{ .x = x, .y = y }; self.result.delta = .{ .x = x - self.press_start_pos.x, .y = y - self.press_start_pos.y, }; self.result.duration_ms = @intCast(@max(0, duration)); self.result.velocity = vel; self.result.tap_count = self.tap_count; self.result.phase = .ended; // Determine gesture type if (self.is_dragging) { // Was dragging - check for swipe const total_vel = @sqrt(vel.x * vel.x + vel.y * vel.y); if (total_vel >= self.config.swipe_min_velocity and dist >= self.config.swipe_min_distance) { // Swipe detected if (@abs(vel.x) > @abs(vel.y)) { // Horizontal swipe self.result.gesture_type = if (vel.x < 0) .swipe_left else .swipe_right; } else { // Vertical swipe self.result.gesture_type = if (vel.y < 0) .swipe_up else .swipe_down; } } else { // Just a drag end self.result.gesture_type = .drag; } } else if (dist <= self.config.tap_max_distance) { // Tap if (self.tap_count >= 2) { self.result.gesture_type = .double_tap; } else { self.result.gesture_type = .tap; } self.last_tap_time = time; self.last_tap_pos = .{ .x = x, .y = y }; } } fn handleMove(self: *Self, x: i32, y: i32, time: i64) void { // Add velocity sample self.addVelocitySample(x, y, time); const dist = distance( @floatFromInt(x), @floatFromInt(y), @floatFromInt(self.press_start_pos.x), @floatFromInt(self.press_start_pos.y), ); const duration = time - self.press_start_time; // Check for drag start if (!self.is_dragging and dist >= self.config.drag_threshold) { self.is_dragging = true; self.result.gesture_type = .drag; self.result.phase = .began; } // Check for long press if (!self.long_press_fired and !self.is_dragging and duration >= self.config.long_press_time_ms and dist <= self.config.tap_max_distance) { self.long_press_fired = true; self.result.gesture_type = .long_press; self.result.phase = .ended; } // Update drag if (self.is_dragging) { self.result.gesture_type = .drag; self.result.phase = .changed; self.result.current_pos = .{ .x = x, .y = y }; self.result.delta = .{ .x = x - self.press_start_pos.x, .y = y - self.press_start_pos.y, }; self.result.velocity = self.calculateVelocity(); } self.result.start_pos = self.press_start_pos; self.result.duration_ms = @intCast(@max(0, duration)); self.last_pos = .{ .x = x, .y = y }; } fn addVelocitySample(self: *Self, x: i32, y: i32, time: i64) void { self.velocity_samples[self.velocity_sample_index] = .{ .x = x, .y = y, .time = time, }; self.velocity_sample_index = (self.velocity_sample_index + 1) % 5; if (self.velocity_sample_count < 5) { self.velocity_sample_count += 1; } } fn calculateVelocity(self: *const Self) struct { x: f32, y: f32 } { if (self.velocity_sample_count < 2) { return .{ .x = 0, .y = 0 }; } // Get oldest and newest samples const oldest_idx = if (self.velocity_sample_count < 5) 0 else self.velocity_sample_index; const newest_idx = if (self.velocity_sample_index == 0) self.velocity_sample_count - 1 else self.velocity_sample_index - 1; const oldest = self.velocity_samples[oldest_idx]; const newest = self.velocity_samples[newest_idx]; const dt_ms = newest.time - oldest.time; if (dt_ms <= 0) return .{ .x = 0, .y = 0 }; const dt_sec = @as(f32, @floatFromInt(dt_ms)) / 1000.0; const dx = @as(f32, @floatFromInt(newest.x - oldest.x)); const dy = @as(f32, @floatFromInt(newest.y - oldest.y)); return .{ .x = dx / dt_sec, .y = dy / dt_sec, }; } /// Check if specific gesture was just detected pub fn detected(self: *const Self, gesture: GestureType) bool { return self.result.gesture_type == gesture and (self.result.phase == .ended or self.result.phase == .began); } /// Check if drag gesture is active pub fn isDragging(self: *const Self) bool { return self.result.gesture_type == .drag and self.result.isActive(); } /// Get current drag delta pub fn dragDelta(self: *const Self) struct { x: i32, y: i32 } { if (self.isDragging()) { return self.result.delta; } return .{ .x = 0, .y = 0 }; } }; fn distance(x1: f32, y1: f32, x2: f32, y2: f32) f32 { const dx = x2 - x1; const dy = y2 - y1; return @sqrt(dx * dx + dy * dy); } // ============================================================================= // Multi-gesture recognizer for handling multiple simultaneous gestures // ============================================================================= /// Multi-gesture configuration pub const MultiGestureConfig = struct { /// Enable tap recognition enable_tap: bool = true, /// Enable double tap enable_double_tap: bool = true, /// Enable long press enable_long_press: bool = true, /// Enable drag enable_drag: bool = true, /// Enable swipe enable_swipe: bool = true, }; /// Callbacks for gesture events pub const GestureCallbacks = struct { on_tap: ?*const fn (x: i32, y: i32) void = null, on_double_tap: ?*const fn (x: i32, y: i32) void = null, on_long_press: ?*const fn (x: i32, y: i32) void = null, on_drag_start: ?*const fn (x: i32, y: i32) void = null, on_drag: ?*const fn (x: i32, y: i32, dx: i32, dy: i32) void = null, on_drag_end: ?*const fn (x: i32, y: i32) void = null, on_swipe: ?*const fn (direction: SwipeDirection) void = null, }; // ============================================================================= // Tests // ============================================================================= test "gesture recognizer init" { const recognizer = Recognizer.init(.{}); try std.testing.expect(!recognizer.is_pressed); try std.testing.expect(!recognizer.is_dragging); } test "gesture config defaults" { const config = Config{}; try std.testing.expect(config.double_tap_time_ms == 300); try std.testing.expect(config.long_press_time_ms == 500); } test "gesture result methods" { var result = Result{}; try std.testing.expect(!result.isActive()); try std.testing.expect(!result.ended()); result.phase = .began; try std.testing.expect(result.isActive()); result.phase = .ended; try std.testing.expect(result.ended()); } test "swipe direction detection" { var result = Result{ .gesture_type = .swipe_left }; try std.testing.expect(result.swipeDirection().? == .left); result.gesture_type = .swipe_right; try std.testing.expect(result.swipeDirection().? == .right); result.gesture_type = .tap; try std.testing.expect(result.swipeDirection() == null); } test "distance calculation" { try std.testing.expectApproxEqAbs(distance(0, 0, 3, 4), 5.0, 0.001); try std.testing.expectApproxEqAbs(distance(0, 0, 0, 0), 0.0, 0.001); }