Ports apprt/embedded.zig, main_c.zig and config/CApi.zig + c_get.zig as crates/ghostty-lib (cdylib/staticlib/rlib `libghostty`), exporting every function of the unchanged include/ghostty.h - the ABI Ghostty's Swift macOS/iOS app links against. The embedded runtime implements the runtime traits over the host's C callbacks, so the real core drives it. - ghostty-core selects the renderer per target: Metal on Apple (surface NSView/UIView via RtSurface::platform_view), OpenGL elsewhere. - ghostty_config_get follows Zig's c_get rules via a CValue type (143 supported keys, checked against the Zig field types). - The inspector C API is complete; its Metal backend replaces ImGui's Metal renderer and has not run. Verification: generated C ABI probe (sizeof/alignof/offsetof of every struct/union, 755 enum values, every function linked, nm exports, headers byte-identical to upstream); a C host on Linux covering init, CLI actions, config, app, keys, focus and color scheme; an opt-in test-platform feature creating real surfaces on Linux with headless EGL and a pty (13 tests). Apple targets are clippy-clean but not run; the Swift app has not been built against it. Test Plan: - cargo test -p ghostty-lib (and --features test-platform) - cargo clippy --target aarch64-apple-darwin -p ghostty-lib (cross env in docs/rewrite/notes/macos-M1.md)
478 lines
15 KiB
Rust
478 lines
15 KiB
Rust
//! The inspector C API (`ghostty_surface_inspector`, `ghostty_inspector_*`;
|
|
//! the `Inspector` struct of ZIG/src/apprt/embedded.zig).
|
|
//!
|
|
//! The embedded inspector owns a Dear ImGui context, feeds it the host's
|
|
//! input events and, on Apple platforms, draws it with Metal into the
|
|
//! command buffer the host provides (`ghostty_inspector_metal_*`).
|
|
#![allow(unsafe_code)]
|
|
|
|
#[cfg(target_vendor = "apple")]
|
|
use std::time::Instant;
|
|
use std::{
|
|
ffi::{CStr, c_char, c_int, c_void},
|
|
rc::Rc,
|
|
};
|
|
|
|
use ghostty_apprt::runtime_api::RtSurface;
|
|
use ghostty_core::Surface as CoreSurfaceImpl;
|
|
use ghostty_input::{Action as KeyAction, Key, MouseButtonState, ScrollMods};
|
|
use ghostty_inspector::imgui::{Context, Key as ImguiKey, MouseButton as ImguiMouseButton};
|
|
|
|
use crate::{
|
|
embedded::{SurfaceRt, mouse_button_from_c, surface},
|
|
types::{ghostty_inspector_t, ghostty_surface_t},
|
|
};
|
|
|
|
/// The inspector state (Zig: `embedded.Inspector`). 1:1 with a surface.
|
|
pub struct Inspector {
|
|
surface: Rc<SurfaceRt>,
|
|
core: Rc<CoreSurfaceImpl>,
|
|
ctx: Context,
|
|
#[cfg(target_vendor = "apple")]
|
|
backend: Option<crate::inspector_metal::MetalBackend>,
|
|
content_scale: f64,
|
|
/// The scale the ImGui style currently has (a `Style` cannot be rebuilt
|
|
/// from Rust like Zig's `ImGuiStyle_ImGuiStyle`, so it is scaled by the
|
|
/// change of the factor).
|
|
style_scale: f32,
|
|
/// Our previous instant used to calculate delta time for animations.
|
|
#[cfg(target_vendor = "apple")]
|
|
instant: Option<Instant>,
|
|
}
|
|
|
|
impl Inspector {
|
|
fn init(surface: Rc<SurfaceRt>, core: Rc<CoreSurfaceImpl>) -> Option<Inspector> {
|
|
let mut ctx = match Context::try_create() {
|
|
Ok(c) => c,
|
|
Err(err) => {
|
|
log::error!("error initializing inspector err={err}");
|
|
return None;
|
|
}
|
|
};
|
|
if let Err(err) = ctx.set_platform_name(Some("ghostty_embedded")) {
|
|
log::warn!("unable to set the imgui platform name err={err}");
|
|
}
|
|
|
|
// Setup our core inspector
|
|
let scale = f64::from(RtSurface::get_content_scale(surface.as_ref()).x);
|
|
ghostty_inspector::setup(&mut ctx, scale);
|
|
if let Err(err) = core.activate_inspector() {
|
|
log::error!("failed to activate inspector err={err}");
|
|
}
|
|
|
|
Some(Inspector {
|
|
surface,
|
|
core,
|
|
ctx,
|
|
#[cfg(target_vendor = "apple")]
|
|
backend: None,
|
|
content_scale: 1.0,
|
|
style_scale: 1.0,
|
|
#[cfg(target_vendor = "apple")]
|
|
instant: None,
|
|
})
|
|
}
|
|
|
|
/// Stop collecting data and release the backend (Zig: `deinit`).
|
|
pub(crate) fn deinit(&mut self, _core: Option<&CoreSurfaceImpl>) {
|
|
self.core.deactivate_inspector();
|
|
#[cfg(target_vendor = "apple")]
|
|
if let Some(b) = self.backend.take() {
|
|
b.shutdown();
|
|
}
|
|
}
|
|
|
|
/// Queue a render for the next frame.
|
|
fn queue_render(&self) {
|
|
RtSurface::redraw_inspector(self.surface.as_ref());
|
|
}
|
|
|
|
fn update_content_scale(&mut self, x: f64) {
|
|
// Cache our scale because we use it for cursor position
|
|
// calculations.
|
|
self.content_scale = x;
|
|
|
|
// Scale the style appropriately.
|
|
let scale = x as f32;
|
|
let last = std::mem::replace(&mut self.style_scale, scale);
|
|
if (scale - last).abs() > f32::EPSILON && last > 0.0 {
|
|
self.ctx.style_mut().scale_all_sizes(scale / last);
|
|
}
|
|
}
|
|
|
|
fn update_size(&mut self, width: u32, height: u32) {
|
|
self.ctx
|
|
.io_mut()
|
|
.set_display_size([width as f32, height as f32]);
|
|
}
|
|
|
|
fn mouse_button(&mut self, action: MouseButtonState, button: ghostty_input::MouseButton) {
|
|
self.queue_render();
|
|
let imgui_button = match button {
|
|
ghostty_input::MouseButton::Left => ImguiMouseButton::Left,
|
|
ghostty_input::MouseButton::Middle => ImguiMouseButton::Middle,
|
|
ghostty_input::MouseButton::Right => ImguiMouseButton::Right,
|
|
_ => return, // unsupported
|
|
};
|
|
self.ctx
|
|
.io_mut()
|
|
.add_mouse_button_event(imgui_button, action == MouseButtonState::Press);
|
|
}
|
|
|
|
fn scroll(&mut self, xoff: f64, yoff: f64, mods: ScrollMods) {
|
|
self.queue_render();
|
|
|
|
// For precision scrolling (trackpads), the values are in pixels
|
|
// which scroll way too fast. Scale them down to approximate
|
|
// discrete wheel notches. imgui expects 1.0 to scroll ~5 lines of
|
|
// text.
|
|
let scale = if mods.precision { 0.1 } else { 1.0 };
|
|
self.ctx
|
|
.io_mut()
|
|
.add_mouse_wheel_event([(xoff * scale) as f32, (yoff * scale) as f32]);
|
|
}
|
|
|
|
fn cursor_pos(&mut self, x: f64, y: f64) {
|
|
self.queue_render();
|
|
self.ctx.io_mut().add_mouse_pos_event([
|
|
(x * self.content_scale) as f32,
|
|
(y * self.content_scale) as f32,
|
|
]);
|
|
}
|
|
|
|
fn focus(&mut self, focused: bool) {
|
|
self.queue_render();
|
|
self.ctx.io_mut().add_focus_event(focused);
|
|
}
|
|
|
|
fn text(&mut self, text: &str) {
|
|
self.queue_render();
|
|
self.ctx.io_mut().add_input_characters_utf8(text);
|
|
}
|
|
|
|
fn key(&mut self, action: KeyAction, key: Key, mods: ghostty_input::Mods) {
|
|
self.queue_render();
|
|
let io = self.ctx.io_mut();
|
|
|
|
// Update all our modifiers
|
|
io.add_key_event(ImguiKey::LeftShift, mods.shift);
|
|
io.add_key_event(ImguiKey::LeftCtrl, mods.ctrl);
|
|
io.add_key_event(ImguiKey::LeftAlt, mods.alt);
|
|
io.add_key_event(ImguiKey::LeftSuper, mods.super_);
|
|
|
|
// Send our keypress
|
|
if let Some(imgui_key) = ghostty_inspector::keys::imgui_key(key) {
|
|
io.add_key_event(
|
|
imgui_key,
|
|
matches!(action, KeyAction::Press | KeyAction::Repeat),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Update the delta time (Zig: `newFrame`).
|
|
#[cfg(target_vendor = "apple")]
|
|
pub(crate) fn new_frame(&mut self) {
|
|
let now = Instant::now();
|
|
let delta = self.instant.map_or(1.0 / 60.0, |prev| {
|
|
now.duration_since(prev).as_secs_f32().max(0.00001)
|
|
});
|
|
self.instant = Some(now);
|
|
self.ctx.io_mut().set_delta_time(delta);
|
|
}
|
|
|
|
/// Build one UI frame, calling the core inspector (`inspector.render`).
|
|
/// `end` finishes the frame without producing draw data.
|
|
#[cfg(target_vendor = "apple")]
|
|
pub(crate) fn build_frame(&mut self) -> &mut ghostty_inspector::imgui::Ui {
|
|
self.new_frame();
|
|
let core = Rc::clone(&self.core);
|
|
let ui = self.ctx.frame();
|
|
core.render_inspector(ui);
|
|
ui
|
|
}
|
|
|
|
#[cfg(target_vendor = "apple")]
|
|
pub(crate) fn context(&mut self) -> &mut Context {
|
|
&mut self.ctx
|
|
}
|
|
|
|
#[cfg(target_vendor = "apple")]
|
|
pub(crate) fn take_backend(&mut self) -> Option<crate::inspector_metal::MetalBackend> {
|
|
self.backend.take()
|
|
}
|
|
|
|
#[cfg(target_vendor = "apple")]
|
|
pub(crate) fn set_backend(&mut self, backend: crate::inspector_metal::MetalBackend) {
|
|
self.backend = Some(backend);
|
|
}
|
|
}
|
|
|
|
/// Borrow the inspector behind a handle.
|
|
///
|
|
/// # Safety
|
|
/// `p` must be null or a handle from `ghostty_surface_inspector` whose
|
|
/// surface is still alive and whose inspector wasn't freed.
|
|
unsafe fn inspector<'a>(p: ghostty_inspector_t) -> Option<&'a mut Inspector> {
|
|
// SAFETY: per the function contract.
|
|
unsafe { p.cast::<Inspector>().as_mut() }
|
|
}
|
|
|
|
/// Initialize the inspector instance. A surface can only have one inspector
|
|
/// at any given time, so this will return the previous inspector if it was
|
|
/// already initialized.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live surface handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_surface_inspector(ptr: ghostty_surface_t) -> ghostty_inspector_t {
|
|
// SAFETY: per the function contract.
|
|
let Some(s) = (unsafe { surface(ptr) }) else {
|
|
return std::ptr::null_mut();
|
|
};
|
|
let slot = s.inspector_slot();
|
|
if let Some(i) = slot.borrow_mut().as_mut() {
|
|
return std::ptr::from_mut::<Inspector>(&mut **i).cast::<c_void>();
|
|
}
|
|
let Some(core) = s.core_surface() else {
|
|
return std::ptr::null_mut();
|
|
};
|
|
let Some(i) = Inspector::init(Rc::clone(s.rt()), Rc::clone(core)) else {
|
|
return std::ptr::null_mut();
|
|
};
|
|
let mut boxed = Box::new(i);
|
|
let handle = std::ptr::from_mut::<Inspector>(&mut *boxed).cast::<c_void>();
|
|
*slot.borrow_mut() = Some(boxed);
|
|
handle
|
|
}
|
|
|
|
/// Free the inspector of a surface (note: takes the surface).
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live surface handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_free(ptr: ghostty_surface_t) {
|
|
// SAFETY: per the function contract.
|
|
let Some(s) = (unsafe { surface(ptr) }) else {
|
|
return;
|
|
};
|
|
let taken = s.inspector_slot().borrow_mut().take();
|
|
if let Some(mut i) = taken {
|
|
i.deinit(None);
|
|
}
|
|
}
|
|
|
|
/// Set the size of the inspector view in pixels.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_set_size(ptr: ghostty_inspector_t, w: u32, h: u32) {
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.update_size(w, h);
|
|
}
|
|
}
|
|
|
|
/// Set the content scale of the inspector view.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_set_content_scale(
|
|
ptr: ghostty_inspector_t,
|
|
x: f64,
|
|
_y: f64,
|
|
) {
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.update_content_scale(x);
|
|
}
|
|
}
|
|
|
|
/// Send a mouse button event to the inspector.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_mouse_button(
|
|
ptr: ghostty_inspector_t,
|
|
action: c_int,
|
|
button: c_int,
|
|
_mods: c_int,
|
|
) {
|
|
let state = match action {
|
|
0 => MouseButtonState::Release,
|
|
1 => MouseButtonState::Press,
|
|
_ => return,
|
|
};
|
|
let Some(button) = mouse_button_from_c(button) else {
|
|
return;
|
|
};
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.mouse_button(state, button);
|
|
}
|
|
}
|
|
|
|
/// Send a mouse position to the inspector.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_mouse_pos(ptr: ghostty_inspector_t, x: f64, y: f64) {
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.cursor_pos(x, y);
|
|
}
|
|
}
|
|
|
|
/// Send a scroll event to the inspector.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_mouse_scroll(
|
|
ptr: ghostty_inspector_t,
|
|
x: f64,
|
|
y: f64,
|
|
scroll_mods: c_int,
|
|
) {
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.scroll(x, y, ScrollMods::from_int(scroll_mods as u32 as u8));
|
|
}
|
|
}
|
|
|
|
/// Send a key event to the inspector.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_key(
|
|
ptr: ghostty_inspector_t,
|
|
action: c_int,
|
|
key: c_int,
|
|
c_mods: c_int,
|
|
) {
|
|
let action = match action {
|
|
0 => KeyAction::Release,
|
|
1 => KeyAction::Press,
|
|
2 => KeyAction::Repeat,
|
|
_ => return,
|
|
};
|
|
let Some(key) = usize::try_from(key)
|
|
.ok()
|
|
.and_then(|k| Key::ALL.get(k))
|
|
.copied()
|
|
else {
|
|
return;
|
|
};
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.key(
|
|
action,
|
|
key,
|
|
ghostty_input::Mods::from_int(c_mods as u32 as u16),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Send text input to the inspector.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` a live inspector handle; `text` NUL terminated.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_text(ptr: ghostty_inspector_t, text: *const c_char) {
|
|
if text.is_null() {
|
|
return;
|
|
}
|
|
// SAFETY: per the function contract.
|
|
let text = unsafe { CStr::from_ptr(text) }.to_string_lossy();
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.text(&text);
|
|
}
|
|
}
|
|
|
|
/// Set the focus state of the inspector.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_set_focus(ptr: ghostty_inspector_t, focused: bool) {
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) } {
|
|
i.focus(focused);
|
|
}
|
|
}
|
|
|
|
#[cfg(target_vendor = "apple")]
|
|
#[allow(clippy::wildcard_imports)]
|
|
mod metal_exports {
|
|
use super::*;
|
|
|
|
/// Initialize the inspector for a Metal backend. Takes ownership of one
|
|
/// reference to `device`.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` a live inspector handle; `device` a retained `MTLDevice`.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_metal_init(
|
|
ptr: ghostty_inspector_t,
|
|
device: *mut c_void,
|
|
) -> bool {
|
|
// SAFETY: per the function contract.
|
|
let Some(i) = (unsafe { inspector(ptr) }) else {
|
|
return false;
|
|
};
|
|
if let Some(b) = i.backend.take() {
|
|
b.shutdown();
|
|
}
|
|
// SAFETY: `device` is a retained MTLDevice whose reference we own.
|
|
match unsafe { crate::inspector_metal::MetalBackend::init(device, i.context()) } {
|
|
Ok(b) => {
|
|
i.backend = Some(b);
|
|
log::debug!("initialized metal backend");
|
|
true
|
|
}
|
|
Err(err) => {
|
|
log::warn!("failed to initialize metal backend err={err}");
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Render the inspector into the host's command buffer. Takes ownership
|
|
/// of one reference to each of `command_buffer` and `descriptor`.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` a live inspector handle; the others retained Metal objects.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_metal_render(
|
|
ptr: ghostty_inspector_t,
|
|
command_buffer: *mut c_void,
|
|
descriptor: *mut c_void,
|
|
) {
|
|
// SAFETY: per the function contract.
|
|
let Some(i) = (unsafe { inspector(ptr) }) else {
|
|
return;
|
|
};
|
|
// SAFETY: retained objects whose reference we own.
|
|
unsafe { crate::inspector_metal::render(i, command_buffer, descriptor) };
|
|
}
|
|
|
|
/// Shut the Metal backend down.
|
|
///
|
|
/// # Safety
|
|
/// `ptr` must be a live inspector handle.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn ghostty_inspector_metal_shutdown(ptr: ghostty_inspector_t) {
|
|
// SAFETY: per the function contract.
|
|
if let Some(i) = unsafe { inspector(ptr) }
|
|
&& let Some(b) = i.backend.take()
|
|
{
|
|
b.shutdown();
|
|
}
|
|
}
|
|
}
|