diff --git a/meson.build b/meson.build
index db8f89a7..77ddbbf5 100644
--- a/meson.build
+++ b/meson.build
@@ -165,6 +165,7 @@ libedataserver_dep = dependency('libedataserver-1.2', version: '>= 3.45.1')
libecal_dep = dependency('libecal-2.0', version: '>= 3.45.1')
libsoup_dep = dependency('libsoup-3.0')
libadwaita_dep = dependency('libadwaita-1', version: '>= 1.8.alpha')
+fribidi_dep = dependency('fribidi')
glib_dep = dependency('glib-2.0', version: '>= 2.80.0')
gtk_dep = dependency('gtk4', version: '>= 4.21.2')
gio_dep = dependency('gio-2.0', version: '>= 2.58.0')
diff --git a/src/gui/gcal-bidi.c b/src/gui/gcal-bidi.c
new file mode 100644
index 00000000..30cff658
--- /dev/null
+++ b/src/gui/gcal-bidi.c
@@ -0,0 +1,71 @@
+/* GDK - The GIMP Drawing Kit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library. If not, see .
+ */
+
+/*
+ * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS
+ * file for a list of people on the GTK+ Team. See the ChangeLog
+ * files for a list of changes. These files are distributed with
+ * GTK+ at ftp://ftp.gtk.org/pub/gtk/..
+ */
+
+/* Bits taken from GTK 4.0.2 and tweaked to be used by libadwaita. */
+/* Copied from libadwaita 1.8 and adapted for use in gnome-calendar. */
+
+#include "gcal-bidi.h"
+
+#include
+
+static PangoDirection
+gcal_unichar_direction (gunichar ch)
+{
+ FriBidiCharType fribidi_ch_type;
+
+ G_STATIC_ASSERT (sizeof (FriBidiChar) == sizeof (gunichar));
+
+ fribidi_ch_type = fribidi_get_bidi_type (ch);
+
+ if (!FRIBIDI_IS_STRONG (fribidi_ch_type))
+ return PANGO_DIRECTION_NEUTRAL;
+ else if (FRIBIDI_IS_RTL (fribidi_ch_type))
+ return PANGO_DIRECTION_RTL;
+ else
+ return PANGO_DIRECTION_LTR;
+}
+
+PangoDirection
+gcal_find_base_dir (const gchar *text,
+ gint length)
+{
+ PangoDirection dir = PANGO_DIRECTION_NEUTRAL;
+ const char *p;
+
+ g_return_val_if_fail (text != NULL || length == 0, PANGO_DIRECTION_NEUTRAL);
+
+ p = text;
+ while ((length < 0 || p < text + length) && *p) {
+ gunichar wc = g_utf8_get_char (p);
+
+ dir = gcal_unichar_direction (wc);
+
+ if (dir != PANGO_DIRECTION_NEUTRAL)
+ break;
+
+ p = g_utf8_next_char (p);
+ }
+
+ return dir;
+}
diff --git a/src/gui/gcal-bidi.h b/src/gui/gcal-bidi.h
new file mode 100644
index 00000000..dca20046
--- /dev/null
+++ b/src/gui/gcal-bidi.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+/* Copied from libadwaita 1.8 and adapted for use in gnome-calendar. */
+
+#pragma once
+
+#include
+#include
+
+G_BEGIN_DECLS
+
+PangoDirection gcal_find_base_dir (const gchar *text,
+ gint length);
+
+G_END_DECLS
diff --git a/src/gui/gcal-fading-label.c b/src/gui/gcal-fading-label.c
new file mode 100644
index 00000000..5c702afb
--- /dev/null
+++ b/src/gui/gcal-fading-label.c
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alice Mikhaylenko
+ */
+
+/* Copied from libadwaita 1.8 and adapted for use in gnome-calendar. */
+
+#include "config.h"
+#include "gcal-fading-label.h"
+
+#include
+#include "gcal-bidi.h"
+
+#define FADE_WIDTH 18.0f
+
+struct _GcalFadingLabel
+{
+ GtkWidget parent_instance;
+
+ GtkWidget *label;
+ gfloat align;
+};
+
+G_DEFINE_FINAL_TYPE (GcalFadingLabel, gcal_fading_label, GTK_TYPE_WIDGET)
+
+enum {
+ PROP_0,
+ PROP_LABEL,
+ PROP_ALIGN,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gboolean
+is_rtl (GcalFadingLabel *self)
+{
+ PangoDirection pango_direction = PANGO_DIRECTION_NEUTRAL;
+ const gchar *label = gcal_fading_label_get_label (self);
+
+ if (label)
+ pango_direction = gcal_find_base_dir (label, -1);
+
+ if (pango_direction == PANGO_DIRECTION_RTL)
+ return TRUE;
+
+ if (pango_direction == PANGO_DIRECTION_LTR)
+ return FALSE;
+
+ return gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+}
+
+static void
+gcal_fading_label_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *min,
+ gint *nat,
+ gint *min_baseline,
+ gint *nat_baseline)
+{
+ GcalFadingLabel *self = GCAL_FADING_LABEL (widget);
+
+ gtk_widget_measure (self->label,
+ orientation,
+ for_size,
+ min, nat,
+ min_baseline, nat_baseline);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL && min)
+ *min = 0;
+}
+
+static void
+gcal_fading_label_size_allocate (GtkWidget *widget,
+ gint width,
+ gint height,
+ gint baseline)
+{
+ GcalFadingLabel *self = GCAL_FADING_LABEL (widget);
+ GskTransform *transform;
+ gfloat align = is_rtl (self) ? 1 - self->align : self->align;
+ gint child_width;
+ gfloat offset;
+
+ gtk_widget_measure (self->label,
+ GTK_ORIENTATION_HORIZONTAL,
+ height,
+ NULL,
+ &child_width,
+ NULL, NULL);
+
+ offset = (width - child_width) * align;
+ transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (offset, 0));
+
+ gtk_widget_allocate (self->label, child_width, height, baseline, transform);
+}
+
+static void
+gcal_fading_label_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ GcalFadingLabel *self = GCAL_FADING_LABEL (widget);
+ g_autoptr (GskRenderNode) node = NULL;
+ g_autoptr (GtkSnapshot) child_snapshot = NULL;
+ graphene_rect_t bounds;
+ gfloat align = is_rtl (self) ? 1 - self->align : self->align;
+ gint width = gtk_widget_get_width (widget);
+ gint clipped_size;
+
+ if (width <= 0)
+ return;
+
+ clipped_size = gtk_widget_get_width (self->label) - width;
+
+ if (clipped_size <= 0)
+ {
+ gtk_widget_snapshot_child (widget, self->label, snapshot);
+ return;
+ }
+
+ child_snapshot = gtk_snapshot_new ();
+ gtk_widget_snapshot_child (widget, self->label, child_snapshot);
+ node = gtk_snapshot_free_to_node (g_steal_pointer (&child_snapshot));
+
+ gsk_render_node_get_bounds (node, &bounds);
+ bounds.origin.x = 0;
+ bounds.origin.y = floor (bounds.origin.y);
+ bounds.size.width = width;
+ bounds.size.height = ceil (bounds.size.height) + 1;
+
+ gtk_snapshot_push_mask (snapshot, GSK_MASK_MODE_INVERTED_ALPHA);
+
+ if (align > 0)
+ {
+ gtk_snapshot_append_linear_gradient (snapshot,
+ &GRAPHENE_RECT_INIT (0, bounds.origin.y,
+ FADE_WIDTH, bounds.size.height),
+ &GRAPHENE_POINT_INIT (0, 0),
+ &GRAPHENE_POINT_INIT (FADE_WIDTH, 0),
+ (GskColorStop[2]) {
+ { 0, { 0, 0, 0, 1 } },
+ { 1, { 0, 0, 0, 0 } },
+ },
+ 2);
+ }
+
+ if (align < 1)
+ {
+ gtk_snapshot_append_linear_gradient (snapshot,
+ &GRAPHENE_RECT_INIT (width - FADE_WIDTH, bounds.origin.y,
+ FADE_WIDTH, bounds.size.height),
+ &GRAPHENE_POINT_INIT (width, 0),
+ &GRAPHENE_POINT_INIT (width - FADE_WIDTH, 0),
+ (GskColorStop[2]) {
+ { 0, { 0, 0, 0, 1 } },
+ { 1, { 0, 0, 0, 0 } },
+ },
+ 2);
+ }
+
+ gtk_snapshot_pop (snapshot);
+
+ gtk_snapshot_push_clip (snapshot, &bounds);
+ gtk_snapshot_append_node (snapshot, node);
+ gtk_snapshot_pop (snapshot);
+
+ gtk_snapshot_pop (snapshot);
+}
+
+static void
+gcal_fading_label_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GcalFadingLabel *self = GCAL_FADING_LABEL (object);
+
+ switch (prop_id) {
+ case PROP_LABEL:
+ g_value_set_string (value, gcal_fading_label_get_label (self));
+ break;
+
+ case PROP_ALIGN:
+ g_value_set_float (value, gcal_fading_label_get_align (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_fading_label_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GcalFadingLabel *self = GCAL_FADING_LABEL (object);
+
+ switch (prop_id) {
+ case PROP_LABEL:
+ gcal_fading_label_set_label (self, g_value_get_string (value));
+ break;
+
+ case PROP_ALIGN:
+ gcal_fading_label_set_align (self, g_value_get_float (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+gcal_fading_label_dispose (GObject *object)
+{
+ GcalFadingLabel *self = GCAL_FADING_LABEL (object);
+
+ g_clear_pointer (&self->label, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (gcal_fading_label_parent_class)->dispose (object);
+}
+
+static void
+gcal_fading_label_class_init (GcalFadingLabelClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = gcal_fading_label_get_property;
+ object_class->set_property = gcal_fading_label_set_property;
+ object_class->dispose = gcal_fading_label_dispose;
+
+ widget_class->measure = gcal_fading_label_measure;
+ widget_class->size_allocate = gcal_fading_label_size_allocate;
+ widget_class->snapshot = gcal_fading_label_snapshot;
+
+ props[PROP_LABEL] =
+ g_param_spec_string ("label", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_ALIGN] =
+ g_param_spec_float ("align", NULL, NULL,
+ 0.0, 1.0, 0.0,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+gcal_fading_label_init (GcalFadingLabel *self)
+{
+ self->label = gtk_label_new (NULL);
+ gtk_label_set_single_line_mode (GTK_LABEL (self->label), TRUE);
+
+ gtk_widget_set_parent (self->label, GTK_WIDGET (self));
+}
+
+const gchar *
+gcal_fading_label_get_label (GcalFadingLabel *self)
+{
+ g_return_val_if_fail (GCAL_IS_FADING_LABEL (self), NULL);
+
+ return gtk_label_get_label (GTK_LABEL (self->label));
+}
+
+void
+gcal_fading_label_set_label (GcalFadingLabel *self,
+ const gchar *label)
+{
+ g_return_if_fail (GCAL_IS_FADING_LABEL (self));
+
+ if (!g_strcmp0 (label, gcal_fading_label_get_label (self)))
+ return;
+
+ gtk_label_set_label (GTK_LABEL (self->label), label);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LABEL]);
+}
+
+float
+gcal_fading_label_get_align (GcalFadingLabel *self)
+{
+ g_return_val_if_fail (GCAL_IS_FADING_LABEL (self), 0.0f);
+
+ return self->align;
+}
+
+void
+gcal_fading_label_set_align (GcalFadingLabel *self,
+ gfloat align)
+{
+ g_return_if_fail (GCAL_IS_FADING_LABEL (self));
+
+ align = CLAMP (align, 0.0, 1.0);
+
+ if (G_APPROX_VALUE (self->align, align, FLT_EPSILON))
+ return;
+
+ self->align = align;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALIGN]);
+}
diff --git a/src/gui/gcal-fading-label.h b/src/gui/gcal-fading-label.h
new file mode 100644
index 00000000..b3692862
--- /dev/null
+++ b/src/gui/gcal-fading-label.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alice Mikhaylenko
+ */
+
+/* Copied from libadwaita 1.8 and adapted for use in gnome-calendar. */
+
+#pragma once
+
+#include
+
+G_BEGIN_DECLS
+
+#define GCAL_TYPE_FADING_LABEL (gcal_fading_label_get_type())
+
+G_DECLARE_FINAL_TYPE (GcalFadingLabel, gcal_fading_label, GCAL, FADING_LABEL, GtkWidget)
+
+const gchar *gcal_fading_label_get_label (GcalFadingLabel *self);
+void gcal_fading_label_set_label (GcalFadingLabel *self,
+ const gchar *label);
+gfloat gcal_fading_label_get_align (GcalFadingLabel *self);
+void gcal_fading_label_set_align (GcalFadingLabel *self,
+ gfloat align);
+
+G_END_DECLS
diff --git a/src/gui/meson.build b/src/gui/meson.build
index aff98194..e22489a8 100644
--- a/src/gui/meson.build
+++ b/src/gui/meson.build
@@ -41,11 +41,13 @@ built_sources += gnome.compile_resources(
sources += files(
'gcal-application.c',
+ 'gcal-bidi.c',
'gcal-calendar-list.c',
'gcal-calendar-navigation-button.c',
'gcal-drop-overlay.c',
'gcal-event-popover.c',
'gcal-event-widget.c',
+ 'gcal-fading-label.c',
'gcal-meeting-row.c',
'gcal-overflow-bin.c',
'gcal-overflow-bin-layout.c',
diff --git a/src/meson.build b/src/meson.build
index 1879d611..7b86f803 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -8,6 +8,7 @@ gcal_deps = [
libecal_dep,
libsoup_dep,
libical_dep,
+ fribidi_dep,
glib_dep,
gtk_dep,
gio_dep,