diff options
author | Aylur <[email protected]> | 2024-05-19 02:39:53 +0200 |
---|---|---|
committer | Aylur <[email protected]> | 2024-05-19 02:39:53 +0200 |
commit | 1425b396b08f0e91d45bbd0f92b1309115c7c870 (patch) | |
tree | 8af1a899a14d8a01a9ef50e248c077b48aed25bc /src |
init 0.1.0
Diffstat (limited to 'src')
-rw-r--r-- | src/astal.vala | 171 | ||||
-rw-r--r-- | src/client.vala.in | 79 | ||||
-rw-r--r-- | src/meson.build | 85 | ||||
-rw-r--r-- | src/process.vala | 126 | ||||
-rw-r--r-- | src/time.vala | 68 | ||||
-rw-r--r-- | src/variable.vala | 190 | ||||
-rw-r--r-- | src/widget/box.vala | 60 | ||||
-rw-r--r-- | src/widget/button.vala | 35 | ||||
-rw-r--r-- | src/widget/centerbox.vala | 35 | ||||
-rw-r--r-- | src/widget/circularprogress.vala | 174 | ||||
-rw-r--r-- | src/widget/eventbox.vala | 39 | ||||
-rw-r--r-- | src/widget/icon.vala | 87 | ||||
-rw-r--r-- | src/widget/widget.vala | 132 | ||||
-rw-r--r-- | src/widget/window.vala | 136 |
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(); +} +} |