diff --git a/demo/GraniteDemo.vala b/demo/GraniteDemo.vala index e1b817367..b202832a2 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..c50e94525 --- /dev/null +++ b/demo/Views/SymbolView.vala @@ -0,0 +1,82 @@ +/* + * Copyright 2026 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: LGPL-3.0-or-later + */ + +public class SymbolView : DemoPage { + construct { + title = "Granite.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 + }; + 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; + + 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 int pixel_size { get; set; } + public double weight { get; set; } + public Granite.SymbolName symbol_name { get; construct; } + + 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; + 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 new file mode 100644 index 000000000..87c26df45 --- /dev/null +++ b/lib/Symbols/CONTRIBUTING.md @@ -0,0 +1,22 @@ +## Canvas Size + +16px x 16px + +## Fill + +10% opacity + +## Strokes + +Main stroke width is 1px + +## Corner Radius + +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/audio-volume.svg b/lib/Symbols/audio-volume.svg new file mode 100644 index 000000000..2b5d1b262 --- /dev/null +++ b/lib/Symbols/audio-volume.svg @@ -0,0 +1,295 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..2e4ef70e1 --- /dev/null +++ b/lib/Symbols/symbols.gresource.xml @@ -0,0 +1,6 @@ + + + + audio-volume.svg + + diff --git a/lib/Widgets/Symbol.vala b/lib/Widgets/Symbol.vala new file mode 100644 index 000000000..8fb347330 --- /dev/null +++ b/lib/Widgets/Symbol.vala @@ -0,0 +1,109 @@ +/* +* 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 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; } + + public int pixel_size { + get { return image.pixel_size; } + set { image.pixel_size = value; } + } + + public double weight { + get { return svg.weight; } + set { svg.weight = value; } + } + + public uint states_length { + get { + uint length = -1; + svg.get_state_names (out length); + + 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; + } + + 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 (SymbolName name) { + Object (resource_path: name.to_path ()); + } + + public Symbol.from_resource (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 (); + }); + } +} diff --git a/lib/meson.build b/lib/meson.build index 57d6ae8ae..98e94cad7 100644 --- a/lib/meson.build +++ b/lib/meson.build @@ -1,5 +1,6 @@ subdir('Icons') subdir('Styles') +subdir('Symbols') libgranite_sources = files( 'Animation.vala', @@ -30,6 +31,7 @@ libgranite_sources = files( 'Widgets/Placeholder.vala', 'Widgets/Settings.vala', 'Widgets/SwitchModelButton.vala', + 'Widgets/Symbol.vala', 'Widgets/TimePicker.vala', 'Widgets/ToolBox.vala', 'Widgets/Toast.vala', @@ -67,6 +69,7 @@ libgranite = library( libgranite_sources, icons_resource, stylesheet_resource, + symbols_resource, config_vala, dependencies: [