From 16d0dfc5bde93557dbb02ea580ab10231a2de57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 25 Aug 2026 14:38:30 -0700 Subject: [PATCH 1/6] Add Granite.Symbol --- lib/meson.build | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/meson.build b/lib/meson.build index a3215c723..22ea6aa69 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -33,6 +33,7 @@ libgranite_sources = files( 'Widgets/SettingsSidebar.vala', 'Widgets/Settings.vala', 'Widgets/SwitchModelButton.vala', + 'Widgets/Symbol.vala', 'Widgets/TimePicker.vala', 'Widgets/ToolBox.vala', 'Widgets/Toast.vala', From d8509be963733137c543a51692fef3d5ba7ecd36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 25 Aug 2026 14:38:57 -0700 Subject: [PATCH 2/6] Bump GTK dep --- README.md | 2 +- meson.build | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 428fbd9a6..b05f7b50b 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ You'll need the following dependencies: * gettext * libgee-0.8-dev * libgirepository1.0-dev -* libgtk-4-dev >= 4.12.0 +* libgtk-4-dev >= 4.22.0 * sassc * valac diff --git a/meson.build b/meson.build index 004caaeeb..61c75e353 100644 --- a/meson.build +++ b/meson.build @@ -57,7 +57,7 @@ libgranite_deps = [ gio_os_dep, dependency('glib-2.0', version: '>=' + glib_min_version), dependency('gobject-2.0', version: '>=' + glib_min_version), - dependency('gtk4', version: '>=4.12'), + dependency('gtk4', version: '>=4.22'), ] pkgconfig = import('pkgconfig') From b608dce30bcf40ab46b37369f76c9f1dab97f46c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Tue, 25 Aug 2026 14:41:38 -0700 Subject: [PATCH 3/6] Add file --- lib/Widgets/Symbol.vala | 78 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 lib/Widgets/Symbol.vala diff --git a/lib/Widgets/Symbol.vala b/lib/Widgets/Symbol.vala new file mode 100644 index 000000000..10b5d00b8 --- /dev/null +++ b/lib/Widgets/Symbol.vala @@ -0,0 +1,78 @@ +/* +* SPDX-License-Identifier: LGPL-3.0-or-later +* SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) +*/ + +namespace Granite.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 Granite.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 (); + }); + } +} From 96354580ddd0a38847b2d90501fccb3b9c29976f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Mon, 7 Sep 2026 11:47:23 -0700 Subject: [PATCH 4/6] Add first example symbol --- lib/Symbols/CONTRIBUTING.md | 16 ++ lib/Symbols/meson.build | 4 + lib/Symbols/symbols.gresource.xml | 6 + lib/Symbols/volume.svg | 295 ++++++++++++++++++++++++++++++ lib/meson.build | 2 + 5 files changed, 323 insertions(+) create mode 100644 lib/Symbols/CONTRIBUTING.md create mode 100644 lib/Symbols/meson.build create mode 100644 lib/Symbols/symbols.gresource.xml create mode 100644 lib/Symbols/volume.svg diff --git a/lib/Symbols/CONTRIBUTING.md b/lib/Symbols/CONTRIBUTING.md new file mode 100644 index 000000000..674962816 --- /dev/null +++ b/lib/Symbols/CONTRIBUTING.md @@ -0,0 +1,16 @@ +## Canvas Size + +16px x 16px + +## Fill + +10% opacity + +## Strokes + +Main stroke width is 1px + +## Corner Radius + +outside corners 0.5px +inside corners 0.25px diff --git a/lib/Symbols/meson.build b/lib/Symbols/meson.build new file mode 100644 index 000000000..cfd349167 --- /dev/null +++ b/lib/Symbols/meson.build @@ -0,0 +1,4 @@ +symbols_resource = gnome.compile_resources( + 'symbols-resource', + 'symbols.gresource.xml' +) diff --git a/lib/Symbols/symbols.gresource.xml b/lib/Symbols/symbols.gresource.xml new file mode 100644 index 000000000..6b3dd72dc --- /dev/null +++ b/lib/Symbols/symbols.gresource.xml @@ -0,0 +1,6 @@ + + + + volume.svg + + diff --git a/lib/Symbols/volume.svg b/lib/Symbols/volume.svg new file mode 100644 index 000000000..1d95f74d7 --- /dev/null +++ b/lib/Symbols/volume.svg @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/meson.build b/lib/meson.build index 22ea6aa69..4d14504a4 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -1,5 +1,6 @@ subdir('Icons') subdir('Styles') +subdir('Symbols') libgranite_sources = files( 'DateTime.vala', @@ -71,6 +72,7 @@ libgranite = library( libgranite_sources, icons_resource, stylesheet_resource, + symbols_resource, config_vala, dependencies: [ From 59b17120659b17d816340bb07d275a735f95946a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Mon, 7 Sep 2026 12:16:26 -0700 Subject: [PATCH 5/6] Add demo --- demo/GraniteDemo.vala | 1 + demo/Views/SymbolView.vala | 49 ++++++++++++++++++++ demo/meson.build | 1 + lib/Symbols/CONTRIBUTING.md | 6 +++ lib/Symbols/{volume.svg => audio-volume.svg} | 36 +++++++------- lib/Symbols/symbols.gresource.xml | 2 +- lib/Widgets/Symbol.vala | 36 ++++++++++++-- 7 files changed, 107 insertions(+), 24 deletions(-) create mode 100644 demo/Views/SymbolView.vala rename lib/Symbols/{volume.svg => audio-volume.svg} (88%) diff --git a/demo/GraniteDemo.vala b/demo/GraniteDemo.vala index f1d95ad73..5b26fe92a 100644 --- a/demo/GraniteDemo.vala +++ b/demo/GraniteDemo.vala @@ -43,6 +43,7 @@ public class Granite.Demo : Gtk.Application { main_stack.add_titled (lists_view, "lists", "Lists & Grids"); main_stack.add_titled (accel_label_view, "accel_label", "AccelLabel"); main_stack.add_titled (css_view, "css", "Style Classes"); + main_stack.add_titled (new SymbolView (), "symbols", "Symbols"); main_stack.add_titled (date_time_picker_view, "pickers", "Date & Time"); main_stack.add_titled (form_view, "formview", "Forms"); main_stack.add_titled (hypertext_view, "hypertextview", "HyperTextView"); diff --git a/demo/Views/SymbolView.vala b/demo/Views/SymbolView.vala new file mode 100644 index 000000000..32a715b3f --- /dev/null +++ b/demo/Views/SymbolView.vala @@ -0,0 +1,49 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +public class SymbolView : DemoPage { + construct { + title = "Granite.Symbol"; + + var audio_volume_symbol = new Granite.Symbol (AUDIO_VOLUME); + + var symbol_button = new SymbolButton (audio_volume_symbol); + + var flowbox = new Gtk.FlowBox () { + column_spacing = 12, + row_spacing = 12, + margin_top = 12, + margin_start = 12, + margin_end = 12, + margin_bottom = 12 + }; + flowbox.append (symbol_button); + flowbox.add_css_class (Granite.CssClass.CARD); + + child = flowbox; + } + + private class SymbolButton : Gtk.Button { + public Granite.Symbol symbol { get; construct; } + + public SymbolButton (Granite.Symbol symbol) { + Object (symbol: symbol); + } + + construct { + child = symbol; + add_css_class ("image-button"); + + clicked.connect (() => { + if (symbol.state_index == symbol.states_length - 1) { + symbol.state_index = 0; + return; + } + + symbol.state_index++; + }); + } + } +} diff --git a/demo/meson.build b/demo/meson.build index f39e416f7..12472c475 100644 --- a/demo/meson.build +++ b/demo/meson.build @@ -21,6 +21,7 @@ executable( 'Views/MapsView.vala', 'Views/OverlayBarView.vala', 'Views/SettingsUrisView.vala', + 'Views/SymbolView.vala', 'Views/ToastView.vala', 'Views/UtilsView.vala', 'Views/VideoView.vala', diff --git a/lib/Symbols/CONTRIBUTING.md b/lib/Symbols/CONTRIBUTING.md index 674962816..87c26df45 100644 --- a/lib/Symbols/CONTRIBUTING.md +++ b/lib/Symbols/CONTRIBUTING.md @@ -14,3 +14,9 @@ Main stroke width is 1px outside corners 0.5px inside corners 0.25px + +## States + +All symbols must have `normal` and `disabled` states + +symbols can have additional custom states diff --git a/lib/Symbols/volume.svg b/lib/Symbols/audio-volume.svg similarity index 88% rename from lib/Symbols/volume.svg rename to lib/Symbols/audio-volume.svg index 1d95f74d7..2b5d1b262 100644 --- a/lib/Symbols/volume.svg +++ b/lib/Symbols/audio-volume.svg @@ -15,21 +15,21 @@ id='animation-group'> - volume.svg + audio-volume.svg diff --git a/lib/Widgets/Symbol.vala b/lib/Widgets/Symbol.vala index 10b5d00b8..9c263b1a0 100644 --- a/lib/Widgets/Symbol.vala +++ b/lib/Widgets/Symbol.vala @@ -12,6 +12,22 @@ namespace Granite.SymbolState { public const string ACTIVE = "active"; } +public enum Granite.SymbolName { + // Audio volume level + AUDIO_VOLUME; + + public string to_path () { + var resource_base_path = "/io/elementary/granite/"; + switch (this) { + case AUDIO_VOLUME: + return resource_base_path + "audio-volume.svg"; + } + + // FIXME: image-missing + return ""; + } +} + public class Granite.Symbol : Granite.Bin { public string resource_path { get; construct; } @@ -20,13 +36,19 @@ public class Granite.Symbol : Granite.Bin { set { image.pixel_size = value; } } - public uint state_index { - get { return svg.state; } - set { + public uint states_length { + get { uint length = -1; svg.get_state_names (out length); - if (value > length - 1) { + return length; + } + } + + public uint state_index { + get { return svg.state; } + set { + if (value > states_length - 1) { warning ("Granite.Symbol set to undefined state. Ignoring."); return; } @@ -59,7 +81,11 @@ public class Granite.Symbol : Granite.Bin { private Gtk.Image image; private Gtk.Svg svg; - public Symbol (string resource_path) { + public Symbol (SymbolName name) { + Object (resource_path: name.to_path ()); + } + + public Symbol.from_resource (string resource_path) { Object (resource_path: resource_path); } From d2b38471f2ebe995d8da6c1ff1b1a043b176ef9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danielle=20For=C3=A9?= Date: Mon, 7 Sep 2026 12:45:23 -0700 Subject: [PATCH 6/6] Fix up demo --- demo/Views/SymbolView.vala | 51 +++++++++++++++++++++++++++++++------- lib/Widgets/Symbol.vala | 5 ++++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/demo/Views/SymbolView.vala b/demo/Views/SymbolView.vala index 32a715b3f..c50e94525 100644 --- a/demo/Views/SymbolView.vala +++ b/demo/Views/SymbolView.vala @@ -7,35 +7,68 @@ public class SymbolView : DemoPage { construct { title = "Granite.Symbol"; - var audio_volume_symbol = new Granite.Symbol (AUDIO_VOLUME); - - var symbol_button = new SymbolButton (audio_volume_symbol); + var symbol_button = new SymbolButton (AUDIO_VOLUME); var flowbox = new Gtk.FlowBox () { column_spacing = 12, row_spacing = 12, + height_request = 256 + }; + flowbox.append (symbol_button); + flowbox.add_css_class (Granite.CssClass.CARD); + + var size_header = new Granite.HeaderLabel ("Size"); + + var size_scale = new Gtk.Scale.with_range (HORIZONTAL, 16, 128, 1) { + draw_value = true, + hexpand = true + }; + size_scale.adjustment.value = 32; + + var weight_header = new Granite.HeaderLabel ("Weight"); + + var weight_scale = new Gtk.Scale.with_range (HORIZONTAL, 100, 800, 100) { + draw_value = true, + hexpand = true + }; + weight_scale.adjustment.value = 300; + + var main_box = new Granite.Box (VERTICAL) { margin_top = 12, margin_start = 12, margin_end = 12, margin_bottom = 12 }; - flowbox.append (symbol_button); - flowbox.add_css_class (Granite.CssClass.CARD); + main_box.append (flowbox); + main_box.append (size_header); + main_box.append (size_scale); + main_box.append (weight_header); + main_box.append (weight_scale); + + child = main_box; - child = flowbox; + size_scale.adjustment.bind_property ("value", symbol_button, "pixel-size", SYNC_CREATE); + weight_scale.adjustment.bind_property ("value", symbol_button, "weight", SYNC_CREATE); } private class SymbolButton : Gtk.Button { - public Granite.Symbol symbol { get; construct; } + public int pixel_size { get; set; } + public double weight { get; set; } + public Granite.SymbolName symbol_name { get; construct; } - public SymbolButton (Granite.Symbol symbol) { - Object (symbol: symbol); + public SymbolButton (Granite.SymbolName symbol_name) { + Object (symbol_name: symbol_name); } construct { + var symbol = new Granite.Symbol (symbol_name); + child = symbol; add_css_class ("image-button"); + bind_property ("pixel-size", symbol, "pixel-size", SYNC_CREATE); + bind_property ("weight", symbol, "weight", SYNC_CREATE); + clicked.connect (() => { if (symbol.state_index == symbol.states_length - 1) { symbol.state_index = 0; diff --git a/lib/Widgets/Symbol.vala b/lib/Widgets/Symbol.vala index 9c263b1a0..8fb347330 100644 --- a/lib/Widgets/Symbol.vala +++ b/lib/Widgets/Symbol.vala @@ -36,6 +36,11 @@ public class Granite.Symbol : Granite.Bin { set { image.pixel_size = value; } } + public double weight { + get { return svg.weight; } + set { svg.weight = value; } + } + public uint states_length { get { uint length = -1;