summaryrefslogtreecommitdiff
path: root/src/widgets/control-center
diff options
context:
space:
mode:
Diffstat (limited to 'src/widgets/control-center')
-rw-r--r--src/widgets/control-center/control-button.c496
-rw-r--r--src/widgets/control-center/control-button.h31
-rw-r--r--src/widgets/control-center/control-details-item.c307
-rw-r--r--src/widgets/control-center/control-details-item.h32
-rw-r--r--src/widgets/control-center/control-details.c289
-rw-r--r--src/widgets/control-center/control-details.h22
-rw-r--r--src/widgets/control-center/control-slider.c428
-rw-r--r--src/widgets/control-center/control-slider.h28
-rw-r--r--src/widgets/control-center/meson.build6
9 files changed, 1639 insertions, 0 deletions
diff --git a/src/widgets/control-center/control-button.c b/src/widgets/control-center/control-button.c
new file mode 100644
index 0000000..f535a61
--- /dev/null
+++ b/src/widgets/control-center/control-button.c
@@ -0,0 +1,496 @@
+#include "widgets/control-center/control-button.h"
+
+//
+// FoobarControlButton:
+//
+// A button in the "controls" section of the control center. This can be a regular button or a toggle button with an
+// optional secondary button to toggle a details view.
+//
+
+struct _FoobarControlButton
+{
+ GtkWidget parent_instance;
+ gchar* icon_name;
+ gchar* label;
+ gboolean can_expand;
+ gboolean can_toggle;
+ gboolean is_expanded;
+ gboolean is_toggled;
+};
+
+enum
+{
+ PROP_ICON_NAME = 1,
+ PROP_LABEL,
+ PROP_CAN_EXPAND,
+ PROP_CAN_TOGGLE,
+ PROP_IS_EXPANDED,
+ PROP_IS_TOGGLED,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_button_class_init ( FoobarControlButtonClass* klass );
+static void foobar_control_button_init ( FoobarControlButton* self );
+static void foobar_control_button_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_button_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_button_dispose ( GObject* object );
+static void foobar_control_button_finalize ( GObject* object );
+static void foobar_control_button_handle_primary_clicked( GtkButton* button,
+ gpointer userdata );
+static void foobar_control_button_handle_expand_clicked ( GtkButton* button,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarControlButton, foobar_control_button, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control buttons.
+//
+void foobar_control_button_class_init( FoobarControlButtonClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-button" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_button_get_property;
+ object_klass->set_property = foobar_control_button_set_property;
+ object_klass->dispose = foobar_control_button_dispose;
+ object_klass->finalize = foobar_control_button_finalize;
+
+ props[PROP_ICON_NAME] = g_param_spec_string(
+ "icon-name",
+ "Icon Name",
+ "Name of the icon to be displayed next to the label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_LABEL] = g_param_spec_string(
+ "label",
+ "Label",
+ "The button's main label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_CAN_EXPAND] = g_param_spec_boolean(
+ "can-expand",
+ "Can Expand",
+ "Indicates whether some details are hidden and can be revealed by a toggle button next to the main action.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_CAN_TOGGLE] = g_param_spec_boolean(
+ "can-toggle",
+ "Can Toggle",
+ "Indicates whether the main button can be toggled.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_IS_EXPANDED] = g_param_spec_boolean(
+ "is-expanded",
+ "Is Expanded",
+ "Indicates whether additional details should be shown.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_IS_TOGGLED] = g_param_spec_boolean(
+ "is-toggled",
+ "Is Toggled",
+ "Indicates whether the main button is currently toggled.",
+ FALSE,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control buttons.
+//
+void foobar_control_button_init( FoobarControlButton* self )
+{
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ g_object_bind_property(
+ self,
+ "icon-name",
+ icon,
+ "icon-name",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_widget_set_hexpand( label, TRUE );
+ gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( label ), 0 );
+ g_object_bind_property(
+ self,
+ "label",
+ label,
+ "label",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* content = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( content ), icon );
+ gtk_box_append( GTK_BOX( content ), label );
+
+ GtkWidget* primary_button = gtk_button_new( );
+ gtk_widget_add_css_class( primary_button, "primary" );
+ gtk_widget_set_hexpand( primary_button, TRUE );
+ gtk_button_set_child( GTK_BUTTON( primary_button ), content );
+ g_object_bind_property(
+ self,
+ "can-toggle",
+ primary_button,
+ "sensitive",
+ G_BINDING_SYNC_CREATE );
+ g_signal_connect( primary_button, "clicked", G_CALLBACK( foobar_control_button_handle_primary_clicked ), self );
+
+ GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" );
+ gtk_widget_add_css_class( expand_button, "expand" );
+ g_object_bind_property(
+ self,
+ "can-expand",
+ expand_button,
+ "visible",
+ G_BINDING_SYNC_CREATE );
+ g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_button_handle_expand_clicked ), self );
+
+ GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) );
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL );
+ gtk_widget_insert_before( primary_button, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_button_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ g_value_set_string( value, foobar_control_button_get_icon_name( self ) );
+ break;
+ case PROP_LABEL:
+ g_value_set_string( value, foobar_control_button_get_label( self ) );
+ break;
+ case PROP_CAN_EXPAND:
+ g_value_set_boolean( value, foobar_control_button_can_expand( self ) );
+ break;
+ case PROP_CAN_TOGGLE:
+ g_value_set_boolean( value, foobar_control_button_can_toggle( self ) );
+ break;
+ case PROP_IS_EXPANDED:
+ g_value_set_boolean( value, foobar_control_button_is_expanded( self ) );
+ break;
+ case PROP_IS_TOGGLED:
+ g_value_set_boolean( value, foobar_control_button_is_toggled( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_button_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ foobar_control_button_set_icon_name( self, g_value_get_string( value ) );
+ break;
+ case PROP_LABEL:
+ foobar_control_button_set_label( self, g_value_get_string( value ) );
+ break;
+ case PROP_CAN_EXPAND:
+ foobar_control_button_set_can_expand( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_CAN_TOGGLE:
+ foobar_control_button_set_can_toggle( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_IS_EXPANDED:
+ foobar_control_button_set_expanded( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_IS_TOGGLED:
+ foobar_control_button_set_toggled( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control buttons.
+//
+void foobar_control_button_dispose( GObject* object )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_control_button_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for control buttons.
+//
+void foobar_control_button_finalize( GObject* object )
+{
+ FoobarControlButton* self = (FoobarControlButton*)object;
+
+ g_clear_pointer( &self->icon_name, g_free );
+ g_clear_pointer( &self->label, g_free );
+
+ G_OBJECT_CLASS( foobar_control_button_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control button instance.
+//
+GtkWidget* foobar_control_button_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_BUTTON, NULL );
+}
+
+//
+// Get the name of the icon to be displayed next to the label.
+//
+gchar const* foobar_control_button_get_icon_name( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL );
+ return self->icon_name;
+}
+
+//
+// Get the button's main label.
+//
+gchar const* foobar_control_button_get_label( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), NULL );
+ return self->label;
+}
+
+//
+// Check whether some details are hidden and can be revealed by a toggle button next to the main action.
+//
+gboolean foobar_control_button_can_expand( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->can_expand;
+}
+
+//
+// Check whether the main button can be toggled.
+//
+gboolean foobar_control_button_can_toggle( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->can_toggle;
+}
+
+//
+// Check whether additional details should currently be shown.
+//
+gboolean foobar_control_button_is_expanded( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->is_expanded;
+}
+
+//
+// Check whether the main button is currently toggled.
+//
+gboolean foobar_control_button_is_toggled( FoobarControlButton* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ), FALSE );
+ return self->is_toggled;
+}
+
+//
+// Update the name of the icon to be displayed next to the label.
+//
+void foobar_control_button_set_icon_name(
+ FoobarControlButton* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( g_strcmp0( self->icon_name, value ) )
+ {
+ g_clear_pointer( &self->icon_name, g_free );
+ self->icon_name = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] );
+ }
+}
+
+//
+// Update the button's main label.
+//
+void foobar_control_button_set_label(
+ FoobarControlButton* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( g_strcmp0( self->label, value ) )
+ {
+ g_clear_pointer( &self->label, g_free );
+ self->label = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] );
+ }
+}
+
+//
+// Update whether some details are hidden and can be revealed by a toggle button next to the main action.
+//
+void foobar_control_button_set_can_expand(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( !value ) { foobar_control_button_set_expanded( self, FALSE ); }
+
+ value = !!value;
+ if ( self->can_expand != value )
+ {
+ self->can_expand = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] );
+ }
+}
+
+//
+// Update whether the main button can be toggled.
+//
+void foobar_control_button_set_can_toggle(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ if ( !value ) { foobar_control_button_set_toggled( self, FALSE ); }
+
+ value = !!value;
+ if ( self->can_toggle != value )
+ {
+ self->can_toggle = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_TOGGLE] );
+ }
+}
+
+//
+// Update whether additional details are currently shown.
+//
+void foobar_control_button_set_expanded(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ value = value && self->can_expand;
+ if ( self->is_expanded != value )
+ {
+ self->is_expanded = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] );
+
+ if ( self->is_expanded )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ }
+}
+
+//
+// Update whether the main button is currently toggled.
+//
+void foobar_control_button_set_toggled(
+ FoobarControlButton* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_BUTTON( self ) );
+
+ value = value && self->can_toggle;
+ if ( self->is_toggled != value )
+ {
+ self->is_toggled = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_TOGGLED] );
+
+ if ( self->is_toggled )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "toggled" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "toggled" );
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the primary button was clicked.
+//
+// If allowed, this will toggle the button.
+//
+void foobar_control_button_handle_primary_clicked( GtkButton* button, gpointer userdata )
+{
+ (void)button;
+ FoobarControlButton* self = (FoobarControlButton*)userdata;
+
+ if ( foobar_control_button_can_toggle( self ) )
+ {
+ foobar_control_button_set_toggled( self, !foobar_control_button_is_toggled( self ) );
+ }
+}
+
+//
+// Called when the expand button was clicked.
+//
+// If allowed, this will toggle the "expanded" state of the button.
+//
+void foobar_control_button_handle_expand_clicked( GtkButton* button, gpointer userdata )
+{
+ (void)button;
+ FoobarControlButton* self = (FoobarControlButton*)userdata;
+
+ if ( foobar_control_button_can_expand( self ) )
+ {
+ foobar_control_button_set_expanded( self, !foobar_control_button_is_expanded( self ) );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-button.h b/src/widgets/control-center/control-button.h
new file mode 100644
index 0000000..48dcfb1
--- /dev/null
+++ b/src/widgets/control-center/control-button.h
@@ -0,0 +1,31 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_BUTTON foobar_control_button_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarControlButton, foobar_control_button, FOOBAR, CONTROL_BUTTON, GtkWidget )
+
+GtkWidget* foobar_control_button_new ( void );
+gchar const* foobar_control_button_get_icon_name ( FoobarControlButton* self );
+gchar const* foobar_control_button_get_label ( FoobarControlButton* self );
+gboolean foobar_control_button_can_expand ( FoobarControlButton* self );
+gboolean foobar_control_button_can_toggle ( FoobarControlButton* self );
+gboolean foobar_control_button_is_expanded ( FoobarControlButton* self );
+gboolean foobar_control_button_is_toggled ( FoobarControlButton* self );
+void foobar_control_button_set_icon_name ( FoobarControlButton* self,
+ gchar const* value );
+void foobar_control_button_set_label ( FoobarControlButton* self,
+ gchar const* value );
+void foobar_control_button_set_can_expand( FoobarControlButton* self,
+ gboolean value );
+void foobar_control_button_set_can_toggle( FoobarControlButton* self,
+ gboolean value );
+void foobar_control_button_set_expanded ( FoobarControlButton* self,
+ gboolean value );
+void foobar_control_button_set_toggled ( FoobarControlButton* self,
+ gboolean value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/control-details-item.c b/src/widgets/control-center/control-details-item.c
new file mode 100644
index 0000000..f8858d5
--- /dev/null
+++ b/src/widgets/control-center/control-details-item.c
@@ -0,0 +1,307 @@
+#include "widgets/control-center/control-details-item.h"
+
+//
+// FoobarControlDetailsAccessory:
+//
+// The accessory shown next to an item in the control center details list (if any).
+//
+
+G_DEFINE_ENUM_TYPE(
+ FoobarControlDetailsAccessory,
+ foobar_control_details_accessory,
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE, "none" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS, "progress" ),
+ G_DEFINE_ENUM_VALUE( FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED, "checked" ) )
+
+//
+// FoobarControlDetailsItem:
+//
+// Standard row layout for items in a FoobarControlDetails list. The rows are made up of a label and an accessory that
+// is shown or hidden depending on the item's state.
+//
+
+struct _FoobarControlDetailsItem
+{
+ GtkWidget parent_instance;
+ GtkWidget* spinner;
+ GtkWidget* checkmark;
+ gchar* label;
+ FoobarControlDetailsAccessory accessory;
+};
+
+enum
+{
+ PROP_LABEL = 1,
+ PROP_ACCESSORY,
+ PROP_IS_CHECKED,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_details_item_class_init ( FoobarControlDetailsItemClass* klass );
+static void foobar_control_details_item_init ( FoobarControlDetailsItem* self );
+static void foobar_control_details_item_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_details_item_set_property( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_details_item_dispose ( GObject* object );
+static void foobar_control_details_item_finalize ( GObject* object );
+
+G_DEFINE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control details items.
+//
+void foobar_control_details_item_class_init( FoobarControlDetailsItemClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-details-item" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_details_item_get_property;
+ object_klass->set_property = foobar_control_details_item_set_property;
+ object_klass->dispose = foobar_control_details_item_dispose;
+ object_klass->finalize = foobar_control_details_item_finalize;
+
+ props[PROP_LABEL] = g_param_spec_string(
+ "label",
+ "Label",
+ "Label shown for the item.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_ACCESSORY] = g_param_spec_enum(
+ "accessory",
+ "Accessory",
+ "Accessory shown for the item.",
+ FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY,
+ 0,
+ G_PARAM_READWRITE );
+ props[PROP_IS_CHECKED] = g_param_spec_boolean(
+ "is-checked",
+ "Is Checked",
+ "Current checked-state for the item.",
+ FALSE,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control details items.
+//
+void foobar_control_details_item_init( FoobarControlDetailsItem* self )
+{
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_hexpand( label, TRUE );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_label_set_ellipsize( GTK_LABEL( label ), PANGO_ELLIPSIZE_END );
+ gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( label ), 0 );
+ gtk_label_set_wrap( GTK_LABEL( label ), FALSE );
+ g_object_bind_property(
+ self,
+ "label",
+ label,
+ "label",
+ G_BINDING_SYNC_CREATE );
+
+ self->spinner = gtk_spinner_new( );
+ gtk_spinner_set_spinning( GTK_SPINNER( self->spinner ), TRUE );
+ gtk_widget_add_css_class( self->spinner, "accessory" );
+ gtk_widget_set_valign( self->spinner, GTK_ALIGN_CENTER );
+ gtk_widget_set_visible( self->spinner, FALSE );
+
+ self->checkmark = gtk_image_new_from_icon_name( "fluent-checkmark-symbolic" );
+ gtk_widget_add_css_class( self->checkmark, "checkmark" );
+ gtk_widget_add_css_class( self->checkmark, "accessory" );
+ gtk_widget_set_valign( self->checkmark, GTK_ALIGN_CENTER );
+ gtk_widget_set_visible( self->checkmark, FALSE );
+
+ gtk_widget_insert_before( label, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( self->spinner, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( self->checkmark, GTK_WIDGET( self ), NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_details_item_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_LABEL:
+ g_value_set_string( value, foobar_control_details_item_get_label( self ) );
+ break;
+ case PROP_ACCESSORY:
+ g_value_set_enum( value, foobar_control_details_item_get_accessory( self ) );
+ break;
+ case PROP_IS_CHECKED:
+ g_value_set_boolean( value, foobar_control_details_item_is_checked( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_details_item_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_LABEL:
+ foobar_control_details_item_set_label( self, g_value_get_string( value ) );
+ break;
+ case PROP_ACCESSORY:
+ foobar_control_details_item_set_accessory( self, g_value_get_enum( value ) );
+ break;
+ case PROP_IS_CHECKED:
+ foobar_control_details_item_set_checked( self, g_value_get_boolean( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control details items.
+//
+void foobar_control_details_item_dispose( GObject* object )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_control_details_item_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for control details items.
+//
+void foobar_control_details_item_finalize( GObject* object )
+{
+ FoobarControlDetailsItem* self = (FoobarControlDetailsItem*)object;
+
+ g_clear_pointer( &self->label, g_free );
+
+ G_OBJECT_CLASS( foobar_control_details_item_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control details item instance.
+//
+GtkWidget* foobar_control_details_item_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS_ITEM, NULL );
+}
+
+//
+// Get the label shown for the item.
+//
+gchar const* foobar_control_details_item_get_label( FoobarControlDetailsItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), NULL );
+ return self->label;
+}
+
+//
+// Get the accessory shown for the item.
+//
+FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), 0 );
+ return self->accessory;
+}
+
+//
+// Get the current checked-state for the item.
+//
+gboolean foobar_control_details_item_is_checked( FoobarControlDetailsItem* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ), FALSE );
+ return self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED;
+}
+
+//
+// Update the label shown for the item.
+//
+void foobar_control_details_item_set_label(
+ FoobarControlDetailsItem* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) );
+
+ if ( g_strcmp0( self->label, value ) )
+ {
+ g_clear_pointer( &self->label, g_free );
+ self->label = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] );
+ }
+}
+
+//
+// Update the current accessory shown for the item.
+//
+void foobar_control_details_item_set_accessory(
+ FoobarControlDetailsItem* self,
+ FoobarControlDetailsAccessory value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) );
+
+ if ( self->accessory != value )
+ {
+ self->accessory = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ACCESSORY] );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_CHECKED] );
+
+ gtk_widget_set_visible( self->spinner, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS );
+ gtk_widget_set_visible( self->checkmark, self->accessory == FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED );
+ }
+}
+
+//
+// Update the current checked-state for the item.
+//
+void foobar_control_details_item_set_checked(
+ FoobarControlDetailsItem* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS_ITEM( self ) );
+
+ FoobarControlDetailsAccessory accessory = value
+ ? FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED
+ : FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE;
+ foobar_control_details_item_set_accessory( self, accessory );
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-details-item.h b/src/widgets/control-center/control-details-item.h
new file mode 100644
index 0000000..b9be4a7
--- /dev/null
+++ b/src/widgets/control-center/control-details-item.h
@@ -0,0 +1,32 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_DETAILS_ACCESSORY foobar_control_details_accessory_get_type( )
+#define FOOBAR_TYPE_CONTROL_DETAILS_ITEM foobar_control_details_item_get_type( )
+
+typedef enum
+{
+ FOOBAR_CONTROL_DETAILS_ACCESSORY_NONE,
+ FOOBAR_CONTROL_DETAILS_ACCESSORY_CHECKED,
+ FOOBAR_CONTROL_DETAILS_ACCESSORY_PROGRESS,
+} FoobarControlDetailsAccessory;
+
+GType foobar_control_details_accessory_get_type( void );
+
+G_DECLARE_FINAL_TYPE( FoobarControlDetailsItem, foobar_control_details_item, FOOBAR, CONTROL_DETAILS_ITEM, GtkWidget )
+
+GtkWidget* foobar_control_details_item_new ( void );
+gchar const* foobar_control_details_item_get_label ( FoobarControlDetailsItem* self );
+FoobarControlDetailsAccessory foobar_control_details_item_get_accessory( FoobarControlDetailsItem* self );
+gboolean foobar_control_details_item_is_checked ( FoobarControlDetailsItem* self );
+void foobar_control_details_item_set_label ( FoobarControlDetailsItem* self,
+ gchar const* value );
+void foobar_control_details_item_set_accessory( FoobarControlDetailsItem* self,
+ FoobarControlDetailsAccessory value );
+void foobar_control_details_item_set_checked ( FoobarControlDetailsItem* self,
+ gboolean value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/control-details.c b/src/widgets/control-center/control-details.c
new file mode 100644
index 0000000..4f0d422
--- /dev/null
+++ b/src/widgets/control-center/control-details.c
@@ -0,0 +1,289 @@
+#include "widgets/control-center/control-details.h"
+
+//
+// FoobarControlDetails:
+//
+// A revealer for details in the "controls" section of the control center. This widget is usually used to embed a list
+// view. It takes care of scrolling by embedding the child in a GtkScrolledWindow.
+//
+
+struct _FoobarControlDetails
+{
+ GtkWidget parent_instance;
+ GtkWidget* child;
+ gboolean is_expanded;
+ gint inset_top;
+};
+
+enum
+{
+ PROP_CHILD = 1,
+ PROP_IS_EXPANDED,
+ PROP_INSET_TOP,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_details_class_init ( FoobarControlDetailsClass* klass );
+static void foobar_control_details_init ( FoobarControlDetails* self );
+static void foobar_control_details_get_property( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_details_set_property( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_details_dispose ( GObject* object );
+
+G_DEFINE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control details.
+//
+void foobar_control_details_class_init( FoobarControlDetailsClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-details" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BIN_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_details_get_property;
+ object_klass->set_property = foobar_control_details_set_property;
+ object_klass->dispose = foobar_control_details_dispose;
+
+ props[PROP_CHILD] = g_param_spec_object(
+ "child",
+ "Child",
+ "Child widget for the details revealer.",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE );
+ props[PROP_IS_EXPANDED] = g_param_spec_boolean(
+ "is-expanded",
+ "Is Expanded",
+ "Current state of the details revealer.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_INSET_TOP] = g_param_spec_int(
+ "inset-top",
+ "Inset Top",
+ "Inset from the top edge (only visible when expanded).",
+ 0,
+ INT_MAX,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control details.
+//
+void foobar_control_details_init( FoobarControlDetails* self )
+{
+ GtkWidget* scrolled_window = gtk_scrolled_window_new( );
+ gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolled_window ), GTK_POLICY_NEVER, GTK_POLICY_EXTERNAL );
+ gtk_scrolled_window_set_propagate_natural_height( GTK_SCROLLED_WINDOW( scrolled_window ), TRUE );
+ g_object_bind_property(
+ self,
+ "child",
+ scrolled_window,
+ "child",
+ G_BINDING_SYNC_CREATE );
+ g_object_bind_property(
+ self,
+ "inset-top",
+ scrolled_window,
+ "margin-top",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* revealer = gtk_revealer_new( );
+ gtk_revealer_set_transition_type( GTK_REVEALER( revealer ), GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN );
+ gtk_revealer_set_transition_duration( GTK_REVEALER( revealer ), 100 );
+ gtk_revealer_set_child( GTK_REVEALER( revealer ), scrolled_window );
+ g_object_bind_property(
+ self,
+ "is-expanded",
+ revealer,
+ "reveal-child",
+ G_BINDING_SYNC_CREATE );
+
+ gtk_widget_set_parent( revealer, GTK_WIDGET( self ) );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_details_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetails* self = (FoobarControlDetails*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ g_value_set_object( value, foobar_control_details_get_child( self ) );
+ break;
+ case PROP_IS_EXPANDED:
+ g_value_set_boolean( value, foobar_control_details_is_expanded( self ) );
+ break;
+ case PROP_INSET_TOP:
+ g_value_set_int( value, foobar_control_details_get_inset_top( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_details_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlDetails* self = (FoobarControlDetails*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_CHILD:
+ foobar_control_details_set_child( self, g_value_get_object( value ) );
+ break;
+ case PROP_IS_EXPANDED:
+ foobar_control_details_set_expanded( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_INSET_TOP:
+ foobar_control_details_set_inset_top( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control details.
+//
+void foobar_control_details_dispose( GObject* object )
+{
+ FoobarControlDetails* self = (FoobarControlDetails*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ g_clear_object( &self->child );
+
+ G_OBJECT_CLASS( foobar_control_details_parent_class )->dispose( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control details instance.
+//
+GtkWidget* foobar_control_details_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_DETAILS, NULL );
+}
+
+//
+// Get the child widget for the details revealer.
+//
+GtkWidget* foobar_control_details_get_child( FoobarControlDetails* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), NULL );
+ return self->child;
+}
+
+//
+// Get the current state of the details revealer.
+//
+gboolean foobar_control_details_is_expanded( FoobarControlDetails* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE );
+ return self->is_expanded;
+}
+
+//
+// Get the inset from the top edge (only visible when expanded).
+//
+gint foobar_control_details_get_inset_top( FoobarControlDetails* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ), FALSE );
+ return self->inset_top;
+}
+
+//
+// Update the child widget for the details revealer.
+//
+void foobar_control_details_set_child(
+ FoobarControlDetails* self,
+ GtkWidget* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) );
+
+ if ( self->child != value )
+ {
+ g_clear_object( &self->child );
+ if ( value ) { self->child = g_object_ref( value ); }
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CHILD] );
+ }
+}
+
+//
+// Update the current state of the details revealer.
+//
+void foobar_control_details_set_expanded(
+ FoobarControlDetails* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) );
+
+ value = !!value;
+ if ( self->is_expanded != value )
+ {
+ self->is_expanded = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] );
+
+ if ( self->is_expanded )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ }
+}
+
+//
+// Update the inset from the top edge (only visible when expanded).
+//
+void foobar_control_details_set_inset_top(
+ FoobarControlDetails* self,
+ gint value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_DETAILS( self ) );
+
+ value = MAX( value, 0 );
+ if ( self->inset_top != value )
+ {
+ self->inset_top = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_INSET_TOP] );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-details.h b/src/widgets/control-center/control-details.h
new file mode 100644
index 0000000..71df683
--- /dev/null
+++ b/src/widgets/control-center/control-details.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_DETAILS foobar_control_details_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarControlDetails, foobar_control_details, FOOBAR, CONTROL_DETAILS, GtkWidget )
+
+GtkWidget* foobar_control_details_new ( void );
+GtkWidget* foobar_control_details_get_child ( FoobarControlDetails* self );
+gboolean foobar_control_details_is_expanded ( FoobarControlDetails* self );
+gint foobar_control_details_get_inset_top( FoobarControlDetails* self );
+void foobar_control_details_set_child ( FoobarControlDetails* self,
+ GtkWidget* value );
+void foobar_control_details_set_expanded ( FoobarControlDetails* self,
+ gboolean value );
+void foobar_control_details_set_inset_top( FoobarControlDetails* self,
+ gint value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/control-slider.c b/src/widgets/control-center/control-slider.c
new file mode 100644
index 0000000..d98dc9e
--- /dev/null
+++ b/src/widgets/control-center/control-slider.c
@@ -0,0 +1,428 @@
+#include "widgets/control-center/control-slider.h"
+
+//
+// FoobarControlSlider:
+//
+// A slider in the "controls" section of the control center with a title and icon, as well as an optional secondary
+// button to toggle a details view.
+//
+
+struct _FoobarControlSlider
+{
+ GtkWidget parent_instance;
+ gchar* icon_name;
+ gchar* label;
+ gboolean can_expand;
+ gboolean is_expanded;
+ gint percentage;
+};
+
+enum
+{
+ PROP_ICON_NAME = 1,
+ PROP_LABEL,
+ PROP_CAN_EXPAND,
+ PROP_IS_EXPANDED,
+ PROP_PERCENTAGE,
+ N_PROPS,
+};
+
+static GParamSpec* props[N_PROPS] = { 0 };
+
+static void foobar_control_slider_class_init ( FoobarControlSliderClass* klass );
+static void foobar_control_slider_init ( FoobarControlSlider* self );
+static void foobar_control_slider_get_property ( GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec );
+static void foobar_control_slider_set_property ( GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec );
+static void foobar_control_slider_dispose ( GObject* object );
+static void foobar_control_slider_finalize ( GObject* object );
+static void foobar_control_slider_handle_expand_clicked( GtkButton* button,
+ gpointer userdata );
+
+G_DEFINE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, GTK_TYPE_WIDGET )
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Widget Implementation
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Static initialization for control sliders.
+//
+void foobar_control_slider_class_init( FoobarControlSliderClass* klass )
+{
+ GtkWidgetClass* widget_klass = GTK_WIDGET_CLASS( klass );
+ gtk_widget_class_set_css_name( widget_klass, "control-slider" );
+ gtk_widget_class_set_layout_manager_type( widget_klass, GTK_TYPE_BOX_LAYOUT );
+
+ GObjectClass* object_klass = G_OBJECT_CLASS( klass );
+ object_klass->get_property = foobar_control_slider_get_property;
+ object_klass->set_property = foobar_control_slider_set_property;
+ object_klass->dispose = foobar_control_slider_dispose;
+ object_klass->finalize = foobar_control_slider_finalize;
+
+ props[PROP_ICON_NAME] = g_param_spec_string(
+ "icon-name",
+ "Icon Name",
+ "Name of the icon to be displayed next to the label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_LABEL] = g_param_spec_string(
+ "label",
+ "Label",
+ "The slider's main label.",
+ NULL,
+ G_PARAM_READWRITE );
+ props[PROP_CAN_EXPAND] = g_param_spec_boolean(
+ "can-expand",
+ "Can Expand",
+ "Indicates whether some details are hidden and can be revealed by a toggle button next to the slider.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_IS_EXPANDED] = g_param_spec_boolean(
+ "is-expanded",
+ "Is Expanded",
+ "Indicates whether additional details should be shown.",
+ FALSE,
+ G_PARAM_READWRITE );
+ props[PROP_PERCENTAGE] = g_param_spec_int(
+ "percentage",
+ "Percentage",
+ "Value shown by the slider.",
+ 0,
+ 100,
+ 0,
+ G_PARAM_READWRITE );
+ g_object_class_install_properties( object_klass, N_PROPS, props );
+}
+
+//
+// Instance initialization for control sliders.
+//
+void foobar_control_slider_init( FoobarControlSlider* self )
+{
+ GtkWidget* icon = gtk_image_new( );
+ gtk_widget_set_valign( icon, GTK_ALIGN_CENTER );
+ g_object_bind_property(
+ self,
+ "icon-name",
+ icon,
+ "icon-name",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* label = gtk_label_new( NULL );
+ gtk_widget_set_valign( label, GTK_ALIGN_CENTER );
+ gtk_widget_set_hexpand( label, TRUE );
+ gtk_label_set_justify( GTK_LABEL( label ), GTK_JUSTIFY_LEFT );
+ gtk_label_set_xalign( GTK_LABEL( label ), 0 );
+ g_object_bind_property(
+ self,
+ "label",
+ label,
+ "label",
+ G_BINDING_SYNC_CREATE );
+
+ GtkWidget* header = gtk_box_new( GTK_ORIENTATION_HORIZONTAL, 0 );
+ gtk_box_append( GTK_BOX( header ), icon );
+ gtk_box_append( GTK_BOX( header ), label );
+
+ GtkWidget* scale = gtk_scale_new_with_range( GTK_ORIENTATION_HORIZONTAL, 0, 100, 2 );
+ g_object_bind_property(
+ self,
+ "percentage",
+ gtk_range_get_adjustment( GTK_RANGE( scale ) ),
+ "value",
+ G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL );
+
+ GtkWidget* primary_content = gtk_box_new( GTK_ORIENTATION_VERTICAL, 0 );
+ gtk_widget_add_css_class( primary_content, "primary" );
+ gtk_box_append( GTK_BOX( primary_content ), header );
+ gtk_box_append( GTK_BOX( primary_content ), scale );
+
+ GtkWidget* expand_button = gtk_button_new_from_icon_name( "fluent-chevron-right-symbolic" );
+ gtk_widget_add_css_class( expand_button, "expand" );
+ g_object_bind_property(
+ self,
+ "can-expand",
+ expand_button,
+ "visible",
+ G_BINDING_SYNC_CREATE );
+ g_signal_connect( expand_button, "clicked", G_CALLBACK( foobar_control_slider_handle_expand_clicked ), self );
+
+ GtkLayoutManager* layout = gtk_widget_get_layout_manager( GTK_WIDGET( self ) );
+ gtk_orientable_set_orientation( GTK_ORIENTABLE( layout ), GTK_ORIENTATION_HORIZONTAL );
+ gtk_widget_insert_before( primary_content, GTK_WIDGET( self ), NULL );
+ gtk_widget_insert_before( expand_button, GTK_WIDGET( self ), NULL );
+}
+
+//
+// Property getter implementation, mapping a property id to a method.
+//
+void foobar_control_slider_get_property(
+ GObject* object,
+ guint prop_id,
+ GValue* value,
+ GParamSpec* pspec )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ g_value_set_string( value, foobar_control_slider_get_icon_name( self ) );
+ break;
+ case PROP_LABEL:
+ g_value_set_string( value, foobar_control_slider_get_label( self ) );
+ break;
+ case PROP_CAN_EXPAND:
+ g_value_set_boolean( value, foobar_control_slider_can_expand( self ) );
+ break;
+ case PROP_IS_EXPANDED:
+ g_value_set_boolean( value, foobar_control_slider_is_expanded( self ) );
+ break;
+ case PROP_PERCENTAGE:
+ g_value_set_int( value, foobar_control_slider_get_percentage( self ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Property setter implementation, mapping a property id to a method.
+//
+void foobar_control_slider_set_property(
+ GObject* object,
+ guint prop_id,
+ GValue const* value,
+ GParamSpec* pspec )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ switch ( prop_id )
+ {
+ case PROP_ICON_NAME:
+ foobar_control_slider_set_icon_name( self, g_value_get_string( value ) );
+ break;
+ case PROP_LABEL:
+ foobar_control_slider_set_label( self, g_value_get_string( value ) );
+ break;
+ case PROP_CAN_EXPAND:
+ foobar_control_slider_set_can_expand( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_IS_EXPANDED:
+ foobar_control_slider_set_expanded( self, g_value_get_boolean( value ) );
+ break;
+ case PROP_PERCENTAGE:
+ foobar_control_slider_set_percentage( self, g_value_get_int( value ) );
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID( object, prop_id, pspec );
+ break;
+ }
+}
+
+//
+// Instance de-initialization for control sliders.
+//
+void foobar_control_slider_dispose( GObject* object )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ GtkWidget* child;
+ while ( ( child = gtk_widget_get_first_child( GTK_WIDGET( self ) ) ) )
+ {
+ gtk_widget_unparent( child );
+ }
+
+ G_OBJECT_CLASS( foobar_control_slider_parent_class )->dispose( object );
+}
+
+//
+// Instance cleanup for control sliders.
+//
+void foobar_control_slider_finalize( GObject* object )
+{
+ FoobarControlSlider* self = (FoobarControlSlider*)object;
+
+ g_clear_pointer( &self->icon_name, g_free );
+ g_clear_pointer( &self->label, g_free );
+
+ G_OBJECT_CLASS( foobar_control_slider_parent_class )->finalize( object );
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Public API
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Create a new control slider instance.
+//
+GtkWidget* foobar_control_slider_new( void )
+{
+ return g_object_new( FOOBAR_TYPE_CONTROL_SLIDER, NULL );
+}
+
+//
+// Get the name of the icon to be displayed next to the label.
+//
+gchar const* foobar_control_slider_get_icon_name( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL );
+ return self->icon_name;
+}
+
+//
+// Get the slider's main label.
+//
+gchar const* foobar_control_slider_get_label( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), NULL );
+ return self->label;
+}
+
+//
+// Check whether some details are hidden and can be revealed by a toggle button next to the slider.
+//
+gboolean foobar_control_slider_can_expand( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE );
+ return self->can_expand;
+}
+
+//
+// Check whether additional details should currently be shown.
+//
+gboolean foobar_control_slider_is_expanded( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE );
+ return self->is_expanded;
+}
+
+//
+// Get the value shown by the slider.
+//
+gint foobar_control_slider_get_percentage( FoobarControlSlider* self )
+{
+ g_return_val_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ), FALSE );
+ return self->percentage;
+}
+
+//
+// Update the name of the icon to be displayed next to the label.
+//
+void foobar_control_slider_set_icon_name(
+ FoobarControlSlider* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ if ( g_strcmp0( self->icon_name, value ) )
+ {
+ g_clear_pointer( &self->icon_name, g_free );
+ self->icon_name = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_ICON_NAME] );
+ }
+}
+
+//
+// Update the slider's main label.
+//
+void foobar_control_slider_set_label(
+ FoobarControlSlider* self,
+ gchar const* value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ if ( g_strcmp0( self->label, value ) )
+ {
+ g_clear_pointer( &self->label, g_free );
+ self->label = g_strdup( value );
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_LABEL] );
+ }
+}
+
+//
+// Update whether some details are hidden and can be revealed by a toggle button next to the slider.
+//
+void foobar_control_slider_set_can_expand(
+ FoobarControlSlider* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ if ( !value ) { foobar_control_slider_set_expanded( self, FALSE ); }
+
+ value = !!value;
+ if ( self->can_expand != value )
+ {
+ self->can_expand = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_CAN_EXPAND] );
+ }
+}
+
+//
+// Update whether additional details are currently shown.
+//
+void foobar_control_slider_set_expanded(
+ FoobarControlSlider* self,
+ gboolean value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ value = value && self->can_expand;
+ if ( self->is_expanded != value )
+ {
+ self->is_expanded = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_IS_EXPANDED] );
+
+ if ( self->is_expanded )
+ {
+ gtk_widget_add_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ else
+ {
+ gtk_widget_remove_css_class( GTK_WIDGET( self ), "expanded" );
+ }
+ }
+}
+
+//
+// Update the value shown by the slider.
+//
+void foobar_control_slider_set_percentage( FoobarControlSlider* self, gint value )
+{
+ g_return_if_fail( FOOBAR_IS_CONTROL_SLIDER( self ) );
+
+ value = CLAMP( value, 0, 100 );
+ if ( self->percentage != value )
+ {
+ self->percentage = value;
+ g_object_notify_by_pspec( G_OBJECT( self ), props[PROP_PERCENTAGE] );
+ }
+}
+
+// ---------------------------------------------------------------------------------------------------------------------
+// Signal Handlers
+// ---------------------------------------------------------------------------------------------------------------------
+
+//
+// Called when the expand button was clicked.
+//
+// If allowed, this will toggle the "expanded" state of the slider.
+//
+void foobar_control_slider_handle_expand_clicked( GtkButton* button, gpointer userdata )
+{
+ (void)button;
+ FoobarControlSlider* self = (FoobarControlSlider*)userdata;
+
+ if ( foobar_control_slider_can_expand( self ) )
+ {
+ foobar_control_slider_set_expanded( self, !foobar_control_slider_is_expanded( self ) );
+ }
+} \ No newline at end of file
diff --git a/src/widgets/control-center/control-slider.h b/src/widgets/control-center/control-slider.h
new file mode 100644
index 0000000..e7dfac0
--- /dev/null
+++ b/src/widgets/control-center/control-slider.h
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define FOOBAR_TYPE_CONTROL_SLIDER foobar_control_slider_get_type( )
+
+G_DECLARE_FINAL_TYPE( FoobarControlSlider, foobar_control_slider, FOOBAR, CONTROL_SLIDER, GtkWidget )
+
+GtkWidget* foobar_control_slider_new ( void );
+gchar const* foobar_control_slider_get_icon_name ( FoobarControlSlider* self );
+gchar const* foobar_control_slider_get_label ( FoobarControlSlider* self );
+gboolean foobar_control_slider_can_expand ( FoobarControlSlider* self );
+gboolean foobar_control_slider_is_expanded ( FoobarControlSlider* self );
+gint foobar_control_slider_get_percentage( FoobarControlSlider* self );
+void foobar_control_slider_set_icon_name ( FoobarControlSlider* self,
+ gchar const* value );
+void foobar_control_slider_set_label ( FoobarControlSlider* self,
+ gchar const* value );
+void foobar_control_slider_set_can_expand( FoobarControlSlider* self,
+ gboolean value );
+void foobar_control_slider_set_expanded ( FoobarControlSlider* self,
+ gboolean value );
+void foobar_control_slider_set_percentage( FoobarControlSlider* self,
+ gint value );
+
+G_END_DECLS \ No newline at end of file
diff --git a/src/widgets/control-center/meson.build b/src/widgets/control-center/meson.build
new file mode 100644
index 0000000..5b1252d
--- /dev/null
+++ b/src/widgets/control-center/meson.build
@@ -0,0 +1,6 @@
+foobar_sources += files(
+ 'control-button.c',
+ 'control-slider.c',
+ 'control-details.c',
+ 'control-details-item.c',
+) \ No newline at end of file