[libadwaita/wip/cdavis/spin-row: 34/34] Add AdwSpinRow




commit ab373c6b5b7b531a8fd12894f687bbdc3114ea40
Author: Christopher Davis <christopherdavis gnome org>
Date:   Mon Jul 11 17:47:34 2022 -0400

    Add AdwSpinRow

 demo/pages/lists/adw-demo-page-lists.ui |  14 +
 src/adw-spin-row.c                      | 618 ++++++++++++++++++++++++++++++++
 src/adw-spin-row.h                      |  99 +++++
 src/adw-spin-row.ui                     | 105 ++++++
 src/adwaita.gresources.xml              |   1 +
 src/adwaita.h                           |   1 +
 src/meson.build                         |   2 +
 src/stylesheet/widgets/_lists.scss      |  80 ++++-
 8 files changed, 903 insertions(+), 17 deletions(-)
---
diff --git a/demo/pages/lists/adw-demo-page-lists.ui b/demo/pages/lists/adw-demo-page-lists.ui
index 03780e56..406e43de 100644
--- a/demo/pages/lists/adw-demo-page-lists.ui
+++ b/demo/pages/lists/adw-demo-page-lists.ui
@@ -105,6 +105,20 @@
                         <property name="title" translatable="yes">Password Entry</property>
                       </object>
                     </child>
+                    <child>
+                      <object class="AdwSpinRow">
+                        <property name="title" translatable="yes">Spin Row</property>
+                        <property name="adjustment">
+                          <object class="GtkAdjustment">
+                            <property name="lower">1998</property>
+                            <property name="upper">2022</property>
+                            <property name="value">1998</property>
+                            <property name="page-increment">10</property>
+                            <property name="step-increment">1</property>
+                          </object>
+                        </property>
+                      </object>
+                    </child>
                   </object>
                 </child>
                 <child>
diff --git a/src/adw-spin-row.c b/src/adw-spin-row.c
new file mode 100644
index 00000000..58d623c7
--- /dev/null
+++ b/src/adw-spin-row.c
@@ -0,0 +1,618 @@
+/*
+ * Copyright 2022 Christopher Davis <christopherdavis gnome org>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+#include <math.h>
+
+#include "adw-spin-row.h"
+
+#include "adw-macros-private.h"
+#include "adw-widget-utils-private.h"
+
+#define MAX_DIGITS 20
+
+/**
+ * AdwSpinRow
+ */
+
+struct _AdwSpinRow
+{
+  AdwPreferencesRow parent_instance;
+
+  GtkWidget *header;
+  GtkWidget *title;
+  GtkWidget *subtitle;
+  GtkWidget *prefixes;
+  GtkWidget *suffixes;
+  GtkWidget *title_box;
+
+  GtkWidget *spin_button;
+};
+
+static void adw_spin_row_editable_init (GtkEditableInterface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwSpinRow, adw_spin_row, ADW_TYPE_PREFERENCES_ROW,
+                               G_IMPLEMENT_INTERFACE (GTK_TYPE_EDITABLE, adw_spin_row_editable_init))
+
+enum {
+  PROP_0,
+  PROP_ADJUSTMENT,
+  PROP_CLIMB_RATE,
+  PROP_DIGITS,
+  PROP_NUMERIC,
+  PROP_SNAP_TO_TICKS,
+  PROP_UPDATE_POLICY,
+  PROP_VALUE,
+  PROP_WRAP,
+  PROP_LAST_PROP,
+};
+
+static GParamSpec *props[PROP_LAST_PROP];
+
+enum {
+  SIGNAL_INPUT,
+  SIGNAL_OUTPUT,
+  SIGNAL_VALUE_CHANGED,
+  SIGNAL_WRAPPED,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static gboolean
+boolean_handled_accumulator (GSignalInvocationHint *ihint,
+                             GValue                *return_accu,
+                             const GValue          *handler_return,
+                             gpointer               dummy)
+{
+  gboolean continue_emission;
+  gboolean signal_handled;
+
+  signal_handled = g_value_get_boolean (handler_return);
+  g_value_set_boolean (return_accu, signal_handled);
+  continue_emission = !signal_handled;
+
+  return continue_emission;
+}
+
+static gboolean
+string_is_not_empty (AdwSpinRow *self,
+                     const char *string)
+{
+  return string && string[0];
+}
+
+static void
+pressed_cb (GtkGesture  *gesture,
+            int          n_press,
+            double       x,
+            double       y,
+            AdwSpinRow *self)
+{
+  GtkWidget *picked;
+
+  picked = gtk_widget_pick (GTK_WIDGET (self), x, y, GTK_PICK_DEFAULT);
+
+  if (picked != GTK_WIDGET (self) &&
+      picked != self->header &&
+      picked != GTK_WIDGET (self->prefixes) &&
+      picked != GTK_WIDGET (self->suffixes)) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  gtk_widget_grab_focus (GTK_WIDGET (self->spin_button));
+
+  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static void
+spin_button_state_flags_changed_cb (AdwSpinRow *self)
+{
+  GtkStateFlags flags = gtk_widget_get_state_flags (self->spin_button);
+
+  if (flags & GTK_STATE_FLAG_FOCUS_WITHIN) {
+    gtk_widget_add_css_class (GTK_WIDGET (self), "focused");
+  } else {
+    gtk_widget_remove_css_class (GTK_WIDGET (self), "focused");
+  }
+}
+
+static gboolean
+spin_button_keynav_failed_cb (AdwSpinRow       *self,
+                              GtkDirectionType  direction)
+{
+  if (direction == GTK_DIR_LEFT || direction == GTK_DIR_RIGHT)
+    return gtk_widget_child_focus (GTK_WIDGET (self), direction);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static int
+spin_button_input_cb (AdwSpinRow *self,
+                      double     *new_value)
+{
+  int return_value;
+
+  g_signal_emit (self, signals[SIGNAL_INPUT], 0, &new_value, &return_value);
+
+  return return_value;
+}
+
+static gboolean
+spin_button_output_cb (AdwSpinRow *self)
+{
+  gboolean return_value;
+
+  g_signal_emit (self, signals[SIGNAL_OUTPUT], 0, &return_value);
+
+  return return_value;
+}
+
+static void
+spin_button_value_changed_cb (AdwSpinRow *self)
+{
+  g_signal_emit (self, signals[SIGNAL_VALUE_CHANGED], 0);
+}
+
+static void
+spin_button_wrapped_cb (AdwSpinRow *self)
+{
+  g_signal_emit (self, signals[SIGNAL_WRAPPED], 0);
+}
+
+static gboolean
+adw_spin_row_grab_focus (GtkWidget *widget)
+{
+  AdwSpinRow *self = ADW_SPIN_ROW (widget);
+
+  return gtk_widget_grab_focus (self->spin_button);
+}
+
+static void
+adw_spin_row_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  AdwSpinRow *self = ADW_SPIN_ROW (object);
+
+  if (gtk_editable_delegate_get_property (object, prop_id, value, pspec))
+    return;
+
+  switch (prop_id) {
+  case PROP_ADJUSTMENT:
+    g_value_set_object (value, adw_spin_row_get_adjustment (self));
+    break;
+  case PROP_CLIMB_RATE:
+    g_value_set_double (value, adw_spin_row_get_climb_rate (self));
+    break;
+  case PROP_DIGITS:
+    g_value_set_uint (value, adw_spin_row_get_digits (self));
+    break;
+  case PROP_NUMERIC:
+    g_value_set_boolean (value, adw_spin_row_get_numeric (self));
+    break;
+  case PROP_SNAP_TO_TICKS:
+    g_value_set_boolean (value, adw_spin_row_get_snap_to_ticks (self));
+    break;
+  case PROP_UPDATE_POLICY:
+    g_value_set_enum (value, adw_spin_row_get_update_policy (self));
+    break;
+  case PROP_VALUE:
+    g_value_set_double (value, adw_spin_row_get_value (self));
+    break;
+  case PROP_WRAP:
+    g_value_set_boolean (value, adw_spin_row_get_wrap (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_spin_row_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  AdwSpinRow *self = ADW_SPIN_ROW (object);
+
+  if (gtk_editable_delegate_set_property (object, prop_id, value, pspec))
+    return;
+
+  switch (prop_id) {
+  case PROP_ADJUSTMENT:
+    adw_spin_row_set_adjustment (self, g_value_get_object (value));
+    break;
+  case PROP_CLIMB_RATE:
+    adw_spin_row_set_climb_rate (self, g_value_get_double (value));
+    break;
+  case PROP_DIGITS:
+    adw_spin_row_set_digits (self, g_value_get_uint (value));
+    break;
+  case PROP_NUMERIC:
+    adw_spin_row_set_numeric (self, g_value_get_boolean (value));
+    break;
+  case PROP_SNAP_TO_TICKS:
+    adw_spin_row_set_snap_to_ticks (self, g_value_get_boolean (value));
+    break;
+  case PROP_UPDATE_POLICY:
+    adw_spin_row_set_update_policy (self, g_value_get_enum (value));
+    break;
+  case PROP_VALUE:
+    adw_spin_row_set_value (self, g_value_get_double (value));
+    break;
+  case PROP_WRAP:
+    adw_spin_row_set_wrap (self, g_value_get_boolean (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_spin_row_class_init (AdwSpinRowClass *klass)
+{
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->get_property = adw_spin_row_get_property;
+  object_class->set_property = adw_spin_row_set_property;
+
+  widget_class->focus = adw_widget_focus_child;
+  widget_class->grab_focus = adw_spin_row_grab_focus;
+
+  props[PROP_ADJUSTMENT] =
+    g_param_spec_object ("adjustment", NULL, NULL,
+                         GTK_TYPE_ADJUSTMENT,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+  props[PROP_CLIMB_RATE] =
+    g_param_spec_double ("climb-rate", NULL, NULL,
+                         0.0, G_MAXDOUBLE, 0.0,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+  props[PROP_DIGITS] =
+    g_param_spec_uint ("digits", NULL, NULL,
+                       0, MAX_DIGITS, 0,
+                       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+  props[PROP_NUMERIC] =
+    g_param_spec_boolean ("numeric", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+  props[PROP_SNAP_TO_TICKS] =
+    g_param_spec_boolean ("snap-to-ticks", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+  props[PROP_UPDATE_POLICY] =
+    g_param_spec_enum ("update-policy", NULL, NULL,
+                       GTK_TYPE_SPIN_BUTTON_UPDATE_POLICY,
+                       GTK_UPDATE_ALWAYS,
+                       G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+  props[PROP_VALUE] =
+    g_param_spec_double ("value", NULL, NULL,
+                         -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+  props[PROP_WRAP] =
+    g_param_spec_boolean ("wrap", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+  gtk_editable_install_properties (object_class, PROP_LAST_PROP);
+
+  signals[SIGNAL_INPUT] =
+    g_signal_new ("input",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_INT, 1,
+                  G_TYPE_POINTER);
+
+  signals[SIGNAL_OUTPUT] =
+    g_signal_new ("output",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  boolean_handled_accumulator, NULL,
+                  NULL,
+                  G_TYPE_BOOLEAN, 0);
+
+  signals[SIGNAL_VALUE_CHANGED] =
+    g_signal_new ("value-changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+
+  signals[SIGNAL_WRAPPED] =
+    g_signal_new ("wrapped",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 0);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/Adwaita/ui/adw-spin-row.ui");
+  gtk_widget_class_bind_template_child (widget_class, AdwSpinRow, header);
+  gtk_widget_class_bind_template_child (widget_class, AdwSpinRow, prefixes);
+  gtk_widget_class_bind_template_child (widget_class, AdwSpinRow, suffixes);
+  gtk_widget_class_bind_template_child (widget_class, AdwSpinRow, title_box);
+  gtk_widget_class_bind_template_child (widget_class, AdwSpinRow, title);
+  gtk_widget_class_bind_template_child (widget_class, AdwSpinRow, subtitle);
+  gtk_widget_class_bind_template_child (widget_class, AdwSpinRow, spin_button);
+
+  gtk_widget_class_bind_template_callback (widget_class, string_is_not_empty);
+  gtk_widget_class_bind_template_callback (widget_class, pressed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, spin_button_state_flags_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, spin_button_keynav_failed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, spin_button_input_cb);
+  gtk_widget_class_bind_template_callback (widget_class, spin_button_output_cb);
+  gtk_widget_class_bind_template_callback (widget_class, spin_button_value_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, spin_button_wrapped_cb);
+
+  gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_TEXT_BOX);
+}
+
+static void
+adw_spin_row_init (AdwSpinRow *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+  gtk_editable_init_delegate (GTK_EDITABLE (self));
+}
+
+static GtkEditable *
+adw_spin_row_editable_get_delegate (GtkEditable *editable)
+{
+  AdwSpinRow *self = ADW_SPIN_ROW (editable);
+  return GTK_EDITABLE (self->spin_button);
+}
+
+void
+adw_spin_row_editable_init (GtkEditableInterface *iface)
+{
+    iface->get_delegate = adw_spin_row_editable_get_delegate;
+}
+
+GtkWidget *
+adw_spin_row_new (GtkAdjustment *adjustment,
+                  double         climb_rate,
+                  guint          digits)
+{
+  AdwSpinRow *self = g_object_new (ADW_TYPE_SPIN_ROW, NULL);
+
+  adw_spin_row_configure (self, adjustment, climb_rate, digits);
+
+  return GTK_WIDGET (self);
+}
+
+GtkWidget *
+adw_spin_row_new_with_range (double min,
+                             double max,
+                             double step)
+{
+
+  AdwSpinRow *self = g_object_new (ADW_TYPE_SPIN_ROW, NULL);
+  GtkAdjustment *adjustment;
+  int digits;
+
+  g_return_val_if_fail (min <= max, NULL);
+  g_return_val_if_fail (step != 0.0, NULL);
+
+  adjustment = gtk_adjustment_new (min, min, max, step, 10 * step, 0);
+
+  if (fabs (step) >= 1.0 || step == 0.0) {
+      digits = 0;
+  } else {
+      digits = abs ((int) floor (log10 (fabs (step))));
+      if (digits > MAX_DIGITS)
+        digits = MAX_DIGITS;
+  }
+
+  adw_spin_row_configure (self, adjustment, step, digits);
+
+  adw_spin_row_set_numeric (self, TRUE);
+
+  return GTK_WIDGET (self);
+}
+
+void
+adw_spin_row_configure (AdwSpinRow    *self,
+                        GtkAdjustment *adjustment,
+                        double         climb_rate,
+                        guint          digits)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  g_object_freeze_notify (G_OBJECT (self));
+
+  adw_spin_row_set_adjustment (self, adjustment);
+  adw_spin_row_set_climb_rate (self, climb_rate);
+  adw_spin_row_set_digits (self, digits);
+
+  g_object_thaw_notify (G_OBJECT (self));
+}
+
+GtkAdjustment *
+adw_spin_row_get_adjustment (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), NULL);
+
+  return gtk_spin_button_get_adjustment (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_adjustment (AdwSpinRow    *self,
+                             GtkAdjustment *adjustment)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (adjustment == adw_spin_row_get_adjustment (self))
+    return;
+
+  gtk_spin_button_set_adjustment (GTK_SPIN_BUTTON (self->spin_button), adjustment);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ADJUSTMENT]);
+}
+
+double
+adw_spin_row_get_climb_rate (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), 0.0);
+
+  return gtk_spin_button_get_climb_rate (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_climb_rate (AdwSpinRow *self,
+                             double      climb_rate)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (climb_rate == adw_spin_row_get_climb_rate (self))
+    return;
+
+  gtk_spin_button_set_climb_rate (GTK_SPIN_BUTTON (self->spin_button), climb_rate);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CLIMB_RATE]);
+}
+
+guint
+adw_spin_row_get_digits (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), 0);
+
+  return gtk_spin_button_get_digits (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_digits (AdwSpinRow *self,
+                         guint       digits)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (digits == adw_spin_row_get_digits (self))
+    return;
+
+  gtk_spin_button_set_digits (GTK_SPIN_BUTTON (self->spin_button), digits);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DIGITS]);
+}
+
+gboolean
+adw_spin_row_get_numeric (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), FALSE);
+
+  return gtk_spin_button_get_numeric (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_numeric (AdwSpinRow *self,
+                          gboolean    numeric)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (numeric == adw_spin_row_get_numeric (self))
+    return;
+
+  gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (self->spin_button), numeric);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NUMERIC]);
+}
+
+gboolean
+adw_spin_row_get_snap_to_ticks (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), FALSE);
+
+  return gtk_spin_button_get_snap_to_ticks (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_snap_to_ticks (AdwSpinRow *self,
+                                gboolean    snap_to_ticks)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (snap_to_ticks == adw_spin_row_get_snap_to_ticks (self))
+    return;
+
+  gtk_spin_button_set_snap_to_ticks (GTK_SPIN_BUTTON (self->spin_button), snap_to_ticks);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SNAP_TO_TICKS]);
+}
+
+GtkSpinButtonUpdatePolicy
+adw_spin_row_get_update_policy (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), GTK_UPDATE_ALWAYS);
+
+  return gtk_spin_button_get_update_policy (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_update_policy (AdwSpinRow *self,
+                                GtkSpinButtonUpdatePolicy policy)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (policy == adw_spin_row_get_update_policy (self))
+    return;
+
+  gtk_spin_button_set_update_policy (GTK_SPIN_BUTTON (self->spin_button), policy);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_UPDATE_POLICY]);
+}
+
+double
+adw_spin_row_get_value (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), 0.0);
+
+  return gtk_spin_button_get_value (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_value (AdwSpinRow *self,
+                        double      value)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (value == adw_spin_row_get_value (self))
+    return;
+
+  gtk_spin_button_set_value (GTK_SPIN_BUTTON (self->spin_button), value);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VALUE]);
+}
+
+gboolean
+adw_spin_row_get_wrap (AdwSpinRow *self)
+{
+  g_return_val_if_fail (ADW_IS_SPIN_ROW (self), FALSE);
+
+  return gtk_spin_button_get_wrap (GTK_SPIN_BUTTON (self->spin_button));
+}
+
+void
+adw_spin_row_set_wrap (AdwSpinRow *self,
+                       gboolean    wrap)
+{
+  g_return_if_fail (ADW_IS_SPIN_ROW (self));
+
+  if (wrap == adw_spin_row_get_wrap (self))
+    return;
+
+  gtk_spin_button_set_wrap (GTK_SPIN_BUTTON (self->spin_button), wrap);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_WRAP]);
+}
diff --git a/src/adw-spin-row.h b/src/adw-spin-row.h
new file mode 100644
index 00000000..6b4d4552
--- /dev/null
+++ b/src/adw-spin-row.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2022 Christopher Davis <christopherdavis gnome org>
+ *
+ * SPDX-License-Identifier: LGPL-3.0-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+
+#include "adw-preferences-row.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_SPIN_ROW (adw_spin_row_get_type ())
+
+ADW_AVAILABLE_IN_1_2
+G_DECLARE_FINAL_TYPE (AdwSpinRow, adw_spin_row, ADW, SPIN_ROW, AdwPreferencesRow)
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_spin_row_new            (GtkAdjustment *adjustment,
+                                        double         climb_rate,
+                                        guint          digits) G_GNUC_WARN_UNUSED_RESULT;
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_spin_row_new_with_range (double         min,
+                                        double         max,
+                                        double         step) G_GNUC_WARN_UNUSED_RESULT;
+ADW_AVAILABLE_IN_1_2
+void       adw_spin_row_configure      (AdwSpinRow    *self,
+                                        GtkAdjustment *adjustment,
+                                        double         climb_rate,
+                                        guint          digits);
+
+ADW_AVAILABLE_IN_1_2
+GtkAdjustment *adw_spin_row_get_adjustment (AdwSpinRow    *self);
+ADW_AVAILABLE_IN_1_2
+void           adw_spin_row_set_adjustment (AdwSpinRow    *self,
+                                            GtkAdjustment *adjustment);
+
+ADW_AVAILABLE_IN_1_2
+double adw_spin_row_get_climb_rate (AdwSpinRow *self);
+ADW_AVAILABLE_IN_1_2
+void   adw_spin_row_set_climb_rate (AdwSpinRow *self,
+                                    double      climb_rate);
+
+ADW_AVAILABLE_IN_1_2
+guint adw_spin_row_get_digits (AdwSpinRow *self);
+ADW_AVAILABLE_IN_1_2
+void  adw_spin_row_set_digits (AdwSpinRow *self,
+                               guint       digits);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_spin_row_get_numeric (AdwSpinRow *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_spin_row_set_numeric (AdwSpinRow *self,
+                                   gboolean    numeric);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_spin_row_get_snap_to_ticks (AdwSpinRow *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_spin_row_set_snap_to_ticks (AdwSpinRow *self,
+                                         gboolean    snap_to_ticks);
+
+ADW_AVAILABLE_IN_1_2
+GtkSpinButtonUpdatePolicy adw_spin_row_get_update_policy (AdwSpinRow                *self);
+ADW_AVAILABLE_IN_1_2
+void                      adw_spin_row_set_update_policy (AdwSpinRow                *self,
+                                                          GtkSpinButtonUpdatePolicy  policy);
+
+ADW_AVAILABLE_IN_1_2
+double adw_spin_row_get_value (AdwSpinRow *self);
+ADW_AVAILABLE_IN_1_2
+void   adw_spin_row_set_value (AdwSpinRow *self,
+                               double      value);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_spin_row_get_wrap (AdwSpinRow *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_spin_row_set_wrap (AdwSpinRow *self,
+                                gboolean    wrap);
+
+ADW_AVAILABLE_IN_1_2
+void adw_spin_row_add_prefix (AdwSpinRow *self,
+                              GtkWidget  *widget);
+ADW_AVAILABLE_IN_1_2
+void adw_spin_row_add_suffix (AdwSpinRow *self,
+                              GtkWidget  *widget);
+ADW_AVAILABLE_IN_1_2
+void adw_spin_row_remove     (AdwSpinRow *self,
+                              GtkWidget  *widget);
+
+G_END_DECLS
+
diff --git a/src/adw-spin-row.ui b/src/adw-spin-row.ui
new file mode 100644
index 00000000..01258e97
--- /dev/null
+++ b/src/adw-spin-row.ui
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="libadwaita">
+  <requires lib="gtk" version="4.0"/>
+  <template class="AdwSpinRow" parent="AdwPreferencesRow">
+    <accessibility>
+      <relation name="labelled-by">title</relation>
+    </accessibility>
+    <property name="activatable">True</property>
+    <child>
+      <object class="GtkGestureClick">
+        <signal name="pressed" handler="pressed_cb"/>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox" id="header">
+        <property name="valign">center</property>
+        <style>
+          <class name="header"/>
+        </style>
+        <child>
+          <object class="GtkBox" id="prefixes">
+            <property name="visible">False</property>
+            <style>
+              <class name="prefixes"/>
+            </style>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="title_box">
+            <property name="orientation">vertical</property>
+            <property name="valign">center</property>
+            <property name="hexpand">True</property>
+            <style>
+              <class name="title"/>
+            </style>
+            <child>
+              <object class="GtkLabel" id="title">
+                <binding name="visible">
+                  <closure function="string_is_not_empty" type="gboolean">
+                    <lookup name="label">title</lookup>
+                  </closure>
+                </binding>
+                <property name="ellipsize">none</property>
+                <property name="label" bind-source="AdwSpinRow" bind-property="title" 
bind-flags="sync-create"/>
+                <property name="lines">0</property>
+                <property name="mnemonic-widget">AdwSpinRow</property>
+                <property name="use-underline" bind-source="AdwSpinRow" bind-property="use-underline" 
bind-flags="sync-create"/>
+                <property name="selectable" bind-source="AdwSpinRow" bind-property="title-selectable" 
bind-flags="sync-create"/>
+                <property name="wrap">True</property>
+                <property name="wrap-mode">word-char</property>
+                <property name="xalign">0</property>
+                <property name="use-markup" bind-source="AdwSpinRow" bind-property="use-markup" 
bind-flags="sync-create"/>
+                <style>
+                  <class name="title"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkLabel" id="subtitle">
+                <binding name="visible">
+                  <closure function="string_is_not_empty" type="gboolean">
+                    <lookup name="label">subtitle</lookup>
+                  </closure>
+                </binding>
+                <property name="ellipsize">none</property>
+                <property name="lines">0</property>
+                <property name="wrap">True</property>
+                <property name="wrap-mode">word-char</property>
+                <property name="xalign">0</property>
+                <property name="use-markup" bind-source="AdwSpinRow" bind-property="use-markup" 
bind-flags="sync-create"/>
+                <style>
+                  <class name="subtitle"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSpinButton" id="spin_button">
+            <property name="valign">center</property>
+            <property name="hexpand">True</property>
+            <property name="xalign">1</property>
+            <signal name="state-flags-changed" handler="spin_button_state_flags_changed_cb" swapped="yes"/>
+            <signal name="keynav-failed" handler="spin_button_keynav_failed_cb" swapped="yes"/>
+            <signal name="input" handler="spin_button_input_cb" swapped="yes"/>
+            <signal name="output" handler="spin_button_output_cb" swapped="yes"/>
+            <signal name="value-changed" handler="spin_button_value_changed_cb" swapped="yes"/>
+            <signal name="wrapped" handler="spin_button_wrapped_cb" swapped="yes"/>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="suffixes">
+            <property name="visible">False</property>
+            <style>
+              <class name="suffixes"/>
+            </style>
+          </object>
+        </child>
+     </object>
+    </child>
+    <style>
+      <class name="spin"/>
+    </style>
+  </template>
+</interface>
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 58a0d7c4..df251529 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -21,6 +21,7 @@
     <file preprocess="xml-stripblanks">adw-preferences-group.ui</file>
     <file preprocess="xml-stripblanks">adw-preferences-page.ui</file>
     <file preprocess="xml-stripblanks">adw-preferences-window.ui</file>
+    <file preprocess="xml-stripblanks">adw-spin-row.ui</file>
     <file preprocess="xml-stripblanks">adw-status-page.ui</file>
     <file preprocess="xml-stripblanks">adw-tab.ui</file>
     <file preprocess="xml-stripblanks">adw-tab-bar.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index 509a9740..8f03dcbb 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -55,6 +55,7 @@ G_BEGIN_DECLS
 #include "adw-preferences-page.h"
 #include "adw-preferences-row.h"
 #include "adw-preferences-window.h"
+#include "adw-spin-row.h"
 #include "adw-split-button.h"
 #include "adw-spring-animation.h"
 #include "adw-spring-params.h"
diff --git a/src/meson.build b/src/meson.build
index 54664c1e..ef5336be 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -116,6 +116,7 @@ src_headers = [
   'adw-preferences-page.h',
   'adw-preferences-row.h',
   'adw-preferences-window.h',
+  'adw-spin-row.h',
   'adw-split-button.h',
   'adw-spring-animation.h',
   'adw-spring-params.h',
@@ -180,6 +181,7 @@ src_sources = [
   'adw-preferences-page.c',
   'adw-preferences-row.c',
   'adw-preferences-window.c',
+  'adw-spin-row.c',
   'adw-split-button.c',
   'adw-spring-animation.c',
   'adw-spring-params.c',
diff --git a/src/stylesheet/widgets/_lists.scss b/src/stylesheet/widgets/_lists.scss
index 5cc2c6f8..582da82e 100644
--- a/src/stylesheet/widgets/_lists.scss
+++ b/src/stylesheet/widgets/_lists.scss
@@ -127,30 +127,76 @@ row {
 row.entry {
   @include focus-ring($focus-state: '.focused', $offset: -1px);
 
-  &:not(:selected).activatable.focused:hover,
-  &:not(:selected).activatable.focused:active {
-    background-color: transparent;
-  }
+  @at-root %entry_row & {
+    &:not(:selected).activatable.focused:hover,
+    &:not(:selected).activatable.focused:active {
+      background-color: transparent;
+    }
 
-  .edit-icon, .indicator {
-    min-width: 24px;
-    min-height: 24px;
-    padding: 5px;
-  }
+    .edit-icon, .indicator {
+      min-width: 24px;
+      min-height: 24px;
+      padding: 5px;
+    }
+
+    .edit-icon:disabled {
+      opacity: $strong_disabled_opacity;
+    }
+
+    .indicator {
+      opacity: $dimmer_opacity;
+    }
 
-  .edit-icon:disabled {
-    opacity: $strong_disabled_opacity;
+    &.monospace {
+      font-family: inherit;
+
+      text {
+        font-family: monospace;
+      }
+    }
   }
 
-  .indicator {
-    opacity: $dimmer_opacity;
+  @each $e_type, $e_color in (error,   $error_color),
+                             (warning, $warning_color),
+                             (success, $success_color) {
+    &.#{$e_type} {
+      @include focus-ring($focus-state: '.focused', $offset: -1px, $fc: gtkalpha(currentColor, 
$focus_border_opacity));
+
+      text {
+        > selection:focus-within { background-color: gtkalpha($e_color, .2); }
+
+        > cursor-handle > contents { background-color: currentColor; }
+      }
+    }
   }
+}
 
-  &.monospace {
-    font-family: inherit;
+/***************
+ * AdwSpinRow *
+ ***************/
+
+row.spin {
+  @include focus-ring($focus-state: '.focused', $offset: -1px);
 
-    text {
-      font-family: monospace;
+  @extend %entry_row;
+
+  spinbutton {
+    background: none;
+    border-spacing: 6px;
+    box-shadow: none;
+
+    &, &:focus {
+      outline: none;
+    }
+
+    > 
button.image-button.up:not(.flat):not(.raised):not(.suggested-action):not(.destructive-action):not(.opaque):last-child,
+    > 
button.image-button.down:not(.flat):not(.raised):not(.suggested-action):not(.destructive-action):not(.opaque) 
{
+      &, &:dir(ltr):last-child, &:dir(rtl):first-child {
+        @extend %button_basic;
+        @extend %circular_button;
+
+        border: none;
+      }
     }
   }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]