text-align: justify
Giacomo Cariello <[email protected]> Fri, 05 Aug 2005 12:43:52 +0200
| Newsgroups | gmane.text.xml.xmlroff.general |
|---|---|
| Organization | BSD Users Group Italia |
| Message-ID | <[email protected]> |
Dear xmlroff list,
I'm posting here a brief summary of what I found out recently regarding
justification of paragraphs (sorry Tony, I didn't notice your request at
first, due to scrolling-hurry):
1) Since Pango didn't support justification natively, Damon Chaplin has
written a patch which adds justification, however the initial patch had
some problems, so he sent me a new one which should be almost final (in
attachment). The full justification story can be found here:
http://bugzilla.gnome.org/show_bug.cgi?id=64538
2) After applying the above patch, xmlroff could be modified to support
justification, so I wrote a small patch (in attachment). Please notice
that text-alignment and direction is still in early stage and most
combinations aren't supported. I suppose xmlroff needs some kind of
masterplan to face all the possible orientations/alignments. I'm willing
to contribute, however I need some time and training on pango etc. ;-)
3) The result isn't completely satisfactory due to the following problem:
(pseudo-quoting from a private mail to Tony Graham)
---> snip <---
When a 3-line paragraph is compressed into a 2-line paragraph, the
following assertion shows up:
process:26210): libfo-CRITICAL **: fo_doc_gp_do_callbacks: assertion
`line_last >= line_first && line_last <= g_slist_length
(pango_layout_get_lines (layout)) - 1' failed
(process:26210): libfo-CRITICAL **: fo_doc_gp_do_callbacks: assertion
`line_last >= line_first && line_last <= g_slist_length
(pango_layout_get_lines (layout)) - 1' failed
and an empty line is
showed after the paragraph. I suppose for some reason the real number of
lines isn't updated correctly. Damon tells me that he doesn't experience
the above problem using pango with GtkTextView widget, so could you
please verify how/when lines count are fetched from pango and what
actually happens if pango returns a shorter line count?
---> snip <---
Tony answers that:
---> snip <---
The assertion happens in the xmlroff code in libfo/fo-doc-gp.c:
------------------------------------------------------------
static void
fo_doc_gp_do_callbacks (GnomePrintContext *context,
PangoLayout *layout,
gint line_first,
gint line_last,
gint x,
gint y)
{
PangoLayoutIter *iter;
g_return_if_fail (context != NULL);
g_return_if_fail (PANGO_IS_LAYOUT (layout));
g_return_if_fail (line_first >= 0);
g_return_if_fail (line_last >= line_first &&
line_last <= g_slist_length (pango_layout_get_lines
(layout)) - 1);
iter = pango_layout_get_iter (layout);
gint line_number = -1;
do
{
PangoRectangle logical_rect;
PangoLayoutLine *line;
int baseline;
line_number++;
if (line_number < line_first)
{
continue;
}
line = pango_layout_iter_get_line (iter);
pango_layout_iter_get_line_extents (iter, NULL, &logical_rect);
baseline = pango_layout_iter_get_baseline (iter);
fo_doc_gp_do_line_callbacks (context,
line,
x + logical_rect.x,
y - baseline);
if (line_number >= line_last)
{
break;
}
}
while (pango_layout_iter_next_line (iter));
pango_layout_iter_free (iter);
}
------------------------------------------------------------
The assertion is to stop you trying to lay out more lines than are in the
PangoLayout.
xmlroff handles printing subsets of the lines in a PangoLayout since a
PangoLayout may be broken across a page.
I suspect that xmlroff is getting a count of the lines before the
justification code removes the last line.
I would expect that the do-while in the function only iterates for as many
lines as there are in the PangoLayout and that what you're seeing as an
extra
line is because xmlroff is leaving enough room for the original 3-line block
and not the current 2-line block.
---> snip <---
I suppose that's about all.
Sincerely,
- Giacomo Cariello
pango-justify.patch
(text/x-patch, 59.6 KB)
Index: pango/break.c
===================================================================
RCS file: /cvs/gnome/pango/pango/break.c,v
retrieving revision 1.31
diff -u -p -r1.31 break.c
--- pango/break.c 18 Jun 2005 05:40:00 -0000 1.31
+++ pango/break.c 4 Aug 2005 11:02:00 -0000
@@ -374,6 +374,25 @@ static const int line_break_indexes[] =
#define HANGUL(wc) ((wc) >= 0xAC00 && (wc) <= 0xD7A3)
#define BACKSPACE_DELETES_CHARACTER(wc) (!LATIN (wc) && !CYRILLIC (wc) && !GREEK (wc) && !KANA(wc) && !HANGUL(wc))
+/* Unicode 6.1 says the fixed width spaces U+2000-U+200A do not expand
+ on justification (U+2009 THIN SPACE does sometimes, but not here!).
+ FIXME: U+1361 ETHIOPIC WORDSPACE is like a ':' between words and expands
+ when justified, but is not elided at ends of lines, so would need slight
+ changes to the calculations, as half the space would be trimmed (#138223).
+ FIXME: U+1680 OGHAM SPACE MARK is rendered as a glyph between words, but
+ is elided at ends of lines (Unicode TR14). Should it expand? If it does,
+ we should probably move it so the glyph is in the middle.
+ FIXME: U+3000 IDEOGRAPHIC SPACE is fixed to the width of an ideograph, so
+ probably shouldn't expand, so long as we do letter-spacing (though I'm not
+ 100% sure).
+ FIXME: U+200B ZERO WIDTH SPACE may expand when letter-spacing Thai (13.2).
+
+ U+0020 is SPACE, the normal ASCII space character, ' '.
+ U+00A0 is NO-BREAK SPACE.
+ U+202F is NARROW NO-BREAK SPACE.
+ U+205F is MEDIUM MATHEMATICAL SPACE. */
+#define IS_RESIZABLE(wc) \
+ ((wc) == 0x0020 || (wc) == 0x00A0 || (wc) == 0x202F || (wc) == 0x205F)
/* p. 132-133 of Unicode spec table 5-6 will help understand this */
typedef enum
@@ -538,6 +557,7 @@ pango_default_break (const gchar *text
*/
attrs[i].is_white = g_unichar_isspace (wc);
+ attrs[i].is_resizable = IS_RESIZABLE (wc);
/* ---- Cursor position breaks (Grapheme breaks) ---- */
Index: pango/pango-break.h
===================================================================
RCS file: /cvs/gnome/pango/pango/pango-break.h,v
retrieving revision 1.6
diff -u -p -r1.6 pango-break.h
--- pango/pango-break.h 14 Aug 2003 16:33:36 -0000 1.6
+++ pango/pango-break.h 4 Aug 2005 11:02:00 -0000
@@ -69,6 +69,10 @@ struct _PangoLogAttr
* the entire grapheme cluster
*/
guint backspace_deletes_character : 1;
+
+ /* If the character can be stretched and shrunk for justification, typically
+ a space character. */
+ guint is_resizable : 1;
};
/* Determine information about cluster/word/line breaks in a string
Index: pango/pango-layout-private.h
===================================================================
RCS file: /cvs/gnome/pango/pango/pango-layout-private.h,v
retrieving revision 1.1
diff -u -p -r1.1 pango-layout-private.h
--- pango/pango-layout-private.h 14 Jul 2004 22:17:35 -0000 1.1
+++ pango/pango-layout-private.h 4 Aug 2005 11:02:00 -0000
@@ -61,6 +61,8 @@ struct _PangoLayout
PangoTabArray *tabs;
GSList *lines;
+
+ PangoJustification justification;
};
G_END_DECLS
Index: pango/pango-layout.c
===================================================================
RCS file: /cvs/gnome/pango/pango/pango-layout.c,v
retrieving revision 1.140
diff -u -p -r1.140 pango-layout.c
--- pango/pango-layout.c 26 Jul 2005 18:07:59 -0000 1.140
+++ pango/pango-layout.c 4 Aug 2005 11:02:02 -0000
@@ -27,6 +27,11 @@
#include "pango-layout-private.h"
+/* Define this to get debugging output about potential breaks. */
+/*#define DEBUG_BREAKS*/
+/* Define this to get debugging output about line overflows. */
+/*#define DEBUG_LINE_OVERFLOW*/
+
#define LINE_IS_VALID(line) ((line)->layout != NULL)
typedef struct _Extents Extents;
@@ -135,6 +140,13 @@ static void pango_layout_get_item_proper
static void pango_layout_finalize (GObject *object);
+static void pango_layout_paragraph (PangoLayout *layout,
+ GList *items,
+ PangoDirection base_dir,
+ gint start_char_offset,
+ PangoAttrList *attrs,
+ GSList **lines);
+
G_DEFINE_TYPE (PangoLayout, pango_layout, G_TYPE_OBJECT)
static void
@@ -149,6 +161,7 @@ pango_layout_init (PangoLayout *layout)
layout->alignment = PANGO_ALIGN_LEFT;
layout->justify = FALSE;
+ layout->justification = PANGO_JUSTIFICATION_NONE;
layout->auto_dir = TRUE;
layout->log_attrs = NULL;
@@ -528,12 +541,10 @@ pango_layout_get_font_description (Pango
* @layout: a #PangoLayout
* @justify: whether the lines in the layout should be justified.
*
- * Sets whether or not each complete line should be stretched to
- * fill the entire width of the layout. This stretching is typically
- * done by adding whitespace, but for some scripts (such as Arabic),
- * the justification is done by extending the characters.
+ * Sets whether or not the lines in the layout are justified.
*
- * Note that as of Pango-1.4, this functionality is not yet implemented.
+ * Deprecated: This function was never implemented. Use
+ * pango_layout_set_justification() instead.
**/
void
pango_layout_set_justify (PangoLayout *layout,
@@ -548,10 +559,12 @@ pango_layout_set_justify (PangoLayout *l
* pango_layout_get_justify:
* @layout: a #PangoLayout
*
- * Gets whether or not each complete line should be stretched to
- * fill the entire width of the layout.
+ * Gets whether or not the lines in the layout are justified.
*
- * Return value: the justify
+ * Return value: %TRUE if the lines are justified.
+ *
+ * Deprecated: This function was never implemented. Use
+ * pango_layout_get_justification() instead.
**/
gboolean
pango_layout_get_justify (PangoLayout *layout)
@@ -560,6 +573,49 @@ pango_layout_get_justify (PangoLayout *l
return layout->justify;
}
+
+/**
+ * pango_layout_set_justification:
+ * @layout: a #PangoLayout
+ * @justify: the justification method to use for the layout.
+ *
+ * Sets the justification method to use for the layout.
+ *
+ * Since: 1.10
+ **/
+void
+pango_layout_set_justification (PangoLayout *layout,
+ PangoJustification justification)
+{
+ g_return_if_fail (layout != NULL);
+
+ if (layout->justification != justification)
+ {
+ layout->justification = justification;
+
+ pango_layout_clear_lines (layout);
+ }
+}
+
+
+/**
+ * pango_layout_get_justification:
+ * @layout: a #PangoLayout
+ *
+ * Gets the justification method being used for the layout.
+ *
+ * Return value: the justification method being used.
+ *
+ * Since: 1.10
+ **/
+PangoJustification
+pango_layout_get_justification (PangoLayout *layout)
+{
+ g_return_val_if_fail (layout != NULL, PANGO_JUSTIFICATION_NONE);
+ return layout->justification;
+}
+
+
/**
* pango_layout_set_auto_dir:
* @layout: a #PangoLayout
@@ -3273,16 +3329,32 @@ pango_layout_check_lines (PangoLayout *l
if (state.items)
{
- state.first_line = TRUE;
- state.base_dir = base_dir;
- state.start_offset = start_offset;
- state.line_start_index = start - layout->text;
+#if 0
+ /* Enable this to use the new layout code for everything. */
+ pango_layout_paragraph (layout, state.items, base_dir, start_offset,
+ state.attrs, &layout->lines);
+#else
+ /* Here we just use the new layout code for justification. */
+ if (layout->justification != PANGO_JUSTIFICATION_NONE)
+ {
+ pango_layout_paragraph (layout, state.items, base_dir,
+ start_offset, state.attrs,
+ &layout->lines);
+ }
+ else
+ {
+ state.first_line = TRUE;
+ state.base_dir = base_dir;
+ state.start_offset = start_offset;
+ state.line_start_index = start - layout->text;
- state.glyphs = NULL;
- state.log_widths = NULL;
+ state.glyphs = NULL;
+ state.log_widths = NULL;
- while (state.items)
- process_line (layout, &state);
+ while (state.items)
+ process_line (layout, &state);
+ }
+#endif
}
else
{
@@ -5235,4 +5307,1443 @@ pango_layout_iter_get_layout_extents (P
if (logical_rect)
*logical_rect = iter->logical_rect;
+}
+
+
+
+
+/****************************************************************************
+ * NEW LAYOUT CODE - we could split this out into a separate file.
+ ****************************************************************************/
+
+/*
+ * The layout code is split into 2 stages:
+ *
+ * Stage 1 - Decide where to break the lines.
+ *
+ * This stage creates an array of BreakInfo structs with information about each
+ * break point. There are 3 different functions for this stage:
+ *
+ * layout_single_line() - used when the layout is in single paragraph mode.
+ * It only adds 2 BreakInfo elements, for the start and end of the text.
+ *
+ * layout_with_unlimited_width() - used when the layout width is -1 or
+ * ellipsization is being used. It only breaks at LINE_SEPARATOR characters.
+ *
+ * layout_with_specific_width() - used in all other cases. This steps through
+ * the characters in a line, scoring each potential break point. When it
+ * reaches the end of the line it chooses the best break, adds it to the
+ * BreakInfo array, then starts again at the best break position. It repeats
+ * this until it reaches the end of the text.
+ *
+ *
+ * Stage 2 - Create the PangoLayoutLines.
+ *
+ * build_lines() is called with the GArray of BreakInfo structs created in
+ * stage 1. It creates a PangoLayoutLine for each line, and adds the
+ * PangoLayoutRun structs to them, splitting the PangoItems as needed. After
+ * creating each line, it calls finish_line() to stretch or compress the
+ * glyphs in the line as appropriate.
+ */
+
+
+
+/* This is the maximum fraction we can shrink resizable space, i.e. 1/3. */
+#define SPACE_SHRINKABILITY 3
+
+
+/* These are the possible types of spacing used to justify a PangoItem.
+ Note that kashida spacing isn't implemented yet. */
+typedef enum {
+ PANGO_ITEM_SPACING_NONE = 0,
+ PANGO_ITEM_SPACING_WORD = 1 << 0,
+ PANGO_ITEM_SPACING_LETTER = 1 << 1,
+ PANGO_ITEM_SPACING_KASHIDA = 1 << 2,
+
+ PANGO_ITEM_SPACING_WORD_LETTER = PANGO_ITEM_SPACING_WORD
+ | PANGO_ITEM_SPACING_LETTER,
+ PANGO_ITEM_SPACING_KASHIDA_WORD = PANGO_ITEM_SPACING_KASHIDA
+ | PANGO_ITEM_SPACING_WORD
+} PangoItemSpacing;
+
+
+/* This holds information about the break at the end of each line. */
+typedef struct _BreakInfo BreakInfo;
+struct _BreakInfo
+{
+ /* The byte offset into the text of the character immediately after the
+ break, i.e. the start of the next line. */
+ gint offset;
+
+ /* The byte offset of the character after the end of the line. */
+ gint line_end_offset;
+
+ /* The char offset of the character after the end of the line. */
+ gint line_end_char_offset;
+
+ /* The width of the glyphs in the line. */
+ PangoGlyphUnit break_line_width;
+
+ /* The desired width of the line. */
+ gint line_width;
+
+ /* This is the minimum letter spacing we've seen on any PangoItem in the
+ line. We assumed this amount will be trimmed off the ends of the line.
+ If it turns out to be more than this we need to expand the line a bit. */
+ gint min_letter_spacing;
+
+ /* The amount of resizable width in the line. */
+ PangoGlyphUnit resizable_width;
+
+ /* This is the amount of space at the end of the line, which will usually be
+ trimmed, unless this is the last line in the paragraph. (The user may
+ be editing this line so we need to keep the spaces.) */
+ PangoGlyphUnit trailing_space_width;
+
+ /* This is the amount of resizable space at the end of the line. Similarly
+ this is usually trimmed, unless this is the last line. */
+ PangoGlyphUnit trailing_resizable_width;
+
+ /* The number of clusters in items that are just using letter spacing. */
+ gint letter_spaced_clusters;
+
+ /* The total number of clusters that could use letter spacing, if needed. */
+ gint resizable_clusters;
+
+ /* This is the byte offset of the last tab character in the line, or -1 if
+ there are no tabs in the line. We don't do any justification before tab
+ characters. */
+ gint last_tab_offset;
+};
+
+
+/* When deciding where to split lines we need to keep some data about each
+ item. */
+typedef struct _ItemData ItemData;
+struct _ItemData
+{
+ /* This is the item's element in the list of items from pango_break(). */
+ GList *elem;
+
+ /* This is the character offset of the item within the PangoLayout's text. */
+ gint item_char_offset;
+
+ /* These are the logical widths of each character in the item. */
+ PangoGlyphUnit *log_widths;
+
+ /* This is the type of spacing to use when justifying the item. */
+ PangoItemSpacing spacing;
+
+ /* This is the letter spacing setting for the item, from its attributes. */
+ gint letter_spacing;
+};
+
+
+/*
+ * STAGE 1 & STAGE 2 SHARED CODE.
+ */
+
+/* These are the script groups used to determine the spacing method to use for
+ a particular PangoItem, as defined in the CSS3 Text module. The values
+ are used as indices to the spacing_table, so don't change them. */
+typedef enum
+{
+ SCRIPT_GROUP_LATIN = 0,
+ SCRIPT_GROUP_CJK = 1,
+ SCRIPT_GROUP_DEVANAGARI = 2,
+ SCRIPT_GROUP_ARABIC = 3,
+ SCRIPT_GROUP_SE_ASIAN = 4
+} ScriptGroup;
+
+
+/* This is the table of spacing methods to use for each of the five script
+ groups against each of the 8 PangoJustification settings.
+ This is almost the same as the table in CSS3 except for the AUTO and
+ DISTRIBUTE settings for Latin scripts. For AUTO we use word & letter
+ spacing for better results, and for DISTRIBUTE we use letter spacing only,
+ since that seems to be what the CSS3 description implies. */
+PangoItemSpacing spacing_table[5][8] =
+ {
+ /* Format of entries below for each script group:
+ { PANGO_JUSTIFICATION_NONE, PANGO_JUSTIFICATION_AUTO,
+ PANGO_JUSTIFICATION_INTER_WORD, PANGO_JUSTIFICATION_NEWSPAPER,
+ PANGO_JUSTIFICATION_INTER_IDEOGRAPH, PANGO_JUSTIFICATION_DISTRIBUTE,
+ PANGO_JUSTIFICATION_INTER_CLUSTER, PANGO_JUSTIFICATION_KASHIDA } */
+
+ /* SCRIPT_GROUP_LATIN */
+ { PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_WORD_LETTER,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_WORD_LETTER,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_LETTER,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_WORD },
+
+ /* SCRIPT_GROUP_CJK */
+ { PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_NONE,
+ PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_LETTER,
+ PANGO_ITEM_SPACING_LETTER, PANGO_ITEM_SPACING_LETTER,
+ PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_NONE },
+
+ /* SCRIPT_GROUP_DEVANAGARI */
+ { PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_WORD,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_WORD,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_WORD,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_WORD },
+
+ /* SCRIPT_GROUP_ARABIC */
+ { PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_KASHIDA_WORD,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_KASHIDA_WORD,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_KASHIDA_WORD,
+ PANGO_ITEM_SPACING_WORD, PANGO_ITEM_SPACING_KASHIDA_WORD },
+
+ /* SCRIPT_GROUP_SE_ASIAN */
+ { PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_LETTER,
+ PANGO_ITEM_SPACING_LETTER, PANGO_ITEM_SPACING_LETTER,
+ PANGO_ITEM_SPACING_NONE, PANGO_ITEM_SPACING_LETTER,
+ PANGO_ITEM_SPACING_LETTER, PANGO_ITEM_SPACING_NONE },
+ };
+
+/* This returns the item spacing method to use for justification according
+ to the layout's justificaion setting and the item's script. */
+static PangoItemSpacing
+get_item_spacing (PangoLayout *layout,
+ PangoItem *item)
+{
+ static gboolean initialized = FALSE;
+ static PangoLanguage *ja, *ko, *zh_cn, *zh_hk, *zh_mo, *zh_sg, *zh_tw;
+ static PangoLanguage *bh, *bho, *bn, *hi, *kok, *ks, *mr, *ne, *sa;
+ static PangoLanguage *ar, *fa, *syr, *ug, *ur;
+ static PangoLanguage *km, *lo, *my, *th;
+
+ PangoLanguage *language = item->analysis.language;
+ ScriptGroup group;
+
+ /* We need to compare against lots of languages, so we get the pointers
+ just the once. */
+ if (!initialized)
+ {
+ /* CJK group. */
+ ja = pango_language_from_string ("ja");
+ ko = pango_language_from_string ("ko");
+ zh_cn = pango_language_from_string ("zh-cn");
+ zh_hk = pango_language_from_string ("zh-hk");
+ zh_mo = pango_language_from_string ("zh-mo");
+ zh_sg = pango_language_from_string ("zh-sg");
+ zh_tw = pango_language_from_string ("zh-tw");
+
+ /* Devanagari group. */
+ bh = pango_language_from_string ("bh");
+ bho = pango_language_from_string ("bho");
+ bn = pango_language_from_string ("bn");
+ hi = pango_language_from_string ("hi");
+ kok = pango_language_from_string ("kok");
+ ks = pango_language_from_string ("ks");
+ mr = pango_language_from_string ("mr");
+ ne = pango_language_from_string ("ne");
+ sa = pango_language_from_string ("sa");
+
+ /* Arabic group. */
+ ar = pango_language_from_string ("ar");
+ fa = pango_language_from_string ("fa");
+ syr = pango_language_from_string ("syr");
+ ug = pango_language_from_string ("ug");
+ ur = pango_language_from_string ("ur");
+
+ /* SE Asian group. */
+ km = pango_language_from_string ("km");
+ lo = pango_language_from_string ("lo");
+ my = pango_language_from_string ("my");
+ th = pango_language_from_string ("th");
+
+ initialized = TRUE;
+ }
+
+ /* Determine which of the script groups the item's language is in.
+ FIXME: Most languages use one script. A few use Latin and Cyrillic
+ (az, cv, ku, mo), but they are in the same script group so that
+ doesn't matter. However, Japanese uses Han, Hiragana and Katakana,
+ and Korean uses Hangul and Han, and I'm not sure if these are in
+ different script groups, so they may need special treatment. */
+ if (language == ja || language == ko || language == zh_cn
+ || language == zh_hk || language == zh_mo || language == zh_sg
+ || language == zh_tw)
+ group = SCRIPT_GROUP_CJK;
+ else if (language == bh || language == bho || language == bn
+ || language == hi || language == kok || language == ks
+ || language == mr || language == ne || language == sa)
+ group = SCRIPT_GROUP_DEVANAGARI;
+ else if (language == bh || language == bho || language == bn
+ || language == hi || language == kok || language == ks
+ || language == mr || language == ne || language == sa)
+ group = SCRIPT_GROUP_ARABIC;
+ else if (language == bh || language == bho || language == bn
+ || language == hi || language == kok || language == ks
+ || language == mr || language == ne || language == sa)
+ group = SCRIPT_GROUP_SE_ASIAN;
+ else
+ group = SCRIPT_GROUP_LATIN;
+
+ /* Now we just look up the spacing in the table. */
+ return spacing_table[group][layout->justification];
+}
+
+
+/* This adds letter spacing to each glyph in a PangoGlyphString. It adds the
+ letter spacing to the cluster's width, and moves each glyph to the right
+ by half of the spacing amount (if it is > 0). */
+static void
+letter_space (PangoGlyphString *glyph_string,
+ gint letter_spacing)
+{
+ gint x_adjustment = 0, glyph_index, offset = -1;
+
+ if (letter_spacing > 0)
+ x_adjustment = letter_spacing / 2;
+
+ for (glyph_index = glyph_string->num_glyphs - 1; glyph_index >= 0;
+ glyph_index--)
+ {
+ glyph_string->glyphs[glyph_index].geometry.x_offset += x_adjustment;
+
+ /* We check the log_clusters byte offset to see if we've moved to the
+ next cluster. We initialized offset to -1 so it doesn't match on
+ the first iteration. */
+ if (glyph_string->log_clusters[glyph_index] != offset)
+ {
+ offset = glyph_string->log_clusters[glyph_index];
+ glyph_string->glyphs[glyph_index].geometry.width += letter_spacing;
+ }
+ }
+}
+
+
+/*
+ * STAGE 1 CODE - Deciding where to break lines.
+ */
+
+
+/* This creates an array containing a few bits of information about each
+ PangoItem in the paragraph, e.g. the logical widths of each character.
+ It is used while we are stepping through the paragraph looking for the
+ best break positions. */
+static GArray*
+create_items_data (PangoLayout *layout,
+ GList *items,
+ gint start_char_offset)
+{
+ GArray *items_data;
+ PangoGlyphString *glyphs;
+ GList *elem;
+ PangoItem *item;
+ ItemProperties properties;
+ ItemData item_data;
+ gint item_char_offset;
+
+ items_data = g_array_sized_new (FALSE, FALSE, sizeof (ItemData), 32);
+
+ glyphs = pango_glyph_string_new ();
+
+ item_char_offset = start_char_offset;
+ for (elem = items; elem; elem = elem->next)
+ {
+ item_data.elem = elem;
+ item_data.item_char_offset = item_char_offset;
+
+ item = elem->data;
+
+ item_data.spacing = get_item_spacing (layout, item);
+
+ /* Check if the item is a shape, e.g. an embedded widget. */
+ pango_layout_get_item_properties (item, &properties);
+ item_data.letter_spacing = properties.letter_spacing;
+
+ /* Convert the item to glyphs. */
+ pango_glyph_string_set_size (glyphs, 0);
+ if (layout->text[item->offset] == '\t')
+ {
+ item_data.log_widths = NULL;
+ }
+ else
+ {
+ if (properties.shape_set)
+ {
+ imposed_shape (layout->text + item->offset,
+ item->num_chars,
+ properties.shape_ink_rect,
+ properties.shape_logical_rect,
+ glyphs);
+ }
+ else
+ {
+ pango_shape (layout->text + item->offset, item->length,
+ &item->analysis, glyphs);
+ }
+
+ if (properties.letter_spacing)
+ letter_space (glyphs, properties.letter_spacing);
+
+ /* Calculate the logical width of each character. */
+ item_data.log_widths = g_new (PangoGlyphUnit, item->num_chars);
+ pango_glyph_string_get_logical_widths (glyphs,
+ layout->text + item->offset,
+ item->length,
+ item->analysis.level,
+ item_data.log_widths);
+ }
+
+ g_array_append_val (items_data, item_data);
+ item_char_offset += item->num_chars;
+ }
+
+ pango_glyph_string_free (glyphs);
+
+ return items_data;
+}
+
+
+/* This frees the array created above in create_items_data(). We only need
+ to free the log_widths and the GArray itself. */
+static void
+free_items_data (GArray *items_data)
+{
+ gint i;
+
+ for (i = 0; i < items_data->len; i++)
+ {
+ ItemData *item_data = &g_array_index (items_data, ItemData, i);
+ g_free (item_data->log_widths);
+ }
+ g_array_free (items_data, TRUE);
+}
+
+
+/* This returns the next tab position after the given distance. */
+static gint
+find_next_tab_position (PangoLayout *layout,
+ gint distance)
+{
+ int i, tab_pos;
+
+ for (i=0;;i++)
+ {
+ tab_pos = get_tab_pos (layout, i);
+ if (tab_pos > distance)
+ return tab_pos;
+ }
+}
+
+
+/* This does the layout when we have a specific layout width. */
+static GArray*
+layout_with_specific_width (PangoLayout *layout,
+ GList *items,
+ PangoDirection base_dir,
+ gint start_char_offset,
+ PangoAttrList *attrs)
+{
+ GArray *breaks, *items_data;
+ ItemData *item_data;
+ BreakInfo break_info = { 0 };
+ PangoItem *item;
+ gint item_index = 0, char_offset, line_end_char_offset, char_num = 0;
+ gint line_width, normal_line_width;
+ gint letter_spaced_clusters = 0, total_letter_spaced_clusters = 0;
+ gint resizable_clusters = 0, total_resizable_clusters = 0;
+ gint is_line_break, is_char_break, break_score, min_letter_spacing;
+ gint break_line_width, last_tab_offset = -1;
+ const gchar *byte_pos = NULL, *line_end_byte_pos = NULL;
+ PangoGlyphUnit distance = 0, resizable_width = 0, max_compression, log_width;
+ PangoGlyphUnit trailing_space_width = 0, trailing_resizable_width = 0;
+ gboolean use_line_breaks = TRUE, use_char_breaks = TRUE;
+
+ /* This is the information we keep for the best break found so far. */
+ gint best_break_item_index = -1, best_break_char_num = 0;
+ const gchar *best_break_byte_pos = NULL;
+ gboolean best_break_is_line_break = FALSE;
+ gint best_break_score = G_MAXINT;
+
+ breaks = g_array_sized_new (FALSE, FALSE, sizeof (BreakInfo), 32);
+ items_data = create_items_data (layout, items, start_char_offset);
+
+ /* Calculate the width to use for the first & subsequent lines. */
+ if (layout->indent >= 0)
+ {
+ line_width = layout->width - layout->indent;
+ normal_line_width = layout->width;
+ }
+ else
+ {
+ line_width = layout->width;
+ normal_line_width = layout->width + layout->indent;
+ }
+
+ /* FIXME: Temp workaround for errors due to kerning changes at breaks. */
+#if 1
+ line_width -= 10;
+ normal_line_width -= 10;
+#endif
+
+ /* If the wrap mode is PANGO_WRAP_WORD we don't check for char breaks and if
+ it is PANGO_WRAP_CHAR we don't check for line breaks. */
+ if (layout->wrap == PANGO_WRAP_WORD)
+ use_char_breaks = FALSE;
+ else if (layout->wrap == PANGO_WRAP_CHAR)
+ use_line_breaks = FALSE;
+
+ /* Add an initial break to indicate the start of the paragraph. */
+ item = items->data;
+ break_info.offset = item->offset;
+ g_array_append_val (breaks, break_info);
+
+ byte_pos = line_end_byte_pos = layout->text + item->offset;
+ char_offset = line_end_char_offset = start_char_offset;
+ item_data = &g_array_index (items_data, ItemData, 0);
+ min_letter_spacing = item_data->letter_spacing;
+
+#ifdef DEBUG_BREAKS
+ g_print ("##############################################################\n");
+ g_print ("Line Width:%i Text:%.20s\n",
+ line_width, byte_pos);
+#endif
+
+ /*
+ * This is the main loop. It steps through the paragraph computing the best
+ * break positions to use.
+ *
+ * Note that we set item_index to -1 to indicate that we've reached the end
+ * of the text. item_data and item will be NULL as well.
+ */
+ for (;;)
+ {
+ /* Check if there is a potential line or char break immediately before
+ this character. */
+ is_line_break = layout->log_attrs[char_offset].is_line_break;
+ is_char_break = layout->log_attrs[char_offset].is_char_break;
+
+ /* We assume the end of the text is a line & char break position. */
+ if (item_index < 0)
+ is_line_break = is_char_break = TRUE;
+
+ /* Check if we can break here. */
+ if ((use_line_breaks && is_line_break)
+ || (use_char_breaks && is_char_break))
+ {
+ /* This is the maximum amount we could subtract from the line width
+ by compressing all the resizable spaces. */
+ max_compression = resizable_width / SPACE_SHRINKABILITY;
+
+ /* Calculate the width of the line up to the break, which is just
+ the total width of all the clusters, minus the minimum letter
+ spacing, since we'll trim the letter spacing off the ends.
+ If the letter spacing on either end turns out to be more than the
+ minimum we'll have to stretch the line a little. */
+ break_line_width = distance - min_letter_spacing;
+ if (break_line_width < 0)
+ break_line_width = 0;
+
+ /* Calculate a score for the break. We just use the difference from
+ the natural line width to the necessary line width. */
+ break_score = break_line_width - line_width;
+ if (break_score < 0)
+ break_score = - break_score;
+
+#ifdef DEBUG_BREAKS
+ g_print ("Break Width:%7i Max Compression:%6i Score:%7i Text:%.10s\n",
+ break_line_width, max_compression, break_score, byte_pos);
+#endif
+
+ /* See if this break is better than the previous one. Note that line
+ breaks are preferred over char breaks, and low scores are best. */
+ if ((break_line_width - max_compression <= line_width
+ && (is_line_break || !best_break_is_line_break)
+ && break_score < best_break_score)
+ || best_break_score == G_MAXINT)
+ {
+ break_info.offset = byte_pos - layout->text;
+ break_info.line_end_offset = line_end_byte_pos - layout->text;
+ break_info.line_end_char_offset = line_end_char_offset;
+ break_info.break_line_width = break_line_width;
+ break_info.line_width = line_width;
+ break_info.min_letter_spacing = min_letter_spacing;
+ break_info.resizable_width = resizable_width;
+ break_info.trailing_space_width = trailing_space_width;
+ break_info.trailing_resizable_width = trailing_resizable_width;
+ break_info.letter_spaced_clusters = letter_spaced_clusters;
+ break_info.resizable_clusters = resizable_clusters;
+ break_info.last_tab_offset = last_tab_offset;
+
+ best_break_item_index = item_index;
+ best_break_char_num = char_num;
+ best_break_byte_pos = byte_pos;
+ best_break_is_line_break = is_line_break;
+ best_break_score = break_score;
+ }
+
+ /* Check if we've gone past the end of the line. */
+ if (break_line_width - max_compression > line_width || item_index < 0
+ || layout->log_attrs[char_offset].is_mandatory_break)
+ {
+ /* Add the best break we've found for this line. */
+ g_array_append_val (breaks, break_info);
+
+ /* If we've reached the end of the paragraph, exit the loop. */
+ if (best_break_item_index < 0)
+ break;
+
+ /* Go back to the break position and carry on from there. */
+ item_index = best_break_item_index;
+ item_data = &g_array_index (items_data, ItemData, item_index);
+ item = item_data->elem->data;
+ char_num = best_break_char_num;
+ char_offset = item_data->item_char_offset + char_num;
+ byte_pos = line_end_byte_pos = best_break_byte_pos;
+
+ distance = 0;
+ resizable_width = 0;
+ trailing_space_width = 0;
+ trailing_resizable_width = 0;
+ letter_spaced_clusters = total_letter_spaced_clusters = 0;
+ resizable_clusters = total_resizable_clusters = 0;
+ min_letter_spacing = item_data->letter_spacing;
+ last_tab_offset = -1;
+
+ line_width = normal_line_width;
+ best_break_score = G_MAXINT;
+ best_break_is_line_break = FALSE;
+
+#ifdef DEBUG_BREAKS
+ g_print ("\nLine Width:%i Text:%.20s\n",
+ line_width, byte_pos);
+#endif
+ }
+
+ } /* if is_line_break or is_char_break. */
+
+ /* If we've reached the end of the paragraph we don't need to adjust
+ any widths or positions. */
+ if (item_index < 0)
+ continue;
+
+ /* Check for a tab character. */
+ if (*byte_pos == '\t')
+ {
+ last_tab_offset = byte_pos - layout->text;
+ distance = find_next_tab_position (layout, distance);
+
+ /* Any resizable space & clusters before the tab is forgotten. */
+ resizable_width = 0;
+ trailing_space_width = 0;
+ trailing_resizable_width = 0;
+ letter_spaced_clusters = total_letter_spaced_clusters = 0;
+ resizable_clusters = total_resizable_clusters = 0;
+
+ byte_pos = g_utf8_next_char (byte_pos);
+
+ line_end_byte_pos = byte_pos;
+ line_end_char_offset = char_offset + 1;
+ }
+ else
+ {
+ /* Add on the width of the current char. */
+ log_width = item_data->log_widths[char_num];
+ distance += log_width;
+
+ byte_pos = g_utf8_next_char (byte_pos);
+
+ /* We need to remember how many clusters are in the line, in case we
+ need to do letter spacing for justification. Note that we use
+ char_offset + 1 since that means we're at the end of a cluster. */
+ if (layout->log_attrs[char_offset + 1].is_cursor_position)
+ {
+ if (item_data->spacing == PANGO_ITEM_SPACING_LETTER)
+ total_letter_spaced_clusters++;
+ if (item_data->spacing & PANGO_ITEM_SPACING_LETTER)
+ total_resizable_clusters++;
+ }
+
+ /* We need to remember how much resizable space there is in the line,
+ and also how much space there is at the end of the line. Any
+ trailing space will be cut off if we break here. */
+ if ((item_data->spacing & PANGO_ITEM_SPACING_WORD)
+ && layout->log_attrs[char_offset].is_resizable)
+ {
+ resizable_width += log_width;
+ trailing_space_width += log_width;
+ trailing_resizable_width += log_width;
+ }
+ else if (layout->log_attrs[char_offset].is_white)
+ {
+ trailing_space_width += log_width;
+ }
+ else
+ {
+ /* This isn't a resizable or whitespace character, so we can
+ reset the trailing widths to 0. */
+ trailing_space_width = 0;
+ trailing_resizable_width = 0;
+
+ /* Remember the offset after the last non-whitespace char. */
+ line_end_byte_pos = byte_pos;
+
+ line_end_char_offset = char_offset + 1;
+
+ /* Also remember how many clusters there are up to the last
+ non-whitespace character. */
+ letter_spaced_clusters = total_letter_spaced_clusters;
+ resizable_clusters = total_resizable_clusters;
+ }
+ }
+
+ /* Move on to the next char. If we've reached the end of the item we
+ move on to the next item in the list. When we reach the end of the
+ text we set item_index to -1, and item and item_data to NULL. */
+ char_offset++;
+ if (++char_num == item->num_chars)
+ {
+ item_index++;
+ char_num = 0;
+ if (item_index < items_data->len)
+ {
+ item_data = &g_array_index (items_data, ItemData, item_index);
+ item = item_data->elem->data;
+ if (item_data->letter_spacing < min_letter_spacing)
+ min_letter_spacing = item_data->letter_spacing;
+ }
+ else
+ {
+ item_index = -1;
+ item_data = NULL;
+ item = NULL;
+ }
+ }
+
+ } /* End of main loop stepping through the paragraph. */
+
+ free_items_data (items_data);
+
+ return breaks;
+}
+
+
+/* This does the layout for single-paragraph mode, where we want all text on
+ one line with no breaks at all (for use in GtkEntry). */
+static GArray*
+layout_single_line (PangoLayout *layout,
+ GList *items,
+ PangoDirection base_dir,
+ gint start_char_offset,
+ PangoAttrList *attrs)
+{
+ GArray *breaks;
+ BreakInfo break_info = { 0 };
+
+ /* We just create a simple breaks array with breaks representing the start
+ and end of the paragraph. */
+ breaks = g_array_sized_new (FALSE, FALSE, sizeof (BreakInfo), 2);
+
+ /* Add an initial break to indicate the start of the paragraph. */
+ g_array_append_val (breaks, break_info);
+
+ /* Add a break for the end of the paragraph. */
+ break_info.offset = break_info.line_end_offset = layout->length;
+ break_info.line_end_char_offset = layout->n_chars;
+ g_array_append_val (breaks, break_info);
+
+ return breaks;
+}
+
+
+/* This does the layout when width is set to -1, where we only break at
+ mandatory breaks. Note that we don't strip trailing whitespace in this
+ mode. We can do that, but we'd need to use the logical attributes (which
+ may only be created when needed in future, to speed things up). */
+static GArray*
+layout_with_unlimited_width (PangoLayout *layout,
+ GList *items,
+ PangoDirection base_dir,
+ gint start_char_offset,
+ PangoAttrList *attrs)
+{
+ GArray *breaks;
+ BreakInfo break_info = { 0 };
+ GList *elem;
+ PangoItem *item;
+ gint item_end_offset = 0, item_char_offset;
+
+ breaks = g_array_sized_new (FALSE, FALSE, sizeof (BreakInfo), 32);
+
+ /* Add an initial break to indicate the start of the paragraph. */
+ item = items->data;
+ break_info.offset = item->offset;
+ g_array_append_val (breaks, break_info);
+
+ /* Step through each PangoItem in the paragraph. */
+ item_char_offset = start_char_offset;
+ for (elem = items; elem; elem = elem->next)
+ {
+ item = elem->data;
+ item_end_offset = item->offset + item->length;
+
+ /* We assume each LINE_SEPARATOR char appears in an item of its own. */
+ if (g_utf8_get_char (layout->text + item->offset) == LINE_SEPARATOR)
+ {
+ break_info.offset = item->offset + item->length;
+ break_info.line_end_offset = item->offset;
+ break_info.line_end_char_offset = item_char_offset;
+ g_array_append_val (breaks, break_info);
+ }
+
+ item_char_offset += item->num_chars;
+ } /* End of loop stepping through all the PangoItems. */
+
+ /* Add a break for the end of the paragraph. */
+ break_info.offset = break_info.line_end_offset = item_end_offset;
+ break_info.line_end_char_offset = item_char_offset;
+ g_array_append_val (breaks, break_info);
+
+ return breaks;
+}
+
+
+/*
+ * STAGE 2 CODE - Creating the PangoLayoutLines.
+ */
+
+
+/* This trims any cluster spacing off the ends of the line, once it has
+ been reordered into visual order. */
+static void
+trim_line_ends (PangoLayoutRun *line_start_run,
+ gint line_start_letter_spacing,
+ gint line_start_adjustment,
+ PangoLayoutRun *line_end_run,
+ gint line_end_letter_spacing,
+ gint line_end_adjustment)
+{
+ /* Trim the left edge of the line. */
+ if (line_start_run)
+ {
+ PangoGlyphString *glyph_string = line_start_run->glyphs;
+ gint start_glyph_index = 0, glyph_index, trim_width;
+ gint offset = glyph_string->log_clusters[0];
+
+ /* Find the first non-empty cluster. */
+ for (glyph_index = 0; glyph_index <= glyph_string->num_glyphs;
+ glyph_index++)
+ {
+ if (glyph_index == glyph_string->num_glyphs
+ || glyph_string->log_clusters[glyph_index] != offset)
+ {
+ if (glyph_string->glyphs[glyph_index - 1].geometry.width != 0)
+ {
+ /* We need to remove the adjustment due to the item's
+ letter spacing setting as well as the adjustment
+ from justification. */
+ trim_width = line_start_adjustment
+ + line_start_letter_spacing / 2;
+
+ /* Adjust the x_offsets of all the glyphs. */
+ while (start_glyph_index < glyph_index)
+ {
+ glyph_string->glyphs[start_glyph_index].geometry.x_offset -= trim_width;
+ start_glyph_index++;
+ }
+
+ /* Adjust the geometry width of the last glyph. */
+ glyph_string->glyphs[glyph_index - 1].geometry.width -= trim_width;
+ break;
+ }
+
+ start_glyph_index = glyph_index;
+ if (glyph_index != glyph_string->num_glyphs)
+ offset = glyph_string->log_clusters[glyph_index];
+ }
+ }
+ }
+
+ /* Now trim the right edge of the line. */
+ if (line_end_run)
+ {
+ PangoGlyphString *glyph_string = line_end_run->glyphs;
+ gint glyph_index, offset = -1, trim_width;
+
+ /* Find the last non-empty cluster. */
+ for (glyph_index = glyph_string->num_glyphs - 1; glyph_index >= 0;
+ glyph_index--)
+ {
+ if (glyph_string->log_clusters[glyph_index] != offset)
+ {
+ if (glyph_string->glyphs[glyph_index].geometry.width != 0)
+ {
+ /* Adjust the geometry width of the last glyph. */
+ trim_width = line_end_adjustment
+ + (line_end_letter_spacing - line_end_letter_spacing / 2);
+ glyph_string->glyphs[glyph_index].geometry.width -= trim_width;
+ break;
+ }
+
+ offset = glyph_string->log_clusters[glyph_index];
+ }
+ }
+ }
+}
+
+
+/* This returns the first and last runs in the line that have non-zero-width
+ glyphs and their letter spacing settings. We need to know these settings
+ so we can adjust the line widths before calculating how much to stretch
+ spaces and clusters by. */
+static void
+get_line_ends (PangoLayoutLine *line,
+ PangoLayoutRun **line_start_run,
+ gint *line_start_letter_spacing,
+ PangoLayoutRun **line_end_run,
+ gint *line_end_letter_spacing)
+{
+ GSList *elem;
+
+ *line_start_run = *line_end_run = NULL;
+ *line_start_letter_spacing = *line_end_letter_spacing = 0;
+
+ /* Look through each run to find the first non-zero-width cluster, and the
+ last run with a non-zero-width cluster. */
+ for (elem = line->runs; elem; elem = elem->next)
+ {
+ PangoLayoutRun *run = elem->data;
+ PangoGlyphString *glyph_string = run->glyphs;
+
+ if (glyph_string->num_glyphs > 0)
+ {
+ gint glyph_index, offset, letter_spacing;
+ PangoItem *item = run->item;
+ ItemProperties properties;
+
+ pango_layout_get_item_properties (item, &properties);
+ letter_spacing = properties.letter_spacing;
+
+ /* Find the left-most, non-zero-width cluster. */
+ offset = glyph_string->log_clusters[0];
+ for (glyph_index = 0; glyph_index <= glyph_string->num_glyphs;
+ glyph_index++)
+ {
+ if (glyph_index == glyph_string->num_glyphs
+ || glyph_string->log_clusters[glyph_index] != offset)
+ {
+ if (glyph_string->glyphs[glyph_index - 1].geometry.width != 0)
+ {
+ /* Remember this run has a non-zero-width cluster. */
+ *line_end_run = run;
+ *line_end_letter_spacing = letter_spacing;
+
+ if (*line_start_run == NULL)
+ {
+ *line_start_run = run;
+ /* We need to remove the adjustment due to the item's
+ letter spacing setting as well as the adjustment
+ from justification. */
+ *line_start_letter_spacing = letter_spacing;
+ }
+ }
+
+ if (glyph_index != glyph_string->num_glyphs)
+ offset = glyph_string->log_clusters[glyph_index];
+ }
+ }
+ }
+ }
+}
+
+
+/* This takes the PangoLayoutLine created in build_lines() and reorders the
+ runs into visual order, and does any justification required. */
+static void
+finish_line (PangoLayout *layout,
+ PangoLayoutLine *line,
+ gint item_char_offset,
+ PangoAttrList *attrs,
+ GArray *breaks,
+ gint break_num)
+{
+ BreakInfo *break_info;
+ GSList *elem;
+ gfloat space_multiplier = 1;
+ PangoGlyphUnit resizable_width, total_width;
+ gint cluster_spacing = 0, x_adjustment = 0;
+ gboolean is_last_line = FALSE;
+ PangoLayoutRun *line_start_run, *line_end_run;
+ gint line_start_letter_spacing, line_end_letter_spacing;
+ gint line_start_adjustment = 0, line_end_adjustment = 0;
+ PangoItemSpacing spacing;
+
+ /* We don't expand or compress space in single line mode or when the width
+ is unlimited. */
+ if (layout->single_paragraph || layout->width == -1
+ || layout->ellipsize != PANGO_ELLIPSIZE_NONE
+ || layout->justification == PANGO_JUSTIFICATION_NONE)
+ {
+ /* FIXME: I think ellipsization should be done last, after we've
+ reordered the line and trimmed any space off the ends. But the
+ current Pango code does it before reordering for some reason. */
+ _pango_layout_line_ellipsize (line, attrs);
+
+ /* Reorder the line from logical to visual order. */
+ pango_layout_line_reorder (line);
+
+ /* Get the first & last run in the line, and the letter space in each. */
+ get_line_ends (line, &line_start_run, &line_start_letter_spacing,
+ &line_end_run, &line_end_letter_spacing);
+
+ /* Trim the space off the ends of the line. */
+ trim_line_ends (line_start_run, line_start_letter_spacing, 0,
+ line_end_run, line_end_letter_spacing, 0);
+
+ return;
+ }
+
+ /* Reorder the line into visual order, so we can find out the letter
+ spacing we need to trim from the start and end of the line, if any. */
+ pango_layout_line_reorder (line);
+
+ /* Get the first & last run in the line, and the letter space in each. */
+ get_line_ends (line, &line_start_run, &line_start_letter_spacing,
+ &line_end_run, &line_end_letter_spacing);
+
+ break_info = &g_array_index (breaks, BreakInfo, break_num);
+
+ /* Check if this is the last line. On the last line we don't cut off any
+ trailing space, since the user may be editing the text. */
+ if (break_num == breaks->len - 1)
+ {
+ is_last_line = TRUE;
+ resizable_width = break_info->resizable_width;
+ total_width = break_info->break_line_width;
+ }
+ else
+ {
+ resizable_width = break_info->resizable_width - break_info->trailing_resizable_width;
+ total_width = break_info->break_line_width - break_info->trailing_space_width;
+ }
+
+ /* Adjust the width of the text to take account of the letter spacing that
+ we are going to trim from the ends of the line. Note that we assumed this
+ was break_info->min_letter_spacing when doing line breaking, so we have to
+ add that back and take off the real values. */
+ total_width += break_info->min_letter_spacing;
+ total_width -= (line_start_letter_spacing / 2
+ + line_end_letter_spacing - line_end_letter_spacing / 2);
+
+ /* FIXME: Currently we only do word spacing or letter spacing, but we could
+ mix them together e.g. for newspaper justification. */
+
+ /* If there is no space to stretch, we stretch clusters instead.
+ FIXME: If scripts are mixed, it may be better to stretch other scripts. */
+ if (resizable_width <= 0)
+ {
+ break_info->letter_spaced_clusters = break_info->resizable_clusters;
+ }
+
+ /* We can only add cluster spacing if there are 2 or more clusters in the
+ line, and for the last line we only allow compression. */
+ if (break_info->letter_spaced_clusters > 1
+ && (!is_last_line || total_width > break_info->line_width))
+ {
+ cluster_spacing = (break_info->line_width - total_width) / (break_info->letter_spaced_clusters - 1);
+ if (cluster_spacing > 0)
+ x_adjustment = cluster_spacing / 2;
+
+ line_start_adjustment = x_adjustment;
+ line_end_adjustment = cluster_spacing - x_adjustment;
+
+#if 0
+ g_print ("LW:%i TW:%i CLUSTERS:%i CS:%i LSA:%i\n",
+ break_info->line_width, total_width,
+ break_info->letter_spaced_clusters,
+ cluster_spacing, line_start_adjustment);
+#endif
+ }
+
+ if (resizable_width
+ && (!is_last_line || total_width > break_info->line_width))
+ space_multiplier += (float) (break_info->line_width - total_width) / resizable_width;
+
+ /* Step through the runs in the line. */
+ for (elem = line->runs; elem; elem = elem->next)
+ {
+ PangoLayoutRun *run = elem->data;
+ PangoItem *item = run->item;
+ PangoGlyphString *glyph_string = run->glyphs;
+ gint glyph_index, glyph_step, glyph_bound, i;
+ gint start_glyph_index, change_glyph_index;
+ const gchar *p;
+
+ /* Calculate the spacing method to use for the item. */
+ spacing = get_item_spacing (layout, item);
+
+ /* We can't use word spacing if there is no space between words. */
+ if (resizable_width <= 0)
+ spacing &= ~PANGO_ITEM_SPACING_WORD;
+
+ /* Check if we really need to space the glyphs in the item. */
+ if (glyph_string->num_glyphs == 0
+ || item->offset <= break_info->last_tab_offset
+ || spacing == PANGO_ITEM_SPACING_NONE)
+ {
+ item_char_offset += item->num_chars;
+ continue;
+ }
+
+ /* Word spacing. */
+ if (spacing & PANGO_ITEM_SPACING_WORD)
+ {
+ /* Step through the characters in the item looking for spaces. */
+ if (item->analysis.level % 2 == 0)
+ {
+ /* LTR. The glyphs for the first character are first. */
+ glyph_index = 0;
+ glyph_step = 1;
+ glyph_bound = glyph_string->num_glyphs;
+ }
+ else
+ {
+ /* RTL. The glyphs for the first character are last. */
+ glyph_index = glyph_string->num_glyphs - 1;
+ glyph_step = -1;
+ glyph_bound = -1;
+ }
+
+ p = layout->text + item->offset;
+ for (i = 0; i < item->num_chars; i++)
+ {
+ if (layout->log_attrs[item_char_offset + i].is_resizable)
+ {
+ gint offset = p - (layout->text + item->offset);
+ gint old_width, new_width;
+ gfloat new_width_float;
+
+ /* Step through the glyphs until we find this cluster. */
+ while (glyph_index != glyph_bound
+ && glyph_string->log_clusters[glyph_index] < offset)
+ glyph_index += glyph_step;
+
+ start_glyph_index = glyph_index;
+
+ /* Find the glyph at the start of the next cluster. */
+ while (glyph_string->log_clusters[glyph_index] == offset)
+ {
+ if (glyph_index == glyph_bound)
+ break;
+ glyph_index += glyph_step;
+ }
+
+ /* Adjust the glyphs in the cluster, if there are any. */
+ if (start_glyph_index != glyph_index)
+ {
+ /* We need to change the geometry width of the last
+ glyph in the cluster, which is the first one we
+ found in RTL since we are stepping backwards. */
+ if (glyph_step == 1)
+ change_glyph_index = glyph_index - 1;
+ else
+ change_glyph_index = start_glyph_index;
+
+ /* Calculate the new width of the cluster. */
+ old_width = glyph_string->glyphs[change_glyph_index].geometry.width;
+ new_width_float = (old_width * space_multiplier) + 0.5;
+ new_width = new_width_float;
+
+ /* Update the width of the appropriate glyph. */
+ glyph_string->glyphs[change_glyph_index].geometry.width = new_width;
+
+ /* If the cluster has been expanded, we place the glyphs
+ in the middle of the space. */
+ if (new_width > old_width)
+ {
+ gint x_offset = (new_width - old_width) / 2;
+
+ while (start_glyph_index != glyph_index)
+ {
+ glyph_string->glyphs[start_glyph_index].geometry.x_offset += x_offset;
+ start_glyph_index += glyph_step;
+ }
+ }
+ }
+ }
+
+ p = g_utf8_next_char (p);
+ }
+ }
+
+ /* Letter/cluster spacing. */
+ if ((spacing & PANGO_ITEM_SPACING_LETTER) && cluster_spacing)
+ {
+ /* We have to adjust the width of the last glyph of each cluster,
+ and the x_offset of all the glyphs. We initialize offset to -1
+ so it doesn't match the first glyph. */
+ gint offset = -1;
+
+ for (glyph_index = glyph_string->num_glyphs - 1;
+ glyph_index >= 0; glyph_index--)
+ {
+ glyph_string->glyphs[glyph_index].geometry.x_offset += x_adjustment;
+ if (glyph_string->log_clusters[glyph_index] != offset)
+ {
+ offset = glyph_string->log_clusters[glyph_index];
+ glyph_string->glyphs[glyph_index].geometry.width += cluster_spacing;
+ }
+ }
+ }
+ else
+ {
+ /* If the run at the start or end of the line isn't justified with
+ letter spacing, we don't need to trim the space at the ends. */
+ if (run == line_start_run)
+ line_start_adjustment = 0;
+ else if (run == line_end_run)
+ line_end_adjustment = 0;
+ }
+
+ item_char_offset += item->num_chars;
+ }
+
+ /* Trim the letter spacing at the ends of the line. */
+ trim_line_ends (line_start_run, line_start_letter_spacing,
+ line_start_adjustment,
+ line_end_run, line_end_letter_spacing,
+ line_end_adjustment);
+}
+
+
+#ifdef DEBUG_LINE_OVERFLOW
+static void
+check_line (PangoLayout *layout,
+ PangoLayoutLine *line,
+ GArray *breaks,
+ gint break_num)
+{
+ BreakInfo *break_info;
+ GSList *elem;
+ PangoGlyphUnit width = 0;
+ gint glyph_index;
+
+ break_info = &g_array_index (breaks, BreakInfo, break_num);
+
+ /* Step through the runs in the line, summing all the glyph widths. */
+ for (elem = line->runs; elem; elem = elem->next)
+ {
+ PangoLayoutRun *run = elem->data;
+ PangoGlyphString *glyph_string = run->glyphs;
+
+ for (glyph_index = 0;
+ glyph_index < glyph_string->num_glyphs;
+ glyph_index++)
+ {
+ width += glyph_string->glyphs[glyph_index].geometry.width;
+ }
+ }
+
+ if (width > break_info->line_width + 10 /* FIXME: workaround for kerning.*/
+ && break_info->line_width > 0)
+ {
+ g_print ("### Line Overflow Width: %i Desired: %i Overflow: %i\n",
+ width, break_info->line_width, width - break_info->line_width);
+ g_print (" Text: %.60s\n", layout->text + break_info->offset);
+ }
+}
+#endif
+
+/* This builds the PangoLayoutLines, given the GArray of BreakInfo data about
+ each break point to use. */
+static void
+build_lines (PangoLayout *layout,
+ GList *items,
+ PangoDirection base_dir,
+ gint start_char_offset,
+ PangoAttrList *attrs,
+ GArray *breaks,
+ GSList **lines)
+{
+ const char *text = layout->text;
+ PangoLayoutLine *line = NULL;
+ BreakInfo *break_info = NULL;
+ gint break_num = 0, item_char_offset, line_char_offset, end_offset = 0;
+ GList *elem;
+ PangoItem *item, *run_item;
+ ItemProperties properties;
+ PangoLayoutRun *run;
+ PangoGlyphString *glyphs;
+ gboolean need_split;
+
+ break_info = &g_array_index (breaks, BreakInfo, break_num);
+
+ /* Step through each PangoItem in the paragraph. */
+ line_char_offset = item_char_offset = start_char_offset;
+ for (elem = items; elem; elem = elem->next)
+ {
+ item = elem->data;
+
+ /* Check if the item is a shape, e.g. an embedded widget. */
+ pango_layout_get_item_properties (item, &properties);
+
+ /* Loop around splitting the item across lines, if necessary. */
+ for (;;)
+ {
+ need_split = FALSE;
+
+ /* Create a new line if we don't have one, and move the break to
+ the end of the new line. */
+ if (!line)
+ {
+ line = pango_layout_line_new (layout);
+ line->start_index = break_info->offset;
+ line->resolved_dir = base_dir;
+ line->is_paragraph_start = (break_num == 0) ? TRUE : FALSE;
+
+ break_num++;
+ break_info = &g_array_index (breaks, BreakInfo, break_num);
+ end_offset = break_info->line_end_offset;
+
+ line->length = end_offset - line->start_index;
+ }
+
+ /* Check if we need to split the item across lines. */
+ if (item->offset + item->length > break_info->offset)
+ need_split = TRUE;
+
+ /* Split the item at the break. The new item will cover up to the
+ break, and the original item contains the rest of the text. */
+ if (need_split)
+ {
+ gint run_item_bytes, run_item_chars;
+ run_item_bytes = break_info->offset - item->offset;
+ run_item_chars = g_utf8_pointer_to_offset (text + item->offset, text + item->offset + run_item_bytes);
+ run_item = pango_item_split (item, run_item_bytes, run_item_chars);
+ }
+ else
+ run_item = item;
+
+ /* Add the run from the item. */
+ glyphs = NULL;
+ if (text[item->offset] == '\t')
+ {
+ glyphs = pango_glyph_string_new ();
+ shape_tab (line, glyphs);
+ }
+ else if (properties.shape_set)
+ {
+ glyphs = pango_glyph_string_new ();
+ imposed_shape (text + run_item->offset,
+ run_item->num_chars,
+ properties.shape_ink_rect,
+ properties.shape_logical_rect,
+ glyphs);
+ }
+ else if (run_item->offset < end_offset)
+ {
+ gint run_end_offset, run_bytes;
+
+ /* We need to cut off any bytes after end_offset. */
+ run_end_offset = run_item->offset + run_item->length;
+ if (run_end_offset > end_offset)
+ run_end_offset = end_offset;
+ run_bytes = run_end_offset - run_item->offset;
+
+ if (run_bytes > 0)
+ {
+ glyphs = pango_glyph_string_new ();
+ pango_shape (text + run_item->offset, run_bytes,
+ &run_item->analysis, glyphs);
+ }
+ }
+
+ item_char_offset += run_item->num_chars;
+
+ if (glyphs)
+ {
+ if (properties.letter_spacing)
+ letter_space (glyphs, properties.letter_spacing);
+
+ /* We may need to trim a few whitespace characters off if this
+ is the end of the line. */
+ if (item_char_offset > break_info->line_end_char_offset)
+ run_item->num_chars -= item_char_offset - break_info->line_end_char_offset;
+
+ run = g_new (PangoLayoutRun, 1);
+ run->item = run_item;
+ run->glyphs = glyphs;
+ line->runs = g_slist_prepend (line->runs, run);
+ }
+
+ /* If that run completed the line, then finish it off. */
+ if (item->offset + item->length >= break_info->offset)
+ {
+ /* The runs were prepended, so reverse them to logical order. */
+ line->runs = g_slist_reverse (line->runs);
+
+ /* Add the completed line to the layout. */
+ finish_line (layout, line, line_char_offset, attrs,
+ breaks, break_num);
+
+#ifdef DEBUG_LINE_OVERFLOW
+ check_line (layout, line, breaks, break_num);
+#endif
+
+ *lines = g_slist_prepend (*lines, line);
+ line = NULL;
+ line_char_offset = item_char_offset;
+ }
+
+ if (!glyphs)
+ {
+ /* We don't need run_item at all so we can free it. */
+ pango_item_free (run_item);
+ }
+
+ /* If we didn't need to split the item, then we've finished it,
+ so break out of this inner loop and move to the next item. */
+ if (!need_split)
+ break;
+ }
+ }
+}
+
+
+/* This is the main entry-point to the justification code. Note that the
+ code also supports ragged (non-justified) text, so could be used for
+ all text layout in Pango in future, once any bugs are ironed out. */
+static void
+pango_layout_paragraph (PangoLayout *layout,
+ GList *items,
+ PangoDirection base_dir,
+ gint start_char_offset,
+ PangoAttrList *attrs,
+ GSList **lines)
+{
+ GArray *breaks;
+
+ if (layout->single_paragraph)
+ {
+ /* In single-line mode we don't break anywhere at all. Characters like
+ newlines will be displayed as special glyphs. */
+ breaks = layout_single_line (layout, items, base_dir, start_char_offset,
+ attrs);
+ }
+ else if (layout->width == -1 || layout->ellipsize != PANGO_ELLIPSIZE_NONE)
+ {
+ /* If the width is set to -1, or ellipsization is being used, we only
+ break at mandatory breaks. */
+ breaks = layout_with_unlimited_width (layout, items, base_dir,
+ start_char_offset, attrs);
+ }
+ else
+ {
+ /* This is the more complicated case, where we need to layout the
+ paragraph text within a specific width. */
+ breaks = layout_with_specific_width (layout, items, base_dir,
+ start_char_offset, attrs);
+ }
+
+ build_lines (layout, items, base_dir, start_char_offset, attrs, breaks,
+ lines);
+
+ g_array_free (breaks, TRUE);
+
+ g_list_free (items);
}
Index: pango/pango-layout.h
===================================================================
RCS file: /cvs/gnome/pango/pango/pango-layout.h,v
retrieving revision 1.36
diff -u -p -r1.36 pango-layout.h
--- pango/pango-layout.h 22 Jul 2005 15:33:12 -0000 1.36
+++ pango/pango-layout.h 4 Aug 2005 11:02:02 -0000
@@ -68,6 +68,35 @@ typedef enum {
PANGO_ELLIPSIZE_END
} PangoEllipsizeMode;
+/**
+ * PangoJustification
+ * @PANGO_JUSTIFICATION_NONE: No justification.
+ * @PANGO_JUSTIFICATION_AUTO: The default justification method will be used.
+ * @PANGO_JUSTIFICATION_INTER_WORD: Inter-word spacing only will be used.
+ * @PANGO_JUSTIFICATION_NEWSPAPER: More complex justification will be used.
+ * @PANGO_JUSTIFICATION_INTER_IDEOGRAPH: Uses letter spacing between ideographs
+ * and word spacing elsewhere.
+ * @PANGO_JUSTIFICATION_DISTRIBUTE: Does not prioritize between word and letter
+ * spacing.
+ * @PANGO_JUSTIFICATION_INTER_CLUSTER: Uses letter spacing for Southeast Asian
+ * scripts, and word spacing elsewhere.
+ * @PANGO_JUSTIFICATION_KASHIDA: Uses kashidas in the Arabic script, and
+ * word spacing elsewhere.
+ *
+ * These are the different justification settings, based on the CSS3 draft
+ * specificaton. Note that we don't support them all yet.
+ */
+typedef enum {
+ PANGO_JUSTIFICATION_NONE = 0,
+ PANGO_JUSTIFICATION_AUTO = 1,
+ PANGO_JUSTIFICATION_INTER_WORD = 2,
+ PANGO_JUSTIFICATION_NEWSPAPER = 3,
+ PANGO_JUSTIFICATION_INTER_IDEOGRAPH = 4,
+ PANGO_JUSTIFICATION_DISTRIBUTE = 5,
+ PANGO_JUSTIFICATION_INTER_CLUSTER = 6,
+ PANGO_JUSTIFICATION_KASHIDA = 7
+} PangoJustification;
+
struct _PangoLayoutLine
{
PangoLayout *layout;
@@ -131,9 +160,14 @@ int pango_layout_get_indent
void pango_layout_set_spacing (PangoLayout *layout,
int spacing);
int pango_layout_get_spacing (PangoLayout *layout);
+#ifndef PANGO_DISABLE_DEPRECATED
void pango_layout_set_justify (PangoLayout *layout,
gboolean justify);
gboolean pango_layout_get_justify (PangoLayout *layout);
+#endif
+void pango_layout_set_justification (PangoLayout *layout,
+ PangoJustification justification);
+PangoJustification pango_layout_get_justification (PangoLayout *layout);
void pango_layout_set_auto_dir (PangoLayout *layout,
gboolean auto_dir);
gboolean pango_layout_get_auto_dir (PangoLayout *layout);
xmlroff-justify.patch
(text/x-patch, 592 B)
--- area/fo-area-layout.c.orig 2005-08-03 13:06:40.243677527 +0200
+++ area/fo-area-layout.c 2005-08-03 13:09:30.344900134 +0200
@@ -805,6 +805,15 @@
pango_align = PANGO_ALIGN_LEFT;
}
+ if (area_align == FO_ENUM_TEXT_ALIGN_JUSTIFY)
+ {
+ pango_layout_set_justification (area_layout->layout, PANGO_JUSTIFICATION_INTER_WORD);
+ }
+ else
+ {
+ pango_layout_set_justification (area_layout->layout, PANGO_JUSTIFICATION_NONE);
+ }
+
pango_layout_set_alignment (area_layout->layout,
pango_align);
pango_layout_set_auto_dir (area_layout->layout,