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,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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user