summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAylur <[email protected]>2024-05-19 02:39:53 +0200
committerAylur <[email protected]>2024-05-19 02:39:53 +0200
commit1425b396b08f0e91d45bbd0f92b1309115c7c870 (patch)
tree8af1a899a14d8a01a9ef50e248c077b48aed25bc /src
init 0.1.0
Diffstat (limited to 'src')
-rw-r--r--src/astal.vala171
-rw-r--r--src/client.vala.in79
-rw-r--r--src/meson.build85
-rw-r--r--src/process.vala126
-rw-r--r--src/time.vala68
-rw-r--r--src/variable.vala190
-rw-r--r--src/widget/box.vala60
-rw-r--r--src/widget/button.vala35
-rw-r--r--src/widget/centerbox.vala35
-rw-r--r--src/widget/circularprogress.vala174
-rw-r--r--src/widget/eventbox.vala39
-rw-r--r--src/widget/icon.vala87
-rw-r--r--src/widget/widget.vala132
-rw-r--r--src/widget/window.vala136
14 files changed, 1417 insertions, 0 deletions
diff --git a/src/astal.vala b/src/astal.vala
new file mode 100644
index 0000000..e9d1484
--- /dev/null
+++ b/src/astal.vala
@@ -0,0 +1,171 @@
+namespace Astal {
+public class Application : Gtk.Application {
+ public signal void request (string request);
+ private List<Gtk.CssProvider> css_providers;
+ private SocketService service;
+ private string socket;
+
+ public string instance_name { get; construct set; }
+
+ public List<Gtk.Window> windows {
+ get { return get_windows(); }
+ }
+
+ public Gtk.Settings settings {
+ get { return Gtk.Settings.get_default(); }
+ }
+
+ public Gdk.Screen screen {
+ get { return Gdk.Screen.get_default(); }
+ }
+
+ public string gtk_theme {
+ owned get { return settings.gtk_theme_name; }
+ set { settings.gtk_theme_name = value; }
+ }
+
+ public string icon_theme {
+ owned get { return settings.gtk_icon_theme_name; }
+ set { settings.gtk_icon_theme_name = value; }
+ }
+
+ public string cursor_theme {
+ owned get { return settings.gtk_cursor_theme_name; }
+ set { settings.gtk_cursor_theme_name = value; }
+ }
+
+ public void reset_css() {
+ foreach(var provider in css_providers) {
+ Gtk.StyleContext.remove_provider_for_screen(screen, provider);
+ css_providers.remove_all(provider);
+ }
+ }
+
+ public void inspector() {
+ Gtk.Window.set_interactive_debugging(true);
+ }
+
+ public Gtk.Window get_window(string name) throws WindowError {
+ foreach(var win in windows) {
+ if (win.name == name)
+ return win;
+ }
+
+ throw new WindowError.NO_WINDOW_WITH_NAME(name);
+ }
+
+ public void apply_css(string style, bool reset = false) throws Error {
+ var provider = new Gtk.CssProvider();
+
+ if (reset)
+ reset_css();
+
+ if (FileUtils.test(style, FileTest.EXISTS))
+ provider.load_from_path(style);
+ else
+ provider.load_from_data(style);
+
+ Gtk.StyleContext.add_provider_for_screen(
+ screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_USER);
+
+ css_providers.append(provider);
+ }
+
+ private async void _socket_request(SocketConnection conn) {
+ string message = yield read_sock(conn);
+ request(message.strip());
+ response(message.strip(), conn);
+ }
+
+ public virtual void response(string msg, SocketConnection conn) {
+ write_sock.begin(conn, "missing response implementation on ".concat(application_id));
+ }
+
+ /**
+ * should be called before `run()`
+ * the return value indicates if instance is already running
+ */
+ public bool acquire_socket() {
+ socket = GLib.Environment.get_user_runtime_dir().concat(
+ "/",
+ instance_name,
+ ".sock");
+
+ if (FileUtils.test(socket, GLib.FileTest.EXISTS)) {
+ info("socket %s exists", socket);
+ return false;
+ }
+
+ try {
+ SocketAddress _;
+ service = new SocketService();
+ service.add_address(
+ new UnixSocketAddress(socket),
+ SocketType.STREAM,
+ SocketProtocol.DEFAULT,
+ null,
+ out _);
+
+ service.incoming.connect((conn) => {
+ _socket_request.begin(conn);
+ return false;
+ });
+
+ info("socket acquired: %s\n", socket);
+ return true;
+ } catch (Error err) {
+ critical("could not acquire socket %s\n", application_id);
+ critical(err.message);
+ return false;
+ }
+ }
+
+ construct {
+ if (instance_name == null)
+ instance_name = "astal";
+
+ if (application_id == null)
+ application_id = "io.Astal.".concat(instance_name);
+
+ shutdown.connect(() => {
+ if (FileUtils.test(socket, GLib.FileTest.EXISTS)){
+ try {
+ File.new_for_path(socket).delete(null);
+ } catch (Error err) {
+ warning(err.message);
+ }
+ }
+ });
+
+ SourceFunc close = () => { quit(); };
+ Unix.signal_add(1, close, Priority.HIGH);
+ Unix.signal_add(2, close, Priority.HIGH);
+ Unix.signal_add(15, close, Priority.HIGH);
+ }
+}
+
+public errordomain WindowError {
+ NO_WINDOW_WITH_NAME
+}
+
+public async string read_sock(SocketConnection conn) {
+ try {
+ var stream = new DataInputStream(conn.input_stream);
+ size_t size;
+ return yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, out size);
+ } catch (Error err) {
+ critical(err.message);
+ return err.message;
+ }
+}
+
+public async void write_sock(SocketConnection conn, string response) {
+ try {
+ yield conn.output_stream.write_async(
+ response.concat("\x04").data,
+ Priority.DEFAULT);
+ } catch (Error err) {
+ critical(err.message);
+ }
+}
+}
diff --git a/src/client.vala.in b/src/client.vala.in
new file mode 100644
index 0000000..82a99a4
--- /dev/null
+++ b/src/client.vala.in
@@ -0,0 +1,79 @@
+private static bool version;
+private static bool help;
+private static string? instance_name;
+
+private const GLib.OptionEntry[] options = {
+ { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
+ { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
+ { "instance-name", 'i', OptionFlags.NONE, OptionArg.STRING, ref instance_name, null, null },
+ { null },
+};
+
+async int main(string[] argv) {
+ try {
+ var opts = new OptionContext();
+ opts.add_main_entries(options, null);
+ opts.set_help_enabled(false);
+ opts.set_ignore_unknown_options(false);
+ opts.parse(ref argv);
+ } catch (OptionError err) {
+ printerr (err.message);
+ return 1;
+ }
+
+ if (help) {
+ print("Client for the socket of an Astal.Application instance\n\n");
+ print("Usage:\n");
+ print(" %s [flags] message\n\n", argv[0]);
+ print("Flags:\n");
+ print(" -h, --help Print this help and exit\n");
+ print(" -v, --version Print version number and exit\n");
+ print(" -i, --instance-name Instance name of the Astal instance\n");
+ return 0;
+ }
+
+ if (version) {
+ print("@VERSION@");
+ return 0;
+ }
+
+ if (instance_name == null)
+ instance_name = "astal";
+
+ var request = "";
+ for (var i = 1; i < argv.length; ++i) {
+ request = request.concat(" ", argv[i]);
+ }
+
+ var client = new SocketClient();
+ var rundir = GLib.Environment.get_user_runtime_dir();
+ var socket = rundir.concat("/", instance_name, ".sock");
+
+ try {
+ var conn = client.connect(new UnixSocketAddress(socket), null);
+
+ try {
+ yield conn.output_stream.write_async(
+ request.concat("\x04").data,
+ Priority.DEFAULT);
+ } catch (Error err) {
+ printerr("could not write to app '%s'", instance_name);
+ }
+
+ var stream = new DataInputStream(conn.input_stream);
+ size_t size;
+
+ try {
+ var res = yield stream.read_upto_async("\x04", -1, Priority.DEFAULT, null, out size);
+ if (res != null)
+ print("%s", res);
+ } catch (Error err) {
+ printerr(err.message);
+ }
+ } catch (Error err) {
+ printerr("could not connect to app '%s'", instance_name);
+ return 1;
+ }
+
+ return 0;
+}
diff --git a/src/meson.build b/src/meson.build
new file mode 100644
index 0000000..2227075
--- /dev/null
+++ b/src/meson.build
@@ -0,0 +1,85 @@
+version_split = meson.project_version().split('.')
+api_version = version_split[0] + '.' + version_split[1]
+astal_gir = 'Astal-' + api_version + '.gir'
+astal_typelib = 'Astal-' + api_version + '.typelib'
+astal_so = 'libastal.so.' + meson.project_version()
+
+deps = [
+ 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'),
+]
+
+sources = files(
+ 'widget/box.vala',
+ 'widget/button.vala',
+ 'widget/centerbox.vala',
+ 'widget/eventbox.vala',
+ 'widget/icon.vala',
+ # 'widget/circularprogress.vala', # TODO: math lib -X -lm
+ 'widget/widget.vala',
+ 'widget/window.vala',
+ 'astal.vala',
+ 'process.vala',
+ 'time.vala',
+ 'variable.vala',
+)
+
+libastal = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '.vapi',
+ vala_gir: astal_gir,
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true, true],
+)
+
+import('pkgconfig').generate(
+ description: 'libastal',
+ libraries: libastal,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: 'gio-2.0',
+ install_dir: get_option('libdir') / 'pkgconfig',
+)
+
+if get_option('typelib')
+ custom_target(
+ astal_typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@',
+ meson.current_build_dir() / astal_gir,
+ ],
+ input: libastal,
+ output: astal_typelib,
+ depends: libastal,
+ install: true,
+ install_dir: get_option('libdir') / 'girepository-1.0',
+ )
+endif
+
+if get_option('cli_client')
+ executable(
+ meson.project_name(),
+ configure_file(
+ input: 'client.vala.in',
+ output: 'client.vala',
+ configuration: {
+ 'VERSION': meson.project_version(),
+ },
+ ),
+ dependencies: deps,
+ install: true,
+ )
+endif
diff --git a/src/process.vala b/src/process.vala
new file mode 100644
index 0000000..6e63264
--- /dev/null
+++ b/src/process.vala
@@ -0,0 +1,126 @@
+namespace Astal {
+public class Process : Object {
+ private void read_stream(DataInputStream stream, bool err) {
+ stream.read_line_utf8_async.begin(Priority.DEFAULT, null, (_, res) => {
+ try {
+ var output = stream.read_line_utf8_async.end(res);
+ if (output != null) {
+ if (err)
+ stdout(output.strip());
+ else
+ stderr(output.strip());
+
+ read_stream(stream, err);
+ }
+ } catch (Error err) {
+ printerr("%s\n", err.message);
+ }
+ });
+ }
+
+ private DataInputStream out_stream;
+ private DataInputStream err_stream;
+ private DataOutputStream in_stream;
+ private Subprocess process;
+ public string[] argv { construct; get; }
+
+ public signal void stdout (string out);
+ public signal void stderr (string err);
+
+ public void kill() {
+ process.force_exit();
+ }
+
+ public void write(string in) throws Error {
+ in_stream.put_string(in);
+ }
+
+ public void write_async(string in) {
+ in_stream.write_all_async.begin(
+ in.data,
+ Priority.DEFAULT, null, (_, res) => {
+ try {
+ in_stream.write_all_async.end(res, null);
+ } catch (Error err) {
+ printerr("%s\n", err.message);
+ }
+ }
+ );
+ }
+
+ public Process.subprocessv(string[] cmd) throws Error {
+ Object(argv: cmd);
+ process = new Subprocess.newv(cmd,
+ SubprocessFlags.STDIN_PIPE |
+ SubprocessFlags.STDERR_PIPE |
+ SubprocessFlags.STDOUT_PIPE
+ );
+ out_stream = new DataInputStream(process.get_stdout_pipe());
+ err_stream = new DataInputStream(process.get_stderr_pipe());
+ in_stream = new DataOutputStream(process.get_stdin_pipe());
+ read_stream(out_stream, true);
+ read_stream(err_stream, false);
+ }
+
+ public static Process subprocess(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return new Process.subprocessv(argv);
+ }
+
+ public static string execv(string[] cmd) throws Error {
+ var process = new Subprocess.newv(
+ cmd,
+ SubprocessFlags.STDERR_PIPE |
+ SubprocessFlags.STDOUT_PIPE
+ );
+
+ string err_str, out_str;
+ process.communicate_utf8(null, null, out out_str, out err_str);
+ var success = process.get_successful();
+ process.dispose();
+ if (success)
+ return out_str.strip();
+ else
+ throw new ProcessError.FAILED(err_str.strip());
+ }
+
+ public static string exec(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return Process.execv(argv);
+ }
+
+ public Process.exec_asyncv(string[] cmd) throws Error {
+ Object(argv: cmd);
+ process = new Subprocess.newv(cmd,
+ SubprocessFlags.STDERR_PIPE |
+ SubprocessFlags.STDOUT_PIPE
+ );
+
+ process.communicate_utf8_async.begin(null, null, (_, res) => {
+ string err_str, out_str;
+ try {
+ process.communicate_utf8_async.end(res, out out_str, out err_str);
+ if (process.get_successful())
+ stdout(out_str.strip());
+ else
+ stderr(err_str.strip());
+ } catch (Error err) {
+ printerr("%s\n", err.message);
+ } finally {
+ dispose();
+ }
+ });
+ }
+
+ public static Process exec_async(string cmd) throws Error {
+ string[] argv;
+ Shell.parse_argv(cmd, out argv);
+ return new Process.exec_asyncv(argv);
+ }
+}
+errordomain ProcessError {
+ FAILED
+}
+}
diff --git a/src/time.vala b/src/time.vala
new file mode 100644
index 0000000..05ffff9
--- /dev/null
+++ b/src/time.vala
@@ -0,0 +1,68 @@
+namespace Astal {
+public class Time : Object {
+ public signal void now ();
+ public signal void cancelled ();
+ private Cancellable cancellable;
+ private uint timeout_id;
+
+ construct {
+ cancellable = new Cancellable();
+ cancellable.cancelled.connect(() => {
+ Source.remove(timeout_id);
+ cancelled();
+ dispose();
+ });
+ }
+
+ private void connect_closure(Closure? closure) {
+ if (closure == null)
+ return;
+
+ now.connect(() => {
+ Value ret = Value(Type.POINTER); // void
+ closure.invoke(ref ret, {});
+ });
+ }
+
+ public Time.interval_prio(uint interval, int prio = Priority.DEFAULT, Closure? fn) {
+ connect_closure(fn);
+ Idle.add_once(() => now());
+ timeout_id = Timeout.add(interval, () => {
+ now();
+ return Source.CONTINUE;
+ }, prio);
+ }
+
+ public Time.timeout_prio(uint timeout, int prio = Priority.DEFAULT, Closure? fn) {
+ connect_closure(fn);
+ timeout_id = Timeout.add(timeout, () => {
+ now();
+ return Source.REMOVE;
+ }, prio);
+ }
+
+ public Time.idle_prio(int prio = Priority.DEFAULT_IDLE, Closure? fn) {
+ connect_closure(fn);
+ timeout_id = Idle.add(() => {
+ now();
+ return Source.REMOVE;
+ }, prio);
+ }
+
+ public static Time interval(uint interval, Closure? fn) {
+ return new Time.interval_prio(interval, Priority.DEFAULT, fn);
+ }
+
+ public static Time timeout(uint timeout, Closure? fn) {
+ return new Time.timeout_prio(timeout, Priority.DEFAULT, fn);
+ }
+
+ public static Time idle(Closure? fn) {
+ return new Time.idle_prio(Priority.DEFAULT_IDLE, fn);
+ }
+
+ public void cancel() {
+ cancellable.cancel();
+ }
+}
+}
diff --git a/src/variable.vala b/src/variable.vala
new file mode 100644
index 0000000..e1d6414
--- /dev/null
+++ b/src/variable.vala
@@ -0,0 +1,190 @@
+namespace Astal {
+public class VariableBase : Object {
+ public signal void changed ();
+ public signal void dropped ();
+ public signal void error (string err);
+
+ // lua-lgi crashes when using its emitting mechanism
+ public void emit_changed() { changed(); }
+ public void emit_dropped() { dropped(); }
+ public void emit_error(string err) { this.error(err); }
+
+ ~VariableBase() {
+ dropped();
+ }
+}
+
+public class Variable : VariableBase {
+ public Value value { owned get; set; }
+
+ private uint poll_id = 0;
+ private Process? watch_proc;
+
+ private uint poll_interval { get; set; default = 1000; }
+ private string[] poll_exec { get; set; }
+ private Closure? poll_transform { get; set; }
+ private Closure? poll_fn { get; set; }
+
+ private Closure? watch_transform { get; set; }
+ private string[] watch_exec { get; set; }
+
+ public Variable(Value init) {
+ Object(value: init);
+ }
+
+ public Variable poll(
+ uint interval,
+ string exec,
+ Closure? transform
+ ) throws Error {
+ string[] argv;
+ Shell.parse_argv(exec, out argv);
+ return pollv(interval, argv, transform);
+ }
+
+ public Variable pollv(
+ uint interval,
+ string[] execv,
+ Closure? transform
+ ) throws Error {
+ if (is_polling())
+ stop_poll();
+
+ poll_interval = interval;
+ poll_exec = execv;
+ poll_transform = transform;
+ poll_fn = null;
+ start_poll();
+ return this;
+ }
+
+ public Variable pollfn(
+ uint interval,
+ Closure fn
+ ) throws Error {
+ if (is_polling())
+ stop_poll();
+
+ poll_interval = interval;
+ poll_fn = fn;
+ poll_exec = null;
+ start_poll();
+ return this;
+ }
+
+ public Variable watch(
+ string exec,
+ Closure? transform
+ ) throws Error {
+ string[] argv;
+ Shell.parse_argv(exec, out argv);
+ return watchv(argv, transform);
+ }
+
+ public Variable watchv(
+ string[] execv,
+ Closure? transform
+ ) throws Error {
+ if (is_watching())
+ stop_watch();
+
+ watch_exec = execv;
+ watch_transform = transform;
+ start_watch();
+ return this;
+ }
+
+ construct {
+ notify["value"].connect(() => changed());
+ dropped.connect(() => {
+ if (is_polling())
+ stop_poll();
+
+ if (is_watching())
+ stop_watch();
+ });
+ }
+
+ private void set_closure(string val, Closure? transform) {
+ if (transform != null) {
+ var str = Value(typeof(string));
+ str.set_string(val);
+
+ var ret_val = Value(this.value.type());
+ transform.invoke(ref ret_val, { str, this.value });
+ this.value = ret_val;
+ }
+ else {
+ if (this.value.type() == Type.STRING && this.value.get_string() == val)
+ return;
+
+ var str = Value(typeof(string));
+ str.set_string(val);
+ this.value = str;
+ }
+ }
+
+ private void set_fn() {
+ var ret_val = Value(this.value.type());
+ poll_fn.invoke(ref ret_val, { this.value });
+ this.value = ret_val;
+ }
+
+ public void start_poll() throws Error {
+ return_if_fail(poll_id == 0);
+
+ if (poll_fn != null) {
+ set_fn();
+ poll_id = Timeout.add(poll_interval, () => {
+ set_fn();
+ return Source.CONTINUE;
+ }, Priority.DEFAULT);
+ }
+ if (poll_exec != null) {
+ var proc = new Process.exec_asyncv(poll_exec);
+ proc.stdout.connect((str) => set_closure(str, poll_transform));
+ proc.stderr.connect((str) => this.error(str));
+ poll_id = Timeout.add(poll_interval, () => {
+ try {
+ proc = new Process.exec_asyncv(poll_exec);
+ proc.stdout.connect((str) => set_closure(str, poll_transform));
+ proc.stderr.connect((str) => this.error(str));
+ return Source.CONTINUE;
+ } catch (Error err) {
+ printerr("%s\n", err.message);
+ poll_id = 0;
+ return Source.REMOVE;
+ }
+ }, Priority.DEFAULT);
+ }
+ }
+
+ public void start_watch() throws Error {
+ return_if_fail(watch_proc == null);
+ return_if_fail(watch_exec != null);
+
+ watch_proc = new Process.subprocessv(watch_exec);
+ watch_proc.stdout.connect((str) => set_closure(str, watch_transform));
+ watch_proc.stderr.connect((str) => this.error(str));
+ }
+
+ public void stop_poll() {
+ return_if_fail(poll_id != 0);
+ Source.remove(poll_id);
+ poll_id = 0;
+ }
+
+ public void stop_watch() {
+ return_if_fail(watch_proc != null);
+ watch_proc.kill();
+ watch_proc = null;
+ }
+
+ public bool is_polling() { return poll_id > 0; }
+ public bool is_watching() { return watch_proc != null; }
+
+ ~Variable() {
+ dropped();
+ }
+}
+}
diff --git a/src/widget/box.vala b/src/widget/box.vala
new file mode 100644
index 0000000..482948d
--- /dev/null
+++ b/src/widget/box.vala
@@ -0,0 +1,60 @@
+namespace Astal {
+public class Box : Gtk.Box {
+ 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);
+ }
+
+ public Box.newh(List<weak Gtk.Widget> children) {
+ this.vertical = false;
+ _set_children(children);
+ }
+
+ public Box.newv(List<weak Gtk.Widget> children) {
+ this.vertical = true;
+ _set_children(children);
+ }
+}
+}
diff --git a/src/widget/button.vala b/src/widget/button.vala
new file mode 100644
index 0000000..601733f
--- /dev/null
+++ b/src/widget/button.vala
@@ -0,0 +1,35 @@
+namespace Astal {
+public class Button : Gtk.Button {
+ public signal void hover (Gdk.EventCrossing event);
+ public signal void hover_lost (Gdk.EventCrossing event);
+ public signal void click (Gdk.EventButton event);
+ public signal void click_release (Gdk.EventButton event);
+
+ construct {
+ add_events(Gdk.EventMask.SCROLL_MASK);
+ add_events(Gdk.EventMask.SMOOTH_SCROLL_MASK);
+
+ enter_notify_event.connect((self, event) => {
+ if (event.window == self.get_window() &&
+ event.detail != Gdk.NotifyType.INFERIOR) {
+ hover(event);
+ }
+ });
+
+ leave_notify_event.connect((self, event) => {
+ if (event.window == self.get_window() &&
+ event.detail != Gdk.NotifyType.INFERIOR) {
+ hover_lost(event);
+ }
+ });
+
+ button_press_event.connect((event) => {
+ click(event);
+ });
+
+ button_release_event.connect((event) => {
+ click_release(event);
+ });
+ }
+}
+}
diff --git a/src/widget/centerbox.vala b/src/widget/centerbox.vala
new file mode 100644
index 0000000..5f41600
--- /dev/null
+++ b/src/widget/centerbox.vala
@@ -0,0 +1,35 @@
+namespace Astal {
+public class CenterBox : Gtk.Box {
+ public bool vertical {
+ get { return orientation == Gtk.Orientation.VERTICAL; }
+ set { orientation = value ? Gtk.Orientation.VERTICAL : Gtk.Orientation.HORIZONTAL; }
+ }
+
+ private Gtk.Widget _start_widget;
+ public Gtk.Widget start_widget {
+ get { return _start_widget; }
+ set {
+ if (_start_widget != null)
+ remove(_start_widget);
+
+ 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);
+
+ pack_end(value, true, true, 0);
+ }
+ }
+
+ public Gtk.Widget center_widget {
+ get { return get_center_widget(); }
+ set { set_center_widget(value); }
+ }
+}
+}
diff --git a/src/widget/circularprogress.vala b/src/widget/circularprogress.vala
new file mode 100644
index 0000000..e96c4de
--- /dev/null
+++ b/src/widget/circularprogress.vala
@@ -0,0 +1,174 @@
+namespace Astal {
+public class CircularProgress : Gtk.Bin {
+ public new Gtk.Widget child { get; set; }
+ 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 new 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 new 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 _map_arc_value_to_range(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 arcLength = end - start;
+ if (arcLength < 0)
+ arcLength += 1; // Adjust for circular representation
+
+ // Calculate the position on the arc based on the percentage value
+ var position = start + (arcLength * value);
+
+ // Ensure the position is between 0 and 1
+ position = (position % 1 + 1) % 1;
+
+ return position;
+ }
+
+ 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 new 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(this.start_at);
+ var end_background = _to_radian(this.end_at);
+ var ranged_value = this.value + this.start_at;
+
+ var is_circle = _is_full_circle(this.start_at, this.end_at);
+
+ if (is_circle) {
+ // Redefine endDraw in radius to create an accurate full circle
+ end_background = start_background + 2 * Math.PI;
+ } else {
+ // Range the value for the arc shape
+ ranged_value = _map_arc_value_to_range(
+ this.start_at,
+ this.end_at,
+ this.value
+ );
+ }
+
+ var to = _to_radian(ranged_value);
+ double start_progress, end_progress;
+
+ if (this.inverted) {
+ start_progress = (2 * Math.PI - to) - start_background;
+ end_progress = (2 * Math.PI - start_background) - start_background;
+ } else {
+ start_progress = start_background;
+ end_progress = to;
+ }
+
+ // 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 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 ends
+ if (this.rounded) {
+ var start_x = center_x + Math.cos(start_background);
+ var start_y = center_y + Math.cos(start_background);
+ var end_x = center_x + Math.cos(to) * radius;
+ var end_y = center_y + Math.cos(to) * 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 (this.child != null) {
+ this.child.size_allocate(allocation);
+ this.propagate_draw(this.child, cr);
+ }
+
+ return true;
+ }
+}
+}
diff --git a/src/widget/eventbox.vala b/src/widget/eventbox.vala
new file mode 100644
index 0000000..de04a32
--- /dev/null
+++ b/src/widget/eventbox.vala
@@ -0,0 +1,39 @@
+namespace Astal {
+public class EventBox : Gtk.EventBox {
+ public signal void hover (Gdk.EventCrossing event);
+ public signal void hover_lost (Gdk.EventCrossing event);
+ public signal void click (Gdk.EventButton event);
+ public signal void click_release (Gdk.EventButton event);
+
+ construct {
+ add_events(Gdk.EventMask.SCROLL_MASK);
+ add_events(Gdk.EventMask.SMOOTH_SCROLL_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(event);
+ }
+ });
+
+ 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(event);
+ }
+ });
+
+ button_press_event.connect((event) => {
+ // TODO: abstract event for easier use
+ click(event);
+ });
+
+ button_release_event.connect((event) => {
+ // TODO: abstract event for easier use
+ click_release(event);
+ });
+ }
+}
+}
diff --git a/src/widget/icon.vala b/src/widget/icon.vala
new file mode 100644
index 0000000..fc493da
--- /dev/null
+++ b/src/widget/icon.vala
@@ -0,0 +1,87 @@
+namespace Astal {
+public Gtk.IconInfo? lookup_icon(string icon) {
+ var theme = Gtk.IconTheme.get_default();
+ return theme.lookup_icon(icon, 16, Gtk.IconLookupFlags.USE_BUILTIN);
+}
+
+public class 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 = ""; }
+
+ private void display_icon() {
+ switch(type) {
+ case IconType.NAMED:
+ icon_name = icon;
+ pixel_size = (int)size;
+ break;
+ case IconType.FILE:
+ try {
+ var pb = new Gdk.Pixbuf.from_file_at_size(
+ icon,
+ (int)size * scale_factor,
+ (int)size * scale_factor
+ );
+ 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;
+ }
+ }
+
+ 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();
+ });
+
+ notify["pixbuf"].connect(() => {
+ type = IconType.PIXBUF;
+ display_icon();
+ });
+
+ size_allocate.connect(() => {
+ size = get_style_context()
+ .get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
+
+ display_icon();
+ });
+
+ get_style_context().changed.connect(() => {
+ size = get_style_context()
+ .get_property("font-size", Gtk.StateFlags.NORMAL).get_double();
+
+ display_icon();
+ });
+ }
+}
+
+private enum IconType {
+ NAMED,
+ FILE,
+ PIXBUF,
+}
+}
diff --git a/src/widget/widget.vala b/src/widget/widget.vala
new file mode 100644
index 0000000..de34c6d
--- /dev/null
+++ b/src/widget/widget.vala
@@ -0,0 +1,132 @@
+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) {
+ 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);
+}
+}
diff --git a/src/widget/window.vala b/src/widget/window.vala
new file mode 100644
index 0000000..1faaf7b
--- /dev/null
+++ b/src/widget/window.vala
@@ -0,0 +1,136 @@
+using GtkLayerShell;
+
+namespace Astal {
+public enum WindowAnchor {
+ NONE = 0,
+ TOP = 1,
+ RIGHT = 2,
+ LEFT = 4,
+ BOTTOM = 8,
+}
+
+public enum Exclusivity {
+ NORMAL,
+ EXCLUSIVE,
+ IGNORE,
+}
+
+public enum Layer {
+ TOP = GtkLayerShell.Layer.TOP,
+ OVERLAY = GtkLayerShell.Layer.OVERLAY,
+ BOTTOM = GtkLayerShell.Layer.BOTTOM,
+ BACKGROUND = GtkLayerShell.Layer.BACKGROUND,
+}
+
+public enum Keymode {
+ NONE = GtkLayerShell.KeyboardMode.NONE,
+ ON_DEMAND = GtkLayerShell.KeyboardMode.ON_DEMAND,
+ EXCLUSIVE = GtkLayerShell.KeyboardMode.EXCLUSIVE,
+}
+
+public class Window : Gtk.Window {
+ construct {
+ height_request = 1;
+ width_request = 1;
+ init_for_window(this);
+ set_namespace(this, name);
+ notify["name"].connect(() => set_namespace(this, name));
+ }
+
+ public int anchor {
+ set {
+ 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 {
+ 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 { set_layer(this, (GtkLayerShell.Layer)value); }
+ }
+
+ public Keymode keymode {
+ set { set_keyboard_mode(this, (GtkLayerShell.KeyboardMode)value); }
+ get { return (Keymode)get_keyboard_mode(this); }
+ }
+
+ public Gdk.Monitor gdkmonitor {
+ set { set_monitor (this, value); }
+ get { return get_monitor(this); }
+ }
+
+ /**
+ * 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 (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;
+ }
+ }
+}
+
+/**
+ * CAUTION: the id might not be the same mapped by the compositor
+ */
+public uint get_num_monitors() {
+ return Gdk.Display.get_default().get_n_monitors();
+}
+}