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