diff options
Diffstat (limited to 'lib/astal/gtk3/src')
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, ®istry_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; + } + } +} |