[libadwaita/wip/exalm/adaptive-states: 2/7] wip: Adaptive states




commit 44237706436f341043184de6177ce1517d63de38
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Mon Oct 3 18:50:51 2022 +0400

    wip: Adaptive states

 src/adw-adaptive-state-private.h |   26 +
 src/adw-adaptive-state.c         | 1058 ++++++++++++++++++++++++++++++++++++++
 src/adw-adaptive-state.h         |   91 ++++
 src/adw-application-window.c     |   86 ++++
 src/adw-application-window.h     |    9 +
 src/adw-window-mixin-private.h   |   12 +
 src/adw-window-mixin.c           |  328 +++++++++++-
 src/adw-window.c                 |   86 ++++
 src/adw-window.h                 |    9 +
 src/adwaita.h                    |    1 +
 src/meson.build                  |    3 +
 11 files changed, 1702 insertions(+), 7 deletions(-)
---
diff --git a/src/adw-adaptive-state-private.h b/src/adw-adaptive-state-private.h
new file mode 100644
index 00000000..7b5723d5
--- /dev/null
+++ b/src/adw-adaptive-state-private.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-adaptive-state.h"
+
+G_BEGIN_DECLS
+
+void adw_adaptive_state_apply   (AdwAdaptiveState *self);
+void adw_adaptive_state_unapply (AdwAdaptiveState *self);
+
+gboolean adw_adaptive_state_check_condition (AdwAdaptiveState *self,
+                                             int               width,
+                                             int               height);
+
+G_END_DECLS
diff --git a/src/adw-adaptive-state.c b/src/adw-adaptive-state.c
new file mode 100644
index 00000000..c1066813
--- /dev/null
+++ b/src/adw-adaptive-state.c
@@ -0,0 +1,1058 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+
+#include "adw-adaptive-state-private.h"
+
+#include "adw-gtkbuilder-utils-private.h"
+#include "adw-macros-private.h"
+
+#include <gobject/gvaluecollector.h>
+
+G_DEFINE_BOXED_TYPE (AdwAdaptiveCondition, adw_adaptive_condition,
+                     adw_adaptive_condition_copy, adw_adaptive_condition_free)
+
+typedef struct {
+  AdwAdaptiveConditionType type;
+  double value;
+} AdwAdaptiveConditionDataSingle;
+
+typedef struct {
+  AdwAdaptiveMultiConditionType type;
+  AdwAdaptiveCondition **children;
+  int n_children;
+} AdwAdaptiveConditionDataMulti;
+
+struct _AdwAdaptiveCondition
+{
+  gboolean multi_condition;
+
+  union {
+    AdwAdaptiveConditionDataSingle single;
+    AdwAdaptiveConditionDataMulti multi;
+  } data;
+};
+
+static gboolean
+check_condition (AdwAdaptiveCondition *self,
+                 int                   width,
+                 int                   height)
+{
+  g_assert (self != NULL);
+
+  if (self->multi_condition) {
+    int i;
+    gboolean ret = (self->data.multi.type == ADW_MULTI_CONDITION_ALL);
+
+    for (i = 0; i < self->data.multi.n_children; i++) {
+      AdwAdaptiveCondition *child = self->data.multi.children[i];
+
+      if (self->data.multi.type == ADW_MULTI_CONDITION_ALL)
+        ret &= check_condition (child, width, height);
+      else
+        ret |= check_condition (child, width, height);
+    }
+
+    return ret;
+  }
+
+  switch (self->data.single.type) {
+  case ADW_CONDITION_MIN_WIDTH:
+    return width >= self->data.single.value;
+  case ADW_CONDITION_MAX_WIDTH:
+    return width <= self->data.single.value;
+  case ADW_CONDITION_MIN_HEIGHT:
+    return height >= self->data.single.value;
+  case ADW_CONDITION_MAX_HEIGHT:
+    return height <= self->data.single.value;
+  case ADW_CONDITION_MIN_ASPECT_RATIO:
+    return (double) width / height >= self->data.single.value;
+  case ADW_CONDITION_MAX_ASPECT_RATIO:
+    return (double) width / height <= self->data.single.value;
+  default:
+    g_assert_not_reached ();
+  }
+
+  return FALSE;
+}
+
+AdwAdaptiveCondition *
+adw_adaptive_condition_new (AdwAdaptiveConditionType type,
+                            double                   value)
+{
+  AdwAdaptiveCondition *self;
+
+  g_return_val_if_fail (type >= 0, NULL);
+  g_return_val_if_fail (type <= ADW_CONDITION_MAX_ASPECT_RATIO, NULL);
+  g_return_val_if_fail (value >= 0, NULL);
+
+  self = g_new0 (AdwAdaptiveCondition, 1);
+  self->multi_condition = FALSE;
+  self->data.single.type = type;
+  self->data.single.value = value;
+
+  return self;
+}
+
+AdwAdaptiveCondition *
+adw_adaptive_multi_condition_new (AdwAdaptiveMultiConditionType  type,
+                                  AdwAdaptiveCondition          *first_condition,
+                                  ...)
+{
+  AdwAdaptiveCondition *self;
+  GPtrArray *children;
+  va_list args;
+  AdwAdaptiveCondition *child;
+
+  g_return_val_if_fail (type >= 0, NULL);
+  g_return_val_if_fail (type <= ADW_MULTI_CONDITION_ANY, NULL);
+  g_return_val_if_fail (first_condition != NULL, NULL);
+
+  children = g_ptr_array_new ();
+  child = first_condition;
+
+  va_start (args, first_condition);
+
+  while (child) {
+    g_ptr_array_add (children, child);
+
+    child = va_arg (args, AdwAdaptiveCondition *);
+  }
+
+  va_end (args);
+
+  self = adw_adaptive_multi_condition_newv (type,
+                                            (AdwAdaptiveCondition **) children->pdata,
+                                            children->len);
+
+  g_ptr_array_free (children, FALSE);
+
+  return self;
+}
+
+AdwAdaptiveCondition *
+adw_adaptive_multi_condition_newv (AdwAdaptiveMultiConditionType   type,
+                                   AdwAdaptiveCondition          **conditions,
+                                   int                             n_conditions)
+{
+  AdwAdaptiveCondition *self;
+
+  g_return_val_if_fail (type >= 0, NULL);
+  g_return_val_if_fail (type <= ADW_MULTI_CONDITION_ANY, NULL);
+  g_return_val_if_fail (conditions != NULL, NULL);
+  g_return_val_if_fail (n_conditions > 0, NULL);
+
+  self = g_new0 (AdwAdaptiveCondition, 1);
+  self->multi_condition = TRUE;
+  self->data.multi.type = type;
+  self->data.multi.children = conditions;
+  self->data.multi.n_children = n_conditions;
+
+  return self;
+}
+
+AdwAdaptiveCondition *
+adw_adaptive_condition_copy (AdwAdaptiveCondition *self)
+{
+  AdwAdaptiveCondition *copy;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  if (self->multi_condition) {
+#if GLIB_CHECK_VERSION(2, 67, 3)
+    AdwAdaptiveCondition **children =
+      g_memdup2 (self->data.multi.children,
+                 self->data.multi.n_children * sizeof (AdwAdaptiveCondition *));
+#else
+    AdwAdaptiveCondition **children =
+      g_memdup (self->data.multi.children,
+                self->data.multi.n_children * sizeof (AdwAdaptiveCondition *));
+#endif
+
+    copy = adw_adaptive_multi_condition_newv (self->data.multi.type,
+                                              children,
+                                              self->data.multi.n_children);
+  } else {
+    copy = adw_adaptive_condition_new (self->data.single.type,
+                                       self->data.single.value);
+  }
+
+  return copy;
+}
+
+void
+adw_adaptive_condition_free (AdwAdaptiveCondition *self)
+{
+  g_return_if_fail (self != NULL);
+
+  if (self->multi_condition)
+    g_free (self->data.multi.children);
+
+  g_free (self);
+}
+
+char *
+adw_adaptive_condition_to_string (AdwAdaptiveCondition *self)
+{
+  const char *type;
+
+  g_return_val_if_fail (self != NULL, NULL);
+
+  if (self->multi_condition) {
+    char **children;
+    char *joined;
+    char *ret;
+    int i;
+
+    switch (self->data.multi.type) {
+    case ADW_MULTI_CONDITION_ALL:
+      type = "all";
+      break;
+    case ADW_MULTI_CONDITION_ANY:
+      type = "any";
+      break;
+    default:
+      g_assert_not_reached ();
+    }
+
+    children = g_new0 (char *, self->data.multi.n_children + 1);
+
+    for (i = 0; i < self->data.multi.n_children; i++) {
+      AdwAdaptiveCondition *child = self->data.multi.children[i];
+
+      children[i] = adw_adaptive_condition_to_string (child);
+    }
+
+    joined = g_strjoinv (", ", children);
+
+    ret = g_strdup_printf ("%s(%s)", type, joined);
+
+    g_strfreev (children);
+    g_free (joined);
+
+    return ret;
+  }
+
+  switch (self->data.single.type) {
+  case ADW_CONDITION_MIN_WIDTH:
+    type = "min-width";
+    break;
+  case ADW_CONDITION_MAX_WIDTH:
+    type = "max-width";
+    break;
+  case ADW_CONDITION_MIN_HEIGHT:
+    type = "min-height";
+    break;
+  case ADW_CONDITION_MAX_HEIGHT:
+    type = "max-height";
+    break;
+  case ADW_CONDITION_MIN_ASPECT_RATIO:
+    type = "min-aspect-ratio";
+    break;
+  case ADW_CONDITION_MAX_ASPECT_RATIO:
+    type = "max-aspect-ratio";
+    break;
+  default:
+    g_assert_not_reached ();
+  }
+
+  return g_strdup_printf ("%s: %lf", type, self->data.single.value);
+}
+
+typedef struct {
+  GObject *object;
+  GParamSpec *pspec;
+  GValue value;
+  GValue original_value;
+} SetterData;
+
+struct _AdwAdaptiveState
+{
+  GObject parent_instance;
+
+  AdwAdaptiveCondition *condition;
+  GList *setters;
+  gboolean active;
+};
+
+static void adw_adaptive_state_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwAdaptiveState, adw_adaptive_state, G_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_adaptive_state_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+  PROP_0,
+  PROP_CONDITION,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_ENTER,
+  SIGNAL_EXIT,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static inline GParamSpec *
+find_pspec (GObject    *object,
+            const char *name)
+{
+  GParamSpec *pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (object), name);
+
+  if (!pspec) {
+    g_critical ("Type '%s' does not have a property named '%s'",
+                G_OBJECT_TYPE_NAME (object), name);
+    return NULL;
+  }
+
+  return pspec;
+}
+
+static SetterData *
+find_setter_by_object (AdwAdaptiveState *self,
+                       GObject          *object)
+{
+  GList *l;
+
+  for (l = self->setters; l; l = l->next) {
+    SetterData *setter = l->data;
+
+    if (setter->object == object)
+      return setter;
+  }
+
+  return NULL;
+}
+
+static void free_setter_data (AdwAdaptiveState *self,
+                              SetterData       *data);
+
+static void
+setter_weak_notify (AdwAdaptiveState *self,
+                    GObject          *where_the_object_was)
+{
+  SetterData *setter = find_setter_by_object (self, where_the_object_was);
+
+  g_assert (setter);
+
+  setter->object = NULL;
+  free_setter_data (self, setter);
+  self->setters = g_list_remove (self->setters, setter);
+}
+
+static void
+free_setter_data (AdwAdaptiveState *self,
+                  SetterData       *setter)
+{
+  if (setter->object)
+    g_object_weak_unref (setter->object,
+                         (GWeakNotify) setter_weak_notify,
+                         self);
+
+  g_param_spec_unref (setter->pspec);
+  g_value_unset (&setter->value);
+  g_value_unset (&setter->original_value);
+
+  g_free (setter);
+}
+
+static void
+adw_adaptive_state_dispose (GObject *object)
+{
+  AdwAdaptiveState *self = ADW_ADAPTIVE_STATE (object);
+  GList *l;
+
+  g_clear_pointer (&self->condition, adw_adaptive_condition_free);
+
+  for (l = self->setters; l; l = l->next) {
+    SetterData *setter = l->data;
+
+    free_setter_data (self, setter);
+  }
+
+  g_clear_pointer (&self->setters, g_list_free);
+
+  G_OBJECT_CLASS (adw_adaptive_state_parent_class)->dispose (object);
+}
+
+static void
+adw_adaptive_state_get_property (GObject    *object,
+                                 guint       prop_id,
+                                 GValue     *value,
+                                 GParamSpec *pspec)
+{
+  AdwAdaptiveState *self = ADW_ADAPTIVE_STATE (object);
+
+  switch (prop_id) {
+  case PROP_CONDITION:
+    g_value_set_boxed (value, adw_adaptive_state_get_condition (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_adaptive_state_set_property (GObject      *object,
+                                 guint         prop_id,
+                                 const GValue *value,
+                                 GParamSpec   *pspec)
+{
+  AdwAdaptiveState *self = ADW_ADAPTIVE_STATE (object);
+
+  switch (prop_id) {
+  case PROP_CONDITION:
+    adw_adaptive_state_set_condition (self, g_value_get_boxed (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_adaptive_state_class_init (AdwAdaptiveStateClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = adw_adaptive_state_dispose;
+  object_class->get_property = adw_adaptive_state_get_property;
+  object_class->set_property = adw_adaptive_state_set_property;
+
+  props[PROP_CONDITION] =
+    g_param_spec_boxed ("condition", NULL, NULL,
+                        ADW_TYPE_ADAPTIVE_CONDITION,
+                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  signals[SIGNAL_ENTER] =
+    g_signal_new ("enter",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  signals[SIGNAL_EXIT] =
+    g_signal_new ("exit",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+}
+
+static void
+adw_adaptive_state_init (AdwAdaptiveState *self)
+{
+}
+
+typedef struct {
+  AdwAdaptiveMultiConditionType type;
+  GPtrArray *children;
+} ConditionParserMultiData;
+
+typedef struct {
+  GObject *object;
+  GtkBuilder *builder;
+
+  gboolean single_condition;
+  AdwAdaptiveConditionType single_type;
+  GString *value;
+  GList *multi_conditions;
+
+  AdwAdaptiveCondition *result;
+} ConditionParserData;
+
+static void
+condition_data_free (gpointer data)
+{
+  ConditionParserData *setter = data;
+
+  g_string_free (setter->value, TRUE);
+  g_free (setter);
+}
+
+static void
+condition_start_element (GtkBuildableParseContext  *context,
+                         const char                *element_name,
+                         const char               **names,
+                         const char               **values,
+                         gpointer                   user_data,
+                         GError                   **error)
+{
+  ConditionParserData *data = user_data;
+
+  if (strcmp (element_name, "conditions") == 0)
+    return;
+
+  if (strcmp (element_name, "condition") == 0) {
+    const char *type_str = NULL;
+    gboolean multi_condition = FALSE;
+    AdwAdaptiveMultiConditionType multi_type;
+
+    if (!g_markup_collect_attributes (element_name, names, values, error,
+                                      G_MARKUP_COLLECT_STRING, "type", &type_str,
+                                      G_MARKUP_COLLECT_INVALID)) {
+      _gtk_builder_prefix_error (data->builder, context, error);
+      return;
+    }
+
+    if (!g_strcmp0 (type_str, "all")) {
+      multi_condition = TRUE;
+      multi_type = ADW_MULTI_CONDITION_ALL;
+    } else if (!g_strcmp0 (type_str, "any")) {
+      multi_condition = TRUE;
+      multi_type = ADW_MULTI_CONDITION_ANY;
+    } else if (!g_strcmp0 (type_str, "min-width")) {
+      data->single_type = ADW_CONDITION_MIN_WIDTH;
+    } else if (!g_strcmp0 (type_str, "max-width")) {
+      data->single_type = ADW_CONDITION_MAX_WIDTH;
+    } else if (!g_strcmp0 (type_str, "min-height")) {
+      data->single_type = ADW_CONDITION_MIN_HEIGHT;
+    } else if (!g_strcmp0 (type_str, "max-height")) {
+      data->single_type = ADW_CONDITION_MAX_HEIGHT;
+    } else if (!g_strcmp0 (type_str, "min-aspect-ratio")) {
+      data->single_type = ADW_CONDITION_MIN_ASPECT_RATIO;
+    } else if (!g_strcmp0 (type_str, "max-aspect-ratio")) {
+      data->single_type = ADW_CONDITION_MAX_ASPECT_RATIO;
+    } else {
+      g_set_error (error,
+                   GTK_BUILDER_ERROR,
+                   GTK_BUILDER_ERROR_INVALID_VALUE,
+                   "Invalid condition type '%s'",
+                   type_str);
+      return;
+    }
+
+    data->single_condition = !multi_condition;
+
+    if (multi_condition) {
+      ConditionParserMultiData *multi_data;
+
+      multi_data = g_new0 (ConditionParserMultiData, 1);
+      multi_data->type = multi_type;
+      multi_data->children = g_ptr_array_new ();
+
+      data->multi_conditions = g_list_prepend (data->multi_conditions, multi_data);
+    }
+
+    return;
+  }
+
+  _gtk_builder_error_unhandled_tag (data->builder, context,
+                                    "AdwAdaptiveState", element_name,
+                                    error);
+}
+
+static void
+condition_end_element (GtkBuildableParseContext  *context,
+                       const char                *element_name,
+                       gpointer                   user_data,
+                       GError                   **error)
+{
+  ConditionParserData *data = user_data;
+
+  if (strcmp (element_name, "conditions") == 0)
+    return;
+
+  if (strcmp (element_name, "condition") == 0) {
+    AdwAdaptiveCondition *condition;
+
+    if (data->single_condition) {
+      double value;
+      char *endptr = NULL;
+
+      data->single_condition = FALSE;
+
+      value = g_ascii_strtod (data->value->str, &endptr);
+
+      if (endptr == data->value->str) {
+        g_set_error (error,
+                     GTK_BUILDER_ERROR,
+                     GTK_BUILDER_ERROR_INVALID_VALUE,
+                     "Could not parse double '%s'",
+                     data->value->str);
+        g_string_erase (data->value, 0, -1);
+        return;
+      }
+
+      g_string_erase (data->value, 0, -1);
+
+      condition = adw_adaptive_condition_new (data->single_type, value);
+    } else {
+      ConditionParserMultiData *multi_data = data->multi_conditions->data;
+
+      condition = adw_adaptive_multi_condition_newv (multi_data->type,
+                                                     (AdwAdaptiveCondition **) multi_data->children->pdata,
+                                                     multi_data->children->len);
+
+      data->multi_conditions = g_list_remove (data->multi_conditions, multi_data);
+
+      g_ptr_array_free (multi_data->children, FALSE);
+      g_free (multi_data);
+    }
+
+    if (data->multi_conditions) {
+      ConditionParserMultiData *multi_data = data->multi_conditions->data;
+
+      g_ptr_array_add (multi_data->children, condition);
+    } else {
+      g_clear_pointer (&data->result, adw_adaptive_condition_free);
+      data->result = condition;
+    }
+
+    return;
+  }
+
+  _gtk_builder_error_unhandled_tag (data->builder, context,
+                                    "AdwAdaptiveState", element_name,
+                                    error);
+}
+
+static void
+condition_text (GtkBuildableParseContext  *context,
+                const char                *text,
+                gsize                      text_len,
+                gpointer                   user_data,
+                GError                   **error)
+{
+  ConditionParserData *data = user_data;
+
+  if (data->single_condition)
+    g_string_append_len (data->value, text, text_len);
+}
+
+static const GtkBuildableParser condition_parser = {
+  condition_start_element,
+  condition_end_element,
+  condition_text,
+  NULL
+};
+
+typedef struct {
+  GObject *object;
+  GtkBuilder *builder;
+
+  char *object_id;
+  char *property_name;
+  GString *value;
+
+  char *context;
+  gboolean translatable;
+} SetterParserData;
+
+static void
+setter_data_free (gpointer data)
+{
+  SetterParserData *setter = data;
+
+  g_free (setter->object_id);
+  g_free (setter->property_name);
+  g_string_free (setter->value, TRUE);
+  g_free (setter->context);
+  g_free (setter);
+}
+
+static void
+setter_start_element (GtkBuildableParseContext  *context,
+                      const char                *element_name,
+                      const char               **names,
+                      const char               **values,
+                      gpointer                   user_data,
+                      GError                   **error)
+{
+  SetterParserData *data = user_data;
+  const char *object_str = NULL;
+  const char *property_str = NULL;
+  const char *msg_context = NULL;
+  gboolean translatable = FALSE;
+
+  if (strcmp (element_name, "setter") != 0) {
+    _gtk_builder_error_unhandled_tag (data->builder, context,
+                                      "AdwAdaptiveState", element_name,
+                                      error);
+    return;
+  }
+
+  if (!_gtk_builder_check_parent (data->builder, context, "object", error))
+    return;
+
+  if (!g_markup_collect_attributes (element_name, names, values, error,
+                                    G_MARKUP_COLLECT_STRING, "object", &object_str,
+                                    G_MARKUP_COLLECT_STRING, "property", &property_str,
+                                    G_MARKUP_COLLECT_BOOLEAN | G_MARKUP_COLLECT_OPTIONAL, "translatable", 
&translatable,
+                                    G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "comments", NULL,
+                                    G_MARKUP_COLLECT_STRING | G_MARKUP_COLLECT_OPTIONAL, "context", 
&msg_context,
+                                    G_MARKUP_COLLECT_INVALID)) {
+    _gtk_builder_prefix_error (data->builder, context, error);
+    return;
+  }
+
+  data->object_id = g_strdup (object_str);
+  data->property_name = g_strdup (property_str);
+  data->translatable = translatable;
+  data->context = g_strdup (msg_context);
+}
+
+static void
+setter_text (GtkBuildableParseContext  *context,
+             const char                *text,
+             gsize                      text_len,
+             gpointer                   user_data,
+             GError                   **error)
+{
+  SetterParserData *data = user_data;
+
+  g_string_append_len (data->value, text, text_len);
+}
+
+static const GtkBuildableParser setter_parser = {
+  setter_start_element,
+  NULL,
+  setter_text,
+  NULL
+};
+
+static gboolean
+adw_adaptive_state_buildable_custom_tag_start (GtkBuildable       *buildable,
+                                               GtkBuilder         *builder,
+                                               GObject            *child,
+                                               const char         *tagname,
+                                               GtkBuildableParser *parser,
+                                               gpointer           *parser_data)
+{
+  if (child)
+    return FALSE;
+
+  if (strcmp (tagname, "setter") == 0) {
+    SetterParserData *data;
+
+    data = g_new0 (SetterParserData, 1);
+    data->object = G_OBJECT (buildable);
+    data->builder = builder;
+    data->value = g_string_new ("");
+
+    *parser = setter_parser;
+    *parser_data = data;
+
+    return TRUE;
+  }
+
+  if (strcmp (tagname, "conditions") == 0) {
+    ConditionParserData *data;
+
+    data = g_new0 (ConditionParserData, 1);
+    data->object = G_OBJECT (buildable);
+    data->builder = builder;
+    data->value = g_string_new ("");
+
+    *parser = condition_parser;
+    *parser_data = data;
+
+    return TRUE;
+  }
+
+  return FALSE;
+}
+
+static void
+adw_adaptive_state_buildable_custom_finished (GtkBuildable *buildable,
+                                              GtkBuilder   *builder,
+                                              GObject      *child,
+                                              const char   *tagname,
+                                              gpointer      user_data)
+{
+  if (strcmp (tagname, "setter") == 0) {
+    SetterParserData *data = user_data;
+    GObject *object;
+    GParamSpec *pspec;
+    const char *value_str;
+    GValue value = G_VALUE_INIT;
+    GError *error = NULL;
+    GType type;
+
+    object = gtk_builder_get_object (data->builder, data->object_id);
+
+    if (!object) {
+      g_critical ("Unable to find object '%s' for setter", data->object_id);
+      setter_data_free (data);
+      return;
+    }
+
+    pspec = find_pspec (object, data->property_name);
+
+    if (!pspec) {
+      setter_data_free (data);
+      return;
+    }
+
+    if (data->translatable && data->value->len)
+      value_str = _gtk_builder_parser_translate (gtk_builder_get_translation_domain (builder),
+                                                 data->context,
+                                                 data->value->str);
+    else
+      value_str = data->value->str;
+
+    type = G_TYPE_FUNDAMENTAL (G_PARAM_SPEC_VALUE_TYPE (pspec));
+
+    /* Treat empty strings like NULL */
+    if ((type == G_TYPE_OBJECT || type == G_TYPE_INTERFACE) && !g_strcmp0 (value_str, "")) {
+      g_value_init (&value, type);
+
+      g_value_set_object (&value, NULL);
+    } else if (!gtk_builder_value_from_string (builder, pspec, value_str, &value, &error)) {
+      g_warning ("Invalid value %s for property %s: %s",
+                 value_str, data->property_name, error->message);
+      g_error_free (error);
+      setter_data_free (data);
+      return;
+    }
+
+    adw_adaptive_state_add_setter (ADW_ADAPTIVE_STATE (data->object),
+                                   object,
+                                   data->property_name,
+                                   &value);
+
+    g_value_unset (&value);
+
+    setter_data_free (data);
+    return;
+  }
+
+  if (strcmp (tagname, "conditions") == 0) {
+    ConditionParserData *data = user_data;
+
+    if (data->result)
+      adw_adaptive_state_set_condition (ADW_ADAPTIVE_STATE (data->object),
+                                        data->result);
+
+    condition_data_free (data);
+    return;
+  }
+
+  parent_buildable_iface->custom_finished (buildable, builder, child,
+                                           tagname, user_data);
+}
+
+static void
+adw_adaptive_state_buildable_init (GtkBuildableIface *iface)
+{
+  parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+  iface->custom_tag_start = adw_adaptive_state_buildable_custom_tag_start;
+  iface->custom_finished = adw_adaptive_state_buildable_custom_finished;
+}
+
+AdwAdaptiveState *
+adw_adaptive_state_new (void)
+{
+  return g_object_new (ADW_TYPE_ADAPTIVE_STATE, NULL);
+}
+
+AdwAdaptiveCondition *
+adw_adaptive_state_get_condition (AdwAdaptiveState *self)
+{
+  g_return_val_if_fail (ADW_IS_ADAPTIVE_STATE (self), NULL);
+
+  return self->condition;
+}
+
+void
+adw_adaptive_state_set_condition (AdwAdaptiveState     *self,
+                                  AdwAdaptiveCondition *condition)
+{
+  g_return_if_fail (ADW_IS_ADAPTIVE_STATE (self));
+
+  if (self->condition == condition)
+    return;
+
+  g_clear_pointer (&self->condition, adw_adaptive_condition_free);
+  self->condition = adw_adaptive_condition_copy (condition);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONDITION]);
+}
+
+void
+adw_adaptive_state_add_setter (AdwAdaptiveState *self,
+                               GObject          *object,
+                               const char       *property,
+                               const GValue     *value)
+{
+  SetterData *setter;
+  GParamSpec *pspec;
+  GValue validated_value = G_VALUE_INIT;
+  GValue original_value = G_VALUE_INIT;
+
+  g_return_if_fail (ADW_IS_ADAPTIVE_STATE (self));
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (property != NULL);
+  g_return_if_fail (G_IS_VALUE (value));
+
+  pspec = find_pspec (object, property);
+
+  if (!pspec)
+    return;
+
+  g_value_init (&validated_value, pspec->value_type);
+
+  if (!g_value_transform (value, &validated_value)) {
+    g_error ("Unable to add setter for property '%s' of type '%s' from value of type '%s'",
+             pspec->name,
+             g_type_name (pspec->value_type),
+             G_VALUE_TYPE_NAME (value));
+
+    g_value_unset (&validated_value);
+
+    return;
+  } else if (g_param_value_validate (pspec, &validated_value) &&
+             !(pspec->flags & G_PARAM_LAX_VALIDATION)) {
+    char *contents = g_strdup_value_contents (value);
+
+    g_warning ("Unable to add setter: value \"%s\" of type '%s' is invalid or"
+               "out of range for property '%s' of type '%s'",
+               contents,
+               G_VALUE_TYPE_NAME (value),
+               pspec->name,
+               g_type_name (pspec->value_type));
+
+    g_free (contents);
+    g_value_unset (&validated_value);
+
+    return;
+  }
+
+  g_object_get_property (object, property, &original_value);
+
+  setter = g_new0 (SetterData, 1);
+
+  setter->object = object;
+  setter->pspec = g_param_spec_ref (pspec);
+  setter->value = validated_value;
+  setter->original_value = original_value;
+
+  g_object_weak_ref (object,
+                     (GWeakNotify) setter_weak_notify,
+                     self);
+
+  self->setters = g_list_append (self->setters, setter);
+
+  if (self->active)
+    g_object_set_property (setter->object,
+                           setter->pspec->name,
+                           &setter->value);
+}
+
+void
+adw_adaptive_state_add_setters (AdwAdaptiveState *self,
+                                GObject          *first_object,
+                                const char       *first_property,
+                                ...)
+{
+  GObject *object = first_object;
+  const char *property = first_property;
+  va_list args;
+
+  g_return_if_fail (ADW_IS_ADAPTIVE_STATE (self));
+  g_return_if_fail (G_IS_OBJECT (object));
+  g_return_if_fail (property != NULL);
+
+  va_start (args, first_property);
+
+  while (object && property) {
+    GValue value = G_VALUE_INIT;
+    GParamSpec *pspec;
+    char *error;
+
+    pspec = find_pspec (object, property);
+
+    if (!pspec)
+      break;
+
+    G_VALUE_COLLECT_INIT (&value, pspec->value_type, args,
+                          G_VALUE_NOCOPY_CONTENTS, &error);
+
+    if (error) {
+      g_warning ("%s: %s", G_STRLOC, error);
+      g_free (error);
+      break;
+    }
+
+    adw_adaptive_state_add_setter (self, object, property, &value);
+
+    g_value_unset (&value);
+
+    object = va_arg (args, GObject *);
+
+    if (object)
+      property = va_arg (args, const char *);
+  }
+
+  va_end (args);
+}
+
+void
+adw_adaptive_state_apply (AdwAdaptiveState *self)
+{
+  GList *l;
+
+  g_assert (ADW_IS_ADAPTIVE_STATE (self));
+
+  self->active = TRUE;
+
+  for (l = self->setters; l; l = l->next) {
+    SetterData *setter = l->data;
+
+    g_object_set_property (setter->object,
+                           setter->pspec->name,
+                           &setter->value);
+  }
+
+  g_signal_emit (self, signals[SIGNAL_ENTER], 0);
+}
+
+void
+adw_adaptive_state_unapply (AdwAdaptiveState *self)
+{
+  GList *l;
+
+  g_assert (ADW_IS_ADAPTIVE_STATE (self));
+
+  g_signal_emit (self, signals[SIGNAL_EXIT], 0);
+
+  for (l = self->setters; l; l = l->next) {
+    SetterData *setter = l->data;
+
+    g_object_set_property (setter->object,
+                           setter->pspec->name,
+                           &setter->original_value);
+  }
+
+  self->active = FALSE;
+}
+
+gboolean
+adw_adaptive_state_check_condition (AdwAdaptiveState *self,
+                                    int               width,
+                                    int               height)
+{
+  g_assert (ADW_IS_ADAPTIVE_STATE (self));
+
+  if (!self->condition)
+    return FALSE;
+
+  return check_condition (self->condition, width, height);
+}
diff --git a/src/adw-adaptive-state.h b/src/adw-adaptive-state.h
new file mode 100644
index 00000000..a628bceb
--- /dev/null
+++ b/src/adw-adaptive-state.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#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>
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_ADAPTIVE_CONDITION (adw_adaptive_condition_get_type ())
+
+typedef enum {
+  ADW_CONDITION_MIN_WIDTH,
+  ADW_CONDITION_MAX_WIDTH,
+  ADW_CONDITION_MIN_HEIGHT,
+  ADW_CONDITION_MAX_HEIGHT,
+  ADW_CONDITION_MIN_ASPECT_RATIO,
+  ADW_CONDITION_MAX_ASPECT_RATIO,
+} AdwAdaptiveConditionType;
+
+typedef enum {
+  ADW_MULTI_CONDITION_ALL,
+  ADW_MULTI_CONDITION_ANY,
+} AdwAdaptiveMultiConditionType;
+
+typedef struct _AdwAdaptiveCondition AdwAdaptiveCondition;
+
+ADW_AVAILABLE_IN_1_3
+GType adw_adaptive_condition_get_type (void) G_GNUC_CONST;
+
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveCondition *adw_adaptive_condition_new (AdwAdaptiveConditionType type,
+                                                  double                   value) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveCondition *adw_adaptive_multi_condition_new  (AdwAdaptiveMultiConditionType   type,
+                                                         AdwAdaptiveCondition           *first_condition,
+                                                         ...) G_GNUC_NULL_TERMINATED 
G_GNUC_WARN_UNUSED_RESULT;
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveCondition *adw_adaptive_multi_condition_newv (AdwAdaptiveMultiConditionType   type,
+                                                         AdwAdaptiveCondition          **conditions,
+                                                         int                             n_conditions) 
G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveCondition *adw_adaptive_condition_copy     (AdwAdaptiveCondition *self);
+ADW_AVAILABLE_IN_1_3
+void                  adw_adaptive_condition_free     (AdwAdaptiveCondition *self);
+
+ADW_AVAILABLE_IN_1_3
+char *adw_adaptive_condition_to_string (AdwAdaptiveCondition *self);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (AdwAdaptiveCondition, adw_adaptive_condition_free)
+
+#define ADW_TYPE_ADAPTIVE_STATE (adw_adaptive_state_get_type())
+
+ADW_AVAILABLE_IN_1_3
+G_DECLARE_FINAL_TYPE (AdwAdaptiveState, adw_adaptive_state, ADW, ADAPTIVE_STATE, GObject)
+
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveState *adw_adaptive_state_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveCondition *adw_adaptive_state_get_condition (AdwAdaptiveState     *self);
+ADW_AVAILABLE_IN_1_3
+void                  adw_adaptive_state_set_condition (AdwAdaptiveState     *self,
+                                                        AdwAdaptiveCondition *condition);
+
+ADW_AVAILABLE_IN_1_3
+void adw_adaptive_state_add_setter (AdwAdaptiveState *self,
+                                    GObject          *object,
+                                    const char       *property,
+                                    const GValue     *value);
+
+ADW_AVAILABLE_IN_1_3
+void adw_adaptive_state_add_setters (AdwAdaptiveState *self,
+                                     GObject          *first_object,
+                                     const char       *first_property,
+                                     ...) G_GNUC_NULL_TERMINATED;
+
+G_END_DECLS
diff --git a/src/adw-application-window.c b/src/adw-application-window.c
index c218e0d0..5f21e4ef 100644
--- a/src/adw-application-window.c
+++ b/src/adw-application-window.c
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2020 Alexander Mikhaylenko <alexm gnome org>
+ * Copyright (C) 2022 Purism SPC
  *
  * SPDX-License-Identifier: LGPL-2.1-or-later
  */
@@ -7,6 +8,8 @@
 #include "config.h"
 
 #include "adw-application-window.h"
+
+#include "adw-adaptive-state.h"
 #include "adw-window-mixin-private.h"
 
 /**
@@ -46,6 +49,7 @@ static GtkBuildableIface *parent_buildable_iface;
 enum {
   PROP_0,
   PROP_CONTENT,
+  PROP_CURRENT_STATE,
   LAST_PROP,
 };
 
@@ -53,6 +57,12 @@ static GParamSpec *props[LAST_PROP];
 
 #define ADW_GET_WINDOW_MIXIN(obj) (((AdwApplicationWindowPrivate *) 
adw_application_window_get_instance_private (ADW_APPLICATION_WINDOW (obj)))->mixin)
 
+static void
+notify_current_state_cb (AdwApplicationWindow *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CURRENT_STATE]);
+}
+
 static void
 adw_application_window_size_allocate (GtkWidget *widget,
                                       int        width,
@@ -65,6 +75,19 @@ adw_application_window_size_allocate (GtkWidget *widget,
                                   baseline);
 }
 
+static void
+adw_application_window_snapshot (GtkWidget   *widget,
+                                 GtkSnapshot *snapshot)
+{
+  adw_window_mixin_snapshot (ADW_GET_WINDOW_MIXIN (widget), snapshot);
+}
+
+static void
+adw_application_window_realize (GtkWidget *widget)
+{
+  adw_window_mixin_realize (ADW_GET_WINDOW_MIXIN (widget));
+}
+
 static void
 adw_application_window_finalize (GObject *object)
 {
@@ -88,6 +111,9 @@ adw_application_window_get_property (GObject    *object,
   case PROP_CONTENT:
     g_value_set_object (value, adw_application_window_get_content (self));
     break;
+  case PROP_CURRENT_STATE:
+    g_value_set_object (value, adw_application_window_get_current_state (self));
+    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
   }
@@ -119,7 +145,10 @@ adw_application_window_class_init (AdwApplicationWindowClass *klass)
   object_class->finalize = adw_application_window_finalize;
   object_class->get_property = adw_application_window_get_property;
   object_class->set_property = adw_application_window_set_property;
+
   widget_class->size_allocate = adw_application_window_size_allocate;
+  widget_class->snapshot = adw_application_window_snapshot;
+  widget_class->realize = adw_application_window_realize;
 
   /**
    * AdwApplicationWindow:content: (attributes org.gtk.Property.get=adw_application_window_get_content 
org.gtk.Property.set=adw_application_window_set_content)
@@ -135,6 +164,18 @@ adw_application_window_class_init (AdwApplicationWindowClass *klass)
                          GTK_TYPE_WIDGET,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
 
+  /**
+   * AdwApplicationWindow:current-state: (attributes 
org.gtk.Property.get=adw_application_window_get_current_state)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_CURRENT_STATE] =
+    g_param_spec_object ("current-state", NULL, NULL,
+                         ADW_TYPE_ADAPTIVE_STATE,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
   g_object_class_install_properties (object_class, LAST_PROP, props);}
 
 static void
@@ -145,6 +186,11 @@ adw_application_window_init (AdwApplicationWindow *self)
   priv->mixin = adw_window_mixin_new (GTK_WINDOW (self),
                                       GTK_WINDOW_CLASS (adw_application_window_parent_class));
 
+  g_signal_connect_swapped (priv->mixin,
+                            "state-changed",
+                            G_CALLBACK (notify_current_state_cb),
+                            self);
+
   gtk_application_window_set_show_menubar (GTK_APPLICATION_WINDOW (self), FALSE);
 }
 
@@ -158,6 +204,9 @@ adw_application_window_buildable_add_child (GtkBuildable *buildable,
     GTK_BUILDER_WARN_INVALID_CHILD_TYPE (buildable, type);
   else if (GTK_IS_WIDGET (child))
     adw_application_window_set_content (ADW_APPLICATION_WINDOW (buildable), GTK_WIDGET (child));
+  else if (ADW_IS_ADAPTIVE_STATE (child))
+    adw_application_window_add_adaptive_state (ADW_APPLICATION_WINDOW (buildable),
+                                               g_object_ref (ADW_ADAPTIVE_STATE (child)));
   else
     parent_buildable_iface->add_child (buildable, builder, child, type);
 }
@@ -233,3 +282,40 @@ adw_application_window_get_content (AdwApplicationWindow *self)
 
   return adw_window_mixin_get_content (ADW_GET_WINDOW_MIXIN (self));
 }
+
+/**
+ * adw_application_window_add_adaptive_state:
+ * @self: an application window
+ * @state: (transfer full): TODO
+ *
+ * TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_application_window_add_adaptive_state (AdwApplicationWindow *self,
+                                           AdwAdaptiveState     *state)
+{
+  g_return_if_fail (ADW_IS_APPLICATION_WINDOW (self));
+  g_return_if_fail (ADW_IS_ADAPTIVE_STATE (state));
+
+  adw_window_mixin_add_adaptive_state (ADW_GET_WINDOW_MIXIN (self), state);
+}
+
+/**
+ * adw_application_window_get_current_state: (attributes org.gtk.Method.get_property=current-state)
+ * @self: an application window
+ *
+ * TODO
+ *
+ * Returns: (nullable) (transfer none): TODO
+ *
+ * Since: 1.3
+ */
+AdwAdaptiveState *
+adw_application_window_get_current_state (AdwApplicationWindow *self)
+{
+  g_return_val_if_fail (ADW_IS_APPLICATION_WINDOW (self), NULL);
+
+  return adw_window_mixin_get_current_state (ADW_GET_WINDOW_MIXIN (self));
+}
diff --git a/src/adw-application-window.h b/src/adw-application-window.h
index 5c330e85..aeb092c6 100644
--- a/src/adw-application-window.h
+++ b/src/adw-application-window.h
@@ -14,6 +14,8 @@
 
 #include <gtk/gtk.h>
 
+#include "adw-adaptive-state.h"
+
 G_BEGIN_DECLS
 
 #define ADW_TYPE_APPLICATION_WINDOW (adw_application_window_get_type())
@@ -38,4 +40,11 @@ void       adw_application_window_set_content (AdwApplicationWindow *self,
 ADW_AVAILABLE_IN_ALL
 GtkWidget *adw_application_window_get_content (AdwApplicationWindow *self);
 
+ADW_AVAILABLE_IN_1_3
+void adw_application_window_add_adaptive_state (AdwApplicationWindow *self,
+                                                AdwAdaptiveState     *state);
+
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveState *adw_application_window_get_current_state (AdwApplicationWindow *self);
+
 G_END_DECLS
diff --git a/src/adw-window-mixin-private.h b/src/adw-window-mixin-private.h
index e64b1957..1e9cfa21 100644
--- a/src/adw-window-mixin-private.h
+++ b/src/adw-window-mixin-private.h
@@ -12,6 +12,8 @@
 
 #include <gtk/gtk.h>
 
+#include "adw-adaptive-state.h"
+
 G_BEGIN_DECLS
 
 #define ADW_TYPE_WINDOW_MIXIN (adw_window_mixin_get_type())
@@ -26,8 +28,18 @@ void adw_window_mixin_size_allocate (AdwWindowMixin *self,
                                      int             height,
                                      int             baseline);
 
+void adw_window_mixin_snapshot (AdwWindowMixin *self,
+                                GtkSnapshot    *snapshot);
+
+void adw_window_mixin_realize (AdwWindowMixin *self);
+
 GtkWidget *adw_window_mixin_get_content (AdwWindowMixin *self);
 void       adw_window_mixin_set_content (AdwWindowMixin *self,
                                          GtkWidget      *content);
 
+void adw_window_mixin_add_adaptive_state (AdwWindowMixin   *self,
+                                          AdwAdaptiveState *state);
+
+AdwAdaptiveState *adw_window_mixin_get_current_state (AdwWindowMixin *self);
+
 G_END_DECLS
diff --git a/src/adw-window-mixin.c b/src/adw-window-mixin.c
index f7fc47df..d2a27e55 100644
--- a/src/adw-window-mixin.c
+++ b/src/adw-window-mixin.c
@@ -1,15 +1,21 @@
 /*
  * Copyright (C) 2020 Alexander Mikhaylenko <alexm gnome org>
+ * Copyright (C) 2022 Purism SPC
  *
  * SPDX-License-Identifier: LGPL-2.1-or-later
  */
 
 #include "config.h"
 
+#include "adw-window-mixin-private.h"
+
+#include "adw-adaptive-state-private.h"
 #include "adw-gizmo-private.h"
 #include "adw-macros-private.h"
+#include "adw-timed-animation.h"
 #include "adw-widget-utils-private.h"
-#include "adw-window-mixin-private.h"
+
+#define TRANSITION_DURATION 250
 
 struct _AdwWindowMixin
 {
@@ -22,16 +28,161 @@ struct _AdwWindowMixin
   GtkWidget *child;
 
   GtkWidget *content;
+
+  GList *states;
+  AdwAdaptiveState *current_state;
+
+  gboolean state_changing;
+  GskRenderNode *old_node;
+  int old_width;
+  int old_height;
+  AdwAnimation *animation;
+  gboolean first_allocation;
+  guint tick_cb_id;
+  gboolean block_warnings;
 };
 
 G_DEFINE_FINAL_TYPE (AdwWindowMixin, adw_window_mixin, G_TYPE_OBJECT)
 
+enum {
+  SIGNAL_STATE_CHANGED,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+animation_cb (double          value,
+              AdwWindowMixin *self)
+{
+  gtk_widget_queue_draw (GTK_WIDGET (self->window));
+}
+
+static void
+animation_done_cb (AdwWindowMixin *self)
+{
+  self->old_node = NULL;
+  self->old_width = 0;
+  self->old_height = 0;
+}
+
+static void
+measure_content (AdwGizmo       *gizmo,
+                 GtkOrientation  orientation,
+                 int             for_size,
+                 int            *minimum,
+                 int            *natural,
+                 int            *minimum_baseline,
+                 int            *natural_baseline)
+{
+  AdwWindowMixin *self = g_object_get_data (G_OBJECT (gizmo), "adw-window-mixin");
+  int min, nat;
+
+  if (self->content)
+    gtk_widget_measure (self->content, orientation, for_size,
+                        &min, &nat, NULL, NULL);
+  else
+    min = nat = 0;
+
+  if (self->states)
+    min = 0;
+
+  if (minimum)
+    *minimum = min;
+  if (natural)
+    *natural = nat;
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+allocate_content (AdwGizmo *gizmo,
+                  int       width,
+                  int       height,
+                  int       baseline)
+{
+  AdwWindowMixin *self = g_object_get_data (G_OBJECT (gizmo), "adw-window-mixin");
+  int min_width, min_height;
+
+  if (self->state_changing)
+    return;
+
+  if (!self->content)
+    return;
+
+  if (!self->block_warnings && self->states) {
+    int window_width, window_height;
+
+    gtk_widget_get_size_request (GTK_WIDGET (self->window), &window_width, &window_height);
+
+    if (window_width <= 0 && window_height <= 0)
+      g_warning ("%s %p does not have a minimum size",
+                 G_OBJECT_TYPE_NAME (self->window), self->window);
+    else if (window_width <= 0)
+      g_warning ("%s %p does not have a minimum width",
+                 G_OBJECT_TYPE_NAME (self->window), self->window);
+    else if (window_height <= 0)
+      g_warning ("%s %p does not have a minimum height",
+                 G_OBJECT_TYPE_NAME (self->window), self->window);
+  }
+
+  gtk_widget_measure (self->content, GTK_ORIENTATION_HORIZONTAL, -1,
+                      &min_width, NULL, NULL, NULL);
+  gtk_widget_measure (self->content, GTK_ORIENTATION_VERTICAL, -1,
+                      &min_height, NULL, NULL, NULL);
+
+  if (width >= min_width && height >= min_height) {
+    gtk_widget_allocate (self->content, width, height, baseline, NULL);
+
+    return;
+  }
+
+  if (!self->block_warnings) {
+    if (min_width > width && min_height > height)
+      g_warning ("%s %p exceeds its window size: requested %d×%d px, %d×%d px available",
+                 G_OBJECT_TYPE_NAME (self->content), self->content, min_width, min_height, width, height);
+    else if (min_width > width)
+      g_warning ("%s %p exceeds its window width: requested %d px, %d px available",
+                 G_OBJECT_TYPE_NAME (self->content), self->content, min_width, width);
+    else
+      g_warning ("%s %p exceeds its window height: requested %d px, %d px available",
+                 G_OBJECT_TYPE_NAME (self->content), self->content, min_height, height);
+  }
+
+  width = MAX (width, min_width);
+  height = MAX (height, min_height);
+
+  gtk_widget_allocate (self->content, width, height, baseline, NULL);
+}
+
+static gboolean
+state_changed_tick_cb (GtkWidget      *widget,
+                       GdkFrameClock  *frame_clock,
+                       AdwWindowMixin *self)
+{
+  self->tick_cb_id = 0;
+  self->state_changing = FALSE;
+  gtk_widget_set_child_visible (self->child, TRUE);
+  gtk_widget_queue_resize (GTK_WIDGET (self->window));
+
+  adw_animation_reset (self->animation);
+  adw_animation_play (self->animation);
+
+  return G_SOURCE_REMOVE;
+}
+
 void
 adw_window_mixin_size_allocate (AdwWindowMixin *self,
                                 int             width,
                                 int             height,
                                 int             baseline)
 {
+  GList *l;
+  GtkSnapshot *snapshot;
+  AdwAdaptiveState *new_state = NULL;
+
   /* We don't want to allow any other titlebar */
   if (gtk_window_get_titlebar (self->window) != self->titlebar)
     g_error ("gtk_window_set_titlebar() is not supported for AdwWindow");
@@ -39,15 +190,154 @@ adw_window_mixin_size_allocate (AdwWindowMixin *self,
   if (gtk_window_get_child (self->window) != self->child)
     g_error ("gtk_window_set_child() is not supported for AdwWindow");
 
-  GTK_WIDGET_CLASS (self->klass)->size_allocate (GTK_WIDGET (self->window),
-                                                 width,
-                                                 height,
-                                                 baseline);
+  for (l = self->states; l; l = l->next) {
+    AdwAdaptiveState *state = l->data;
+
+    if (adw_adaptive_state_check_condition (state, width, height)) {
+      new_state = state;
+      break;
+    }
+  }
+
+  if (new_state == self->current_state) {
+    GTK_WIDGET_CLASS (self->klass)->size_allocate (GTK_WIDGET (self->window),
+                                                   width,
+                                                   height,
+                                                   baseline);
+
+    self->first_allocation = FALSE;
+
+    return;
+  }
+
+  if (!self->first_allocation) {
+    self->block_warnings = TRUE;
+    GTK_WIDGET_CLASS (self->klass)->size_allocate (GTK_WIDGET (self->window),
+                                                   width,
+                                                   height,
+                                                   baseline);
+    self->block_warnings = FALSE;
+
+    snapshot = gtk_snapshot_new ();
+    adw_window_mixin_snapshot (self, snapshot);
+
+    self->old_node = gtk_snapshot_free_to_node (snapshot);
+    self->old_width = gtk_widget_get_width (GTK_WIDGET (self->window));
+    self->old_height = gtk_widget_get_height (GTK_WIDGET (self->window));
+
+    self->state_changing = TRUE;
+    gtk_widget_set_child_visible (self->child, FALSE);
+  }
+
+  if (self->current_state)
+    adw_adaptive_state_unapply (self->current_state);
+
+  self->current_state = new_state;
+  g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0);
+
+  if (self->current_state)
+    adw_adaptive_state_apply (self->current_state);
+
+  if (self->first_allocation) {
+    self->block_warnings = TRUE;
+    GTK_WIDGET_CLASS (self->klass)->size_allocate (GTK_WIDGET (self->window),
+                                                   width,
+                                                   height,
+                                                   baseline);
+    self->block_warnings = FALSE;
+    self->first_allocation = FALSE;
+  } else {
+    self->tick_cb_id = gtk_widget_add_tick_callback (GTK_WIDGET (self->window),
+                                                     (GtkTickCallback) state_changed_tick_cb,
+                                                     self, NULL);
+  }
+}
+
+static inline void
+draw_old_node (AdwWindowMixin *self,
+               GtkSnapshot    *snapshot)
+{
+  float scale_x, scale_y;
+
+  if (!self->old_node)
+    return;
+
+  scale_x = (float) gtk_widget_get_width (GTK_WIDGET (self->window)) / self->old_width;
+  scale_y = (float) gtk_widget_get_height (GTK_WIDGET (self->window)) / self->old_height;
+
+  gtk_snapshot_save (snapshot);
+  gtk_snapshot_scale (snapshot, scale_x, scale_y);
+  gtk_snapshot_append_node (snapshot, self->old_node);
+  gtk_snapshot_restore (snapshot);
+}
+
+void
+adw_window_mixin_snapshot (AdwWindowMixin *self,
+                           GtkSnapshot    *snapshot)
+{
+  if (self->state_changing) {
+    draw_old_node (self, snapshot);
+    return;
+  }
+
+  if (!self->old_node ||
+      adw_animation_get_state (self->animation) != ADW_ANIMATION_PLAYING) {
+    GTK_WIDGET_CLASS (self->klass)->snapshot (GTK_WIDGET (self->window), snapshot);
+    return;
+  }
+
+  gtk_snapshot_push_cross_fade (snapshot, adw_animation_get_value (self->animation));
+
+  draw_old_node (self, snapshot);
+  gtk_snapshot_pop (snapshot);
+
+  GTK_WIDGET_CLASS (self->klass)->snapshot (GTK_WIDGET (self->window), snapshot);
+  gtk_snapshot_pop (snapshot);
+}
+
+void
+adw_window_mixin_realize (AdwWindowMixin *self)
+{
+  self->first_allocation = TRUE;
+
+  GTK_WIDGET_CLASS (self->klass)->realize (GTK_WIDGET (self->window));
+}
+
+static void
+adw_window_mixin_dispose (GObject *object)
+{
+  AdwWindowMixin *self = ADW_WINDOW_MIXIN (object);
+
+  if (self->tick_cb_id) {
+    gtk_widget_remove_tick_callback (GTK_WIDGET (self->window), self->tick_cb_id);
+    self->tick_cb_id = 0;
+  }
+
+  g_clear_object (&self->animation);
+
+  if (self->states) {
+    g_list_free_full (self->states, g_object_unref);
+    self->states = NULL;
+  }
+
+  G_OBJECT_CLASS (adw_window_mixin_parent_class)->dispose (object);
 }
 
 static void
 adw_window_mixin_class_init (AdwWindowMixinClass *klass)
 {
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = adw_window_mixin_dispose;
+
+  signals[SIGNAL_STATE_CHANGED] =
+    g_signal_new ("state-changed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
 }
 
 static void
@@ -60,6 +350,7 @@ adw_window_mixin_new (GtkWindow      *window,
                       GtkWindowClass *klass)
 {
   AdwWindowMixin *self;
+  AdwAnimationTarget *target;
 
   g_return_val_if_fail (GTK_IS_WINDOW (window), NULL);
   g_return_val_if_fail (GTK_IS_WINDOW_CLASS (klass), NULL);
@@ -76,12 +367,17 @@ adw_window_mixin_new (GtkWindow      *window,
   gtk_window_set_titlebar (self->window, self->titlebar);
 
   self->child = adw_gizmo_new_with_role ("contents", GTK_ACCESSIBLE_ROLE_GROUP,
-                                         NULL, NULL, NULL, NULL,
+                                         measure_content, allocate_content,
+                                         NULL, NULL,
                                          (AdwGizmoFocusFunc) adw_widget_focus_child,
                                          (AdwGizmoGrabFocusFunc) adw_widget_grab_focus_child);
-  gtk_widget_set_layout_manager (self->child, gtk_bin_layout_new ());
+  g_object_set_data (G_OBJECT (self->child), "adw-window-mixin", self);
   gtk_window_set_child (window, self->child);
 
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc) animation_cb, self, NULL);
+  self->animation = adw_timed_animation_new (GTK_WIDGET (window), 0, 1, TRANSITION_DURATION, target);
+  g_signal_connect_swapped (self->animation, "done", G_CALLBACK (animation_done_cb), self);
+
   return self;
 }
 
@@ -102,3 +398,21 @@ adw_window_mixin_get_content (AdwWindowMixin *self)
 {
   return self->content;
 }
+
+void
+adw_window_mixin_add_adaptive_state (AdwWindowMixin   *self,
+                                     AdwAdaptiveState *state)
+{
+  self->states = g_list_prepend (self->states, state);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self->window));
+
+  g_signal_connect_swapped (state, "notify::condition",
+                            G_CALLBACK (gtk_widget_queue_allocate), self->window);
+}
+
+AdwAdaptiveState *
+adw_window_mixin_get_current_state (AdwWindowMixin *self)
+{
+  return self->current_state;
+}
diff --git a/src/adw-window.c b/src/adw-window.c
index 074a1cae..02fbe633 100644
--- a/src/adw-window.c
+++ b/src/adw-window.c
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2020 Alexander Mikhaylenko <alexm gnome org>
+ * Copyright (C) 2022 Purism SPC
  *
  * SPDX-License-Identifier: LGPL-2.1-or-later
  */
@@ -7,6 +8,8 @@
 #include "config.h"
 
 #include "adw-window.h"
+
+#include "adw-adaptive-state.h"
 #include "adw-window-mixin-private.h"
 
 /**
@@ -61,6 +64,7 @@ static GtkBuildableIface *parent_buildable_iface;
 enum {
   PROP_0,
   PROP_CONTENT,
+  PROP_CURRENT_STATE,
   LAST_PROP,
 };
 
@@ -68,6 +72,12 @@ static GParamSpec *props[LAST_PROP];
 
 #define ADW_GET_WINDOW_MIXIN(obj) (((AdwWindowPrivate *) adw_window_get_instance_private (ADW_WINDOW 
(obj)))->mixin)
 
+static void
+notify_current_state_cb (AdwWindow *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CURRENT_STATE]);
+}
+
 static void
 adw_window_size_allocate (GtkWidget *widget,
                           int        width,
@@ -80,6 +90,19 @@ adw_window_size_allocate (GtkWidget *widget,
                                   baseline);
 }
 
+static void
+adw_window_snapshot (GtkWidget   *widget,
+                     GtkSnapshot *snapshot)
+{
+  adw_window_mixin_snapshot (ADW_GET_WINDOW_MIXIN (widget), snapshot);
+}
+
+static void
+adw_window_realize (GtkWidget *widget)
+{
+  adw_window_mixin_realize (ADW_GET_WINDOW_MIXIN (widget));
+}
+
 static void
 adw_window_finalize (GObject *object)
 {
@@ -103,6 +126,9 @@ adw_window_get_property (GObject    *object,
   case PROP_CONTENT:
     g_value_set_object (value, adw_window_get_content (self));
     break;
+  case PROP_CURRENT_STATE:
+    g_value_set_object (value, adw_window_get_current_state (self));
+    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
   }
@@ -134,7 +160,10 @@ adw_window_class_init (AdwWindowClass *klass)
   object_class->finalize = adw_window_finalize;
   object_class->get_property = adw_window_get_property;
   object_class->set_property = adw_window_set_property;
+
   widget_class->size_allocate = adw_window_size_allocate;
+  widget_class->snapshot = adw_window_snapshot;
+  widget_class->realize = adw_window_realize;
 
   /**
    * AdwWindow:content: (attributes org.gtk.Property.get=adw_window_get_content 
org.gtk.Property.set=adw_window_set_content)
@@ -150,6 +179,18 @@ adw_window_class_init (AdwWindowClass *klass)
                          GTK_TYPE_WIDGET,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
 
+  /**
+   * AdwWindow:current-state: (attributes org.gtk.Property.get=adw_window_get_current_state)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_CURRENT_STATE] =
+    g_param_spec_object ("current-state", NULL, NULL,
+                         ADW_TYPE_ADAPTIVE_STATE,
+                         G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
   g_object_class_install_properties (object_class, LAST_PROP, props);
 }
 
@@ -160,6 +201,11 @@ adw_window_init (AdwWindow *self)
 
   priv->mixin = adw_window_mixin_new (GTK_WINDOW (self),
                                       GTK_WINDOW_CLASS (adw_window_parent_class));
+
+  g_signal_connect_swapped (priv->mixin,
+                            "state-changed",
+                            G_CALLBACK (notify_current_state_cb),
+                            self);
 }
 
 static void
@@ -172,6 +218,9 @@ adw_window_buildable_add_child (GtkBuildable *buildable,
     GTK_BUILDER_WARN_INVALID_CHILD_TYPE (buildable, type);
   else if (GTK_IS_WIDGET (child))
     adw_window_set_content (ADW_WINDOW (buildable), GTK_WIDGET (child));
+  else if (ADW_IS_ADAPTIVE_STATE (child))
+    adw_window_add_adaptive_state (ADW_WINDOW (buildable),
+                                   g_object_ref (ADW_ADAPTIVE_STATE (child)));
   else
     parent_buildable_iface->add_child (buildable, builder, child, type);
 }
@@ -244,3 +293,40 @@ adw_window_get_content (AdwWindow *self)
 
   return adw_window_mixin_get_content (ADW_GET_WINDOW_MIXIN (self));
 }
+
+/**
+ * adw_window_add_adaptive_state:
+ * @self: a window
+ * @state: (transfer full): TODO
+ *
+ * TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_window_add_adaptive_state (AdwWindow        *self,
+                               AdwAdaptiveState *state)
+{
+  g_return_if_fail (ADW_IS_WINDOW (self));
+  g_return_if_fail (ADW_IS_ADAPTIVE_STATE (state));
+
+  adw_window_mixin_add_adaptive_state (ADW_GET_WINDOW_MIXIN (self), state);
+}
+
+/**
+ * adw_window_get_current_state: (attributes org.gtk.Method.get_property=current-state)
+ * @self: a window
+ *
+ * TODO
+ *
+ * Returns: (nullable) (transfer none): TODO
+ *
+ * Since: 1.3
+ */
+AdwAdaptiveState *
+adw_window_get_current_state (AdwWindow *self)
+{
+  g_return_val_if_fail (ADW_IS_WINDOW (self), NULL);
+
+  return adw_window_mixin_get_current_state (ADW_GET_WINDOW_MIXIN (self));
+}
diff --git a/src/adw-window.h b/src/adw-window.h
index dc6be3c5..4e404923 100644
--- a/src/adw-window.h
+++ b/src/adw-window.h
@@ -14,6 +14,8 @@
 
 #include <gtk/gtk.h>
 
+#include "adw-adaptive-state.h"
+
 G_BEGIN_DECLS
 
 #define ADW_TYPE_WINDOW (adw_window_get_type())
@@ -38,4 +40,11 @@ ADW_AVAILABLE_IN_ALL
 void       adw_window_set_content (AdwWindow *self,
                                    GtkWidget *content);
 
+ADW_AVAILABLE_IN_1_3
+void adw_window_add_adaptive_state (AdwWindow        *self,
+                                    AdwAdaptiveState *state);
+
+ADW_AVAILABLE_IN_1_3
+AdwAdaptiveState *adw_window_get_current_state (AdwWindow *self);
+
 G_END_DECLS
diff --git a/src/adwaita.h b/src/adwaita.h
index 9e277ded..077b6521 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -23,6 +23,7 @@ G_BEGIN_DECLS
 #include "adw-version.h"
 #include "adw-about-window.h"
 #include "adw-action-row.h"
+#include "adw-adaptive-state.h"
 #include "adw-animation.h"
 #include "adw-animation-target.h"
 #include "adw-animation-util.h"
diff --git a/src/meson.build b/src/meson.build
index e14484ca..ade587ef 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -11,6 +11,7 @@ libadwaita_resources = gnome.compile_resources(
 
 adw_public_enum_headers = [
   'adw-animation.h',
+  'adw-adaptive-state.h',
   'adw-flap.h',
   'adw-fold-threshold-policy.h',
   'adw-easing.h',
@@ -84,6 +85,7 @@ libadwaita_generated_headers += [adw_public_enums[1]]
 src_headers = [
   'adw-about-window.h',
   'adw-action-row.h',
+  'adw-adaptive-state.h',
   'adw-animation.h',
   'adw-animation-target.h',
   'adw-animation-util.h',
@@ -151,6 +153,7 @@ libadwaita_init_public_types = custom_target('adw-public-types.c',
 src_sources = [
   'adw-about-window.c',
   'adw-action-row.c',
+  'adw-adaptive-state.c',
   'adw-animation.c',
   'adw-animation-target.c',
   'adw-animation-util.c',


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