The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

TextArea.vala in libbirdfont/Renderer

This file is a part of the Birdfont project.

Contributing

Send patches or pull requests to johan.mattsson.m@gmail.com.
Clone this repository: git clone https://github.com/johanmattssonm/birdfont.git

Revisions

View the latest version of libbirdfont/Renderer/TextArea.vala.
Fix compile time warnings
1 /* 2 Copyright (C) 2014 Johan Mattsson 3 4 This library is free software; you can redistribute it and/or modify 5 it under the terms of the GNU Lesser General Public License as 6 published by the Free Software Foundation; either version 3 of the 7 License, or (at your option) any later version. 8 9 This library is distributed in the hope that it will be useful, but 10 WITHOUT ANY WARRANTY; without even the implied warranty of 11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 Lesser General Public License for more details. 13 */ 14 15 using Cairo; 16 using Math; 17 18 namespace BirdFont { 19 20 public class TextArea : Widget { 21 22 public double min_width = 500; 23 public double min_height = 100; 24 public double font_size; 25 public double padding = 3.3; 26 public bool single_line = false; 27 public Color text_color = Color.black (); 28 29 public bool draw_carret { 30 get { return carret_is_visible; } 31 set { 32 carret_is_visible = value; 33 if (!value) { 34 update_selection = false; 35 selection_end = carret.copy (); 36 } 37 } 38 } 39 public bool carret_is_visible = false; 40 public bool draw_border = true; 41 42 public double width; 43 public double height; 44 45 Carret carret = new Carret (); 46 Carret selection_end = new Carret (); 47 bool update_selection = false; 48 public bool show_selection = false; 49 50 public signal void scroll (double pixels); 51 public signal void text_changed (string text); 52 public signal void enter (string text); 53 54 Gee.ArrayList<Paragraph> paragraphs = new Gee.ArrayList<Paragraph> (); 55 private const int DONE = -2; 56 57 int last_paragraph = 0; 58 string text; 59 int text_length; 60 61 Gee.ArrayList<TextUndoItem> undo_items = new Gee.ArrayList<TextUndoItem> (); 62 Gee.ArrayList<TextUndoItem> redo_items = new Gee.ArrayList<TextUndoItem> (); 63 64 bool store_undo_state_at_next_event = false; 65 66 public bool editable; 67 68 public TextArea (double font_size = 20, Color? c = null) { 69 this.font_size = font_size; 70 width = min_width; 71 height = min_height; 72 editable = true; 73 74 if (c != null) { 75 text_color = (!) c; 76 } 77 } 78 79 public override void focus (bool focus) { 80 draw_carret = focus; 81 } 82 83 public override double get_height () { 84 return height + 2 * padding; 85 } 86 87 public override double get_width () { 88 return width + 2 * padding; 89 } 90 91 public void set_font_size (double z) { 92 font_size = z; 93 } 94 95 bool generate_paragraphs () { 96 Paragraph paragraph; 97 98 int next_paragraph = -1; 99 100 if (is_null (text)) { 101 warning ("No text"); 102 return false; 103 } 104 105 if (last_paragraph == DONE) { 106 return false; 107 } 108 109 next_paragraph = text.index_of ("\n", last_paragraph); 110 111 if (next_paragraph == -1) { 112 paragraph = new Paragraph (text.substring (last_paragraph), font_size, paragraphs.size, text_color); 113 paragraphs.add (paragraph); 114 last_paragraph = DONE; 115 } else { 116 next_paragraph += "\n".length; 117 paragraph = new Paragraph (text.substring (last_paragraph, next_paragraph - last_paragraph), font_size, paragraphs.size, text_color); 118 paragraphs.add (paragraph); 119 last_paragraph = next_paragraph; 120 } 121 122 return last_paragraph != DONE; 123 } 124 125 void generate_all_paragraphs () { 126 while (generate_paragraphs ()) { 127 } 128 } 129 130 public override void key_press (uint keyval) { 131 unichar c; 132 TextUndoItem ui; 133 134 if (!editable) { 135 return; 136 } 137 138 c = (unichar) keyval; 139 140 switch (c) { 141 case ' ': 142 store_undo_edit_state (); 143 add_character (keyval); 144 break; 145 case 'a': 146 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 147 select_all (); 148 } else { 149 add_character (keyval); 150 } 151 break; 152 case 'c': 153 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 154 ClipTool.copy_text (this); 155 } else { 156 add_character (keyval); 157 } 158 break; 159 case 'v': 160 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 161 ClipTool.paste_text (this); 162 store_undo_state_at_next_event = true; 163 } else { 164 add_character (keyval); 165 } 166 break; 167 case 'y': 168 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 169 redo (); 170 } else { 171 add_character (keyval); 172 } 173 break; 174 case 'z': 175 if (KeyBindings.has_ctrl () || KeyBindings.has_logo ()) { 176 undo (); 177 } else { 178 add_character (keyval); 179 } 180 break; 181 case Key.RIGHT: 182 check_selection (); 183 move_carret_next (); 184 break; 185 case Key.LEFT: 186 check_selection (); 187 move_carret_previous (); 188 break; 189 case Key.DOWN: 190 check_selection (); 191 move_carret_next_row (); 192 break; 193 case Key.UP: 194 check_selection (); 195 move_carret_previous_row (); 196 break; 197 case Key.END: 198 check_selection (); 199 move_carret_to_end_of_line (); 200 break; 201 case Key.HOME: 202 check_selection (); 203 move_carret_to_beginning_of_line (); 204 break; 205 case Key.BACK_SPACE: 206 if (has_selection ()) { 207 ui = delete_selected_text (); 208 undo_items.add (ui); 209 redo_items.clear (); 210 store_undo_state_at_next_event = true; 211 } else { 212 ui = remove_last_character (); 213 undo_items.add (ui); 214 redo_items.clear (); 215 store_undo_state_at_next_event = true; 216 } 217 text_changed (get_text ()); 218 break; 219 case Key.ENTER: 220 store_undo_edit_state (); 221 insert_text ("\n"); 222 223 if (single_line) { 224 enter (get_text ()); 225 } 226 break; 227 case Key.DEL: 228 if (has_selection ()) { 229 ui = delete_selected_text (); 230 undo_items.add (ui); 231 redo_items.clear (); 232 store_undo_state_at_next_event = true; 233 } else { 234 ui = remove_next_character (); 235 undo_items.add (ui); 236 redo_items.clear (); 237 store_undo_state_at_next_event = true; 238 } 239 text_changed (get_text ()); 240 break; 241 default: 242 if (!KeyBindings.has_ctrl () && !KeyBindings.has_logo ()) { 243 add_character (keyval); 244 } 245 break; 246 } 247 248 GlyphCanvas.redraw (); 249 } 250 251 void check_selection () { 252 if (!has_selection () && KeyBindings.has_shift ()) { 253 show_selection = true; 254 selection_end = carret.copy (); 255 } 256 257 if (!KeyBindings.has_shift ()) { 258 show_selection = false; 259 } 260 } 261 262 private void add_character (uint keyval) { 263 unichar c = (unichar) keyval; 264 string s; 265 266 if (!is_modifier_key (keyval) 267 && !KeyBindings.has_ctrl () 268 && !KeyBindings.has_alt ()) { 269 270 s = (!) c.to_string (); 271 272 if (s.validate ()) { 273 if (store_undo_state_at_next_event) { 274 store_undo_edit_state (); 275 store_undo_state_at_next_event = false; 276 } 277 278 insert_text (s); 279 } 280 } 281 } 282 283 Paragraph get_current_paragraph () { 284 Paragraph p; 285 286 if (unlikely (!(0 <= carret.paragraph < paragraphs.size))) { 287 warning (@"No paragraph, index: $(carret.paragraph), size: $(paragraphs.size)"); 288 p = new Paragraph ("", 0, 0, text_color); 289 paragraphs.add (p); 290 return p; 291 } 292 293 p = paragraphs.get (carret.paragraph); 294 return p; 295 } 296 297 public void set_text (string t) { 298 int tl; 299 300 if (single_line) { 301 text = t.replace ("\n", "").replace ("\r", ""); 302 } else { 303 text = t; 304 } 305 306 tl = t.length; 307 text_length += tl; 308 309 paragraphs.clear (); 310 generate_paragraphs (); 311 312 return_if_fail (paragraphs.size != 0); 313 314 carret.paragraph = paragraphs.size - 1; 315 carret.character_index = paragraphs.get (paragraphs.size - 1).text.length; 316 selection_end = carret.copy (); 317 show_selection = false; 318 319 text_changed (get_text ()); 320 } 321 322 Carret get_selection_start () { 323 if (carret.paragraph == selection_end.paragraph) { 324 return carret.character_index < selection_end.character_index ? carret : selection_end; 325 } 326 327 return carret.paragraph < selection_end.paragraph ? carret : selection_end; 328 } 329 330 Carret get_selection_stop () { 331 if (carret.paragraph == selection_end.paragraph) { 332 return carret.character_index > selection_end.character_index ? carret : selection_end; 333 } 334 335 return carret.paragraph > selection_end.paragraph ? carret : selection_end; 336 } 337 338 public string get_selected_text () { 339 Carret selection_start, selection_stop; 340 int i; 341 Paragraph pg; 342 StringBuilder sb; 343 344 sb = new StringBuilder (); 345 346 if (!has_selection ()) { 347 return "".dup (); 348 } 349 350 selection_start = get_selection_start (); 351 selection_stop = get_selection_stop (); 352 353 if (selection_start.paragraph == selection_stop.paragraph) { 354 pg = paragraphs.get (selection_start.paragraph); 355 return pg.text.substring (selection_start.character_index, selection_stop.character_index - selection_start.character_index); 356 } 357 358 pg = paragraphs.get (selection_start.paragraph); 359 sb.append (pg.text.substring (selection_start.character_index)); 360 361 for (i = selection_start.paragraph + 1; i < selection_stop.paragraph; i++) { 362 return_val_if_fail (0 <= i < paragraphs.size, "".dup ()); 363 pg = paragraphs.get (i); 364 sb.append (pg.text); 365 } 366 367 pg = paragraphs.get (selection_stop.paragraph); 368 sb.append (pg.text.substring (0, selection_stop.character_index)); 369 370 return sb.str; 371 } 372 373 public void select_all () { 374 while (last_paragraph != DONE) { 375 generate_paragraphs (); 376 } 377 378 if (paragraphs.size > 0) { 379 carret.paragraph = 0; 380 carret.character_index = 0; 381 selection_end.paragraph = paragraphs.size - 1; 382 selection_end.character_index = paragraphs.get (paragraphs.size - 1).text_length; 383 show_selection = true; 384 } 385 } 386 387 public TextUndoItem delete_selected_text () { 388 Carret selection_start, selection_stop; 389 int i; 390 Paragraph pg, pge; 391 string e, s, n; 392 bool same; 393 TextUndoItem ui; 394 395 ui = new TextUndoItem (carret); 396 397 e = ""; 398 s = ""; 399 n = ""; 400 401 if (!has_selection ()) { 402 warning ("No selected text."); 403 return ui; 404 } 405 406 selection_start = get_selection_start (); 407 selection_stop = get_selection_stop (); 408 409 same = selection_start.paragraph == selection_stop.paragraph; 410 411 if (!same) { 412 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 413 pg = paragraphs.get (selection_start.paragraph); 414 s = pg.text.substring (0, selection_start.character_index); 415 416 return_val_if_fail (0 <= selection_stop.paragraph < paragraphs.size, ui); 417 pge = paragraphs.get (selection_stop.paragraph); 418 e = pge.text.substring (selection_stop.character_index); 419 420 if (!s.has_suffix ("\n")) { 421 ui.deleted.add (pge.copy ()); 422 ui.edited.add (pg.copy ()); 423 424 pg.set_text (s + e); 425 pge.set_text (""); 426 } else { 427 ui.edited.add (pg.copy ()); 428 ui.edited.add (pge.copy ()); 429 430 pg.set_text (s); 431 pge.set_text (e); 432 } 433 } else { 434 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 435 436 pg = paragraphs.get (selection_start.paragraph); 437 n = pg.text.substring (0, selection_start.character_index); 438 n += pg.text.substring (selection_stop.character_index); 439 440 if (n == "") { 441 ui.deleted.add (pg.copy ()); 442 paragraphs.remove_at (selection_start.paragraph); 443 } else { 444 ui.edited.add (pg.copy ()); 445 } 446 447 pg.set_text (n); 448 } 449 450 if (e == "" && !same) { 451 paragraphs.remove_at (selection_stop.paragraph); 452 } 453 454 for (i = selection_stop.paragraph - 1; i > selection_start.paragraph; i--) { 455 return_val_if_fail (0 <= i < paragraphs.size, ui); 456 ui.deleted.add (paragraphs.get (i)); 457 paragraphs.remove_at (i); 458 } 459 460 if (s == "" && !same) { 461 return_val_if_fail (0 <= selection_start.paragraph < paragraphs.size, ui); 462 paragraphs.remove_at (selection_start.paragraph); 463 } 464 465 carret = selection_start.copy (); 466 selection_end = carret.copy (); 467 468 show_selection = false; 469 update_paragraph_index (); 470 layout (); 471 472 return ui; 473 } 474 475 void update_paragraph_index () { 476 int i = 0; 477 foreach (Paragraph p in paragraphs) { 478 p.index = i; 479 i++; 480 } 481 } 482 483 public TextUndoItem remove_last_character () { 484 TextUndoItem ui; 485 move_carret_previous (); 486 ui = remove_next_character (); 487 return ui; 488 } 489 490 public TextUndoItem remove_next_character () { 491 Paragraph paragraph; 492 Paragraph next_paragraph; 493 int index; 494 unichar c; 495 string np; 496 TextUndoItem ui; 497 498 ui = new TextUndoItem (carret); 499 500 return_val_if_fail (0 <= carret.paragraph < paragraphs.size, ui); 501 paragraph = paragraphs.get (carret.paragraph); 502 503 index = carret.character_index; 504 505 paragraph.text.get_next_char (ref index, out c); 506 507 if (index >= paragraph.text_length) { 508 np = paragraph.text.substring (0, carret.character_index); 509 510 if (carret.paragraph + 1 < paragraphs.size) { 511 next_paragraph = paragraphs.get (carret.paragraph + 1); 512 paragraphs.remove_at (carret.paragraph + 1); 513 514 np = np + next_paragraph.text; 515 516 ui.deleted.add (next_paragraph); 517 } 518 519 paragraph.set_text (np); 520 ui.edited.add (paragraph); 521 } else { 522 np = paragraph.text.substring (0, carret.character_index) + paragraph.text.substring (index); 523 paragraph.set_text (np); 524 525 if (np == "") { 526 return_val_if_fail (carret.paragraph > 0, ui); 527 carret.paragraph--; 528 paragraph = paragraphs.get (carret.paragraph); 529 carret.character_index = paragraph.text_length; 530 531 ui.deleted.add (paragraphs.get (carret.paragraph + 1)); 532 533 paragraphs.remove_at (carret.paragraph + 1); 534 } else { 535 ui.edited.add (paragraph); 536 } 537 } 538 539 update_paragraph_index (); 540 layout (); 541 542 return ui; 543 } 544 545 public void move_carret_next () { 546 unichar c; 547 548 move_carret_one_character (); 549 550 if (KeyBindings.has_ctrl ()) { 551 while (true) { 552 c = move_carret_one_character (); 553 554 if (c == '\0' || c == ' ') { 555 break; 556 } 557 } 558 } 559 } 560 561 unichar move_carret_one_character () { 562 Paragraph paragraph; 563 int index; 564 unichar c; 565 566 return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0'); 567 paragraph = paragraphs.get (carret.paragraph); 568 569 index = carret.character_index; 570 571 paragraph.text.get_next_char (ref index, out c); 572 573 if (index >= paragraph.text_length && carret.paragraph + 1 < paragraphs.size) { 574 carret.paragraph++; 575 carret.character_index = 0; 576 c = ' '; 577 } else { 578 carret.character_index = index; 579 } 580 581 return c; 582 } 583 584 public void move_carret_previous () { 585 unichar c; 586 587 move_carret_back_one_character (); 588 589 if (KeyBindings.has_ctrl ()) { 590 while (true) { 591 c = move_carret_back_one_character (); 592 593 if (c == '\0' || c == ' ') { 594 break; 595 } 596 } 597 } 598 } 599 600 unichar move_carret_back_one_character () { 601 Paragraph paragraph; 602 int index, last_index; 603 unichar c; 604 605 return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0'); 606 paragraph = paragraphs.get (carret.paragraph); 607 608 index = 0; 609 last_index = -1; 610 611 while (paragraph.text.get_next_char (ref index, out c) && index < carret.character_index) { 612 last_index = index; 613 } 614 615 if (last_index <= 0 && carret.paragraph > 0) { 616 carret.paragraph--; 617 618 return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0'); 619 paragraph = paragraphs.get (carret.paragraph); 620 carret.character_index = paragraph.text_length; 621 622 if (paragraph.text.has_suffix ("\n")) { 623 carret.character_index -= "\n".length; 624 } 625 626 c = ' '; 627 } else if (last_index > 0) { 628 carret.character_index = last_index; 629 } else { 630 carret.character_index = 0; 631 c = ' '; 632 } 633 634 return_val_if_fail (0 <= carret.paragraph < paragraphs.size, '\0'); 635 636 return c; 637 } 638 639 public void move_carret_next_row () { 640 double nr = font_size; 641 642 if (carret.desired_y + 2 * font_size >= allocation.height) { 643 scroll (2 * font_size); 644 nr = -font_size; 645 } 646 647 if (carret.desired_y + nr < widget_y + height - padding) { 648 carret = get_carret_at (carret.desired_x - widget_x - padding, carret.desired_y + nr); 649 } 650 } 651 652 public void move_carret_to_end_of_line () { 653 carret = get_carret_at (widget_x + padding + width, carret.desired_y, false); 654 } 655 656 public void move_carret_to_beginning_of_line () { 657 carret = get_carret_at (widget_x, carret.desired_y, false); 658 } 659 660 public void move_carret_previous_row () { 661 double nr = -font_size; 662 663 if (carret.desired_y - 2 * font_size < 0) { 664 scroll (-2 * font_size); 665 nr = font_size; 666 } 667 668 if (carret.desired_y + nr > widget_y + padding) { 669 carret = get_carret_at (carret.desired_x, carret.desired_y + nr); 670 } 671 } 672 673 public bool has_selection () { 674 return show_selection && selection_is_visible (); 675 } 676 677 private bool selection_is_visible () { 678 return carret.paragraph != selection_end.paragraph || carret.character_index != selection_end.character_index; 679 } 680 681 public void insert_text (string t) { 682 string s; 683 Paragraph paragraph; 684 TextUndoItem ui; 685 Gee.ArrayList<string> pgs; 686 bool u = false; 687 688 pgs = new Gee.ArrayList<string> (); 689 690 if (single_line) { 691 s = t.replace ("\n", "").replace ("\r", ""); 692 pgs.add (s); 693 } else { 694 if (t.last_index_of ("\n") > 0) { 695 string[] parts = t.split ("\n"); 696 int i; 697 for (i = 0; i < parts.length -1; i++) { 698 pgs.add (parts[i]); 699 pgs.add ("\n"); 700 } 701 702 pgs.add (parts[parts.length - 1]); 703 704 if (t.has_suffix ("\n")) { 705 pgs.add ("\n"); 706 } 707 } else { 708 s = t; 709 pgs.add (s); 710 } 711 } 712 713 if (has_selection () && show_selection) { 714 ui = delete_selected_text (); 715 u = true; 716 717 if (paragraphs.size == 0) { 718 paragraphs.add (new Paragraph ("", font_size, 0, text_color)); 719 } 720 } else { 721 ui = new TextUndoItem (carret); 722 } 723 724 return_if_fail (0 <= carret.paragraph < paragraphs.size); 725 paragraph = paragraphs.get (carret.paragraph); 726 727 if (pgs.size > 0) { 728 if (!u) { 729 ui.edited.add (paragraph.copy ()); 730 } 731 732 string first = pgs.get (0); 733 734 string end; 735 string nt = paragraph.text.substring (0, carret.character_index); 736 737 nt += first; 738 end = paragraph.text.substring (carret.character_index); 739 740 paragraph.set_text (nt); 741 742 int paragraph_index = carret.paragraph; 743 Paragraph next_paragraph = paragraph; 744 for (int i = 1; i < pgs.size; i++) { 745 paragraph_index++; 746 string next = pgs.get (i); 747 next_paragraph = new Paragraph (next, font_size, paragraph_index, text_color); 748 paragraphs.insert (paragraph_index, next_paragraph); 749 ui.added.add (next_paragraph); 750 u = true; 751 } 752 753 carret.paragraph = paragraph_index; 754 carret.character_index = next_paragraph.text.length; 755 756 next_paragraph.set_text (next_paragraph.text + end); 757 } 758 759 if (u) { 760 undo_items.add (ui); 761 redo_items.clear (); 762 } 763 764 update_paragraph_index (); 765 layout (); 766 767 text_changed (get_text ()); 768 show_selection = false; 769 } 770 771 public string get_text () { 772 StringBuilder sb = new StringBuilder (); 773 774 generate_all_paragraphs (); 775 776 foreach (Paragraph p in paragraphs) { 777 sb.append (p.text); 778 } 779 780 return sb.str; 781 } 782 783 Carret get_carret_at (double click_x, double click_y, 784 bool check_boundaries = true) { 785 786 int i = 0; 787 double tx, ty; 788 double p; 789 string w; 790 int ch_index; 791 double min_d = double.MAX; 792 Carret c = new Carret (); 793 double dt; 794 795 c.paragraph = -1; 796 c.desired_x = click_x; 797 c.desired_y = click_y; 798 799 foreach (Paragraph paragraph in paragraphs) { 800 if (!check_boundaries || paragraph.text_is_on_screen (allocation, widget_y)) { 801 ch_index = 0; 802 803 if (paragraph.start_y + widget_y - font_size <= click_y <= paragraph.end_y + widget_y + font_size) { 804 foreach (Text next_word in paragraph.words) { 805 double tt_click = click_y - widget_y - padding + font_size; 806 807 w = next_word.text; 808 809 if (next_word.widget_y <= tt_click <= next_word.widget_y + font_size) { 810 811 p = next_word.get_sidebearing_extent (); 812 813 if ((next_word.widget_y <= tt_click <= next_word.widget_y + font_size) 814 && (next_word.widget_x + widget_x <= click_x <= next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())) { 815 816 tx = widget_x + next_word.widget_x + padding; 817 ty = widget_y + next_word.widget_y + padding; 818 819 next_word.iterate ((glyph, kerning, last) => { 820 double cw; 821 int ci; 822 double d; 823 string gc = (!) glyph.get_unichar ().to_string (); 824 825 d = Math.fabs (click_x - tx); 826 827 if (d <= min_d) { 828 min_d = d; 829 c.character_index = ch_index; 830 c.paragraph = i; 831 } 832 833 cw = (glyph.get_width ()) * next_word.get_font_scale () + kerning; 834 ci = gc.length; 835 836 tx += cw; 837 ch_index += ci; 838 }); 839 840 dt = Math.fabs (click_x - (tx + widget_x + padding)); 841 if (dt < min_d) { 842 min_d = dt; 843 c.character_index = ch_index; 844 c.paragraph = i; 845 } 846 } else { 847 dt = Math.fabs (click_x - (next_word.widget_x + widget_x + padding + next_word.get_sidebearing_extent ())); 848 849 if (dt < min_d) { 850 min_d = dt; 851 c.character_index = ch_index + w.length; 852 853 if (w.has_suffix ("\n")) { 854 c.character_index -= "\n".length; 855 } 856 857 c.paragraph = i; 858 } 859 860 ch_index += w.length; 861 } 862 } else { 863 ch_index += w.length; 864 } 865 } 866 } 867 } 868 i++; 869 } 870 871 if (unlikely (c.paragraph < 0)) { 872 c.paragraph = paragraphs.size > 0 ? paragraphs.size - 1 : 0; 873 c.character_index = paragraphs.size > 0 ? paragraphs.get (c.paragraph).text.length : 0; 874 } 875 876 store_undo_state_at_next_event = true; 877 878 return c; 879 } 880 881 /** @return offset to click in text. */ 882 public override void layout () { 883 double p; 884 double tx, ty; 885 string w; 886 double xmax = 0; 887 int i = 0; 888 double dd; 889 890 tx = 0; 891 ty = font_size; 892 893 if (allocation.width <= 0 || allocation.height <= 0) { 894 warning ("Parent widget allocation is not set."); 895 } 896 897 for (i = paragraphs.size - 1; i >= 0 && paragraphs.size > 1; i--) { 898 if (unlikely (paragraphs.get (i).is_empty ())) { 899 warning ("Empty paragraph."); 900 paragraphs.remove_at (i); 901 update_paragraph_index (); 902 } 903 } 904 905 i = 0; 906 foreach (Paragraph paragraph in paragraphs) { 907 if (paragraph.need_layout 908 || (paragraph.text_area_width != width 909 && paragraph.text_is_on_screen (allocation, widget_y))) { 910 911 paragraph.start_y = ty; 912 paragraph.start_x = tx; 913 914 paragraph.cached_surface = null; 915 916 foreach (Text next_word in paragraph.words) { 917 next_word.set_font_size (font_size); 918 919 w = next_word.text; 920 p = next_word.get_sidebearing_extent (); 921 922 if (unlikely (p == 0)) { 923 warning (@"Zero width word: $(w)"); 924 } 925 926 if (w == "") { 927 break; 928 } 929 930 if (w == "\n") { 931 next_word.widget_x = tx; 932 next_word.widget_y = ty; 933 934 tx = 0; 935 ty += next_word.font_size; 936 } else { 937 if (!single_line) { 938 if (tx + p + 2 * padding > width || w == "\n") { 939 tx = 0; 940 ty += next_word.font_size; 941 } 942 } 943 944 if (tx + p > xmax) { 945 xmax = tx + p; 946 } 947 948 next_word.widget_x = tx; 949 next_word.widget_y = ty; 950 951 if (w != "\n") { 952 tx += p; 953 } 954 } 955 } 956 957 if (tx > xmax) { 958 xmax = tx; 959 } 960 961 paragraph.text_area_width = width; 962 paragraph.width = xmax; 963 paragraph.end_x = tx; 964 paragraph.end_y = ty; 965 paragraph.need_layout = false; 966 } 967 968 if (xmax > width) { 969 break; 970 } 971 972 tx = paragraph.end_x; 973 ty = paragraph.end_y; 974 i++; 975 } 976 977 if (xmax > width) { 978 this.width = xmax + 2 * padding; 979 layout (); 980 return; 981 } 982 983 this.height = fmax (min_height, ty + 2 * padding); 984 985 if (last_paragraph != DONE) { 986 this.height = (text_length / (double) last_paragraph) * ty + 2 * padding; // estimate height 987 } 988 989 if (ty + widget_y < allocation.height && last_paragraph != DONE) { 990 generate_paragraphs (); 991 layout (); 992 return; 993 } 994 995 ty = font_size; 996 tx = 0; 997 998 foreach (Paragraph paragraph in paragraphs) { 999 dd = ty - paragraph.start_y; 1000 1001 if (dd != 0) { 1002 paragraph.start_y += dd; 1003 paragraph.end_y += dd; 1004 foreach (Text word in paragraph.words) { 1005 word.widget_y += dd; 1006 } 1007 } 1008 1009 ty = paragraph.end_y; 1010 } 1011 } 1012 1013 public override void button_press (uint button, double x, double y) { 1014 if (is_over (x, y)) { 1015 carret = get_carret_at (x, y); 1016 selection_end = carret.copy (); 1017 update_selection = true; 1018 } 1019 } 1020 1021 public override void button_release (uint button, double x, double y) { 1022 update_selection = false; 1023 show_selection = selection_is_visible (); 1024 } 1025 1026 public override bool motion (double x, double y) { 1027 if (update_selection) { 1028 selection_end = get_carret_at (x, y); 1029 show_selection = selection_is_visible (); 1030 } 1031 1032 return update_selection; 1033 } 1034 1035 public override void draw (Context cr) { 1036 Text word; 1037 double tx, ty; 1038 string w; 1039 double scale; 1040 double width; 1041 double x = widget_x; 1042 double y = widget_y; 1043 Carret selection_start, selection_stop; 1044 double carret_x; 1045 double carret_y; 1046 1047 layout (); 1048 1049 if (draw_border) { 1050 // background 1051 cr.save (); 1052 cr.set_line_width (1); 1053 Theme.color (cr, "Text Area Background"); 1054 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding); 1055 cr.fill (); 1056 cr.restore (); 1057 1058 // border 1059 cr.save (); 1060 cr.set_line_width (1); 1061 Theme.color (cr, "Foreground 1"); 1062 draw_rounded_rectangle (cr, x, y, this.width, this.height - padding, padding); 1063 cr.stroke (); 1064 cr.restore (); 1065 } 1066 1067 cr.save (); 1068 1069 word = new Text (); 1070 1071 width = this.width - padding; 1072 x += padding; 1073 scale = word.get_font_scale (); 1074 y += font_size; 1075 1076 // draw selection background 1077 if (has_selection ()) { 1078 tx = 0; 1079 ty = 0; 1080 1081 selection_start = get_selection_start (); 1082 selection_stop = get_selection_stop (); 1083 1084 cr.save (); 1085 Theme.color (cr, "Highlighted 1"); 1086 1087 for (int i = selection_start.paragraph; i <= selection_stop.paragraph; i++) { 1088 return_if_fail (0 <= i < paragraphs.size); 1089 Paragraph pg = paragraphs.get (i); 1090 1091 if (pg.text_is_on_screen (allocation, widget_y)) { 1092 int char_index = 0; 1093 1094 foreach (Text next_word in pg.words) { 1095 double cw = next_word.get_sidebearing_extent (); 1096 bool paint_background = false; 1097 bool partial_start = false; 1098 bool partial_stop = false; 1099 int wl; 1100 1101 w = next_word.text; 1102 wl = w.length; 1103 scale = next_word.get_font_scale (); 1104 1105 if (selection_start.paragraph == selection_stop.paragraph) { 1106 partial_start = true; 1107 partial_stop = true; 1108 } else if (selection_start.paragraph < i < selection_stop.paragraph) { 1109 paint_background = true; 1110 } else if (selection_start.paragraph == i) { 1111 paint_background = true; 1112 partial_start = true; 1113 } else if (selection_stop.paragraph == i) { 1114 paint_background = char_index + wl < selection_stop.character_index; 1115 partial_stop = !paint_background; 1116 } 1117 1118 if (paint_background && !(partial_start || partial_stop)) { 1119 double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size; 1120 cr.rectangle (widget_x + padding + next_word.widget_x - 1, selection_y, cw + 1, font_size); 1121 cr.fill (); 1122 } 1123 1124 if (partial_start || partial_stop) { 1125 int index = char_index; 1126 double bx = widget_x + padding + next_word.widget_x + (partial_start ? 0 : 1); 1127 1128 next_word.iterate ((glyph, kerning, last) => { 1129 double cwi; 1130 int ci; 1131 bool draw = (index >= selection_start.character_index && partial_start && !partial_stop) 1132 || (index < selection_stop.character_index && !partial_start && partial_stop) 1133 || (selection_start.character_index <= index < selection_stop.character_index && partial_start && partial_stop); 1134 1135 cwi = (glyph.get_width ()) * next_word.get_font_scale () + kerning; 1136 1137 if (draw) { 1138 double selection_y = widget_y + next_word.widget_y + scale * -next_word.cached_font.bottom_limit - font_size; 1139 cr.rectangle (bx - 1, selection_y, cwi + 1, font_size); 1140 cr.fill (); 1141 } 1142 1143 bx += cwi; 1144 ci = ((!) glyph.get_unichar ().to_string ()).length; 1145 index += ci; 1146 }); 1147 } 1148 1149 char_index += w.length; 1150 } 1151 } 1152 } 1153 1154 cr.restore (); 1155 } 1156 1157 tx = 0; 1158 ty = 0; 1159 1160 int first_visible = 0; 1161 int last_visible; 1162 int paragraphs_size = paragraphs.size; 1163 while (first_visible < paragraphs_size) { 1164 if (paragraphs.get (first_visible).text_is_on_screen (allocation, widget_y)) { 1165 break; 1166 } 1167 first_visible++; 1168 } 1169 1170 last_visible = first_visible; 1171 while (last_visible < paragraphs_size) { 1172 if (!paragraphs.get (last_visible).text_is_on_screen (allocation, widget_y)) { 1173 last_visible++; 1174 break; 1175 } 1176 last_visible++; 1177 } 1178 1179 if (paragraphs_size == 0) { 1180 if (carret_is_visible) { 1181 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1182 } 1183 1184 return; 1185 } 1186 1187 Context cc; // cached context 1188 Paragraph paragraph; 1189 paragraph = paragraphs.get (0); 1190 1191 tx = paragraph.start_x; 1192 ty = paragraph.start_y; 1193 1194 for (int i = first_visible; i < last_visible; i++) { 1195 paragraph = paragraphs.get (i); 1196 1197 tx = paragraph.start_x; 1198 ty = paragraph.start_y; 1199 1200 if (paragraph.cached_surface == null) { 1201 paragraph.cached_surface = Screen.create_background_surface ((int) width + 2, paragraph.get_height () + (int) font_size + 2); 1202 cc = new Context ((!) paragraph.cached_surface); 1203 cc.scale (Screen.get_scale(), Screen.get_scale()); 1204 1205 foreach (Text next_word in paragraph.words) { 1206 if (next_word.text != "\n") { 1207 next_word.draw_at_top (cc, next_word.widget_x, next_word.widget_y - ty); 1208 } 1209 } 1210 } 1211 1212 if (likely (paragraph.cached_surface != null)) { 1213 // FIXME: subpixel offset in text area 1214 Screen.paint_background_surface(cr, 1215 (!) paragraph.cached_surface, 1216 (int) (x + tx), 1217 (int) (widget_y + paragraph.start_y - font_size + padding)); 1218 } else { 1219 warning ("No paragraph image."); 1220 } 1221 } 1222 1223 if (carret_is_visible) { 1224 get_carret_position (carret, out carret_x, out carret_y); 1225 1226 if (carret_y < 0) { 1227 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1228 } else { 1229 draw_carret_at (cr, carret_x, carret_y); 1230 } 1231 } 1232 1233 if (has_selection ()) { 1234 get_carret_position (selection_end, out carret_x, out carret_y); 1235 1236 if (carret_y < 0) { 1237 draw_carret_at (cr, widget_x + padding, widget_y + font_size + padding); 1238 } else { 1239 draw_carret_at (cr, carret_x, carret_y); 1240 } 1241 } 1242 } 1243 1244 void get_carret_position (Carret carret, out double carret_x, out double carret_y) { 1245 Paragraph paragraph; 1246 double tx; 1247 double ty; 1248 int ch_index; 1249 int wl; 1250 double pos_x, pos_y; 1251 1252 ch_index = 0; 1253 1254 carret_x = -1; 1255 carret_y = -1; 1256 1257 return_if_fail (0 <= carret.paragraph < paragraphs.size); 1258 paragraph = paragraphs.get (carret.paragraph); 1259 1260 pos_x = -1; 1261 pos_y = -1; 1262 1263 foreach (Text next_word in paragraph.words) { 1264 string w = next_word.text; 1265 wl = w.length; 1266 1267 if (carret.character_index == ch_index) { 1268 pos_x = next_word.widget_x + widget_x + padding; 1269 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); 1270 } else if (carret.character_index >= ch_index + wl) { 1271 pos_x = next_word.widget_x + next_word.get_sidebearing_extent () + widget_x + padding; 1272 pos_y = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); 1273 1274 if (next_word.text.has_suffix ("\n")) { 1275 pos_x = widget_x + padding; 1276 pos_y += next_word.font_size; 1277 } 1278 } else if (ch_index < carret.character_index <= ch_index + wl) { 1279 tx = widget_x + next_word.widget_x; 1280 ty = widget_y + next_word.widget_y + next_word.get_baseline_to_bottom_for_font (); 1281 1282 if (carret.character_index <= ch_index) { 1283 pos_x = widget_x + padding; 1284 pos_y = ty; 1285 } 1286 1287 next_word.iterate ((glyph, kerning, last) => { 1288 double cw; 1289 int ci; 1290 1291 cw = (glyph.get_width ()) * next_word.get_font_scale () + kerning; 1292 ci = ((!) glyph.get_unichar ().to_string ()).length; 1293 1294 if (ch_index < carret.character_index <= ch_index + ci) { 1295 pos_x = tx + cw + padding; 1296 pos_y = ty; 1297 1298 if (glyph.get_unichar () == '\n') { 1299 pos_x = widget_x + padding; 1300 pos_y += next_word.font_size; 1301 } 1302 } 1303 1304 tx += cw; 1305 ch_index += ci; 1306 }); 1307 } 1308 1309 ch_index += wl; 1310 } 1311 1312 carret_x = pos_x; 1313 carret_y = pos_y; 1314 } 1315 1316 void draw_carret_at (Context cr, double x, double y) { 1317 cr.save (); 1318 cr.set_source_rgba (0, 0, 0, 0.5); 1319 cr.set_line_width (1); 1320 cr.move_to (x, y); 1321 cr.line_to (x, y - font_size); 1322 cr.stroke (); 1323 cr.restore (); 1324 } 1325 1326 public void store_undo_edit_state () { 1327 TextUndoItem ui = new TextUndoItem (carret); 1328 ui.edited.add (get_current_paragraph ().copy ()); 1329 undo_items.add (ui); 1330 redo_items.clear (); 1331 } 1332 1333 public void redo () { 1334 TextUndoItem i; 1335 TextUndoItem undo_item; 1336 1337 if (redo_items.size > 0) { 1338 i = redo_items.get (redo_items.size - 1); 1339 1340 undo_item = new TextUndoItem (i.carret); 1341 1342 i.deleted.sort ((a, b) => { 1343 Paragraph pa = (Paragraph) a; 1344 Paragraph pb = (Paragraph) b; 1345 return pb.index - pa.index; 1346 }); 1347 1348 i.added.sort ((a, b) => { 1349 Paragraph pa = (Paragraph) a; 1350 Paragraph pb = (Paragraph) b; 1351 return pa.index - pb.index; 1352 }); 1353 1354 foreach (Paragraph p in i.deleted) { 1355 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1356 warning ("Paragraph not found."); 1357 } else { 1358 undo_item.deleted.add (p.copy ()); 1359 paragraphs.remove_at (p.index); 1360 } 1361 } 1362 1363 foreach (Paragraph p in i.added) { 1364 if (p.index == paragraphs.size) { 1365 paragraphs.add (p.copy ()); 1366 } else { 1367 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1368 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1369 } else { 1370 undo_item.added.add (paragraphs.get (p.index).copy ()); 1371 paragraphs.insert (p.index, p.copy ()); 1372 } 1373 } 1374 } 1375 1376 foreach (Paragraph p in i.edited) { 1377 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1378 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1379 return; 1380 } 1381 1382 undo_item.edited.add (paragraphs.get (p.index).copy ()); 1383 paragraphs.set (p.index, p.copy ()); 1384 } 1385 1386 redo_items.remove_at (redo_items.size - 1); 1387 undo_items.add (undo_item); 1388 1389 carret = i.carret.copy (); 1390 layout (); 1391 } 1392 } 1393 1394 public void undo () { 1395 TextUndoItem i; 1396 TextUndoItem redo_item; 1397 1398 if (undo_items.size > 0) { 1399 i = undo_items.get (undo_items.size - 1); 1400 redo_item = new TextUndoItem (i.carret); 1401 1402 i.deleted.sort ((a, b) => { 1403 Paragraph pa = (Paragraph) a; 1404 Paragraph pb = (Paragraph) b; 1405 return pa.index - pb.index; 1406 }); 1407 1408 i.added.sort ((a, b) => { 1409 Paragraph pa = (Paragraph) a; 1410 Paragraph pb = (Paragraph) b; 1411 return pb.index - pa.index; 1412 }); 1413 1414 foreach (Paragraph p in i.added) { 1415 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1416 warning ("Paragraph not found."); 1417 } else { 1418 redo_item.added.add (paragraphs.get (p.index).copy ()); 1419 paragraphs.remove_at (p.index); 1420 } 1421 } 1422 1423 foreach (Paragraph p in i.deleted) { 1424 if (p.index == paragraphs.size) { 1425 paragraphs.add (p.copy ()); 1426 } else { 1427 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1428 warning (@"Index: $(p.index) out of bounds, size: $(paragraphs.size)"); 1429 } else { 1430 redo_item.deleted.add (p.copy ()); 1431 paragraphs.insert (p.index, p.copy ()); 1432 } 1433 } 1434 } 1435 1436 foreach (Paragraph p in i.edited) { 1437 if (unlikely (!(0 <= p.index < paragraphs.size))) { 1438 warning (@"Index: $(p.index ) out of bounds, size: $(paragraphs.size)"); 1439 return; 1440 } 1441 1442 redo_item.edited.add (paragraphs.get (p.index).copy ()); 1443 paragraphs.set (p.index, p.copy ()); 1444 } 1445 1446 undo_items.remove_at (undo_items.size - 1); 1447 redo_items.add (redo_item); 1448 1449 carret = i.carret.copy (); 1450 layout (); 1451 } 1452 } 1453 1454 public void set_editable (bool editable) { 1455 this.editable = editable; 1456 } 1457 1458 public class TextUndoItem : GLib.Object { 1459 public Carret carret; 1460 public Gee.ArrayList<Paragraph> added = new Gee.ArrayList<Paragraph> (); 1461 public Gee.ArrayList<Paragraph> edited = new Gee.ArrayList<Paragraph> (); 1462 public Gee.ArrayList<Paragraph> deleted = new Gee.ArrayList<Paragraph> (); 1463 1464 public TextUndoItem (Carret c) { 1465 carret = c.copy (); 1466 } 1467 } 1468 1469 public class Paragraph : GLib.Object { 1470 public double end_x = -10000; 1471 public double end_y = -10000; 1472 1473 public double start_x = -10000; 1474 public double start_y = -10000; 1475 1476 public double width = -10000; 1477 public double text_area_width = -10000; 1478 1479 public string text; 1480 1481 public Gee.ArrayList<Text> words { 1482 get { 1483 if (words_in_paragraph.size == 0) { 1484 generate_words (); 1485 } 1486 1487 return words_in_paragraph; 1488 } 1489 } 1490 1491 private Gee.ArrayList<Text> words_in_paragraph = new Gee.ArrayList<Text> (); 1492 public int text_length; 1493 public bool need_layout = true; 1494 public Surface? cached_surface = null; 1495 double font_size; 1496 public int index; 1497 Color text_color; 1498 1499 public Paragraph (string text, double font_size, int index, Color c) { 1500 this.index = index; 1501 this.font_size = font_size; 1502 text_color = c; 1503 set_text (text); 1504 } 1505 1506 public Paragraph copy () { 1507 Paragraph p = new Paragraph (text.dup (), font_size, index, text_color); 1508 p.need_layout = true; 1509 return p; 1510 } 1511 1512 public bool is_empty () { 1513 return text == ""; 1514 } 1515 1516 public void set_text (string t) { 1517 this.text = t; 1518 text_length = t.length; 1519 need_layout = true; 1520 words.clear (); 1521 cached_surface = null; 1522 } 1523 1524 public int get_height () { 1525 return (int) (end_y - start_y) + 1; 1526 } 1527 1528 public int get_width () { 1529 return (int) width + 1; 1530 } 1531 1532 public bool text_is_on_screen (WidgetAllocation alloc, double widget_y) { 1533 bool v = (0 <= start_y + widget_y <= alloc.height) 1534 || (0 <= end_y + widget_y <= alloc.height) 1535 || (start_y + widget_y <= 0 && alloc.height <= end_y + widget_y); 1536 return v; 1537 } 1538 1539 private void generate_words () { 1540 string w; 1541 int p = 0; 1542 bool carret_at_word_end = false; 1543 Text word; 1544 int carret = 0; 1545 int iter_pos = 0; 1546 1547 return_if_fail (words_in_paragraph.size == 0); 1548 1549 while (p < text_length) { 1550 w = get_next_word (out carret_at_word_end, ref iter_pos, carret); 1551 1552 if (w == "") { 1553 break; 1554 } 1555 1556 word = new Text (w, font_size); 1557 1558 word.r = text_color.r; 1559 word.g = text_color.g; 1560 word.b = text_color.b; 1561 word.a = text_color.a; 1562 1563 words_in_paragraph.add (word); 1564 } 1565 } 1566 1567 string get_next_word (out bool carret_at_end_of_word, ref int iter_pos, int carret) { 1568 int i; 1569 int ni; 1570 int pi; 1571 string n; 1572 int nl; 1573 1574 carret_at_end_of_word = false; 1575 1576 if (iter_pos >= text_length) { 1577 carret_at_end_of_word = true; 1578 return "".dup (); 1579 } 1580 1581 if (text.get_char (iter_pos) == '\n') { 1582 iter_pos += "\n".length; 1583 carret_at_end_of_word = (iter_pos == carret); 1584 return "\n".dup (); 1585 } 1586 1587 i = text.index_of (" ", iter_pos); 1588 pi = i + " ".length; 1589 1590 ni = text.index_of ("\t", iter_pos); 1591 if (ni != -1 && ni < pi || i == -1) { 1592 i = ni; 1593 pi = i + "\t".length; 1594 } 1595 1596 ni = text.index_of ("\n", iter_pos); 1597 if (ni != -1 && ni < pi || i == -1) { 1598 i = ni; 1599 pi = i; 1600 } 1601 1602 if (iter_pos + iter_pos - pi > text_length || i == -1) { 1603 n = text.substring (iter_pos); 1604 } else { 1605 n = text.substring (iter_pos, pi - iter_pos); 1606 } 1607 1608 nl = n.length; 1609 if (iter_pos < carret < iter_pos + nl) { 1610 n = text.substring (iter_pos, carret - iter_pos); 1611 nl = n.length; 1612 carret_at_end_of_word = true; 1613 } 1614 1615 iter_pos += nl; 1616 1617 if (iter_pos == carret) { 1618 carret_at_end_of_word = true; 1619 } 1620 1621 return n; 1622 } 1623 } 1624 1625 public class Carret : GLib.Object { 1626 1627 public int paragraph = 0; 1628 1629 public int character_index { 1630 get { 1631 return ci; 1632 } 1633 1634 set { 1635 ci = value; 1636 } 1637 } 1638 1639 private int ci = 0; 1640 1641 public double desired_x = 0; 1642 public double desired_y = 0; 1643 1644 public Carret () { 1645 } 1646 1647 public void print () { 1648 stdout.printf (@"paragraph: $paragraph, character_index: $character_index\n"); 1649 } 1650 1651 public Carret copy () { 1652 Carret c = new Carret (); 1653 1654 c.paragraph = paragraph; 1655 c.character_index = character_index; 1656 1657 c.desired_x = desired_x; 1658 c.desired_y = desired_y; 1659 1660 return c; 1661 } 1662 } 1663 1664 public override void double_click (uint button, double x, double y) { 1665 if (is_over (x, y)) { 1666 carret = get_carret_at (x, y); 1667 1668 Paragraph paragraph = paragraphs.get (carret.paragraph); 1669 1670 int index = carret.character_index; 1671 int prev_index = index; 1672 unichar c; 1673 1674 while (paragraph.text.get_prev_char (ref index, out c)) { 1675 if (c == '\t' || c == ' ' || c == '\n') { 1676 break; 1677 } 1678 1679 prev_index = index; 1680 } 1681 1682 carret.character_index = prev_index; 1683 1684 selection_end = carret.copy (); 1685 index = selection_end.character_index; 1686 1687 while (paragraph.text.get_next_char (ref index, out c)) { 1688 if (c == '\t' || c == ' ' || c == '\n') { 1689 break; 1690 } 1691 1692 prev_index = index; 1693 } 1694 1695 selection_end.character_index = prev_index; 1696 show_selection = selection_is_visible (); 1697 1698 update_selection = true; 1699 } 1700 } 1701 } 1702 1703 } 1704