diff --git a/README.md b/README.md
index 9c262485..63749db9 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ You'll need the following dependencies:
libgranite-7-dev >= 7.7.0
libglib2.0-dev
- libgtk-4-dev
+ libgtk-4-dev >= 4.22
libnotify-dev
libpulse-dev
libwingpanel-9-dev
diff --git a/data/24/volume.svg b/data/24/volume.svg
new file mode 100644
index 00000000..6d3135c0
--- /dev/null
+++ b/data/24/volume.svg
@@ -0,0 +1,294 @@
+
diff --git a/data/gresource.xml b/data/gresource.xml
index c90ac866..ea7d6dcb 100644
--- a/data/gresource.xml
+++ b/data/gresource.xml
@@ -2,6 +2,7 @@
indicator.css
+ 24/volume.svg
16/audio-subwoofer-symbolic.svg
diff --git a/meson.build b/meson.build
index f11e0bd2..90ed7a2c 100644
--- a/meson.build
+++ b/meson.build
@@ -52,6 +52,7 @@ shared_module(
'src/Widgets/PlayerList.vala',
'src/Widgets/DeviceManagerWidget.vala',
'src/Widgets/DeviceItem.vala',
+ 'src/Widgets/Symbol.vala',
'src/Services/PulseAudioManager.vala',
'src/Services/MprisClient.vala',
'src/Services/Volume-control.vala',
@@ -65,7 +66,7 @@ shared_module(
dependency('gio-2.0'),
dependency('gobject-2.0'),
dependency('granite-7', version: '>=7.7.0'),
- dependency('gtk4'),
+ dependency('gtk4', version: '>=4.22'),
dependency('gtk4-wayland'),
dependency('libpulse'),
dependency('libpulse-mainloop-glib'),
diff --git a/src/Indicator.vala b/src/Indicator.vala
index 86787edb..e75bbe2a 100644
--- a/src/Indicator.vala
+++ b/src/Indicator.vala
@@ -115,7 +115,7 @@ public class Sound.Indicator : Wingpanel.Indicator {
display_widget.volume_press_event.connect (volume_control.toggle_mute);
display_widget.mic_press_event.connect (volume_control.toggle_mic_mute);
- display_widget.icon_name = get_volume_icon (volume_control.volume.volume);
+ display_widget.volume_state = get_volume_state (volume_control.volume.volume);
display_widget.volume_scroll_event.connect_after (on_volume_icon_scroll_event);
display_widget.mic_scroll_event.connect_after (on_mic_icon_scroll_event);
@@ -268,7 +268,7 @@ public class Sound.Indicator : Wingpanel.Indicator {
double volume = volume_control.volume.volume / max_volume;
if (volume != volume_adjustment.get_value ()) {
volume_adjustment.set_value (volume);
- display_widget.icon_name = get_volume_icon (volume);
+ display_widget.volume_state = get_volume_state (volume);
}
}
@@ -283,13 +283,12 @@ public class Sound.Indicator : Wingpanel.Indicator {
private void on_mute_change () {
volume_scale.active = !volume_control.mute;
- string volume_icon = get_volume_icon (volume_control.volume.volume);
- display_widget.icon_name = volume_icon;
+ display_widget.volume_state = get_volume_state (volume_control.volume.volume);
if (volume_control.mute) {
volume_scale.icon = "audio-volume-muted-symbolic";
} else {
- volume_scale.icon = volume_icon;
+ volume_scale.icon = get_volume_icon (volume_control.volume.volume);
}
}
@@ -320,12 +319,12 @@ public class Sound.Indicator : Wingpanel.Indicator {
sound_was_blocked_timeout_id = Timeout.add_seconds (5, () => {
mute_blocks_sound = false;
sound_was_blocked_timeout_id = 0;
- display_widget.icon_name = get_volume_icon (volume_control.volume.volume);
+ display_widget.volume_state = get_volume_state (volume_control.volume.volume);
return false;
});
}
- display_widget.icon_name = get_volume_icon (volume_control.volume.volume);
+ display_widget.volume_state = get_volume_state (volume_control.volume.volume);
}
private void on_volume_icon_scroll_event (Value event_value) {
@@ -356,6 +355,18 @@ public class Sound.Indicator : Wingpanel.Indicator {
}
}
+ private unowned string get_volume_state (double volume) {
+ if (volume <= 0 || this.volume_control.mute) {
+ return this.mute_blocks_sound ? "disabled-blocking" : SoundIndicator.SymbolState.DISABLED;
+ } else if (volume <= 0.3) {
+ return "low";
+ } else if (volume <= 0.7) {
+ return SoundIndicator.SymbolState.NORMAL;
+ } else {
+ return "high";
+ }
+ }
+
private void on_volume_switch_change () {
if (volume_scale.active) {
volume_control.set_mute (false);
diff --git a/src/Widgets/DisplayWidget.vala b/src/Widgets/DisplayWidget.vala
index 56c13a46..10fe18f2 100644
--- a/src/Widgets/DisplayWidget.vala
+++ b/src/Widgets/DisplayWidget.vala
@@ -21,7 +21,7 @@ public class Sound.DisplayWidget : Gtk.Box {
public bool show_mic { get; set; }
public bool mic_muted { get; set; }
- public string icon_name { get; set; }
+ public string volume_state { get; set; }
// HACK: Using Gdk.ScrollEvent instead of Value as the type of the parameter
// resulsts build error with valac 0.56.18
@@ -30,7 +30,7 @@ public class Sound.DisplayWidget : Gtk.Box {
public signal void mic_scroll_event (Value event_value);
construct {
- var volume_icon = new Gtk.Image () {
+ var volume_symbol = new SoundIndicator.Symbol ("/io/elementary/wingpanel/sound/24/volume.svg") {
pixel_size = 24
};
@@ -45,7 +45,7 @@ public class Sound.DisplayWidget : Gtk.Box {
valign = Gtk.Align.CENTER;
append (mic_revealer);
- append (volume_icon);
+ append (volume_symbol);
var mic_scroll_controller = new Gtk.EventControllerLegacy ();
mic_scroll_controller.event.connect ((e) => {
@@ -72,7 +72,7 @@ public class Sound.DisplayWidget : Gtk.Box {
});
mic_icon.add_controller (mic_scroll_controller);
- volume_icon.add_controller (volume_scroll_controller);
+ volume_symbol.add_controller (volume_scroll_controller);
var mic_gesture_click = new Gtk.GestureClick () {
button = Gdk.BUTTON_MIDDLE
@@ -94,12 +94,12 @@ public class Sound.DisplayWidget : Gtk.Box {
volume_gesture_click.reset ();
});
- volume_icon.add_controller (volume_gesture_click);
+ volume_symbol.add_controller (volume_gesture_click);
bind_property (
- "icon-name",
- volume_icon,
- "icon-name",
+ "volume-state",
+ volume_symbol,
+ "state",
GLib.BindingFlags.BIDIRECTIONAL | GLib.BindingFlags.SYNC_CREATE
);
bind_property (
diff --git a/src/Widgets/Symbol.vala b/src/Widgets/Symbol.vala
new file mode 100644
index 00000000..e131bc57
--- /dev/null
+++ b/src/Widgets/Symbol.vala
@@ -0,0 +1,78 @@
+/*
+* SPDX-License-Identifier: LGPL-2.1-or-later
+* SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io)
+*/
+
+namespace SoundIndicator.SymbolState {
+ // The default state
+ public const string NORMAL = "normal";
+ // Disabled state represented by a slash
+ public const string DISABLED = "disabled";
+ // e.g. paired, connected, needs attention
+ public const string ACTIVE = "active";
+}
+
+public class SoundIndicator.Symbol : Granite.Bin {
+ public string resource_path { get; construct; }
+
+ public int pixel_size {
+ get { return image.pixel_size; }
+ set { image.pixel_size = value; }
+ }
+
+ public uint state_index {
+ get { return svg.state; }
+ set {
+ uint length = -1;
+ svg.get_state_names (out length);
+
+ if (value > length - 1) {
+ warning ("Granite.Symbol set to undefined state. Ignoring.");
+ return;
+ }
+
+ svg.state = value;
+ notify_property ("state");
+ }
+ }
+
+ public string state {
+ get {
+ uint length = -1;
+ return svg.get_state_names (out length)[state_index];
+ }
+ set {
+ uint length = -1;
+ var names = svg.get_state_names (out length);
+
+ for (int i = 0; i < length; i++) {
+ if (names[i] == value) {
+ state_index = i;
+ return;
+ }
+ }
+
+ warning ("Granite.Symbol set to undefined state. Ignoring.");
+ }
+ }
+
+ private Gtk.Image image;
+ private Gtk.Svg svg;
+
+ public Symbol (string resource_path) {
+ Object (resource_path: resource_path);
+ }
+
+ construct {
+ svg = new Gtk.Svg.from_resource (resource_path);
+
+ image = new Gtk.Image.from_paintable (svg);
+
+ child = image;
+
+ child.realize.connect (() => {
+ svg.set_frame_clock (get_frame_clock ());
+ svg.play ();
+ });
+ }
+}