diff --git a/src/gcal-enums.h b/src/gcal-enums.h index df71a6e5..3955a7b0 100644 --- a/src/gcal-enums.h +++ b/src/gcal-enums.h @@ -42,6 +42,7 @@ typedef enum { GCAL_WINDOW_VIEW_WEEK, GCAL_WINDOW_VIEW_MONTH, + GCAL_WINDOW_VIEW_DAZZLING_MONTH, } GcalWindowView; typedef enum diff --git a/src/gui/gcal-window.c b/src/gui/gcal-window.c index 126b8b4e..41faefbf 100644 --- a/src/gui/gcal-window.c +++ b/src/gui/gcal-window.c @@ -25,6 +25,7 @@ #include "gcal-calendar-button.h" #include "config.h" #include "gcal-date-chooser.h" +#include "gcal-dazzling-month-view.h" #include "gcal-debug.h" #include "gcal-event-editor-dialog.h" #include "gcal-event-widget.h" @@ -104,6 +105,7 @@ struct _GcalWindow AdwViewStack *views_stack; GtkWidget *week_view; GtkWidget *month_view; + GtkWidget *dazzling_month_view; GtkWidget *agenda_view; GtkWidget *date_chooser; GtkWidget *action_bar; @@ -204,6 +206,7 @@ update_today_action_enabled (GcalWindow *window) break; case GCAL_WINDOW_VIEW_MONTH: + case GCAL_WINDOW_VIEW_DAZZLING_MONTH: enabled = g_date_time_get_year (window->active_date) != g_date_time_get_year (now) || g_date_time_get_month (window->active_date) != g_date_time_get_month (now); break; @@ -877,6 +880,7 @@ gcal_window_constructed (GObject *object) g_object_bind_property (self, "context", self->calendar_management_dialog, "context", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); g_object_bind_property (self, "context", self->week_view, "context", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); g_object_bind_property (self, "context", self->month_view, "context", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + g_object_bind_property (self, "context", self->dazzling_month_view, "context", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); g_object_bind_property (self, "context", self->agenda_view, "context", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); g_object_bind_property (self, "context", self->date_chooser, "context", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); g_object_bind_property (self, "context", self->event_editor, "context", G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); @@ -1012,6 +1016,7 @@ gcal_window_class_init (GcalWindowClass *klass) g_type_ensure (GCAL_TYPE_CALENDAR_MANAGEMENT_DIALOG); g_type_ensure (GCAL_TYPE_CALENDAR_BUTTON); g_type_ensure (GCAL_TYPE_DATE_CHOOSER); + g_type_ensure (GCAL_TYPE_DAZZLING_MONTH_VIEW); g_type_ensure (GCAL_TYPE_EVENT_EDITOR_DIALOG); g_type_ensure (GCAL_TYPE_MANAGER); g_type_ensure (GCAL_TYPE_MONTH_VIEW); @@ -1065,6 +1070,7 @@ gcal_window_class_init (GcalWindowClass *klass) gtk_widget_class_bind_template_child (widget_class, GcalWindow, agenda_view); gtk_widget_class_bind_template_child (widget_class, GcalWindow, calendars_button); gtk_widget_class_bind_template_child (widget_class, GcalWindow, date_chooser); + gtk_widget_class_bind_template_child (widget_class, GcalWindow, dazzling_month_view); gtk_widget_class_bind_template_child (widget_class, GcalWindow, event_editor); gtk_widget_class_bind_template_child (widget_class, GcalWindow, header_bar); gtk_widget_class_bind_template_child (widget_class, GcalWindow, main_box); @@ -1123,6 +1129,7 @@ gcal_window_init (GcalWindow *self) self->views[GCAL_WINDOW_VIEW_WEEK] = self->week_view; self->views[GCAL_WINDOW_VIEW_MONTH] = self->month_view; + self->views[GCAL_WINDOW_VIEW_DAZZLING_MONTH] = self->dazzling_month_view; self->active_date = g_date_time_new_from_unix_local (0); self->rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL; diff --git a/src/gui/gcal-window.ui b/src/gui/gcal-window.ui index 50a67aec..9fd9d867 100644 --- a/src/gui/gcal-window.ui +++ b/src/gui/gcal-window.ui @@ -280,6 +280,22 @@ + + + dazzling-month + _Month + True + calendar-month-symbolic + + + + + + + + + + diff --git a/src/gui/views/gcal-dazzling-month-view.c b/src/gui/views/gcal-dazzling-month-view.c new file mode 100644 index 00000000..2b11efc1 --- /dev/null +++ b/src/gui/views/gcal-dazzling-month-view.c @@ -0,0 +1,885 @@ +/* + * gcal-dazzling-month-view.c + * + * Copyright 2023 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GcalDazzlingMonthView" + +#include "config.h" +#include "gcal-context.h" +#include "gcal-dazzling-month-view.h" +#include "gcal-debug.h" +#include "gcal-month-view-row.h" +#include "gcal-utils.h" +#include "gcal-view-private.h" + +#include +#include + +#define N_ROWS_PER_PAGE 5 +#define N_PAGES 5 +#define N_TOTAL_ROWS (N_ROWS_PER_PAGE * N_PAGES) + +// N_PAGES must be an odd number +G_STATIC_ASSERT (N_PAGES % 2 != 0); + +struct _GcalDazzlingMonthView +{ + GtkWidget parent; + + GtkWidget *header; + GtkWidget *month_label; + GtkWidget *year_label; + GtkWidget *weekday_label[7]; + + GPtrArray *week_rows; + + /* Ranges from [0.0, 1.0] */ + gdouble row_offset; + AdwAnimation *row_offset_animation; + + AdwAnimation *kinetic_scroll_animation; + gdouble last_velocity; + + GDateTime *date; + GcalContext *context; +}; + +static void gcal_view_interface_init (GcalViewInterface *iface); + +static void gtk_buildable_interface_init (GtkBuildableIface *iface); + +G_DEFINE_FINAL_TYPE_WITH_CODE (GcalDazzlingMonthView, gcal_dazzling_month_view, GTK_TYPE_WIDGET, + G_IMPLEMENT_INTERFACE (GCAL_TYPE_VIEW, gcal_view_interface_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, gtk_buildable_interface_init)) + +enum +{ + PROP_0, + PROP_CONTEXT, + PROP_DATE, + N_PROPS, +}; + + +/* + * Auxiliary methods + */ + +static void +update_header_labels (GcalDazzlingMonthView *self) +{ + gchar year_str[10] = { 0, }; + + g_snprintf (year_str, 10, "%d", g_date_time_get_year (self->date)); + + gtk_label_set_label (GTK_LABEL (self->month_label), gcal_get_month_name (g_date_time_get_month (self->date) - 1)); + gtk_label_set_label (GTK_LABEL (self->year_label), year_str); +} + +static inline void +update_weekday_labels (GcalDazzlingMonthView *self) +{ + const gint first_weekday = get_first_weekday (); + + for (gint i = 0; i < 7; i++) + { + g_autofree gchar *weekday_name = NULL; + + weekday_name = g_utf8_strup (gcal_get_weekday ((i + first_weekday) % 7), -1); + + gtk_label_set_label (GTK_LABEL (self->weekday_label[i]), weekday_name); + } +} + +static void +update_week_ranges (GcalDazzlingMonthView *self) +{ + gint n_weeks_before; + + /* + * The current date corresponds to the week of the first + * visible row. + */ + n_weeks_before = N_ROWS_PER_PAGE * (N_PAGES - 1) / 2; + + for (gint i = 0; i < self->week_rows->len; i++) + { + g_autoptr (GDateTime) week_start = NULL; + g_autoptr (GDateTime) week_end = NULL; + g_autoptr (GcalRange) range = NULL; + g_autoptr (GDateTime) date = NULL; + GcalMonthViewRow *row; + + date = g_date_time_add_weeks (self->date, i - n_weeks_before); + week_start = gcal_date_time_get_start_of_week (date); + week_end = gcal_date_time_get_end_of_week (date); + range = gcal_range_new (week_start, week_end, GCAL_RANGE_DATE_ONLY); + + row = g_ptr_array_index (self->week_rows, i); + gcal_month_view_row_set_range (row, range); + } +} + +static void +cancel_deceleration (GcalDazzlingMonthView *self) +{ + if (self->kinetic_scroll_animation == 0) + return; + + adw_animation_pause (self->kinetic_scroll_animation); + g_clear_object (&self->kinetic_scroll_animation); +} + +static void +cancel_row_offset_animation (GcalDazzlingMonthView *self) +{ + if (!self->row_offset_animation) + return; + + adw_animation_pause (self->row_offset_animation); + g_clear_object (&self->row_offset_animation); +} + +static void +update_active_date (GcalDazzlingMonthView *self) +{ + g_autoptr (GcalRange) top_row_range = NULL; + GtkWidget *top_row; + + top_row = g_ptr_array_index (self->week_rows, N_ROWS_PER_PAGE * (N_PAGES - 1) / 2); + top_row_range = gcal_month_view_row_get_range (GCAL_MONTH_VIEW_ROW (top_row)); + g_assert (top_row_range != NULL); + + gcal_view_set_date (GCAL_VIEW (self), gcal_range_get_start (top_row_range)); +} + +static void +move_bottom_row_to_top (GcalDazzlingMonthView *self) +{ + g_autoptr (GcalRange) first_row_range = NULL; + g_autoptr (GcalRange) new_range = NULL; + GDateTime *first_row_range_start; + GtkWidget *first_row; + GtkWidget *last_row; + + first_row = g_ptr_array_index (self->week_rows, 0); + first_row_range = gcal_month_view_row_get_range (GCAL_MONTH_VIEW_ROW (first_row)); + g_assert (first_row_range != NULL); + first_row_range_start = gcal_range_get_end (first_row_range); + g_assert (first_row_range_start != NULL); + + new_range = gcal_range_new_take (g_date_time_add_weeks (first_row_range_start, -1), + g_date_time_ref (first_row_range_start), + GCAL_RANGE_DATE_ONLY); + +#ifdef GCAL_ENABLE_TRACE + { + g_autofree gchar *new_range_str = gcal_range_to_string (new_range); + GCAL_TRACE_MSG ("Moved bottom row to top, new range: %s", new_range_str); + } +#endif + + last_row = g_ptr_array_steal_index (self->week_rows, self->week_rows->len - 1); + gcal_month_view_row_set_range (GCAL_MONTH_VIEW_ROW (last_row), new_range); + g_ptr_array_insert (self->week_rows, 0, last_row); +} + +static void +move_top_row_to_bottom (GcalDazzlingMonthView *self) +{ + g_autoptr (GcalRange) last_row_range = NULL; + g_autoptr (GcalRange) new_range = NULL; + GDateTime *last_row_range_end; + GtkWidget *first_row; + GtkWidget *last_row; + + last_row = g_ptr_array_index (self->week_rows, self->week_rows->len - 1); + last_row_range = gcal_month_view_row_get_range (GCAL_MONTH_VIEW_ROW (last_row)); + g_assert (last_row_range != NULL); + last_row_range_end = gcal_range_get_end (last_row_range); + g_assert (last_row_range_end != NULL); + + new_range = gcal_range_new_take (g_date_time_ref (last_row_range_end), + g_date_time_add_weeks (last_row_range_end, 1), + GCAL_RANGE_DATE_ONLY); + +#ifdef GCAL_ENABLE_TRACE + { + g_autofree gchar *new_range_str = gcal_range_to_string (new_range); + GCAL_TRACE_MSG ("Moved top row to bottom, new range: %s", new_range_str); + } +#endif + + first_row = g_ptr_array_steal_index (self->week_rows, 0); + gcal_month_view_row_set_range (GCAL_MONTH_VIEW_ROW (first_row), new_range); + g_ptr_array_insert (self->week_rows, -1, first_row); +} + +static inline void +dump_row_ranges (GcalDazzlingMonthView *self) +{ +#if defined(GCAL_ENABLE_TRACE) && 0 + for (guint i = 0; i < self->week_rows->len; i++) + { + g_autoptr (GcalRange) range = NULL; + g_autofree gchar *range_str = NULL; + GtkWidget *row; + + row = g_ptr_array_index (self->week_rows, i); + range = gcal_month_view_row_get_range (GCAL_MONTH_VIEW_ROW (row)); + range_str = gcal_range_to_string (range); + + GCAL_TRACE_MSG (" Row %u: %s", i, range_str); + } +#endif +} + +static inline gint +get_grid_height (GcalDazzlingMonthView *self) +{ + return gtk_widget_get_height (GTK_WIDGET (self)) - gtk_widget_get_height (self->header); +} + +static void +offset_and_shuffle_rows (GcalDazzlingMonthView *self, + gdouble dy) +{ + gdouble row_height; + gint grid_height; + + GCAL_ENTRY; + + grid_height = get_grid_height (self); + row_height = grid_height / (gdouble) N_ROWS_PER_PAGE; + self->row_offset += dy / row_height; + + if (fabs (self->row_offset) > 0.5) + { + gint rows_to_shuffle = round (fabs (self->row_offset) + 0.5); + + g_assert (rows_to_shuffle > 0); + + if (self->row_offset >= 0.0) + { + /* Positive offset: move top rows to the bottom */ + while (rows_to_shuffle--) + move_top_row_to_bottom (self); + self->row_offset = fmod (self->row_offset, 0.5) - 0.5; + } + else + { + /* Negative offset: move bottom rows to the top */ + while (rows_to_shuffle--) + move_bottom_row_to_top (self); + self->row_offset = fmod (self->row_offset, 0.5) + 0.5; + } + + update_active_date (self); + dump_row_ranges (self); + } + + gtk_widget_queue_allocate (GTK_WIDGET (self)); + + GCAL_EXIT; +} + +static void +animate_row_offset_cb (gdouble value, + gpointer user_data) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (user_data); + + self->row_offset = value; + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + +static void +on_row_offset_animation_done (AdwAnimation *animation, + GcalDazzlingMonthView *self) +{ + gtk_widget_remove_css_class (GTK_WIDGET (self), "scrolling"); + cancel_row_offset_animation (self); +} + +static void +snap_to_top_row (GcalDazzlingMonthView *self) +{ + g_autoptr (AdwAnimationTarget) animation_target = NULL; + + GCAL_ENTRY; + + update_active_date (self); + dump_row_ranges (self); + + animation_target = adw_callback_animation_target_new (animate_row_offset_cb, self, NULL); + + g_clear_object (&self->row_offset_animation); + self->row_offset_animation = adw_timed_animation_new (GTK_WIDGET (self), + self->row_offset, + 0.0, + 200, // ms + g_steal_pointer (&animation_target)); + adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->row_offset_animation), + ADW_EASE_OUT_QUAD); + g_signal_connect (self->row_offset_animation, + "done", + G_CALLBACK (on_row_offset_animation_done), + self); + + adw_animation_play (self->row_offset_animation); + + GCAL_EXIT; +} + + +/* + * Callbacks + */ + +static void +on_scroll_controller_scroll_begin_cb (GtkEventControllerScroll *scroll_controller, + GcalDazzlingMonthView *self) +{ + GdkEvent *event; + + GCAL_ENTRY; + + event = gtk_event_controller_get_current_event (GTK_EVENT_CONTROLLER (scroll_controller)); + if (gdk_event_get_event_type (event) != GDK_TOUCHPAD_HOLD || + gdk_touchpad_event_get_n_fingers (event) > 1) + { + gtk_widget_add_css_class (GTK_WIDGET (self), "scrolling"); + cancel_row_offset_animation (self); + cancel_deceleration (self); + } + + GCAL_EXIT; +} + +static gboolean +on_scroll_controller_scroll_cb (GtkEventControllerScroll *scroll_controller, + gdouble dx, + gdouble dy, + GcalDazzlingMonthView *self) +{ + GCAL_ENTRY; + + cancel_row_offset_animation (self); + cancel_deceleration (self); + + offset_and_shuffle_rows (self, dy); + + GCAL_RETURN (GDK_EVENT_STOP); +} + +static void +on_scroll_controller_scroll_end_cb (GtkEventControllerScroll *scroll_controller, + GcalDazzlingMonthView *self) +{ + GCAL_ENTRY; + + snap_to_top_row (self); + + GCAL_EXIT; +} + +static void +decelerate_scroll_cb (gdouble value, + gpointer user_data) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (user_data); + gdouble dy; + + dy = self->last_velocity - value; + self->last_velocity = value; + + if (fabs (value) > (get_grid_height (self) / (gdouble) N_ROWS_PER_PAGE / 5.0)) + offset_and_shuffle_rows (self, dy); + else + adw_animation_skip (self->kinetic_scroll_animation); +} + +static void +on_kinetic_scroll_done_cb (AdwAnimation *animation, + GcalDazzlingMonthView *self) +{ + GCAL_ENTRY; + + snap_to_top_row (self); + + GCAL_EXIT; +} + +static void +on_scroll_controller_decelerate_cb (GtkEventControllerScroll *scroll_controller, + gdouble velocity_x, + gdouble velocity_y, + GcalDazzlingMonthView *self) +{ + g_autoptr (AdwAnimationTarget) animation_target = NULL; + gdouble row_height; + gdouble duration; + gint grid_height; + + GCAL_ENTRY; + + /* XXX: I don't understand, but this just feels better */ + velocity_y /= 2.0; + + cancel_row_offset_animation (self); + cancel_deceleration (self); + + grid_height = get_grid_height (self); + if (fabs (velocity_y) < (grid_height / (gdouble) N_ROWS_PER_PAGE / 5.0)) + { + snap_to_top_row (self); + GCAL_RETURN (); + } + + duration = fabs (velocity_y) / (gdouble) gtk_widget_get_height (GTK_WIDGET (self)); + animation_target = adw_callback_animation_target_new (decelerate_scroll_cb, self, NULL); + + /* Hijack the velocity so that it always ends in an actual row */ + row_height = grid_height / (gdouble) N_ROWS_PER_PAGE; + velocity_y -= fmod (velocity_y, row_height) + self->row_offset * row_height; + + g_assert (self->kinetic_scroll_animation == NULL); + self->kinetic_scroll_animation = adw_timed_animation_new (GTK_WIDGET (self), + velocity_y, + 0.0, + duration * 1000, // ms + g_steal_pointer (&animation_target)); + self->last_velocity = velocity_y; + adw_timed_animation_set_easing (ADW_TIMED_ANIMATION (self->kinetic_scroll_animation), + ADW_EASE_OUT_EXPO); + + g_signal_connect (self->kinetic_scroll_animation, + "done", + G_CALLBACK (on_kinetic_scroll_done_cb), + self); + + adw_animation_play (self->kinetic_scroll_animation); + + GCAL_EXIT; +} + + +/* + * GcalView interface + */ + +static GDateTime* +gcal_dazzling_month_view_get_date (GcalView *view) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (view); + + return self->date; +} + +static void +gcal_dazzling_month_view_set_date (GcalView *view, + GDateTime *date) +{ + GcalDazzlingMonthView *self; + gboolean month_changed; + + GCAL_ENTRY; + + self = GCAL_DAZZLING_MONTH_VIEW (view); + + month_changed = !self->date || + !date || + g_date_time_get_month (self->date) != g_date_time_get_month (date) || + g_date_time_get_year (self->date) != g_date_time_get_year (date); + + gcal_set_date_time (&self->date, date); + + if (!month_changed) + GCAL_RETURN (); + +#ifdef GCAL_ENABLE_TRACE + { + g_autofree gchar *new_date_string = g_date_time_format (date, "%x %X %z"); + GCAL_TRACE_MSG ("New date: %s", new_date_string); + } +#endif + + update_header_labels (self); + update_week_ranges (self); + + GCAL_EXIT; +} + +static void +gcal_dazzling_month_view_clear_marks (GcalView *view) +{ + GCAL_TODO ("gcal_dazzling_month_view_clear_marks"); +} + +static GList* +gcal_dazzling_month_view_get_children_by_uuid (GcalView *view, + GcalRecurrenceModType mod, + const gchar *uuid) +{ + return filter_children_by_uid_and_modtype (GTK_WIDGET (view), mod, uuid); +} + +static GDateTime* +gcal_dazzling_month_view_get_next_date (GcalView *view) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (view); + + g_assert (self->date != NULL); + return g_date_time_add_months (self->date, 1); +} + + +static GDateTime* +gcal_dazzling_month_view_get_previous_date (GcalView *view) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (view); + + g_assert (self->date != NULL); + return g_date_time_add_months (self->date, -1); +} + +static void +gcal_view_interface_init (GcalViewInterface *iface) +{ + iface->get_date = gcal_dazzling_month_view_get_date; + iface->set_date = gcal_dazzling_month_view_set_date; + iface->clear_marks = gcal_dazzling_month_view_clear_marks; + iface->get_children_by_uuid = gcal_dazzling_month_view_get_children_by_uuid; + iface->get_next_date = gcal_dazzling_month_view_get_next_date; + iface->get_previous_date = gcal_dazzling_month_view_get_previous_date; +} + + +/* + * GtkBuildable interface + */ + +static GtkBuildableIface *parent_buildable_iface; + +static void +gcal_dazzling_month_view_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (buildable); + + if (g_strcmp0 (type, "header") == 0) + { + self->header = GTK_WIDGET (child); + gtk_widget_set_parent (self->header, GTK_WIDGET (self)); + } + else + { + parent_buildable_iface->add_child (buildable, builder, child, type); + } +} + +static void +gtk_buildable_interface_init (GtkBuildableIface *iface) +{ + parent_buildable_iface = g_type_interface_peek_parent (iface); + + iface->add_child = gcal_dazzling_month_view_add_child; +} + + +/* + * GtkWidget overrides + */ + +static void +gcal_dazzling_month_view_measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + GcalDazzlingMonthView *self; + gint natural_header_size; + gint minimum_header_size; + gint minimum_row_size; + gint natural_row_size; + + self = GCAL_DAZZLING_MONTH_VIEW (widget); + + gtk_widget_measure (self->header, + orientation, + for_size, + &minimum_header_size, + &natural_header_size, + NULL, + NULL); + + for (guint i = 0; i < self->week_rows->len; i++) + { + GtkWidget *row; + gint row_minimum; + gint row_natural; + + row = g_ptr_array_index (self->week_rows, i); + + gtk_widget_measure (row, + orientation, + for_size, + &row_minimum, + &row_natural, + NULL, + NULL); + + if (i == 0) + { + minimum_row_size = row_minimum; + natural_row_size = row_natural; + } + else + { + minimum_row_size = MIN (minimum_row_size, row_minimum); + natural_row_size = MIN (natural_row_size, row_natural); + } + } + + if (minimum) + *minimum = minimum_header_size + minimum_row_size; + + if (natural) + *natural = natural_header_size + natural_row_size; + + if (minimum_baseline) + *minimum_baseline = -1; + + if (natural_baseline) + *natural_baseline = -1; +} + +static void +gcal_dazzling_month_view_size_allocate (GtkWidget *widget, + gint width, + gint height, + gint baseline) +{ + GcalDazzlingMonthView *self; + gdouble row_scroll_offset; + gdouble row_height; + gdouble y_offset; + gint header_height; + gint grid_height; + + self = GCAL_DAZZLING_MONTH_VIEW (widget); + + gtk_widget_measure (self->header, GTK_ORIENTATION_VERTICAL, width, &header_height, NULL, NULL, NULL); + gtk_widget_allocate (self->header, width, header_height, baseline, NULL); + + grid_height = height - header_height; + row_height = grid_height / (gdouble) N_ROWS_PER_PAGE; + row_scroll_offset = self->row_offset * row_height; + y_offset = header_height - row_scroll_offset - (grid_height * ((N_PAGES - 1) / 2.0)); + + for (guint i = 0; i < self->week_rows->len; i++) + { + GtkAllocation row_allocation; + GtkWidget *row; + + row = g_ptr_array_index (self->week_rows, i); + +#define ROW_Y(_i) (row_height * (_i) + y_offset) + + row_allocation.x = 0; + row_allocation.y = round (ROW_Y (i)); + row_allocation.width = width; + row_allocation.height = round (ROW_Y (i + 1)) - row_allocation.y; + +#undef ROW_Y + + gtk_widget_size_allocate (row, &row_allocation, baseline); + } +} + +static void +gcal_dazzling_month_view_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + GcalDazzlingMonthView *self; + gint header_height; + gint height; + gint width; + + self = GCAL_DAZZLING_MONTH_VIEW (widget); + + /* Snapshot rows with a clip */ + width = gtk_widget_get_width (widget); + height = gtk_widget_get_height (widget); + header_height = gtk_widget_get_height (self->header); + + gtk_snapshot_push_clip (snapshot, + &GRAPHENE_RECT_INIT (0, header_height, width, height - header_height)); + for (guint i = 0; i < self->week_rows->len; i++) + gtk_widget_snapshot_child (widget, g_ptr_array_index (self->week_rows, i), snapshot); + + gtk_snapshot_pop (snapshot); + + /* Snapshot header */ + gtk_widget_snapshot_child (widget, self->header, snapshot); +} + + +/* + * GObject overrides + */ + +static void +gcal_dazzling_month_view_dispose (GObject *object) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (object); + + GCAL_ENTRY; + + g_clear_object (&self->kinetic_scroll_animation); + g_clear_object (&self->row_offset_animation); + + g_clear_pointer (&self->header, gtk_widget_unparent); + g_clear_pointer (&self->week_rows, g_ptr_array_unref); + + G_OBJECT_CLASS (gcal_dazzling_month_view_parent_class)->dispose (object); + + GCAL_EXIT; +} + +static void +gcal_dazzling_month_view_finalize (GObject *object) +{ + GcalDazzlingMonthView *self = (GcalDazzlingMonthView *)object; + + gcal_clear_date_time (&self->date); + g_clear_object (&self->context); + + G_OBJECT_CLASS (gcal_dazzling_month_view_parent_class)->finalize (object); +} + +static void +gcal_dazzling_month_view_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (object); + + switch (prop_id) + { + case PROP_DATE: + g_value_set_boxed (value, self->date); + break; + + case PROP_CONTEXT: + g_value_set_object (value, self->context); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_dazzling_month_view_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalDazzlingMonthView *self = GCAL_DAZZLING_MONTH_VIEW (object); + + switch (prop_id) + { + case PROP_DATE: + gcal_view_set_date (GCAL_VIEW (self), g_value_get_boxed (value)); + break; + + case PROP_CONTEXT: + g_assert (self->context == NULL); + self->context = g_value_dup_object (value); + g_object_notify (object, "context"); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_dazzling_month_view_class_init (GcalDazzlingMonthViewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gcal_dazzling_month_view_dispose; + object_class->finalize = gcal_dazzling_month_view_finalize; + object_class->get_property = gcal_dazzling_month_view_get_property; + object_class->set_property = gcal_dazzling_month_view_set_property; + + widget_class->measure = gcal_dazzling_month_view_measure; + widget_class->size_allocate = gcal_dazzling_month_view_size_allocate; + widget_class->snapshot = gcal_dazzling_month_view_snapshot; + + g_object_class_override_property (object_class, PROP_CONTEXT, "context"); + g_object_class_override_property (object_class, PROP_DATE, "active-date"); + + gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/views/gcal-dazzling-month-view.ui"); + + gtk_widget_class_bind_template_child (widget_class, GcalDazzlingMonthView, month_label); + gtk_widget_class_bind_template_child (widget_class, GcalDazzlingMonthView, year_label); + + gtk_widget_class_bind_template_child_full (widget_class, "label_0", FALSE, G_STRUCT_OFFSET (GcalDazzlingMonthView, weekday_label[0])); + gtk_widget_class_bind_template_child_full (widget_class, "label_1", FALSE, G_STRUCT_OFFSET (GcalDazzlingMonthView, weekday_label[1])); + gtk_widget_class_bind_template_child_full (widget_class, "label_2", FALSE, G_STRUCT_OFFSET (GcalDazzlingMonthView, weekday_label[2])); + gtk_widget_class_bind_template_child_full (widget_class, "label_3", FALSE, G_STRUCT_OFFSET (GcalDazzlingMonthView, weekday_label[3])); + gtk_widget_class_bind_template_child_full (widget_class, "label_4", FALSE, G_STRUCT_OFFSET (GcalDazzlingMonthView, weekday_label[4])); + gtk_widget_class_bind_template_child_full (widget_class, "label_5", FALSE, G_STRUCT_OFFSET (GcalDazzlingMonthView, weekday_label[5])); + gtk_widget_class_bind_template_child_full (widget_class, "label_6", FALSE, G_STRUCT_OFFSET (GcalDazzlingMonthView, weekday_label[6])); + + gtk_widget_class_bind_template_callback (widget_class, on_scroll_controller_scroll_begin_cb); + gtk_widget_class_bind_template_callback (widget_class, on_scroll_controller_scroll_cb); + gtk_widget_class_bind_template_callback (widget_class, on_scroll_controller_scroll_end_cb); + gtk_widget_class_bind_template_callback (widget_class, on_scroll_controller_decelerate_cb); + + gtk_widget_class_set_css_name (widget_class, "calendar-view"); +} + +static void +gcal_dazzling_month_view_init (GcalDazzlingMonthView *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + update_weekday_labels (self); + + self->week_rows = g_ptr_array_new_full (N_TOTAL_ROWS, (GDestroyNotify) gtk_widget_unparent); + for (gint i = 0; i < N_TOTAL_ROWS; i++) + { + GtkWidget *row = gcal_month_view_row_new (); + gtk_widget_set_parent (row, GTK_WIDGET (self)); + g_ptr_array_add (self->week_rows, row); + } + + gtk_widget_insert_after (self->header, GTK_WIDGET (self), NULL); +} diff --git a/src/gui/views/gcal-dazzling-month-view.h b/src/gui/views/gcal-dazzling-month-view.h new file mode 100644 index 00000000..45bef5e2 --- /dev/null +++ b/src/gui/views/gcal-dazzling-month-view.h @@ -0,0 +1,31 @@ +/* + * gcal-dazzling-month-view.h + * + * Copyright 2023 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GCAL_TYPE_DAZZLING_MONTH_VIEW (gcal_dazzling_month_view_get_type()) +G_DECLARE_FINAL_TYPE (GcalDazzlingMonthView, gcal_dazzling_month_view, GCAL, DAZZLING_MONTH_VIEW, GtkWidget) + +G_END_DECLS diff --git a/src/gui/views/gcal-dazzling-month-view.ui b/src/gui/views/gcal-dazzling-month-view.ui new file mode 100644 index 00000000..d6a88d74 --- /dev/null +++ b/src/gui/views/gcal-dazzling-month-view.ui @@ -0,0 +1,130 @@ + + + + diff --git a/src/gui/views/gcal-month-cell.c b/src/gui/views/gcal-month-cell.c index c48ecd63..1e9e5005 100644 --- a/src/gui/views/gcal-month-cell.c +++ b/src/gui/views/gcal-month-cell.c @@ -78,6 +78,7 @@ static void update_style_flags (GcalMonthCell *self) { g_autoptr (GDateTime) today = NULL; + gint weekday; /* Today */ today = g_date_time_new_now_local (); @@ -86,6 +87,12 @@ update_style_flags (GcalMonthCell *self) gtk_widget_add_css_class (GTK_WIDGET (self), "today"); else gtk_widget_remove_css_class (GTK_WIDGET (self), "today"); + + weekday = g_date_time_get_day_of_week (self->date); + if (is_workday (weekday)) + gtk_widget_add_css_class (GTK_WIDGET (self), "workday"); + else + gtk_widget_remove_css_class (GTK_WIDGET (self), "workday"); } static void diff --git a/src/gui/views/gcal-month-view-row.c b/src/gui/views/gcal-month-view-row.c new file mode 100644 index 00000000..83360f81 --- /dev/null +++ b/src/gui/views/gcal-month-view-row.c @@ -0,0 +1,262 @@ +/* + * gcal-month-view-row.c + * + * Copyright 2023 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "GcalMonthViewRow" + +#include "config.h" +#include "gcal-date-time-utils.h" +#include "gcal-debug.h" +#include "gcal-month-cell.h" +#include "gcal-month-view-row.h" + +struct _GcalMonthViewRow +{ + GtkWidget parent; + + GtkWidget *day_cells[7]; + + GcalRange *range; +}; + +G_DEFINE_FINAL_TYPE (GcalMonthViewRow, gcal_month_view_row, GTK_TYPE_WIDGET) + +enum +{ + PROP_0, + N_PROPS, +}; + +static GParamSpec *properties [N_PROPS]; + + +/* + * GtkWidget overrides + */ + +static void +gcal_month_view_row_measure (GtkWidget *widget, + GtkOrientation orientation, + gint for_size, + gint *out_minimum, + gint *out_natural, + gint *out_minimum_baseline, + gint *out_natural_baseline) +{ + GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (widget); + gint minimum = 0; + gint natural = 0; + + for (guint i = 0; i < 7; i++) + { + gint child_minimum_baseline; + gint child_natural_baseline; + gint child_minimum; + gint child_natural; + + gtk_widget_measure (self->day_cells[i], + orientation, + for_size, + &child_minimum, + &child_natural, + NULL, + NULL); + + + minimum += child_minimum; + natural += child_natural; + } + + if (out_minimum) + *out_minimum = minimum; + + if (out_natural) + *out_natural = natural; + + if (out_minimum_baseline) + *out_minimum_baseline = -1; + + if (out_natural_baseline) + *out_natural_baseline = -1; +} + +static void +gcal_month_view_row_size_allocate (GtkWidget *widget, + gint width, + gint height, + gint baseline) +{ + GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (widget); + gboolean is_ltr; + gdouble cell_width; + + is_ltr = gtk_widget_get_direction (widget) != GTK_TEXT_DIR_RTL; + cell_width = width / 7.0; + + for (guint i = 0; i < 7; i++) + { + GtkAllocation allocation; + GtkWidget *cell; + + allocation.x = round (cell_width * i); + allocation.y = 0; + allocation.width = round (cell_width * (i + 1)) - allocation.x; + allocation.height = height; + + cell = is_ltr ? self->day_cells[i] : self->day_cells[7 - i + 1]; + gtk_widget_size_allocate (cell, &allocation, baseline); + } +} + +static void +gcal_month_view_row_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (widget); + + for (guint i = 0; i < 7; i++) + gtk_widget_snapshot_child (widget, self->day_cells[i], snapshot); +} + + +/* + * GObject overrides + */ + +static void +gcal_month_view_row_dispose (GObject *object) +{ + GcalMonthViewRow *self = (GcalMonthViewRow *)object; + + for (guint i = 0; i < 7; i++) + g_clear_pointer (&self->day_cells[i], gtk_widget_unparent); + + G_OBJECT_CLASS (gcal_month_view_row_parent_class)->dispose (object); +} + +static void +gcal_month_view_row_finalize (GObject *object) +{ + GcalMonthViewRow *self = (GcalMonthViewRow *)object; + + g_clear_pointer (&self->range, gcal_range_unref); + + G_OBJECT_CLASS (gcal_month_view_row_parent_class)->finalize (object); +} + +static void +gcal_month_view_row_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (object); + + switch (prop_id) + { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_month_view_row_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GcalMonthViewRow *self = GCAL_MONTH_VIEW_ROW (object); + + switch (prop_id) + { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +gcal_month_view_row_class_init (GcalMonthViewRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = gcal_month_view_row_dispose; + object_class->finalize = gcal_month_view_row_finalize; + object_class->get_property = gcal_month_view_row_get_property; + object_class->set_property = gcal_month_view_row_set_property; + + widget_class->measure = gcal_month_view_row_measure; + widget_class->size_allocate = gcal_month_view_row_size_allocate; + widget_class->snapshot = gcal_month_view_row_snapshot; + + gtk_widget_class_set_css_name (widget_class, "monthviewrow"); +} + +static void +gcal_month_view_row_init (GcalMonthViewRow *self) +{ + for (guint i = 0; i < 7; i++) + { + self->day_cells[i] = gcal_month_cell_new (); + gcal_month_cell_set_overflow (GCAL_MONTH_CELL (self->day_cells[i]), 0); + gtk_widget_set_parent (self->day_cells[i], GTK_WIDGET (self)); + } +} + +GtkWidget * +gcal_month_view_row_new (void) +{ + return g_object_new (GCAL_TYPE_MONTH_VIEW_ROW, NULL); +} + +GcalRange * +gcal_month_view_row_get_range (GcalMonthViewRow *self) +{ + g_return_val_if_fail (GCAL_IS_MONTH_VIEW_ROW (self), NULL); + + return self->range ? gcal_range_ref (self->range) : NULL; +} + +void +gcal_month_view_row_set_range (GcalMonthViewRow *self, + GcalRange *range) +{ + g_autoptr (GDateTime) start = NULL; + + g_return_if_fail (GCAL_IS_MONTH_VIEW_ROW (self)); + g_return_if_fail (range != NULL); + + GCAL_ENTRY; + + if (self->range && gcal_range_compare (self->range, range) == 0) + GCAL_RETURN (); + + g_clear_pointer (&self->range, gcal_range_unref); + self->range = gcal_range_ref (range); + + /* TODO: remove all widgets */ + + start = gcal_range_get_start (range); + for (guint i = 0; i < 7; i++) + { + g_autoptr (GDateTime) day = g_date_time_add_days (start, i); + gcal_month_cell_set_date (GCAL_MONTH_CELL (self->day_cells[i]), day); + } +} diff --git a/src/gui/views/gcal-month-view-row.h b/src/gui/views/gcal-month-view-row.h new file mode 100644 index 00000000..8e7e3634 --- /dev/null +++ b/src/gui/views/gcal-month-view-row.h @@ -0,0 +1,40 @@ +/* + * gcal-month-view-row.h + * + * Copyright 2023 Georges Basile Stavracas Neto + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "gcal-range.h" + +G_BEGIN_DECLS + +#define GCAL_TYPE_MONTH_VIEW_ROW (gcal_month_view_row_get_type()) +G_DECLARE_FINAL_TYPE (GcalMonthViewRow, gcal_month_view_row, GCAL, MONTH_VIEW_ROW, GtkWidget) + +GtkWidget * gcal_month_view_row_new (void); + +GcalRange * gcal_month_view_row_get_range (GcalMonthViewRow *self); + +void gcal_month_view_row_set_range (GcalMonthViewRow *self, + GcalRange *range); + +G_END_DECLS diff --git a/src/gui/views/meson.build b/src/gui/views/meson.build index f68b3dc9..1df8125a 100644 --- a/src/gui/views/meson.build +++ b/src/gui/views/meson.build @@ -8,9 +8,11 @@ built_sources += gnome.compile_resources( sources += files( 'gcal-agenda-view.c', + 'gcal-dazzling-month-view.c', 'gcal-month-cell.c', 'gcal-month-popover.c', 'gcal-month-view.c', + 'gcal-month-view-row.c', 'gcal-view.c', 'gcal-week-grid.c', 'gcal-week-header.c', diff --git a/src/gui/views/views.gresource.xml b/src/gui/views/views.gresource.xml index e1c21c87..ee68d8d3 100644 --- a/src/gui/views/views.gresource.xml +++ b/src/gui/views/views.gresource.xml @@ -2,6 +2,7 @@ gcal-agenda-view.ui + gcal-dazzling-month-view.ui gcal-month-cell.ui gcal-month-popover.ui gcal-month-view.ui diff --git a/src/theme/style.css b/src/theme/style.css index f1c4a63c..91846602 100644 --- a/src/theme/style.css +++ b/src/theme/style.css @@ -317,10 +317,15 @@ monthcell:nth-child(7n + 1) { border-left-width: 0; } monthcell.today { background-color: alpha(@accent_bg_color, 0.25); } monthcell:selected.today { background-color: alpha(@accent_bg_color, 0.33); } -monthcell.out-of-month { +monthcell.out-of-month, +monthcell:not(.workday) { background-color: alpha(@theme_bg_color, 0.4); } +monthcell.today:not(.workday) { + background-color: alpha(@accent_bg_color, 0.15); +} + monthcell label { color: @theme_fg_color; font-size: 0.9rem; @@ -346,6 +351,21 @@ monthcell button { border-right: none; } +/* + * Month row + */ + +calendar-view.scrolling > box { + background-color: transparent; + box-shadow: @headerbar_shade_color 0 2px 4px; + transition: box-shadow 200ms; +} + + +calendar-view:not(.scrolling) > box { + transition: box-shadow 200ms; +} + /* * Month popover */