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