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     protected @property leftText ()
99     {
100         auto index = _cursorIndex;
101         if ( isSelecting ) {
102             index = min( _cursorIndex, _selectionIndex );
103         }
104         return _text[0 .. index.to!size_t];
105     }
106     protected @property rightText ()
107     {
108         auto index = _cursorIndex;
109         if ( isSelecting ) {
110             index = max( _cursorIndex, _selectionIndex );
111         }
112         return _text[index.to!size_t .. $];
113     }
114 
115     /// Inserts text at cursor.
116     void insert ( dstring v )
117     {
118         if ( isLocked ) return;
119 
120         setText( leftText ~v~ rightText );
121         moveCursor( v.length );
122     }
123     /// Removes a left char of cursor.
124     void backspace ()
125     {
126         if ( isLocked ) return;
127 
128         if ( isSelecting ) {
129             removeSelecting();
130             return;
131         }
132         auto left  = leftText;
133         auto right = rightText;
134 
135         if ( left.length ) {
136             left = left[0..$-1];
137             moveCursor( -1 );
138             setText( left~right );
139         }
140     }
141     /// Removes a right char of cursor.
142     void del ()
143     {
144         if ( isLocked ) return;
145 
146         if ( isSelecting ) {
147             removeSelecting();
148             return;
149         }
150         auto left  = leftText;
151         auto right = rightText;
152 
153         if ( right.length ) {
154             right = right[1..$];
155             setText( left~right );
156         }
157     }
158 
159     /// Removes the selected text.
160     void removeSelecting ()
161     {
162         if ( !isSelecting || isLocked ) return;
163 
164         long cursorMove = 0;
165         if ( _cursorIndex > _selectionIndex ) {
166             cursorMove = -(_cursorIndex-_selectionIndex);
167         }
168         setText( leftText ~ rightText );
169 
170         moveCursor( cursorMove );
171     }
172     /// Sets all text selected.
173     void selectAll ()
174     {
175         moveCursorTo( 0 );
176         moveCursorTo( _text.length, true );
177     }
178     ///
179     void deselect ()
180     {
181         moveCursor( 0 );
182         // This method doesn't call onCursorMove.
183         // So requstRedraw won't be called.
184     }
185 
186     /// Changes text.
187     void setText ( dstring v )
188     {
189         if ( _text != v ) {
190             deselect();
191             _text = v;
192             moveCursor(0);
193             onTextChange.call( v );
194         }
195     }
196 
197     /// Locks editing.
198     void lock ()
199     {
200         _locked = true;
201     }
202     /// Unlocks editing.
203     void unlock ()
204     {
205         _locked = false;
206     }
207 }