forked from manaflow-ai/cmux
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsurface.zig
More file actions
359 lines (306 loc) · 14.3 KB
/
surface.zig
File metadata and controls
359 lines (306 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
/// Terminal surface: embeds a libghostty surface in a GtkGLArea widget.
///
/// Handles OpenGL rendering, input forwarding, and resize events.
/// Reference: ghostty/src/apprt/gtk/Surface.zig
const std = @import("std");
const c = @import("c_api.zig");
/// State for a single terminal surface.
pub const Surface = struct {
/// The libghostty surface handle.
ghostty_surface: c.ghostty_surface_t = null,
/// The GtkGLArea widget.
gl_area: ?*c.GtkGLArea = null,
/// The parent ghostty app.
ghostty_app: c.ghostty_app_t = null,
/// Create a new terminal surface embedded in a GtkGLArea.
pub fn create(app: c.ghostty_app_t) !*c.GtkWidget {
const gl_area: *c.GtkGLArea = @ptrCast(c.gtk.gtk_gl_area_new() orelse
return error.WidgetCreationFailed);
// Set OpenGL requirements
c.gtk.gtk_gl_area_set_required_version(gl_area, 3, 3);
c.gtk.gtk_gl_area_set_has_depth_buffer(gl_area, 0);
c.gtk.gtk_gl_area_set_has_stencil_buffer(gl_area, 0);
c.gtk.gtk_gl_area_set_auto_render(gl_area, 0);
// Make it focusable for keyboard input
c.gtk.gtk_widget_set_focusable(@ptrCast(@alignCast(gl_area)), 1);
c.gtk.gtk_widget_set_can_focus(@ptrCast(@alignCast(gl_area)), 1);
// Set expand to fill available space
c.gtk.gtk_widget_set_hexpand(@ptrCast(@alignCast(gl_area)), 1);
c.gtk.gtk_widget_set_vexpand(@ptrCast(@alignCast(gl_area)), 1);
// Allocate surface state
const alloc = std.heap.c_allocator;
const surface = try alloc.create(Surface);
surface.* = .{
.ghostty_app = app,
.gl_area = gl_area,
};
// Store surface pointer as widget data
c.gtk.g_object_set_data(@ptrCast(@alignCast(gl_area)), "cmux-surface", surface);
// Connect signals
_ = c.gtk.g_signal_connect_data(
@ptrCast(@alignCast(gl_area)),
"realize",
@ptrCast(&onRealize),
surface,
null,
0,
);
_ = c.gtk.g_signal_connect_data(
@ptrCast(@alignCast(gl_area)),
"render",
@ptrCast(&onRender),
surface,
null,
0,
);
_ = c.gtk.g_signal_connect_data(
@ptrCast(@alignCast(gl_area)),
"resize",
@ptrCast(&onResize),
surface,
null,
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));
}
/// Called when the GtkGLArea is realized (OpenGL context available).
fn onRealize(_: *c.GtkGLArea, surface: *Surface) callconv(.c) void {
const gl_area = surface.gl_area orelse return;
c.gtk.gtk_gl_area_make_current(gl_area);
// Get content scale from the GDK surface
const native = c.gtk.gtk_native_get_surface(
@ptrCast(c.gtk.gtk_widget_get_native(@ptrCast(@alignCast(gl_area)))),
);
const scale: f64 = if (native != null)
@floatFromInt(c.gtk.gdk_surface_get_scale_factor(native))
else
1.0;
// Create the ghostty surface with linux platform config
var surface_config = c.ghostty.ghostty_surface_config_new();
surface_config.platform_tag = c.ghostty.GHOSTTY_PLATFORM_LINUX;
surface_config.platform = .{ .linux = .{
.surface = @ptrCast(@alignCast(gl_area)),
} };
surface_config.scale_factor = scale;
// Pass the GtkWidget pointer as surface userdata so the
// close_surface_cb can identify which panel to remove.
surface_config.userdata = @ptrCast(@alignCast(gl_area));
surface.ghostty_surface = c.ghostty.ghostty_surface_new(
surface.ghostty_app,
&surface_config,
);
if (surface.ghostty_surface == null) {
std.log.err("Failed to create ghostty surface", .{});
}
}
/// Called on each OpenGL render frame.
fn onRender(_: *c.GtkGLArea, _: ?*anyopaque, surface: *Surface) callconv(.c) c.gtk.gboolean {
if (surface.ghostty_surface) |s| {
c.ghostty.ghostty_surface_draw(s);
}
return c.gtk.G_SOURCE_REMOVE; // We handle rendering
}
/// Called when the GtkGLArea is resized.
fn onResize(_: *c.GtkGLArea, width: c_int, height: c_int, surface: *Surface) callconv(.c) void {
if (surface.ghostty_surface) |s| {
c.ghostty.ghostty_surface_set_size(
s,
@intCast(width),
@intCast(height),
);
}
}
// ── 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,
.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;
}
/// 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");
if (data) |d| return @ptrCast(@alignCast(d));
return null;
}