[gtk/wip/chergert/textview-caching: 15/15] textlayout: introduce caching for GtkTextLineDisplay



commit 9926e6ebde28956ae65e5127966fd740552fbe6a
Author: Christian Hergert <chergert redhat com>
Date:   Fri Jul 26 10:40:43 2019 -0700

    textlayout: introduce caching for GtkTextLineDisplay
    
    This adds a GtkTextLineDisplayCache which can be used to cache a number
    of GtkTextLineDisplay (and thus, PangoLayout) while displaying a view.
    
    It uses a GSequence to track the position of the GtkTextLineDisplay
    relative to each other, a MRU to cull the least recently used display,
    and and a direct hashtable to lookup display by GtkTextLine.
    
    We only cache lines that are to be displayed (!size_only). We may want to
    either create a second collection of "size_only" lines to speed that up,
    or determine that it is unnecessary (which is likely the case).

 gtk/gtktextlayout.c                  | 468 ++++++++++++-----------
 gtk/gtktextlayoutprivate.h           |  25 +-
 gtk/gtktextlinedisplaycache.c        | 716 +++++++++++++++++++++++++++++++++++
 gtk/gtktextlinedisplaycacheprivate.h |  59 +++
 gtk/meson.build                      |   1 +
 5 files changed, 1051 insertions(+), 218 deletions(-)
---
diff --git a/gtk/gtktextlayout.c b/gtk/gtktextlayout.c
index 3196db22f8..2ee27ed762 100644
--- a/gtk/gtktextlayout.c
+++ b/gtk/gtktextlayout.c
@@ -82,6 +82,7 @@
 #include "gtktextbtree.h"
 #include "gtktextbufferprivate.h"
 #include "gtktextiterprivate.h"
+#include "gtktextlinedisplaycacheprivate.h"
 #include "gtktextutil.h"
 #include "gskpango.h"
 #include "gtkintl.h"
@@ -101,6 +102,9 @@ struct _GtkTextLayoutPrivate
      direction only influences the direction of the cursor line.
   */
   GtkTextLine *cursor_line;
+
+  /* Cache for GtkTextLineDisplay to reduce overhead creating layouts */
+  GtkTextLineDisplayCache *cache;
 };
 
 static GtkTextLineData *gtk_text_layout_real_wrap (GtkTextLayout *layout,
@@ -133,19 +137,33 @@ static void gtk_text_layout_invalidate_all (GtkTextLayout *layout);
 
 static PangoAttribute *gtk_text_attr_appearance_new (const GtkTextAppearance *appearance);
 
-static void gtk_text_layout_mark_set_handler    (GtkTextBuffer     *buffer,
-                                                const GtkTextIter *location,
-                                                GtkTextMark       *mark,
-                                                gpointer           data);
-static void gtk_text_layout_buffer_insert_text  (GtkTextBuffer     *textbuffer,
-                                                GtkTextIter       *iter,
-                                                gchar             *str,
-                                                gint               len,
-                                                gpointer           data);
-static void gtk_text_layout_buffer_delete_range (GtkTextBuffer     *textbuffer,
-                                                GtkTextIter       *start,
-                                                GtkTextIter       *end,
-                                                gpointer           data);
+static void gtk_text_layout_after_mark_set_handler     (GtkTextBuffer     *buffer,
+                                                        const GtkTextIter *location,
+                                                        GtkTextMark       *mark,
+                                                        gpointer           data);
+static void gtk_text_layout_after_buffer_insert_text   (GtkTextBuffer     *textbuffer,
+                                                        GtkTextIter       *iter,
+                                                        gchar             *str,
+                                                        gint               len,
+                                                        gpointer           data);
+static void gtk_text_layout_after_buffer_delete_range  (GtkTextBuffer     *textbuffer,
+                                                        GtkTextIter       *start,
+                                                        GtkTextIter       *end,
+                                                        gpointer           data);
+static void gtk_text_layout_before_mark_set_handler    (GtkTextBuffer     *buffer,
+                                                        const GtkTextIter *location,
+                                                        GtkTextMark       *mark,
+                                                        gpointer           data);
+static void gtk_text_layout_before_buffer_insert_text  (GtkTextBuffer     *textbuffer,
+                                                        GtkTextIter       *iter,
+                                                        gchar             *str,
+                                                        gint               len,
+                                                        gpointer           data);
+static void gtk_text_layout_before_buffer_delete_range (GtkTextBuffer     *textbuffer,
+                                                        GtkTextIter       *start,
+                                                        GtkTextIter       *end,
+                                                        gpointer           data);
+
 
 static void gtk_text_layout_update_cursor_line (GtkTextLayout *layout);
 
@@ -182,9 +200,10 @@ G_DEFINE_TYPE_WITH_PRIVATE (GtkTextLayout, gtk_text_layout, G_TYPE_OBJECT)
 static void
 gtk_text_layout_dispose (GObject *object)
 {
-  GtkTextLayout *layout;
+  GtkTextLayout *layout = GTK_TEXT_LAYOUT (object);
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
 
-  layout = GTK_TEXT_LAYOUT (object);
+  g_clear_pointer (&priv->cache, gtk_text_line_display_cache_free);
 
   gtk_text_layout_set_buffer (layout, NULL);
 
@@ -197,8 +216,6 @@ gtk_text_layout_dispose (GObject *object)
   g_clear_object (&layout->ltr_context);
   g_clear_object (&layout->rtl_context);
 
-  g_clear_pointer (&layout->one_display_cache, gtk_text_line_display_unref);
-
   if (layout->preedit_attrs != NULL)
     {
       pango_attr_list_unref (layout->preedit_attrs);
@@ -275,7 +292,10 @@ gtk_text_layout_class_init (GtkTextLayoutClass *klass)
 static void
 gtk_text_layout_init (GtkTextLayout *text_layout)
 {
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (text_layout);
+
   text_layout->cursor_visible = TRUE;
+  priv->cache = gtk_text_line_display_cache_new ();
 }
 
 GtkTextLayout*
@@ -315,14 +335,24 @@ gtk_text_layout_set_buffer (GtkTextLayout *layout,
       _gtk_text_btree_remove_view (_gtk_text_buffer_get_btree (layout->buffer),
                                   layout);
 
-      g_signal_handlers_disconnect_by_func (layout->buffer, 
-                                            G_CALLBACK (gtk_text_layout_mark_set_handler), 
+      g_signal_handlers_disconnect_by_func (layout->buffer,
+                                            G_CALLBACK (gtk_text_layout_after_mark_set_handler),
+                                            layout);
+      g_signal_handlers_disconnect_by_func (layout->buffer,
+                                            G_CALLBACK (gtk_text_layout_after_buffer_insert_text),
+                                            layout);
+      g_signal_handlers_disconnect_by_func (layout->buffer,
+                                            G_CALLBACK (gtk_text_layout_after_buffer_delete_range),
+                                            layout);
+
+      g_signal_handlers_disconnect_by_func (layout->buffer,
+                                            G_CALLBACK (gtk_text_layout_before_mark_set_handler),
                                             layout);
-      g_signal_handlers_disconnect_by_func (layout->buffer, 
-                                            G_CALLBACK (gtk_text_layout_buffer_insert_text), 
+      g_signal_handlers_disconnect_by_func (layout->buffer,
+                                            G_CALLBACK (gtk_text_layout_before_buffer_insert_text),
                                             layout);
-      g_signal_handlers_disconnect_by_func (layout->buffer, 
-                                            G_CALLBACK (gtk_text_layout_buffer_delete_range), 
+      g_signal_handlers_disconnect_by_func (layout->buffer,
+                                            G_CALLBACK (gtk_text_layout_before_buffer_delete_range),
                                             layout);
 
       g_object_unref (layout->buffer);
@@ -339,11 +369,18 @@ gtk_text_layout_set_buffer (GtkTextLayout *layout,
 
       /* Bind to all signals that move the insert mark. */
       g_signal_connect_after (layout->buffer, "mark-set",
-                              G_CALLBACK (gtk_text_layout_mark_set_handler), layout);
+                              G_CALLBACK (gtk_text_layout_after_mark_set_handler), layout);
       g_signal_connect_after (layout->buffer, "insert-text",
-                              G_CALLBACK (gtk_text_layout_buffer_insert_text), layout);
+                              G_CALLBACK (gtk_text_layout_after_buffer_insert_text), layout);
       g_signal_connect_after (layout->buffer, "delete-range",
-                              G_CALLBACK (gtk_text_layout_buffer_delete_range), layout);
+                              G_CALLBACK (gtk_text_layout_after_buffer_delete_range), layout);
+
+      g_signal_connect (layout->buffer, "mark-set",
+                        G_CALLBACK (gtk_text_layout_before_mark_set_handler), layout);
+      g_signal_connect (layout->buffer, "insert-text",
+                        G_CALLBACK (gtk_text_layout_before_buffer_insert_text), layout);
+      g_signal_connect (layout->buffer, "delete-range",
+                        G_CALLBACK (gtk_text_layout_before_buffer_delete_range), layout);
 
       gtk_text_layout_update_cursor_line (layout);
     }
@@ -627,46 +664,26 @@ gtk_text_layout_emit_changed (GtkTextLayout *layout,
   g_signal_emit (layout, signals[CHANGED], 0, y, old_height, new_height);
 }
 
-static void
-text_layout_changed (GtkTextLayout *layout,
-                     gint           y,
-                     gint           old_height,
-                     gint           new_height,
-                     gboolean       cursors_only)
-{
-  /* Check if the range intersects our cached line display,
-   * and invalidate the cached line if so.
-   */
-  if (layout->one_display_cache)
-    {
-      GtkTextLine *line = layout->one_display_cache->line;
-      gint cache_y = _gtk_text_btree_find_line_top (_gtk_text_buffer_get_btree (layout->buffer),
-                                                   line, layout);
-      gint cache_height = layout->one_display_cache->height;
-
-      if (cache_y + cache_height > y && cache_y < y + old_height)
-       gtk_text_layout_invalidate_cache (layout, line, cursors_only);
-    }
-
-  gtk_text_layout_emit_changed (layout, y, old_height, new_height);
-}
-
 void
 gtk_text_layout_changed (GtkTextLayout *layout,
                          gint           y,
                          gint           old_height,
                          gint           new_height)
 {
-  text_layout_changed (layout, y, old_height, new_height, FALSE);
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+  gtk_text_line_display_cache_invalidate_y_range (priv->cache, layout, y, old_height, FALSE);
+  gtk_text_layout_emit_changed (layout, y, old_height, new_height);
 }
 
 void
 gtk_text_layout_cursors_changed (GtkTextLayout *layout,
                                  gint           y,
-                                gint           old_height,
-                                gint           new_height)
+                                 gint           old_height,
+                                 gint           new_height)
 {
-  text_layout_changed (layout, y, old_height, new_height, TRUE);
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+  gtk_text_line_display_cache_invalidate_y_range (priv->cache, layout, y, old_height, TRUE);
+  gtk_text_layout_emit_changed (layout, y, old_height, new_height);
 }
 
 void
@@ -819,20 +836,16 @@ gtk_text_layout_invalidate_cache (GtkTextLayout *layout,
                                   GtkTextLine   *line,
                                  gboolean       cursors_only)
 {
-  if (layout->one_display_cache && line == layout->one_display_cache->line)
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+
+  g_assert (GTK_IS_TEXT_LAYOUT (layout));
+
+  if (priv->cache != NULL)
     {
       if (cursors_only)
-       {
-          GtkTextLineDisplay *display = layout->one_display_cache;
-
-          g_clear_pointer (&display->cursors, g_array_unref);
-         display->cursors_invalid = TRUE;
-         display->has_block_cursor = FALSE;
-       }
+        gtk_text_line_display_cache_invalidate_cursors (priv->cache, line);
       else
-       {
-          g_clear_pointer (&layout->one_display_cache, gtk_text_line_display_unref);
-       }
+        gtk_text_line_display_cache_invalidate_line (priv->cache, line);
     }
 }
 
@@ -840,7 +853,7 @@ gtk_text_layout_invalidate_cache (GtkTextLayout *layout,
  */
 static void
 gtk_text_layout_invalidate_cursor_line (GtkTextLayout *layout,
-                                       gboolean cursors_only)
+                                       gboolean       cursors_only)
 {
   GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
   GtkTextLineData *line_data;
@@ -849,22 +862,20 @@ gtk_text_layout_invalidate_cursor_line (GtkTextLayout *layout,
     return;
 
   line_data = _gtk_text_line_get_data (priv->cursor_line, layout);
-  if (line_data)
+
+  if (line_data != NULL)
     {
-      if (cursors_only)
-         gtk_text_layout_invalidate_cache (layout, priv->cursor_line, TRUE);
-      else
-       {
-         gtk_text_layout_invalidate_cache (layout, priv->cursor_line, FALSE);
-         _gtk_text_line_invalidate_wrap (priv->cursor_line, line_data);
-       }
+      gtk_text_layout_invalidate_cache (layout, priv->cursor_line, cursors_only);
+
+      if (!cursors_only)
+        _gtk_text_line_invalidate_wrap (priv->cursor_line, line_data);
 
       gtk_text_layout_invalidated (layout);
     }
 }
 
 static void
-gtk_text_layout_update_cursor_line(GtkTextLayout *layout)
+gtk_text_layout_update_cursor_line (GtkTextLayout *layout)
 {
   GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
   GtkTextIter iter;
@@ -873,6 +884,8 @@ gtk_text_layout_update_cursor_line(GtkTextLayout *layout)
                                     gtk_text_buffer_get_insert (layout->buffer));
 
   priv->cursor_line = _gtk_text_iter_get_text_line (&iter);
+
+  gtk_text_line_display_cache_set_cursor_line (priv->cache, priv->cursor_line);
 }
 
 static void
@@ -924,34 +937,8 @@ gtk_text_layout_real_invalidate_cursors (GtkTextLayout     *layout,
                                         const GtkTextIter *start,
                                         const GtkTextIter *end)
 {
-  /* Check if the range intersects our cached line display,
-   * and invalidate the cached line if so.
-   */
-  if (layout->one_display_cache)
-    {
-      GtkTextIter line_start, line_end;
-      GtkTextLine *line = layout->one_display_cache->line;
-
-      gtk_text_layout_get_iter_at_line (layout, &line_start, line, 0);
-
-      line_end = line_start;
-      if (!gtk_text_iter_ends_line (&line_end))
-       gtk_text_iter_forward_to_line_end (&line_end);
-
-      if (gtk_text_iter_compare (start, end) > 0)
-       {
-         const GtkTextIter *tmp = start;
-         start = end;
-         end = tmp;
-       }
-
-      if (gtk_text_iter_compare (&line_start, end) <= 0 &&
-         gtk_text_iter_compare (start, &line_end) <= 0)
-       {
-         gtk_text_layout_invalidate_cache (layout, line, TRUE);
-       }
-    }
-
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+  gtk_text_line_display_cache_invalidate_range (priv->cache, layout, start, end, TRUE);
   gtk_text_layout_invalidated (layout);
 }
 
@@ -2122,10 +2109,10 @@ add_preedit_attrs (GtkTextLayout     *layout,
 
 /* Iterate over the line and fill in display->cursors.
  * It’s a stripped copy of gtk_text_layout_get_line_display() */
-static void
-update_text_display_cursors (GtkTextLayout      *layout,
-                            GtkTextLine        *line,
-                            GtkTextLineDisplay *display)
+void
+gtk_text_layout_update_display_cursors (GtkTextLayout      *layout,
+                                        GtkTextLine        *line,
+                                        GtkTextLineDisplay *display)
 {
   GtkTextLineSegment *seg;
   GtkTextIter iter;
@@ -2275,9 +2262,9 @@ tags_array_toggle_tag (GPtrArray  *array,
 }
 
 GtkTextLineDisplay *
-gtk_text_layout_get_line_display (GtkTextLayout *layout,
-                                  GtkTextLine   *line,
-                                  gboolean       size_only)
+gtk_text_layout_create_display (GtkTextLayout *layout,
+                                GtkTextLine   *line,
+                                gboolean       size_only)
 {
   GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
   GtkTextLineDisplay *display;
@@ -2305,26 +2292,10 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
 
   g_return_val_if_fail (line != NULL, NULL);
 
-  if (layout->one_display_cache)
-    {
-      if (line == layout->one_display_cache->line &&
-          (size_only || !layout->one_display_cache->size_only))
-       {
-         if (!size_only)
-            update_text_display_cursors (layout, line, layout->one_display_cache);
-         return gtk_text_line_display_ref (layout->one_display_cache);
-       }
-      else
-        {
-          g_clear_pointer (&layout->one_display_cache, gtk_text_line_display_unref);
-        }
-    }
-
-  DV (g_print ("creating one line display cache (%s)\n", G_STRLOC));
-
   display = g_rc_box_new0 (GtkTextLineDisplay);
 
-  display->size_only = size_only;
+  display->mru_link.data = display;
+  display->size_only = !!size_only;
   display->line = line;
   display->insert_index = -1;
 
@@ -2335,7 +2306,7 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
   if (totally_invisible_line (layout, line, &iter))
     {
       display->layout = pango_layout_new (layout->ltr_context);
-      return display;
+      return g_steal_pointer (&display);
     }
 
   /* Find the bidi base direction */
@@ -2347,7 +2318,7 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
       line->dir_strong == PANGO_DIRECTION_NEUTRAL)
     {
       base_dir = (layout->keyboard_direction == GTK_TEXT_DIR_LTR) ?
-        PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL;
+         PANGO_DIRECTION_LTR : PANGO_DIRECTION_RTL;
     }
 
   /* Allocate space for flat text for buffer
@@ -2371,7 +2342,7 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
           seg->type == &gtk_text_child_type)
         {
           style = get_style (layout, tags);
-         initial_toggle_segments = FALSE;
+          initial_toggle_segments = FALSE;
 
           /* We have to delay setting the paragraph values until we
            * hit the first texture or text segment because toggles at
@@ -2401,9 +2372,9 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
                    */
 
                   gint bytes = 0;
-                 GtkTextLineSegment *prev_seg = NULL;
+                  GtkTextLineSegment *prev_seg = NULL;
   
-                 while (seg)
+                  while (seg)
                     {
                       if (seg->type == &gtk_text_char_type)
                         {
@@ -2412,35 +2383,35 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
                           buffer_byte_offset += seg->byte_count;
                           bytes += seg->byte_count;
                         }
-                     else if (seg->type == &gtk_text_right_mark_type ||
-                              seg->type == &gtk_text_left_mark_type)
+                      else if (seg->type == &gtk_text_right_mark_type ||
+                               seg->type == &gtk_text_left_mark_type)
                         {
-                         /* If we have preedit string, break out of this loop - we'll almost
-                          * certainly have different attributes on the preedit string
-                          */
-
-                         if (layout->preedit_len > 0 &&
-                             _gtk_text_btree_mark_is_insert (_gtk_text_buffer_get_btree (layout->buffer),
-                                                            seg->body.mark.obj))
-                           break;
-
-                         if (seg->body.mark.visible)
-                           {
-                             cursor_byte_offsets = g_slist_prepend (cursor_byte_offsets, GINT_TO_POINTER 
(layout_byte_offset));
-                             cursor_segs = g_slist_prepend (cursor_segs, seg);
-                             if (_gtk_text_btree_mark_is_insert (_gtk_text_buffer_get_btree (layout->buffer),
-                                                                 seg->body.mark.obj))
-                               display->insert_index = layout_byte_offset;
-                           }
+                          /* If we have preedit string, break out of this loop - we'll almost
+                           * certainly have different attributes on the preedit string
+                           */
+
+                          if (layout->preedit_len > 0 &&
+                              _gtk_text_btree_mark_is_insert (_gtk_text_buffer_get_btree (layout->buffer),
+                                                             seg->body.mark.obj))
+                            break;
+
+                          if (seg->body.mark.visible)
+                            {
+                              cursor_byte_offsets = g_slist_prepend (cursor_byte_offsets, GINT_TO_POINTER 
(layout_byte_offset));
+                              cursor_segs = g_slist_prepend (cursor_segs, seg);
+                              if (_gtk_text_btree_mark_is_insert (_gtk_text_buffer_get_btree 
(layout->buffer),
+                                                                  seg->body.mark.obj))
+                                display->insert_index = layout_byte_offset;
+                            }
                         }
-                     else
-                       break;
+                      else
+                        break;
 
-                     prev_seg = seg;
+                      prev_seg = seg;
                       seg = seg->next;
                     }
 
-                 seg = prev_seg; /* Back up one */
+                  seg = prev_seg; /* Back up one */
                   add_generic_attrs (layout, &style->appearance,
                                      bytes,
                                      attrs, layout_byte_offset - bytes,
@@ -2503,42 +2474,42 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
           /* Style may have changed, drop our
              current cached style */
           invalidate_cached_style (layout);
-         /* Add the tag only after we have seen some non-toggle non-mark segment,
-          * otherwise the tag is already accounted for by _gtk_text_btree_get_tags(). */
-         if (!initial_toggle_segments)
-           tags = tags_array_toggle_tag (tags, seg->body.toggle.info->tag);
+          /* Add the tag only after we have seen some non-toggle non-mark segment,
+           * otherwise the tag is already accounted for by _gtk_text_btree_get_tags(). */
+          if (!initial_toggle_segments)
+            tags = tags_array_toggle_tag (tags, seg->body.toggle.info->tag);
         }
 
       /* Marks */
       else if (seg->type == &gtk_text_right_mark_type ||
                seg->type == &gtk_text_left_mark_type)
         {
-         gint cursor_offset = 0;
-         
-         /* At the insertion point, add the preedit string, if any */
-         
-         if (_gtk_text_btree_mark_is_insert (_gtk_text_buffer_get_btree (layout->buffer),
-                                            seg->body.mark.obj))
-           {
-             display->insert_index = layout_byte_offset;
-             
-             if (layout->preedit_len > 0)
-               {
-                 text_allocated += layout->preedit_len;
-                 text = g_realloc (text, text_allocated);
+          gint cursor_offset = 0;
+          
+          /* At the insertion point, add the preedit string, if any */
+          
+          if (_gtk_text_btree_mark_is_insert (_gtk_text_buffer_get_btree (layout->buffer),
+                                             seg->body.mark.obj))
+            {
+              display->insert_index = layout_byte_offset;
+              
+              if (layout->preedit_len > 0)
+                {
+                  text_allocated += layout->preedit_len;
+                  text = g_realloc (text, text_allocated);
 
-                 style = get_style (layout, tags);
-                 add_preedit_attrs (layout, style, attrs, layout_byte_offset, size_only);
-                 release_style (layout, style);
+                  style = get_style (layout, tags);
+                  add_preedit_attrs (layout, style, attrs, layout_byte_offset, size_only);
+                  release_style (layout, style);
                   
-                 memcpy (text + layout_byte_offset, layout->preedit_string, layout->preedit_len);
-                 layout_byte_offset += layout->preedit_len;
+                  memcpy (text + layout_byte_offset, layout->preedit_string, layout->preedit_len);
+                  layout_byte_offset += layout->preedit_len;
                   /* DO NOT increment the buffer byte offset for preedit */
                   
-                 cursor_offset = layout->preedit_cursor - layout->preedit_len;
-               }
-           }
-         
+                  cursor_offset = layout->preedit_cursor - layout->preedit_len;
+                }
+            }
+          
 
           /* Display visible marks */
 
@@ -2623,17 +2594,17 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
       gint excess = display->total_width - text_pixel_width;
 
       switch (pango_layout_get_alignment (display->layout))
-       {
-       case PANGO_ALIGN_LEFT:
+        {
+        case PANGO_ALIGN_LEFT:
         default:
-         break;
-       case PANGO_ALIGN_CENTER:
-         display->x_offset += excess / 2;
-         break;
-       case PANGO_ALIGN_RIGHT:
-         display->x_offset += excess;
-         break;
-       }
+          break;
+        case PANGO_ALIGN_CENTER:
+          display->x_offset += excess / 2;
+          break;
+        case PANGO_ALIGN_RIGHT:
+          display->x_offset += excess;
+          break;
+        }
     }
   
   /* Free this if we aren't in a loop */
@@ -2645,19 +2616,31 @@ gtk_text_layout_get_line_display (GtkTextLayout *layout,
   if (tags != NULL)
     g_ptr_array_free (tags, TRUE);
 
-  g_assert (layout->one_display_cache == NULL);
-
-  layout->one_display_cache = gtk_text_line_display_ref (display);
-
   if (saw_widget)
     allocate_child_widgets (layout, display);
   
-  return display;
+  return g_steal_pointer (&display);
+}
+
+GtkTextLineDisplay *
+gtk_text_layout_get_line_display (GtkTextLayout *layout,
+                                  GtkTextLine   *line,
+                                  gboolean       size_only)
+{
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+
+  return gtk_text_line_display_cache_get (priv->cache, layout, line, size_only);
 }
 
 static void
 gtk_text_line_display_finalize (GtkTextLineDisplay *display)
 {
+  g_assert (display != NULL);
+  g_assert (display->cache_iter == NULL);
+  g_assert (display->mru_link.prev == NULL);
+  g_assert (display->mru_link.next == NULL);
+  g_assert (display->mru_link.data == display);
+
   g_clear_object (&display->layout);
   g_clear_pointer (&display->cursors, g_array_unref);
 }
@@ -3784,26 +3767,57 @@ gtk_text_layout_spew (GtkTextLayout *layout)
 #endif
 }
 
+static void
+gtk_text_layout_before_mark_set_handler (GtkTextBuffer     *buffer,
+                                         const GtkTextIter *location,
+                                         GtkTextMark       *mark,
+                                         gpointer           data)
+{
+  GtkTextLayout *layout = GTK_TEXT_LAYOUT (data);
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+
+  if (mark == gtk_text_buffer_get_insert (buffer))
+    gtk_text_line_display_cache_set_cursor_line (priv->cache, NULL);
+}
+
 /* Catch all situations that move the insertion point.
  */
 static void
-gtk_text_layout_mark_set_handler (GtkTextBuffer     *buffer,
-                                  const GtkTextIter *location,
-                                  GtkTextMark       *mark,
-                                  gpointer           data)
+gtk_text_layout_after_mark_set_handler (GtkTextBuffer     *buffer,
+                                        const GtkTextIter *location,
+                                        GtkTextMark       *mark,
+                                        gpointer           data)
 {
   GtkTextLayout *layout = GTK_TEXT_LAYOUT (data);
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
 
   if (mark == gtk_text_buffer_get_insert (buffer))
-    gtk_text_layout_update_cursor_line (layout);
+    {
+      gtk_text_layout_update_cursor_line (layout);
+      gtk_text_line_display_cache_set_cursor_line (priv->cache, priv->cursor_line);
+    }
 }
 
 static void
-gtk_text_layout_buffer_insert_text (GtkTextBuffer *textbuffer,
-                                   GtkTextIter   *iter,
-                                   gchar         *str,
-                                   gint           len,
-                                   gpointer       data)
+gtk_text_layout_before_buffer_insert_text (GtkTextBuffer *textbuffer,
+                                           GtkTextIter   *iter,
+                                           gchar         *str,
+                                           gint           len,
+                                           gpointer       data)
+{
+  GtkTextLayout *layout = GTK_TEXT_LAYOUT (data);
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+  GtkTextLine *line = _gtk_text_iter_get_text_line (iter);
+
+  gtk_text_line_display_cache_invalidate_line (priv->cache, line);
+}
+
+static void
+gtk_text_layout_after_buffer_insert_text (GtkTextBuffer *textbuffer,
+                                          GtkTextIter   *iter,
+                                          gchar         *str,
+                                          gint           len,
+                                          gpointer       data)
 {
   GtkTextLayout *layout = GTK_TEXT_LAYOUT (data);
 
@@ -3811,10 +3825,22 @@ gtk_text_layout_buffer_insert_text (GtkTextBuffer *textbuffer,
 }
 
 static void
-gtk_text_layout_buffer_delete_range (GtkTextBuffer *textbuffer,
-                                    GtkTextIter   *start,
-                                    GtkTextIter   *end,
-                                    gpointer       data)
+gtk_text_layout_before_buffer_delete_range (GtkTextBuffer *textbuffer,
+                                            GtkTextIter   *start,
+                                            GtkTextIter   *end,
+                                            gpointer       data)
+{
+  GtkTextLayout *layout = GTK_TEXT_LAYOUT (data);
+  GtkTextLayoutPrivate *priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+
+  gtk_text_line_display_cache_invalidate_range (priv->cache, layout, start, end, FALSE);
+}
+
+static void
+gtk_text_layout_after_buffer_delete_range (GtkTextBuffer *textbuffer,
+                                           GtkTextIter   *start,
+                                           GtkTextIter   *end,
+                                           gpointer       data)
 {
   GtkTextLayout *layout = GTK_TEXT_LAYOUT (data);
 
@@ -4041,6 +4067,7 @@ gtk_text_layout_snapshot (GtkTextLayout      *layout,
                           const GdkRectangle *clip,
                           float               cursor_alpha)
 {
+  GtkTextLayoutPrivate *priv;
   GskPangoRenderer *crenderer;
   GtkStyleContext *context;
   gint offset_y;
@@ -4055,6 +4082,8 @@ gtk_text_layout_snapshot (GtkTextLayout      *layout,
   g_return_if_fail (layout->buffer != NULL);
   g_return_if_fail (snapshot != NULL);
 
+  priv = GTK_TEXT_LAYOUT_GET_PRIVATE (layout);
+
   context = gtk_widget_get_style_context (widget);
   gtk_style_context_get_color (context, &color);
 
@@ -4155,7 +4184,24 @@ gtk_text_layout_snapshot (GtkTextLayout      *layout,
 
   gtk_text_layout_wrap_loop_end (layout);
 
+  /* Only update eviction source once per snapshot */
+  gtk_text_line_display_cache_delay_eviction (priv->cache);
+
   g_slist_free (line_list);
 
   gsk_pango_renderer_release (crenderer);
 }
+
+gint
+gtk_text_line_display_compare (const GtkTextLineDisplay *display1,
+                               const GtkTextLineDisplay *display2,
+                               GtkTextLayout            *layout)
+{
+  GtkTextIter iter1;
+  GtkTextIter iter2;
+
+  gtk_text_layout_get_iter_at_line (layout, &iter1, display1->line, 0);
+  gtk_text_layout_get_iter_at_line (layout, &iter2, display2->line, 0);
+
+  return gtk_text_iter_compare (&iter1, &iter2);
+}
diff --git a/gtk/gtktextlayoutprivate.h b/gtk/gtktextlayoutprivate.h
index 7905d2b542..27da15d2bd 100644
--- a/gtk/gtktextlayoutprivate.h
+++ b/gtk/gtktextlayoutprivate.h
@@ -138,11 +138,6 @@ struct _GtkTextLayout
    * over long runs with the same style. */
   GtkTextAttributes *one_style_cache;
 
-  /* A cache of one line display. Getting the same line
-   * many times in a row is the most common case.
-   */
-  GtkTextLineDisplay *one_display_cache;
-
   /* Whether we are allowed to wrap right now */
   gint wrap_loop_count;
   
@@ -223,6 +218,12 @@ struct _GtkTextLineDisplay
   PangoLayout *layout;
   GArray *cursors;      /* indexes of cursors in the PangoLayout */
 
+  /* GSequenceIter backpointer for use within cache */
+  GSequenceIter *cache_iter;
+
+  /* GQueue link for use in MRU to help cull cache */
+  GList          mru_link;
+
   GtkTextDirection direction;
 
   gint width;                   /* Width of layout */
@@ -298,8 +299,12 @@ void gtk_text_layout_wrap_loop_end   (GtkTextLayout *layout);
 GtkTextLineDisplay* gtk_text_layout_get_line_display  (GtkTextLayout      *layout,
                                                        GtkTextLine        *line,
                                                        gboolean            size_only);
-GtkTextLineDisplay *gtk_text_line_display_ref         (GtkTextLineDisplay *display);
-void                gtk_text_line_display_unref       (GtkTextLineDisplay *display);
+
+GtkTextLineDisplay *gtk_text_line_display_ref         (GtkTextLineDisplay       *display);
+void                gtk_text_line_display_unref       (GtkTextLineDisplay       *display);
+gint                gtk_text_line_display_compare     (const GtkTextLineDisplay *display1,
+                                                       const GtkTextLineDisplay *display2,
+                                                       GtkTextLayout            *layout);
 
 void gtk_text_layout_get_line_at_y     (GtkTextLayout     *layout,
                                         GtkTextIter       *target_iter,
@@ -354,6 +359,12 @@ void     gtk_text_layout_get_cursor_locations (GtkTextLayout     *layout,
                                                GtkTextIter       *iter,
                                                GdkRectangle      *strong_pos,
                                                GdkRectangle      *weak_pos);
+GtkTextLineDisplay *gtk_text_layout_create_display (GtkTextLayout *layout,
+                                                    GtkTextLine   *line,
+                                                    gboolean       size_only);
+void     gtk_text_layout_update_display_cursors (GtkTextLayout      *layout,
+                                                 GtkTextLine        *line,
+                                                 GtkTextLineDisplay *display);
 gboolean _gtk_text_layout_get_block_cursor    (GtkTextLayout     *layout,
                                               GdkRectangle      *pos);
 gboolean gtk_text_layout_clamp_iter_to_vrange (GtkTextLayout     *layout,
diff --git a/gtk/gtktextlinedisplaycache.c b/gtk/gtktextlinedisplaycache.c
new file mode 100644
index 0000000000..6129d787fc
--- /dev/null
+++ b/gtk/gtktextlinedisplaycache.c
@@ -0,0 +1,716 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#include "gtktextbtree.h"
+#include "gtktextbufferprivate.h"
+#include "gtktextiterprivate.h"
+#include "gtktextlinedisplaycacheprivate.h"
+
+#define MRU_MAX_SIZE             250
+#define BLOW_CACHE_TIMEOUT_SEC   20
+#define DEBUG_LINE_DISPLAY_CACHE 0
+
+struct _GtkTextLineDisplayCache
+{
+  GSequence   *sorted_by_line;
+  GHashTable  *line_to_display;
+  GtkTextLine *cursor_line;
+  GQueue       mru;
+  GSource     *evict_source;
+
+#if DEBUG_LINE_DISPLAY_CACHE
+  guint       log_source;
+  gint        hits;
+  gint        misses;
+  gint        inval;
+  gint        inval_cursors;
+  gint        inval_by_line;
+  gint        inval_by_range;
+  gint        inval_by_y_range;
+#endif
+};
+
+#if DEBUG_LINE_DISPLAY_CACHE
+# define STAT_ADD(val,n) ((val) += n)
+# define STAT_INC(val)   STAT_ADD(val,1)
+static gboolean
+dump_stats (gpointer data)
+{
+  GtkTextLineDisplayCache *cache = data;
+  g_printerr ("%p: size=%u hits=%d misses=%d inval_total=%d "
+              "inval_cursors=%d inval_by_line=%d "
+              "inval_by_range=%d inval_by_y_range=%d\n",
+              cache, g_hash_table_size (cache->line_to_display),
+              cache->hits, cache->misses,
+              cache->inval, cache->inval_cursors,
+              cache->inval_by_line, cache->inval_by_range,
+              cache->inval_by_y_range);
+  return G_SOURCE_CONTINUE;
+}
+#else
+# define STAT_ADD(val,n)
+# define STAT_INC(val)
+#endif
+
+GtkTextLineDisplayCache *
+gtk_text_line_display_cache_new (void)
+{
+  GtkTextLineDisplayCache *ret;
+
+  ret = g_slice_new0 (GtkTextLineDisplayCache);
+  ret->sorted_by_line = g_sequence_new ((GDestroyNotify)gtk_text_line_display_unref);
+  ret->line_to_display = g_hash_table_new (NULL, NULL);
+
+#if DEBUG_LINE_DISPLAY_CACHE
+  ret->log_source = g_timeout_add_seconds (1, dump_stats, ret);
+#endif
+
+  return g_steal_pointer (&ret);
+}
+
+void
+gtk_text_line_display_cache_free (GtkTextLineDisplayCache *cache)
+{
+#if DEBUG_LINE_DISPLAY_CACHE
+  g_clear_handle_id (&cache->log_source, g_source_remove);
+#endif
+
+  g_clear_pointer (&cache->evict_source, g_source_destroy);
+  g_clear_pointer (&cache->sorted_by_line, g_sequence_free);
+  g_clear_pointer (&cache->line_to_display, g_hash_table_unref);
+  g_slice_free (GtkTextLineDisplayCache, cache);
+}
+
+static gboolean
+gtk_text_line_display_cache_blow_cb (gpointer data)
+{
+  GtkTextLineDisplayCache *cache = data;
+
+  g_assert (cache != NULL);
+
+#if DEBUG_LINE_DISPLAY_CACHE
+  g_printerr ("Evicting GtkTextLineDisplayCache\n");
+#endif
+
+  cache->evict_source = NULL;
+
+  gtk_text_line_display_cache_invalidate (cache);
+
+  return G_SOURCE_REMOVE;
+}
+
+void
+gtk_text_line_display_cache_delay_eviction (GtkTextLineDisplayCache *cache)
+{
+  g_assert (cache != NULL);
+
+  if (cache->evict_source != NULL)
+    {
+      gint64 deadline;
+
+      deadline = g_get_monotonic_time () + (BLOW_CACHE_TIMEOUT_SEC * G_USEC_PER_SEC);
+      g_source_set_ready_time (cache->evict_source, deadline);
+    }
+  else
+    {
+      guint tag;
+
+      tag = g_timeout_add_seconds (BLOW_CACHE_TIMEOUT_SEC,
+                                   gtk_text_line_display_cache_blow_cb,
+                                   cache);
+      cache->evict_source = g_main_context_find_source_by_id (NULL, tag);
+      g_source_set_name (cache->evict_source, "[gtk+] gtk_text_line_display_cache_blow_cb");
+    }
+}
+
+#if DEBUG_LINE_DISPLAY_CACHE
+static void
+check_disposition (GtkTextLineDisplayCache *cache,
+                   GtkTextLayout           *layout)
+{
+  GSequenceIter *iter;
+  gint last = G_MAXUINT;
+
+  g_assert (cache != NULL);
+  g_assert (cache->sorted_by_line != NULL);
+  g_assert (cache->line_to_display != NULL);
+
+  for (iter = g_sequence_get_begin_iter (cache->sorted_by_line);
+       !g_sequence_iter_is_end (iter);
+       iter = g_sequence_iter_next (iter))
+    {
+      GtkTextLineDisplay *display = g_sequence_get (iter);
+      GtkTextIter text_iter;
+      guint line;
+
+      gtk_text_layout_get_iter_at_line (layout, &text_iter, display->line, 0);
+      line = gtk_text_iter_get_line (&text_iter);
+
+      g_assert_cmpint (line, >, last);
+
+      last = line;
+    }
+}
+#endif
+
+static void
+gtk_text_line_display_cache_take_display (GtkTextLineDisplayCache *cache,
+                                          GtkTextLineDisplay      *display,
+                                          GtkTextLayout           *layout)
+{
+  g_assert (cache != NULL);
+  g_assert (display != NULL);
+  g_assert (display->line != NULL);
+  g_assert (display->cache_iter == NULL);
+  g_assert (display->mru_link.data == display);
+  g_assert (display->mru_link.prev == NULL);
+  g_assert (display->mru_link.next == NULL);
+  g_assert (g_hash_table_lookup (cache->line_to_display, display->line) == NULL);
+
+#if DEBUG_LINE_DISPLAY_CACHE
+  check_disposition (cache, layout);
+#endif
+
+  display->cache_iter =
+    g_sequence_insert_sorted (cache->sorted_by_line,
+                              display,
+                              (GCompareDataFunc) gtk_text_line_display_compare,
+                              layout);
+  g_hash_table_insert (cache->line_to_display, display->line, display);
+  g_queue_push_head_link (&cache->mru, &display->mru_link);
+
+  /* Cull the cache if we're at capacity */
+  while (cache->mru.length > MRU_MAX_SIZE)
+    {
+      display = g_queue_peek_tail (&cache->mru);
+
+      gtk_text_line_display_cache_invalidate_display (cache, display, FALSE);
+    }
+}
+
+/*
+ * gtk_text_line_display_cache_invalidate_display:
+ * @cache: a GtkTextLineDisplayCache
+ * @display: a GtkTextLineDisplay
+ * @cursors_only: if only the cursor positions should be invalidated
+ *
+ * If @cursors_only is TRUE, then only the cursors are invalidated. Otherwise,
+ * @display is removed from the cache.
+ *
+ * Use this function when you already have access to a display as it reduces
+ * some overhead.
+ */
+void
+gtk_text_line_display_cache_invalidate_display (GtkTextLineDisplayCache *cache,
+                                                GtkTextLineDisplay      *display,
+                                                gboolean                 cursors_only)
+{
+  g_assert (cache != NULL);
+  g_assert (display != NULL);
+  g_assert (display->line != NULL);
+
+  if (cursors_only)
+    {
+      g_clear_pointer (&display->cursors, g_array_unref);
+      display->cursors_invalid = TRUE;
+      display->has_block_cursor = FALSE;
+    }
+  else
+    {
+      GSequenceIter *iter = g_steal_pointer (&display->cache_iter);
+
+      if (cache->cursor_line == display->line)
+        cache->cursor_line = NULL;
+
+      g_hash_table_remove (cache->line_to_display, display->line);
+      g_queue_unlink (&cache->mru, &display->mru_link);
+
+      if (iter != NULL)
+        g_sequence_remove (iter);
+    }
+
+  STAT_INC (cache->inval);
+}
+
+/*
+ * gtk_text_line_display_cache_get:
+ * @cache: a #GtkTextLineDisplayCache
+ * @layout: a GtkTextLayout
+ * @line: a GtkTextLine
+ * @size_only: if only line sizing is needed
+ *
+ * Gets a GtkTextLineDisplay for @line.
+ *
+ * If no cached display exists, a new display will be created.
+ *
+ * It's possible that calling this function will cause some existing
+ * cached displays to be released and destroyed.
+ *
+ * Returns: (transfer full) (not nullable): a #GtkTextLineDisplay
+ */
+GtkTextLineDisplay *
+gtk_text_line_display_cache_get (GtkTextLineDisplayCache *cache,
+                                 GtkTextLayout           *layout,
+                                 GtkTextLine             *line,
+                                 gboolean                 size_only)
+{
+  GtkTextLineDisplay *display;
+
+  g_assert (cache != NULL);
+  g_assert (layout != NULL);
+  g_assert (line != NULL);
+
+  display = g_hash_table_lookup (cache->line_to_display, line);
+
+  if (display != NULL)
+    {
+      if (size_only || !display->size_only)
+        {
+          STAT_INC (cache->hits);
+
+          if (!size_only && display->line == cache->cursor_line)
+            gtk_text_layout_update_display_cursors (layout, display->line, display);
+
+          /* Move to front of MRU */
+          g_queue_unlink (&cache->mru, &display->mru_link);
+          g_queue_push_head_link (&cache->mru, &display->mru_link);
+
+          return gtk_text_line_display_ref (display);
+        }
+
+      /* We need an updated display that includes more than just
+       * sizing, so we need to drop this entry and force the layout
+       * to create a new one.
+       */
+      gtk_text_line_display_cache_invalidate_display (cache, display, FALSE);
+    }
+
+  STAT_INC (cache->misses);
+
+  g_assert (!g_hash_table_lookup (cache->line_to_display, line));
+
+  display = gtk_text_layout_create_display (layout, line, size_only);
+
+  g_assert (display != NULL);
+  g_assert (display->line == line);
+
+  if (!size_only)
+    {
+      if (line == cache->cursor_line)
+        gtk_text_layout_update_display_cursors (layout, line, display);
+
+      gtk_text_line_display_cache_take_display (cache,
+                                                gtk_text_line_display_ref (display),
+                                                layout);
+    }
+
+  return g_steal_pointer (&display);
+}
+
+void
+gtk_text_line_display_cache_invalidate (GtkTextLineDisplayCache *cache)
+{
+  g_assert (cache != NULL);
+  g_assert (cache->sorted_by_line != NULL);
+  g_assert (cache->line_to_display != NULL);
+
+  STAT_ADD (cache->inval, g_hash_table_size (cache->line_to_display));
+
+  cache->cursor_line = NULL;
+
+  while (cache->mru.head != NULL)
+    {
+      GtkTextLineDisplay *display = g_queue_peek_head (&cache->mru);
+
+      gtk_text_line_display_cache_invalidate_display (cache, display, FALSE);
+    }
+
+  g_assert (g_hash_table_size (cache->line_to_display) == 0);
+  g_assert (g_sequence_get_length (cache->sorted_by_line) == 0);
+  g_assert (cache->mru.length == 0);
+}
+
+void
+gtk_text_line_display_cache_invalidate_cursors (GtkTextLineDisplayCache *cache,
+                                                GtkTextLine             *line)
+{
+  GtkTextLineDisplay *display;
+
+  g_assert (cache != NULL);
+  g_assert (line != NULL);
+
+  STAT_INC (cache->inval_cursors);
+
+  display = g_hash_table_lookup (cache->line_to_display, line);
+
+  if (display != NULL)
+    gtk_text_line_display_cache_invalidate_display (cache, display, TRUE);
+}
+
+/*
+ * gtk_text_line_display_cache_invalidate_line:
+ * @self: a GtkTextLineDisplayCache
+ * @line: a GtkTextLine
+ *
+ * Removes a cached display for @line.
+ *
+ * Compare to gtk_text_line_display_cache_invalidate_cursors() which
+ * only invalidates the cursors for this row.
+ */
+void
+gtk_text_line_display_cache_invalidate_line (GtkTextLineDisplayCache *cache,
+                                             GtkTextLine             *line)
+{
+  GtkTextLineDisplay *display;
+
+  g_assert (cache != NULL);
+  g_assert (line != NULL);
+
+  display = g_hash_table_lookup (cache->line_to_display, line);
+
+  if (display != NULL)
+    gtk_text_line_display_cache_invalidate_display (cache, display, FALSE);
+
+  STAT_INC (cache->inval_by_line);
+}
+
+static GSequenceIter *
+find_iter_at_text_iter (GtkTextLineDisplayCache *cache,
+                        GtkTextLayout           *layout,
+                        const GtkTextIter       *iter)
+{
+  GSequenceIter *left;
+  GSequenceIter *right;
+  GSequenceIter *mid;
+  GSequenceIter *end;
+  GtkTextLine *target;
+  guint target_lineno;
+
+  g_assert (cache != NULL);
+  g_assert (iter != NULL);
+
+  if (g_sequence_is_empty (cache->sorted_by_line))
+    return NULL;
+
+  /* gtk_text_iter_get_line() will have cached value */
+  target_lineno = gtk_text_iter_get_line (iter);
+  target = _gtk_text_iter_get_text_line (iter);
+
+  /* Get some iters so we can work with pointer compare */
+  end = g_sequence_get_end_iter (cache->sorted_by_line);
+  left = g_sequence_get_begin_iter (cache->sorted_by_line);
+  right = g_sequence_iter_prev (end);
+
+  /* We already checked for empty above */
+  g_assert (!g_sequence_iter_is_end (left));
+  g_assert (!g_sequence_iter_is_end (right));
+
+  for (;;)
+    {
+      GtkTextLineDisplay *display;
+      guint lineno;
+
+      if (left == right)
+        mid = left;
+      else
+        mid = g_sequence_range_get_midpoint (left, right);
+
+      g_assert (mid != NULL);
+      g_assert (!g_sequence_iter_is_end (mid));
+
+      if (mid == end)
+        break;
+
+      display = g_sequence_get (mid);
+
+      g_assert (display != NULL);
+      g_assert (display->line != NULL);
+      g_assert (display->cache_iter != NULL);
+
+      if (target == display->line)
+        return mid;
+
+      if (right == left)
+        break;
+
+      lineno = _gtk_text_line_get_number (display->line);
+
+      if (target_lineno < lineno)
+        right = mid;
+      else if (target_lineno > lineno)
+        left = g_sequence_iter_next (mid);
+      else
+        g_assert_not_reached ();
+    }
+
+  return NULL;
+}
+
+
+/*
+ * gtk_text_line_display_cache_invalidate_range:
+ * @cache: a GtkTextLineDisplayCache
+ * @begin: the starting text iter
+ * @end: the ending text iter
+ *
+ * Removes all GtkTextLineDisplay that fall between or including
+ * @begin and @end.
+ */
+void
+gtk_text_line_display_cache_invalidate_range (GtkTextLineDisplayCache *cache,
+                                              GtkTextLayout           *layout,
+                                              const GtkTextIter       *begin,
+                                              const GtkTextIter       *end,
+                                              gboolean                 cursors_only)
+{
+  GSequenceIter *begin_iter;
+  GSequenceIter *end_iter;
+  GSequenceIter *iter;
+
+  g_assert (cache != NULL);
+  g_assert (layout != NULL);
+  g_assert (begin != NULL);
+  g_assert (end != NULL);
+
+  STAT_INC (cache->inval_by_range);
+
+  /* Short-circuit, is_empty() is O(1) */
+  if (g_sequence_is_empty (cache->sorted_by_line))
+    return;
+
+  /* gtk_text_iter_order() preserving const */
+  if (gtk_text_iter_compare (begin, end) > 0)
+    {
+      const GtkTextIter *tmp = begin;
+      end = begin;
+      begin = tmp;
+    }
+
+  /* Common case, begin/end on same line. Just try to find the line by
+   * line number and invalidate it alone.
+   */
+  if G_LIKELY (_gtk_text_iter_same_line (begin, end))
+    {
+      begin_iter = find_iter_at_text_iter (cache, layout, begin);
+
+      if (begin_iter != NULL)
+        {
+          GtkTextLineDisplay *display = g_sequence_get (begin_iter);
+
+          g_assert (display != NULL);
+          g_assert (display->line != NULL);
+
+          gtk_text_line_display_cache_invalidate_display (cache, display, cursors_only);
+        }
+
+      return;
+    }
+
+  /* Find GSequenceIter containing GtkTextLineDisplay that correspond
+   * to each of the text positions.
+   */
+  begin_iter = find_iter_at_text_iter (cache, layout, begin);
+  end_iter = find_iter_at_text_iter (cache, layout, end);
+
+  /* Short-circuit if we found nothing */
+  if (begin_iter == NULL && end_iter == NULL)
+    return;
+
+  /* If nothing matches the end, we need to walk to the end of our
+   * cached displays. We know there is a non-zero number of items
+   * in the sequence at this point, so we can iter_prev() safely.
+   */
+  if (end_iter == NULL)
+    end_iter = g_sequence_iter_prev (g_sequence_get_end_iter (cache->sorted_by_line));
+
+  /* If nothing matched the begin, we need to walk starting from
+   * the first display we have cached.
+   */
+  if (begin_iter == NULL)
+    begin_iter = g_sequence_get_begin_iter (cache->sorted_by_line);
+
+  iter = begin_iter;
+
+  for (;;)
+    {
+      GtkTextLineDisplay *display = g_sequence_get (iter);
+      GSequenceIter *next = g_sequence_iter_next (iter);
+
+      gtk_text_line_display_cache_invalidate_display (cache, display, cursors_only);
+
+      if (iter == end_iter)
+        break;
+
+      iter = next;
+    }
+}
+
+static GSequenceIter *
+find_iter_at_at_y (GtkTextLineDisplayCache *cache,
+                   GtkTextLayout           *layout,
+                   gint                     y)
+{
+  GtkTextBTree *btree;
+  GSequenceIter *left;
+  GSequenceIter *right;
+  GSequenceIter *mid;
+  GSequenceIter *end;
+
+  g_assert (cache != NULL);
+  g_assert (layout != NULL);
+
+  if (g_sequence_is_empty (cache->sorted_by_line))
+    return NULL;
+
+  btree = _gtk_text_buffer_get_btree (layout->buffer);
+
+  /* Get some iters so we can work with pointer compare */
+  end = g_sequence_get_end_iter (cache->sorted_by_line);
+  left = g_sequence_get_begin_iter (cache->sorted_by_line);
+  right = g_sequence_iter_prev (end);
+
+  /* We already checked for empty above */
+  g_assert (!g_sequence_iter_is_end (left));
+  g_assert (!g_sequence_iter_is_end (right));
+
+  for (;;)
+    {
+      GtkTextLineDisplay *display;
+      gint cache_y;
+      gint cache_height;
+
+      if (left == right)
+        mid = left;
+      else
+        mid = g_sequence_range_get_midpoint (left, right);
+
+      g_assert (mid != NULL);
+      g_assert (!g_sequence_iter_is_end (mid));
+
+      if (mid == end)
+        break;
+
+      display = g_sequence_get (mid);
+
+      g_assert (display != NULL);
+      g_assert (display->line != NULL);
+
+      cache_y = _gtk_text_btree_find_line_top (btree, display->line, layout);
+      cache_height = display->height;
+
+      if (y >= cache_y && y <= (cache_y + cache_height))
+        return mid;
+
+      if (left == right)
+        break;
+
+      if (y < cache_y)
+        right = mid;
+      else if (y > (cache_y + cache_height))
+        left = g_sequence_iter_next (mid);
+      else
+        g_assert_not_reached ();
+    }
+
+  return NULL;
+}
+
+/*
+ * gtk_text_line_display_cache_invalidate_y_range:
+ * @cache: a GtkTextLineDisplayCache
+ * @y: the starting Y position
+ * @old_height: the height to invalidate
+ * @cursors_only: if only cursors should be invalidated
+ *
+ * Remove all GtkTextLineDisplay that fall into the range starting
+ * from the Y position to Y+Height.
+ */
+void
+gtk_text_line_display_cache_invalidate_y_range (GtkTextLineDisplayCache *cache,
+                                                GtkTextLayout           *layout,
+                                                gint                     y,
+                                                gint                     old_height,
+                                                gboolean                 cursors_only)
+{
+  GSequenceIter *iter;
+  GtkTextBTree *btree;
+
+  g_assert (cache != NULL);
+  g_assert (layout != NULL);
+
+  STAT_INC (cache->inval_by_y_range);
+
+  btree = _gtk_text_buffer_get_btree (layout->buffer);
+  iter = find_iter_at_at_y (cache, layout, y);
+
+  if (iter == NULL)
+    return;
+
+  while (!g_sequence_iter_is_end (iter))
+    {
+      GtkTextLineDisplay *display;
+      gint cache_y;
+      gint cache_height;
+
+      display = g_sequence_get (iter);
+      iter = g_sequence_iter_next (iter);
+
+      cache_y = _gtk_text_btree_find_line_top (btree, display->line, layout);
+      cache_height = display->height;
+
+      if (cache_y + cache_height > y && cache_y < y + old_height)
+        {
+          gtk_text_line_display_cache_invalidate_display (cache, display, cursors_only);
+
+          y += cache_height;
+          old_height -= cache_height;
+
+          if (old_height > 0)
+            continue;
+        }
+
+      break;
+    }
+}
+
+void
+gtk_text_line_display_cache_set_cursor_line (GtkTextLineDisplayCache *cache,
+                                             GtkTextLine             *cursor_line)
+{
+  GtkTextLineDisplay *display;
+
+  g_assert (cache != NULL);
+
+  if (cursor_line == cache->cursor_line)
+    return;
+
+  display = g_hash_table_lookup (cache->line_to_display, cache->cursor_line);
+
+  if (display != NULL)
+    gtk_text_line_display_cache_invalidate_display (cache, display, FALSE);
+
+  cache->cursor_line = cursor_line;
+
+  display = g_hash_table_lookup (cache->line_to_display, cache->cursor_line);
+
+  if (display != NULL)
+    gtk_text_line_display_cache_invalidate_display (cache, display, FALSE);
+}
diff --git a/gtk/gtktextlinedisplaycacheprivate.h b/gtk/gtktextlinedisplaycacheprivate.h
new file mode 100644
index 0000000000..5639ec2070
--- /dev/null
+++ b/gtk/gtktextlinedisplaycacheprivate.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef __GTK_TEXT_LINE_DISPLAY_CACHE_PRIVATE_H__
+#define __GTK_TEXT_LINE_DISPLAY_CACHE_PRIVATE_H__
+
+#include "gtktextlayoutprivate.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GtkTextLineDisplayCache GtkTextLineDisplayCache;
+
+GtkTextLineDisplayCache *gtk_text_line_display_cache_new                (void);
+void                     gtk_text_line_display_cache_free               (GtkTextLineDisplayCache *cache);
+GtkTextLineDisplay      *gtk_text_line_display_cache_get                (GtkTextLineDisplayCache *cache,
+                                                                         GtkTextLayout           *layout,
+                                                                         GtkTextLine             *line,
+                                                                         gboolean                 size_only);
+void                     gtk_text_line_display_cache_delay_eviction     (GtkTextLineDisplayCache *cache);
+void                     gtk_text_line_display_cache_set_cursor_line    (GtkTextLineDisplayCache *cache,
+                                                                         GtkTextLine             *line);
+void                     gtk_text_line_display_cache_invalidate         (GtkTextLineDisplayCache *cache);
+void                     gtk_text_line_display_cache_invalidate_cursors (GtkTextLineDisplayCache *cache,
+                                                                         GtkTextLine             *line);
+void                     gtk_text_line_display_cache_invalidate_display (GtkTextLineDisplayCache *cache,
+                                                                         GtkTextLineDisplay      *display,
+                                                                         gboolean                 
cursors_only);
+void                     gtk_text_line_display_cache_invalidate_line    (GtkTextLineDisplayCache *cache,
+                                                                         GtkTextLine             *line);
+void                     gtk_text_line_display_cache_invalidate_range   (GtkTextLineDisplayCache *cache,
+                                                                         GtkTextLayout           *layout,
+                                                                         const GtkTextIter       *begin,
+                                                                         const GtkTextIter       *end,
+                                                                         gboolean                 
cursors_only);
+void                     gtk_text_line_display_cache_invalidate_y_range (GtkTextLineDisplayCache *cache,
+                                                                         GtkTextLayout           *layout,
+                                                                         gint                     y,
+                                                                         gint                     height,
+                                                                         gboolean                 
cursors_only);
+
+G_END_DECLS
+
+#endif /* __GTK_TEXT_LINE_DISPLAY_CACHE_PRIVATE_H__ */
diff --git a/gtk/meson.build b/gtk/meson.build
index 22fe73077f..cdf186af91 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -369,6 +369,7 @@ gtk_public_sources = files([
   'gtktexthandle.c',
   'gtktextiter.c',
   'gtktextlayout.c',
+  'gtktextlinedisplaycache.c',
   'gtktextmark.c',
   'gtktextsegment.c',
   'gtktexttag.c',


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