summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Clasen <mclasen@redhat.com>2022-01-14 22:08:59 -0500
committerMatthias Clasen <mclasen@redhat.com>2022-01-24 10:57:29 -0500
commitfac87e376bff4499bca881187e5730494307e401 (patch)
tree5ce013db4eb9f08f1bb862c53d5d7f4f43bad327
parent495f377f024f6c874bfb9afa2da169a09e15b6d8 (diff)
downloadpango-fac87e376bff4499bca881187e5730494307e401.tar.gz
Add PangoLineBreaker
This is the guts of PangoLayout, spilled out.
-rw-r--r--pango/meson.build2
-rw-r--r--pango/pango-line-breaker.c2638
-rw-r--r--pango/pango-line-breaker.h57
-rw-r--r--pango/pango.h1
4 files changed, 2698 insertions, 0 deletions
diff --git a/pango/meson.build b/pango/meson.build
index 7844703d..41a78a3c 100644
--- a/pango/meson.build
+++ b/pango/meson.build
@@ -33,6 +33,7 @@ pango_sources = [
'json/gtkjsonprinter.c',
'pango-layout-run.c',
'pango-line.c',
+ 'pango-line-breaker.c',
]
pango_headers = [
@@ -56,6 +57,7 @@ pango_headers = [
'pango-language.h',
'pango-layout-run.h',
'pango-line.h',
+ 'pango-line-breaker.h',
'pango-layout.h',
'pango-matrix.h',
'pango-markup.h',
diff --git a/pango/pango-line-breaker.c b/pango/pango-line-breaker.c
new file mode 100644
index 00000000..bb3bd027
--- /dev/null
+++ b/pango/pango-line-breaker.c
@@ -0,0 +1,2638 @@
+#include "config.h"
+
+#include "pango-line-breaker.h"
+#include "pango-line-private.h"
+
+#include "pango-tabs.h"
+#include "pango-impl-utils.h"
+#include "pango-attributes-private.h"
+#include "pango-item-private.h"
+
+#include <locale.h>
+
+#include <hb-ot.h>
+
+#if 0
+# define DEBUG1(...) g_debug (__VA_ARGS__)
+#else
+# define DEBUG1(...) do { } while (0)
+#endif
+
+/**
+ * PangoLineBreaker:
+ *
+ * A `PangoLineBreaker` breaks text into lines.
+ *
+ * To use a `PangoLineBreaker`, you must call [method@Pango.LineBreaker.add_text]
+ * to provide text that you want to break into lines, plus possibly attribute
+ * to influence the formatting.
+ *
+ * Then you can call [method@Pango.LineBreaker.next_line] repeatedly to obtain
+ * `PangoLine` objects for the text, one by one.
+ *
+ * `PangoLineBreaker` is meant to enable use cases like flowing text around images,
+ * or shaped paragraphs. For simple formatting needs, [class@Pango.SimpleLayout]
+ * is probably more convenient to use.
+ */
+
+typedef struct _LastTabState LastTabState;
+struct _LastTabState
+{
+ PangoGlyphString *glyphs;
+ int index;
+ int width;
+ int pos;
+ PangoTabAlign align;
+ gunichar decimal;
+};
+
+struct _PangoLineBreaker
+{
+ GObject parent_instance;
+
+ /* Properties */
+ PangoContext *context;
+ PangoDirection base_dir;
+ PangoTabArray *tabs;
+
+ /* Data that we're building lines from, shared among all the lines */
+ GSList *datas; /* Queued up LineData */
+ LineData *data; /* The LineData we're currently processing */
+ GList *data_items; /* Original items for data (only used for undoing) */
+ GList *items; /* The remaining unprocessed items for data */
+ PangoAttrList *render_attrs; /* Attributes to be re-added after line breaking */
+
+ /* Arguments to next_line, for use while processing the next line */
+ PangoWrapMode line_wrap;
+ PangoEllipsizeMode line_ellipsize;
+
+ int tab_width; /* Cached width of a tab. -1 == not yet calculated */
+ int hyphen_width; /* Cached width of a hyphen. -1 == not yet calculated */
+ gunichar decimal; /* Cached decimal point. 0 == not yet calculated */
+
+ /* State for line breaking */
+ int n_lines; /* Line count, starting from 0 */
+ PangoGlyphString *glyphs; /* Glyphs for the first item in self->items */
+ int start_offset; /* Character offset of first item in self->items in self->data->text */
+ ItemProperties properties; /* Properties of the first item in self->items */
+ int *log_widths; /* Logical widths for th efirst item in self->items */
+ int num_log_widths; /* Length o fo log_widths */
+ int log_widths_offset; /* Offset into log_widths to the point corresponding to
+ * the remaining portion of the fist item
+ */
+ int line_start_index; /* Byte offset of line in self->data->text */
+ int line_start_offset; /* Character offset of line in self->data->text */
+
+ int line_x; /* X offset for current line */
+ int line_width; /* Goal width of current line; < 0 for unlimited */
+ int remaining_width; /* Amount of spac remaining on line; < 0 for unlimited */
+
+ gboolean at_paragraph_start; /* TRUE if the next line starts a new paragraph */
+
+ GList *baseline_shifts;
+
+ LastTabState last_tab;
+};
+
+struct _PangoLineBreakerClass
+{
+ GObjectClass parent_class;
+};
+
+/* {{{ Utilities */
+
+static LineData *
+make_line_data (PangoLineBreaker *self,
+ const char *text,
+ int length,
+ PangoAttrList *attrs)
+{
+ LineData *data;
+
+ if (length < 0)
+ length = strlen (text);
+
+ data = line_data_new ();
+
+ if (self->base_dir == PANGO_DIRECTION_NEUTRAL)
+ {
+G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+ data->direction = pango_find_base_dir (text, length);
+G_GNUC_END_IGNORE_DEPRECATIONS
+
+ if (data->direction == PANGO_DIRECTION_NEUTRAL)
+ data->direction = pango_context_get_base_dir (self->context);
+ }
+ else
+ data->direction = self->base_dir;
+
+ data->text = g_strndup (text, length);
+ data->length = length;
+ data->n_chars = g_utf8_strlen (text, length);
+ if (attrs)
+ data->attrs = pango_attr_list_copy (attrs);
+
+ return data;
+}
+
+static gboolean
+item_is_paragraph_separator (PangoLineBreaker *self,
+ PangoItem *item)
+{
+ gunichar ch;
+
+ if (self->properties.no_paragraph_break)
+ return FALSE;
+
+ ch = g_utf8_get_char (self->data->text + item->offset);
+
+ return ch == '\r' || ch == '\n' || ch == 0x2029;
+}
+
+static gboolean
+affects_itemization (PangoAttribute *attr,
+ gpointer data)
+{
+ switch ((int)attr->klass->type)
+ {
+ /* These affect font selection */
+ case PANGO_ATTR_LANGUAGE:
+ case PANGO_ATTR_FAMILY:
+ case PANGO_ATTR_STYLE:
+ case PANGO_ATTR_WEIGHT:
+ case PANGO_ATTR_VARIANT:
+ case PANGO_ATTR_STRETCH:
+ case PANGO_ATTR_SIZE:
+ case PANGO_ATTR_FONT_DESC:
+ case PANGO_ATTR_SCALE:
+ case PANGO_ATTR_FALLBACK:
+ case PANGO_ATTR_ABSOLUTE_SIZE:
+ case PANGO_ATTR_GRAVITY:
+ case PANGO_ATTR_GRAVITY_HINT:
+ case PANGO_ATTR_FONT_SCALE:
+ /* These need to be constant across runs */
+ case PANGO_ATTR_LETTER_SPACING:
+ case PANGO_ATTR_SHAPE:
+ case PANGO_ATTR_RISE:
+ case PANGO_ATTR_BASELINE_SHIFT:
+ case PANGO_ATTR_LINE_HEIGHT:
+ case PANGO_ATTR_ABSOLUTE_LINE_HEIGHT:
+ case PANGO_ATTR_TEXT_TRANSFORM:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static gboolean
+affects_break_or_shape (PangoAttribute *attr,
+ gpointer data)
+{
+ switch ((int)attr->klass->type)
+ {
+ /* Affects breaks */
+ case PANGO_ATTR_ALLOW_BREAKS:
+ case PANGO_ATTR_WORD:
+ case PANGO_ATTR_SENTENCE:
+ case PANGO_ATTR_PARAGRAPH:
+ /* Affects shaping */
+ case PANGO_ATTR_INSERT_HYPHENS:
+ case PANGO_ATTR_FONT_FEATURES:
+ case PANGO_ATTR_SHOW:
+ return TRUE;
+ default:
+ return FALSE;
+ }
+}
+
+static void
+apply_attributes_to_items (GList *items,
+ PangoAttrList *attrs)
+{
+ GList *l;
+ PangoAttrIterator iter;
+
+ if (!attrs)
+ return;
+
+ _pango_attr_list_get_iterator (attrs, &iter);
+
+ for (l = items; l; l = l->next)
+ {
+ PangoItem *item = l->data;
+ pango_item_apply_attrs (item, &iter);
+ }
+
+ _pango_attr_iterator_destroy (&iter);
+}
+
+static PangoLogAttr *
+get_log_attrs (LineData *data,
+ GList *items)
+{
+ PangoLogAttr *log_attrs;
+ int offset;
+
+ log_attrs = g_new0 (PangoLogAttr, (data->n_chars + 1));
+
+ pango_default_break (data->text,
+ data->length,
+ NULL,
+ log_attrs,
+ data->n_chars + 1);
+
+ offset = 0;
+ for (GList *l = items; l; l = l->next)
+ {
+ PangoItem *item = l->data;
+
+ pango_tailor_break (data->text + item->offset,
+ item->length,
+ &item->analysis,
+ item->offset,
+ log_attrs + offset,
+ item->num_chars + 1);
+
+ offset += item->num_chars;
+ }
+
+ if (data->attrs)
+ pango_attr_break (data->text,
+ data->length,
+ data->attrs,
+ 0,
+ log_attrs,
+ data->n_chars + 1);
+
+ return log_attrs;
+}
+
+static void
+ensure_items (PangoLineBreaker *self)
+{
+ PangoAttrList *itemize_attrs = NULL;
+ PangoAttrList *shape_attrs = NULL;
+
+ if (self->items)
+ return;
+
+ if (!self->data && self->datas)
+ {
+ self->data = self->datas->data;
+ self->datas = g_slist_remove (self->datas, self->data);
+ }
+
+ if (!self->data)
+ return;
+
+ self->render_attrs = pango_attr_list_copy (self->data->attrs);
+ if (self->render_attrs)
+ {
+ shape_attrs = pango_attr_list_filter (self->render_attrs, affects_break_or_shape, NULL);
+ itemize_attrs = pango_attr_list_filter (self->render_attrs, affects_itemization, NULL);
+ }
+
+ self->items = pango_itemize_with_font (self->context,
+ self->data->direction,
+ self->data->text,
+ 0,
+ self->data->length,
+ itemize_attrs,
+ NULL,
+ NULL);
+
+ apply_attributes_to_items (self->items, shape_attrs);
+
+ pango_attr_list_unref (itemize_attrs);
+ pango_attr_list_unref (shape_attrs);
+
+ self->data->log_attrs = get_log_attrs (self->data, self->items);
+
+ self->items = pango_itemize_post_process_items (self->context,
+ self->data->text,
+ self->data->log_attrs,
+ self->items);
+
+ g_assert (self->data_items == NULL);
+ self->data_items = g_list_copy_deep (self->items, (GCopyFunc) pango_item_copy, NULL);
+
+ self->hyphen_width = -1;
+ self->tab_width = -1;
+
+ self->start_offset = 0;
+ self->line_start_offset = 0;
+ self->line_start_index = 0;
+
+ g_list_free_full (self->baseline_shifts, g_free);
+ self->baseline_shifts = NULL;
+ g_clear_pointer (&self->glyphs, pango_glyph_string_free);
+ g_clear_pointer (&self->log_widths, g_free);
+ self->num_log_widths = 0;
+ self->log_widths_offset = 0;
+
+ self->remaining_width = -1;
+ self->at_paragraph_start = TRUE;
+}
+
+/* The resolved direction for the line is always one
+ * of LTR/RTL; not a week or neutral directions
+ */
+static PangoDirection
+get_resolved_dir (PangoLineBreaker *self)
+{
+ PangoDirection dir;
+
+ ensure_items (self);
+
+ if (!self->data)
+ return PANGO_DIRECTION_NEUTRAL;
+
+ switch (self->data->direction)
+ {
+ default:
+ case PANGO_DIRECTION_LTR:
+ case PANGO_DIRECTION_TTB_RTL:
+ case PANGO_DIRECTION_WEAK_LTR:
+ case PANGO_DIRECTION_NEUTRAL:
+ dir = PANGO_DIRECTION_LTR;
+ break;
+ case PANGO_DIRECTION_RTL:
+ case PANGO_DIRECTION_WEAK_RTL:
+ case PANGO_DIRECTION_TTB_LTR:
+ dir = PANGO_DIRECTION_RTL;
+ break;
+ }
+
+ /* The direction vs. gravity dance:
+ * - If gravity is SOUTH, leave direction untouched.
+ * - If gravity is NORTH, switch direction.
+ * - If gravity is EAST, set to LTR, as
+ * it's a clockwise-rotated layout, so the rotated
+ * top is unrotated left.
+ * - If gravity is WEST, set to RTL, as
+ * it's a counter-clockwise-rotated layout, so the rotated
+ * top is unrotated right.
+ *
+ * A similar dance is performed in pango-context.c:
+ * itemize_state_add_character(). Keep in synch.
+ */
+
+ switch (pango_context_get_gravity (self->context))
+ {
+ default:
+ case PANGO_GRAVITY_AUTO:
+ case PANGO_GRAVITY_SOUTH:
+ break;
+ case PANGO_GRAVITY_NORTH:
+ dir = PANGO_DIRECTION_LTR + PANGO_DIRECTION_RTL - dir;
+ break;
+ case PANGO_GRAVITY_EAST:
+ /* This is in fact why deprecated TTB_RTL is LTR */
+ dir = PANGO_DIRECTION_LTR;
+ break;
+ case PANGO_GRAVITY_WEST:
+ /* This is in fact why deprecated TTB_LTR is RTL */
+ dir = PANGO_DIRECTION_RTL;
+ break;
+ }
+
+ return dir;
+}
+
+static gboolean
+should_ellipsize_current_line (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ return self->line_ellipsize != PANGO_ELLIPSIZE_NONE && self->line_width >= 0;
+}
+
+static void
+get_decimal_prefix_width (PangoItem *item,
+ PangoGlyphString *glyphs,
+ const char *text,
+ gunichar decimal,
+ int *width,
+ gboolean *found)
+{
+ PangoGlyphItem glyph_item = { item, glyphs, 0, 0, 0 };
+ int *log_widths;
+ int i;
+ const char *p;
+
+ log_widths = g_new (int, item->num_chars);
+
+ pango_glyph_item_get_logical_widths (&glyph_item, text, log_widths);
+
+ *width = 0;
+ *found = FALSE;
+
+ for (i = 0, p = text + item->offset; i < item->num_chars; i++, p = g_utf8_next_char (p))
+ {
+ if (g_utf8_get_char (p) == decimal)
+ {
+ *width += log_widths[i] / 2;
+ *found = TRUE;
+ break;
+ }
+
+ *width += log_widths[i];
+ }
+
+ g_free (log_widths);
+}
+
+static int
+pango_line_compute_width (PangoLine *line)
+{
+ int width = 0;
+
+ /* Compute the width of the line currently - inefficient, but easier
+ * than keeping the current width of the line up to date everywhere
+ */
+ for (GSList *l = line->runs; l; l = l->next)
+ {
+ PangoGlyphItem *run = l->data;
+ width += pango_glyph_string_get_width (run->glyphs);
+ }
+
+ return width;
+}
+
+static inline int
+get_line_width (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ if (self->remaining_width > -1)
+ return self->line_width - self->remaining_width;
+
+ return pango_line_compute_width (line);
+}
+
+static inline void
+ensure_decimal (PangoLineBreaker *self)
+{
+ if (self->decimal == 0)
+ self->decimal = g_utf8_get_char (localeconv ()->decimal_point);
+}
+
+static void
+ensure_tab_width (PangoLineBreaker *self)
+{
+ if (self->tab_width == -1)
+ {
+ /* Find out how wide 8 spaces are in the context's default
+ * font. Utter performance killer. :-(
+ */
+ PangoGlyphString *glyphs = pango_glyph_string_new ();
+ PangoItem *item;
+ GList *items;
+ PangoAttribute *attr;
+ PangoAttrList *attrs;
+ PangoAttrList tmp_attrs;
+ PangoFontDescription *font_desc = pango_font_description_copy_static (pango_context_get_font_description (self->context));
+ PangoLanguage *language = NULL;
+ PangoShapeFlags shape_flags = PANGO_SHAPE_NONE;
+
+ if (pango_context_get_round_glyph_positions (self->context))
+ shape_flags |= PANGO_SHAPE_ROUND_POSITIONS;
+
+ attrs = self->data->attrs;
+ if (attrs)
+ {
+ PangoAttrIterator iter;
+
+ _pango_attr_list_get_iterator (attrs, &iter);
+ pango_attr_iterator_get_font (&iter, font_desc, &language, NULL);
+ _pango_attr_iterator_destroy (&iter);
+ }
+
+ _pango_attr_list_init (&tmp_attrs);
+ attr = pango_attr_font_desc_new (font_desc);
+ pango_font_description_free (font_desc);
+ pango_attr_list_insert_before (&tmp_attrs, attr);
+
+ if (language)
+ {
+ attr = pango_attr_language_new (language);
+ pango_attr_list_insert_before (&tmp_attrs, attr);
+ }
+
+ items = pango_itemize (self->context, " ", 0, 1, &tmp_attrs, NULL);
+
+ if (attrs != self->data->attrs)
+ {
+ pango_attr_list_unref (attrs);
+ attrs = NULL;
+ }
+
+ _pango_attr_list_destroy (&tmp_attrs);
+
+ item = items->data;
+ pango_shape_with_flags (" ", 8, " ", 8, &item->analysis, glyphs, shape_flags);
+
+ pango_item_free (item);
+ g_list_free (items);
+
+ self->tab_width = pango_glyph_string_get_width (glyphs);
+
+ pango_glyph_string_free (glyphs);
+
+ /* We need to make sure the tab_width is > 0 so finding tab positions
+ * terminates. This check should be necessary only under extreme
+ * problems with the font.
+ */
+ if (self->tab_width <= 0)
+ self->tab_width = 50 * PANGO_SCALE; /* pretty much arbitrary */
+ }
+}
+
+static int
+get_item_letter_spacing (PangoItem *item)
+{
+ ItemProperties properties;
+
+ pango_item_get_properties (item, &properties);
+
+ return properties.letter_spacing;
+}
+
+static void
+pad_glyphstring_right (PangoLineBreaker *self,
+ PangoGlyphString *glyphs,
+ int adjustment)
+{
+ int glyph = glyphs->num_glyphs - 1;
+
+ while (glyph >= 0 && glyphs->glyphs[glyph].geometry.width == 0)
+ glyph--;
+
+ if (glyph < 0)
+ return;
+
+ self->remaining_width -= adjustment;
+
+ glyphs->glyphs[glyph].geometry.width += adjustment;
+ if (glyphs->glyphs[glyph].geometry.width < 0)
+ {
+ self->remaining_width += glyphs->glyphs[glyph].geometry.width;
+ glyphs->glyphs[glyph].geometry.width = 0;
+ }
+}
+
+static void
+pad_glyphstring_left (PangoLineBreaker *self,
+ PangoGlyphString *glyphs,
+ int adjustment)
+{
+ int glyph = 0;
+
+ while (glyph < glyphs->num_glyphs && glyphs->glyphs[glyph].geometry.width == 0)
+ glyph++;
+
+ if (glyph == glyphs->num_glyphs)
+ return;
+
+ self->remaining_width -= adjustment;
+
+ glyphs->glyphs[glyph].geometry.width += adjustment;
+ glyphs->glyphs[glyph].geometry.x_offset += adjustment;
+}
+
+static gboolean
+is_tab_run (PangoLine *line,
+ PangoGlyphItem *run)
+{
+ return line->data->text[run->item->offset] == '\t';
+}
+
+/*
+ * NB: This implement the exact same algorithm as
+ * reorder-items.c:pango_reorder_items().
+ */
+static GSList *
+reorder_runs_recurse (GSList *items,
+ int n_items)
+{
+ GSList *tmp_list, *level_start_node;
+ int i, level_start_i;
+ int min_level = G_MAXINT;
+ GSList *result = NULL;
+
+ if (n_items == 0)
+ return NULL;
+
+ tmp_list = items;
+ for (i=0; i<n_items; i++)
+ {
+ PangoGlyphItem *run = tmp_list->data;
+
+ min_level = MIN (min_level, run->item->analysis.level);
+
+ tmp_list = tmp_list->next;
+ }
+
+ level_start_i = 0;
+ level_start_node = items;
+ tmp_list = items;
+ for (i=0; i<n_items; i++)
+ {
+ PangoGlyphItem *run = tmp_list->data;
+
+ if (run->item->analysis.level == min_level)
+ {
+ if (min_level % 2)
+ {
+ if (i > level_start_i)
+ result = g_slist_concat (reorder_runs_recurse (level_start_node, i - level_start_i), result);
+ result = g_slist_prepend (result, run);
+ }
+ else
+ {
+ if (i > level_start_i)
+ result = g_slist_concat (result, reorder_runs_recurse (level_start_node, i - level_start_i));
+ result = g_slist_append (result, run);
+ }
+
+ level_start_i = i + 1;
+ level_start_node = tmp_list->next;
+ }
+
+ tmp_list = tmp_list->next;
+ }
+
+ if (min_level % 2)
+ {
+ if (i > level_start_i)
+ result = g_slist_concat (reorder_runs_recurse (level_start_node, i - level_start_i), result);
+ }
+ else
+ {
+ if (i > level_start_i)
+ result = g_slist_concat (result, reorder_runs_recurse (level_start_node, i - level_start_i));
+ }
+
+ return result;
+}
+
+static void
+pango_line_reorder (PangoLine *line)
+{
+ GSList *logical_runs = line->runs;
+ GSList *tmp_list;
+ gboolean all_even, all_odd;
+ guint8 level_or = 0, level_and = 1;
+ int length = 0;
+
+ /* Check if all items are in the same direction, in that case, the
+ * line does not need modification and we can avoid the expensive
+ * reorder runs recurse procedure.
+ */
+ for (tmp_list = logical_runs; tmp_list != NULL; tmp_list = tmp_list->next)
+ {
+ PangoGlyphItem *run = tmp_list->data;
+
+ level_or |= run->item->analysis.level;
+ level_and &= run->item->analysis.level;
+
+ length++;
+ }
+
+ /* If none of the levels had the LSB set, all numbers were even. */
+ all_even = (level_or & 0x1) == 0;
+
+ /* If all of the levels had the LSB set, all numbers were odd. */
+ all_odd = (level_and & 0x1) == 1;
+
+ if (!all_even && !all_odd)
+ {
+ line->runs = reorder_runs_recurse (logical_runs, length);
+ g_slist_free (logical_runs);
+ }
+ else if (all_odd)
+ line->runs = g_slist_reverse (logical_runs);
+}
+
+static int
+compute_n_chars (PangoLine *line)
+{
+ int n_chars = 0;
+
+ for (GSList *l = line->runs; l; l = l->next)
+ {
+ PangoGlyphItem *run = l->data;
+ n_chars += run->item->num_chars;
+ }
+
+ return n_chars;
+}
+
+/* }}} */
+/* {{{ Line Breaking */
+
+static void
+get_tab_pos (PangoLineBreaker *self,
+ PangoLine *line,
+ int index,
+ int *tab_pos,
+ PangoTabAlign *alignment,
+ gunichar *decimal,
+ gboolean *is_default)
+{
+ int n_tabs;
+ gboolean in_pixels;
+ int offset = 0;
+
+ offset = self->line_x;
+
+ if (self->tabs)
+ {
+ n_tabs = pango_tab_array_get_size (self->tabs);
+ in_pixels = pango_tab_array_get_positions_in_pixels (self->tabs);
+ *is_default = FALSE;
+ }
+ else
+ {
+ n_tabs = 0;
+ in_pixels = FALSE;
+ *is_default = TRUE;
+ }
+
+ if (index < n_tabs)
+ {
+ pango_tab_array_get_tab (self->tabs, index, alignment, tab_pos);
+
+ if (in_pixels)
+ *tab_pos *= PANGO_SCALE;
+
+ *decimal = pango_tab_array_get_decimal_point (self->tabs, index);
+ }
+ else if (n_tabs > 0)
+ {
+ /* Extrapolate tab position, repeating the last tab gap to infinity. */
+ int last_pos = 0;
+ int next_to_last_pos = 0;
+ int tab_width;
+
+ pango_tab_array_get_tab (self->tabs, n_tabs - 1, alignment, &last_pos);
+ *decimal = pango_tab_array_get_decimal_point (self->tabs, n_tabs - 1);
+
+ if (n_tabs > 1)
+ pango_tab_array_get_tab (self->tabs, n_tabs - 2, NULL, &next_to_last_pos);
+ else
+ next_to_last_pos = 0;
+
+ if (in_pixels)
+ {
+ next_to_last_pos *= PANGO_SCALE;
+ last_pos *= PANGO_SCALE;
+ }
+
+ if (last_pos > next_to_last_pos)
+ tab_width = last_pos - next_to_last_pos;
+ else
+ tab_width = self->tab_width;
+
+ *tab_pos = last_pos + tab_width * (index - n_tabs + 1);
+ }
+ else
+ {
+ /* No tab array set, so use default tab width */
+ *tab_pos = self->tab_width * index;
+ *alignment = PANGO_TAB_LEFT;
+ *decimal = 0;
+ }
+
+ *tab_pos -= offset;
+}
+
+static void
+shape_tab (PangoLineBreaker *self,
+ PangoLine *line,
+ int current_width,
+ PangoItem *item,
+ PangoGlyphString *glyphs)
+{
+ int i, space_width;
+ int tab_pos;
+ PangoTabAlign tab_align;
+ gunichar tab_decimal;
+
+ pango_glyph_string_set_size (glyphs, 1);
+
+ if (self->properties.showing_space)
+ glyphs->glyphs[0].glyph = PANGO_GET_UNKNOWN_GLYPH ('\t');
+ else
+ glyphs->glyphs[0].glyph = PANGO_GLYPH_EMPTY;
+
+ glyphs->glyphs[0].geometry.x_offset = 0;
+ glyphs->glyphs[0].geometry.y_offset = 0;
+ glyphs->glyphs[0].attr.is_cluster_start = 1;
+ glyphs->glyphs[0].attr.is_color = 0;
+
+ glyphs->log_clusters[0] = 0;
+
+ ensure_tab_width (self);
+ space_width = self->tab_width / 8;
+
+ for (i = self->last_tab.index; ; i++)
+ {
+ gboolean is_default;
+
+ get_tab_pos (self, line, i, &tab_pos, &tab_align, &tab_decimal, &is_default);
+
+ /* Make sure there is at least a space-width of space between
+ * tab-aligned text and the text before it. However, only do
+ * this if no tab array is set on the line breaker, ie. using default
+ * tab positions. If the user has set tab positions, respect it
+ * to the pixel.
+ */
+ if (tab_pos >= current_width + (is_default ? space_width : 1))
+ {
+ glyphs->glyphs[0].geometry.width = tab_pos - current_width;
+ break;
+ }
+ }
+
+ if (tab_decimal == 0)
+ {
+ ensure_decimal (self);
+ tab_decimal = self->decimal;
+ }
+
+ self->last_tab.glyphs = glyphs;
+ self->last_tab.index = i;
+ self->last_tab.width = current_width;
+ self->last_tab.pos = tab_pos;
+ self->last_tab.align = tab_align;
+ self->last_tab.decimal = tab_decimal;
+}
+
+static inline gboolean
+can_break_at (PangoLineBreaker *self,
+ gint offset,
+ PangoWrapMode wrap)
+{
+ if (offset == self->data->n_chars)
+ return TRUE;
+ else if (wrap == PANGO_WRAP_CHAR)
+ return self->data->log_attrs[offset].is_char_break;
+ else
+ return self->data->log_attrs[offset].is_line_break;
+}
+
+static inline gboolean
+can_break_in (PangoLineBreaker *self,
+ int start_offset,
+ int num_chars,
+ gboolean allow_break_at_start)
+{
+ for (int i = allow_break_at_start ? 0 : 1; i < num_chars; i++)
+ {
+ if (can_break_at (self, start_offset + i, self->line_wrap))
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static inline void
+distribute_letter_spacing (int letter_spacing,
+ int *space_left,
+ int *space_right)
+{
+ *space_left = letter_spacing / 2;
+
+ /* hinting */
+ if ((letter_spacing & (PANGO_SCALE - 1)) == 0)
+ *space_left = PANGO_UNITS_ROUND (*space_left);
+ *space_right = letter_spacing - *space_left;
+}
+
+
+static PangoGlyphString *
+shape_run (PangoLineBreaker *self,
+ PangoLine *line,
+ PangoItem *item)
+{
+ PangoGlyphString *glyphs = pango_glyph_string_new ();
+
+ if (self->data->text[item->offset] == '\t')
+ shape_tab (self, line, get_line_width (self, line), item, glyphs);
+ else
+ {
+ PangoShapeFlags shape_flags = PANGO_SHAPE_NONE;
+
+ if (pango_context_get_round_glyph_positions (self->context))
+ shape_flags |= PANGO_SHAPE_ROUND_POSITIONS;
+
+ if (self->properties.shape_set)
+ _pango_shape_shape (self->data->text + item->offset, item->num_chars,
+ self->properties.shape_ink_rect, self->properties.shape_logical_rect,
+ glyphs);
+ else
+ pango_shape_item (item,
+ self->data->text, self->data->length,
+ self->data->log_attrs + self->start_offset,
+ glyphs,
+ shape_flags);
+
+ if (self->properties.letter_spacing)
+ {
+ PangoGlyphItem glyph_item;
+ int space_left, space_right;
+
+ glyph_item.item = item;
+ glyph_item.glyphs = glyphs;
+
+ pango_glyph_item_letter_space (&glyph_item,
+ self->data->text,
+ self->data->log_attrs + self->start_offset,
+ self->properties.letter_spacing);
+
+ distribute_letter_spacing (self->properties.letter_spacing, &space_left, &space_right);
+
+ glyphs->glyphs[0].geometry.width += space_left;
+ glyphs->glyphs[0].geometry.x_offset += space_left;
+ glyphs->glyphs[glyphs->num_glyphs - 1].geometry.width += space_right;
+ }
+
+ if (self->last_tab.glyphs != NULL)
+ {
+ int w;
+
+ g_assert (self->last_tab.glyphs->num_glyphs == 1);
+
+ /* Update the width of the current tab to position this run properly */
+
+ w = self->last_tab.pos - self->last_tab.width;
+
+ if (self->last_tab.align == PANGO_TAB_RIGHT)
+ w -= pango_glyph_string_get_width (glyphs);
+ else if (self->last_tab.align == PANGO_TAB_CENTER)
+ w -= pango_glyph_string_get_width (glyphs) / 2;
+ else if (self->last_tab.align == PANGO_TAB_DECIMAL)
+ {
+ int width;
+ gboolean found;
+
+ get_decimal_prefix_width (item, glyphs, self->data->text, self->last_tab.decimal, &width, &found);
+
+ w -= width;
+ }
+
+ self->last_tab.glyphs->glyphs[0].geometry.width = MAX (w, 0);
+ }
+ }
+
+ return glyphs;
+}
+
+static void
+free_run (PangoGlyphItem *run,
+ gpointer data)
+{
+ gboolean free_item = data != NULL;
+ if (free_item)
+ pango_item_free (run->item);
+
+ pango_glyph_string_free (run->glyphs);
+ g_slice_free (PangoGlyphItem, run);
+}
+
+static PangoItem *
+uninsert_run (PangoLine *line)
+{
+ PangoGlyphItem *run;
+ PangoItem *item;
+
+ GSList *tmp_node = line->runs;
+
+ run = tmp_node->data;
+ item = run->item;
+
+ line->runs = tmp_node->next;
+ line->length -= item->length;
+
+ g_slist_free_1 (tmp_node);
+ free_run (run, NULL);
+
+ return item;
+}
+
+static void
+insert_run (PangoLineBreaker *self,
+ PangoLine *line,
+ PangoItem *run_item,
+ PangoGlyphString *glyphs,
+ gboolean last_run)
+{
+ PangoGlyphItem *run = g_slice_new (PangoGlyphItem);
+
+ run->item = run_item;
+
+ if (glyphs)
+ run->glyphs = glyphs;
+ else if (last_run &&
+ self->log_widths_offset == 0 &&
+ !(run_item->analysis.flags & PANGO_ANALYSIS_FLAG_NEED_HYPHEN))
+ {
+ run->glyphs = self->glyphs;
+ self->glyphs = NULL;
+ }
+ else
+ run->glyphs = shape_run (self, line, run_item);
+
+ if (last_run && self->glyphs)
+ {
+ pango_glyph_string_free (self->glyphs);
+ self->glyphs = NULL;
+ }
+
+ line->runs = g_slist_prepend (line->runs, run);
+ line->length += run_item->length;
+
+ if (self->last_tab.glyphs && run->glyphs != self->last_tab.glyphs)
+ {
+ gboolean found_decimal = FALSE;
+ int width;
+
+ /* Adjust the tab position so placing further runs will continue to
+ * maintain the tab placement. In the case of decimal tabs, we are
+ * done once we've placed the run with the decimal point.
+ */
+
+ if (self->last_tab.align == PANGO_TAB_RIGHT)
+ self->last_tab.width += pango_glyph_string_get_width (run->glyphs);
+ else if (self->last_tab.align == PANGO_TAB_CENTER)
+ self->last_tab.width += pango_glyph_string_get_width (run->glyphs) / 2;
+ else if (self->last_tab.align == PANGO_TAB_DECIMAL)
+ {
+ int width;
+
+ get_decimal_prefix_width (run->item, run->glyphs, line->data->text, self->last_tab.decimal, &width, &found_decimal);
+
+ self->last_tab.width += width;
+ }
+
+ width = MAX (self->last_tab.pos - self->last_tab.width, 0);
+ self->last_tab.glyphs->glyphs[0].geometry.width = width;
+
+ if (found_decimal || width == 0)
+ self->last_tab.glyphs = NULL;
+ }
+}
+
+static gboolean
+break_needs_hyphen (PangoLineBreaker *self,
+ int pos)
+{
+ return self->data->log_attrs[self->start_offset + pos].break_inserts_hyphen ||
+ self->data->log_attrs[self->start_offset + pos].break_removes_preceding;
+}
+
+
+static int
+find_hyphen_width (PangoItem *item)
+{
+ hb_font_t *hb_font;
+ hb_codepoint_t glyph;
+
+ if (!item->analysis.font)
+ return 0;
+
+ /* This is not technically correct, since
+ * a) we may end up inserting a different hyphen
+ * b) we should reshape the entire run
+ * But it is close enough in practice
+ */
+ hb_font = pango_font_get_hb_font (item->analysis.font);
+ if (hb_font_get_nominal_glyph (hb_font, 0x2010, &glyph) ||
+ hb_font_get_nominal_glyph (hb_font, '-', &glyph))
+ return hb_font_get_glyph_h_advance (hb_font, glyph);
+
+ return 0;
+}
+
+static inline void
+ensure_hyphen_width (PangoLineBreaker *self)
+{
+ if (self ->hyphen_width < 0)
+ {
+ PangoItem *item = self->items->data;
+ self->hyphen_width = find_hyphen_width (item);
+ }
+}
+
+static int
+find_break_extra_width (PangoLineBreaker *self,
+ int pos)
+{
+ /* Check whether to insert a hyphen,
+ * or whether we are breaking after one of those
+ * characters that turn into a hyphen,
+ * or after a space.
+ */
+ if (self->data->log_attrs[self->start_offset + pos].break_inserts_hyphen)
+ {
+ ensure_hyphen_width (self);
+
+ if (self->data->log_attrs[self->start_offset + pos].break_removes_preceding && pos > 0)
+ return self->hyphen_width - self->log_widths[self->log_widths_offset + pos - 1];
+ else
+ return self->hyphen_width;
+ }
+ else if (pos > 0 &&
+ self->data->log_attrs[self->start_offset + pos - 1].is_white)
+ {
+ return - self->log_widths[self->log_widths_offset + pos - 1];
+ }
+
+ return 0;
+}
+
+static inline void
+compute_log_widths (PangoLineBreaker *self)
+{
+ PangoItem *item = self->items->data;
+ PangoGlyphItem glyph_item = { item, self->glyphs };
+
+ if (item->num_chars > self->num_log_widths)
+ {
+ self->log_widths = g_renew (int, self->log_widths, item->num_chars);
+ self->num_log_widths = item->num_chars;
+ }
+
+ pango_glyph_item_get_logical_widths (&glyph_item, self->data->text, self->log_widths);
+}
+
+/* If last_tab is set, we've added a tab and remaining_width has been updated to
+ * account for its origin width, which is last_tab_pos - last_tab_width. shape_run
+ * updates the tab width, so we need to consider the delta when comparing
+ * against remaining_width.
+ */
+static int
+tab_width_change (PangoLineBreaker *self)
+{
+ if (self->last_tab.glyphs)
+ return self->last_tab.glyphs->glyphs[0].geometry.width - (self->last_tab.pos - self->last_tab.width);
+
+ return 0;
+}
+
+typedef enum
+{
+ BREAK_NONE_FIT,
+ BREAK_SOME_FIT,
+ BREAK_ALL_FIT,
+ BREAK_EMPTY_FIT,
+ BREAK_LINE_SEPARATOR,
+ BREAK_PARAGRAPH_SEPARATOR
+} BreakResult;
+
+/* Tries to insert as much as possible of the item at the head of
+ * self->items onto @line. Five results are possible:
+ *
+ * %BREAK_NONE_FIT: Couldn't fit anything.
+ * %BREAK_SOME_FIT: The item was broken in the middle.
+ * %BREAK_ALL_FIT: Everything fit.
+ * %BREAK_EMPTY_FIT: Nothing fit, but that was ok, as we can break at the first char.
+ * %BREAK_LINE_SEPARATOR: Item begins with a line separator.
+ * %BREAK_PARAGRAPH_SEPARATOR: Item begins with a paragraph separator
+ *
+ * If @force_fit is %TRUE, then %BREAK_NONE_FIT will never
+ * be returned, a run will be added even if inserting the minimum amount
+ * will cause the line to overflow. This is used at the start of a line
+ * and until we've found at least some place to break.
+ *
+ * If @no_break_at_end is %TRUE, then %BREAK_ALL_FIT will never be
+ * returned even everything fits; the run will be broken earlier,
+ * or %BREAK_NONE_FIT returned. This is used when the end of the
+ * run is not a break position.
+ *
+ * This function is the core of our line-breaking, and it is long and involved.
+ * Here is an outline of the algorithm, without all the bookkeeping:
+ *
+ * if item appears to fit entirely
+ * measure it
+ * if it actually fits
+ * return BREAK_ALL_FIT
+ *
+ * retry_break:
+ * for each position p in the item
+ * if adding more is 'obviously' not going to help and we have a breakpoint
+ * exit the loop
+ * if p is a possible break position
+ * if p is 'obviously' going to fit
+ * bc = p
+ * else
+ * measure breaking at p (taking extra break width into account
+ * if we don't have a break candidate yet
+ * bc = p
+ * else
+ * if p is better than bc
+ * bc = p
+ *
+ * if bc does not fit and we can loosen break conditions
+ * loosen break conditions and retry break
+ *
+ * return bc
+ */
+
+static BreakResult
+process_item (PangoLineBreaker *self,
+ PangoLine *line,
+ gboolean force_fit,
+ gboolean no_break_at_end,
+ gboolean is_last_item)
+{
+ PangoItem *item = self->items->data;
+ gboolean shape_set = FALSE;
+ int width;
+ int extra_width;
+ int orig_extra_width;
+ int length;
+ int i;
+ int processing_new_item;
+ int num_chars;
+ int orig_width;
+ PangoWrapMode wrap;
+ int break_num_chars;
+ int break_width;
+ int break_extra_width;
+ PangoGlyphString *break_glyphs;
+ PangoFontMetrics *metrics;
+ int safe_distance;
+
+ DEBUG1 ("process item '%.*s'. Remaining width %d",
+ item->length, self->data->text + item->offset,
+ self->remaining_width);
+
+ /* We don't want to shape more than necessary, so we keep the results
+ * of shaping a new item in self->glyphs, self->log_widths. Once
+ * we break off initial parts of the item, we update self->log_widths_offset
+ * to take that into account. Note that the widths we calculate from the
+ * log_widths are an approximation, because a) log_widths are just
+ * evenly divided for clusters, and b) clusters may change as we
+ * break in the middle (think ff- i).
+ *
+ * We use self->log_widths_offset != 0 to detect if we are dealing
+ * with the original item, or one that has been chopped off.
+ */
+ if (!self->glyphs)
+ {
+ pango_item_get_properties (item, &self->properties);
+ self->glyphs = shape_run (self, line, item);
+ self->log_widths_offset = 0;
+ processing_new_item = TRUE;
+ }
+ else
+ processing_new_item = FALSE;
+
+ if (item_is_paragraph_separator (self, item))
+ {
+ return BREAK_PARAGRAPH_SEPARATOR;
+ }
+
+ /* Only one character has type G_UNICODE_LINE_SEPARATOR in Unicode 5.0;
+ * update this if that changes.
+ */
+#define LINE_SEPARATOR 0x2028
+
+ if (g_utf8_get_char (self->data->text + item->offset) == LINE_SEPARATOR &&
+ !should_ellipsize_current_line (self, line))
+ {
+ insert_run (self, line, item, NULL, TRUE);
+ self->log_widths_offset += item->num_chars;
+
+ return BREAK_LINE_SEPARATOR;
+ }
+
+ if (self->remaining_width < 0 && !no_break_at_end) /* Wrapping off */
+ {
+ insert_run (self, line, item, NULL, TRUE);
+ DEBUG1 ("no wrapping, all-fit");
+ return BREAK_ALL_FIT;
+ }
+
+ if (processing_new_item)
+ {
+ compute_log_widths (self);
+ processing_new_item = FALSE;
+ }
+
+ width = 0;
+ g_assert (self->log_widths_offset + item->num_chars <= self->num_log_widths);
+ for (i = 0; i < item->num_chars; i++)
+ width += self->log_widths[self->log_widths_offset + i];
+
+ if (self->data->text[item->offset] == '\t')
+ {
+ insert_run (self, line, item, NULL, TRUE);
+ self->remaining_width -= width;
+ self->remaining_width = MAX (self->remaining_width, 0);
+
+ DEBUG1 ("tab run, all-fit");
+ return BREAK_ALL_FIT;
+ }
+
+ wrap = self->line_wrap;
+ if (!no_break_at_end &&
+ can_break_at (self, self->start_offset + item->num_chars, wrap))
+ {
+ extra_width = find_break_extra_width (self, item->num_chars);
+ }
+ else
+ extra_width = 0;
+
+ if ((width + extra_width <= self->remaining_width || (item->num_chars == 1 && !line->runs) ||
+ (self->last_tab.glyphs && self->last_tab.align != PANGO_TAB_LEFT)) &&
+ !no_break_at_end)
+ {
+ PangoGlyphString *glyphs;
+
+ DEBUG1 ("%d + %d <= %d", width, extra_width, self->remaining_width);
+ glyphs = shape_run (self, line, item);
+
+ width = pango_glyph_string_get_width (glyphs) + tab_width_change (self);
+
+ if (width + extra_width <= self->remaining_width || (item->num_chars == 1 && !line->runs))
+ {
+ insert_run (self, line, item, glyphs, TRUE);
+
+ self->remaining_width -= width;
+ self->remaining_width = MAX (self->remaining_width, 0);
+
+ DEBUG1 ("early accept '%.*s', all-fit, remaining %d",
+ item->length, self->data->text + item->offset,
+ self->remaining_width);
+ return BREAK_ALL_FIT;
+ }
+
+ /* if it doesn't fit after shaping, discard and proceed to break the item */
+ pango_glyph_string_free (glyphs);
+ }
+
+ /*** From here on, we look for a way to break item ***/
+
+ orig_width = width;
+ orig_extra_width = extra_width;
+ break_width = width;
+ break_extra_width = extra_width;
+ break_num_chars = item->num_chars;
+ wrap = self->line_wrap;;
+ break_glyphs = NULL;
+
+ /* Add some safety margin here. If we are farther away from the end of the
+ * line than this, we don't look carefully at a break possibility.
+ */
+ metrics = pango_font_get_metrics (item->analysis.font, item->analysis.language);
+ safe_distance = pango_font_metrics_get_approximate_char_width (metrics) * 3;
+ pango_font_metrics_unref (metrics);
+
+ if (processing_new_item)
+ {
+ compute_log_widths (self);
+ processing_new_item = FALSE;
+ }
+
+retry_break:
+
+ for (num_chars = 0, width = 0; num_chars < (no_break_at_end ? item->num_chars : (item->num_chars + 1)); num_chars++)
+ {
+ extra_width = find_break_extra_width (self, num_chars);
+
+ /* We don't want to walk the entire item if we can help it, but
+ * we need to keep going at least until we've found a breakpoint
+ * that 'works' (as in, it doesn't overflow the budget we have,
+ * or there is no hope of finding a better one).
+ *
+ * We rely on the fact that MIN(width + extra_width, width) is
+ * monotonically increasing.
+ */
+
+ if (MIN (width + extra_width, width) > self->remaining_width + safe_distance &&
+ break_num_chars < item->num_chars)
+ {
+ DEBUG1 ("at %d, MIN(%d, %d + %d) > %d + MARGIN, breaking at %d",
+ num_chars, width, extra_width, width, self->remaining_width, break_num_chars);
+ break;
+ }
+
+ /* If there are no previous runs we have to take care to grab at least one char. */
+ if (can_break_at (self, self->start_offset + num_chars, wrap) &&
+ (num_chars > 0 || line->runs))
+ {
+ DEBUG1 ("possible breakpoint: %d, extra_width %d", num_chars, extra_width);
+ if (num_chars == 0 ||
+ width + extra_width < self->remaining_width - safe_distance)
+ {
+ DEBUG1 ("trivial accept");
+ break_num_chars = num_chars;
+ break_width = width;
+ break_extra_width = extra_width;
+ }
+ else
+ {
+ int length;
+ int new_break_width;
+ PangoItem *new_item;
+ PangoGlyphString *glyphs;
+
+ length = g_utf8_offset_to_pointer (self->data->text + item->offset, num_chars) - (self->data->text + item->offset);
+
+ if (num_chars < item->num_chars)
+ {
+ new_item = pango_item_split (item, length, num_chars);
+
+ if (break_needs_hyphen (self, num_chars))
+ new_item->analysis.flags |= PANGO_ANALYSIS_FLAG_NEED_HYPHEN;
+ else
+ new_item->analysis.flags &= ~PANGO_ANALYSIS_FLAG_NEED_HYPHEN;
+ }
+ else
+ new_item = item;
+
+ glyphs = shape_run (self, line, new_item);
+
+ new_break_width = pango_glyph_string_get_width (glyphs) + tab_width_change (self);
+
+ if (num_chars > 0 &&
+ (item != new_item || !is_last_item) && /* We don't collapse space at the very end */
+ self->data->log_attrs[self->start_offset + num_chars - 1].is_white)
+ extra_width = - self->log_widths[self->log_widths_offset + num_chars - 1];
+ else if (item == new_item && !is_last_item &&
+ break_needs_hyphen (self, num_chars))
+ extra_width = self->hyphen_width;
+ else
+ extra_width = 0;
+
+ DEBUG1 ("measured breakpoint %d: %d, extra %d", num_chars, new_break_width, extra_width);
+
+ if (new_item != item)
+ {
+ pango_item_free (new_item);
+ pango_item_unsplit (item, length, num_chars);
+ }
+
+ if (break_num_chars == item->num_chars ||
+ new_break_width + extra_width <= self->remaining_width ||
+ new_break_width + extra_width < break_width + break_extra_width)
+ {
+ DEBUG1 ("accept breakpoint %d: %d + %d <= %d + %d",
+ num_chars, new_break_width, extra_width, break_width, break_extra_width);
+ DEBUG1 ("replace bp %d by %d", break_num_chars, num_chars);
+ break_num_chars = num_chars;
+ break_width = new_break_width;
+ break_extra_width = extra_width;
+
+ if (break_glyphs)
+ pango_glyph_string_free (break_glyphs);
+ break_glyphs = glyphs;
+ }
+ else
+ {
+ DEBUG1 ("ignore breakpoint %d", num_chars);
+ pango_glyph_string_free (glyphs);
+ }
+ }
+ }
+
+ DEBUG1 ("bp now %d", break_num_chars);
+ if (num_chars < item->num_chars)
+ width += self->log_widths[self->log_widths_offset + num_chars];
+ }
+
+ if (wrap == PANGO_WRAP_WORD_CHAR &&
+ force_fit &&
+ break_width + break_extra_width > self->remaining_width)
+ {
+ /* Try again, with looser conditions */
+ DEBUG1 ("does not fit, try again with wrap-char");
+ wrap = PANGO_WRAP_CHAR;
+ break_num_chars = item->num_chars;
+ break_width = orig_width;
+ break_extra_width = orig_extra_width;
+ if (break_glyphs)
+ pango_glyph_string_free (break_glyphs);
+ break_glyphs = NULL;
+ goto retry_break;
+ }
+
+ if (force_fit || break_width + break_extra_width <= self->remaining_width) /* Successfully broke the item */
+ {
+ if (self->remaining_width >= 0)
+ {
+ self->remaining_width -= break_width + break_extra_width;
+ self->remaining_width = MAX (self->remaining_width, 0);
+ }
+
+ if (break_num_chars == item->num_chars)
+ {
+ if (can_break_at (self, self->start_offset + break_num_chars, wrap) &&
+ break_needs_hyphen (self, break_num_chars))
+ item->analysis.flags |= PANGO_ANALYSIS_FLAG_NEED_HYPHEN;
+
+ insert_run (self, line, item, NULL, TRUE);
+
+ if (break_glyphs)
+ pango_glyph_string_free (break_glyphs);
+
+ DEBUG1 ("all-fit '%.*s', remaining %d",
+ item->length, self->data->text + item->offset,
+ self->remaining_width);
+ return BREAK_ALL_FIT;
+ }
+ else if (break_num_chars == 0)
+ {
+ if (break_glyphs)
+ pango_glyph_string_free (break_glyphs);
+
+ DEBUG1 ("empty-fit, remaining %d", self->remaining_width);
+ return BREAK_EMPTY_FIT;
+ }
+ else
+ {
+ PangoItem *new_item;
+
+ length = g_utf8_offset_to_pointer (self->data->text + item->offset, break_num_chars) - (self->data->text + item->offset);
+
+ new_item = pango_item_split (item, length, break_num_chars);
+
+ insert_run (self, line, new_item, break_glyphs, FALSE);
+
+ self->log_widths_offset += break_num_chars;
+
+ /* Shaped items should never be broken */
+ g_assert (!shape_set);
+
+ DEBUG1 ("some-fit '%.*s', remaining %d",
+ new_item->length, self->data->text + new_item->offset,
+ self->remaining_width);
+ return BREAK_SOME_FIT;
+ }
+ }
+ else
+ {
+ pango_glyph_string_free (self->glyphs);
+ self->glyphs = NULL;
+
+ if (break_glyphs)
+ pango_glyph_string_free (break_glyphs);
+
+ DEBUG1 ("none-fit, remaining %d", self->remaining_width);
+ return BREAK_NONE_FIT;
+ }
+}
+
+static void
+process_line (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ gboolean have_break = FALSE; /* If we've seen a possible break yet */
+ int break_remaining_width = 0; /* Remaining width before adding run with break */
+ int break_start_offset = 0; /* Start offset before adding run with break */
+ GSList *break_link = NULL; /* Link holding run before break */
+ gboolean wrapped = FALSE;
+
+ while (self->items)
+ {
+ PangoItem *item = self->items->data;
+ BreakResult result;
+ int old_num_chars;
+ int old_remaining_width;
+ gboolean first_item_in_line;
+ gboolean last_item_in_line;
+
+ old_num_chars = item->num_chars;
+ old_remaining_width = self->remaining_width;
+ first_item_in_line = line->runs == NULL;
+ last_item_in_line = self->items->next == NULL;
+
+ result = process_item (self, line, !have_break, FALSE, last_item_in_line);
+
+ switch (result)
+ {
+ case BREAK_ALL_FIT:
+ if (self->data->text[item->offset] != '\t' &&
+ can_break_in (self, self->start_offset, old_num_chars, !first_item_in_line))
+ {
+ have_break = TRUE;
+ break_remaining_width = old_remaining_width;
+ break_start_offset = self->start_offset;
+ break_link = line->runs->next;
+ }
+
+ self->items = g_list_delete_link (self->items, self->items);
+ self->start_offset += old_num_chars;
+ break;
+
+ case BREAK_EMPTY_FIT:
+ wrapped = TRUE;
+ goto done;
+
+ case BREAK_SOME_FIT:
+ self->start_offset += old_num_chars - item->num_chars;
+ wrapped = TRUE;
+ goto done;
+
+ case BREAK_NONE_FIT:
+ /* Back up over unused runs to run where there is a break */
+ while (line->runs && line->runs != break_link)
+ {
+ PangoGlyphItem *run = line->runs->data;
+
+ /* Reset tab stat if we uninsert the current tab run */
+ if (run->glyphs == self->last_tab.glyphs)
+ {
+ self->last_tab.glyphs = NULL;
+ self->last_tab.index = 0;
+ self->last_tab.align = PANGO_TAB_LEFT;
+ }
+
+ self->items = g_list_prepend (self->items, uninsert_run (line));
+ }
+
+ self->start_offset = break_start_offset;
+ self->remaining_width = break_remaining_width;
+ last_item_in_line = self->items->next == NULL;
+
+ /* Reshape run to break */
+ item = self->items->data;
+
+ old_num_chars = item->num_chars;
+ result = process_item (self, line, TRUE, TRUE, last_item_in_line);
+ g_assert (result == BREAK_SOME_FIT || result == BREAK_EMPTY_FIT);
+
+ self->start_offset += old_num_chars - item->num_chars;
+
+ wrapped = TRUE;
+ goto done;
+
+ case BREAK_LINE_SEPARATOR:
+ self->items = g_list_delete_link (self->items, self->items);
+ self->start_offset += old_num_chars;
+ /* A line-separate is just a forced break. Set wrapped, so we justify */
+ wrapped = TRUE;
+ goto done;
+
+ case BREAK_PARAGRAPH_SEPARATOR:
+ /* We don't add the item as a run, so don't add to
+ * line->length or line->n_chars here.
+ * But we still need the next line to start after
+ * the terminators, so add to self->line_start_index
+ */
+ line->ends_paragraph = TRUE;
+ self->line_start_index += item->length;
+ self->start_offset += item->num_chars;
+ self->items = g_list_delete_link (self->items, self->items);
+ pango_item_free (item);
+ goto done;
+
+ default:
+ g_assert_not_reached ();
+ }
+ }
+
+done:
+ line->wrapped = wrapped;
+}
+
+/* {{{ Post-processing */
+
+static void
+add_missing_hyphen (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ PangoGlyphItem *run;
+ PangoItem *item;
+
+ if (!line->runs)
+ return;
+
+ run = line->runs->data;
+ item = run->item;
+
+ if (self->data->log_attrs[self->line_start_offset + line->n_chars].break_inserts_hyphen &&
+ !(item->analysis.flags & PANGO_ANALYSIS_FLAG_NEED_HYPHEN))
+ {
+ int width;
+ int start_offset;
+
+ DEBUG1 ("add a missing hyphen");
+
+ /* The last run fit onto the line without breaking it, but it still needs a hyphen */
+ width = pango_glyph_string_get_width (run->glyphs);
+
+ /* Ugly, shape_run uses self->start_offset, so temporarily rewind things
+ * to the state before the run was inserted. Otherwise, we end up passing
+ * the wrong log attrs to the shaping machinery.
+ */
+ start_offset = self->start_offset;
+ self->start_offset = self->line_start_offset + line->n_chars - item->num_chars;
+
+ pango_glyph_string_free (run->glyphs);
+ item->analysis.flags |= PANGO_ANALYSIS_FLAG_NEED_HYPHEN;
+ run->glyphs = shape_run (self, line, item);
+
+ self->start_offset = start_offset;
+
+ self->remaining_width += pango_glyph_string_get_width (run->glyphs) - width;
+ }
+
+ line->hyphenated = (item->analysis.flags & PANGO_ANALYSIS_FLAG_NEED_HYPHEN) != 0;
+}
+
+static void
+zero_line_final_space (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ PangoGlyphItem *run;
+ PangoItem *item;
+ PangoGlyphString *glyphs;
+ int glyph;
+
+ if (!line->runs)
+ return;
+
+ run = line->runs->data;
+ item = run->item;
+
+ glyphs = run->glyphs;
+ glyph = item->analysis.level % 2 ? 0 : glyphs->num_glyphs - 1;
+
+ if (glyphs->glyphs[glyph].glyph == PANGO_GET_UNKNOWN_GLYPH (0x2028))
+ {
+ DEBUG1 ("zero final space: visible space");
+ return; /* this LS is visible */
+ }
+
+ /* if the final char of line forms a cluster, and it's
+ * a whitespace char, zero its glyph's width as it's been wrapped
+ */
+ if (glyphs->num_glyphs < 1 || self->start_offset == 0 ||
+ !self->data->log_attrs[self->start_offset - 1].is_white)
+ {
+ DEBUG1 ("zero final space: not whitespace");
+ return;
+ }
+
+ if (glyphs->num_glyphs >= 2 &&
+ glyphs->log_clusters[glyph] == glyphs->log_clusters[glyph + (item->analysis.level % 2 ? 1 : -1)])
+ {
+
+ DEBUG1 ("zero final space: its a cluster");
+ return;
+ }
+
+ DEBUG1 ("zero line final space: collapsing the space");
+ glyphs->glyphs[glyph].geometry.width = 0;
+ glyphs->glyphs[glyph].glyph = PANGO_GLYPH_EMPTY;
+}
+
+/* When doing shaping, we add the letter spacing value for a
+ * run after every grapheme in the run. This produces ugly
+ * asymmetrical results, so what this routine is redistributes
+ * that space to the beginning and the end of the run.
+ *
+ * We also trim the letter spacing from runs adjacent to
+ * tabs and from the outside runs of the lines so that things
+ * line up properly. The line breaking and tab positioning
+ * were computed without this trimming so they are no longer
+ * exactly correct, but this won't be very noticeable in most
+ * cases.
+ */
+static void
+adjust_line_letter_spacing (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ gboolean reversed;
+ PangoGlyphItem *last_run;
+ int tab_adjustment;
+ GSList *l;
+
+ /* If we have tab stops and the resolved direction of the
+ * line is RTL, then we need to walk through the line
+ * in reverse direction to figure out the corrections for
+ * tab stops.
+ */
+ reversed = FALSE;
+ if (line->direction == PANGO_DIRECTION_RTL)
+ {
+ for (l = line->runs; l; l = l->next)
+ if (is_tab_run (line, l->data))
+ {
+ line->runs = g_slist_reverse (line->runs);
+ reversed = TRUE;
+ break;
+ }
+ }
+ /* Walk over the runs in the line, redistributing letter
+ * spacing from the end of the run to the start of the
+ * run and trimming letter spacing from the ends of the
+ * runs adjacent to the ends of the line or tab stops.
+ *
+ * We accumulate a correction factor from this trimming
+ * which we add onto the next tab stop space to keep the
+ * things properly aligned.
+ */
+ last_run = NULL;
+ tab_adjustment = 0;
+ for (l = line->runs; l; l = l->next)
+ {
+ PangoGlyphItem *run = l->data;
+ PangoGlyphItem *next_run = l->next ? l->next->data : NULL;
+
+ if (is_tab_run (line, run))
+ {
+ pad_glyphstring_right (self, run->glyphs, tab_adjustment);
+ tab_adjustment = 0;
+ }
+ else
+ {
+ PangoGlyphItem *visual_next_run = reversed ? last_run : next_run;
+ PangoGlyphItem *visual_last_run = reversed ? next_run : last_run;
+ int run_spacing = get_item_letter_spacing (run->item);
+ int space_left, space_right;
+
+ distribute_letter_spacing (run_spacing, &space_left, &space_right);
+
+ if (run->glyphs->glyphs[0].geometry.width == 0)
+ {
+ /* we've zeroed this space glyph at the end of line, now remove
+ * the letter spacing added to its adjacent glyph
+ */
+ pad_glyphstring_left (self, run->glyphs, - space_left);
+ }
+ else if (!visual_last_run || is_tab_run (line, visual_last_run))
+ {
+ pad_glyphstring_left (self, run->glyphs, - space_left);
+ tab_adjustment += space_left;
+ }
+
+ if (run->glyphs->glyphs[run->glyphs->num_glyphs - 1].geometry.width == 0)
+ {
+ /* we've zeroed this space glyph at the end of line, now remove
+ * the letter spacing added to its adjacent glyph
+ */
+ pad_glyphstring_right (self, run->glyphs, - space_right);
+ }
+
+ else if (!visual_next_run || is_tab_run (line, visual_next_run))
+ {
+ pad_glyphstring_right (self, run->glyphs, - space_right);
+ tab_adjustment += space_right;
+ }
+ }
+
+ last_run = run;
+ }
+
+ if (reversed)
+ line->runs = g_slist_reverse (line->runs);
+}
+
+typedef struct {
+ PangoAttribute *attr;
+ int x_offset;
+ int y_offset;
+} BaselineItem;
+
+static void
+collect_baseline_shift (PangoLineBreaker *self,
+ PangoItem *item,
+ PangoItem *prev,
+ int *start_x_offset,
+ int *start_y_offset,
+ int *end_x_offset,
+ int *end_y_offset)
+{
+ *start_x_offset = 0;
+ *start_y_offset = 0;
+ *end_x_offset = 0;
+ *end_y_offset = 0;
+
+ for (GSList *l = item->analysis.extra_attrs; l; l = l->next)
+ {
+ PangoAttribute *attr = l->data;
+
+ if (attr->klass->type == PANGO_ATTR_RISE)
+ {
+ int value = ((PangoAttrInt *)attr)->value;
+
+ *start_y_offset += value;
+ *end_y_offset -= value;
+ }
+ else if (attr->klass->type == PANGO_ATTR_BASELINE_SHIFT)
+ {
+ if (attr->start_index == item->offset)
+ {
+ BaselineItem *entry;
+ int value;
+
+ entry = g_new0 (BaselineItem, 1);
+ entry->attr = attr;
+ self->baseline_shifts = g_list_prepend (self->baseline_shifts, entry);
+
+ value = ((PangoAttrInt *)attr)->value;
+
+ if (value > 1024 || value < -1024)
+ {
+ entry->y_offset = value;
+ /* FIXME: compute an x_offset from value to italic angle */
+ }
+ else
+ {
+ int superscript_x_offset = 0;
+ int superscript_y_offset = 0;
+ int subscript_x_offset = 0;
+ int subscript_y_offset = 0;
+
+
+ if (prev)
+ {
+ hb_font_t *hb_font = pango_font_get_hb_font (prev->analysis.font);
+ hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUPERSCRIPT_EM_Y_OFFSET, &superscript_y_offset);
+ hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUPERSCRIPT_EM_X_OFFSET, &superscript_x_offset);
+ hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUBSCRIPT_EM_Y_OFFSET, &subscript_y_offset);
+ hb_ot_metrics_get_position (hb_font, HB_OT_METRICS_TAG_SUBSCRIPT_EM_X_OFFSET, &subscript_x_offset);
+ }
+
+ if (superscript_y_offset == 0)
+ superscript_y_offset = 5000;
+ if (subscript_y_offset == 0)
+ subscript_y_offset = 5000;
+
+ switch (value)
+ {
+ case PANGO_BASELINE_SHIFT_NONE:
+ entry->x_offset = 0;
+ entry->y_offset = 0;
+ break;
+ case PANGO_BASELINE_SHIFT_SUPERSCRIPT:
+ entry->x_offset = superscript_x_offset;
+ entry->y_offset = superscript_y_offset;
+ break;
+ case PANGO_BASELINE_SHIFT_SUBSCRIPT:
+ entry->x_offset = subscript_x_offset;
+ entry->y_offset = -subscript_y_offset;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+ }
+
+ *start_x_offset += entry->x_offset;
+ *start_y_offset += entry->y_offset;
+ }
+
+ if (attr->end_index == item->offset + item->length)
+ {
+ GList *t;
+
+ for (t = self->baseline_shifts; t; t = t->next)
+ {
+ BaselineItem *entry = t->data;
+
+ if (attr->start_index == entry->attr->start_index &&
+ attr->end_index == entry->attr->end_index &&
+ ((PangoAttrInt *)attr)->value == ((PangoAttrInt *)entry->attr)->value)
+ {
+ *end_x_offset -= entry->x_offset;
+ *end_y_offset -= entry->y_offset;
+ }
+
+ self->baseline_shifts = g_list_remove (self->baseline_shifts, entry);
+ g_free (entry);
+ break;
+ }
+ if (t == NULL)
+ g_warning ("Baseline attributes mismatch\n");
+ }
+ }
+ }
+}
+
+static void
+apply_baseline_shift (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ int y_offset = 0;
+ PangoItem *prev = NULL;
+
+ for (GSList *l = line->runs; l; l = l->next)
+ {
+ PangoGlyphItem *run = l->data;
+ PangoItem *item = run->item;
+ int start_x_offset, end_x_offset;
+ int start_y_offset, end_y_offset;
+
+ collect_baseline_shift (self, item, prev, &start_x_offset, &start_y_offset, &end_x_offset, &end_y_offset);
+
+ y_offset += start_y_offset;
+
+ run->y_offset = y_offset;
+ run->start_x_offset = start_x_offset;
+ run->end_x_offset = end_x_offset;
+
+ y_offset += end_y_offset;
+
+ prev = item;
+ }
+}
+
+static void
+apply_render_attributes (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ GSList *runs;
+
+ if (!self->render_attrs)
+ return;
+
+ runs = g_slist_reverse (line->runs);
+ line->runs = NULL;
+
+ for (GSList *l = runs; l; l = l->next)
+ {
+ PangoGlyphItem *glyph_item = l->data;
+ GSList *new_runs;
+
+ new_runs = pango_glyph_item_apply_attrs (glyph_item,
+ line->data->text,
+ self->render_attrs);
+
+ line->runs = g_slist_concat (new_runs, line->runs);
+ }
+
+ g_slist_free (runs);
+}
+
+static void
+postprocess_line (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ add_missing_hyphen (self, line);
+
+ /* Truncate the logical-final whitespace in the line if we broke the line at it */
+ if (line->wrapped)
+ zero_line_final_space (self, line);
+
+ line->runs = g_slist_reverse (line->runs);
+
+ apply_baseline_shift (self, line);
+
+ if (should_ellipsize_current_line (self, line))
+ pango_line_ellipsize (line, self->context, self->line_ellipsize, self->line_width);
+
+ /* Now convert logical to visual order */
+ pango_line_reorder (line);
+
+ /* Fixup letter spacing between runs */
+ adjust_line_letter_spacing (self, line);
+
+ apply_render_attributes (self, line);
+}
+
+/* }}} */
+/* }}} */
+/* {{{ PangoLineBreaker implementation */
+
+G_DEFINE_TYPE (PangoLineBreaker, pango_line_breaker, G_TYPE_OBJECT)
+
+enum {
+ PROP_CONTEXT = 1,
+ PROP_BASE_DIR,
+ PROP_TABS,
+ N_PROPERTIES
+};
+
+static GParamSpec *properties[N_PROPERTIES];
+
+static void
+pango_line_breaker_init (PangoLineBreaker *self)
+{
+ self->tabs = NULL;
+ self->tab_width = -1;
+ self->decimal = 0;
+ self->n_lines = 0;
+}
+
+static void
+pango_line_breaker_finalize (GObject *object)
+{
+ PangoLineBreaker *self = PANGO_LINE_BREAKER (object);
+
+ g_list_free_full (self->baseline_shifts, g_free);
+ g_clear_pointer (&self->glyphs, pango_glyph_string_free);
+ g_clear_pointer (&self->log_widths, g_free);
+ g_list_free_full (self->items, (GDestroyNotify) pango_item_free);
+ g_clear_pointer (&self->data, line_data_unref);
+ g_list_free_full (self->data_items, (GDestroyNotify) pango_item_free);
+ g_clear_pointer (&self->render_attrs, pango_attr_list_unref);
+ g_slist_free_full (self->datas, (GDestroyNotify) line_data_unref);
+ g_clear_pointer (&self->tabs, pango_tab_array_free);
+ g_object_unref (self->context);
+
+ G_OBJECT_CLASS (pango_line_breaker_parent_class)->finalize (object);
+}
+
+static void
+pango_line_breaker_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ PangoLineBreaker *self = PANGO_LINE_BREAKER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONTEXT:
+ g_assert (self->context == NULL);
+ self->context = g_value_dup_object (value);
+ break;
+
+ case PROP_BASE_DIR:
+ pango_line_breaker_set_base_dir (self, g_value_get_enum (value));
+ break;
+
+ case PROP_TABS:
+ pango_line_breaker_set_tabs (self, g_value_get_boxed (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+pango_line_breaker_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ PangoLineBreaker *self = PANGO_LINE_BREAKER (object);
+
+ switch (prop_id)
+ {
+ case PROP_CONTEXT:
+ g_value_set_object (value, self->context);
+ break;
+
+ case PROP_BASE_DIR:
+ g_value_set_enum (value, pango_line_breaker_get_base_dir (self));
+ break;
+
+ case PROP_TABS:
+ g_value_set_boxed (value, pango_line_breaker_get_tabs (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+pango_line_breaker_class_init (PangoLineBreakerClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->finalize = pango_line_breaker_finalize;
+ object_class->set_property = pango_line_breaker_set_property;
+ object_class->get_property = pango_line_breaker_get_property;
+
+ /**
+ * PangoLineBreaker:context: (attributes org.gtk.Property.get=pango_line_breaker_get_context)
+ *
+ * The context for the `PangoLineBreaker`.
+ */
+ properties[PROP_CONTEXT] =
+ g_param_spec_object ("context", "context", "context",
+ PANGO_TYPE_CONTEXT,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * PangoLineBreaker:base-dir: (attributes org.gtk.Property.get=pango_line_breaker_get_base_dir org.gtk.Property.set=pango_line_breaker_set_base_dir)
+ *
+ * The base direction for the `PangoLineBreaker`.
+ *
+ * The default value is `PANGO_DIRECTION_NEUTRAL`.
+ */
+ properties[PROP_BASE_DIR] =
+ g_param_spec_enum ("base-dir", "base-dir", "base-dir",
+ PANGO_TYPE_DIRECTION,
+ PANGO_DIRECTION_NEUTRAL,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * PangoLineBreaker:tabs: (attributes org.gtk.Property.get=pango_line_breaker_get_tabs org.gtk.Property.set=pango_line_breaker_set_tabs)
+ *
+ * The tabs to use when formatting the next line of the `PangoLineBreaker`.
+ *
+ * `PangoLineBreaker` will place content at the next tab position
+ * whenever it meets a Tab character (U+0009).
+ */
+ properties[PROP_TABS] =
+ g_param_spec_boxed ("tabs", "tabs", "tabs",
+ PANGO_TYPE_TAB_ARRAY,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, N_PROPERTIES, properties);
+}
+
+/* }}} */
+/* {{{ Public API */
+
+/**
+ * pango_line_breaker_new:
+ * @context: a `PangoContext`
+ *
+ * Creates a new `PangoLineBreaker`.
+ *
+ * Returns: a newly created `PangoLineBreaker`
+ */
+PangoLineBreaker *
+pango_line_breaker_new (PangoContext *context)
+{
+ g_return_val_if_fail (PANGO_IS_CONTEXT (context), NULL);
+
+ return g_object_new (PANGO_TYPE_LINE_BREAKER, "context", context, NULL);
+}
+
+/**
+ * pango_line_breaker_get_context:
+ * @self: a `PangoLineBreaker`
+ *
+ * Retrieves the context used for this `PangoLineBreaker`.
+ *
+ * Returns: (transfer none): the `PangoContext` for @self
+ */
+PangoContext *
+pango_line_breaker_get_context (PangoLineBreaker *self)
+{
+ g_return_val_if_fail (PANGO_IS_LINE_BREAKER (self), NULL);
+
+ return self->context;
+}
+
+/**
+ * pango_line_breaker_set_tabs:
+ * @self: a `PangoLineBreaker`
+ * @tabs: (nullable): a `PangoTabArray`
+ *
+ * Sets the tab positions to use for lines.
+ *
+ * `PangoLineBreaker` will place content at the next tab position
+ * whenever it meets a Tab character (U+0009).
+ *
+ * By default, tabs are every 8 spaces. If @tabs is %NULL, the
+ * default tabs are reinstated. @tabs is copied by @self, you
+ * must free your copy of @tabs yourself.
+ *
+ * Note that tabs and justification conflict with each other:
+ * Justification will move content away from its tab-aligned
+ * positions. The same is true for alignments other than
+ * %PANGO_ALIGNMENT_LEFT.
+ */
+void
+pango_line_breaker_set_tabs (PangoLineBreaker *self,
+ PangoTabArray *tabs)
+{
+ g_return_if_fail (PANGO_IS_LINE_BREAKER (self));
+
+ if (self->tabs)
+ {
+ pango_tab_array_free (self->tabs);
+ self->tabs = NULL;
+ }
+
+ if (tabs)
+ {
+ self->tabs = pango_tab_array_copy (tabs);
+ pango_tab_array_sort (self->tabs);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_TABS]);
+}
+
+/**
+ * pango_line_breaker_get_tabs:
+ * @self: a `PangoLineBreaker`
+ *
+ * Gets the current `PangoTabArray` used by this `PangoLineBreaker`.
+ *
+ * If no `PangoTabArray` has been set, then the default tabs are
+ * in use and %NULL is returned. Default tabs are every 8 spaces.
+ *
+ * Return value: (transfer none) (nullable): the tabs for @self
+ */
+PangoTabArray *
+pango_line_breaker_get_tabs (PangoLineBreaker *self)
+{
+ g_return_val_if_fail (PANGO_IS_LINE_BREAKER (self), NULL);
+
+ if (self->tabs)
+ return self->tabs;
+ else
+ return NULL;
+}
+
+/**
+ * pango_line_breaker_set_base_dir:
+ * @self: a `PangoLineBreaker`
+ * @direction: the direction
+ *
+ * Sets the base direction for lines produced by this `PangoLineBreaker`.
+ *
+ * If @direction is `PANGO_DIRECTION_NEUTRAL`, the direction is determined
+ * from the content. This is the default behavior.
+ */
+void
+pango_line_breaker_set_base_dir (PangoLineBreaker *self,
+ PangoDirection direction)
+{
+ g_return_if_fail (PANGO_IS_LINE_BREAKER (self));
+
+ if (self->base_dir == direction)
+ return;
+
+ self->base_dir = direction;
+
+ g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BASE_DIR]);
+}
+
+/**
+ * pango_line_breaker_get_base_dir:
+ * @self: a `PangoLineBreaker`
+ *
+ * Gets the base direction for lines produced by this `PangoLineBreaker`.
+ *
+ * See [method@Pango.LineBreaker.set_base_dir].
+ */
+PangoDirection
+pango_line_breaker_get_base_dir (PangoLineBreaker *self)
+{
+ g_return_val_if_fail (PANGO_IS_LINE_BREAKER (self), PANGO_DIRECTION_NEUTRAL);
+
+ return self->base_dir;
+}
+
+/**
+ * pango_line_breaker_add_text:
+ * @text: the text to break into lines
+ * @length: length of @text in bytes, or -1 if @text is nul-terminated
+ * @attrs: (nullable): a `PangoAttrList` with attributes for @text, or `NULL`
+ *
+ * Provides input that the `PangoLineBreaker` should break into lines.
+ *
+ * It is possible to call this function repeatedly to add more
+ * input to an existing `PangoLineBreaker`.
+ *
+ * The end of @text is treated as a paragraph break.
+ */
+void
+pango_line_breaker_add_text (PangoLineBreaker *self,
+ const char *text,
+ int length,
+ PangoAttrList *attrs)
+{
+ g_return_if_fail (PANGO_IS_LINE_BREAKER (self));
+ g_return_if_fail (text != NULL);
+
+ self->datas = g_slist_append (self->datas, make_line_data (self, text, length, attrs));
+}
+
+/**
+ * pango_line_breaker_get_direction:
+ * @self: a `PangoLineBreaker`
+ *
+ * Obtains the resolved direction for the next line.
+ *
+ * If the `PangoLineBreaker` has no more input, then
+ * `PANGO_DIRECTION_NEUTRAL` is returned.
+ *
+ * Returns: the resolved direction of the next line.
+ */
+PangoDirection
+pango_line_breaker_get_direction (PangoLineBreaker *self)
+{
+ g_return_val_if_fail (PANGO_IS_LINE_BREAKER (self), PANGO_DIRECTION_NEUTRAL);
+
+ return get_resolved_dir (self);
+}
+
+/**
+ * pango_line_breaker_done:
+ * @self: a `PangoLineBreaker`
+ *
+ * Returns whether the `PangoLineBreaker` has any text left to process.
+ *
+ * Returns: TRUE if there are more lines.
+ */
+gboolean
+pango_line_breaker_done (PangoLineBreaker *self)
+{
+ g_return_val_if_fail (PANGO_IS_LINE_BREAKER (self), TRUE);
+
+ ensure_items (self);
+
+ return self->items == NULL;
+}
+
+/**
+ * pango_line_breaker_next_line:
+ * @self: a `PangoLineBreaker`
+ * @x: the X position for the line, in Pango units
+ * @width: the width for the line, or -1 for no limit, in Pango units
+ * @wrap: how to wrap the text
+ * @ellipsize: whether to ellipsize the text
+ *
+ * Gets the next line.
+ *
+ * The `PangoLineBreaker` will use as much of its unprocessed text
+ * as will fit into @width. The @x position is used to determine
+ * where tabs are located are.
+ *
+ * If @ellipsize is not `PANGO_ELLIPSIZE_NONE`, then all unprocessed
+ * text will be made to fit by ellipsizing.
+ *
+ * Note that the line is not positioned - the leftmost point of its baseline
+ * is at 0, 0. See [class@Pango.Lines] for a way to hold a list of positioned
+ * `PangoLine` objects.
+ *
+ * line = pango_line_breaker_next_line (breaker,
+ * x, width,
+ * PANGO_WRAP_MODE,
+ * PANGO_ELLIPSIZE_NONE);
+ * pango_line_get_extents (line, &ext);
+ * line = pango_line_justify (line, width);
+ * pango_lines_add_line (lines, line, x, y - ext.y);
+ *
+ * Returns: (transfer full) (nullable): the next line, or `NULL`
+ * if @self has no more input
+ */
+PangoLine *
+pango_line_breaker_next_line (PangoLineBreaker *self,
+ int x,
+ int width,
+ PangoWrapMode wrap,
+ PangoEllipsizeMode ellipsize)
+{
+ PangoLine *line;
+
+ g_return_val_if_fail (PANGO_IS_LINE_BREAKER (self), NULL);
+
+ ensure_items (self);
+
+ if (!self->items)
+ return NULL;
+
+ line = pango_line_new (self->context, self->data);
+
+ line->start_index = self->line_start_index;
+ line->start_offset = self->line_start_offset;
+ line->starts_paragraph = self->at_paragraph_start;
+ line->direction = get_resolved_dir (self);
+
+ self->line_x = x;
+ self->line_width = width;
+ self->line_wrap = wrap;
+ self->line_ellipsize = ellipsize;
+
+ self->last_tab.glyphs = NULL;
+ self->last_tab.index = 0;
+ self->last_tab.align = PANGO_TAB_LEFT;
+
+ if (should_ellipsize_current_line (self, line))
+ self->remaining_width = -1;
+ else
+ self->remaining_width = width;
+
+ process_line (self, line);
+
+ line->n_chars = compute_n_chars (line);
+
+ postprocess_line (self, line);
+
+ if (!self->items)
+ line->ends_paragraph = TRUE;
+
+ self->at_paragraph_start = line->ends_paragraph;
+ self->n_lines++;
+ self->line_start_index += line->length;
+ self->line_start_offset = self->start_offset;
+
+ if (self->items == NULL)
+ {
+ g_clear_pointer (&self->data, line_data_unref);
+ g_list_free_full (self->data_items, (GDestroyNotify) pango_item_free);
+ self->data_items = NULL;
+ g_clear_pointer (&self->render_attrs, pango_attr_list_unref);
+ }
+
+ pango_line_check_invariants (line);
+
+ return line;
+}
+
+/**
+ * pango_line_breaker_undo_line:
+ * @self: a `PangoLineBreaker`
+ * @line: (transfer none): the most recent line produced by @self
+ *
+ * Re-adds the content of @line to the unprocessed
+ * input of the `PangoLineBreaker`.
+ *
+ * This can be used to try this line again with
+ * different parameters passed to
+ * [method@Pango.LineBreaker.next_line].
+ *
+ * When undoing multiple lines, they have to be
+ * undone in the reverse order in which they
+ * were produced.
+ *
+ * Returns: `TRUE` on success, `FALSE` if Pango
+ * determines that the line can't be undone
+ */
+gboolean
+pango_line_breaker_undo_line (PangoLineBreaker *self,
+ PangoLine *line)
+{
+ if (self->data == NULL &&
+ line->start_index == 0 && line->length == line->data->length)
+ {
+ g_assert (self->items == NULL);
+ self->datas = g_slist_prepend (self->datas, line_data_ref (line->data));
+
+ self->n_lines--;
+
+ /* ensure_items will set up everything else */
+
+ g_clear_pointer (&self->glyphs, pango_glyph_string_free);
+
+ return TRUE;
+ }
+
+ if (self->data == line->data &&
+ self->line_start_index == line->start_index + line->length)
+ {
+ GList *items = NULL;
+
+ /* recover the original items */
+ for (GList *l = self->data_items; l; l = l->next)
+ {
+ PangoItem *item = l->data;
+
+ if (item->offset + item->length < line->start_index)
+ continue;
+
+ if (item->offset > self->line_start_index)
+ break;
+
+ item = pango_item_copy (item);
+
+ if (item->offset < line->start_index)
+ {
+ PangoItem *new_item;
+ int n_chars;
+
+ n_chars = g_utf8_strlen (self->data->text + item->offset, line->start_index - item->offset);
+ new_item = pango_item_split (item, line->start_index - item->offset, n_chars);
+ pango_item_free (new_item);
+ }
+
+ if (item->offset + item->length > self->line_start_index)
+ {
+ PangoItem *new_item;
+ int n_chars;
+
+ n_chars = g_utf8_strlen (self->data->text + item->offset, self->line_start_index - item->offset);
+ new_item = pango_item_split (item, self->line_start_index - item->offset, n_chars);
+ pango_item_free (item);
+ item = new_item;
+ }
+
+ items = g_list_prepend (items, item);
+ }
+
+ self->items = g_list_concat (g_list_reverse (items), self->items);
+
+ self->n_lines--;
+
+ self->at_paragraph_start = line->starts_paragraph;
+ self->line_start_index = line->start_index;
+ self->line_start_offset = line->start_offset;
+
+ g_clear_pointer (&self->glyphs, pango_glyph_string_free);
+ self->start_offset = line->start_offset;
+ self->log_widths_offset = 0;
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* }}} */
+
+/* vim:set foldmethod=marker expandtab: */
diff --git a/pango/pango-line-breaker.h b/pango/pango-line-breaker.h
new file mode 100644
index 00000000..050d98ff
--- /dev/null
+++ b/pango/pango-line-breaker.h
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <glib-object.h>
+#include <pango/pango-types.h>
+#include <pango/pango-break.h>
+#include <pango/pango-layout.h>
+#include <pango/pango-line.h>
+
+G_BEGIN_DECLS
+
+#define PANGO_TYPE_LINE_BREAKER pango_line_breaker_get_type ()
+
+PANGO_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (PangoLineBreaker, pango_line_breaker, PANGO, LINE_BREAKER, GObject);
+
+PANGO_AVAILABLE_IN_ALL
+PangoLineBreaker * pango_line_breaker_new (PangoContext *context);
+
+PANGO_AVAILABLE_IN_ALL
+PangoContext * pango_line_breaker_get_context (PangoLineBreaker *self);
+
+PANGO_AVAILABLE_IN_ALL
+void pango_line_breaker_set_tabs (PangoLineBreaker *self,
+ PangoTabArray *tabs);
+PANGO_AVAILABLE_IN_ALL
+PangoTabArray * pango_line_breaker_get_tabs (PangoLineBreaker *self);
+
+PANGO_AVAILABLE_IN_ALL
+void pango_line_breaker_set_base_dir (PangoLineBreaker *self,
+ PangoDirection direction);
+PANGO_AVAILABLE_IN_ALL
+PangoDirection pango_line_breaker_get_base_dir (PangoLineBreaker *self);
+
+PANGO_AVAILABLE_IN_ALL
+void pango_line_breaker_add_text (PangoLineBreaker *self,
+ const char *text,
+ int length,
+ PangoAttrList *attrs);
+
+PANGO_AVAILABLE_IN_ALL
+PangoDirection pango_line_breaker_get_direction (PangoLineBreaker *self);
+
+PANGO_AVAILABLE_IN_ALL
+gboolean pango_line_breaker_done (PangoLineBreaker *self);
+
+PANGO_AVAILABLE_IN_ALL
+PangoLine * pango_line_breaker_next_line (PangoLineBreaker *self,
+ int x,
+ int width,
+ PangoWrapMode wrap,
+ PangoEllipsizeMode ellipsize);
+
+PANGO_AVAILABLE_IN_ALL
+gboolean pango_line_breaker_undo_line (PangoLineBreaker *self,
+ PangoLine *line);
+
+G_END_DECLS
diff --git a/pango/pango.h b/pango/pango.h
index 702ec4fa..99268717 100644
--- a/pango/pango.h
+++ b/pango/pango.h
@@ -44,6 +44,7 @@
#include <pango/pango-layout.h>
#include <pango/pango-layout-run.h>
#include <pango/pango-line.h>
+#include <pango/pango-line-breaker.h>
#include <pango/pango-matrix.h>
#include <pango/pango-markup.h>
#include <pango/pango-renderer.h>