feat(inspector): port the terminal inspector
Ports ZIG/src/inspector (the Dear ImGui terminal inspector) as the ghostty-inspector crate using dear-imgui-rs + dear-imgui-glow (imgui-rs lacks a safe docking API): screen, modes, keyboard events, terminal IO / VT sequence log with filtering, cell inspector with picking, page list, renderer and size/font info, with Ghostty's docking layout. The crate denies unsafe except a single dock.rs module wrapping DockBuilder. GTK: imgui_widget, inspector_widget and inspector_window classes with their blueprints replace the former no-op; ctrl+shift+i toggles the inspector window. Core: activate/deactivate hooks, key event and mouse recording, and VT stream recording in termio while an inspector is active, as in Zig; overlay features reach the renderer through an InspectorHook on the render state. The debug input replay hook gained inspector-screenshot/ click/scroll steps used to verify all tabs in the real app. Deviations: font size scales with the display scale instead of Zig's fixed 16*2; pop-out dock splits apply one frame later; DCS/APC descriptions are approximated. Test Plan: - cargo test -p ghostty-inspector (15 incl. headless full-UI draw) - cargo test -p ghostty-core --test e2e_inspector (4)
This commit is contained in:
1 parent
2dc1c6028d
commit
90ad602969
25 files changed
+6693
No files matched your search
@@ -0,0 +1,693 @@
|
||||
//! Port of apprt/gtk/class/imgui_widget.zig (`GhosttyImguiWidget`).
|
||||
//!
|
||||
//! A widget for embedding a Dear ImGui application: a `GtkGLArea` that owns
|
||||
//! an ImGui context and a glow based renderer, feeds the GTK input events
|
||||
//! into ImGui and drives the frames.
|
||||
//!
|
||||
//! The Zig widget exposes `setup` and `render` virtual methods that subclasses
|
||||
//! implement. zig-gobject cannot define virtual methods, and neither can we
|
||||
//! cheaply in gtk-rs without a lot of class struct boilerplate, so subclasses
|
||||
//! install the two hooks with [`ImguiWidget::set_hooks`] instead.
|
||||
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use dear_imgui_glow::GlowRenderer;
|
||||
use ghostty_inspector::imgui::{Context, MouseButton, Ui};
|
||||
use gtk::glib::translate::IntoGlib;
|
||||
use gtk::{gdk, glib};
|
||||
|
||||
use crate::key;
|
||||
|
||||
/// Called once after the ImGui context is created, with the context current
|
||||
/// (Zig virtual method `setup`).
|
||||
pub type SetupHook = Box<dyn Fn(&mut Context, f64)>;
|
||||
|
||||
/// Called for every frame to draw the UI (Zig virtual method `render`).
|
||||
pub type RenderHook = Box<dyn Fn(&Ui)>;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct ImguiWidget(ObjectSubclass<imp::ImguiWidget>)
|
||||
@extends adw::Bin, gtk::Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
/// Implemented by subclasses of [`ImguiWidget`].
|
||||
pub trait ImguiWidgetImpl: BinImpl {}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
// SAFETY: `ImguiWidget` adds no class/instance data that needs to be set up
|
||||
// for subclasses; we forward to the parent (`adw::Bin`) class/instance
|
||||
// initialization, which is valid for any `T: ImguiWidgetImpl`.
|
||||
unsafe impl<T: ImguiWidgetImpl> IsSubclassable<T> for ImguiWidget {
|
||||
fn class_init(class: &mut glib::Class<Self>) {
|
||||
<adw::Bin as IsSubclassable<T>>::class_init(class.upcast_ref_mut());
|
||||
}
|
||||
|
||||
fn instance_init(instance: &mut glib::subclass::InitializingObject<T>) {
|
||||
<adw::Bin as IsSubclassable<T>>::instance_init(instance);
|
||||
}
|
||||
}
|
||||
|
||||
impl ImguiWidget {
|
||||
/// This should be called anytime the underlying data for the UI changes
|
||||
/// so that the UI can be refreshed.
|
||||
pub fn queue_render(&self) {
|
||||
self.imp().gl_area.queue_render();
|
||||
}
|
||||
|
||||
/// Install the hooks (Zig: the `setup` and `render` virtual methods).
|
||||
pub fn set_hooks(&self, setup: SetupHook, render: RenderHook) {
|
||||
let imp = self.imp();
|
||||
imp.setup_hook.replace(Some(setup));
|
||||
imp.render_hook.replace(Some(render));
|
||||
}
|
||||
|
||||
/// Debug aid: write the next rendered frame to a PNG file.
|
||||
pub fn debug_screenshot(&self, path: &str) {
|
||||
self.debug_render_now(3);
|
||||
self.imp().pending_screenshot.replace(Some(path.to_owned()));
|
||||
self.debug_render_now(1);
|
||||
}
|
||||
|
||||
/// Debug aid: render `frames` frames right now. Compositors only send
|
||||
/// frame callbacks to visible windows, so the GLArea of an inspector
|
||||
/// window that is not on screen would otherwise lag behind the
|
||||
/// synthesized input.
|
||||
pub fn debug_render_now(&self, frames: usize) {
|
||||
let imp = self.imp();
|
||||
if !imp.gl_area.is_realized() {
|
||||
return;
|
||||
}
|
||||
imp.gl_area.make_current();
|
||||
imp.gl_area.attach_buffers();
|
||||
for _ in 0..frames {
|
||||
imp.do_render();
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug aid: synthesize a left click at the given position (in
|
||||
/// logical pixels, like the pointer position GTK reports). Frames are
|
||||
/// rendered synchronously between the events.
|
||||
pub fn debug_click(&self, x: f64, y: f64) {
|
||||
let imp = self.imp();
|
||||
let scale = imp.scale_factor();
|
||||
let pos = [(x * scale) as f32, (y * scale) as f32];
|
||||
imp.with_ctx(|ctx| ctx.io_mut().add_mouse_pos_event(pos));
|
||||
self.debug_render_now(3);
|
||||
imp.with_ctx(|ctx| {
|
||||
ctx.io_mut().add_mouse_button_event(MouseButton::Left, true);
|
||||
});
|
||||
self.debug_render_now(3);
|
||||
imp.with_ctx(|ctx| {
|
||||
ctx.io_mut()
|
||||
.add_mouse_button_event(MouseButton::Left, false);
|
||||
});
|
||||
self.debug_render_now(3);
|
||||
}
|
||||
|
||||
/// Debug aid: synthesize a mouse wheel scroll.
|
||||
pub fn debug_scroll(&self, x: f64, y: f64, dy: f64) {
|
||||
let imp = self.imp();
|
||||
let scale = imp.scale_factor();
|
||||
imp.with_ctx(|ctx| {
|
||||
let io = ctx.io_mut();
|
||||
io.add_mouse_pos_event([(x * scale) as f32, (y * scale) as f32]);
|
||||
io.add_mouse_wheel_event([0.0, dy as f32]);
|
||||
});
|
||||
self.debug_render_now(3);
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
/// The state that only exists while the GLArea is realized.
|
||||
struct Gui {
|
||||
ctx: Context,
|
||||
renderer: GlowRenderer,
|
||||
}
|
||||
|
||||
#[derive(gtk::CompositeTemplate)]
|
||||
#[template(resource = "/com/mitchellh/ghostty/ui/1.5/imgui-widget.ui")]
|
||||
pub struct ImguiWidget {
|
||||
/// GL area where we display the Dear ImGui application.
|
||||
#[template_child]
|
||||
pub gl_area: TemplateChild<gtk::GLArea>,
|
||||
|
||||
/// GTK input method context
|
||||
#[template_child]
|
||||
pub im_context: TemplateChild<gtk::IMMulticontext>,
|
||||
|
||||
pub setup_hook: RefCell<Option<SetupHook>>,
|
||||
pub render_hook: RefCell<Option<RenderHook>>,
|
||||
|
||||
/// Dear ImGui context and renderer. We create a context per widget
|
||||
/// so that we can have multiple active imgui views in the same
|
||||
/// application.
|
||||
gui: RefCell<Option<Gui>>,
|
||||
|
||||
/// Our previous instant used to calculate delta time for animations.
|
||||
instant: Cell<Option<Instant>>,
|
||||
|
||||
/// Tick callback for timed updates.
|
||||
tick_callback: RefCell<Option<gtk::TickCallbackId>>,
|
||||
|
||||
/// Last render time for throttling to 30 FPS.
|
||||
last_render_time: Cell<Option<Instant>>,
|
||||
|
||||
/// A pending debug screenshot path.
|
||||
pub pending_screenshot: RefCell<Option<String>>,
|
||||
|
||||
/// The scale factor the style was last scaled to.
|
||||
style_scale: Cell<f32>,
|
||||
}
|
||||
|
||||
impl Default for ImguiWidget {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
gl_area: TemplateChild::default(),
|
||||
im_context: TemplateChild::default(),
|
||||
setup_hook: RefCell::new(None),
|
||||
render_hook: RefCell::new(None),
|
||||
gui: RefCell::new(None),
|
||||
instant: Cell::new(None),
|
||||
tick_callback: RefCell::new(None),
|
||||
last_render_time: Cell::new(None),
|
||||
pending_screenshot: RefCell::new(None),
|
||||
style_scale: Cell::new(1.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for ImguiWidget {
|
||||
const NAME: &'static str = "GhosttyImguiWidget";
|
||||
type Type = super::ImguiWidget;
|
||||
type ParentType = adw::Bin;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
klass.bind_template();
|
||||
klass.bind_template_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl ObjectImpl for ImguiWidget {
|
||||
fn dispose(&self) {
|
||||
self.dispose_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for ImguiWidget {}
|
||||
impl BinImpl for ImguiWidget {}
|
||||
|
||||
impl ImguiWidget {
|
||||
/// Run `f` with the ImGui context, if there is one.
|
||||
pub(super) fn with_ctx(&self, f: impl FnOnce(&mut Context)) -> bool {
|
||||
let Ok(mut gui) = self.gui.try_borrow_mut() else {
|
||||
return false;
|
||||
};
|
||||
let Some(gui) = gui.as_mut() else {
|
||||
log::warn!("Dear ImGui context not initialized");
|
||||
return false;
|
||||
};
|
||||
f(&mut gui.ctx);
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn scale_factor(&self) -> f64 {
|
||||
f64::from(self.gl_area.scale_factor())
|
||||
}
|
||||
|
||||
/// Handle key press/release events.
|
||||
fn key_event(
|
||||
&self,
|
||||
press: bool,
|
||||
ec_key: >k::EventControllerKey,
|
||||
keyval: gdk::Key,
|
||||
gtk_mods: gdk::ModifierType,
|
||||
) -> bool {
|
||||
self.obj().queue_render();
|
||||
|
||||
let handled = self.with_ctx(|ctx| {
|
||||
let io = ctx.io_mut();
|
||||
let mods = key::translate_mods(gtk_mods);
|
||||
io.add_key_event(ghostty_inspector::imgui::Key::LeftShift, mods.shift);
|
||||
io.add_key_event(ghostty_inspector::imgui::Key::LeftCtrl, mods.ctrl);
|
||||
io.add_key_event(ghostty_inspector::imgui::Key::LeftAlt, mods.alt);
|
||||
io.add_key_event(ghostty_inspector::imgui::Key::LeftSuper, mods.super_);
|
||||
|
||||
// If our keyval has a key, then we send that key event
|
||||
if let Some(imgui_key) = key::key_from_keyval(keyval.into_glib())
|
||||
.and_then(ghostty_inspector::keys::imgui_key)
|
||||
{
|
||||
io.add_key_event(imgui_key, press);
|
||||
}
|
||||
});
|
||||
if !handled {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try to process the event as text
|
||||
if let Some(event) = ec_key.current_event() {
|
||||
self.im_context.filter_keypress(&event);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Translate a GTK mouse button to a Dear ImGui mouse button.
|
||||
fn translate_mouse_button(button: u32) -> Option<MouseButton> {
|
||||
match button {
|
||||
1 => Some(MouseButton::Left),
|
||||
2 => Some(MouseButton::Middle),
|
||||
3 => Some(MouseButton::Right),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_button(&self, gesture: >k::GestureClick, down: bool) {
|
||||
self.obj().queue_render();
|
||||
let button = gesture.current_button();
|
||||
self.with_ctx(|ctx| {
|
||||
if let Some(button) = Self::translate_mouse_button(button) {
|
||||
ctx.io_mut().add_mouse_button_event(button, down);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw a frame, optionally rendering it.
|
||||
fn frame(&self, gui: &mut Gui, render: bool) -> Result<(), String> {
|
||||
// Determine our delta time
|
||||
let now = Instant::now();
|
||||
let delta = self.instant.get().map_or(1.0 / 60.0, |prev| {
|
||||
now.duration_since(prev).as_secs_f32().max(0.00001)
|
||||
});
|
||||
self.instant.set(Some(now));
|
||||
gui.ctx.io_mut().set_delta_time(delta);
|
||||
|
||||
{
|
||||
let ui = gui.ctx.frame();
|
||||
// Call the hook to draw the UI.
|
||||
if let Some(hook) = self.render_hook.borrow().as_ref() {
|
||||
hook(ui);
|
||||
}
|
||||
}
|
||||
|
||||
if !render {
|
||||
gui.ctx.end_frame();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let consumer = gui
|
||||
.renderer
|
||||
.renderer_consumer()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let pending = gui.ctx.render(consumer);
|
||||
gui.renderer.render(pending).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn clear(gui: &Gui) {
|
||||
use glow::HasContext;
|
||||
let Some(gl) = gui.renderer.gl_context() else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: the GLArea context is current while rendering.
|
||||
unsafe {
|
||||
gl.clear_color(0.0, 0.0, 0.0, 1.0);
|
||||
gl.clear(glow::COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
fn screenshot(&self, gui: &Gui, path: &str) {
|
||||
use glow::HasContext;
|
||||
let Some(gl) = gui.renderer.gl_context() else {
|
||||
return;
|
||||
};
|
||||
let w = self.gl_area.width() * self.gl_area.scale_factor();
|
||||
let h = self.gl_area.height() * self.gl_area.scale_factor();
|
||||
if w <= 0 || h <= 0 {
|
||||
return;
|
||||
}
|
||||
let (w, h) = (w as usize, h as usize);
|
||||
let mut pixels = vec![0u8; w * h * 4];
|
||||
// SAFETY: the GLArea context is current while rendering and the
|
||||
// buffer is large enough for `w * h` RGBA8 pixels.
|
||||
unsafe {
|
||||
gl.read_pixels(
|
||||
0,
|
||||
0,
|
||||
w as i32,
|
||||
h as i32,
|
||||
glow::RGBA,
|
||||
glow::UNSIGNED_BYTE,
|
||||
glow::PixelPackData::Slice(Some(&mut pixels)),
|
||||
);
|
||||
}
|
||||
// GL's origin is the bottom-left.
|
||||
let stride = w * 4;
|
||||
let mut flipped = Vec::with_capacity(pixels.len());
|
||||
for row in pixels.chunks_exact(stride).rev() {
|
||||
flipped.extend_from_slice(row);
|
||||
}
|
||||
let texture = gdk::MemoryTexture::new(
|
||||
w as i32,
|
||||
h as i32,
|
||||
gdk::MemoryFormat::R8g8b8a8,
|
||||
&glib::Bytes::from_owned(flipped),
|
||||
stride,
|
||||
);
|
||||
match texture.save_to_png(path) {
|
||||
Ok(()) => log::warn!("inspector screenshot written to {path}"),
|
||||
Err(err) => log::error!("inspector screenshot failed path={path} err={err}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImguiWidget {
|
||||
#[allow(unsafe_code)]
|
||||
fn do_realize(&self, area: >k::GLArea) {
|
||||
area.make_current();
|
||||
if let Some(err) = area.error() {
|
||||
log::warn!(
|
||||
"GLArea for Dear ImGui widget failed to realize: {}",
|
||||
err.message()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut ctx = match Context::try_create() {
|
||||
Ok(ctx) => ctx,
|
||||
Err(err) => {
|
||||
log::warn!("unable to initialize Dear ImGui context: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Setup some basic config
|
||||
if let Err(err) = ctx.set_platform_name(Some("ghostty_gtk")) {
|
||||
log::warn!("unable to set the imgui platform name err={err}");
|
||||
}
|
||||
|
||||
// Call the hook to setup the UI. This happens before the
|
||||
// renderer is created so the fonts exist when it uploads the
|
||||
// atlas.
|
||||
if let Some(hook) = self.setup_hook.borrow().as_ref() {
|
||||
hook(&mut ctx, self.scale_factor());
|
||||
}
|
||||
|
||||
// Realize means that our OpenGL context is ready, so we can now
|
||||
// initialize the ImGui OpenGL backend for our context.
|
||||
//
|
||||
// SAFETY: the GLArea's context is current and `proc_address`
|
||||
// resolves GL functions for the current context.
|
||||
let gl = unsafe { glow::Context::from_loader_function(crate::gl::proc_address) };
|
||||
let renderer = match GlowRenderer::new(gl, &mut ctx) {
|
||||
Ok(r) => r,
|
||||
Err(err) => {
|
||||
log::warn!("unable to initialize the Dear ImGui OpenGL renderer: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.style_scale.set(1.0);
|
||||
self.gui.replace(Some(Gui { ctx, renderer }));
|
||||
|
||||
// Add a tick callback to drive timed updates via the frame clock.
|
||||
// Redraws are limited to 30 FPS max since our imgui widgets don't
|
||||
// usually need higher frame rates than that.
|
||||
let obj = self.obj();
|
||||
let id = obj.add_tick_callback(|widget, _| {
|
||||
let imp = widget.imp();
|
||||
let frame_time = Duration::from_secs(1) / 30;
|
||||
let should_render = imp
|
||||
.last_render_time
|
||||
.get()
|
||||
.is_none_or(|last| last.elapsed() >= frame_time);
|
||||
if should_render {
|
||||
widget.queue_render();
|
||||
}
|
||||
glib::ControlFlow::Continue
|
||||
});
|
||||
self.tick_callback.replace(Some(id));
|
||||
}
|
||||
|
||||
fn do_unrealize(&self, area: >k::GLArea) {
|
||||
// Remove the tick callback if it was registered.
|
||||
if let Some(id) = self.tick_callback.take() {
|
||||
id.remove();
|
||||
}
|
||||
|
||||
// Unrealize is not guaranteed to be called with a current GL
|
||||
// context, so we make it current for ImGui cleanup.
|
||||
area.make_current();
|
||||
if let Some(err) = area.error() {
|
||||
log::warn!(
|
||||
"GLArea for Dear ImGui widget failed to realize: {}",
|
||||
err.message()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(mut gui) = self.gui.take()
|
||||
&& let Err(err) = gui.renderer.shutdown(&mut gui.ctx)
|
||||
{
|
||||
log::warn!("error shutting down the imgui renderer err={err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn do_resize(&self, width: i32, height: i32, area: >k::GLArea) {
|
||||
let scale_factor = area.scale_factor();
|
||||
self.with_ctx(|ctx| {
|
||||
// Our display size is always unscaled. We'll do the scaling
|
||||
// in the style instead. This creates crisper looking fonts.
|
||||
let io = ctx.io_mut();
|
||||
io.set_display_size([width as f32, height as f32]);
|
||||
io.set_display_framebuffer_scale([1.0, 1.0]);
|
||||
|
||||
// Scale the style appropriately. Zig builds a fresh default
|
||||
// style and scales it on every resize, the style of a
|
||||
// Dear ImGui context cannot be rebuilt from Rust so we scale
|
||||
// by the change in the factor instead.
|
||||
let scale = scale_factor as f32;
|
||||
let last = self.style_scale.replace(scale);
|
||||
if (scale - last).abs() > f32::EPSILON {
|
||||
ctx.style_mut().scale_all_sizes(scale / last);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn do_render(&self) -> bool {
|
||||
// Update last render time for tick callback throttling.
|
||||
self.last_render_time.set(Some(Instant::now()));
|
||||
|
||||
let Ok(mut guard) = self.gui.try_borrow_mut() else {
|
||||
return false;
|
||||
};
|
||||
let Some(gui) = guard.as_mut() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Setup our frame. We render twice because some ImGui behaviors
|
||||
// take multiple renders to process. I don't know how to make this
|
||||
// more efficient.
|
||||
if let Err(err) = self.frame(gui, false) {
|
||||
log::warn!("imgui frame failed err={err}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// OpenGL final render
|
||||
Self::clear(gui);
|
||||
if let Err(err) = self.frame(gui, true) {
|
||||
log::warn!("imgui render failed err={err}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(path) = self.pending_screenshot.take() {
|
||||
self.screenshot(gui, &path);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn do_focus_enter(&self, _ec: >k::EventControllerFocus) {
|
||||
self.with_ctx(|ctx| ctx.io_mut().add_focus_event(true));
|
||||
self.obj().queue_render();
|
||||
}
|
||||
|
||||
fn do_focus_leave(&self, _ec: >k::EventControllerFocus) {
|
||||
self.with_ctx(|ctx| ctx.io_mut().add_focus_event(false));
|
||||
self.obj().queue_render();
|
||||
}
|
||||
|
||||
fn do_key_pressed(
|
||||
&self,
|
||||
keyval: gdk::Key,
|
||||
_keycode: u32,
|
||||
state: gdk::ModifierType,
|
||||
ec: >k::EventControllerKey,
|
||||
) -> glib::Propagation {
|
||||
if self.key_event(true, ec, keyval, state) {
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}
|
||||
|
||||
fn do_key_released(
|
||||
&self,
|
||||
keyval: gdk::Key,
|
||||
_keycode: u32,
|
||||
state: gdk::ModifierType,
|
||||
ec: >k::EventControllerKey,
|
||||
) {
|
||||
self.key_event(false, ec, keyval, state);
|
||||
}
|
||||
|
||||
fn do_mouse_pressed(&self, _n: i32, _x: f64, _y: f64, gesture: >k::GestureClick) {
|
||||
self.mouse_button(gesture, true);
|
||||
}
|
||||
|
||||
fn do_mouse_released(&self, _n: i32, _x: f64, _y: f64, gesture: >k::GestureClick) {
|
||||
self.mouse_button(gesture, false);
|
||||
}
|
||||
|
||||
fn do_mouse_motion(&self, x: f64, y: f64, _ec: >k::EventControllerMotion) {
|
||||
self.obj().queue_render();
|
||||
let scale = self.scale_factor();
|
||||
self.with_ctx(|ctx| {
|
||||
ctx.io_mut()
|
||||
.add_mouse_pos_event([(x * scale) as f32, (y * scale) as f32]);
|
||||
});
|
||||
}
|
||||
|
||||
fn do_scroll(&self, x: f64, y: f64, _ec: >k::EventControllerScroll) -> glib::Propagation {
|
||||
self.obj().queue_render();
|
||||
let handled = self.with_ctx(|ctx| {
|
||||
ctx.io_mut().add_mouse_wheel_event([x as f32, -y as f32]);
|
||||
});
|
||||
if handled {
|
||||
glib::Propagation::Stop
|
||||
} else {
|
||||
glib::Propagation::Proceed
|
||||
}
|
||||
}
|
||||
|
||||
fn do_im_commit(&self, text: &str, _im: >k::IMMulticontext) {
|
||||
self.obj().queue_render();
|
||||
self.with_ctx(|ctx| ctx.io_mut().add_input_characters_utf8(text));
|
||||
}
|
||||
}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl ImguiWidget {
|
||||
#[template_callback]
|
||||
fn realize(area: >k::GLArea, this: &super::ImguiWidget) {
|
||||
this.imp().do_realize(area);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn unrealize(area: >k::GLArea, this: &super::ImguiWidget) {
|
||||
this.imp().do_unrealize(area);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn resize(area: >k::GLArea, width: i32, height: i32, this: &super::ImguiWidget) {
|
||||
this.imp().do_resize(width, height, area);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn render(area: >k::GLArea, context: &gdk::GLContext, this: &super::ImguiWidget) -> bool {
|
||||
let _ = (area, context);
|
||||
this.imp().do_render()
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn focus_enter(ec: >k::EventControllerFocus, this: &super::ImguiWidget) {
|
||||
this.imp().do_focus_enter(ec);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn focus_leave(ec: >k::EventControllerFocus, this: &super::ImguiWidget) {
|
||||
this.imp().do_focus_leave(ec);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn key_pressed(
|
||||
ec: >k::EventControllerKey,
|
||||
keyval: gdk::Key,
|
||||
keycode: u32,
|
||||
state: gdk::ModifierType,
|
||||
this: &super::ImguiWidget,
|
||||
) -> glib::Propagation {
|
||||
this.imp().do_key_pressed(keyval, keycode, state, ec)
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn key_released(
|
||||
ec: >k::EventControllerKey,
|
||||
keyval: gdk::Key,
|
||||
keycode: u32,
|
||||
state: gdk::ModifierType,
|
||||
this: &super::ImguiWidget,
|
||||
) {
|
||||
this.imp().do_key_released(keyval, keycode, state, ec);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn mouse_pressed(
|
||||
gesture: >k::GestureClick,
|
||||
n: i32,
|
||||
x: f64,
|
||||
y: f64,
|
||||
this: &super::ImguiWidget,
|
||||
) {
|
||||
this.imp().do_mouse_pressed(n, x, y, gesture);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn mouse_released(
|
||||
gesture: >k::GestureClick,
|
||||
n: i32,
|
||||
x: f64,
|
||||
y: f64,
|
||||
this: &super::ImguiWidget,
|
||||
) {
|
||||
this.imp().do_mouse_released(n, x, y, gesture);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn mouse_motion(
|
||||
ec: >k::EventControllerMotion,
|
||||
x: f64,
|
||||
y: f64,
|
||||
this: &super::ImguiWidget,
|
||||
) {
|
||||
this.imp().do_mouse_motion(x, y, ec);
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn scroll(
|
||||
ec: >k::EventControllerScroll,
|
||||
x: f64,
|
||||
y: f64,
|
||||
this: &super::ImguiWidget,
|
||||
) -> glib::Propagation {
|
||||
this.imp().do_scroll(x, y, ec)
|
||||
}
|
||||
|
||||
#[template_callback]
|
||||
fn im_commit(im: >k::IMMulticontext, text: &str, this: &super::ImguiWidget) {
|
||||
this.imp().do_im_commit(text, im);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
//! Port of apprt/gtk/class/inspector_widget.zig (`GhosttyInspectorWidget`).
|
||||
//!
|
||||
//! Widget for displaying the Ghostty inspector. Setting its `surface`
|
||||
//! property activates the inspector on that surface's core surface and
|
||||
//! clearing it deactivates it again.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use ghostty_apprt::runtime_api::CoreSurface;
|
||||
use gtk::glib;
|
||||
|
||||
use super::imgui_widget::{ImguiWidget, ImguiWidgetImpl};
|
||||
use super::surface::Surface;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct InspectorWidget(ObjectSubclass<imp::InspectorWidget>)
|
||||
@extends ImguiWidget, adw::Bin, gtk::Widget,
|
||||
@implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
|
||||
}
|
||||
|
||||
impl InspectorWidget {
|
||||
/// Queue a render of the Dear ImGui widget.
|
||||
pub fn queue_render(&self) {
|
||||
self.upcast_ref::<ImguiWidget>().queue_render();
|
||||
}
|
||||
|
||||
pub fn get_surface(&self) -> Option<Surface> {
|
||||
self.imp().surface.upgrade()
|
||||
}
|
||||
|
||||
/// Debug aid: synthesize a left click.
|
||||
pub fn debug_click(&self, x: f64, y: f64) {
|
||||
self.upcast_ref::<ImguiWidget>().debug_click(x, y);
|
||||
}
|
||||
|
||||
/// Debug aid: synthesize a mouse wheel scroll.
|
||||
pub fn debug_scroll(&self, x: f64, y: f64, dy: f64) {
|
||||
self.upcast_ref::<ImguiWidget>().debug_scroll(x, y, dy);
|
||||
}
|
||||
|
||||
/// Debug aid: write the next rendered frame to a PNG file.
|
||||
pub fn debug_screenshot(&self, path: &str) {
|
||||
self.upcast_ref::<ImguiWidget>().debug_screenshot(path);
|
||||
}
|
||||
}
|
||||
|
||||
impl ImguiWidgetImpl for imp::InspectorWidget {}
|
||||
|
||||
mod imp {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
#[derive(gtk::CompositeTemplate, glib::Properties, Default)]
|
||||
#[properties(wrapper_type = super::InspectorWidget)]
|
||||
#[template(resource = "/com/mitchellh/ghostty/ui/1.5/inspector-widget.ui")]
|
||||
pub struct InspectorWidget {
|
||||
/// The surface that we are attached to. This is NOT strongly
|
||||
/// referenced (the GObject weak reference is cleared when the surface
|
||||
/// is disposed, which closes our window).
|
||||
pub surface: glib::WeakRef<Surface>,
|
||||
|
||||
/// The core surface of `surface`, kept so that we can deactivate the
|
||||
/// inspector when the surface is going away (the GTK surface only
|
||||
/// drops its core surface when finalized, after the window holding
|
||||
/// this widget was destroyed in its dispose).
|
||||
core: RefCell<Option<Rc<dyn CoreSurface>>>,
|
||||
|
||||
#[property(get = Self::get_surface, set = Self::set_surface, nullable, type = Option<Surface>, name = "surface")]
|
||||
_surface_prop: (),
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for InspectorWidget {
|
||||
const NAME: &'static str = "GhosttyInspectorWidget";
|
||||
type Type = super::InspectorWidget;
|
||||
type ParentType = ImguiWidget;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
ImguiWidget::ensure_type();
|
||||
klass.bind_template();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for InspectorWidget {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
|
||||
let obj = self.obj();
|
||||
let weak = obj.downgrade();
|
||||
obj.upcast_ref::<ImguiWidget>().set_hooks(
|
||||
// Called to do initial setup of the UI.
|
||||
Box::new(ghostty_inspector::setup),
|
||||
// Called for every frame to draw the UI.
|
||||
Box::new(move |ui| {
|
||||
let Some(this) = weak.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(surface) = this.imp().surface.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let Some(core) = surface.core() else {
|
||||
return;
|
||||
};
|
||||
core.render_inspector(ui);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
// Clear our surface so it deactivates the inspector.
|
||||
self.set_surface(None);
|
||||
self.dispose_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for InspectorWidget {}
|
||||
impl BinImpl for InspectorWidget {}
|
||||
|
||||
impl InspectorWidget {
|
||||
fn get_surface(&self) -> Option<Surface> {
|
||||
self.surface.upgrade()
|
||||
}
|
||||
|
||||
fn set_surface(&self, surface: Option<Surface>) {
|
||||
// Do nothing if we're not changing the value.
|
||||
if surface == self.surface.upgrade() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup our notification to happen at the end because we're
|
||||
// changing values no matter what.
|
||||
let obj = self.obj();
|
||||
let _guard = obj.freeze_notify();
|
||||
obj.notify("surface");
|
||||
|
||||
// Deactivate the inspector on the old surface if it exists and
|
||||
// set our value to null.
|
||||
self.surface.set(None);
|
||||
if let Some(core) = self.core.take() {
|
||||
core.deactivate_inspector();
|
||||
}
|
||||
|
||||
// Activate the inspector on the new surface.
|
||||
let Some(surface) = surface else {
|
||||
return;
|
||||
};
|
||||
let Some(core) = surface.core() else {
|
||||
return;
|
||||
};
|
||||
if let Err(err) = core.activate_inspector() {
|
||||
log::warn!("failed to activate inspector err={err}");
|
||||
return;
|
||||
}
|
||||
|
||||
self.surface.set(Some(&surface));
|
||||
self.core.replace(Some(core));
|
||||
obj.queue_render();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
//! Port of apprt/gtk/class/inspector_window.zig (`GhosttyInspectorWindow`).
|
||||
//!
|
||||
//! Window for displaying the Ghostty inspector.
|
||||
|
||||
use adw::prelude::*;
|
||||
use adw::subclass::prelude::*;
|
||||
use gtk::glib;
|
||||
|
||||
use super::debug_warning::DebugWarning;
|
||||
use super::inspector_widget::InspectorWidget;
|
||||
use super::surface::Surface;
|
||||
|
||||
glib::wrapper! {
|
||||
pub struct InspectorWindow(ObjectSubclass<imp::InspectorWindow>)
|
||||
@extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,
|
||||
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
|
||||
gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
|
||||
}
|
||||
|
||||
use gtk::gio;
|
||||
|
||||
impl InspectorWindow {
|
||||
pub fn new(surface: &Surface) -> Self {
|
||||
crate::register_resources();
|
||||
glib::Object::builder().property("surface", surface).build()
|
||||
}
|
||||
|
||||
/// Present the window.
|
||||
pub fn present_window(&self) {
|
||||
self.present();
|
||||
}
|
||||
|
||||
/// Queue a render of the embedded widget.
|
||||
pub fn queue_render(&self) {
|
||||
self.imp().inspector_widget.queue_render();
|
||||
}
|
||||
|
||||
/// Debug aid: synthesize a left click in the inspector.
|
||||
pub fn debug_click(&self, x: f64, y: f64) {
|
||||
self.imp().inspector_widget.debug_click(x, y);
|
||||
}
|
||||
|
||||
/// Debug aid: synthesize a mouse wheel scroll in the inspector.
|
||||
pub fn debug_scroll(&self, x: f64, y: f64, dy: f64) {
|
||||
self.imp().inspector_widget.debug_scroll(x, y, dy);
|
||||
}
|
||||
|
||||
/// Debug aid: write the next rendered inspector frame to a PNG file.
|
||||
pub fn debug_screenshot(&self, path: &str) {
|
||||
self.imp().inspector_widget.debug_screenshot(path);
|
||||
}
|
||||
}
|
||||
|
||||
mod imp {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
#[derive(gtk::CompositeTemplate, glib::Properties, Default)]
|
||||
#[properties(wrapper_type = super::InspectorWindow)]
|
||||
#[template(resource = "/com/mitchellh/ghostty/ui/1.5/inspector-window.ui")]
|
||||
pub struct InspectorWindow {
|
||||
/// The surface that we are attached to.
|
||||
#[property(get, set = Self::set_surface, nullable)]
|
||||
pub surface: glib::WeakRef<Surface>,
|
||||
|
||||
/// The embedded inspector widget.
|
||||
#[template_child]
|
||||
pub inspector_widget: TemplateChild<InspectorWidget>,
|
||||
|
||||
/// Whether this is a debug build (shows the debug warning).
|
||||
#[property(get = Self::get_debug, type = bool, name = "debug")]
|
||||
_debug_prop: (),
|
||||
}
|
||||
|
||||
impl InspectorWindow {
|
||||
#[allow(clippy::unused_self)]
|
||||
fn get_debug(&self) -> bool {
|
||||
cfg!(debug_assertions)
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_pass_by_value)] // property setter signature
|
||||
fn set_surface(&self, surface: Option<Surface>) {
|
||||
self.surface.set(surface.as_ref());
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::object_subclass]
|
||||
impl ObjectSubclass for InspectorWindow {
|
||||
const NAME: &'static str = "GhosttyInspectorWindow";
|
||||
type Type = super::InspectorWindow;
|
||||
type ParentType = adw::ApplicationWindow;
|
||||
|
||||
fn class_init(klass: &mut Self::Class) {
|
||||
DebugWarning::ensure_type();
|
||||
InspectorWidget::ensure_type();
|
||||
klass.bind_template();
|
||||
klass.bind_template_callbacks();
|
||||
}
|
||||
|
||||
fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
|
||||
obj.init_template();
|
||||
}
|
||||
}
|
||||
|
||||
#[glib::derived_properties]
|
||||
impl ObjectImpl for InspectorWindow {
|
||||
fn constructed(&self) {
|
||||
self.parent_constructed();
|
||||
let obj = self.obj();
|
||||
|
||||
// Add our dev CSS class if we're in debug mode.
|
||||
if cfg!(debug_assertions) {
|
||||
obj.add_css_class("devel");
|
||||
}
|
||||
|
||||
// Set our window icon. We can't set this in the blueprint file
|
||||
// because its dependent on the build config.
|
||||
obj.set_icon_name(Some(crate::APPLICATION_ID));
|
||||
}
|
||||
|
||||
fn dispose(&self) {
|
||||
// You MUST clear all weak refs in dispose, otherwise it causes
|
||||
// memory corruption on dispose on the TARGET (weak referenced)
|
||||
// object (Zig comment).
|
||||
self.set_surface(None);
|
||||
self.dispose_template();
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetImpl for InspectorWindow {}
|
||||
impl WindowImpl for InspectorWindow {}
|
||||
impl ApplicationWindowImpl for InspectorWindow {}
|
||||
impl AdwApplicationWindowImpl for InspectorWindow {}
|
||||
|
||||
#[gtk::template_callbacks]
|
||||
impl InspectorWindow {
|
||||
/// `notify::surface` of the inspector widget: if the inspector's
|
||||
/// surface went away, we destroy the window.
|
||||
#[template_callback]
|
||||
fn notify_inspector_surface(
|
||||
inspector: &InspectorWidget,
|
||||
_pspec: &glib::ParamSpec,
|
||||
this: &super::InspectorWindow,
|
||||
) {
|
||||
if inspector.get_surface().is_none() {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user