feat(apprt-gtk): port the GTK4/libadwaita app runtime
Ports ZIG/src/apprt/gtk into ghostty-apprt-gtk: every GObject class (application, window, tab, split tree, surface + scrolled window, child-exited banner, search/key-state/resize overlays, command palette, global shortcuts, all dialogs, config wrapper) as glib subclasses with the same GType names, properties, signals and template children; GDK key translation; X11 and Wayland window-protocol helpers (blur, CSD, quick terminal via gtk4-layer-shell); single-instance D-Bus IPC; gsettings; cgroup scope creation. build.rs compiles Ghostty's blueprints with blueprint-compiler and bundles UI, CSS and icons into a gresource, as Ghostty's build does. The core is not ported yet, so the crate defines the boundary it needs in src/core.rs (CoreApp, CoreSurface, RtApp, RtSurface, Waker); the spec is in docs/rewrite/notes/ghostty-apprt-gtk.md. ghostty-core will implement these traits (the crate no longer depends on core to avoid a cycle). examples/demo.rs runs the app with a dummy core that clears the GLArea; it launches on Wayland without GTK criticals. Stubs/open items: toggle_visibility, float_window, secure_input and close_all_windows return false; inspector is a no-op; X11/Wayland protocol code is untested against real compositors; gtk4-rs doesn't expose the GLArea framebuffer id, so the renderer must query it. Also registers ghostty-apprt and ghostty-cli in workspace.dependencies. Test Plan: - cargo test -p ghostty-apprt-gtk (36 tests) - DEMO_QUIT_AFTER_MS=3000 cargo run -p ghostty-apprt-gtk --example demo
This commit is contained in:
1 parent
6675db1eeb
commit
69d823cbc7
80 files changed
+17271
-4
No files matched your search
@@ -0,0 +1,138 @@
|
||||
//! GAction helpers (port of `apprt/gtk/ext/actions.zig`).
|
||||
|
||||
use gtk::gio;
|
||||
use gtk::glib;
|
||||
use gtk::glib::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
|
||||
fn g_action_name_is_valid(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& name
|
||||
.bytes()
|
||||
.all(|c| c == b'-' || c == b'.' || c.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
/// Callback signature: `(action, parameter, self)`.
|
||||
pub type Callback<T> = fn(&gio::SimpleAction, Option<&glib::Variant>, &T);
|
||||
|
||||
/// An action definition for objects of type `T`.
|
||||
pub struct Action<T> {
|
||||
pub name: &'static str,
|
||||
pub callback: Callback<T>,
|
||||
pub parameter_type: Option<glib::VariantType>,
|
||||
pub state: Option<glib::Variant>,
|
||||
}
|
||||
|
||||
impl<T> Action<T> {
|
||||
pub fn new(
|
||||
name: &'static str,
|
||||
callback: Callback<T>,
|
||||
parameter_type: Option<glib::VariantType>,
|
||||
) -> Self {
|
||||
assert!(g_action_name_is_valid(name));
|
||||
Self {
|
||||
name,
|
||||
callback,
|
||||
parameter_type,
|
||||
state: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stateful(
|
||||
name: &'static str,
|
||||
callback: Callback<T>,
|
||||
parameter_type: Option<glib::VariantType>,
|
||||
state: glib::Variant,
|
||||
) -> Self {
|
||||
assert!(g_action_name_is_valid(name));
|
||||
Self {
|
||||
name,
|
||||
callback,
|
||||
parameter_type,
|
||||
state: Some(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the actions to `obj` itself (it must be an `ActionMap`).
|
||||
pub fn add<T: IsA<gio::ActionMap>>(obj: &T, actions: &[Action<T>]) {
|
||||
add_to_map(obj, obj.upcast_ref::<gio::ActionMap>(), actions);
|
||||
}
|
||||
|
||||
/// Add the actions to `map`; callbacks receive `obj` (held weakly, so no
|
||||
/// reference cycle is created).
|
||||
pub fn add_to_map<T: ObjectType>(obj: &T, map: &impl IsA<gio::ActionMap>, actions: &[Action<T>]) {
|
||||
for entry in actions {
|
||||
assert!(g_action_name_is_valid(entry.name));
|
||||
let action = if let Some(state) = &entry.state {
|
||||
gio::SimpleAction::new_stateful(entry.name, entry.parameter_type.as_deref(), state)
|
||||
} else {
|
||||
gio::SimpleAction::new(entry.name, entry.parameter_type.as_deref())
|
||||
};
|
||||
let weak = obj.downgrade();
|
||||
let callback = entry.callback;
|
||||
action.connect_activate(move |a, p| {
|
||||
if let Some(obj) = weak.upgrade() {
|
||||
callback(a, p, &obj);
|
||||
}
|
||||
});
|
||||
map.add_action(&action);
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect the actions into a group inserted into the widget under `name`.
|
||||
pub fn add_as_group<T: IsA<gtk::Widget>>(
|
||||
obj: &T,
|
||||
name: &str,
|
||||
actions: &[Action<T>],
|
||||
) -> gio::SimpleActionGroup {
|
||||
assert!(g_action_name_is_valid(name));
|
||||
|
||||
// We're a plain widget that doesn't implement ActionGroup directly.
|
||||
let group = gio::SimpleActionGroup::new();
|
||||
add_to_map(obj, &group, actions);
|
||||
obj.insert_action_group(name, Some(&group));
|
||||
group
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn g_action_name_is_valid_test() {
|
||||
assert!(g_action_name_is_valid("ring-bell"));
|
||||
assert!(!g_action_name_is_valid("ring_bell"));
|
||||
assert!(!g_action_name_is_valid(""));
|
||||
}
|
||||
|
||||
fn cb(_: &gio::SimpleAction, v: Option<&glib::Variant>, b: >k::Box) {
|
||||
if let Some(v) = v.and_then(glib::Variant::get::<i32>) {
|
||||
b.set_spacing(v);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_actions_to_an_object() {
|
||||
// This test requires a connection to an active display environment.
|
||||
if gtk::init().is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let b = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
let actions = [Action::new(
|
||||
"test",
|
||||
cb,
|
||||
Some(i32::static_variant_type().into_owned()),
|
||||
)];
|
||||
let _group = add_as_group(&b, "test", &actions);
|
||||
|
||||
let expected = 12345;
|
||||
assert!(
|
||||
b.activate_action("test.test", Some(&expected.to_variant()))
|
||||
.is_ok()
|
||||
);
|
||||
while glib::MainContext::default().iteration(false) {}
|
||||
assert_eq!(b.spacing(), expected);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//! Extensions/helpers for GTK objects (port of `apprt/gtk/ext.zig`).
|
||||
//!
|
||||
//! `boxedCopy`/`boxedFree`/`listFind` are subsumed by glib-rs (`Clone`,
|
||||
//! `Drop`, iterators) and are not ported.
|
||||
|
||||
pub mod actions;
|
||||
pub mod slice;
|
||||
|
||||
pub use slice::StringList;
|
||||
|
||||
use gtk::glib;
|
||||
use gtk::glib::prelude::*;
|
||||
use gtk::prelude::*;
|
||||
|
||||
/// Get the first ancestor of `widget` of type `T`.
|
||||
pub fn get_ancestor<T: IsA<gtk::Widget>>(widget: &impl IsA<gtk::Widget>) -> Option<T> {
|
||||
widget.ancestor(T::static_type()).and_downcast::<T>()
|
||||
}
|
||||
|
||||
/// Whether the value holds (a subtype of) the given type.
|
||||
pub fn g_value_holds(value: Option<&glib::Value>, g_type: glib::Type) -> bool {
|
||||
let Some(value) = value else { return false };
|
||||
value.type_() == g_type || value.type_().is_a(g_type)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn g_value_holds_test() {
|
||||
let v = 5i32.to_value();
|
||||
assert!(g_value_holds(Some(&v), glib::Type::I32));
|
||||
assert!(!g_value_holds(Some(&v), glib::Type::STRING));
|
||||
assert!(!g_value_holds(None, glib::Type::I32));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Port of `apprt/gtk/ext/slice.zig`: a boxed list of strings.
|
||||
|
||||
use gtk::glib;
|
||||
|
||||
/// A boxed list of strings (GType `GhosttyStringList`). Copy/free semantics
|
||||
/// are provided by `Clone`/`Drop`.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, glib::Boxed)]
|
||||
#[boxed_type(name = "GhosttyStringList")]
|
||||
pub struct StringList {
|
||||
pub strings: Vec<String>,
|
||||
}
|
||||
|
||||
impl StringList {
|
||||
pub fn create<S: AsRef<str>>(strings: &[S]) -> Self {
|
||||
Self {
|
||||
strings: strings.iter().map(|s| s.as_ref().to_owned()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gtk::glib::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn string_list_create_and_destroy() {
|
||||
let list = StringList::create(&["hello", "world"]);
|
||||
assert_eq!(list.strings.len(), 2);
|
||||
assert_eq!(list.strings[0], "hello");
|
||||
assert_eq!(list.strings[1], "world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_list_create_empty_list() {
|
||||
let list = StringList::create::<&str>(&[]);
|
||||
assert_eq!(list.strings.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_list_boxed_copy_and_free() {
|
||||
let original = StringList::create(&["foo", "bar", "baz"]);
|
||||
let value = original.to_value();
|
||||
let copied = value.get::<StringList>().expect("boxed value");
|
||||
assert_eq!(copied.strings, ["foo", "bar", "baz"]);
|
||||
assert_ne!(original.strings.as_ptr(), copied.strings.as_ptr());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user