mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-29 18:41:40 +00:00
vt: add mouse encoding C API
Expose the internal mouse encoding functionality through the C API, following the same pattern as the existing key encoding API. This allows external consumers of libvt to encode mouse events into terminal escape sequences (X10, UTF-8, SGR, URxvt, SGR-Pixels). The API is split into two opaque handle types: GhosttyMouseEvent for building normalized mouse events (action, button, modifiers, position) and GhosttyMouseEncoder for converting those events into escape sequences. The encoder is configured via a setopt interface supporting tracking mode, output format, renderer geometry, button state, and optional motion deduplication by last cell. Encoder state can also be bulk-configured from a terminal handle via ghostty_mouse_encoder_setopt_from_terminal. Failed encodes due to insufficient buffer space report the required size without mutating deduplication state.
This commit is contained in:
@@ -4,7 +4,7 @@ const std = @import("std");
|
||||
/// from ButtonState because button state is simply the current state
|
||||
/// of a mouse button but an action is something that triggers via
|
||||
/// an GUI event and supports more.
|
||||
pub const Action = enum { press, release, motion };
|
||||
pub const Action = enum(c_int) { press, release, motion };
|
||||
|
||||
/// The state of a mouse button.
|
||||
///
|
||||
|
||||
@@ -126,6 +126,23 @@ comptime {
|
||||
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
|
||||
@export(&c.key_encoder_setopt_from_terminal, .{ .name = "ghostty_key_encoder_setopt_from_terminal" });
|
||||
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
|
||||
@export(&c.mouse_event_new, .{ .name = "ghostty_mouse_event_new" });
|
||||
@export(&c.mouse_event_free, .{ .name = "ghostty_mouse_event_free" });
|
||||
@export(&c.mouse_event_set_action, .{ .name = "ghostty_mouse_event_set_action" });
|
||||
@export(&c.mouse_event_get_action, .{ .name = "ghostty_mouse_event_get_action" });
|
||||
@export(&c.mouse_event_set_button, .{ .name = "ghostty_mouse_event_set_button" });
|
||||
@export(&c.mouse_event_clear_button, .{ .name = "ghostty_mouse_event_clear_button" });
|
||||
@export(&c.mouse_event_get_button, .{ .name = "ghostty_mouse_event_get_button" });
|
||||
@export(&c.mouse_event_set_mods, .{ .name = "ghostty_mouse_event_set_mods" });
|
||||
@export(&c.mouse_event_get_mods, .{ .name = "ghostty_mouse_event_get_mods" });
|
||||
@export(&c.mouse_event_set_position, .{ .name = "ghostty_mouse_event_set_position" });
|
||||
@export(&c.mouse_event_get_position, .{ .name = "ghostty_mouse_event_get_position" });
|
||||
@export(&c.mouse_encoder_new, .{ .name = "ghostty_mouse_encoder_new" });
|
||||
@export(&c.mouse_encoder_free, .{ .name = "ghostty_mouse_encoder_free" });
|
||||
@export(&c.mouse_encoder_setopt, .{ .name = "ghostty_mouse_encoder_setopt" });
|
||||
@export(&c.mouse_encoder_setopt_from_terminal, .{ .name = "ghostty_mouse_encoder_setopt_from_terminal" });
|
||||
@export(&c.mouse_encoder_reset, .{ .name = "ghostty_mouse_encoder_reset" });
|
||||
@export(&c.mouse_encoder_encode, .{ .name = "ghostty_mouse_encoder_encode" });
|
||||
@export(&c.osc_new, .{ .name = "ghostty_osc_new" });
|
||||
@export(&c.osc_free, .{ .name = "ghostty_osc_free" });
|
||||
@export(&c.osc_next, .{ .name = "ghostty_osc_next" });
|
||||
|
||||
@@ -3,6 +3,8 @@ pub const formatter = @import("formatter.zig");
|
||||
pub const osc = @import("osc.zig");
|
||||
pub const key_event = @import("key_event.zig");
|
||||
pub const key_encode = @import("key_encode.zig");
|
||||
pub const mouse_event = @import("mouse_event.zig");
|
||||
pub const mouse_encode = @import("mouse_encode.zig");
|
||||
pub const paste = @import("paste.zig");
|
||||
pub const sgr = @import("sgr.zig");
|
||||
pub const terminal = @import("terminal.zig");
|
||||
@@ -58,6 +60,25 @@ pub const key_encoder_setopt = key_encode.setopt;
|
||||
pub const key_encoder_setopt_from_terminal = key_encode.setopt_from_terminal;
|
||||
pub const key_encoder_encode = key_encode.encode;
|
||||
|
||||
pub const mouse_event_new = mouse_event.new;
|
||||
pub const mouse_event_free = mouse_event.free;
|
||||
pub const mouse_event_set_action = mouse_event.set_action;
|
||||
pub const mouse_event_get_action = mouse_event.get_action;
|
||||
pub const mouse_event_set_button = mouse_event.set_button;
|
||||
pub const mouse_event_clear_button = mouse_event.clear_button;
|
||||
pub const mouse_event_get_button = mouse_event.get_button;
|
||||
pub const mouse_event_set_mods = mouse_event.set_mods;
|
||||
pub const mouse_event_get_mods = mouse_event.get_mods;
|
||||
pub const mouse_event_set_position = mouse_event.set_position;
|
||||
pub const mouse_event_get_position = mouse_event.get_position;
|
||||
|
||||
pub const mouse_encoder_new = mouse_encode.new;
|
||||
pub const mouse_encoder_free = mouse_encode.free;
|
||||
pub const mouse_encoder_setopt = mouse_encode.setopt;
|
||||
pub const mouse_encoder_setopt_from_terminal = mouse_encode.setopt_from_terminal;
|
||||
pub const mouse_encoder_reset = mouse_encode.reset;
|
||||
pub const mouse_encoder_encode = mouse_encode.encode;
|
||||
|
||||
pub const paste_is_safe = paste.is_safe;
|
||||
|
||||
pub const terminal_new = terminal.new;
|
||||
@@ -73,6 +94,8 @@ test {
|
||||
_ = osc;
|
||||
_ = key_event;
|
||||
_ = key_encode;
|
||||
_ = mouse_event;
|
||||
_ = mouse_encode;
|
||||
_ = paste;
|
||||
_ = sgr;
|
||||
_ = terminal;
|
||||
|
||||
529
src/terminal/c/mouse_encode.zig
Normal file
529
src/terminal/c/mouse_encode.zig
Normal file
@@ -0,0 +1,529 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const input_mouse_encode = @import("../../input/mouse_encode.zig");
|
||||
const renderer_size = @import("../../renderer/size.zig");
|
||||
const point = @import("../point.zig");
|
||||
const terminal_mouse = @import("../mouse.zig");
|
||||
const mouse_event = @import("mouse_event.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
const Event = mouse_event.Event;
|
||||
const Terminal = @import("terminal.zig").Terminal;
|
||||
|
||||
const log = std.log.scoped(.mouse_encode);
|
||||
|
||||
/// Wrapper around mouse encoding options that tracks the allocator for C API usage.
|
||||
const MouseEncoderWrapper = struct {
|
||||
opts: input_mouse_encode.Options,
|
||||
track_last_cell: bool = false,
|
||||
last_cell: ?point.Coordinate = null,
|
||||
alloc: Allocator,
|
||||
};
|
||||
|
||||
/// C: GhosttyMouseEncoder
|
||||
pub const Encoder = ?*MouseEncoderWrapper;
|
||||
|
||||
/// C: GhosttyMouseTrackingMode
|
||||
pub const TrackingMode = terminal_mouse.Event;
|
||||
|
||||
/// C: GhosttyMouseFormat
|
||||
pub const Format = terminal_mouse.Format;
|
||||
|
||||
/// C: GhosttyMouseEncoderSize
|
||||
pub const Size = extern struct {
|
||||
size: usize = @sizeOf(Size),
|
||||
screen_width: u32,
|
||||
screen_height: u32,
|
||||
cell_width: u32,
|
||||
cell_height: u32,
|
||||
padding_top: u32,
|
||||
padding_bottom: u32,
|
||||
padding_right: u32,
|
||||
padding_left: u32,
|
||||
|
||||
fn toRenderer(self: Size) ?renderer_size.Size {
|
||||
if (self.cell_width == 0 or self.cell_height == 0) return null;
|
||||
return .{
|
||||
.screen = .{
|
||||
.width = self.screen_width,
|
||||
.height = self.screen_height,
|
||||
},
|
||||
.cell = .{
|
||||
.width = self.cell_width,
|
||||
.height = self.cell_height,
|
||||
},
|
||||
.padding = .{
|
||||
.top = self.padding_top,
|
||||
.bottom = self.padding_bottom,
|
||||
.right = self.padding_right,
|
||||
.left = self.padding_left,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// C: GhosttyMouseEncoderOption
|
||||
pub const Option = enum(c_int) {
|
||||
event = 0,
|
||||
format = 1,
|
||||
size = 2,
|
||||
any_button_pressed = 3,
|
||||
track_last_cell = 4,
|
||||
|
||||
/// Input type expected for setting the option.
|
||||
pub fn InType(comptime self: Option) type {
|
||||
return switch (self) {
|
||||
.event => TrackingMode,
|
||||
.format => Format,
|
||||
.size => Size,
|
||||
.any_button_pressed,
|
||||
.track_last_cell,
|
||||
=> bool,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn new(
|
||||
alloc_: ?*const CAllocator,
|
||||
result: *Encoder,
|
||||
) callconv(.c) Result {
|
||||
const alloc = lib_alloc.default(alloc_);
|
||||
const ptr = alloc.create(MouseEncoderWrapper) catch
|
||||
return .out_of_memory;
|
||||
ptr.* = .{
|
||||
.opts = .{ .size = defaultSize() },
|
||||
.alloc = alloc,
|
||||
};
|
||||
result.* = ptr;
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn free(encoder_: Encoder) callconv(.c) void {
|
||||
const wrapper = encoder_ orelse return;
|
||||
const alloc = wrapper.alloc;
|
||||
alloc.destroy(wrapper);
|
||||
}
|
||||
|
||||
pub fn setopt(
|
||||
encoder_: Encoder,
|
||||
option: Option,
|
||||
value: ?*const anyopaque,
|
||||
) callconv(.c) void {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
|
||||
log.warn("setopt invalid option value={d}", .{@intFromEnum(option)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
return switch (option) {
|
||||
inline else => |comptime_option| setoptTyped(
|
||||
encoder_,
|
||||
comptime_option,
|
||||
@ptrCast(@alignCast(value orelse return)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
fn setoptTyped(
|
||||
encoder_: Encoder,
|
||||
comptime option: Option,
|
||||
value: *const option.InType(),
|
||||
) void {
|
||||
const wrapper = encoder_.?;
|
||||
switch (option) {
|
||||
.event => {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(TrackingMode, @intFromEnum(value.*)) catch {
|
||||
log.warn("setopt invalid TrackingMode value={d}", .{@intFromEnum(value.*)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
if (wrapper.opts.event != value.*) wrapper.last_cell = null;
|
||||
wrapper.opts.event = value.*;
|
||||
},
|
||||
|
||||
.format => {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Format, @intFromEnum(value.*)) catch {
|
||||
log.warn("setopt invalid Format value={d}", .{@intFromEnum(value.*)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
if (wrapper.opts.format != value.*) wrapper.last_cell = null;
|
||||
wrapper.opts.format = value.*;
|
||||
},
|
||||
|
||||
.size => {
|
||||
if (value.size < @sizeOf(Size)) {
|
||||
log.warn("setopt size struct too small size={d} expected>={d}", .{
|
||||
value.size,
|
||||
@sizeOf(Size),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
wrapper.opts.size = value.toRenderer() orelse {
|
||||
log.warn("setopt invalid size values (cell width and height must be non-zero)", .{});
|
||||
return;
|
||||
};
|
||||
wrapper.last_cell = null;
|
||||
},
|
||||
|
||||
.any_button_pressed => wrapper.opts.any_button_pressed = value.*,
|
||||
|
||||
.track_last_cell => {
|
||||
wrapper.track_last_cell = value.*;
|
||||
if (!value.*) wrapper.last_cell = null;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setopt_from_terminal(
|
||||
encoder_: Encoder,
|
||||
terminal_: Terminal,
|
||||
) callconv(.c) void {
|
||||
const wrapper = encoder_ orelse return;
|
||||
const t = terminal_ orelse return;
|
||||
wrapper.opts.event = t.flags.mouse_event;
|
||||
wrapper.opts.format = t.flags.mouse_format;
|
||||
wrapper.last_cell = null;
|
||||
}
|
||||
|
||||
pub fn reset(encoder_: Encoder) callconv(.c) void {
|
||||
const wrapper = encoder_ orelse return;
|
||||
wrapper.last_cell = null;
|
||||
}
|
||||
|
||||
pub fn encode(
|
||||
encoder_: Encoder,
|
||||
event_: Event,
|
||||
out_: ?[*]u8,
|
||||
out_len: usize,
|
||||
out_written: *usize,
|
||||
) callconv(.c) Result {
|
||||
const wrapper = encoder_ orelse return .invalid_value;
|
||||
const event = event_ orelse return .invalid_value;
|
||||
|
||||
const prev_last_cell = wrapper.last_cell;
|
||||
|
||||
var opts = wrapper.opts;
|
||||
opts.last_cell = if (wrapper.track_last_cell) &wrapper.last_cell else null;
|
||||
|
||||
var writer: std.Io.Writer = .fixed(if (out_) |out| out[0..out_len] else &.{});
|
||||
input_mouse_encode.encode(
|
||||
&writer,
|
||||
event.event,
|
||||
opts,
|
||||
) catch |err| switch (err) {
|
||||
error.WriteFailed => {
|
||||
// Failed writes should not mutate motion dedupe state because no
|
||||
// complete sequence was produced.
|
||||
wrapper.last_cell = prev_last_cell;
|
||||
|
||||
// Use a discarding writer to count how much space we would have needed.
|
||||
var count_last_cell = prev_last_cell;
|
||||
var count_opts = wrapper.opts;
|
||||
count_opts.last_cell = if (wrapper.track_last_cell) &count_last_cell else null;
|
||||
|
||||
var discarding: std.Io.Writer.Discarding = .init(&.{});
|
||||
input_mouse_encode.encode(
|
||||
&discarding.writer,
|
||||
event.event,
|
||||
count_opts,
|
||||
) catch unreachable;
|
||||
|
||||
// Discarding always uses a u64. If we're on 32-bit systems
|
||||
// we cast down. We should make this safer in the future.
|
||||
out_written.* = @intCast(discarding.count);
|
||||
return .out_of_memory;
|
||||
},
|
||||
};
|
||||
|
||||
out_written.* = writer.end;
|
||||
return .success;
|
||||
}
|
||||
|
||||
fn defaultSize() renderer_size.Size {
|
||||
return .{
|
||||
.screen = .{ .width = 1, .height = 1 },
|
||||
.cell = .{ .width = 1, .height = 1 },
|
||||
.padding = .{},
|
||||
};
|
||||
}
|
||||
|
||||
fn testSize() Size {
|
||||
return .{
|
||||
.size = @sizeOf(Size),
|
||||
.screen_width = 1_000,
|
||||
.screen_height = 1_000,
|
||||
.cell_width = 1,
|
||||
.cell_height = 1,
|
||||
.padding_top = 0,
|
||||
.padding_bottom = 0,
|
||||
.padding_right = 0,
|
||||
.padding_left = 0,
|
||||
};
|
||||
}
|
||||
|
||||
test "alloc" {
|
||||
var e: Encoder = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&e,
|
||||
));
|
||||
free(e);
|
||||
}
|
||||
|
||||
test "setopt" {
|
||||
var e: Encoder = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&e,
|
||||
));
|
||||
defer free(e);
|
||||
|
||||
const event_mode: TrackingMode = .any;
|
||||
setopt(e, .event, &event_mode);
|
||||
try testing.expectEqual(TrackingMode.any, e.?.opts.event);
|
||||
|
||||
const format_mode: Format = .sgr;
|
||||
setopt(e, .format, &format_mode);
|
||||
try testing.expectEqual(Format.sgr, e.?.opts.format);
|
||||
|
||||
const size = testSize();
|
||||
setopt(e, .size, &size);
|
||||
try testing.expectEqual(size.screen_width, e.?.opts.size.screen.width);
|
||||
try testing.expectEqual(size.screen_height, e.?.opts.size.screen.height);
|
||||
|
||||
const any_button_pressed = true;
|
||||
setopt(e, .any_button_pressed, &any_button_pressed);
|
||||
try testing.expect(e.?.opts.any_button_pressed);
|
||||
|
||||
const track_last_cell = true;
|
||||
setopt(e, .track_last_cell, &track_last_cell);
|
||||
try testing.expect(e.?.track_last_cell);
|
||||
}
|
||||
|
||||
test "setopt_from_terminal" {
|
||||
const terminal_c = @import("terminal.zig");
|
||||
|
||||
var e: Encoder = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&e,
|
||||
));
|
||||
defer free(e);
|
||||
|
||||
var t: Terminal = undefined;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
|
||||
));
|
||||
defer terminal_c.free(t);
|
||||
|
||||
const event_mode: TrackingMode = .any;
|
||||
setopt(e, .event, &event_mode);
|
||||
const format_mode: Format = .sgr;
|
||||
setopt(e, .format, &format_mode);
|
||||
|
||||
setopt_from_terminal(e, t);
|
||||
try testing.expectEqual(TrackingMode.none, e.?.opts.event);
|
||||
try testing.expectEqual(Format.x10, e.?.opts.format);
|
||||
}
|
||||
|
||||
test "setopt_from_terminal null" {
|
||||
setopt_from_terminal(null, null);
|
||||
|
||||
const terminal_c = @import("terminal.zig");
|
||||
var t: Terminal = undefined;
|
||||
try testing.expectEqual(Result.success, terminal_c.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&t,
|
||||
.{ .cols = 80, .rows = 24, .max_scrollback = 0 },
|
||||
));
|
||||
defer terminal_c.free(t);
|
||||
setopt_from_terminal(null, t);
|
||||
|
||||
var e: Encoder = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&e,
|
||||
));
|
||||
defer free(e);
|
||||
setopt_from_terminal(e, null);
|
||||
}
|
||||
|
||||
test "encode: sgr press left" {
|
||||
var encoder: Encoder = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&encoder,
|
||||
));
|
||||
defer free(encoder);
|
||||
|
||||
const event_mode: TrackingMode = .any;
|
||||
setopt(encoder, .event, &event_mode);
|
||||
const format_mode: Format = .sgr;
|
||||
setopt(encoder, .format, &format_mode);
|
||||
const size = testSize();
|
||||
setopt(encoder, .size, &size);
|
||||
|
||||
var event: Event = undefined;
|
||||
try testing.expectEqual(Result.success, mouse_event.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&event,
|
||||
));
|
||||
defer mouse_event.free(event);
|
||||
|
||||
mouse_event.set_action(event, .press);
|
||||
mouse_event.set_button(event, .left);
|
||||
mouse_event.set_position(event, .{ .x = 0, .y = 0 });
|
||||
|
||||
var required: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_memory, encode(
|
||||
encoder,
|
||||
event,
|
||||
null,
|
||||
0,
|
||||
&required,
|
||||
));
|
||||
try testing.expectEqual(@as(usize, 9), required);
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, encode(
|
||||
encoder,
|
||||
event,
|
||||
&buf,
|
||||
buf.len,
|
||||
&written,
|
||||
));
|
||||
try testing.expectEqual(required, written);
|
||||
try testing.expectEqualStrings("\x1b[<0;1;1M", buf[0..written]);
|
||||
}
|
||||
|
||||
test "encode: motion dedupe and reset" {
|
||||
var encoder: Encoder = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&encoder,
|
||||
));
|
||||
defer free(encoder);
|
||||
|
||||
const event_mode: TrackingMode = .any;
|
||||
setopt(encoder, .event, &event_mode);
|
||||
const format_mode: Format = .sgr;
|
||||
setopt(encoder, .format, &format_mode);
|
||||
const size = testSize();
|
||||
setopt(encoder, .size, &size);
|
||||
const track_last_cell = true;
|
||||
setopt(encoder, .track_last_cell, &track_last_cell);
|
||||
|
||||
var event: Event = undefined;
|
||||
try testing.expectEqual(Result.success, mouse_event.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&event,
|
||||
));
|
||||
defer mouse_event.free(event);
|
||||
|
||||
mouse_event.set_action(event, .motion);
|
||||
mouse_event.set_button(event, .left);
|
||||
mouse_event.set_position(event, .{ .x = 5, .y = 6 });
|
||||
|
||||
{
|
||||
var buf: [32]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, encode(
|
||||
encoder,
|
||||
event,
|
||||
&buf,
|
||||
buf.len,
|
||||
&written,
|
||||
));
|
||||
try testing.expect(written > 0);
|
||||
}
|
||||
|
||||
{
|
||||
var buf: [32]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, encode(
|
||||
encoder,
|
||||
event,
|
||||
&buf,
|
||||
buf.len,
|
||||
&written,
|
||||
));
|
||||
try testing.expectEqual(@as(usize, 0), written);
|
||||
}
|
||||
|
||||
reset(encoder);
|
||||
|
||||
{
|
||||
var buf: [32]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, encode(
|
||||
encoder,
|
||||
event,
|
||||
&buf,
|
||||
buf.len,
|
||||
&written,
|
||||
));
|
||||
try testing.expect(written > 0);
|
||||
}
|
||||
}
|
||||
|
||||
test "encode: querying required size doesn't update dedupe state" {
|
||||
var encoder: Encoder = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&encoder,
|
||||
));
|
||||
defer free(encoder);
|
||||
|
||||
const event_mode: TrackingMode = .any;
|
||||
setopt(encoder, .event, &event_mode);
|
||||
const format_mode: Format = .sgr;
|
||||
setopt(encoder, .format, &format_mode);
|
||||
const size = testSize();
|
||||
setopt(encoder, .size, &size);
|
||||
const track_last_cell = true;
|
||||
setopt(encoder, .track_last_cell, &track_last_cell);
|
||||
|
||||
var event: Event = undefined;
|
||||
try testing.expectEqual(Result.success, mouse_event.new(
|
||||
&lib_alloc.test_allocator,
|
||||
&event,
|
||||
));
|
||||
defer mouse_event.free(event);
|
||||
|
||||
mouse_event.set_action(event, .motion);
|
||||
mouse_event.set_button(event, .left);
|
||||
mouse_event.set_position(event, .{ .x = 5, .y = 6 });
|
||||
|
||||
var required: usize = 0;
|
||||
try testing.expectEqual(Result.out_of_memory, encode(
|
||||
encoder,
|
||||
event,
|
||||
null,
|
||||
0,
|
||||
&required,
|
||||
));
|
||||
try testing.expect(required > 0);
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
var written: usize = 0;
|
||||
try testing.expectEqual(Result.success, encode(
|
||||
encoder,
|
||||
event,
|
||||
&buf,
|
||||
buf.len,
|
||||
&written,
|
||||
));
|
||||
try testing.expect(written > 0);
|
||||
}
|
||||
155
src/terminal/c/mouse_event.zig
Normal file
155
src/terminal/c/mouse_event.zig
Normal file
@@ -0,0 +1,155 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const testing = std.testing;
|
||||
const lib_alloc = @import("../../lib/allocator.zig");
|
||||
const CAllocator = lib_alloc.Allocator;
|
||||
const key = @import("../../input/key.zig");
|
||||
const mouse = @import("../../input/mouse.zig");
|
||||
const mouse_encode = @import("../../input/mouse_encode.zig");
|
||||
const Result = @import("result.zig").Result;
|
||||
|
||||
const log = std.log.scoped(.mouse_event);
|
||||
|
||||
/// Wrapper around mouse event that tracks the allocator for C API usage.
|
||||
const MouseEventWrapper = struct {
|
||||
event: mouse_encode.Event = .{},
|
||||
alloc: Allocator,
|
||||
};
|
||||
|
||||
/// C: GhosttyMouseEvent
|
||||
pub const Event = ?*MouseEventWrapper;
|
||||
|
||||
/// C: GhosttyMouseAction
|
||||
pub const Action = mouse.Action;
|
||||
|
||||
/// C: GhosttyMouseButton
|
||||
pub const Button = mouse.Button;
|
||||
|
||||
/// C: GhosttyMousePosition
|
||||
pub const Position = mouse_encode.Event.Pos;
|
||||
|
||||
/// C: GhosttyMods
|
||||
pub const Mods = key.Mods;
|
||||
|
||||
pub fn new(
|
||||
alloc_: ?*const CAllocator,
|
||||
result: *Event,
|
||||
) callconv(.c) Result {
|
||||
const alloc = lib_alloc.default(alloc_);
|
||||
const ptr = alloc.create(MouseEventWrapper) catch
|
||||
return .out_of_memory;
|
||||
ptr.* = .{ .alloc = alloc };
|
||||
result.* = ptr;
|
||||
return .success;
|
||||
}
|
||||
|
||||
pub fn free(event_: Event) callconv(.c) void {
|
||||
const wrapper = event_ orelse return;
|
||||
const alloc = wrapper.alloc;
|
||||
alloc.destroy(wrapper);
|
||||
}
|
||||
|
||||
pub fn set_action(event_: Event, action: Action) callconv(.c) void {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Action, @intFromEnum(action)) catch {
|
||||
log.warn("set_action invalid action value={d}", .{@intFromEnum(action)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
event_.?.event.action = action;
|
||||
}
|
||||
|
||||
pub fn get_action(event_: Event) callconv(.c) Action {
|
||||
return event_.?.event.action;
|
||||
}
|
||||
|
||||
pub fn set_button(event_: Event, button: Button) callconv(.c) void {
|
||||
if (comptime std.debug.runtime_safety) {
|
||||
_ = std.meta.intToEnum(Button, @intFromEnum(button)) catch {
|
||||
log.warn("set_button invalid button value={d}", .{@intFromEnum(button)});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
event_.?.event.button = button;
|
||||
}
|
||||
|
||||
pub fn clear_button(event_: Event) callconv(.c) void {
|
||||
event_.?.event.button = null;
|
||||
}
|
||||
|
||||
pub fn get_button(event_: Event, out: ?*Button) callconv(.c) bool {
|
||||
if (event_.?.event.button) |button| {
|
||||
if (out) |ptr| ptr.* = button;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn set_mods(event_: Event, mods: Mods) callconv(.c) void {
|
||||
event_.?.event.mods = mods;
|
||||
}
|
||||
|
||||
pub fn get_mods(event_: Event) callconv(.c) Mods {
|
||||
return event_.?.event.mods;
|
||||
}
|
||||
|
||||
pub fn set_position(event_: Event, pos: Position) callconv(.c) void {
|
||||
event_.?.event.pos = pos;
|
||||
}
|
||||
|
||||
pub fn get_position(event_: Event) callconv(.c) Position {
|
||||
return event_.?.event.pos;
|
||||
}
|
||||
|
||||
test "alloc" {
|
||||
var e: Event = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&e,
|
||||
));
|
||||
free(e);
|
||||
}
|
||||
|
||||
test "free null" {
|
||||
free(null);
|
||||
}
|
||||
|
||||
test "set/get" {
|
||||
var e: Event = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
&lib_alloc.test_allocator,
|
||||
&e,
|
||||
));
|
||||
defer free(e);
|
||||
|
||||
// Action
|
||||
set_action(e, .motion);
|
||||
try testing.expectEqual(Action.motion, get_action(e));
|
||||
|
||||
// Button
|
||||
set_button(e, .left);
|
||||
var button: Button = .unknown;
|
||||
try testing.expect(get_button(e, &button));
|
||||
try testing.expectEqual(Button.left, button);
|
||||
try testing.expect(get_button(e, null));
|
||||
|
||||
clear_button(e);
|
||||
try testing.expect(!get_button(e, &button));
|
||||
|
||||
// Mods
|
||||
const mods: Mods = .{ .shift = true, .ctrl = true };
|
||||
set_mods(e, mods);
|
||||
const got_mods = get_mods(e);
|
||||
try testing.expect(got_mods.shift);
|
||||
try testing.expect(got_mods.ctrl);
|
||||
try testing.expect(!got_mods.alt);
|
||||
|
||||
// Position
|
||||
set_position(e, .{ .x = 12.5, .y = -4.0 });
|
||||
const pos = get_position(e);
|
||||
try testing.expectEqual(@as(f32, 12.5), pos.x);
|
||||
try testing.expectEqual(@as(f32, -4.0), pos.y);
|
||||
}
|
||||
Reference in New Issue
Block a user