.
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