summaryrefslogtreecommitdiff
path: root/lib/astal/gtk3/src
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-10-14 16:45:03 +0000
committerAylur <[email protected]>2024-10-14 16:45:03 +0000
commit9fab13452a26ed55c01047d4225f699f43bba20d (patch)
tree8dc3097994e8664572c3a94a62257604bdaa1f8d /lib/astal/gtk3/src
parent6a8c41cd1d5e218d0dacffb836fdd7d4ec6333dd (diff)
feat: Astal3
Diffstat (limited to 'lib/astal/gtk3/src')
-rw-r--r--lib/astal/gtk3/src/application.vala217
-rw-r--r--lib/astal/gtk3/src/config.vala.in6
-rw-r--r--lib/astal/gtk3/src/idle-inhibit.c114
-rw-r--r--lib/astal/gtk3/src/idle-inhibit.h22
-rw-r--r--lib/astal/gtk3/src/meson.build120
-rw-r--r--lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi13
-rw-r--r--lib/astal/gtk3/src/widget/box.vala50
-rw-r--r--lib/astal/gtk3/src/widget/button.vala99
-rw-r--r--lib/astal/gtk3/src/widget/centerbox.vala52
-rw-r--r--lib/astal/gtk3/src/widget/circularprogress.vala180
-rw-r--r--lib/astal/gtk3/src/widget/eventbox.vala64
-rw-r--r--lib/astal/gtk3/src/widget/icon.vala105
-rw-r--r--lib/astal/gtk3/src/widget/label.vala18
-rw-r--r--lib/astal/gtk3/src/widget/levelbar.vala13
-rw-r--r--lib/astal/gtk3/src/widget/overlay.vala57
-rw-r--r--lib/astal/gtk3/src/widget/scrollable.vala40
-rw-r--r--lib/astal/gtk3/src/widget/slider.vala71
-rw-r--r--lib/astal/gtk3/src/widget/stack.vala26
-rw-r--r--lib/astal/gtk3/src/widget/widget.vala157
-rw-r--r--lib/astal/gtk3/src/widget/window.vala246
20 files changed, 1670 insertions, 0 deletions
diff --git a/lib/astal/gtk3/src/application.vala b/lib/astal/gtk3/src/application.vala
new file mode 100644
index 0000000..8539aa0
--- /dev/null
+++ b/lib/astal/gtk3/src/application.vala
@@ -0,0 +1,217 @@
+[DBus (name="io.Astal.Application")]
+public class Astal.Application : Gtk.Application, AstalIO.Application {
+ private List<Gtk.CssProvider> css_providers = new List<Gtk.CssProvider>();
+ private SocketService service;
+ private DBusConnection conn;
+ private string socket_path { get; set; }
+ private string _instance_name;
+
+ [DBus (visible=false)]
+ public signal void monitor_added(Gdk.Monitor monitor);
+
+ [DBus (visible=false)]
+ public signal void monitor_removed(Gdk.Monitor monitor);
+
+ [DBus (visible=false)]
+ public signal void window_toggled(Gtk.Window window);
+
+ [DBus (visible=false)]
+ public List<weak Gdk.Monitor> monitors {
+ owned get {
+ var display = Gdk.Display.get_default();
+ var list = new List<weak Gdk.Monitor>();
+ for (var i = 0; i <= display.get_n_monitors(); ++i) {
+ var mon = display.get_monitor(i);
+ if (mon != null) {
+ list.append(mon);
+ }
+ }
+ return list;
+ }
+ }
+
+ [DBus (visible=false)]
+ public string instance_name {
+ owned get { return _instance_name; }
+ construct set {
+ application_id = "io.Astal." + value;
+ _instance_name = value;
+ }
+ }
+
+ [DBus (visible=false)]
+ public List<Gtk.Window> windows {
+ get { return get_windows(); }
+ }
+
+ [DBus (visible=false)]
+ public Gtk.Settings settings {
+ get { return Gtk.Settings.get_default(); }
+ }
+
+ [DBus (visible=false)]
+ public Gdk.Screen screen {
+ get { return Gdk.Screen.get_default(); }
+ }
+
+ [DBus (visible=false)]
+ public string gtk_theme {
+ owned get { return settings.gtk_theme_name; }
+ set { settings.gtk_theme_name = value; }
+ }
+
+ [DBus (visible=false)]
+ public string icon_theme {
+ owned get { return settings.gtk_icon_theme_name; }
+ set { settings.gtk_icon_theme_name = value; }
+ }
+
+ [DBus (visible=false)]
+ public string cursor_theme {
+ owned get { return settings.gtk_cursor_theme_name; }
+ set { settings.gtk_cursor_theme_name = value; }
+ }
+
+ [DBus (visible=false)]
+ public void reset_css() {
+ foreach(var provider in css_providers) {
+ Gtk.StyleContext.remove_provider_for_screen(screen, provider);
+ }
+ css_providers = new List<Gtk.CssProvider>();
+ }
+
+ public void inspector() throws DBusError, IOError {
+ Gtk.Window.set_interactive_debugging(true);
+ }
+
+ [DBus (visible=false)]
+ public Gtk.Window? get_window(string name) {
+ foreach(var win in windows) {
+ if (win.name == name)
+ return win;
+ }
+
+ critical("no window with name \"%s\"".printf(name));
+ return null;
+ }
+
+ public void toggle_window(string window) throws Error {
+ var win = get_window(window);
+ if (win != null) {
+ win.visible = !win.visible;
+ } else {
+ throw new IOError.FAILED("window not found");
+ }
+ }
+
+ [DBus (visible=false)]
+ public void apply_css(string style, bool reset = false) {
+ var provider = new Gtk.CssProvider();
+
+ if (reset)
+ reset_css();
+
+ try {
+ if (FileUtils.test(style, FileTest.EXISTS))
+ provider.load_from_path(style);
+ else
+ provider.load_from_data(style);
+ } catch (Error err) {
+ critical(err.message);
+ }
+
+ Gtk.StyleContext.add_provider_for_screen(
+ screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER);
+
+ css_providers.append(provider);
+ }
+
+ [DBus (visible=false)]
+ public void add_icons(string? path) {
+ if (path != null) {
+ Gtk.IconTheme.get_default().prepend_search_path(path);
+ }
+ }
+
+ [DBus (visible=false)]
+ public virtual void request(string msg, SocketConnection conn) {
+ AstalIO.write_sock.begin(conn, @"missing response implementation on $application_id");
+ }
+
+ /**
+ * should be called before `run()`
+ * the return value indicates if instance is already running
+ */
+ [DBus (visible=false)]
+ public void acquire_socket() {
+ try {
+ service = AstalIO.acquire_socket(this);
+
+ Bus.own_name(
+ BusType.SESSION,
+ "io.Astal." + instance_name,
+ BusNameOwnerFlags.NONE,
+ (conn) => {
+ try {
+ this.conn = conn;
+ conn.register_object("/io/Astal/Application", this);
+ } catch (Error err) {
+ critical(err.message);
+ }
+ },
+ () => {},
+ () => {}
+ );
+ } catch (Error err) {
+ critical("could not acquire socket %s\n", application_id);
+ critical(err.message);
+ }
+ }
+
+ public new void quit() throws DBusError, IOError {
+ if (service != null) {
+ if (FileUtils.test(socket_path, GLib.FileTest.EXISTS)){
+ try {
+ File.new_for_path(socket_path).delete(null);
+ } catch (Error err) {
+ warning(err.message);
+ }
+ }
+ }
+
+ base.quit();
+ }
+
+ construct {
+ if (instance_name == null)
+ instance_name = "astal";
+
+ activate.connect(() => {
+ var display = Gdk.Display.get_default();
+ display.monitor_added.connect((mon) => {
+ monitor_added(mon);
+ notify_property("monitors");
+ });
+ display.monitor_removed.connect((mon) => {
+ monitor_removed(mon);
+ notify_property("monitors");
+ });
+ });
+
+ window_added.connect((window) => {
+ ulong id1, id2;
+ id1 = window.notify["visible"].connect(() => window_toggled(window));
+ id2 = window_removed.connect((removed) => {
+ if (removed == window) {
+ window.disconnect(id1);
+ this.disconnect(id2);
+ }
+ });
+ });
+
+ shutdown.connect(() => { try { quit(); } catch(Error err) {} });
+ Unix.signal_add(1, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH);
+ Unix.signal_add(2, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH);
+ Unix.signal_add(15, () => { try { quit(); } catch(Error err) {} }, Priority.HIGH);
+ }
+}
diff --git a/lib/astal/gtk3/src/config.vala.in b/lib/astal/gtk3/src/config.vala.in
new file mode 100644
index 0000000..88bfe9c
--- /dev/null
+++ b/lib/astal/gtk3/src/config.vala.in
@@ -0,0 +1,6 @@
+namespace Astal {
+ public const int MAJOR_VERSION = @MAJOR_VERSION@;
+ public const int MINOR_VERSION = @MINOR_VERSION@;
+ public const int MICRO_VERSION = @MICRO_VERSION@;
+ public const string VERSION = "@VERSION@";
+}
diff --git a/lib/astal/gtk3/src/idle-inhibit.c b/lib/astal/gtk3/src/idle-inhibit.c
new file mode 100644
index 0000000..48f2471
--- /dev/null
+++ b/lib/astal/gtk3/src/idle-inhibit.c
@@ -0,0 +1,114 @@
+#include "idle-inhibit.h"
+
+#include <gdk/gdk.h>
+#include <gdk/gdkwayland.h>
+#include <gio/gio.h>
+#include <glib-object.h>
+#include <glib.h>
+#include <gtk/gtk.h>
+#include <wayland-client-protocol.h>
+#include <wayland-client.h>
+
+#include "idle-inhibit-unstable-v1-client.h"
+
+struct _AstalInhibitManager {
+ GObject parent_instance;
+};
+
+typedef struct {
+ gboolean init;
+ struct wl_registry* wl_registry;
+ struct wl_display* display;
+ struct zwp_idle_inhibit_manager_v1* idle_inhibit_manager;
+} AstalInhibitManagerPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE(AstalInhibitManager, astal_inhibit_manager, G_TYPE_OBJECT)
+
+AstalInhibitor* astal_inhibit_manager_inhibit(AstalInhibitManager* self, GtkWindow* window) {
+ AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self);
+ g_assert_true(priv->init);
+ GdkWindow* gdk_window = gtk_widget_get_window(GTK_WIDGET(window));
+ struct wl_surface* surface = gdk_wayland_window_get_wl_surface(gdk_window);
+ return zwp_idle_inhibit_manager_v1_create_inhibitor(priv->idle_inhibit_manager, surface);
+}
+
+static void global_registry_handler(void* data, struct wl_registry* registry, uint32_t id,
+ const char* interface, uint32_t version) {
+ AstalInhibitManager* self = ASTAL_INHIBIT_MANAGER(data);
+ AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self);
+
+ if (strcmp(interface, zwp_idle_inhibit_manager_v1_interface.name) == 0) {
+ priv->idle_inhibit_manager =
+ wl_registry_bind(registry, id, &zwp_idle_inhibit_manager_v1_interface, 1);
+ }
+}
+
+static void global_registry_remover(void* data, struct wl_registry* registry, uint32_t id) {
+ // neither inhibit_manager nor inhibitor is going to be removed by the compositor, so we don't
+ // need do anything here.
+}
+
+static const struct wl_registry_listener registry_listener = {global_registry_handler,
+ global_registry_remover};
+
+static gboolean astal_inhibit_manager_wayland_init(AstalInhibitManager* self) {
+ AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self);
+
+ if (priv->init) return TRUE;
+
+ GdkDisplay* gdk_display = gdk_display_get_default();
+ priv->display = gdk_wayland_display_get_wl_display(gdk_display);
+
+ priv->wl_registry = wl_display_get_registry(priv->display);
+ wl_registry_add_listener(priv->wl_registry, &registry_listener, self);
+
+ wl_display_roundtrip(priv->display);
+
+ if (priv->idle_inhibit_manager == NULL) {
+ g_critical("Can not connect idle inhibitor protocol");
+ return FALSE;
+ }
+
+ priv->init = TRUE;
+ return TRUE;
+}
+
+AstalInhibitManager* astal_inhibit_manager_get_default() {
+ static AstalInhibitManager* self = NULL;
+
+ if (self == NULL) {
+ self = g_object_new(ASTAL_TYPE_INHIBIT_MANAGER, NULL);
+ if (!astal_inhibit_manager_wayland_init(self)) {
+ g_object_unref(self);
+ self = NULL;
+ }
+ }
+
+ return self;
+}
+
+static void astal_inhibit_manager_init(AstalInhibitManager* self) {
+ AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self);
+ priv->init = FALSE;
+ priv->display = NULL;
+ priv->wl_registry = NULL;
+ priv->idle_inhibit_manager = NULL;
+}
+
+static void astal_inhibit_manager_finalize(GObject* object) {
+ AstalInhibitManager* self = ASTAL_INHIBIT_MANAGER(object);
+ AstalInhibitManagerPrivate* priv = astal_inhibit_manager_get_instance_private(self);
+
+ if (priv->display != NULL) wl_display_roundtrip(priv->display);
+
+ if (priv->wl_registry != NULL) wl_registry_destroy(priv->wl_registry);
+ if (priv->idle_inhibit_manager != NULL)
+ zwp_idle_inhibit_manager_v1_destroy(priv->idle_inhibit_manager);
+
+ G_OBJECT_CLASS(astal_inhibit_manager_parent_class)->finalize(object);
+}
+
+static void astal_inhibit_manager_class_init(AstalInhibitManagerClass* class) {
+ GObjectClass* object_class = G_OBJECT_CLASS(class);
+ object_class->finalize = astal_inhibit_manager_finalize;
+}
diff --git a/lib/astal/gtk3/src/idle-inhibit.h b/lib/astal/gtk3/src/idle-inhibit.h
new file mode 100644
index 0000000..5e9a3ab
--- /dev/null
+++ b/lib/astal/gtk3/src/idle-inhibit.h
@@ -0,0 +1,22 @@
+#ifndef ASTAL_IDLE_INHIBITOR_H
+#define ASTAL_IDLE_INHIBITOR_H
+
+#include <glib-object.h>
+#include <gtk/gtk.h>
+
+#include "idle-inhibit-unstable-v1-client.h"
+
+G_BEGIN_DECLS
+
+#define ASTAL_TYPE_INHIBIT_MANAGER (astal_inhibit_manager_get_type())
+
+G_DECLARE_FINAL_TYPE(AstalInhibitManager, astal_inhibit_manager, ASTAL, INHIBIT_MANAGER, GObject)
+
+typedef struct zwp_idle_inhibitor_v1 AstalInhibitor;
+
+AstalInhibitManager* astal_inhibit_manager_get_default();
+AstalInhibitor* astal_inhibit_manager_inhibit(AstalInhibitManager* self, GtkWindow* window);
+
+G_END_DECLS
+
+#endif // !ASTAL_IDLE_INHIBITOR_H
diff --git a/lib/astal/gtk3/src/meson.build b/lib/astal/gtk3/src/meson.build
new file mode 100644
index 0000000..c8c7df2
--- /dev/null
+++ b/lib/astal/gtk3/src/meson.build
@@ -0,0 +1,120 @@
+version_split = meson.project_version().split('.')
+api_version = version_split[0] + '.' + version_split[1]
+gir = 'Astal-' + api_version + '.gir'
+typelib = 'Astal-' + api_version + '.typelib'
+
+vapi_dir = meson.current_source_dir() / 'vapi'
+add_project_arguments(['--vapidir', vapi_dir], language: 'vala')
+
+config = configure_file(
+ input: 'config.vala.in',
+ output: 'config.vala',
+ configuration: {
+ 'VERSION': meson.project_version(),
+ 'MAJOR_VERSION': version_split[0],
+ 'MINOR_VERSION': version_split[1],
+ 'MICRO_VERSION': version_split[2],
+ },
+)
+
+pkgconfig_deps = [
+ dependency('astal-io-0.1'),
+ dependency('glib-2.0'),
+ dependency('gio-unix-2.0'),
+ dependency('gobject-2.0'),
+ dependency('gio-2.0'),
+ dependency('gtk+-3.0'),
+ dependency('gdk-pixbuf-2.0'),
+ dependency('gtk-layer-shell-0'),
+ dependency('wayland-client'),
+]
+
+deps = pkgconfig_deps + meson.get_compiler('c').find_library('m')
+
+wayland_protos = dependency('wayland-protocols')
+wayland_scanner = find_program('wayland-scanner')
+
+wl_protocol_dir = wayland_protos.get_variable(pkgconfig: 'pkgdatadir')
+
+gen_client_header = generator(
+ wayland_scanner,
+ output: ['@[email protected]'],
+ arguments: ['-c', 'client-header', '@INPUT@', '@BUILD_DIR@/@[email protected]'],
+)
+
+gen_private_code = generator(
+ wayland_scanner,
+ output: ['@[email protected]'],
+ arguments: ['-c', 'private-code', '@INPUT@', '@BUILD_DIR@/@[email protected]'],
+)
+
+protocols = [
+ join_paths(wl_protocol_dir, 'unstable/idle-inhibit/idle-inhibit-unstable-v1.xml'),
+]
+
+client_protocol_srcs = []
+
+foreach protocol : protocols
+ client_header = gen_client_header.process(protocol)
+ code = gen_private_code.process(protocol)
+ client_protocol_srcs += [client_header, code]
+endforeach
+
+sources = [
+ config,
+ 'widget/box.vala',
+ 'widget/button.vala',
+ 'widget/centerbox.vala',
+ 'widget/circularprogress.vala',
+ 'widget/eventbox.vala',
+ 'widget/icon.vala',
+ 'widget/label.vala',
+ 'widget/levelbar.vala',
+ 'widget/overlay.vala',
+ 'widget/scrollable.vala',
+ 'widget/slider.vala',
+ 'widget/stack.vala',
+ 'widget/widget.vala',
+ 'widget/window.vala',
+ 'application.vala',
+ 'idle-inhibit.h',
+ 'idle-inhibit.c',
+] + client_protocol_srcs
+
+lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_args: ['--pkg', 'AstalInhibitManager'],
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ vala_gir: gir,
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true, true],
+)
+
+import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: pkgconfig_deps,
+ install_dir: libdir / 'pkgconfig',
+)
+
+custom_target(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', libdir / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ depends: lib,
+ install: true,
+ install_dir: libdir / 'girepository-1.0',
+)
diff --git a/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi b/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi
new file mode 100644
index 0000000..6232a3c
--- /dev/null
+++ b/lib/astal/gtk3/src/vapi/AstalInhibitManager.vapi
@@ -0,0 +1,13 @@
+[CCode (cprefix = "Astal", gir_namespace = "Astal", lower_case_cprefix = "astal_")]
+namespace Astal {
+ [CCode (cheader_filename = "idle-inhibit.h", type_id = "astal_idle_inhibit_manager_get_type()")]
+ public class InhibitManager : GLib.Object {
+ public static unowned InhibitManager? get_default();
+ public Inhibitor inhibit (Gtk.Window window);
+ }
+
+ [CCode (cheader_filename = "idle-inhibit.h", free_function = "zwp_idle_inhibitor_v1_destroy")]
+ [Compact]
+ public class Inhibitor {
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/box.vala b/lib/astal/gtk3/src/widget/box.vala
new file mode 100644
index 0000000..d23a799
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/box.vala
@@ -0,0 +1,50 @@
+public class Astal.Box : Gtk.Box {
+ [CCode (notify = false)]
+ public bool vertical {
+ get { return orientation == Gtk.Orientation.VERTICAL; }
+ set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
+ }
+
+ public List<weak Gtk.Widget> children {
+ set { _set_children(value); }
+ owned get { return get_children(); }
+ }
+
+ public new Gtk.Widget child {
+ owned get { return _get_child(); }
+ set { _set_child(value); }
+ }
+
+ construct {
+ notify["orientation"].connect(() => {
+ notify_property("vertical");
+ });
+ }
+
+ private void _set_child(Gtk.Widget child) {
+ var list = new List<weak Gtk.Widget>();
+ list.append(child);
+ _set_children(list);
+ }
+
+ private Gtk.Widget? _get_child() {
+ foreach(var child in get_children())
+ return child;
+
+ return null;
+ }
+
+ private void _set_children(List<weak Gtk.Widget> arr) {
+ foreach(var child in get_children()) {
+ remove(child);
+ }
+
+ foreach(var child in arr)
+ add(child);
+ }
+
+ public Box(bool vertical, List<weak Gtk.Widget> children) {
+ this.vertical = vertical;
+ _set_children(children);
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/button.vala b/lib/astal/gtk3/src/widget/button.vala
new file mode 100644
index 0000000..bc10577
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/button.vala
@@ -0,0 +1,99 @@
+public class Astal.Button : Gtk.Button {
+ public signal void hover (HoverEvent event);
+ public signal void hover_lost (HoverEvent event);
+ public signal void click (ClickEvent event);
+ public signal void click_release (ClickEvent event);
+ public signal void scroll (ScrollEvent event);
+
+ construct {
+ add_events(Gdk.EventMask.SCROLL_MASK);
+ add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK);
+
+ enter_notify_event.connect((self, event) => {
+ hover(HoverEvent(event) { lost = false });
+ });
+
+ leave_notify_event.connect((self, event) => {
+ hover_lost(HoverEvent(event) { lost = true });
+ });
+
+ button_press_event.connect((event) => {
+ click(ClickEvent(event) { release = false });
+ });
+
+ button_release_event.connect((event) => {
+ click_release(ClickEvent(event) { release = true });
+ });
+
+ scroll_event.connect((event) => {
+ scroll(ScrollEvent(event));
+ });
+ }
+}
+
+public enum Astal.MouseButton {
+ PRIMARY = 1,
+ MIDDLE = 2,
+ SECONDARY = 3,
+ BACK = 4,
+ FORWARD = 5,
+}
+
+// these structs are here because gjs converts every event
+// into a union Gdk.Event, which cannot be destructured
+// and are not as convinent to work with as a struct
+public struct Astal.ClickEvent {
+ bool release;
+ uint time;
+ double x;
+ double y;
+ Gdk.ModifierType modifier;
+ MouseButton button;
+
+ public ClickEvent(Gdk.EventButton event) {
+ this.time = event.time;
+ this.x = event.x;
+ this.y = event.y;
+ this.button = (MouseButton)event.button;
+ this.modifier = event.state;
+ }
+}
+
+public struct Astal.HoverEvent {
+ bool lost;
+ uint time;
+ double x;
+ double y;
+ Gdk.ModifierType modifier;
+ Gdk.CrossingMode mode;
+ Gdk.NotifyType detail;
+
+ public HoverEvent(Gdk.EventCrossing event) {
+ this.time = event.time;
+ this.x = event.x;
+ this.y = event.y;
+ this.modifier = event.state;
+ this.mode = event.mode;
+ this.detail = event.detail;
+ }
+}
+
+public struct Astal.ScrollEvent {
+ uint time;
+ double x;
+ double y;
+ Gdk.ModifierType modifier;
+ Gdk.ScrollDirection direction;
+ double delta_x;
+ double delta_y;
+
+ public ScrollEvent(Gdk.EventScroll event) {
+ this.time = event.time;
+ this.x = event.x;
+ this.y = event.y;
+ this.modifier = event.state;
+ this.direction = event.direction;
+ this.delta_x = event.delta_x;
+ this.delta_y = event.delta_y;
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/centerbox.vala b/lib/astal/gtk3/src/widget/centerbox.vala
new file mode 100644
index 0000000..89bf50b
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/centerbox.vala
@@ -0,0 +1,52 @@
+public class Astal.CenterBox : Gtk.Box {
+ [CCode (notify = false)]
+ public bool vertical {
+ get { return orientation == Gtk.Orientation.VERTICAL; }
+ set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
+ }
+
+ construct {
+ notify["orientation"].connect(() => {
+ notify_property("vertical");
+ });
+ }
+
+ static construct {
+ set_css_name("centerbox");
+ }
+
+ private Gtk.Widget _start_widget;
+ public Gtk.Widget start_widget {
+ get { return _start_widget; }
+ set {
+ if (_start_widget != null)
+ remove(_start_widget);
+
+ if (value != null)
+ pack_start(value, true, true, 0);
+ }
+ }
+
+ private Gtk.Widget _end_widget;
+ public Gtk.Widget end_widget {
+ get { return _end_widget; }
+ set {
+ if (_end_widget != null)
+ remove(_end_widget);
+
+ if (value != null)
+ pack_end(value, true, true, 0);
+ }
+ }
+
+ public Gtk.Widget center_widget {
+ get { return get_center_widget(); }
+ set {
+ if (center_widget != null)
+ remove(center_widget);
+
+ if (value != null)
+ set_center_widget(value);
+ }
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/circularprogress.vala b/lib/astal/gtk3/src/widget/circularprogress.vala
new file mode 100644
index 0000000..dd7c97b
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/circularprogress.vala
@@ -0,0 +1,180 @@
+public class Astal.CircularProgress : Gtk.Bin {
+ public double start_at { get; set; }
+ public double end_at { get; set; }
+ public double value { get; set; }
+ public bool inverted { get; set; }
+ public bool rounded { get; set; }
+
+ construct {
+ notify["start-at"].connect(queue_draw);
+ notify["end-at"].connect(queue_draw);
+ notify["value"].connect(queue_draw);
+ notify["inverted"].connect(queue_draw);
+ notify["rounded"].connect(queue_draw);
+ notify["child"].connect(queue_draw);
+ }
+
+ static construct {
+ set_css_name("circular-progress");
+ }
+
+ public override void get_preferred_height(out int minh, out int nath) {
+ var val = get_style_context().get_property("min-height", Gtk.StateFlags.NORMAL);
+ if (val.get_int() <= 0) {
+ minh = 40;
+ nath = 40;
+ }
+
+ minh = val.get_int();
+ nath = val.get_int();
+ }
+
+ public override void get_preferred_width(out int minw, out int natw) {
+ var val = get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL);
+ if (val.get_int() <= 0) {
+ minw = 40;
+ natw = 40;
+ }
+
+ minw = val.get_int();
+ natw = val.get_int();
+ }
+
+ private double to_radian(double percentage) {
+ percentage = Math.floor(percentage * 100);
+ return (percentage / 100) * (2 * Math.PI);
+ }
+
+ private bool is_full_circle(double start, double end, double epsilon = 1e-10) {
+ // Ensure that start and end are between 0 and 1
+ start = (start % 1 + 1) % 1;
+ end = (end % 1 + 1) % 1;
+
+ // Check if the difference between start and end is close to 1
+ return Math.fabs(start - end) <= epsilon;
+ }
+
+ private double scale_arc_value(double start, double end, double value) {
+ // Ensure that start and end are between 0 and 1
+ start = (start % 1 + 1) % 1;
+ end = (end % 1 + 1) % 1;
+
+ // Calculate the length of the arc
+ var arc_length = end - start;
+ if (arc_length < 0)
+ arc_length += 1; // Adjust for circular representation
+
+ // Calculate the position on the arc based on the percentage value
+ var scaled = arc_length + value;
+
+ // Ensure the position is between 0 and 1
+ return (scaled % 1 + 1) % 1;
+ }
+
+ private double min(double[] arr) {
+ double min = arr[0];
+ foreach(var i in arr)
+ if (min > i) min = i;
+ return min;
+ }
+
+ private double max(double[] arr) {
+ double max = arr[0];
+ foreach(var i in arr)
+ if (max < i) max = i;
+ return max;
+ }
+
+ public override bool draw(Cairo.Context cr) {
+ Gtk.Allocation allocation;
+ get_allocation(out allocation);
+
+ var styles = get_style_context();
+ var width = allocation.width;
+ var height = allocation.height;
+ var thickness = styles.get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
+ var margin = styles.get_margin(Gtk.StateFlags.NORMAL);
+ var fg = styles.get_color(Gtk.StateFlags.NORMAL);
+ var bg = styles.get_background_color(Gtk.StateFlags.NORMAL);
+
+ var bg_stroke = thickness + min({margin.bottom, margin.top, margin.left, margin.right});
+ var fg_stroke = thickness;
+ var radius = min({width, height}) / 2.0 - max({bg_stroke, fg_stroke}) / 2.0;
+ var center_x = width / 2;
+ var center_y = height / 2;
+
+ var start_background = to_radian(start_at);
+ var end_background = to_radian(end_at);
+ var ranged_value = value + start_at;
+
+ var is_circle = is_full_circle(this.start_at, this.end_at);
+
+ if (is_circle) {
+ // Redefine end_draw in radius to create an accurate full circle
+ end_background = start_background + 2 * Math.PI;
+ ranged_value = to_radian(value);
+ } else {
+ // Range the value for the arc shape
+ ranged_value = to_radian(scale_arc_value(
+ start_at,
+ end_at,
+ value
+ ));
+ }
+
+ double start_progress, end_progress;
+
+ if (inverted) {
+ start_progress = end_background - ranged_value;
+ end_progress = end_background;
+ } else {
+ start_progress = start_background;
+ end_progress = start_background + ranged_value;
+ }
+
+ // Draw background
+ cr.set_source_rgba(bg.red, bg.green, bg.blue, bg.alpha);
+ cr.arc(center_x, center_y, radius, start_background, end_background);
+ cr.set_line_width(bg_stroke);
+ cr.stroke();
+
+ // Draw rounded background ends
+ if (rounded) {
+ var start_x = center_x + Math.cos(start_background) * radius;
+ var start_y = center_y + Math.sin(start_background) * radius;
+ var end_x = center_x + Math.cos(end_background) * radius;
+ var end_y = center_y + Math.sin(end_background) * radius;
+ cr.set_line_width(0);
+ cr.arc(start_x, start_y, bg_stroke / 2, 0, 0 - 0.01);
+ cr.fill();
+ cr.arc(end_x, end_y, bg_stroke / 2, 0, 0 - 0.01);
+ cr.fill();
+ }
+
+ // Draw progress
+ cr.set_source_rgba(fg.red, fg.green, fg.blue, fg.alpha);
+ cr.arc(center_x, center_y, radius, start_progress, end_progress);
+ cr.set_line_width(fg_stroke);
+ cr.stroke();
+
+ // Draw rounded progress ends
+ if (rounded) {
+ var start_x = center_x + Math.cos(start_progress) * radius;
+ var start_y = center_y + Math.sin(start_progress) * radius;
+ var end_x = center_x + Math.cos(end_progress) * radius;
+ var end_y = center_y + Math.sin(end_progress) * radius;
+ cr.set_line_width(0);
+ cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01);
+ cr.fill();
+ cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01);
+ cr.fill();
+ }
+
+ if (get_child() != null) {
+ get_child().size_allocate(allocation);
+ propagate_draw(get_child(), cr);
+ }
+
+ return true;
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/eventbox.vala b/lib/astal/gtk3/src/widget/eventbox.vala
new file mode 100644
index 0000000..611da2a
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/eventbox.vala
@@ -0,0 +1,64 @@
+public class Astal.EventBox : Gtk.EventBox {
+ public signal void hover (HoverEvent event);
+ public signal void hover_lost (HoverEvent event);
+ public signal void click (ClickEvent event);
+ public signal void click_release (ClickEvent event);
+ public signal void scroll (ScrollEvent event);
+ public signal void motion (MotionEvent event);
+
+ static construct {
+ set_css_name("eventbox");
+ }
+
+ construct {
+ add_events(Gdk.EventMask.SCROLL_MASK);
+ add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK);
+ add_events(Gdk.EventMask.POINTER_MOTION_MASK);
+
+ enter_notify_event.connect((self, event) => {
+ if (event.window == self.get_window() &&
+ event.detail != Gdk.NotifyType.INFERIOR) {
+ this.set_state_flags(Gtk.StateFlags.PRELIGHT, false);
+ hover(HoverEvent(event) { lost = false });
+ }
+ });
+
+ leave_notify_event.connect((self, event) => {
+ if (event.window == self.get_window() &&
+ event.detail != Gdk.NotifyType.INFERIOR) {
+ this.unset_state_flags(Gtk.StateFlags.PRELIGHT);
+ hover_lost(HoverEvent(event) { lost = true });
+ }
+ });
+
+ button_press_event.connect((event) => {
+ click(ClickEvent(event) { release = false });
+ });
+
+ button_release_event.connect((event) => {
+ click_release(ClickEvent(event) { release = true });
+ });
+
+ scroll_event.connect((event) => {
+ scroll(ScrollEvent(event));
+ });
+
+ motion_notify_event.connect((event) => {
+ motion(MotionEvent(event));
+ });
+ }
+}
+
+public struct Astal.MotionEvent {
+ uint time;
+ double x;
+ double y;
+ Gdk.ModifierType modifier;
+
+ public MotionEvent(Gdk.EventMotion event) {
+ this.time = event.time;
+ this.x = event.x;
+ this.y = event.y;
+ this.modifier = event.state;
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/icon.vala b/lib/astal/gtk3/src/widget/icon.vala
new file mode 100644
index 0000000..f2d59a2
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/icon.vala
@@ -0,0 +1,105 @@
+public class Astal.Icon : Gtk.Image {
+ private IconType type = IconType.NAMED;
+ private double size { get; set; default = 14; }
+
+ public new Gdk.Pixbuf pixbuf { get; set; }
+ public string icon { get; set; default = ""; }
+ public GLib.Icon g_icon {get; set;}
+
+ public static Gtk.IconInfo? lookup_icon(string icon) {
+ var theme = Gtk.IconTheme.get_default();
+ return theme.lookup_icon(icon, 16, Gtk.IconLookupFlags.USE_BUILTIN);
+ }
+
+ private async void display_icon() {
+ switch(type) {
+ case IconType.NAMED:
+ icon_name = icon;
+ pixel_size = (int)size;
+ break;
+ case IconType.FILE:
+ try {
+ var file = File.new_for_path(icon);
+ var stream = yield file.read_async();
+ var pb = yield new Gdk.Pixbuf.from_stream_at_scale_async(
+ stream,
+ (int)size * scale_factor,
+ (int)size * scale_factor,
+ true,
+ null
+ );
+ var cs = Gdk.cairo_surface_create_from_pixbuf(pb, 0, this.get_window());
+ set_from_surface(cs);
+ } catch (Error err) {
+ printerr(err.message);
+ }
+ break;
+ case IconType.PIXBUF:
+ var pb_scaled = pixbuf.scale_simple(
+ (int)size * scale_factor,
+ (int)size * scale_factor,
+ Gdk.InterpType.BILINEAR
+ );
+ if (pb_scaled != null) {
+ var cs = Gdk.cairo_surface_create_from_pixbuf(pb_scaled, 0, this.get_window());
+ set_from_surface(cs);
+ }
+ break;
+ case IconType.GICON:
+ pixel_size = (int)size;
+ gicon = g_icon;
+ break;
+
+ }
+ }
+
+ static construct {
+ set_css_name("icon");
+ }
+
+ construct {
+ notify["icon"].connect(() => {
+ if(FileUtils.test(icon, GLib.FileTest.EXISTS))
+ type = IconType.FILE;
+ else if (lookup_icon(icon) != null)
+ type = IconType.NAMED;
+ else {
+ type = IconType.NAMED;
+ warning("cannot assign %s as icon, "+
+ "it is not a file nor a named icon", icon);
+ }
+ display_icon.begin();
+ });
+
+ notify["pixbuf"].connect(() => {
+ type = IconType.PIXBUF;
+ display_icon.begin();
+ });
+
+ notify["g-icon"].connect(() => {
+ type = IconType.GICON;
+ display_icon.begin();
+ });
+
+ size_allocate.connect(() => {
+ size = get_style_context()
+ .get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
+
+ display_icon.begin();
+ });
+
+ get_style_context().changed.connect(() => {
+ size = get_style_context()
+ .get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
+
+ display_icon.begin();
+ });
+ }
+}
+
+private enum Astal.IconType {
+ NAMED,
+ FILE,
+ PIXBUF,
+ GICON,
+}
diff --git a/lib/astal/gtk3/src/widget/label.vala b/lib/astal/gtk3/src/widget/label.vala
new file mode 100644
index 0000000..4063b6f
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/label.vala
@@ -0,0 +1,18 @@
+using Pango;
+
+public class Astal.Label : Gtk.Label {
+ public bool truncate {
+ set { ellipsize = value ? EllipsizeMode.END : EllipsizeMode.NONE; }
+ get { return ellipsize == EllipsizeMode.END; }
+ }
+
+ public new bool justify_fill {
+ set { justify = value ? Gtk.Justification.FILL : Gtk.Justification.LEFT; }
+ get { return justify == Gtk.Justification.FILL; }
+ }
+
+ construct {
+ notify["ellipsize"].connect(() => notify_property("truncate"));
+ notify["justify"].connect(() => notify_property("justify_fill"));
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/levelbar.vala b/lib/astal/gtk3/src/widget/levelbar.vala
new file mode 100644
index 0000000..9b61957
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/levelbar.vala
@@ -0,0 +1,13 @@
+public class Astal.LevelBar : Gtk.LevelBar {
+ [CCode (notify = false)]
+ public bool vertical {
+ get { return orientation == Gtk.Orientation.VERTICAL; }
+ set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
+ }
+
+ construct {
+ notify["orientation"].connect(() => {
+ notify_property("vertical");
+ });
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/overlay.vala b/lib/astal/gtk3/src/widget/overlay.vala
new file mode 100644
index 0000000..603ee66
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/overlay.vala
@@ -0,0 +1,57 @@
+public class Astal.Overlay : Gtk.Overlay {
+ public bool pass_through { get; set; }
+
+ public Gtk.Widget? overlay {
+ get { return overlays.nth_data(0); }
+ set {
+ foreach (var ch in get_children()) {
+ if (ch != child)
+ remove(ch);
+ }
+
+ if (value != null)
+ add_overlay(value);
+ }
+ }
+
+ public List<weak Gtk.Widget> overlays {
+ owned get { return get_children(); }
+ set {
+ foreach (var ch in get_children()) {
+ if (ch != child)
+ remove(ch);
+ }
+
+ foreach (var ch in value)
+ add_overlay(ch);
+ }
+ }
+
+ public new Gtk.Widget? child {
+ get { return get_child(); }
+ set {
+ var ch = get_child();
+ if (ch != null)
+ remove(ch);
+
+ if (value != null)
+ add(value);
+ }
+ }
+
+ construct {
+ notify["pass-through"].connect(() => {
+ update_pass_through();
+ });
+ }
+
+ private void update_pass_through() {
+ foreach (var child in get_children())
+ set_overlay_pass_through(child, pass_through);
+ }
+
+ public new void add_overlay(Gtk.Widget widget) {
+ base.add_overlay(widget);
+ set_overlay_pass_through(widget, pass_through);
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/scrollable.vala b/lib/astal/gtk3/src/widget/scrollable.vala
new file mode 100644
index 0000000..57afb6e
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/scrollable.vala
@@ -0,0 +1,40 @@
+public class Astal.Scrollable : Gtk.ScrolledWindow {
+ private Gtk.PolicyType _hscroll = Gtk.PolicyType.AUTOMATIC;
+ private Gtk.PolicyType _vscroll = Gtk.PolicyType.AUTOMATIC;
+
+ public Gtk.PolicyType hscroll {
+ get { return _hscroll; }
+ set {
+ _hscroll = value;
+ set_policy(value, vscroll);
+ }
+ }
+
+ public Gtk.PolicyType vscroll {
+ get { return _vscroll; }
+ set {
+ _vscroll = value;
+ set_policy(hscroll, value);
+ }
+ }
+
+ static construct {
+ set_css_name("scrollable");
+ }
+
+ construct {
+ if (hadjustment != null)
+ hadjustment = new Gtk.Adjustment(0,0,0,0,0,0);
+
+ if (vadjustment != null)
+ vadjustment = new Gtk.Adjustment(0,0,0,0,0,0);
+ }
+
+ public new Gtk.Widget get_child() {
+ var ch = base.get_child();
+ if (ch is Gtk.Viewport) {
+ return ch.get_child();
+ }
+ return ch;
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/slider.vala b/lib/astal/gtk3/src/widget/slider.vala
new file mode 100644
index 0000000..466275b
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/slider.vala
@@ -0,0 +1,71 @@
+public class Astal.Slider : Gtk.Scale {
+ [CCode (notify = false)]
+ public bool vertical {
+ get { return orientation == Gtk.Orientation.VERTICAL; }
+ set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
+ }
+
+ // emitted when the user drags the slider
+ public signal void dragged ();
+
+ construct {
+ draw_value = false;
+
+ if (adjustment == null)
+ adjustment = new Gtk.Adjustment(0,0,0,0,0,0);
+
+ if (max == 0 && min == 0) {
+ max = 1;
+ }
+
+ if (step == 0) {
+ step = 0.05;
+ }
+
+ notify["orientation"].connect(() => {
+ notify_property("vertical");
+ });
+
+ button_press_event.connect(() => { dragging = true; });
+ key_press_event.connect(() => { dragging = true; });
+ button_release_event.connect(() => { dragging = false; });
+ key_release_event.connect(() => { dragging = false; });
+ scroll_event.connect((event) => {
+ dragging = true;
+ if (event.delta_y > 0)
+ value -= step;
+ else
+ value += step;
+ dragging = false;
+ });
+
+ value_changed.connect(() => {
+ if (dragging)
+ dragged();
+ });
+ }
+
+ public bool dragging { get; private set; }
+
+ public double value {
+ get { return adjustment.value; }
+ set { if (!dragging) adjustment.value = value; }
+ }
+
+ public double min {
+ get { return adjustment.lower; }
+ set { adjustment.lower = value; }
+ }
+
+ public double max {
+ get { return adjustment.upper; }
+ set { adjustment.upper = value; }
+ }
+
+ public double step {
+ get { return adjustment.step_increment; }
+ set { adjustment.step_increment = value; }
+ }
+
+ // TODO: marks
+}
diff --git a/lib/astal/gtk3/src/widget/stack.vala b/lib/astal/gtk3/src/widget/stack.vala
new file mode 100644
index 0000000..02f9959
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/stack.vala
@@ -0,0 +1,26 @@
+public class Astal.Stack : Gtk.Stack {
+ public string shown {
+ get { return visible_child_name; }
+ set { visible_child_name = value; }
+ }
+
+ public List<weak Gtk.Widget> children {
+ set { _set_children(value); }
+ owned get { return get_children(); }
+ }
+
+ private void _set_children(List<weak Gtk.Widget> arr) {
+ foreach(var child in get_children()) {
+ remove(child);
+ }
+
+ var i = 0;
+ foreach(var child in arr) {
+ if (child.name != null) {
+ add_named(child, child.name);
+ } else {
+ add_named(child, (++i).to_string());
+ }
+ }
+ }
+}
diff --git a/lib/astal/gtk3/src/widget/widget.vala b/lib/astal/gtk3/src/widget/widget.vala
new file mode 100644
index 0000000..2506bc8
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/widget.vala
@@ -0,0 +1,157 @@
+namespace Astal {
+private class Css {
+ private static HashTable<Gtk.Widget, Gtk.CssProvider> _providers;
+ public static HashTable<Gtk.Widget, Gtk.CssProvider> providers {
+ get {
+ if (_providers == null) {
+ _providers = new HashTable<Gtk.Widget, Gtk.CssProvider>(
+ (w) => (uint)w,
+ (a, b) => a == b);
+ }
+
+ return _providers;
+ }
+ }
+}
+
+private void remove_provider(Gtk.Widget widget) {
+ var providers = Css.providers;
+
+ if (providers.contains(widget)) {
+ var p = providers.get(widget);
+ widget.get_style_context().remove_provider(p);
+ providers.remove(widget);
+ p.dispose();
+ }
+}
+
+public void widget_set_css(Gtk.Widget widget, string css) {
+ var providers = Css.providers;
+
+ if (providers.contains(widget)) {
+ remove_provider(widget);
+ } else {
+ widget.destroy.connect(() => {
+ remove_provider(widget);
+ });
+ }
+
+ var style = !css.contains("{") || !css.contains("}")
+ ? "* { ".concat(css, "}") : css;
+
+ var p = new Gtk.CssProvider();
+ widget.get_style_context()
+ .add_provider(p, Gtk.STYLE_PROVIDER_PRIORITY_USER);
+
+ try {
+ p.load_from_data(style, style.length);
+ providers.set(widget, p);
+ } catch (Error err) {
+ warning(err.message);
+ }
+}
+
+public string widget_get_css(Gtk.Widget widget) {
+ var providers = Css.providers;
+
+ if (providers.contains(widget))
+ return providers.get(widget).to_string();
+
+ return "";
+}
+
+public void widget_set_class_names(Gtk.Widget widget, string[] class_names) {
+ foreach (var name in widget_get_class_names(widget))
+ widget_toggle_class_name(widget, name, false);
+
+ foreach (var name in class_names)
+ widget_toggle_class_name(widget, name, true);
+}
+
+public List<weak string> widget_get_class_names(Gtk.Widget widget) {
+ return widget.get_style_context().list_classes();
+}
+
+public void widget_toggle_class_name(
+ Gtk.Widget widget,
+ string class_name,
+ bool condition = true
+) {
+ var c = widget.get_style_context();
+ if (condition)
+ c.add_class(class_name);
+ else
+ c.remove_class(class_name);
+}
+
+private class Cursor {
+ private static HashTable<Gtk.Widget, string> _cursors;
+ public static HashTable<Gtk.Widget, string> cursors {
+ get {
+ if (_cursors == null) {
+ _cursors = new HashTable<Gtk.Widget, string>(
+ (w) => (uint)w,
+ (a, b) => a == b);
+ }
+ return _cursors;
+ }
+ }
+}
+
+private void widget_setup_cursor(Gtk.Widget widget) {
+ widget.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK);
+ widget.add_events(Gdk.EventMask.LEAVE_NOTIFY_MASK);
+ widget.enter_notify_event.connect(() => {
+ widget.get_window().set_cursor(
+ new Gdk.Cursor.from_name(
+ Gdk.Display.get_default(),
+ Cursor.cursors.get(widget)));
+ return false;
+ });
+ widget.leave_notify_event.connect(() => {
+ widget.get_window().set_cursor(
+ new Gdk.Cursor.from_name(
+ Gdk.Display.get_default(),
+ "default"));
+ return false;
+ });
+ widget.destroy.connect(() => {
+ if (Cursor.cursors.contains(widget))
+ Cursor.cursors.remove(widget);
+ });
+}
+
+public void widget_set_cursor(Gtk.Widget widget, string cursor) {
+ if (!Cursor.cursors.contains(widget))
+ widget_setup_cursor(widget);
+
+ Cursor.cursors.set(widget, cursor);
+}
+
+public string widget_get_cursor(Gtk.Widget widget) {
+ return Cursor.cursors.get(widget);
+}
+
+private class ClickThrough {
+ private static HashTable<Gtk.Widget, bool> _click_through;
+ public static HashTable<Gtk.Widget, bool> click_through {
+ get {
+ if (_click_through == null) {
+ _click_through = new HashTable<Gtk.Widget, bool>(
+ (w) => (uint)w,
+ (a, b) => a == b);
+ }
+ return _click_through;
+ }
+ }
+}
+
+public void widget_set_click_through(Gtk.Widget widget, bool click_through) {
+ ClickThrough.click_through.set(widget, click_through);
+ widget.input_shape_combine_region(click_through ? new Cairo.Region() : null);
+}
+
+public bool widget_get_click_through(Gtk.Widget widget) {
+ return ClickThrough.click_through.get(widget);
+}
+}
diff --git a/lib/astal/gtk3/src/widget/window.vala b/lib/astal/gtk3/src/widget/window.vala
new file mode 100644
index 0000000..946e766
--- /dev/null
+++ b/lib/astal/gtk3/src/widget/window.vala
@@ -0,0 +1,246 @@
+using GtkLayerShell;
+
+public enum Astal.WindowAnchor {
+ NONE = 0,
+ TOP = 1,
+ RIGHT = 2,
+ LEFT = 4,
+ BOTTOM = 8,
+}
+
+public enum Astal.Exclusivity {
+ NORMAL,
+ EXCLUSIVE,
+ IGNORE,
+}
+
+public enum Astal.Layer {
+ BACKGROUND = 0, // GtkLayerShell.Layer.BACKGROUND
+ BOTTOM = 1, // GtkLayerShell.Layer.BOTTOM
+ TOP = 2, // GtkLayerShell.Layer.TOP
+ OVERLAY = 3, // GtkLayerShell.Layer.OVERLAY
+}
+
+public enum Astal.Keymode {
+ NONE = 0, // GtkLayerShell.KeyboardMode.NONE
+ ON_DEMAND = 1, // GtkLayerShell.KeyboardMode.ON_DEMAND
+ EXCLUSIVE = 2, // GtkLayerShell.KeyboardMode.EXCLUSIVE
+}
+
+public class Astal.Window : Gtk.Window {
+ private static bool check(string action) {
+ if (!is_supported()) {
+ critical(@"can not $action on window: layer shell not supported");
+ print("tip: running from an xwayland terminal can cause this, for example VsCode");
+ return true;
+ }
+ return false;
+ }
+
+ private InhibitManager? inhibit_manager;
+ private Inhibitor? inhibitor;
+
+ construct {
+ if (check("initialize layer shell"))
+ return;
+
+ height_request = 1;
+ width_request = 1;
+ init_for_window(this);
+ inhibit_manager = InhibitManager.get_default();
+ }
+
+ public bool inhibit {
+ set {
+ if (inhibit_manager == null) {
+ return;
+ }
+ if (value && inhibitor == null) {
+ inhibitor = inhibit_manager.inhibit(this);
+ }
+ else if (!value && inhibitor != null) {
+ inhibitor = null;
+ }
+ }
+ get {
+ return inhibitor != null;
+ }
+ }
+
+ public override void show() {
+ base.show();
+ if(inhibit) {
+ inhibitor = inhibit_manager.inhibit(this);
+ }
+ }
+
+ public string namespace {
+ get { return get_namespace(this); }
+ set { set_namespace(this, value); }
+ }
+
+ public int anchor {
+ set {
+ if (check("set anchor"))
+ return;
+
+ set_anchor(this, Edge.TOP, WindowAnchor.TOP in value);
+ set_anchor(this, Edge.BOTTOM, WindowAnchor.BOTTOM in value);
+ set_anchor(this, Edge.LEFT, WindowAnchor.LEFT in value);
+ set_anchor(this, Edge.RIGHT, WindowAnchor.RIGHT in value);
+ }
+ get {
+ var a = WindowAnchor.NONE;
+ if (get_anchor(this, Edge.TOP))
+ a = a | WindowAnchor.TOP;
+
+ if (get_anchor(this, Edge.RIGHT))
+ a = a | WindowAnchor.RIGHT;
+
+ if (get_anchor(this, Edge.LEFT))
+ a = a | WindowAnchor.LEFT;
+
+ if (get_anchor(this, Edge.BOTTOM))
+ a = a | WindowAnchor.BOTTOM;
+
+ return a;
+ }
+ }
+
+ public Exclusivity exclusivity {
+ set {
+ if (check("set exclusivity"))
+ return;
+
+ switch (value) {
+ case Exclusivity.NORMAL:
+ set_exclusive_zone(this, 0);
+ break;
+ case Exclusivity.EXCLUSIVE:
+ auto_exclusive_zone_enable(this);
+ break;
+ case Exclusivity.IGNORE:
+ set_exclusive_zone(this, -1);
+ break;
+ }
+ }
+ get {
+ if (auto_exclusive_zone_is_enabled(this))
+ return Exclusivity.EXCLUSIVE;
+
+ if (get_exclusive_zone(this) == -1)
+ return Exclusivity.IGNORE;
+
+ return Exclusivity.NORMAL;
+ }
+ }
+
+ public Layer layer {
+ get { return (Layer)get_layer(this); }
+ set {
+ if (check("set layer"))
+ return;
+
+ set_layer(this, (GtkLayerShell.Layer)value);
+ }
+ }
+
+ public Keymode keymode {
+ get { return (Keymode)get_keyboard_mode(this); }
+ set {
+ if (check("set keymode"))
+ return;
+
+ set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value);
+ }
+ }
+
+ public Gdk.Monitor gdkmonitor {
+ get { return get_monitor(this); }
+ set {
+ if (check("set gdkmonitor"))
+ return;
+
+ set_monitor (this, value);
+ }
+ }
+
+ public new int margin_top {
+ get { return GtkLayerShell.get_margin(this, Edge.TOP); }
+ set {
+ if (check("set margin_top"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.TOP, value);
+ }
+ }
+
+ public new int margin_bottom {
+ get { return GtkLayerShell.get_margin(this, Edge.BOTTOM); }
+ set {
+ if (check("set margin_bottom"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.BOTTOM, value);
+ }
+ }
+
+ public new int margin_left {
+ get { return GtkLayerShell.get_margin(this, Edge.LEFT); }
+ set {
+ if (check("set margin_left"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.LEFT, value);
+ }
+ }
+
+ public new int margin_right {
+ get { return GtkLayerShell.get_margin(this, Edge.RIGHT); }
+ set {
+ if (check("set margin_right"))
+ return;
+
+ GtkLayerShell.set_margin(this, Edge.RIGHT, value);
+ }
+ }
+
+ public new int margin {
+ set {
+ if (check("set margin"))
+ return;
+
+ margin_top = value;
+ margin_right = value;
+ margin_bottom = value;
+ margin_left = value;
+ }
+ }
+
+ /**
+ * CAUTION: the id might not be the same mapped by the compositor
+ * to reset and let the compositor map it pass a negative number
+ */
+ public int monitor {
+ set {
+ if (check("set monitor"))
+ return;
+
+ if (value < 0)
+ set_monitor(this, (Gdk.Monitor)null);
+
+ var m = Gdk.Display.get_default().get_monitor(value);
+ set_monitor(this, m);
+ }
+ get {
+ var m = get_monitor(this);
+ var d = Gdk.Display.get_default();
+ for (var i = 0; i < d.get_n_monitors(); ++i) {
+ if (m == d.get_monitor(i))
+ return i;
+ }
+
+ return -1;
+ }
+ }
+}