text view: Smooth cursor blinking

Fade the text cursor in and out, instead
of abruptly turning it on and off.
This commit is contained in:
Matthias Clasen 2019-07-21 13:06:23 -07:00 committed by Christian Hergert
parent 4ff9163c47
commit 064ad42432
4 changed files with 121 additions and 83 deletions

View File

@ -3795,7 +3795,8 @@ render_para (GskPangoRenderer *crenderer,
int offset_y, int offset_y,
GtkTextLineDisplay *line_display, GtkTextLineDisplay *line_display,
int selection_start_index, int selection_start_index,
int selection_end_index) int selection_end_index,
float cursor_alpha)
{ {
GtkStyleContext *context; GtkStyleContext *context;
PangoLayout *layout = line_display->layout; PangoLayout *layout = line_display->layout;
@ -3972,6 +3973,7 @@ render_para (GskPangoRenderer *crenderer,
* (normally white on black) */ * (normally white on black) */
_gtk_style_context_get_cursor_color (context, &cursor_color, NULL); _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); gtk_snapshot_append_color (crenderer->snapshot, &cursor_color, &bounds);
/* draw text under the cursor if any */ /* draw text under the cursor if any */
@ -3985,6 +3987,7 @@ render_para (GskPangoRenderer *crenderer,
baseline); baseline);
gtk_snapshot_pop (crenderer->snapshot); gtk_snapshot_pop (crenderer->snapshot);
} }
gtk_snapshot_pop (crenderer->snapshot);
} }
} }
@ -4002,7 +4005,8 @@ void
gtk_text_layout_snapshot (GtkTextLayout *layout, gtk_text_layout_snapshot (GtkTextLayout *layout,
GtkWidget *widget, GtkWidget *widget,
GtkSnapshot *snapshot, GtkSnapshot *snapshot,
const GdkRectangle *clip) const GdkRectangle *clip,
float cursor_alpha)
{ {
GskPangoRenderer *crenderer; GskPangoRenderer *crenderer;
GtkStyleContext *context; GtkStyleContext *context;
@ -4085,7 +4089,8 @@ gtk_text_layout_snapshot (GtkTextLayout *layout,
} }
render_para (crenderer, offset_y, line_display, 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 /* We paint the cursors last, because they overlap another chunk
* and need to appear on top. * and need to appear on top.
@ -4094,6 +4099,7 @@ gtk_text_layout_snapshot (GtkTextLayout *layout,
{ {
int i; int i;
gtk_snapshot_push_opacity (crenderer->snapshot, cursor_alpha);
for (i = 0; i < line_display->cursors->len; i++) for (i = 0; i < line_display->cursors->len; i++)
{ {
int index; int index;
@ -4106,6 +4112,8 @@ gtk_text_layout_snapshot (GtkTextLayout *layout,
line_display->x_offset, offset_y + line_display->top_margin, line_display->x_offset, offset_y + line_display->top_margin,
line_display->layout, index, dir); line_display->layout, index, dir);
} }
gtk_snapshot_pop (crenderer->snapshot);
} }
} /* line_display->height > 0 */ } /* line_display->height > 0 */

View File

@ -400,7 +400,8 @@ void gtk_text_layout_spew (GtkTextLayout *layout);
void gtk_text_layout_snapshot (GtkTextLayout *layout, void gtk_text_layout_snapshot (GtkTextLayout *layout,
GtkWidget *widget, GtkWidget *widget,
GtkSnapshot *snapshot, GtkSnapshot *snapshot,
const GdkRectangle *clip); const GdkRectangle *clip,
float cursor_alpha);
G_END_DECLS G_END_DECLS

View File

@ -335,7 +335,7 @@ gtk_text_util_create_rich_drag_icon (GtkWidget *widget,
snapshot = gtk_snapshot_new (); 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 (layout);
g_object_unref (new_buffer); g_object_unref (new_buffer);

View File

@ -209,7 +209,10 @@ struct _GtkTextViewPrivate
GtkTextMark *first_para_mark; /* Mark at the beginning of the first onscreen paragraph */ 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 */ 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 scroll_timeout;
guint first_validate_idle; /* Idle to revalidate onscreen portion, runs before resize */ 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_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 (); 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_save (snapshot);
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-priv->xoffset, -priv->yoffset)); gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-priv->xoffset, -priv->yoffset));
@ -5364,7 +5361,8 @@ gtk_text_view_paint (GtkWidget *widget,
priv->yoffset, priv->yoffset,
gtk_widget_get_width (widget), gtk_widget_get_width (widget),
gtk_widget_get_height (widget) gtk_widget_get_height (widget)
}); },
priv->cursor_alpha);
gtk_snapshot_restore (snapshot); gtk_snapshot_restore (snapshot);
} }
@ -5661,15 +5659,80 @@ get_cursor_blink_timeout (GtkTextView *text_view)
* Blink! * Blink!
*/ */
static gint typedef struct {
blink_cb (gpointer data) 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; GtkTextView *text_view;
GtkTextViewPrivate *priv; GtkTextViewPrivate *priv;
gboolean visible;
gint blink_timeout; 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; priv = text_view->priv;
if (!gtk_widget_has_focus (GTK_WIDGET (text_view))) 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" g_warning ("GtkTextView - did not receive a focus-out.\n"
"If you handle this event, you must return\n" "If you handle this event, you must return\n"
"GDK_EVENT_PROPAGATE so the text view gets the event as well"); "GDK_EVENT_PROPAGATE so the text view gets the event as well");
gtk_text_view_check_cursor_blink (text_view); gtk_text_view_check_cursor_blink (text_view);
return FALSE; return FALSE;
@ -5686,47 +5748,41 @@ blink_cb (gpointer data)
g_assert (priv->layout); g_assert (priv->layout);
g_assert (cursor_visible (text_view)); g_assert (cursor_visible (text_view));
visible = gtk_text_layout_get_cursor_visible (priv->layout);
blink_timeout = get_cursor_blink_timeout (text_view); blink_timeout = get_cursor_blink_timeout (text_view);
if (priv->blink_time > 1000 * blink_timeout && blink_time = get_cursor_time (text_view);
blink_timeout < G_MAXINT/1000)
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 */ /* we've blinked enough without the user doing anything, stop blinking */
visible = 0; priv->cursor_alpha = 1.0;
priv->blink_timeout = 0; remove_blink_timeout (text_view);
} gtk_widget_queue_draw (widget);
else if (visible)
{ return G_SOURCE_REMOVE;
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);
} }
gtk_text_layout_set_cursor_visible (priv->layout, !visible); phase = (now - data->start) / (float) (data->end - data->start);
/* Remove ourselves */ priv->cursor_alpha = blink_alpha (phase);
return FALSE;
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 static void
gtk_text_view_stop_cursor_blink (GtkTextView *text_view) gtk_text_view_stop_cursor_blink (GtkTextView *text_view)
{ {
if (text_view->priv->blink_timeout) remove_blink_timeout (text_view);
{
g_source_remove (text_view->priv->blink_timeout);
text_view->priv->blink_timeout = 0;
}
} }
static void static void
@ -5734,52 +5790,25 @@ gtk_text_view_check_cursor_blink (GtkTextView *text_view)
{ {
GtkTextViewPrivate *priv = text_view->priv; GtkTextViewPrivate *priv = text_view->priv;
if (priv->layout != NULL && if (cursor_blinks (text_view))
cursor_visible (text_view) &&
gtk_widget_has_focus (GTK_WIDGET (text_view)))
{ {
if (cursor_blinks (text_view)) if (!priv->blink_tick)
{ add_blink_timeout (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);
}
} }
else else
{ {
gtk_text_view_stop_cursor_blink (text_view); if (priv->blink_tick)
gtk_text_layout_set_cursor_visible (priv->layout, FALSE); remove_blink_timeout (text_view);
} }
} }
static void static void
gtk_text_view_pend_cursor_blink (GtkTextView *text_view) gtk_text_view_pend_cursor_blink (GtkTextView *text_view)
{ {
GtkTextViewPrivate *priv = text_view->priv; if (cursor_blinks (text_view))
if (priv->layout != NULL &&
cursor_visible (text_view) &&
gtk_widget_has_focus (GTK_WIDGET (text_view)) &&
cursor_blinks (text_view))
{ {
gtk_text_view_stop_cursor_blink (text_view); remove_blink_timeout (text_view);
gtk_text_layout_set_cursor_visible (priv->layout, TRUE); add_blink_timeout (text_view);
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");
} }
} }
@ -5788,7 +5817,7 @@ gtk_text_view_reset_blink_time (GtkTextView *text_view)
{ {
GtkTextViewPrivate *priv = text_view->priv; GtkTextViewPrivate *priv = text_view->priv;
priv->blink_time = 0; priv->blink_start_time = g_get_monotonic_time ();
} }