1 | /* Pango |
2 | * ellipsize.c: Routine to ellipsize layout lines |
3 | * |
4 | * Copyright (C) 2004 Red Hat Software |
5 | * |
6 | * This library is free software; you can redistribute it and/or |
7 | * modify it under the terms of the GNU Library General Public |
8 | * License as published by the Free Software Foundation; either |
9 | * version 2 of the License, or (at your option) any later version. |
10 | * |
11 | * This library is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | * Library General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU Library General Public |
17 | * License along with this library; if not, write to the |
18 | * Free Software Foundation, Inc., 59 Temple Place - Suite 330, |
19 | * Boston, MA 02111-1307, USA. |
20 | */ |
21 | |
22 | #include "config.h" |
23 | #include <string.h> |
24 | |
25 | #include "pango-glyph-item.h" |
26 | #include "pango-layout-private.h" |
27 | #include "pango-font-private.h" |
28 | #include "pango-attributes-private.h" |
29 | #include "pango-impl-utils.h" |
30 | |
31 | typedef struct _EllipsizeState EllipsizeState; |
32 | typedef struct _RunInfo RunInfo; |
33 | typedef struct _LineIter LineIter; |
34 | |
35 | |
36 | /* Overall, the way we ellipsize is we grow a "gap" out from an original |
37 | * gap center position until: |
38 | * |
39 | * line_width - gap_width + ellipsize_width <= goal_width |
40 | * |
41 | * Line: [-------------------------------------------] |
42 | * Runs: [------)[---------------)[------------------] |
43 | * Gap center: * |
44 | * Gap: [----------------------] |
45 | * |
46 | * The gap center may be at the start or end in which case the gap grows |
47 | * in only one direction. |
48 | * |
49 | * Note the line and last run are logically closed at the end; this allows |
50 | * us to use a gap position at x=line_width and still have it be part of |
51 | * of a run. |
52 | * |
53 | * We grow the gap out one "span" at a time, where a span is simply a |
54 | * consecutive run of clusters that we can't interrupt with an ellipsis. |
55 | * |
56 | * When choosing whether to grow the gap at the start or the end, we |
57 | * calculate the next span to remove in both directions and see which |
58 | * causes the smaller increase in: |
59 | * |
60 | * MAX (gap_end - gap_center, gap_start - gap_center) |
61 | * |
62 | * All computations are done using logical order; the ellipsization |
63 | * process occurs before the runs are ordered into visual order. |
64 | */ |
65 | |
66 | /* Keeps information about a single run */ |
67 | struct _RunInfo |
68 | { |
69 | PangoGlyphItem *run; |
70 | int start_offset; /* Character offset of run start */ |
71 | int width; /* Width of run in Pango units */ |
72 | }; |
73 | |
74 | /* Iterator to a position within the ellipsized line */ |
75 | struct _LineIter |
76 | { |
77 | PangoGlyphItemIter run_iter; |
78 | int run_index; |
79 | }; |
80 | |
81 | /* State of ellipsization process */ |
82 | struct _EllipsizeState |
83 | { |
84 | PangoLayout *layout; /* Layout being ellipsized */ |
85 | PangoAttrList *attrs; /* Attributes used for itemization/shaping */ |
86 | |
87 | RunInfo *run_info; /* Array of information about each run */ |
88 | int n_runs; |
89 | |
90 | int total_width; /* Original width of line in Pango units */ |
91 | int gap_center; /* Goal for center of gap */ |
92 | |
93 | PangoGlyphItem *ellipsis_run; /* Run created to hold ellipsis */ |
94 | int ellipsis_width; /* Width of ellipsis, in Pango units */ |
95 | int ellipsis_is_cjk; /* Whether the first character in the ellipsized |
96 | * is wide; this triggers us to try to use a |
97 | * mid-line ellipsis instead of a baseline |
98 | */ |
99 | |
100 | PangoAttrIterator *line_start_attr; /* Cached PangoAttrIterator for the start of the run */ |
101 | |
102 | LineIter gap_start_iter; /* Iteratator pointig to the first cluster in gap */ |
103 | int gap_start_x; /* x position of start of gap, in Pango units */ |
104 | PangoAttrIterator *gap_start_attr; /* Attribute iterator pointing to a range containing |
105 | * the first character in gap */ |
106 | |
107 | LineIter gap_end_iter; /* Iterator pointing to last cluster in gap */ |
108 | int gap_end_x; /* x position of end of gap, in Pango units */ |
109 | |
110 | PangoShapeFlags shape_flags; |
111 | }; |
112 | |
113 | /* Compute global information needed for the itemization process |
114 | */ |
115 | static void |
116 | init_state (EllipsizeState *state, |
117 | PangoLayoutLine *line, |
118 | PangoAttrList *attrs, |
119 | PangoShapeFlags shape_flags) |
120 | { |
121 | GSList *l; |
122 | int i; |
123 | int start_offset; |
124 | |
125 | state->layout = line->layout; |
126 | if (attrs) |
127 | state->attrs = pango_attr_list_ref (list: attrs); |
128 | else |
129 | state->attrs = pango_attr_list_new (); |
130 | |
131 | state->shape_flags = shape_flags; |
132 | |
133 | state->n_runs = g_slist_length (list: line->runs); |
134 | state->run_info = g_new (RunInfo, state->n_runs); |
135 | |
136 | start_offset = pango_utf8_strlen (p: line->layout->text, |
137 | max: line->start_index); |
138 | |
139 | state->total_width = 0; |
140 | for (l = line->runs, i = 0; l; l = l->next, i++) |
141 | { |
142 | PangoGlyphItem *run = l->data; |
143 | int width = pango_glyph_string_get_width (glyphs: run->glyphs); |
144 | state->run_info[i].run = run; |
145 | state->run_info[i].width = width; |
146 | state->run_info[i].start_offset = start_offset; |
147 | state->total_width += width; |
148 | |
149 | start_offset += run->item->num_chars; |
150 | } |
151 | |
152 | state->ellipsis_run = NULL; |
153 | state->ellipsis_is_cjk = FALSE; |
154 | state->line_start_attr = NULL; |
155 | state->gap_start_attr = NULL; |
156 | } |
157 | |
158 | /* Cleanup memory allocation |
159 | */ |
160 | static void |
161 | free_state (EllipsizeState *state) |
162 | { |
163 | pango_attr_list_unref (list: state->attrs); |
164 | if (state->line_start_attr) |
165 | pango_attr_iterator_destroy (iterator: state->line_start_attr); |
166 | if (state->gap_start_attr) |
167 | pango_attr_iterator_destroy (iterator: state->gap_start_attr); |
168 | g_free (mem: state->run_info); |
169 | } |
170 | |
171 | /* Computes the width of a single cluster |
172 | */ |
173 | static int |
174 | get_cluster_width (LineIter *iter) |
175 | { |
176 | PangoGlyphItemIter *run_iter = &iter->run_iter; |
177 | PangoGlyphString *glyphs = run_iter->glyph_item->glyphs; |
178 | int width = 0; |
179 | int i; |
180 | |
181 | if (run_iter->start_glyph < run_iter->end_glyph) /* LTR */ |
182 | { |
183 | for (i = run_iter->start_glyph; i < run_iter->end_glyph; i++) |
184 | width += glyphs->glyphs[i].geometry.width; |
185 | } |
186 | else /* RTL */ |
187 | { |
188 | for (i = run_iter->start_glyph; i > run_iter->end_glyph; i--) |
189 | width += glyphs->glyphs[i].geometry.width; |
190 | } |
191 | |
192 | return width; |
193 | } |
194 | |
195 | /* Move forward one cluster. Returns %FALSE if we were already at the end |
196 | */ |
197 | static gboolean |
198 | line_iter_next_cluster (EllipsizeState *state, |
199 | LineIter *iter) |
200 | { |
201 | if (!pango_glyph_item_iter_next_cluster (iter: &iter->run_iter)) |
202 | { |
203 | if (iter->run_index == state->n_runs - 1) |
204 | return FALSE; |
205 | else |
206 | { |
207 | iter->run_index++; |
208 | pango_glyph_item_iter_init_start (iter: &iter->run_iter, |
209 | glyph_item: state->run_info[iter->run_index].run, |
210 | text: state->layout->text); |
211 | } |
212 | } |
213 | |
214 | return TRUE; |
215 | } |
216 | |
217 | /* Move backward one cluster. Returns %FALSE if we were already at the end |
218 | */ |
219 | static gboolean |
220 | line_iter_prev_cluster (EllipsizeState *state, |
221 | LineIter *iter) |
222 | { |
223 | if (!pango_glyph_item_iter_prev_cluster (iter: &iter->run_iter)) |
224 | { |
225 | if (iter->run_index == 0) |
226 | return FALSE; |
227 | else |
228 | { |
229 | iter->run_index--; |
230 | pango_glyph_item_iter_init_end (iter: &iter->run_iter, |
231 | glyph_item: state->run_info[iter->run_index].run, |
232 | text: state->layout->text); |
233 | } |
234 | } |
235 | |
236 | return TRUE; |
237 | } |
238 | |
239 | /* |
240 | * An ellipsization boundary is defined by two things |
241 | * |
242 | * - Starts a cluster - forced by structure of code |
243 | * - Starts a grapheme - checked here |
244 | * |
245 | * In the future we'd also like to add a check for cursive connectivity here. |
246 | * This should be an addition to `PangoGlyphVisAttr` |
247 | * |
248 | */ |
249 | |
250 | /* Checks if there is a ellipsization boundary before the cluster @iter points to |
251 | */ |
252 | static gboolean |
253 | starts_at_ellipsization_boundary (EllipsizeState *state, |
254 | LineIter *iter) |
255 | { |
256 | RunInfo *run_info = &state->run_info[iter->run_index]; |
257 | |
258 | if (iter->run_iter.start_char == 0 && iter->run_index == 0) |
259 | return TRUE; |
260 | |
261 | return state->layout->log_attrs[run_info->start_offset + iter->run_iter.start_char].is_cursor_position; |
262 | } |
263 | |
264 | /* Checks if there is a ellipsization boundary after the cluster @iter points to |
265 | */ |
266 | static gboolean |
267 | ends_at_ellipsization_boundary (EllipsizeState *state, |
268 | LineIter *iter) |
269 | { |
270 | RunInfo *run_info = &state->run_info[iter->run_index]; |
271 | |
272 | if (iter->run_iter.end_char == run_info->run->item->num_chars && iter->run_index == state->n_runs - 1) |
273 | return TRUE; |
274 | |
275 | return state->layout->log_attrs[run_info->start_offset + iter->run_iter.end_char + 1].is_cursor_position; |
276 | } |
277 | |
278 | /* Helper function to re-itemize a string of text |
279 | */ |
280 | static PangoItem * |
281 | itemize_text (EllipsizeState *state, |
282 | const char *text, |
283 | PangoAttrList *attrs) |
284 | { |
285 | GList *items; |
286 | PangoItem *item; |
287 | |
288 | items = pango_itemize (context: state->layout->context, text, start_index: 0, length: strlen (s: text), attrs, NULL); |
289 | g_assert (g_list_length (items) == 1); |
290 | |
291 | item = items->data; |
292 | g_list_free (list: items); |
293 | |
294 | return item; |
295 | } |
296 | |
297 | /* Shapes the ellipsis using the font and is_cjk information computed by |
298 | * update_ellipsis_shape() from the first character in the gap. |
299 | */ |
300 | static void |
301 | shape_ellipsis (EllipsizeState *state) |
302 | { |
303 | PangoAttrList attrs; |
304 | GSList *run_attrs; |
305 | PangoItem *item; |
306 | PangoGlyphString *glyphs; |
307 | GSList *l; |
308 | PangoAttribute *fallback; |
309 | const char *ellipsis_text; |
310 | int len; |
311 | int i; |
312 | |
313 | _pango_attr_list_init (list: &attrs); |
314 | |
315 | /* Create/reset state->ellipsis_run |
316 | */ |
317 | if (!state->ellipsis_run) |
318 | { |
319 | state->ellipsis_run = g_slice_new0 (PangoGlyphItem); |
320 | state->ellipsis_run->glyphs = pango_glyph_string_new (); |
321 | } |
322 | |
323 | if (state->ellipsis_run->item) |
324 | { |
325 | pango_item_free (item: state->ellipsis_run->item); |
326 | state->ellipsis_run->item = NULL; |
327 | } |
328 | |
329 | /* Create an attribute list |
330 | */ |
331 | run_attrs = pango_attr_iterator_get_attrs (iterator: state->gap_start_attr); |
332 | for (l = run_attrs; l; l = l->next) |
333 | { |
334 | PangoAttribute *attr = l->data; |
335 | attr->start_index = 0; |
336 | attr->end_index = G_MAXINT; |
337 | |
338 | pango_attr_list_insert (list: &attrs, attr); |
339 | } |
340 | |
341 | g_slist_free (list: run_attrs); |
342 | |
343 | fallback = pango_attr_fallback_new (FALSE); |
344 | fallback->start_index = 0; |
345 | fallback->end_index = G_MAXINT; |
346 | pango_attr_list_insert (list: &attrs, attr: fallback); |
347 | |
348 | /* First try using a specific ellipsis character in the best matching font |
349 | */ |
350 | if (state->ellipsis_is_cjk) |
351 | ellipsis_text = "\342\213\257" ; /* U+22EF: MIDLINE HORIZONTAL ELLIPSIS, used for CJK */ |
352 | else |
353 | ellipsis_text = "\342\200\246" ; /* U+2026: HORIZONTAL ELLIPSIS */ |
354 | |
355 | item = itemize_text (state, text: ellipsis_text, attrs: &attrs); |
356 | |
357 | /* If that fails we use "..." in the first matching font |
358 | */ |
359 | if (!item->analysis.font || |
360 | !pango_font_has_char (font: item->analysis.font, |
361 | wc: g_utf8_get_char (p: ellipsis_text))) |
362 | { |
363 | pango_item_free (item); |
364 | |
365 | /* Modify the fallback iter while it is inside the PangoAttrList; Don't try this at home |
366 | */ |
367 | ((PangoAttrInt *)fallback)->value = TRUE; |
368 | |
369 | ellipsis_text = "..." ; |
370 | item = itemize_text (state, text: ellipsis_text, attrs: &attrs); |
371 | } |
372 | |
373 | _pango_attr_list_destroy (list: &attrs); |
374 | |
375 | state->ellipsis_run->item = item; |
376 | |
377 | /* Now shape |
378 | */ |
379 | glyphs = state->ellipsis_run->glyphs; |
380 | |
381 | len = strlen (s: ellipsis_text); |
382 | pango_shape_with_flags (item_text: ellipsis_text, item_length: len, |
383 | paragraph_text: ellipsis_text, paragraph_length: len, |
384 | analysis: &item->analysis, glyphs, |
385 | flags: state->shape_flags); |
386 | |
387 | state->ellipsis_width = 0; |
388 | for (i = 0; i < glyphs->num_glyphs; i++) |
389 | state->ellipsis_width += glyphs->glyphs[i].geometry.width; |
390 | } |
391 | |
392 | /* Helper function to advance a PangoAttrIterator to a particular |
393 | * byte index. |
394 | */ |
395 | static void |
396 | advance_iterator_to (PangoAttrIterator *iter, |
397 | int new_index) |
398 | { |
399 | int start, end; |
400 | |
401 | do |
402 | { |
403 | pango_attr_iterator_range (iterator: iter, start: &start, end: &end); |
404 | if (end > new_index) |
405 | break; |
406 | } |
407 | while (pango_attr_iterator_next (iterator: iter)); |
408 | } |
409 | |
410 | /* Updates the shaping of the ellipsis if necessary when we move the |
411 | * position of the start of the gap. |
412 | * |
413 | * The shaping of the ellipsis is determined by two things: |
414 | * |
415 | * - The font attributes applied to the first character in the gap |
416 | * - Whether the first character in the gap is wide or not. If the |
417 | * first character is wide, then we assume that we are ellipsizing |
418 | * East-Asian text, so prefer a mid-line ellipsizes to a baseline |
419 | * ellipsis, since that's typical practice for Chinese/Japanese/Korean. |
420 | */ |
421 | static void |
422 | update_ellipsis_shape (EllipsizeState *state) |
423 | { |
424 | gboolean recompute = FALSE; |
425 | gunichar start_wc; |
426 | gboolean is_cjk; |
427 | |
428 | /* Unfortunately, we can only advance PangoAttrIterator forward; so each |
429 | * time we back up we need to go forward to find the new position. To make |
430 | * this not utterly slow, we cache an iterator at the start of the line |
431 | */ |
432 | if (!state->line_start_attr) |
433 | { |
434 | state->line_start_attr = pango_attr_list_get_iterator (list: state->attrs); |
435 | advance_iterator_to (iter: state->line_start_attr, new_index: state->run_info[0].run->item->offset); |
436 | } |
437 | |
438 | if (state->gap_start_attr) |
439 | { |
440 | /* See if the current attribute range contains the new start position |
441 | */ |
442 | int start, end; |
443 | |
444 | pango_attr_iterator_range (iterator: state->gap_start_attr, start: &start, end: &end); |
445 | |
446 | if (state->gap_start_iter.run_iter.start_index < start) |
447 | { |
448 | pango_attr_iterator_destroy (iterator: state->gap_start_attr); |
449 | state->gap_start_attr = NULL; |
450 | } |
451 | } |
452 | |
453 | /* Check whether we need to recompute the ellipsis because of new font attributes |
454 | */ |
455 | if (!state->gap_start_attr) |
456 | { |
457 | state->gap_start_attr = pango_attr_iterator_copy (iterator: state->line_start_attr); |
458 | advance_iterator_to (iter: state->gap_start_attr, |
459 | new_index: state->run_info[state->gap_start_iter.run_index].run->item->offset); |
460 | |
461 | recompute = TRUE; |
462 | } |
463 | |
464 | /* Check whether we need to recompute the ellipsis because we switch from CJK to not |
465 | * or vice-versa |
466 | */ |
467 | start_wc = g_utf8_get_char (p: state->layout->text + state->gap_start_iter.run_iter.start_index); |
468 | is_cjk = g_unichar_iswide (c: start_wc); |
469 | |
470 | if (is_cjk != state->ellipsis_is_cjk) |
471 | { |
472 | state->ellipsis_is_cjk = is_cjk; |
473 | recompute = TRUE; |
474 | } |
475 | |
476 | if (recompute) |
477 | shape_ellipsis (state); |
478 | } |
479 | |
480 | /* Computes the position of the gap center and finds the smallest span containing it |
481 | */ |
482 | static void |
483 | find_initial_span (EllipsizeState *state) |
484 | { |
485 | PangoGlyphItem *glyph_item; |
486 | PangoGlyphItemIter *run_iter; |
487 | gboolean have_cluster; |
488 | int i; |
489 | int x; |
490 | int cluster_width; |
491 | |
492 | switch (state->layout->ellipsize) |
493 | { |
494 | case PANGO_ELLIPSIZE_NONE: |
495 | default: |
496 | g_assert_not_reached (); |
497 | case PANGO_ELLIPSIZE_START: |
498 | state->gap_center = 0; |
499 | break; |
500 | case PANGO_ELLIPSIZE_MIDDLE: |
501 | state->gap_center = state->total_width / 2; |
502 | break; |
503 | case PANGO_ELLIPSIZE_END: |
504 | state->gap_center = state->total_width; |
505 | break; |
506 | } |
507 | |
508 | /* Find the run containing the gap center |
509 | */ |
510 | x = 0; |
511 | for (i = 0; i < state->n_runs; i++) |
512 | { |
513 | if (x + state->run_info[i].width > state->gap_center) |
514 | break; |
515 | |
516 | x += state->run_info[i].width; |
517 | } |
518 | |
519 | if (i == state->n_runs) /* Last run is a closed interval, so back off one run */ |
520 | { |
521 | i--; |
522 | x -= state->run_info[i].width; |
523 | } |
524 | |
525 | /* Find the cluster containing the gap center |
526 | */ |
527 | state->gap_start_iter.run_index = i; |
528 | run_iter = &state->gap_start_iter.run_iter; |
529 | glyph_item = state->run_info[i].run; |
530 | |
531 | cluster_width = 0; /* Quiet GCC, the line must have at least one cluster */ |
532 | for (have_cluster = pango_glyph_item_iter_init_start (iter: run_iter, glyph_item, text: state->layout->text); |
533 | have_cluster; |
534 | have_cluster = pango_glyph_item_iter_next_cluster (iter: run_iter)) |
535 | { |
536 | cluster_width = get_cluster_width (iter: &state->gap_start_iter); |
537 | |
538 | if (x + cluster_width > state->gap_center) |
539 | break; |
540 | |
541 | x += cluster_width; |
542 | } |
543 | |
544 | if (!have_cluster) /* Last cluster is a closed interval, so back off one cluster */ |
545 | x -= cluster_width; |
546 | |
547 | state->gap_end_iter = state->gap_start_iter; |
548 | |
549 | state->gap_start_x = x; |
550 | state->gap_end_x = x + cluster_width; |
551 | |
552 | /* Expand the gap to a full span |
553 | */ |
554 | while (!starts_at_ellipsization_boundary (state, iter: &state->gap_start_iter)) |
555 | { |
556 | line_iter_prev_cluster (state, iter: &state->gap_start_iter); |
557 | state->gap_start_x -= get_cluster_width (iter: &state->gap_start_iter); |
558 | } |
559 | |
560 | while (!ends_at_ellipsization_boundary (state, iter: &state->gap_end_iter)) |
561 | { |
562 | line_iter_next_cluster (state, iter: &state->gap_end_iter); |
563 | state->gap_end_x += get_cluster_width (iter: &state->gap_end_iter); |
564 | } |
565 | |
566 | update_ellipsis_shape (state); |
567 | } |
568 | |
569 | /* Removes one run from the start or end of the gap. Returns FALSE |
570 | * if there's nothing left to remove in either direction. |
571 | */ |
572 | static gboolean |
573 | remove_one_span (EllipsizeState *state) |
574 | { |
575 | LineIter new_gap_start_iter; |
576 | LineIter new_gap_end_iter; |
577 | int new_gap_start_x; |
578 | int new_gap_end_x; |
579 | int width; |
580 | |
581 | /* Find one span backwards and forward from the gap |
582 | */ |
583 | new_gap_start_iter = state->gap_start_iter; |
584 | new_gap_start_x = state->gap_start_x; |
585 | do |
586 | { |
587 | if (!line_iter_prev_cluster (state, iter: &new_gap_start_iter)) |
588 | break; |
589 | width = get_cluster_width (iter: &new_gap_start_iter); |
590 | new_gap_start_x -= width; |
591 | } |
592 | while (!starts_at_ellipsization_boundary (state, iter: &new_gap_start_iter) || |
593 | width == 0); |
594 | |
595 | new_gap_end_iter = state->gap_end_iter; |
596 | new_gap_end_x = state->gap_end_x; |
597 | do |
598 | { |
599 | if (!line_iter_next_cluster (state, iter: &new_gap_end_iter)) |
600 | break; |
601 | width = get_cluster_width (iter: &new_gap_end_iter); |
602 | new_gap_end_x += width; |
603 | } |
604 | while (!ends_at_ellipsization_boundary (state, iter: &new_gap_end_iter) || |
605 | width == 0); |
606 | |
607 | if (state->gap_end_x == new_gap_end_x && state->gap_start_x == new_gap_start_x) |
608 | return FALSE; |
609 | |
610 | /* In the case where we could remove a span from either end of the |
611 | * gap, we look at which causes the smaller increase in the |
612 | * MAX (gap_end - gap_center, gap_start - gap_center) |
613 | */ |
614 | if (state->gap_end_x == new_gap_end_x || |
615 | (state->gap_start_x != new_gap_start_x && |
616 | state->gap_center - new_gap_start_x < new_gap_end_x - state->gap_center)) |
617 | { |
618 | state->gap_start_iter = new_gap_start_iter; |
619 | state->gap_start_x = new_gap_start_x; |
620 | |
621 | update_ellipsis_shape (state); |
622 | } |
623 | else |
624 | { |
625 | state->gap_end_iter = new_gap_end_iter; |
626 | state->gap_end_x = new_gap_end_x; |
627 | } |
628 | |
629 | return TRUE; |
630 | } |
631 | |
632 | /* Fixes up the properties of the ellipsis run once we've determined the final extents |
633 | * of the gap |
634 | */ |
635 | static void |
636 | fixup_ellipsis_run (EllipsizeState *state, |
637 | int ) |
638 | { |
639 | PangoGlyphString *glyphs = state->ellipsis_run->glyphs; |
640 | PangoItem *item = state->ellipsis_run->item; |
641 | int level; |
642 | int i; |
643 | |
644 | /* Make the entire glyphstring into a single logical cluster */ |
645 | for (i = 0; i < glyphs->num_glyphs; i++) |
646 | { |
647 | glyphs->log_clusters[i] = 0; |
648 | glyphs->glyphs[i].attr.is_cluster_start = FALSE; |
649 | } |
650 | |
651 | glyphs->glyphs[0].attr.is_cluster_start = TRUE; |
652 | |
653 | glyphs->glyphs[glyphs->num_glyphs - 1].geometry.width += extra_width; |
654 | |
655 | /* Fix up the item to point to the entire elided text */ |
656 | item->offset = state->gap_start_iter.run_iter.start_index; |
657 | item->length = state->gap_end_iter.run_iter.end_index - item->offset; |
658 | item->num_chars = pango_utf8_strlen (p: state->layout->text + item->offset, max: item->length); |
659 | |
660 | /* The level for the item is the minimum level of the elided text */ |
661 | level = G_MAXINT; |
662 | for (i = state->gap_start_iter.run_index; i <= state->gap_end_iter.run_index; i++) |
663 | level = MIN (level, state->run_info[i].run->item->analysis.level); |
664 | |
665 | item->analysis.level = level; |
666 | |
667 | item->analysis.flags |= PANGO_ANALYSIS_FLAG_IS_ELLIPSIS; |
668 | } |
669 | |
670 | /* Computes the new list of runs for the line |
671 | */ |
672 | static GSList * |
673 | get_run_list (EllipsizeState *state) |
674 | { |
675 | PangoGlyphItem *partial_start_run = NULL; |
676 | PangoGlyphItem *partial_end_run = NULL; |
677 | GSList *result = NULL; |
678 | RunInfo *run_info; |
679 | PangoGlyphItemIter *run_iter; |
680 | int i; |
681 | |
682 | /* We first cut out the pieces of the starting and ending runs we want to |
683 | * preserve; we do the end first in case the end and the start are |
684 | * the same. Doing the start first would disturb the indices for the end. |
685 | */ |
686 | run_info = &state->run_info[state->gap_end_iter.run_index]; |
687 | run_iter = &state->gap_end_iter.run_iter; |
688 | if (run_iter->end_char != run_info->run->item->num_chars) |
689 | { |
690 | partial_end_run = run_info->run; |
691 | run_info->run = pango_glyph_item_split (orig: run_info->run, text: state->layout->text, |
692 | split_index: run_iter->end_index - run_info->run->item->offset); |
693 | } |
694 | |
695 | run_info = &state->run_info[state->gap_start_iter.run_index]; |
696 | run_iter = &state->gap_start_iter.run_iter; |
697 | if (run_iter->start_char != 0) |
698 | { |
699 | partial_start_run = pango_glyph_item_split (orig: run_info->run, text: state->layout->text, |
700 | split_index: run_iter->start_index - run_info->run->item->offset); |
701 | } |
702 | |
703 | /* Now assemble the new list of runs |
704 | */ |
705 | for (i = 0; i < state->gap_start_iter.run_index; i++) |
706 | result = g_slist_prepend (list: result, data: state->run_info[i].run); |
707 | |
708 | if (partial_start_run) |
709 | result = g_slist_prepend (list: result, data: partial_start_run); |
710 | |
711 | result = g_slist_prepend (list: result, data: state->ellipsis_run); |
712 | |
713 | if (partial_end_run) |
714 | result = g_slist_prepend (list: result, data: partial_end_run); |
715 | |
716 | for (i = state->gap_end_iter.run_index + 1; i < state->n_runs; i++) |
717 | result = g_slist_prepend (list: result, data: state->run_info[i].run); |
718 | |
719 | /* And free the ones we didn't use |
720 | */ |
721 | for (i = state->gap_start_iter.run_index; i <= state->gap_end_iter.run_index; i++) |
722 | pango_glyph_item_free (glyph_item: state->run_info[i].run); |
723 | |
724 | return g_slist_reverse (list: result); |
725 | } |
726 | |
727 | /* Computes the width of the line as currently ellipsized |
728 | */ |
729 | static int |
730 | current_width (EllipsizeState *state) |
731 | { |
732 | return state->total_width - (state->gap_end_x - state->gap_start_x) + state->ellipsis_width; |
733 | } |
734 | |
735 | /** |
736 | * _pango_layout_line_ellipsize: |
737 | * @line: a `PangoLayoutLine` |
738 | * @attrs: Attributes being used for itemization/shaping |
739 | * @shape_flags: Flags to use when shaping |
740 | * |
741 | * Given a `PangoLayoutLine` with the runs still in logical order, ellipsize |
742 | * it according the layout's policy to fit within the set width of the layout. |
743 | * |
744 | * Return value: whether the line had to be ellipsized |
745 | **/ |
746 | gboolean |
747 | _pango_layout_line_ellipsize (PangoLayoutLine *line, |
748 | PangoAttrList *attrs, |
749 | PangoShapeFlags shape_flags, |
750 | int goal_width) |
751 | { |
752 | EllipsizeState state; |
753 | gboolean is_ellipsized = FALSE; |
754 | |
755 | g_return_val_if_fail (line->layout->ellipsize != PANGO_ELLIPSIZE_NONE && goal_width >= 0, is_ellipsized); |
756 | |
757 | init_state (state: &state, line, attrs, shape_flags); |
758 | |
759 | if (state.total_width <= goal_width) |
760 | goto out; |
761 | |
762 | find_initial_span (state: &state); |
763 | |
764 | while (current_width (state: &state) > goal_width) |
765 | { |
766 | if (!remove_one_span (state: &state)) |
767 | break; |
768 | } |
769 | |
770 | fixup_ellipsis_run (state: &state, MAX (goal_width - current_width (&state), 0)); |
771 | |
772 | g_slist_free (list: line->runs); |
773 | line->runs = get_run_list (state: &state); |
774 | is_ellipsized = TRUE; |
775 | |
776 | out: |
777 | free_state (state: &state); |
778 | |
779 | return is_ellipsized; |
780 | } |
781 | |