From 064ad424326ebdee48bd6a1d13da87924c9ad6fd Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Sun, 21 Jul 2019 13:06:23 -0700 Subject: [PATCH] text view: Smooth cursor blinking Fade the text cursor in and out, instead of abruptly turning it on and off. --- gtk/gtktextlayout.c | 14 ++- gtk/gtktextlayoutprivate.h | 3 +- gtk/gtktextutil.c | 2 +- gtk/gtktextview.c | 185 +++++++++++++++++++++---------------- 4 files changed, 121 insertions(+), 83 deletions(-) diff --git a/gtk/gtktextlayout.c b/gtk/gtktextlayout.c index 508aede412..acf003912e 100644 --- a/gtk/gtktextlayout.c +++ b/gtk/gtktextlayout.c @@ -3795,7 +3795,8 @@ render_para (GskPangoRenderer *crenderer, int offset_y, GtkTextLineDisplay *line_display, int selection_start_index, - int selection_end_index) + int selection_end_index, + float cursor_alpha) { GtkStyleContext *context; PangoLayout *layout = line_display->layout; @@ -3972,6 +3973,7 @@ render_para (GskPangoRenderer *crenderer, * (normally white on black) */ _gtk_style_context_get_cursor_color (context, &cursor_color, NULL); + gtk_snapshot_push_opacity (crenderer->snapshot, cursor_alpha); gtk_snapshot_append_color (crenderer->snapshot, &cursor_color, &bounds); /* draw text under the cursor if any */ @@ -3985,6 +3987,7 @@ render_para (GskPangoRenderer *crenderer, baseline); gtk_snapshot_pop (crenderer->snapshot); } + gtk_snapshot_pop (crenderer->snapshot); } } @@ -4002,7 +4005,8 @@ void gtk_text_layout_snapshot (GtkTextLayout *layout, GtkWidget *widget, GtkSnapshot *snapshot, - const GdkRectangle *clip) + const GdkRectangle *clip, + float cursor_alpha) { GskPangoRenderer *crenderer; GtkStyleContext *context; @@ -4085,7 +4089,8 @@ gtk_text_layout_snapshot (GtkTextLayout *layout, } render_para (crenderer, offset_y, line_display, - selection_start_index, selection_end_index); + selection_start_index, selection_end_index, + cursor_alpha); /* We paint the cursors last, because they overlap another chunk * and need to appear on top. @@ -4094,6 +4099,7 @@ gtk_text_layout_snapshot (GtkTextLayout *layout, { int i; + gtk_snapshot_push_opacity (crenderer->snapshot, cursor_alpha); for (i = 0; i < line_display->cursors->len; i++) { int index; @@ -4106,6 +4112,8 @@ gtk_text_layout_snapshot (GtkTextLayout *layout, line_display->x_offset, offset_y + line_display->top_margin, line_display->layout, index, dir); } + + gtk_snapshot_pop (crenderer->snapshot); } } /* line_display->height > 0 */ diff --git a/gtk/gtktextlayoutprivate.h b/gtk/gtktextlayoutprivate.h index 7ba6627b04..7aff2074ff 100644 --- a/gtk/gtktextlayoutprivate.h +++ b/gtk/gtktextlayoutprivate.h @@ -400,7 +400,8 @@ void gtk_text_layout_spew (GtkTextLayout *layout); void gtk_text_layout_snapshot (GtkTextLayout *layout, GtkWidget *widget, GtkSnapshot *snapshot, - const GdkRectangle *clip); + const GdkRectangle *clip, + float cursor_alpha); G_END_DECLS diff --git a/gtk/gtktextutil.c b/gtk/gtktextutil.c index e806c6a205..2c3c43d8bc 100644 --- a/gtk/gtktextutil.c +++ b/gtk/gtktextutil.c @@ -335,7 +335,7 @@ gtk_text_util_create_rich_drag_icon (GtkWidget *widget, snapshot = gtk_snapshot_new (); - gtk_text_layout_snapshot (layout, widget, snapshot, &(GdkRectangle) { 0, 0, layout_width, layout_height }); + gtk_text_layout_snapshot (layout, widget, snapshot, &(GdkRectangle) { 0, 0, layout_width, layout_height }, 1.0); g_object_unref (layout); g_object_unref (new_buffer); diff --git a/gtk/gtktextview.c b/gtk/gtktextview.c index 73c8521455..4b30b8d653 100644 --- a/gtk/gtktextview.c +++ b/gtk/gtktextview.c @@ -209,7 +209,10 @@ struct _GtkTextViewPrivate GtkTextMark *first_para_mark; /* Mark at the beginning of the first onscreen paragraph */ gint first_para_pixels; /* Offset of top of screen in the first onscreen paragraph */ - guint blink_timeout; + guint64 blink_start_time; + guint blink_tick; + float cursor_alpha; + guint scroll_timeout; guint first_validate_idle; /* Idle to revalidate onscreen portion, runs before resize */ @@ -5346,12 +5349,6 @@ gtk_text_view_paint (GtkWidget *widget, g_warning (G_STRLOC ": somehow some text lines were modified or scrolling occurred since the last validation of lines on the screen - may be a text widget bug."); g_assert_not_reached (); } - -#if 0 - printf ("painting %d,%d %d x %d\n", - area->x, area->y, - area->width, area->height); -#endif gtk_snapshot_save (snapshot); gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-priv->xoffset, -priv->yoffset)); @@ -5364,7 +5361,8 @@ gtk_text_view_paint (GtkWidget *widget, priv->yoffset, gtk_widget_get_width (widget), gtk_widget_get_height (widget) - }); + }, + priv->cursor_alpha); gtk_snapshot_restore (snapshot); } @@ -5661,15 +5659,80 @@ get_cursor_blink_timeout (GtkTextView *text_view) * Blink! */ -static gint -blink_cb (gpointer data) +typedef struct { + guint64 start; + guint64 end; +} BlinkData; + +static gboolean blink_cb (GtkWidget *widget, + GdkFrameClock *clock, + gpointer user_data); + + +static void +add_blink_timeout (GtkTextView *self) +{ + GtkTextViewPrivate *priv = self->priv; + BlinkData *data; + int blink_time; + + priv->blink_start_time = g_get_monotonic_time (); + priv->cursor_alpha = 1.0; + + blink_time = get_cursor_time (self); + + data = g_new (BlinkData, 1); + data->start = priv->blink_start_time; + data->end = data->start + blink_time * 1000; + + priv->blink_tick = gtk_widget_add_tick_callback (GTK_WIDGET (self), + blink_cb, + data, + g_free); +} + +static void +remove_blink_timeout (GtkTextView *self) +{ + GtkTextViewPrivate *priv = self->priv; + + if (priv->blink_tick) + { + gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->blink_tick); + priv->blink_tick = 0; + } +} + +static float +blink_alpha (float phase) +{ + /* keep it simple, and split the blink cycle evenly + * into visible, fading out, invisible, fading in + */ + if (phase < 0.25) + return 1; + else if (phase < 0.5) + return 1 - 4 * (phase - 0.25); + else if (phase < 0.75) + return 0; + else + return 4 * (phase - 0.75); +} + +static gboolean +blink_cb (GtkWidget *widget, + GdkFrameClock *clock, + gpointer user_data) { GtkTextView *text_view; GtkTextViewPrivate *priv; - gboolean visible; gint blink_timeout; + gint blink_time; + guint64 now; + float phase; + BlinkData *data = user_data; - text_view = GTK_TEXT_VIEW (data); + text_view = GTK_TEXT_VIEW (widget); priv = text_view->priv; if (!gtk_widget_has_focus (GTK_WIDGET (text_view))) @@ -5677,7 +5740,6 @@ blink_cb (gpointer data) g_warning ("GtkTextView - did not receive a focus-out.\n" "If you handle this event, you must return\n" "GDK_EVENT_PROPAGATE so the text view gets the event as well"); - gtk_text_view_check_cursor_blink (text_view); return FALSE; @@ -5686,47 +5748,41 @@ blink_cb (gpointer data) g_assert (priv->layout); g_assert (cursor_visible (text_view)); - visible = gtk_text_layout_get_cursor_visible (priv->layout); - blink_timeout = get_cursor_blink_timeout (text_view); - if (priv->blink_time > 1000 * blink_timeout && - blink_timeout < G_MAXINT/1000) + blink_time = get_cursor_time (text_view); + + now = g_get_monotonic_time (); + + if (now > priv->blink_start_time + blink_timeout * 1000000) { /* we've blinked enough without the user doing anything, stop blinking */ - visible = 0; - priv->blink_timeout = 0; - } - else if (visible) - { - priv->blink_timeout = g_timeout_add (get_cursor_time (text_view) * CURSOR_OFF_MULTIPLIER / CURSOR_DIVIDER, - blink_cb, - text_view); - g_source_set_name_by_id (priv->blink_timeout, "[gtk] blink_cb"); - } - else - { - priv->blink_timeout = g_timeout_add (get_cursor_time (text_view) * CURSOR_ON_MULTIPLIER / CURSOR_DIVIDER, - blink_cb, - text_view); - g_source_set_name_by_id (priv->blink_timeout, "[gtk] blink_cb"); - priv->blink_time += get_cursor_time (text_view); + priv->cursor_alpha = 1.0; + remove_blink_timeout (text_view); + gtk_widget_queue_draw (widget); + + return G_SOURCE_REMOVE; } - gtk_text_layout_set_cursor_visible (priv->layout, !visible); + phase = (now - data->start) / (float) (data->end - data->start); - /* Remove ourselves */ - return FALSE; + priv->cursor_alpha = blink_alpha (phase); + + if (now >= data->end) + { + data->start = data->end; + data->end = data->start + blink_time * 1000; + } + + gtk_widget_queue_draw (widget); + + return G_SOURCE_CONTINUE; } static void gtk_text_view_stop_cursor_blink (GtkTextView *text_view) { - if (text_view->priv->blink_timeout) - { - g_source_remove (text_view->priv->blink_timeout); - text_view->priv->blink_timeout = 0; - } + remove_blink_timeout (text_view); } static void @@ -5734,52 +5790,25 @@ gtk_text_view_check_cursor_blink (GtkTextView *text_view) { GtkTextViewPrivate *priv = text_view->priv; - if (priv->layout != NULL && - cursor_visible (text_view) && - gtk_widget_has_focus (GTK_WIDGET (text_view))) + if (cursor_blinks (text_view)) { - if (cursor_blinks (text_view)) - { - if (priv->blink_timeout == 0) - { - gtk_text_layout_set_cursor_visible (priv->layout, TRUE); - - priv->blink_timeout = g_timeout_add (get_cursor_time (text_view) * CURSOR_OFF_MULTIPLIER / CURSOR_DIVIDER, - blink_cb, - text_view); - g_source_set_name_by_id (priv->blink_timeout, "[gtk] blink_cb"); - } - } - else - { - gtk_text_view_stop_cursor_blink (text_view); - gtk_text_layout_set_cursor_visible (priv->layout, TRUE); - } + if (!priv->blink_tick) + add_blink_timeout (text_view); } else { - gtk_text_view_stop_cursor_blink (text_view); - gtk_text_layout_set_cursor_visible (priv->layout, FALSE); + if (priv->blink_tick) + remove_blink_timeout (text_view); } } static void gtk_text_view_pend_cursor_blink (GtkTextView *text_view) { - GtkTextViewPrivate *priv = text_view->priv; - - if (priv->layout != NULL && - cursor_visible (text_view) && - gtk_widget_has_focus (GTK_WIDGET (text_view)) && - cursor_blinks (text_view)) + if (cursor_blinks (text_view)) { - gtk_text_view_stop_cursor_blink (text_view); - gtk_text_layout_set_cursor_visible (priv->layout, TRUE); - - priv->blink_timeout = g_timeout_add (get_cursor_time (text_view) * CURSOR_PEND_MULTIPLIER / CURSOR_DIVIDER, - blink_cb, - text_view); - g_source_set_name_by_id (priv->blink_timeout, "[gtk] blink_cb"); + remove_blink_timeout (text_view); + add_blink_timeout (text_view); } } @@ -5788,7 +5817,7 @@ gtk_text_view_reset_blink_time (GtkTextView *text_view) { GtkTextViewPrivate *priv = text_view->priv; - priv->blink_time = 0; + priv->blink_start_time = g_get_monotonic_time (); }