add blend mode tests
[swfdec.git] / swfdec / swfdec_text_layout.c
blob0653499921726bb387defbb88072ca202338a212
1 /* Swfdec
2 * Copyright (C) 2006-2008 Benjamin Otte <otte@gnome.org>
3 * 2007 Pekka Lampila <pekka.lampila@iki.fi>
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301 USA
21 #ifdef HAVE_CONFIG_H
22 #include "config.h"
23 #endif
25 #include <pango/pango.h>
26 #include <pango/pangocairo.h>
27 #include <string.h>
29 #include "swfdec_text_layout.h"
30 #include "swfdec_debug.h"
31 #include "swfdec_decoder.h"
33 #define BULLET_MARGIN 36
35 static void
36 swfdec_text_attribute_apply_bold (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr,
37 PangoFontDescription *desc)
39 pango_font_description_set_weight (desc, attr->bold ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL);
42 static PangoAttribute *
43 swfdec_text_attribute_create_color (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr)
45 return pango_attr_foreground_new (SWFDEC_COLOR_RED (attr->color) * 255,
46 SWFDEC_COLOR_GREEN (attr->color) * 255, SWFDEC_COLOR_BLUE (attr->color) * 255);
49 static void
50 swfdec_text_attribute_apply_font (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr,
51 PangoFontDescription *desc)
53 /* FIXME: resolve _sans, _serif and friends */
54 pango_font_description_set_family (desc, attr->font);
57 static void
58 swfdec_text_attribute_apply_italic (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr,
59 PangoFontDescription *desc)
61 pango_font_description_set_style (desc, attr->italic ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL);
64 static PangoAttribute *
65 swfdec_text_attribute_create_letter_spacing (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr)
67 return pango_attr_letter_spacing_new (attr->letter_spacing * PANGO_SCALE);
70 static void
71 swfdec_text_attribute_apply_size (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr,
72 PangoFontDescription *desc)
74 guint size = attr->size * layout->scale;
75 size = MAX (size, 1);
76 pango_font_description_set_absolute_size (desc, size * PANGO_SCALE);
79 static PangoAttribute *
80 swfdec_text_attribute_create_underline (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr)
82 return pango_attr_underline_new (attr->underline ? PANGO_UNDERLINE_SINGLE : PANGO_UNDERLINE_NONE);
85 /*** THE BIG OPTIONS TABLE ***/
87 typedef enum {
88 FORMAT_APPLY_UNKNOWN,
89 FORMAT_APPLY_BLOCK,
90 FORMAT_APPLY_LINE,
91 FORMAT_APPLY_GLYPH
92 } FormatApplication;
94 struct {
95 FormatApplication application;
96 PangoAttribute * (* create_attribute) (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr);
97 void (* apply_attribute) (SwfdecTextLayout *layout, const SwfdecTextAttributes *attr, PangoFontDescription *desc);
98 } format_table[] = {
99 /* unknown or unhandled properties */
100 [SWFDEC_TEXT_ATTRIBUTE_DISPLAY] = { FORMAT_APPLY_UNKNOWN, NULL, NULL },
101 [SWFDEC_TEXT_ATTRIBUTE_KERNING] = { FORMAT_APPLY_UNKNOWN, NULL, NULL },
102 /* per-paragraph options */
103 [SWFDEC_TEXT_ATTRIBUTE_BULLET] = { FORMAT_APPLY_BLOCK, NULL, NULL },
104 [SWFDEC_TEXT_ATTRIBUTE_INDENT] = { FORMAT_APPLY_BLOCK, NULL, NULL },
105 /* per-line options */
106 [SWFDEC_TEXT_ATTRIBUTE_ALIGN] = { FORMAT_APPLY_LINE, NULL, NULL },
107 [SWFDEC_TEXT_ATTRIBUTE_BLOCK_INDENT] = { FORMAT_APPLY_LINE, NULL, NULL },
108 [SWFDEC_TEXT_ATTRIBUTE_LEADING] = { FORMAT_APPLY_LINE, NULL, NULL },
109 [SWFDEC_TEXT_ATTRIBUTE_LEFT_MARGIN] = { FORMAT_APPLY_LINE, NULL, NULL },
110 [SWFDEC_TEXT_ATTRIBUTE_RIGHT_MARGIN] = { FORMAT_APPLY_LINE, NULL, NULL },
111 [SWFDEC_TEXT_ATTRIBUTE_TAB_STOPS] = { FORMAT_APPLY_LINE, NULL, NULL },
112 /* per-glyph options */
113 [SWFDEC_TEXT_ATTRIBUTE_BOLD] = { FORMAT_APPLY_GLYPH, NULL, swfdec_text_attribute_apply_bold },
114 [SWFDEC_TEXT_ATTRIBUTE_COLOR] = { FORMAT_APPLY_GLYPH, swfdec_text_attribute_create_color, NULL },
115 [SWFDEC_TEXT_ATTRIBUTE_FONT] = { FORMAT_APPLY_GLYPH, NULL, swfdec_text_attribute_apply_font },
116 [SWFDEC_TEXT_ATTRIBUTE_ITALIC] = { FORMAT_APPLY_GLYPH, NULL, swfdec_text_attribute_apply_italic },
117 [SWFDEC_TEXT_ATTRIBUTE_LETTER_SPACING] = { FORMAT_APPLY_GLYPH, swfdec_text_attribute_create_letter_spacing, NULL },
118 [SWFDEC_TEXT_ATTRIBUTE_SIZE] = { FORMAT_APPLY_GLYPH, NULL, swfdec_text_attribute_apply_size },
119 [SWFDEC_TEXT_ATTRIBUTE_TARGET] = { FORMAT_APPLY_GLYPH, NULL, NULL },
120 [SWFDEC_TEXT_ATTRIBUTE_UNDERLINE] = { FORMAT_APPLY_GLYPH, swfdec_text_attribute_create_underline, NULL },
121 [SWFDEC_TEXT_ATTRIBUTE_URL] = { FORMAT_APPLY_GLYPH, NULL }
124 #define SWFDEC_TEXT_ATTRIBUTES_MASK_NEW_BLOCK (\
125 (1 << SWFDEC_TEXT_ATTRIBUTE_ALIGN) | \
126 (1 << SWFDEC_TEXT_ATTRIBUTE_BLOCK_INDENT) | \
127 (1 << SWFDEC_TEXT_ATTRIBUTE_LEADING) | \
128 (1 << SWFDEC_TEXT_ATTRIBUTE_LEFT_MARGIN) | \
129 (1 << SWFDEC_TEXT_ATTRIBUTE_RIGHT_MARGIN) | \
130 (1 << SWFDEC_TEXT_ATTRIBUTE_TAB_STOPS))
132 /*** BLOCK ***/
134 /* A block is one vertical part of the layout that can be represented by exactly
135 * one PangoLayout. */
137 typedef struct _SwfdecTextBlock SwfdecTextBlock;
139 struct _SwfdecTextBlock {
140 PangoLayout * layout; /* the layout we render */
141 SwfdecRectangle rect; /* the area occupied by this layout */
142 guint row; /* index of start row */
143 gsize start; /* first character drawn */
144 gsize end; /* first character not drawn anymore */
145 gboolean bullet; /* TRUE if we need to draw a bullet in front of this layout */
148 static void
149 swfdec_text_block_free (gpointer blockp)
151 SwfdecTextBlock *block = blockp;
153 g_object_unref (block->layout);
155 g_slice_free (SwfdecTextBlock, block);
158 static SwfdecTextBlock *
159 swfdec_text_block_new (PangoContext *context)
161 SwfdecTextBlock *block;
163 block = g_slice_new0 (SwfdecTextBlock);
164 block->layout = pango_layout_new (context);
165 pango_layout_set_wrap (block->layout, PANGO_WRAP_WORD_CHAR);
167 return block;
170 /*** MAINTAINING THE LAYOUT ***/
172 static void
173 swfdec_text_layout_apply_paragraph_attributes (SwfdecTextLayout *layout,
174 SwfdecTextBlock *block, const SwfdecTextAttributes *attr)
176 int indent;
178 /* SWFDEC_TEXT_ATTRIBUTE_BULLET */
179 block->bullet = attr->bullet;
180 if (block->bullet) {
181 block->rect.x += BULLET_MARGIN;
182 if (block->rect.width <= BULLET_MARGIN) {
183 SWFDEC_FIXME ("out of horizontal space, what now?");
184 block->rect.width = 0;
185 } else {
186 block->rect.width -= BULLET_MARGIN;
190 /* SWFDEC_TEXT_ATTRIBUTE_INDENT */
191 indent = attr->indent;
192 indent = MAX (indent, - attr->left_margin - attr->block_indent);
193 pango_layout_set_indent (block->layout, indent * PANGO_SCALE);
196 static void
197 swfdec_text_layout_apply_line_attributes (SwfdecTextBlock *block,
198 const SwfdecTextAttributes *attr)
200 /* SWFDEC_TEXT_ATTRIBUTE_ALIGN */
201 switch (attr->align) {
202 case SWFDEC_TEXT_ALIGN_LEFT:
203 pango_layout_set_alignment (block->layout, PANGO_ALIGN_LEFT);
204 pango_layout_set_justify (block->layout, FALSE);
205 break;
206 case SWFDEC_TEXT_ALIGN_RIGHT:
207 pango_layout_set_alignment (block->layout, PANGO_ALIGN_RIGHT);
208 pango_layout_set_justify (block->layout, FALSE);
209 break;
210 case SWFDEC_TEXT_ALIGN_CENTER:
211 pango_layout_set_alignment (block->layout, PANGO_ALIGN_CENTER);
212 pango_layout_set_justify (block->layout, FALSE);
213 break;
214 case SWFDEC_TEXT_ALIGN_JUSTIFY:
215 pango_layout_set_alignment (block->layout, PANGO_ALIGN_LEFT);
216 pango_layout_set_justify (block->layout, TRUE);
217 break;
218 default:
219 g_assert_not_reached ();
222 /* SWFDEC_TEXT_ATTRIBUTE_BLOCK_INDENT */
223 /* SWFDEC_TEXT_ATTRIBUTE_LEFT_MARGIN */
224 /* SWFDEC_TEXT_ATTRIBUTE_RIGHT_MARGIN */
225 block->rect.x += attr->left_margin + attr->block_indent;
226 if (block->rect.width <= attr->left_margin + attr->block_indent + attr->right_margin) {
227 SWFDEC_FIXME ("out of horizontal space, what now?");
228 block->rect.width = 0;
229 } else {
230 block->rect.width -= attr->left_margin + attr->block_indent + attr->right_margin;
233 /* SWFDEC_TEXT_ATTRIBUTE_LEADING */
234 if (attr->leading > 0)
235 pango_layout_set_spacing (block->layout, attr->leading * PANGO_SCALE);
236 else
237 pango_layout_set_spacing (block->layout, (attr->leading - 1) * PANGO_SCALE);
239 /* SWFDEC_TEXT_ATTRIBUTE_TAB_STOPS */
240 if (attr->n_tab_stops != 0) {
241 PangoTabArray *tabs = pango_tab_array_new (attr->n_tab_stops, TRUE);
242 guint i;
243 for (i = 0; i < attr->n_tab_stops; i++) {
244 pango_tab_array_set_tab (tabs, i, PANGO_TAB_LEFT, attr->tab_stops[i]);
246 pango_layout_set_tabs (block->layout, tabs);
247 pango_tab_array_free (tabs);
251 static void
252 swfdec_text_layout_apply_attributes_to_description (SwfdecTextLayout *layout,
253 const SwfdecTextAttributes *attr, PangoFontDescription *desc)
255 guint i;
257 for (i = 0; i < G_N_ELEMENTS (format_table); i++) {
258 if (format_table[i].apply_attribute) {
259 format_table[i].apply_attribute (layout, attr, desc);
264 static void
265 swfdec_text_layout_apply_attributes (SwfdecTextLayout *layout, PangoAttrList *list,
266 const SwfdecTextAttributes *attr, guint start, guint end)
268 PangoAttribute *attribute;
269 PangoFontDescription *desc;
270 guint i;
272 for (i = 0; i < G_N_ELEMENTS (format_table); i++) {
273 if (format_table[i].create_attribute) {
274 attribute = format_table[i].create_attribute (layout, attr);
275 attribute->start_index = start;
276 attribute->end_index = end;
277 pango_attr_list_change (list, attribute);
280 desc = pango_font_description_new ();
281 swfdec_text_layout_apply_attributes_to_description (layout, attr, desc);
282 attribute = pango_attr_font_desc_new (desc);
283 pango_font_description_free (desc);
284 attribute->start_index = start;
285 attribute->end_index = end;
286 pango_attr_list_change (list, attribute);
289 static const SwfdecTextBlock *
290 swfdec_text_layout_create_paragraph (SwfdecTextLayout *layout, PangoContext *context,
291 const SwfdecTextBlock *last, gsize start, gsize end)
293 SwfdecTextBufferIter *iter;
294 SwfdecTextBlock *block;
295 PangoAttrList *list;
296 const char *string;
297 const SwfdecTextAttributes *attr, *attr_next;
298 gsize new_block, start_next;
299 PangoFontDescription *desc;
300 gboolean first = TRUE;
301 // TODO: kerning, display
303 string = swfdec_text_buffer_get_text (layout->text);
304 do {
305 iter = swfdec_text_buffer_get_iter (layout->text, start);
306 attr = swfdec_text_buffer_iter_get_attributes (layout->text, iter);
307 new_block = end;
309 block = swfdec_text_block_new (context);
310 block->start = start;
311 block->rect.x = 0;
312 if (last) {
313 block->rect.y = last->rect.y + last->rect.height;
314 block->row = last->row + pango_layout_get_line_count (last->layout);
315 } else {
316 block->rect.y = 0;
317 block->row = 0;
319 block->rect.width = layout->width;
320 desc = pango_font_description_new ();
321 swfdec_text_layout_apply_attributes_to_description (layout, attr, desc);
322 pango_layout_set_font_description (block->layout, desc);
323 pango_font_description_free (desc);
324 g_sequence_append (layout->blocks, block);
326 if (layout->password) {
327 /* requires sane line breaking so we can't just replace the text with * chars.
328 * A good idea might be using a custom font that replaces all chars upon
329 * rendering, so Pango still thinks it uses proper utf-8 */
330 SWFDEC_FIXME ("implement password handling.");
332 pango_layout_set_text (block->layout, string + start, end - start);
334 if (first) {
335 swfdec_text_layout_apply_paragraph_attributes (layout, block, attr);
336 first = FALSE;
338 swfdec_text_layout_apply_line_attributes (block, attr);
339 if (layout->word_wrap)
340 pango_layout_set_width (block->layout, block->rect.width * PANGO_SCALE);
342 iter = swfdec_text_buffer_get_iter (layout->text, start);
343 attr = swfdec_text_buffer_iter_get_attributes (layout->text, iter);
344 list = pango_attr_list_new ();
346 start_next = start;
347 while (start_next != end) {
348 gsize cur_start = start_next;
349 g_assert (attr);
350 iter = swfdec_text_buffer_iter_next (layout->text, iter);
351 attr_next = iter ? swfdec_text_buffer_iter_get_attributes (layout->text, iter) : NULL;
352 start_next = iter ? swfdec_text_buffer_iter_get_start (layout->text, iter) :
353 swfdec_text_buffer_get_length (layout->text);
354 start_next = MIN (start_next, end);
356 swfdec_text_layout_apply_attributes (layout, list, attr, cur_start - block->start, start_next - block->start);
358 if (attr_next && new_block > start_next &&
359 (swfdec_text_attributes_diff (attr, attr_next) & SWFDEC_TEXT_ATTRIBUTES_MASK_NEW_BLOCK))
360 new_block = start_next;
362 attr = attr_next;
364 pango_layout_set_attributes (block->layout, list);
365 pango_attr_list_unref (list);
367 if (new_block < end) {
368 int i;
370 for (i = 0; i < pango_layout_get_line_count (block->layout); i++) {
371 PangoLayoutLine *line = pango_layout_get_line_readonly (block->layout, i);
372 if ((gsize) line->start_index + line->length + start >= new_block) {
373 new_block = line->start_index + line->length + start;
374 /* I hope deleting lines actually works by removing text */
375 pango_layout_set_text (block->layout, string + start, new_block - start);
376 g_assert (i + 1 == pango_layout_get_line_count (block->layout));
377 break;
380 /* check that we have found a line */
381 g_assert (i < pango_layout_get_line_count (block->layout));
383 block->end = new_block;
385 start = new_block;
386 pango_layout_get_pixel_size (block->layout, &block->rect.width, &block->rect.height);
387 /* add leading to last line, too */
388 block->rect.height += pango_layout_get_spacing (block->layout) / PANGO_SCALE;
390 last = block;
391 } while (start != end);
393 return last;
396 static void
397 swfdec_text_layout_create (SwfdecTextLayout *layout)
399 const char *p, *end, *string;
400 PangoContext *context;
401 PangoFontMap *map;
402 const SwfdecTextBlock *last = NULL;
404 map = pango_cairo_font_map_get_default ();
405 context = pango_cairo_font_map_create_context (PANGO_CAIRO_FONT_MAP (map));
407 p = string = swfdec_text_buffer_get_text (layout->text);
408 for (;;) {
409 end = strpbrk (p, "\r\n");
410 if (end == NULL) {
411 end = string + swfdec_text_buffer_get_length (layout->text);
414 last = swfdec_text_layout_create_paragraph (layout, context, last, p - string, end - string);
416 if (*end == '\0')
417 break;
418 p = end + 1;
421 g_object_unref (context);
424 static void
425 swfdec_text_layout_ensure (SwfdecTextLayout *layout)
427 if (!g_sequence_iter_is_end (g_sequence_get_begin_iter (layout->blocks)))
428 return;
430 swfdec_text_layout_create (layout);
433 static void
434 swfdec_text_layout_invalidate (SwfdecTextLayout *layout)
436 if (g_sequence_iter_is_end (g_sequence_get_begin_iter (layout->blocks)))
437 return;
439 /* clear caches */
440 g_sequence_remove_range (g_sequence_get_begin_iter (layout->blocks),
441 g_sequence_get_end_iter (layout->blocks));
442 layout->layout_width = 0;
445 /*** LAYOUT ***/
447 /* A layout represents the whole text of a TextFieldMovie, this includes the
448 * invisible parts. It also does not care about transformations.
449 * It's the job of the movie to take care of that. */
450 G_DEFINE_TYPE (SwfdecTextLayout, swfdec_text_layout, G_TYPE_OBJECT)
452 static void
453 swfdec_text_layout_dispose (GObject *object)
455 SwfdecTextLayout *layout = SWFDEC_TEXT_LAYOUT (object);
457 g_signal_handlers_disconnect_matched (layout->text,
458 G_SIGNAL_MATCH_DATA, 0, 0, NULL, NULL, layout);
459 g_object_unref (layout->text);
460 layout->text = NULL;
461 g_sequence_free (layout->blocks);
462 layout->blocks = NULL;
464 G_OBJECT_CLASS (swfdec_text_layout_parent_class)->dispose (object);
467 static void
468 swfdec_text_layout_class_init (SwfdecTextLayoutClass *klass)
470 GObjectClass *object_class = G_OBJECT_CLASS (klass);
472 object_class->dispose = swfdec_text_layout_dispose;
475 static void
476 swfdec_text_layout_init (SwfdecTextLayout *layout)
478 layout->width = G_MAXINT; /* G_MAXUINT causes overflow */
479 layout->scale = 1.0;
480 layout->blocks = g_sequence_new (swfdec_text_block_free);
483 SwfdecTextLayout *
484 swfdec_text_layout_new (SwfdecTextBuffer *buffer)
486 SwfdecTextLayout *layout;
488 g_return_val_if_fail (SWFDEC_IS_TEXT_BUFFER (buffer), NULL);
490 layout = g_object_new (SWFDEC_TYPE_TEXT_LAYOUT, NULL);
491 layout->text = g_object_ref (buffer);
492 g_signal_connect_swapped (buffer, "text-changed",
493 G_CALLBACK (swfdec_text_layout_invalidate), layout);
495 return layout;
498 void
499 swfdec_text_layout_set_wrap_width (SwfdecTextLayout *layout, guint width)
501 g_return_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout));
503 if (layout->width == width)
504 return;
506 layout->width = width;
507 swfdec_text_layout_invalidate (layout);
510 guint
511 swfdec_text_layout_get_wrap_width (SwfdecTextLayout *layout)
513 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), -1);
515 return layout->width;
518 void
519 swfdec_text_layout_set_word_wrap (SwfdecTextLayout *layout, gboolean word_wrap)
521 g_return_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout));
523 if (layout->word_wrap == word_wrap)
524 return;
526 layout->word_wrap = word_wrap;
527 swfdec_text_layout_invalidate (layout);
530 gboolean
531 swfdec_text_layout_get_word_wrap (SwfdecTextLayout *layout)
533 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), -1);
535 return layout->word_wrap;
538 gboolean
539 swfdec_text_layout_get_password (SwfdecTextLayout *layout)
541 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), FALSE);
543 return layout->password;
546 void
547 swfdec_text_layout_set_password (SwfdecTextLayout *layout, gboolean password)
549 g_return_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout));
551 if (layout->password == password)
552 return;
554 layout->password = password;
555 swfdec_text_layout_invalidate (layout);
558 double
559 swfdec_text_layout_get_scale (SwfdecTextLayout *layout)
561 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), 1.0);
563 return layout->scale;
566 void
567 swfdec_text_layout_set_scale (SwfdecTextLayout *layout, double scale)
569 g_return_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout));
570 g_return_if_fail (scale > 0);
572 if (layout->scale == scale)
573 return;
575 layout->scale = scale;
576 swfdec_text_layout_invalidate (layout);
580 * swfdec_text_layout_get_width:
581 * @layout: the layout
583 * Computes the width of the layout in pixels. Note that the width can still
584 * exceed the width set with swfdec_text_layout_set_width() if some
585 * words are too long. Computing the width takes a long time, so it might be
586 * useful to cache the value.
588 * Returns: The width in pixels
590 guint
591 swfdec_text_layout_get_width (SwfdecTextLayout *layout)
593 GSequenceIter *iter;
594 SwfdecTextBlock *block;
596 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), 0);
598 if (layout->layout_width)
599 return layout->layout_width;
601 swfdec_text_layout_ensure (layout);
603 for (iter = g_sequence_get_begin_iter (layout->blocks);
604 !g_sequence_iter_is_end (iter);
605 iter = g_sequence_iter_next (iter)) {
606 block = g_sequence_get (iter);
607 layout->layout_width = MAX (layout->layout_width, (guint) block->rect.x + block->rect.width);
610 return layout->layout_width;
614 * swfdec_text_layout_get_height:
615 * @layout: the layout
617 * Computes the height of the layout in pixels. This is a fast operation.
619 * Returns: The height of the layout in pixels
621 guint
622 swfdec_text_layout_get_height (SwfdecTextLayout *layout)
624 GSequenceIter *iter;
625 SwfdecTextBlock *block;
627 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), 0);
629 swfdec_text_layout_ensure (layout);
631 if (swfdec_text_buffer_get_length (layout->text) == 0)
632 return 0;
633 iter = g_sequence_iter_prev (g_sequence_get_end_iter (layout->blocks));
634 block = g_sequence_get (iter);
635 return block->rect.y + block->rect.height;
638 guint
639 swfdec_text_layout_get_n_rows (SwfdecTextLayout *layout)
641 GSequenceIter *iter;
642 SwfdecTextBlock *block;
644 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), 0);
646 swfdec_text_layout_ensure (layout);
648 iter = g_sequence_iter_prev (g_sequence_get_end_iter (layout->blocks));
649 block = g_sequence_get (iter);
650 return block->row + pango_layout_get_line_count (block->layout);
653 static GSequenceIter *
654 swfdec_text_layout_find_row (SwfdecTextLayout *layout, guint row)
656 GSequenceIter *begin, *end, *mid;
657 SwfdecTextBlock *cur;
659 begin = g_sequence_get_begin_iter (layout->blocks);
660 end = g_sequence_iter_prev (g_sequence_get_end_iter (layout->blocks));
661 while (begin != end) {
662 mid = g_sequence_range_get_midpoint (begin, end);
663 if (mid == begin)
664 mid = g_sequence_iter_next (mid);
665 cur = g_sequence_get (mid);
666 if (cur->row > row)
667 end = g_sequence_iter_prev (mid);
668 else
669 begin = mid;
671 return begin;
675 * swfdec_text_layout_get_visible_rows:
676 * @layout: the layout
677 * @row: the first visible row
678 * @height: the height in pixels of visible text
680 * Queries the number of rows that would be rendered, if rendering were to start
681 * with @row and had to fit into @height pixels.
683 * Returns: the number of rows that would be rendered
685 guint
686 swfdec_text_layout_get_visible_rows (SwfdecTextLayout *layout, guint row, guint height)
688 GSequenceIter *iter;
689 SwfdecTextBlock *block;
690 PangoRectangle extents;
691 guint count = 0;
693 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), 1);
694 g_return_val_if_fail (row < swfdec_text_layout_get_n_rows (layout), 1);
696 swfdec_text_layout_ensure (layout);
698 iter = swfdec_text_layout_find_row (layout, row);
699 block = g_sequence_get (iter);
700 row -= block->row;
702 do {
703 block = g_sequence_get (iter);
704 for (;row < (guint) pango_layout_get_line_count (block->layout); row++) {
705 PangoLayoutLine *line = pango_layout_get_line_readonly (block->layout, row);
707 pango_layout_line_get_pixel_extents (line, NULL, &extents);
708 if (extents.height > (int) height)
709 goto out;
710 height -= extents.height;
711 count++;
713 row = 0;
714 if ((int) height <= pango_layout_get_spacing (block->layout) / PANGO_SCALE)
715 goto out;
716 height -= pango_layout_get_spacing (block->layout) / PANGO_SCALE;
717 iter = g_sequence_iter_next (iter);
718 } while (!g_sequence_iter_is_end (iter));
720 out:
721 return MAX (count, 1);
725 * swfdec_text_layout_get_visible_rows_end:
726 * @layout: the layout
727 * @height: the height in pixels
729 * Computes how many rows will be visible if only the last rows would be
730 * displayed. This is useful for computing maximal scroll offsets.
732 * Returns: The number of rows visible at the bottom.
734 guint
735 swfdec_text_layout_get_visible_rows_end (SwfdecTextLayout *layout, guint height)
737 GSequenceIter *iter;
738 SwfdecTextBlock *block;
739 PangoRectangle extents;
740 guint row, count = 0;
742 g_return_val_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout), 1);
744 swfdec_text_layout_ensure (layout);
745 iter = g_sequence_get_end_iter (layout->blocks);
747 do {
748 iter = g_sequence_iter_prev (iter);
749 block = g_sequence_get (iter);
750 if ((int) height <= pango_layout_get_spacing (block->layout) / PANGO_SCALE)
751 goto out;
752 height -= pango_layout_get_spacing (block->layout) / PANGO_SCALE;
753 for (row = pango_layout_get_line_count (block->layout); row > 0; row--) {
754 PangoLayoutLine *line = pango_layout_get_line_readonly (block->layout, row - 1);
756 pango_layout_line_get_pixel_extents (line, NULL, &extents);
757 if (extents.height > (int) height)
758 goto out;
759 height -= extents.height;
760 count++;
762 } while (!g_sequence_iter_is_begin (iter));
764 out:
765 return MAX (count, 1);
768 void
769 swfdec_text_layout_get_ascent_descent (SwfdecTextLayout *layout, int *ascent,
770 int *descent)
772 SwfdecTextBlock *block;
773 GSequenceIter *iter;
774 PangoLayoutLine *line;
775 PangoRectangle rect;
777 g_return_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout));
778 g_return_if_fail (ascent != NULL);
779 g_return_if_fail (descent != NULL);
781 if (swfdec_text_buffer_get_length (layout->text) == 0) {
782 *ascent = 0;
783 *descent = 0;
784 return;
787 swfdec_text_layout_ensure (layout);
788 iter = g_sequence_get_begin_iter (layout->blocks);
789 block = g_sequence_get (iter);
790 line = pango_layout_get_line_readonly (block->layout, 0);
791 pango_layout_line_get_pixel_extents (line, NULL, &rect);
793 *ascent = PANGO_ASCENT (rect) - 1; /* don't ask me... */
794 *descent = PANGO_DESCENT (rect);
797 static int
798 swfdec_text_layout_get_line_offset (SwfdecTextLayout *layout,
799 SwfdecTextBlock *block, PangoLayoutLine *line)
801 PangoAlignment align;
802 int width, diff;
804 align = pango_layout_get_alignment (block->layout);
805 if (align == PANGO_ALIGN_LEFT)
806 return 0;
808 /* FIXME: realign lines that are too long */
809 pango_layout_get_pixel_size (block->layout, &width, NULL);
810 diff = layout->width - width;
811 if (align == PANGO_ALIGN_CENTER)
812 diff /= 2;
813 else
814 diff -= 1;
816 return diff;
819 static PangoAttrList *
820 swfdec_text_layout_modify_attributes (SwfdecTextLayout *layout,
821 SwfdecTextBlock *block, const SwfdecColorTransform *ctrans, SwfdecColor focus)
823 gsize sel_start, sel_end;
824 PangoAttrList *old, *new;
825 PangoAttrIterator *iter;
827 if (swfdec_color_transform_is_identity (ctrans)) {
828 /* if we're not focused, there's no need to draw a selection */
829 if (!focus)
830 return NULL;
831 swfdec_text_buffer_get_selection (layout->text, &sel_start, &sel_end);
832 /* no selection, we draw a cursor instead */
833 if (sel_start == sel_end)
834 return NULL;
835 /* selection outside of block's range */
836 if (sel_start >= block->end || sel_end <= block->start)
837 return NULL;
838 } else {
839 if (focus) {
840 swfdec_text_buffer_get_selection (layout->text, &sel_start, &sel_end);
841 } else {
842 sel_start = sel_end = 0;
845 if (sel_start <= block->start)
846 sel_start = 0;
847 else
848 sel_start -= block->start;
849 if (sel_end <= block->start)
850 sel_end = 0;
851 else
852 sel_end -= block->start;
854 old = pango_layout_get_attributes (block->layout);
855 pango_attr_list_ref (old);
856 new = pango_attr_list_copy (old);
857 /* we create an iterator through the old list, so we know it
858 * never gets modified. As the new list is identical to the old one, it
859 * achieves exactly what we want. */
860 iter = pango_attr_list_get_iterator (old);
861 do {
862 guint cur_start, cur_end;
863 PangoAttrColor *color_attr;
864 SwfdecColor color;
865 pango_attr_iterator_range (iter, (int *) &cur_start, (int *) &cur_end);
866 if (cur_end == G_MAXINT)
867 break;
868 color_attr = (PangoAttrColor *) pango_attr_iterator_get (iter, PANGO_ATTR_FOREGROUND);
869 /* must hold as we explicitly set color attributes when creating
870 * the list initially */
871 g_assert (color_attr);
872 color = SWFDEC_COLOR_COMBINE (color_attr->color.red >> 8,
873 color_attr->color.green >> 8, color_attr->color.blue >> 8, 0xFF);
874 color = swfdec_color_apply_transform (color, ctrans);
875 /* We differentiate three ranges: before selection, in selection and after selection */
876 if (cur_start < sel_start) {
877 PangoAttribute *fg = pango_attr_foreground_new (SWFDEC_COLOR_RED (color) * 0x101,
878 SWFDEC_COLOR_GREEN (color) * 0x101, SWFDEC_COLOR_BLUE (color) * 0x101);
879 fg->start_index = cur_start;
880 fg->end_index = MIN (cur_end, sel_start);
881 pango_attr_list_change (new, fg);
883 if (sel_start < cur_end && sel_end > cur_start) {
884 PangoAttribute *fg = pango_attr_foreground_new (SWFDEC_COLOR_RED (focus) * 0x101,
885 SWFDEC_COLOR_GREEN (focus) * 0x101, SWFDEC_COLOR_BLUE (focus) * 0x101);
886 PangoAttribute *bg = pango_attr_background_new (SWFDEC_COLOR_RED (color) * 0x101,
887 SWFDEC_COLOR_GREEN (color) * 0x101, SWFDEC_COLOR_BLUE (color) * 0x101);
888 fg->start_index = bg->start_index = MAX (cur_start, sel_start);
889 fg->end_index = bg->end_index = MIN (cur_end, sel_end);
890 pango_attr_list_change (new, fg);
891 pango_attr_list_change (new, bg);
893 if (cur_end > sel_end) {
894 PangoAttribute *fg = pango_attr_foreground_new (SWFDEC_COLOR_RED (color) * 0x101,
895 SWFDEC_COLOR_GREEN (color) * 0x101, SWFDEC_COLOR_BLUE (color) * 0x101);
896 fg->start_index = MAX (sel_end, cur_start);
897 fg->end_index = cur_end;
898 pango_attr_list_change (new, fg);
900 } while (pango_attr_iterator_next (iter));
901 pango_layout_set_attributes (block->layout, new);
902 pango_attr_list_unref (new);
903 pango_attr_iterator_destroy (iter);
904 return old;
908 * swfdec_text_layout_render:
909 * @layout: the layout to render
910 * @cr: the Cairo context to render to. The context will be transformed so that
911 * (0, 0) points to where the @row should be rendered.
912 * @ctrans: The color transform to apply.
913 * @row: index of the first row to render.
914 * @height: The height in pixels of the visible area.
915 * @focus: color to invert the selection with, 0 if no focus
917 * Renders the contents of the layout into the given Cairo context.
919 void
920 swfdec_text_layout_render (SwfdecTextLayout *layout, cairo_t *cr,
921 const SwfdecColorTransform *ctrans, guint row, guint height, SwfdecColor focus)
923 GSequenceIter *iter;
924 SwfdecTextBlock *block;
925 PangoRectangle extents;
926 PangoAttrList *attr;
927 SwfdecColor cursor_color;
928 gsize cursor;
929 int cursor_index; /* Pango neds proper types... */
930 gboolean first_line = TRUE;
932 g_return_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout));
933 g_return_if_fail (cr != NULL);
934 g_return_if_fail (ctrans != NULL);
935 g_return_if_fail (row < swfdec_text_layout_get_n_rows (layout));
937 swfdec_text_layout_ensure (layout);
939 if (!focus || swfdec_text_buffer_has_selection (layout->text)) {
940 cursor_color = 0;
941 cursor = G_MAXSIZE;
942 } else {
943 cursor_color = swfdec_text_buffer_get_attributes (layout->text,
944 swfdec_text_buffer_get_length (layout->text))->color;
945 cursor_color = swfdec_color_apply_transform (cursor_color, ctrans);
946 cursor_color = SWFDEC_COLOR_OPAQUE (cursor_color);
947 cursor = swfdec_text_buffer_get_cursor (layout->text);
949 iter = swfdec_text_layout_find_row (layout, row);
950 block = g_sequence_get (iter);
951 row -= block->row;
952 do {
953 block = g_sequence_get (iter);
954 cursor_index = cursor - block->start;
955 pango_cairo_update_layout (cr, block->layout);
956 cairo_translate (cr, block->rect.x, 0);
957 if (block->bullet && row == 0) {
958 SWFDEC_FIXME ("render bullet");
960 attr = swfdec_text_layout_modify_attributes (layout, block, ctrans, focus);
961 for (;row < (guint) pango_layout_get_line_count (block->layout); row++) {
962 PangoLayoutLine *line = pango_layout_get_line_readonly (block->layout, row);
963 int xoffset = swfdec_text_layout_get_line_offset (layout, block, line);
965 pango_layout_line_get_pixel_extents (line, NULL, &extents);
966 if (extents.height > (int) height && !first_line) {
967 if (attr) {
968 pango_layout_set_attributes (block->layout, attr);
969 pango_attr_list_unref (attr);
971 return;
973 first_line = FALSE;
974 cairo_translate (cr, xoffset, - extents.y);
975 pango_cairo_show_layout_line (cr, line);
976 if (line->start_index + line->length >= cursor_index &&
977 line->start_index <= cursor_index &&
978 (line->start_index + line->length != cursor_index ||
979 (gsize) line->start_index + line->length == block->end - block->start)) {
980 int x_index;
981 /* FIXME: implement (trailing for) RTL */
982 pango_layout_line_index_to_x (line, cursor_index, FALSE, &x_index);
983 x_index = PANGO_PIXELS (x_index);
984 swfdec_color_set_source (cr, cursor_color);
985 cairo_set_line_width (cr, 1.0);
986 cairo_move_to (cr, x_index + 0.5, extents.y);
987 cairo_rel_line_to (cr, 0, extents.height);
988 cairo_stroke (cr);
990 height -= extents.height;
991 cairo_translate (cr, - xoffset, extents.height + extents.y);
993 if (attr) {
994 pango_layout_set_attributes (block->layout, attr);
995 pango_attr_list_unref (attr);
997 if ((int) height <= pango_layout_get_spacing (block->layout) / PANGO_SCALE)
998 return;
999 height -= pango_layout_get_spacing (block->layout) / PANGO_SCALE;
1000 cairo_translate (cr, -block->rect.x, pango_layout_get_spacing (block->layout) / PANGO_SCALE);
1001 row = 0;
1002 iter = g_sequence_iter_next (iter);
1003 } while (!g_sequence_iter_is_end (iter));
1007 * swfdec_text_layout_query_position:
1008 * @layout: the layout to query
1009 * @row: the start of this row indicates the (0,0) position to which @x and @y
1010 * are relative
1011 * @x: x offset
1012 * @y: y offset
1013 * @index_: %NULL or variable that will be set to the index in the layout's
1014 * buffer that corresponds to the position of the pressed grapheme.
1015 * @hit: %NULL or variable that will be set to %TRUE if the coordinate is inside
1016 * the glyhs's extents and %FALSE otherwise
1017 * @trailing: %NULL or variable that will be set to thenumber of characters
1018 * inside the grapheme that have been passed.
1020 * Magic function to query everything of interest about the string at a current
1021 * position inisde the @layout.
1023 void
1024 swfdec_text_layout_query_position (SwfdecTextLayout *layout, guint row,
1025 int x, int y, gsize *index_, gboolean *hit, int *trailing)
1027 GSequenceIter *iter;
1028 SwfdecTextBlock *block;
1029 PangoRectangle extents;
1031 g_return_if_fail (SWFDEC_IS_TEXT_LAYOUT (layout));
1032 g_return_if_fail (row < swfdec_text_layout_get_n_rows (layout));
1034 swfdec_text_layout_ensure (layout);
1036 iter = swfdec_text_layout_find_row (layout, row);
1037 block = g_sequence_get (iter);
1038 row -= block->row;
1040 do {
1041 block = g_sequence_get (iter);
1042 if (y < 0) {
1043 if (index_)
1044 *index_ = block->start;
1045 if (hit)
1046 *hit = FALSE;
1047 if (trailing)
1048 *trailing = 0;
1049 return;
1052 for (;row < (guint) pango_layout_get_line_count (block->layout); row++) {
1053 PangoLayoutLine *line = pango_layout_get_line_readonly (block->layout, row);
1055 pango_layout_line_get_pixel_extents (line, NULL, &extents);
1056 if (extents.height > y) {
1057 gboolean tmp;
1058 int ind;
1059 x -= swfdec_text_layout_get_line_offset (layout, block, line);
1060 tmp = pango_layout_line_x_to_index (line, x * PANGO_SCALE, &ind, trailing);
1061 if (hit)
1062 *hit = tmp;
1063 if (index_)
1064 *index_ = block->start + ind;
1065 return;
1067 y -= extents.height;
1069 y -= pango_layout_get_spacing (block->layout) / PANGO_SCALE;
1070 row = 0;
1071 iter = g_sequence_iter_next (iter);
1072 } while (!g_sequence_iter_is_end (iter));
1074 if (index_)
1075 *index_ = swfdec_text_buffer_get_length (layout->text);
1076 if (hit)
1077 *hit = FALSE;
1078 if (trailing)
1079 *trailing = 0;