Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 220 additions & 0 deletions cmux-linux/src/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,41 @@ pub const Surface = struct {
0,
);

// --- Input event controllers ---

const widget: *c.GtkWidget = @ptrCast(@alignCast(gl_area));

// Keyboard: key-pressed / key-released
const key_ctrl = c.gtk.gtk_event_controller_key_new();
c.gtk.gtk_widget_add_controller(widget, key_ctrl);
_ = c.gtk.g_signal_connect_data(key_ctrl, "key-pressed", @ptrCast(&onKeyPressed), surface, null, 0);
_ = c.gtk.g_signal_connect_data(key_ctrl, "key-released", @ptrCast(&onKeyReleased), surface, null, 0);

// Mouse motion
const motion_ctrl = c.gtk.gtk_event_controller_motion_new();
c.gtk.gtk_widget_add_controller(widget, motion_ctrl);
_ = c.gtk.g_signal_connect_data(motion_ctrl, "motion", @ptrCast(&onMouseMotion), surface, null, 0);

// Mouse buttons (click)
const click_gesture = c.gtk.gtk_gesture_click_new();
c.gtk.gtk_gesture_single_set_button(@ptrCast(click_gesture), 0); // all buttons
c.gtk.gtk_widget_add_controller(widget, @ptrCast(click_gesture));
_ = c.gtk.g_signal_connect_data(@ptrCast(click_gesture), "pressed", @ptrCast(&onMousePressed), surface, null, 0);
_ = c.gtk.g_signal_connect_data(@ptrCast(click_gesture), "released", @ptrCast(&onMouseReleased), surface, null, 0);

// Scroll
const scroll_ctrl = c.gtk.gtk_event_controller_scroll_new(
c.gtk.GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES | c.gtk.GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
);
c.gtk.gtk_widget_add_controller(widget, scroll_ctrl);
_ = c.gtk.g_signal_connect_data(scroll_ctrl, "scroll", @ptrCast(&onScroll), surface, null, 0);

// Focus in/out
const focus_ctrl = c.gtk.gtk_event_controller_focus_new();
c.gtk.gtk_widget_add_controller(widget, focus_ctrl);
_ = c.gtk.g_signal_connect_data(focus_ctrl, "enter", @ptrCast(&onFocusEnter), surface, null, 0);
_ = c.gtk.g_signal_connect_data(focus_ctrl, "leave", @ptrCast(&onFocusLeave), surface, null, 0);

return @ptrCast(@alignCast(gl_area));
}

Expand Down Expand Up @@ -129,8 +164,193 @@ pub const Surface = struct {
);
}
}

// ── Keyboard input ──────────────────────────────────────────────

/// GtkEventControllerKey "key-pressed" signal.
fn onKeyPressed(
_: ?*anyopaque,
keyval: c_uint,
keycode: c_uint,
state: c_uint,
surface: *Surface,
) callconv(.c) c.gtk.gboolean {
return @intFromBool(surface.handleKey(c.ghostty.GHOSTTY_ACTION_PRESS, keyval, keycode, state));
}

/// GtkEventControllerKey "key-released" signal.
fn onKeyReleased(
_: ?*anyopaque,
keyval: c_uint,
keycode: c_uint,
state: c_uint,
surface: *Surface,
) callconv(.c) void {
_ = surface.handleKey(c.ghostty.GHOSTTY_ACTION_RELEASE, keyval, keycode, state);
}

/// Common keyboard handler: translate GDK key event → ghostty_input_key_s.
fn handleKey(surface: *Surface, action: c_int, keyval: c_uint, keycode: c_uint, state: c_uint) bool {
const s = surface.ghostty_surface orelse return false;

// Convert GDK modifier state to ghostty mods
const mods = gtkModsToGhostty(state);

// Build key input struct. The keycode from GTK4 is the hardware
// scancode (evdev code), which ghostty uses directly. The text
// is derived from the GDK keyval for printable characters.
var text_buf: [8]u8 = undefined;
var text_ptr: [*c]const u8 = null;
var text_len: usize = 0;

// Only send text for press/repeat, not release
if (action != c.ghostty.GHOSTTY_ACTION_RELEASE) {
const uc = c.gtk.gdk_keyval_to_unicode(keyval);
if (uc > 0 and uc != 0xFFFF) {
text_len = std.unicode.utf8Encode(@intCast(uc), &text_buf) catch 0;
if (text_len > 0) {
text_buf[text_len] = 0; // null-terminate
text_ptr = &text_buf;
}
}
}

// Get the unshifted codepoint (keyval without shift modifier)
const unshifted_codepoint = c.gtk.gdk_keyval_to_unicode(
c.gtk.gdk_keyval_to_lower(keyval),
);

const key_event = c.ghostty.ghostty_input_key_s{
.action = action,
.mods = mods,
.consumed_mods = c.ghostty.GHOSTTY_MODS_NONE,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 consumed_mods always zero may cause double-modifier effects

Hardcoding consumed_mods = GHOSTTY_MODS_NONE means ghostty never learns that a modifier was "consumed" producing the text character. For Shift+letter ghostty would see both the shift modifier and the uppercase letter, and may emit an extra modifier keypress sequence. Ideally, when text_len > 0 and the keyval differs from its lowercase version, GHOSTTY_MODS_SHIFT should be included in consumed_mods. This matches the approach in ghostty/src/apprt/gtk/Surface.zig where consumed mods are derived from the GTK key event's consumed_modifiers.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto, I think we should take note of this

.keycode = keycode,
.text = text_ptr,
.unshifted_codepoint = unshifted_codepoint,
.composing = false,
};

return c.ghostty.ghostty_surface_key(s, key_event);
}

// ── Mouse input ─────────────────────────────────────────────────

/// GtkGestureClick "pressed" signal.
fn onMousePressed(
gesture: ?*anyopaque,
_: c_int, // n_press
x: f64,
y: f64,
surface: *Surface,
) callconv(.c) void {
const s = surface.ghostty_surface orelse return;

// Grab focus on click
if (surface.gl_area) |gl| {
c.gtk.gtk_widget_grab_focus(@ptrCast(@alignCast(gl)));
}

const button = gtkButtonToGhostty(c.gtk.gtk_gesture_single_get_current_button(@ptrCast(gesture)));
const event = c.gtk.gtk_event_controller_get_current_event(@ptrCast(gesture));
const mods = gtkModsToGhostty(if (event != null) c.gtk.gdk_event_get_modifier_state(event) else 0);

// Send position first, then button press
c.ghostty.ghostty_surface_mouse_pos(s, x, y, mods);
_ = c.ghostty.ghostty_surface_mouse_button(s, c.ghostty.GHOSTTY_MOUSE_PRESS, button, mods);
}

/// GtkGestureClick "released" signal.
fn onMouseReleased(
gesture: ?*anyopaque,
_: c_int, // n_press
x: f64,
y: f64,
surface: *Surface,
) callconv(.c) void {
const s = surface.ghostty_surface orelse return;
const button = gtkButtonToGhostty(c.gtk.gtk_gesture_single_get_current_button(@ptrCast(gesture)));
const event = c.gtk.gtk_event_controller_get_current_event(@ptrCast(gesture));
const mods = gtkModsToGhostty(if (event != null) c.gtk.gdk_event_get_modifier_state(event) else 0);

c.ghostty.ghostty_surface_mouse_pos(s, x, y, mods);
_ = c.ghostty.ghostty_surface_mouse_button(s, c.ghostty.GHOSTTY_MOUSE_RELEASE, button, mods);
}

/// GtkEventControllerMotion "motion" signal.
fn onMouseMotion(
controller: ?*anyopaque,
x: f64,
y: f64,
surface: *Surface,
) callconv(.c) void {
const s = surface.ghostty_surface orelse return;
const event = c.gtk.gtk_event_controller_get_current_event(@ptrCast(controller));
const mods = gtkModsToGhostty(if (event != null) c.gtk.gdk_event_get_modifier_state(event) else 0);
c.ghostty.ghostty_surface_mouse_pos(s, x, y, mods);
}

/// GtkEventControllerScroll "scroll" signal.
fn onScroll(
controller: ?*anyopaque,
dx: f64,
dy: f64,
surface: *Surface,
) callconv(.c) c.gtk.gboolean {
const s = surface.ghostty_surface orelse return 0;
const event = c.gtk.gtk_event_controller_get_current_event(@ptrCast(controller));
const mods_raw = if (event != null) c.gtk.gdk_event_get_modifier_state(event) else @as(c_uint, 0);

// ghostty_input_scroll_mods_t is a packed int: lower bits are mods
const scroll_mods: c.ghostty.ghostty_input_scroll_mods_t = @intCast(gtkModsToGhostty(mods_raw));
c.ghostty.ghostty_surface_mouse_scroll(s, dx, dy, scroll_mods);
return 1; // handled
}

// ── Focus ───────────────────────────────────────────────────────

/// GtkEventControllerFocus "enter" signal.
fn onFocusEnter(_: ?*anyopaque, surface: *Surface) callconv(.c) void {
if (surface.ghostty_surface) |s| {
c.ghostty.ghostty_surface_set_focus(s, true);
}
}

/// GtkEventControllerFocus "leave" signal.
fn onFocusLeave(_: ?*anyopaque, surface: *Surface) callconv(.c) void {
if (surface.ghostty_surface) |s| {
c.ghostty.ghostty_surface_set_focus(s, false);
}
}
};

// ── Shared helpers ──────────────────────────────────────────────────

/// Translate GDK modifier state bitmask to ghostty modifier bitmask.
fn gtkModsToGhostty(state: c_uint) c.ghostty.ghostty_input_mods_e {
var mods: c_int = c.ghostty.GHOSTTY_MODS_NONE;
if (state & c.gtk.GDK_SHIFT_MASK != 0) mods |= c.ghostty.GHOSTTY_MODS_SHIFT;
if (state & c.gtk.GDK_CONTROL_MASK != 0) mods |= c.ghostty.GHOSTTY_MODS_CTRL;
if (state & c.gtk.GDK_ALT_MASK != 0) mods |= c.ghostty.GHOSTTY_MODS_ALT;
if (state & c.gtk.GDK_SUPER_MASK != 0) mods |= c.ghostty.GHOSTTY_MODS_SUPER;
if (state & c.gtk.GDK_LOCK_MASK != 0) mods |= c.ghostty.GHOSTTY_MODS_CAPS;
return mods;
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.

/// Translate GDK button number (1=left, 2=middle, 3=right) to ghostty button.
fn gtkButtonToGhostty(button: c_uint) c.ghostty.ghostty_input_mouse_button_e {
return switch (button) {
1 => c.ghostty.GHOSTTY_MOUSE_LEFT,
2 => c.ghostty.GHOSTTY_MOUSE_MIDDLE,
3 => c.ghostty.GHOSTTY_MOUSE_RIGHT,
4 => c.ghostty.GHOSTTY_MOUSE_FOUR,
5 => c.ghostty.GHOSTTY_MOUSE_FIVE,
6 => c.ghostty.GHOSTTY_MOUSE_SIX,
7 => c.ghostty.GHOSTTY_MOUSE_SEVEN,
8 => c.ghostty.GHOSTTY_MOUSE_EIGHT,
else => c.ghostty.GHOSTTY_MOUSE_UNKNOWN,
};
}

/// Get the Surface wrapper from a GtkWidget (if it's a terminal panel).
pub fn fromWidget(widget: *c.GtkWidget) ?*Surface {
const data = c.gtk.g_object_get_data(@ptrCast(@alignCast(widget)), "cmux-surface");
Expand Down
Loading