From: Robert Ancell <robert.ancell@canonical.com>
Date: Sun, 15 Mar 2020 09:07:51 +0100
Subject: [PATCH 24/26] display: Allow fractional scaling to be enabled

---
 panels/display/cc-display-config-dbus.c |  16 +++
 panels/display/cc-display-config.c      | 237 ++++++++++++++++++++++++++++++++
 panels/display/cc-display-config.h      |   5 +
 panels/display/cc-display-settings.c    |  28 +++-
 panels/display/cc-display-settings.ui   |  16 +++
 5 files changed, 301 insertions(+), 1 deletion(-)

diff --git a/panels/display/cc-display-config-dbus.c b/panels/display/cc-display-config-dbus.c
index fc050a3..7eda67a 100644
--- a/panels/display/cc-display-config-dbus.c
+++ b/panels/display/cc-display-config-dbus.c
@@ -887,6 +887,7 @@ struct _CcDisplayConfigDBus
   gboolean supports_changing_layout_mode;
   gboolean global_scale_required;
   CcDisplayLayoutMode layout_mode;
+  gint legacy_ui_scale;
 
   GList *monitors;
   CcDisplayMonitorDBus *primary;
@@ -1059,6 +1060,9 @@ cc_display_config_dbus_equal (CcDisplayConfig *pself,
   g_return_val_if_fail (pself, FALSE);
   g_return_val_if_fail (pother, FALSE);
 
+  if (self->layout_mode != other->layout_mode)
+    return FALSE;
+
   cc_display_config_dbus_ensure_non_offset_coords (self);
   cc_display_config_dbus_ensure_non_offset_coords (other);
 
@@ -1215,6 +1219,13 @@ cc_display_config_dbus_layout_use_ui_scale (CcDisplayConfig *pself)
   return self->layout_mode == CC_DISPLAY_LAYOUT_MODE_GLOBAL_UI_LOGICAL;
 }
 
+static gint
+cc_display_config_dbus_get_legacy_ui_scale (CcDisplayConfig *pself)
+{
+  CcDisplayConfigDBus *self = CC_DISPLAY_CONFIG_DBUS (pself);
+  return self->legacy_ui_scale;
+}
+
 static gboolean
 is_scaled_mode_allowed (CcDisplayConfigDBus *self,
                         CcDisplayMode       *pmode,
@@ -1469,6 +1480,10 @@ cc_display_config_dbus_constructed (GObject *object)
         {
           g_variant_get (v, "b", &self->global_scale_required);
         }
+      else if (g_str_equal (s, "legacy-ui-scaling-factor"))
+        {
+          g_variant_get (v, "i", &self->legacy_ui_scale);
+        }
       else if (g_str_equal (s, "layout-mode"))
         {
           guint32 u = 0;
@@ -1565,6 +1580,7 @@ cc_display_config_dbus_class_init (CcDisplayConfigDBusClass *klass)
   parent_class->is_scaled_mode_valid = cc_display_config_dbus_is_scaled_mode_valid;
   parent_class->set_minimum_size = cc_display_config_dbus_set_minimum_size;
   parent_class->layout_use_ui_scale = cc_display_config_dbus_layout_use_ui_scale;
+  parent_class->get_legacy_ui_scale = cc_display_config_dbus_get_legacy_ui_scale;
 
   pspec = g_param_spec_variant ("state",
                                 "GVariant",
diff --git a/panels/display/cc-display-config.c b/panels/display/cc-display-config.c
index 552a1a6..2ff5a06 100644
--- a/panels/display/cc-display-config.c
+++ b/panels/display/cc-display-config.c
@@ -17,10 +17,23 @@
  *
  */
 
+#define MUTTER_SCHEMA                     "org.gnome.mutter"
+#define MUTTER_EXPERIMENTAL_FEATURES_KEY  "experimental-features"
+#define MUTTER_FEATURE_FRACTIONAL_SCALING_X11 "x11-randr-fractional-scaling"
+#define MUTTER_FEATURE_FRACTIONAL_SCALING_WAYLAND "scale-monitor-framebuffer"
+
+#include <gdk/gdk.h>
 #include <gio/gio.h>
 #include <math.h>
 #include "cc-display-config.h"
 
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/gdkwayland.h>
+#endif
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#endif
+
 static const double known_diagonals[] = {
   12.1,
   13.3,
@@ -419,6 +432,10 @@ cc_display_monitor_set_ui_info (CcDisplayMonitor *self, gint ui_number, gchar *u
 
 struct _CcDisplayConfigPrivate {
   GList *ui_sorted_monitors;
+
+  GSettings *mutter_settings;
+  gboolean fractional_scaling;
+  gboolean fractional_scaling_pending_disable;
 };
 typedef struct _CcDisplayConfigPrivate CcDisplayConfigPrivate;
 
@@ -426,12 +443,80 @@ G_DEFINE_TYPE_WITH_PRIVATE (CcDisplayConfig,
                             cc_display_config,
                             G_TYPE_OBJECT)
 
+static const char *
+get_fractional_scaling_key (void)
+{
+  GdkDisplay *display;
+
+  display = gdk_display_get_default ();
+
+#if defined(GDK_WINDOWING_X11)
+  if (GDK_IS_X11_DISPLAY (display))
+    return MUTTER_FEATURE_FRACTIONAL_SCALING_X11;
+#endif /* GDK_WINDOWING_X11 */
+#if defined(GDK_WINDOWING_WAYLAND)
+  if (GDK_IS_WAYLAND_DISPLAY (display))
+    return MUTTER_FEATURE_FRACTIONAL_SCALING_WAYLAND;
+#endif /* GDK_WINDOWING_WAYLAND */
+  g_return_val_if_reached (NULL);
+}
+
+static gboolean
+get_fractional_scaling_active (CcDisplayConfig *self)
+{
+  CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
+  g_auto(GStrv) features = NULL;
+  const char *key = get_fractional_scaling_key ();
+
+  g_return_val_if_fail (key, FALSE);
+
+  features = g_settings_get_strv (priv->mutter_settings, MUTTER_EXPERIMENTAL_FEATURES_KEY);
+  return g_strv_contains ((const gchar **) features, key);
+}
+
+static void
+set_fractional_scaling_active (CcDisplayConfig *self,
+                               gboolean         enable)
+{
+  CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
+  g_auto(GStrv) existing_features = NULL;
+  gboolean have_fractional_scaling = FALSE;
+  g_autoptr(GVariantBuilder) builder = NULL;
+  const char *key = get_fractional_scaling_key ();
+
+  /* Add or remove the fractional scaling feature from mutter */
+  existing_features = g_settings_get_strv (priv->mutter_settings,
+                                           MUTTER_EXPERIMENTAL_FEATURES_KEY);
+  builder = g_variant_builder_new (G_VARIANT_TYPE ("as"));
+  for (int i = 0; existing_features[i] != NULL; i++)
+    {
+      if (g_strcmp0 (existing_features[i], key) == 0)
+        {
+          if (enable)
+            have_fractional_scaling = TRUE;
+          else
+            continue;
+        }
+
+      g_variant_builder_add (builder, "s", existing_features[i]);
+    }
+  if (enable && !have_fractional_scaling && key)
+    g_variant_builder_add (builder, "s", key);
+
+  g_settings_set_value (priv->mutter_settings, MUTTER_EXPERIMENTAL_FEATURES_KEY,
+                        g_variant_builder_end (builder));
+}
+
 static void
 cc_display_config_init (CcDisplayConfig *self)
 {
   CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
 
   priv->ui_sorted_monitors = NULL;
+
+  /* No need to connect to the setting, as we'll get notified by mutter */
+  priv->mutter_settings = g_settings_new (MUTTER_SCHEMA);
+  priv->fractional_scaling = get_fractional_scaling_active (self);
 }
 
 static void
@@ -472,6 +557,7 @@ cc_display_config_finalize (GObject *object)
   CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
 
   g_list_free (priv->ui_sorted_monitors);
+  g_clear_object (&priv->mutter_settings);
 
   G_OBJECT_CLASS (cc_display_config_parent_class)->finalize (object);
 }
@@ -557,9 +643,16 @@ gboolean
 cc_display_config_equal (CcDisplayConfig *self,
                          CcDisplayConfig *other)
 {
+  CcDisplayConfigPrivate *spriv = cc_display_config_get_instance_private (self);
+  CcDisplayConfigPrivate *opriv = cc_display_config_get_instance_private (other);
+
   g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), FALSE);
   g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (other), FALSE);
 
+  if (spriv->fractional_scaling_pending_disable !=
+      opriv->fractional_scaling_pending_disable)
+    return FALSE;
+
   return CC_DISPLAY_CONFIG_GET_CLASS (self)->equal (self, other);
 }
 
@@ -567,6 +660,8 @@ gboolean
 cc_display_config_apply (CcDisplayConfig *self,
                          GError **error)
 {
+  CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
+
   if (!CC_IS_DISPLAY_CONFIG (self))
     {
       g_warning ("Cannot apply invalid configuration");
@@ -577,6 +672,12 @@ cc_display_config_apply (CcDisplayConfig *self,
       return FALSE;
     }
 
+  if (priv->fractional_scaling_pending_disable)
+    {
+      set_fractional_scaling_active (self, FALSE);
+      priv->fractional_scaling_pending_disable = FALSE;
+    }
+
   return CC_DISPLAY_CONFIG_GET_CLASS (self)->apply (self, error);
 }
 
@@ -618,13 +719,31 @@ cc_display_config_set_minimum_size (CcDisplayConfig *self,
   CC_DISPLAY_CONFIG_GET_CLASS (self)->set_minimum_size (self, width, height);
 }
 
+gint
+cc_display_config_get_legacy_ui_scale (CcDisplayConfig *self)
+{
+  return CC_DISPLAY_CONFIG_GET_CLASS (self)->get_legacy_ui_scale (self);
+}
+
+static gboolean
+scale_value_is_fractional (double scale)
+{
+  return (int) scale != scale;
+}
+
 gboolean
 cc_display_config_is_scaled_mode_valid (CcDisplayConfig *self,
                                         CcDisplayMode   *mode,
                                         double           scale)
 {
+  CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
+
   g_return_val_if_fail (CC_IS_DISPLAY_CONFIG (self), FALSE);
   g_return_val_if_fail (CC_IS_DISPLAY_MODE (mode), FALSE);
+
+  if (priv->fractional_scaling_pending_disable && scale_value_is_fractional (scale))
+    return FALSE;
+
   return CC_DISPLAY_CONFIG_GET_CLASS (self)->is_scaled_mode_valid (self, mode, scale);
 }
 
@@ -653,3 +772,121 @@ cc_display_config_get_maximum_scaling (CcDisplayConfig *self)
 
   return max_scale;
 }
+
+static gboolean
+set_monitors_scaling_to_preferred_integers (CcDisplayConfig *self)
+{
+  GList *l;
+  gboolean any_changed = FALSE;
+
+  for (l = cc_display_config_get_monitors (self); l; l = l->next)
+    {
+      CcDisplayMonitor *monitor = l->data;
+      gdouble monitor_scale = cc_display_monitor_get_scale (monitor);
+
+      if (scale_value_is_fractional (monitor_scale))
+        {
+          CcDisplayMode *preferred_mode;
+          double preferred_scale;
+          double *saved_scale;
+
+          preferred_mode = cc_display_monitor_get_preferred_mode (monitor);
+          preferred_scale = cc_display_mode_get_preferred_scale (preferred_mode);
+          cc_display_monitor_set_scale (monitor, preferred_scale);
+          any_changed = TRUE;
+
+          saved_scale = g_new (double, 1);
+          *saved_scale = monitor_scale;
+          g_object_set_data_full (G_OBJECT (monitor),
+                                  "previous-fractional-scale",
+                                  saved_scale, g_free);
+        }
+      else
+        {
+          g_signal_emit_by_name (monitor, "scale");
+        }
+    }
+
+  return any_changed;
+}
+
+static void
+reset_monitors_scaling_to_selected_values (CcDisplayConfig *self)
+{
+  GList *l;
+
+  for (l = cc_display_config_get_monitors (self); l; l = l->next)
+    {
+      CcDisplayMonitor *monitor = l->data;
+      gdouble *saved_scale;
+
+      saved_scale = g_object_get_data (G_OBJECT (monitor),
+                                       "previous-fractional-scale");
+
+      if (saved_scale)
+        {
+          cc_display_monitor_set_scale (monitor, *saved_scale);
+          g_object_set_data (G_OBJECT (monitor), "previous-fractional-scale", NULL);
+        }
+      else
+        {
+          g_signal_emit_by_name (monitor, "scale");
+        }
+    }
+}
+
+void
+cc_display_config_set_fractional_scaling (CcDisplayConfig *self,
+                                          gboolean         enabled)
+{
+  CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
+
+  if (priv->fractional_scaling == enabled)
+    return;
+
+  priv->fractional_scaling = enabled;
+
+  if (priv->fractional_scaling)
+    {
+      if (priv->fractional_scaling_pending_disable)
+        {
+          priv->fractional_scaling_pending_disable = FALSE;
+          reset_monitors_scaling_to_selected_values (self);
+        }
+
+      if (!get_fractional_scaling_active (self))
+        set_fractional_scaling_active (self, enabled);
+    }
+  else
+    {
+      priv->fractional_scaling_pending_disable = TRUE;
+
+      if (!set_monitors_scaling_to_preferred_integers (self))
+        {
+          gboolean disable_now = FALSE;
+
+          if (cc_display_config_layout_use_ui_scale (self))
+            {
+              disable_now =
+                G_APPROX_VALUE (cc_display_config_get_legacy_ui_scale (self),
+                                cc_display_config_get_maximum_scaling (self),
+                                DBL_EPSILON);
+            }
+
+          if (disable_now)
+            {
+              priv->fractional_scaling_pending_disable = FALSE;
+              reset_monitors_scaling_to_selected_values (self);
+              set_fractional_scaling_active (self, enabled);
+            }
+        }
+    }
+}
+
+gboolean
+cc_display_config_get_fractional_scaling (CcDisplayConfig *self)
+{
+  CcDisplayConfigPrivate *priv = cc_display_config_get_instance_private (self);
+
+  return priv->fractional_scaling;
+}
diff --git a/panels/display/cc-display-config.h b/panels/display/cc-display-config.h
index 1361f16..4a1548e 100644
--- a/panels/display/cc-display-config.h
+++ b/panels/display/cc-display-config.h
@@ -161,6 +161,7 @@ struct _CcDisplayConfigClass
                                     CcDisplayMode    *mode,
                                     double            scale);
   gboolean (*layout_use_ui_scale) (CcDisplayConfig  *self);
+  gint     (*get_legacy_ui_scale) (CcDisplayConfig  *self);
 };
 
 
@@ -188,8 +189,12 @@ gboolean          cc_display_config_is_scaled_mode_valid    (CcDisplayConfig
                                                              CcDisplayMode      *mode,
                                                              double              scale);
 gboolean          cc_display_config_layout_use_ui_scale     (CcDisplayConfig    *self);
+gint              cc_display_config_get_legacy_ui_scale     (CcDisplayConfig    *self);
 
 double            cc_display_config_get_maximum_scaling     (CcDisplayConfig    *self);
+void              cc_display_config_set_fractional_scaling  (CcDisplayConfig    *self,
+                                                             gboolean            enabled);
+gboolean          cc_display_config_get_fractional_scaling  (CcDisplayConfig    *self);
 
 const char*       cc_display_monitor_get_display_name       (CcDisplayMonitor   *monitor);
 gboolean          cc_display_monitor_is_active              (CcDisplayMonitor   *monitor);
diff --git a/panels/display/cc-display-settings.c b/panels/display/cc-display-settings.c
index d793fcc..947335a 100644
--- a/panels/display/cc-display-settings.c
+++ b/panels/display/cc-display-settings.c
@@ -49,6 +49,8 @@ struct _CcDisplaySettings
   GtkWidget        *resolution_row;
   GtkWidget        *scale_bbox;
   GtkWidget        *scale_row;
+  GtkWidget        *scale_fractional_row;
+  GtkWidget        *scale_fractional_switch;
   GtkWidget        *underscanning_row;
   GtkWidget        *underscanning_switch;
 };
@@ -71,7 +73,6 @@ static void on_scale_btn_active_changed_cb (GtkWidget         *widget,
                                             GParamSpec        *pspec,
                                             CcDisplaySettings *self);
 
-
 static gboolean
 should_show_rotation (CcDisplaySettings *self)
 {
@@ -242,6 +243,7 @@ cc_display_settings_rebuild_ui (CcDisplaySettings *self)
       gtk_widget_set_visible (self->refresh_rate_row, FALSE);
       gtk_widget_set_visible (self->resolution_row, FALSE);
       gtk_widget_set_visible (self->scale_row, FALSE);
+      gtk_widget_set_visible (self->scale_fractional_row, FALSE);
       gtk_widget_set_visible (self->underscanning_row, FALSE);
 
       return G_SOURCE_REMOVE;
@@ -429,6 +431,10 @@ cc_display_settings_rebuild_ui (CcDisplaySettings *self)
 
   gtk_widget_set_visible (self->scale_row, buttons > 1);
 
+  gtk_widget_set_visible (self->scale_fractional_row, TRUE);
+  gtk_switch_set_active (GTK_SWITCH (self->scale_fractional_switch),
+                         cc_display_config_get_fractional_scaling (self->config));
+
   gtk_widget_set_visible (self->underscanning_row,
                           cc_display_monitor_supports_underscanning (self->selected_output) &&
                           !cc_display_config_is_cloning (self->config));
@@ -671,6 +677,8 @@ cc_display_settings_class_init (CcDisplaySettingsClass *klass)
   gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, resolution_row);
   gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, scale_bbox);
   gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, scale_row);
+  gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, scale_fractional_row);
+  gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, scale_fractional_switch);
   gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, underscanning_row);
   gtk_widget_class_bind_template_child (widget_class, CcDisplaySettings, underscanning_switch);
 
@@ -680,11 +688,29 @@ cc_display_settings_class_init (CcDisplaySettingsClass *klass)
   gtk_widget_class_bind_template_callback (widget_class, on_underscanning_switch_active_changed_cb);
 }
 
+static void
+on_scale_fractional_toggled (CcDisplaySettings *self)
+{
+  gboolean active;
+
+  active = gtk_switch_get_active (GTK_SWITCH (self->scale_fractional_switch));
+
+  if (self->config)
+    cc_display_config_set_fractional_scaling (self->config, active);
+
+  g_signal_emit_by_name (G_OBJECT (self), "updated", self->selected_output);
+}
+
 static void
 cc_display_settings_init (CcDisplaySettings *self)
 {
   gtk_widget_init_template (GTK_WIDGET (self));
 
+  g_signal_connect_object (self->scale_fractional_switch,
+                           "notify::active",
+                           G_CALLBACK (on_scale_fractional_toggled),
+                           self, G_CONNECT_SWAPPED);
+
   gtk_list_box_set_header_func (GTK_LIST_BOX (self),
                                 cc_list_box_update_header_func,
                                 NULL, NULL);
diff --git a/panels/display/cc-display-settings.ui b/panels/display/cc-display-settings.ui
index 50ef951..cf8ca0a 100644
--- a/panels/display/cc-display-settings.ui
+++ b/panels/display/cc-display-settings.ui
@@ -68,5 +68,21 @@
         </child>
       </object>
     </child>
+    <child>
+      <object class="HdyActionRow" id="scale_fractional_row">
+        <property name="width_request">100</property>
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="title" translatable="yes" context="display setting">Fractional Scaling</property>
+        <property name="subtitle" translatable="yes" context="display setting">May increase power usage, lower speed, or reduce display sharpness.</property>
+        <child type="action">
+          <object class="GtkSwitch" id="scale_fractional_switch">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="valign">center</property>
+          </object>
+        </child>
+      </object>
+    </child>
   </template>
 </interface>