1 // Written in the D programming language.
2 /++
3  + Authors: KanzakiKino
4  + Copyright: KanzakiKino 2018
5  + License: LGPL-3.0
6 ++/
7 module w4d.util.textline;
8 import w4d.event;
9 import std.algorithm,
10        std.conv;
11 
12 /// A handler of chainging text.
13 alias TextChangeHandler = EventHandler!( void, dstring );
14 /// A handler of cursor moving.
15 alias CursorMoveHandler = EventHandler!( void, long );
16 
17 /// An object of editable text.
18 class TextLine
19 {
20     ///
21     TextChangeHandler onTextChange;
22     ///
23     CursorMoveHandler onCursorMove;
24 
25     protected dstring _text;
26     /// Current text.
27     const @property text () { return _text; }
28 
29     protected bool _locked;
30     /// Checks if editing is locked.
31     const @property isLocked () { return _locked; }
32 
33     protected long _cursorIndex;
34     /// Index of cursor.
35     const @property cursorIndex () { return _cursorIndex; }
36 
37     protected long _selectionIndex;
38     /// Index that selection is beginning.
39     const @property selectionIndex () { return _selectionIndex; }
40 
41     /// Checks if text is selected.
42     const @property isSelecting ()
43     {
44         return _selectionIndex >= 0 && _selectionIndex != _cursorIndex;
45     }
46 
47     ///
48     this ()
49     {
50         _text           = ""d;
51         _locked         = false;
52         _cursorIndex    = 0;
53         _selectionIndex = 0;
54     }
55 
56     /// Moves cursor to absolute index.
57     void moveCursorTo ( long i, bool selecting = false )
58     {
59         if ( selecting && !isSelecting ) {
60             _selectionIndex = _cursorIndex;
61         }
62         const temp = _cursorIndex;
63         _cursorIndex = i.clamp( 0, _text.length );
64 
65         if ( !selecting ) {
66             _selectionIndex = -1;
67         }
68         if ( temp != _cursorIndex ) {
69             onCursorMove.call( _cursorIndex );
70         }
71     }
72     /// Moves cursor to relative index.
73     void moveCursor ( long i, bool selecting = false )
74     {
75         moveCursorTo( _cursorIndex+i, selecting );
76     }
77     /// Moves cursor to left.
78     void left ( bool selecting )
79     {
80         moveCursor( -1, selecting );
81     }
82     /// Moves cursor to right.
83     void right ( bool selecting )
84     {
85         moveCursor( 1, selecting );
86     }
87     /// Moves cursor to home.
88     void home ( bool selecting )
89     {
90         moveCursorTo( 0, selecting );
91     }
92     /// Moves cursor to end.
93     void end ( bool selecting )
94     {
95         moveCursorTo( _text.length, selecting );
96     }
97 
98     const @property leftText ()
99     {
100         long index = _cursorIndex;
101         if ( isSelecting ) {
102             index = min( _cursorIndex, _selectionIndex );
103         }
104         return _text[0 .. index.to!size_t];
105     }
106     const @property rightText ()
107     {
108         long index = _cursorIndex;
109         if ( isSelecting ) {
110             index = max( _cursorIndex, _selectionIndex );
111         }
112         return _text[index.to!size_t .. $];
113     }
114     const @property dstring selectedText ()
115     {
116         if ( !isSelecting ) return ""d;
117 
118         long left  = _cursorIndex;
119         long right = _selectionIndex;
120         if ( left > right ) {
121             swap( left, right );
122         }
123         return _text[left .. right];
124     }
125 
126     /// Inserts text at cursor.
127     void insert ( dstring v )
128     {
129         if ( isLocked ) return;
130 
131         setText( leftText ~v~ rightText );
132         moveCursor( v.length );
133     }
134     /// Removes a left char of cursor.
135     void backspace ()
136     {
137         if ( isLocked ) return;
138 
139         if ( isSelecting ) {
140             removeSelecting();
141             return;
142         }
143         auto left  = leftText;
144         auto right = rightText;
145 
146         if ( left.length ) {
147             left = left[0..$-1];
148             moveCursor( -1 );
149             setText( left~right );
150         }
151     }
152     /// Removes a right char of cursor.
153     void del ()
154     {
155         if ( isLocked ) return;
156 
157         if ( isSelecting ) {
158             removeSelecting();
159             return;
160         }
161         auto left  = leftText;
162         auto right = rightText;
163 
164         if ( right.length ) {
165             right = right[1..$];
166             setText( left~right );
167         }
168     }
169 
170     /// Removes the selected text.
171     void removeSelecting ()
172     {
173         if ( !isSelecting || isLocked ) return;
174 
175         long cursorMove = 0;
176         if ( _cursorIndex > _selectionIndex ) {
177             cursorMove = -(_cursorIndex-_selectionIndex);
178         }
179         auto text = leftText ~ rightText;
180         moveCursor( cursorMove );
181         setText( text );
182     }
183     /// Sets all text selected.
184     void selectAll ()
185     {
186         moveCursorTo( 0 );
187         moveCursorTo( _text.length, true );
188     }
189     ///
190     void deselect ()
191     {
192         moveCursor( 0 );
193         // This method doesn't call onCursorMove.
194         // So requstRedraw won't be called.
195     }
196 
197     /// Changes text.
198     void setText ( dstring v )
199     {
200         if ( _text != v ) {
201             deselect();
202             _text = v;
203             moveCursor(0);
204             onTextChange.call( v );
205         }
206     }
207 
208     /// Locks editing.
209     void lock ()
210     {
211         _locked = true;
212     }
213     /// Unlocks editing.
214     void unlock ()
215     {
216         _locked = false;
217     }
218 }