diff --git a/.gitmodules b/.gitmodules index 651473f1e..1a3feed9f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -28,3 +28,6 @@ [submodule "vendor/zig-libxml2"] path = vendor/zig-libxml2 url = https://github.com/mitchellh/zig-libxml2.git +[submodule "vendor/cimgui"] + path = vendor/cimgui + url = https://github.com/cimgui/cimgui.git diff --git a/build.zig b/build.zig index c3f4e776b..a54501d5b 100644 --- a/build.zig +++ b/build.zig @@ -6,6 +6,7 @@ const glfw = @import("vendor/mach/libs/glfw/build.zig"); const fontconfig = @import("pkg/fontconfig/build.zig"); const freetype = @import("pkg/freetype/build.zig"); const harfbuzz = @import("pkg/harfbuzz/build.zig"); +const imgui = @import("pkg/imgui/build.zig"); const libxml2 = @import("vendor/zig-libxml2/libxml2.zig"); const libuv = @import("pkg/libuv/build.zig"); const libpng = @import("pkg/libpng/build.zig"); @@ -189,6 +190,7 @@ fn addDeps( if (enable_fontconfig) step.addPackage(fontconfig.pkg); step.addPackage(freetype.pkg); step.addPackage(harfbuzz.pkg); + step.addPackage(imgui.pkg); step.addPackage(glfw.pkg); step.addPackage(libuv.pkg); step.addPackage(utf8proc.pkg); @@ -214,10 +216,15 @@ fn addDeps( _ = try utf8proc.link(b, step); // Glfw - try glfw.link(b, step, .{ - .metal = false, - .opengl = false, // Found at runtime + const glfw_opts: glfw.Options = .{ .metal = false, .opengl = false }; + try glfw.link(b, step, glfw_opts); + + // Imgui + const imgui_backends = [_][]const u8{ "glfw", "opengl3" }; + const imgui_step = try imgui.link(b, step, .{ + .backends = &imgui_backends, }); + try glfw.link(b, imgui_step, glfw_opts); // Dynamic link if (!static) { diff --git a/pkg/imgui/build.zig b/pkg/imgui/build.zig new file mode 100644 index 000000000..e25082e51 --- /dev/null +++ b/pkg/imgui/build.zig @@ -0,0 +1,93 @@ +const std = @import("std"); + +/// Directories with our includes. +const root = thisDir() ++ "../../../vendor/cimgui/"; +pub const include_paths = [_][]const u8{ + root, + root ++ "imgui", + root ++ "imgui/backends", +}; + +pub const pkg = std.build.Pkg{ + .name = "imgui", + .source = .{ .path = thisDir() ++ "/main.zig" }, +}; + +fn thisDir() []const u8 { + return std.fs.path.dirname(@src().file) orelse "."; +} + +pub const Options = struct { + backends: ?[]const []const u8 = null, +}; + +pub fn link( + b: *std.build.Builder, + step: *std.build.LibExeObjStep, + opt: Options, +) !*std.build.LibExeObjStep { + const lib = try buildImgui(b, step, opt); + step.linkLibrary(lib); + inline for (include_paths) |path| step.addIncludePath(path); + return lib; +} + +pub fn buildImgui( + b: *std.build.Builder, + step: *std.build.LibExeObjStep, + opt: Options, +) !*std.build.LibExeObjStep { + const target = step.target; + const lib = b.addStaticLibrary("imgui", null); + lib.setTarget(step.target); + lib.setBuildMode(step.build_mode); + + // Include + inline for (include_paths) |path| lib.addIncludePath(path); + + // Link + lib.linkLibC(); + + // Compile + var flags = std.ArrayList([]const u8).init(b.allocator); + defer flags.deinit(); + try flags.appendSlice(&.{ + "-DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1", + + //"-fno-sanitize=undefined", + }); + switch (target.getOsTag()) { + .windows => try flags.appendSlice(&.{ + "-DIMGUI_IMPL_API=extern\t\"C\"\t__declspec(dllexport)", + }), + else => try flags.appendSlice(&.{ + "-DIMGUI_IMPL_API=extern\t\"C\"\t", + }), + } + + // C files + lib.addCSourceFiles(srcs, flags.items); + if (opt.backends) |backends| { + for (backends) |backend| { + var buf: [4096]u8 = undefined; + const path = try std.fmt.bufPrint( + &buf, + "{s}imgui/backends/imgui_impl_{s}.cpp", + .{ root, backend }, + ); + + lib.addCSourceFile(path, flags.items); + } + } + + return lib; +} + +const srcs = &.{ + root ++ "cimgui.cpp", + root ++ "imgui/imgui.cpp", + root ++ "imgui/imgui_demo.cpp", + root ++ "imgui/imgui_draw.cpp", + root ++ "imgui/imgui_tables.cpp", + root ++ "imgui/imgui_widgets.cpp", +}; diff --git a/pkg/imgui/c.zig b/pkg/imgui/c.zig new file mode 100644 index 000000000..621fd1335 --- /dev/null +++ b/pkg/imgui/c.zig @@ -0,0 +1,4 @@ +pub usingnamespace @cImport({ + @cDefine("CIMGUI_DEFINE_ENUMS_AND_STRUCTS", ""); + @cInclude("cimgui.h"); +}); diff --git a/pkg/imgui/context.zig b/pkg/imgui/context.zig new file mode 100644 index 000000000..2b68df9ec --- /dev/null +++ b/pkg/imgui/context.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const c = @import("c.zig"); +const Allocator = std.mem.Allocator; + +pub const Context = opaque { + pub fn create() Allocator.Error!*Context { + return @ptrCast( + ?*Context, + c.igCreateContext(null), + ) orelse Allocator.Error.OutOfMemory; + } + + pub fn destroy(self: *Context) void { + c.igDestroyContext(self.cval()); + } + + pub inline fn cval(self: *Context) *c.ImGuiContext { + return @ptrCast( + *c.ImGuiContext, + @alignCast(@alignOf(c.ImGuiContext), self), + ); + } +}; + +test { + var ctx = try Context.create(); + defer ctx.destroy(); +} diff --git a/pkg/imgui/core.zig b/pkg/imgui/core.zig new file mode 100644 index 000000000..259350c20 --- /dev/null +++ b/pkg/imgui/core.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const c = @import("c.zig"); +const imgui = @import("main.zig"); +const Allocator = std.mem.Allocator; + +pub fn newFrame() void { + c.igNewFrame(); +} + +pub fn endFrame() void { + c.igEndFrame(); +} + +pub fn render() void { + c.igRender(); +} + +pub fn showDemoWindow(open: ?*bool) void { + c.igShowDemoWindow(@ptrCast([*c]bool, if (open) |v| v else null)); +} diff --git a/pkg/imgui/draw_data.zig b/pkg/imgui/draw_data.zig new file mode 100644 index 000000000..9f7f04fd1 --- /dev/null +++ b/pkg/imgui/draw_data.zig @@ -0,0 +1,20 @@ +const std = @import("std"); +const c = @import("c.zig"); +const imgui = @import("main.zig"); +const Allocator = std.mem.Allocator; + +pub const DrawData = opaque { + pub fn get() Allocator.Error!*DrawData { + return @ptrCast( + ?*DrawData, + c.igGetDrawData(), + ) orelse Allocator.Error.OutOfMemory; + } + + pub inline fn cval(self: *DrawData) *c.ImGuiDrawData { + return @ptrCast( + *c.ImGuiDrawData, + @alignCast(@alignOf(c.ImGuiDrawData), self), + ); + } +}; diff --git a/pkg/imgui/impl_glfw.zig b/pkg/imgui/impl_glfw.zig new file mode 100644 index 000000000..cb27b86fb --- /dev/null +++ b/pkg/imgui/impl_glfw.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const c = @import("c.zig"); +const imgui = @import("main.zig"); +const Allocator = std.mem.Allocator; + +pub const ImplGlfw = struct { + pub const GLFWWindow = opaque {}; + + pub fn initForOpenGL(win: *GLFWWindow, install_callbacks: bool) bool { + // https://github.com/ocornut/imgui/issues/5785 + defer _ = glfwGetError(null); + + return ImGui_ImplGlfw_InitForOpenGL(win, install_callbacks); + } + + pub fn shutdown() void { + return ImGui_ImplGlfw_Shutdown(); + } + + pub fn newFrame() void { + return ImGui_ImplGlfw_NewFrame(); + } + + extern "c" fn glfwGetError(?*const anyopaque) c_int; + extern "c" fn ImGui_ImplGlfw_InitForOpenGL(*GLFWWindow, bool) bool; + extern "c" fn ImGui_ImplGlfw_Shutdown() void; + extern "c" fn ImGui_ImplGlfw_NewFrame() void; +}; diff --git a/pkg/imgui/impl_opengl3.zig b/pkg/imgui/impl_opengl3.zig new file mode 100644 index 000000000..1e6f6952f --- /dev/null +++ b/pkg/imgui/impl_opengl3.zig @@ -0,0 +1,30 @@ +const std = @import("std"); +const c = @import("c.zig"); +const imgui = @import("main.zig"); +const Allocator = std.mem.Allocator; + +pub const ImplOpenGL3 = struct { + pub fn init(glsl_version: ?[:0]const u8) bool { + return ImGui_ImplOpenGL3_Init( + if (glsl_version) |s| s.ptr else null, + ); + } + + pub fn shutdown() void { + return ImGui_ImplOpenGL3_Shutdown(); + } + + pub fn newFrame() void { + return ImGui_ImplOpenGL3_NewFrame(); + } + + pub fn renderDrawData(data: *imgui.DrawData) void { + ImGui_ImplOpenGL3_RenderDrawData(data); + } + + extern "c" fn glfwGetError(?*const anyopaque) c_int; + extern "c" fn ImGui_ImplOpenGL3_Init([*c]const u8) bool; + extern "c" fn ImGui_ImplOpenGL3_Shutdown() void; + extern "c" fn ImGui_ImplOpenGL3_NewFrame() void; + extern "c" fn ImGui_ImplOpenGL3_RenderDrawData(*imgui.DrawData) void; +}; diff --git a/pkg/imgui/io.zig b/pkg/imgui/io.zig new file mode 100644 index 000000000..b473c61b6 --- /dev/null +++ b/pkg/imgui/io.zig @@ -0,0 +1,26 @@ +const std = @import("std"); +const c = @import("c.zig"); +const imgui = @import("main.zig"); +const Allocator = std.mem.Allocator; + +pub const IO = opaque { + pub fn get() Allocator.Error!*IO { + return @ptrCast( + ?*IO, + c.igGetIO(), + ) orelse Allocator.Error.OutOfMemory; + } + + pub inline fn cval(self: *IO) *c.ImGuiIO { + return @ptrCast( + *c.ImGuiIO, + @alignCast(@alignOf(c.ImGuiIO), self), + ); + } +}; + +test { + const ctx = try imgui.Context.create(); + defer ctx.destroy(); + _ = try IO.get(); +} diff --git a/pkg/imgui/main.zig b/pkg/imgui/main.zig new file mode 100644 index 000000000..795903706 --- /dev/null +++ b/pkg/imgui/main.zig @@ -0,0 +1,13 @@ +pub const c = @import("c.zig"); +pub usingnamespace @import("context.zig"); +pub usingnamespace @import("core.zig"); +pub usingnamespace @import("draw_data.zig"); +pub usingnamespace @import("io.zig"); +pub usingnamespace @import("style.zig"); + +pub usingnamespace @import("impl_glfw.zig"); +pub usingnamespace @import("impl_opengl3.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/pkg/imgui/style.zig b/pkg/imgui/style.zig new file mode 100644 index 000000000..623494ed1 --- /dev/null +++ b/pkg/imgui/style.zig @@ -0,0 +1,35 @@ +const std = @import("std"); +const c = @import("c.zig"); +const Allocator = std.mem.Allocator; + +pub const Style = opaque { + pub fn get() Allocator.Error!*Style { + return @ptrCast( + ?*Style, + c.igGetStyle(), + ) orelse Allocator.Error.OutOfMemory; + } + + pub fn colorsDark(self: *Style) void { + c.igStyleColorsDark(self.cval()); + } + + pub fn colorsLight(self: *Style) void { + c.igStyleColorsLight(self.cval()); + } + + pub fn colorsClassic(self: *Style) void { + c.igStyleColorsClassic(self.cval()); + } + + pub fn scaleAllSizes(self: *Style, factor: f32) void { + c.ImGuiStyle_ScaleAllSizes(self.cval(), factor); + } + + pub inline fn cval(self: *Style) *c.ImGuiStyle { + return @ptrCast( + *c.ImGuiStyle, + @alignCast(@alignOf(c.ImGuiStyle), self), + ); + } +}; diff --git a/src/DevMode.zig b/src/DevMode.zig new file mode 100644 index 000000000..9f26c6f44 --- /dev/null +++ b/src/DevMode.zig @@ -0,0 +1,38 @@ +//! This file implements the "dev mode" interface for the terminal. This +//! includes state managements and rendering. +const DevMode = @This(); + +const imgui = @import("imgui"); + +/// If this is false, the rest of the terminal will be compiled without +/// dev mode support at all. +pub const enabled = true; + +/// The global DevMode instance that can be used app-wide. Assume all functions +/// are NOT thread-safe unless otherwise noted. +pub var instance: DevMode = .{}; + +/// Whether to show the dev mode UI currently. +visible: bool = false, + +/// Update the state associated with the dev mode. This should generally +/// only be called paired with a render since it otherwise wastes CPU +/// cycles. +pub fn update(self: DevMode) void { + _ = self; + imgui.ImplOpenGL3.newFrame(); + imgui.ImplGlfw.newFrame(); + imgui.newFrame(); + + // Just demo for now + imgui.showDemoWindow(null); +} + +/// Render the scene and return the draw data. The caller must be imgui-aware +/// in order to render the draw data. This lets this file be renderer/backend +/// agnostic. +pub fn render(self: DevMode) !*imgui.DrawData { + _ = self; + imgui.render(); + return try imgui.DrawData.get(); +} diff --git a/src/Window.zig b/src/Window.zig index 5053e7457..2680ebd27 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -12,6 +12,7 @@ const Allocator = std.mem.Allocator; const Grid = @import("Grid.zig"); const glfw = @import("glfw"); const gl = @import("opengl.zig"); +const imgui = @import("imgui"); const libuv = @import("libuv"); const Pty = @import("Pty.zig"); const font = @import("font/main.zig"); @@ -22,6 +23,7 @@ const max_timer = @import("max_timer.zig"); const terminal = @import("terminal/main.zig"); const Config = @import("config.zig").Config; const input = @import("input.zig"); +const DevMode = @import("DevMode.zig"); const RenderTimer = max_timer.MaxTimer(renderTimerCallback); @@ -45,6 +47,9 @@ window: glfw.Window, /// The glfw mouse cursor handle. cursor: glfw.Cursor, +/// Imgui context +imgui_ctx: if (DevMode.enabled) *imgui.Context else void, + /// Whether the window is currently focused focused: bool, @@ -475,7 +480,10 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo .bg_g = @intToFloat(f32, config.background.g) / 255.0, .bg_b = @intToFloat(f32, config.background.b) / 255.0, .bg_a = 1.0, + + .imgui_ctx = if (!DevMode.enabled) void else try imgui.Context.create(), }; + errdefer if (DevMode.enabled) self.imgui_ctx.destroy(); // Setup our callbacks and user data window.setUserPointer(self); @@ -499,10 +507,37 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo @intCast(i32, window_size.height), ); + // Load imgui. This must be done LAST because it has to be done after + // all our GLFW setup is complete. + if (DevMode.enabled) { + const io = try imgui.IO.get(); + io.cval().IniFilename = "ghostty_dev_mode.ini"; + + // On Mac imgui handles scaling automatically just fine. On Linux + // and other platforms we need to apply a scaling factor. + if (builtin.os.tag != .macos) io.cval().FontGlobalScale = content_scale.x_scale; + + const style = try imgui.Style.get(); + style.colorsDark(); + + assert(imgui.ImplGlfw.initForOpenGL( + @ptrCast(*imgui.ImplGlfw.GLFWWindow, window.handle), + true, + )); + assert(imgui.ImplOpenGL3.init("#version 330 core")); + } + return self; } pub fn destroy(self: *Window) void { + if (DevMode.enabled) { + // Uninitialize imgui + imgui.ImplOpenGL3.shutdown(); + imgui.ImplGlfw.shutdown(); + self.imgui_ctx.destroy(); + } + // Deinitialize the pty. This closes the pty handles. This should // cause a close in the our subprocess so just wait for that. self.pty.deinit(); @@ -747,6 +782,7 @@ fn keyCallback( .F10 => .f10, .F11 => .f11, .F12 => .f12, + .grave_accent => .grave_accent, else => .invalid, }, }; @@ -795,6 +831,11 @@ fn keyCallback( log.err("error queueing write in keyCallback err={}", .{err}); } }, + + .toggle_dev_mode => if (DevMode.enabled) { + DevMode.instance.visible = !DevMode.instance.visible; + win.render_timer.schedule() catch unreachable; + } else log.warn("dev mode was not compiled into this binary", .{}), } // Bindings always result in us ignoring the char if printable @@ -1109,6 +1150,13 @@ fn mouseButtonCallback( const win = window.getUserPointer(Window) orelse return; + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + win.render_timer.schedule() catch |err| + log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); + } + // Convert glfw button to input button const button: input.MouseButton = switch (glfw_button) { .left => .left, @@ -1186,6 +1234,13 @@ fn cursorPosCallback( const win = window.getUserPointer(Window) orelse return; + // If our dev mode window is visible then we always schedule a render on + // cursor move because the cursor might touch our windows. + if (DevMode.enabled and DevMode.instance.visible) { + win.render_timer.schedule() catch |err| + log.err("error scheduling render timer in cursorPosCallback err={}", .{err}); + } + // Do a mouse report if (win.terminal.modes.mouse_event != .none) { // We use the first mouse button we find pressed in order to report @@ -1525,6 +1580,12 @@ fn renderTimerCallback(t: *libuv.Timer) void { return; }; + if (DevMode.enabled and DevMode.instance.visible) { + DevMode.instance.update(); + const data = DevMode.instance.render() catch unreachable; + imgui.ImplOpenGL3.renderDrawData(data); + } + // Swap win.window.swapBuffers() catch |err| { log.err("error swapping buffers: {}", .{err}); diff --git a/src/config.zig b/src/config.zig index 7448f7ae1..6bd7a3225 100644 --- a/src/config.zig +++ b/src/config.zig @@ -120,6 +120,13 @@ pub const Config = struct { try result.keybind.set.put(alloc, .{ .key = .f11 }, .{ .csi = "23~" }); try result.keybind.set.put(alloc, .{ .key = .f12 }, .{ .csi = "24~" }); + // Dev Mode + try result.keybind.set.put( + alloc, + .{ .key = .grave_accent, .mods = .{ .shift = true, .super = true } }, + .{ .toggle_dev_mode = 0 }, + ); + return result; } diff --git a/src/input/Binding.zig b/src/input/Binding.zig index be59c60f7..184fd7217 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -140,6 +140,9 @@ pub const Action = union(enum) { /// Copy and paste. copy_to_clipboard: Void, paste_from_clipboard: Void, + + /// Dev mode + toggle_dev_mode: Void, }; /// Trigger is the associated key state that can trigger an action. diff --git a/src/input/key.zig b/src/input/key.zig index ddd1b39f4..dda6d6704 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -49,6 +49,9 @@ pub const Key = enum { y, z, + // other + grave_accent, // ` + // control up, down, diff --git a/vendor/cimgui b/vendor/cimgui new file mode 160000 index 000000000..9ce2c32da --- /dev/null +++ b/vendor/cimgui @@ -0,0 +1 @@ +Subproject commit 9ce2c32dada1e1fcb90f2c9bab2568895db719f5