1 // Written in the D programming language.
2 /++
3  + Authors: KanzakiKino
4  + Copyright: KanzakiKino 2018
5  + License: LGPL-3.0
6 ++/
7 module w4d.widget.input.line;
8 import w4d.parser.colorset,
9        w4d.style.rect,
10        w4d.style.scalar,
11        w4d.task.window,
12        w4d.util.clipping,
13        w4d.util.textline,
14        w4d.widget.button,
15        w4d.widget.text,
16        w4d.event,
17        w4d.exception;
18 import g4d.element.shape.rect,
19        g4d.ft.font,
20        g4d.glfw.cursor,
21        g4d.shader.base;
22 import gl3n.linalg;
23 import std.algorithm,
24        std.conv,
25        std.math,
26        std.range;
27 
28 /// A handler that handles chainging text.
29 alias TextChangeHandler = EventHandler!( void, dstring );
30 
31 /// A widget of line input.
32 class LineInputWidget : TextWidget
33 {
34     protected TextLine _line;
35     protected float    _cursorPos;
36     protected float    _scrollLength;
37     protected float    _selectionLength;
38 
39     override @property dstring text () { return _line.text; }
40 
41     protected dchar    _passwordChar;
42     /// Character of password filed.
43     @property passwordChar () { return _passwordChar; }
44     /// Sets character of password field.
45     /// Deprecated: This method causes heavy method (loadText).
46     deprecated @property void passwordChar ( dchar c )
47     {
48         _passwordChar = c;
49         loadText(text);
50     }
51     /// Checks if the line input is password field.
52     @property isPasswordField ()
53     {
54         return _passwordChar != dchar.init;
55     }
56 
57     protected RectElement _cursorElm;
58     protected RectElement _selectionElm;
59 
60     protected ButtonWidget _chainedButton;
61 
62     ///
63     TextChangeHandler onTextChange;
64 
65     ///
66     override bool handleMouseMove ( vec2 pos )
67     {
68         if ( super.handleMouseMove( pos ) ) {
69             return true;
70         }
71         if ( isTracked ) {
72             _line.moveCursorTo( retrieveIndexFromAbsPos( pos.x ), true );
73             return true;
74         }
75         return false;
76     }
77     ///
78     override bool handleMouseButton ( MouseButton btn, bool status, vec2 pos )
79     {
80         if ( super.handleMouseButton( btn, status, pos ) ) {
81             return true;
82         }
83         if ( btn == MouseButton.Left && status ) {
84             auto selecting = _context.shift && isFocused;
85             _line.moveCursorTo( retrieveIndexFromAbsPos( pos.x ), selecting );
86             focus();
87             return true;
88         }
89         return false;
90     }
91     ///
92     override bool handleKey ( Key key, KeyState status )
93     {
94         if ( super.handleKey( key, status ) ) return true;
95 
96         const pressing = ( status != KeyState.Release );
97 
98         if ( key == Key.Backspace && pressing ) {
99             _line.backspace();
100 
101         } else if ( key == Key.Delete && pressing ) {
102             _line.del();
103 
104         } else if ( key == Key.Left && pressing ) {
105             _line.left( _context.shift );
106         } else if ( key == Key.Right && pressing ) {
107             _line.right( _context.shift );
108         } else if ( key == Key.Home && pressing ) {
109             _line.home( _context.shift );
110         } else if ( key == Key.End && pressing ) {
111             _line.end( _context.shift );
112 
113         } else if ( key == Key.A && pressing && _context.ctrl ) {
114             _line.selectAll();
115         } else if ( key == Key.D && pressing && _context.ctrl ) {
116             _line.deselect();
117             requestRedraw();
118 
119         } else if ( key == Key.Enter && pressing ) {
120             if ( _chainedButton ) {
121                 _chainedButton.onButtonPress.call();
122             }
123 
124         } else {
125             return false;
126         }
127         return true;
128     }
129 
130     ///
131     override bool handleTextInput ( dchar c )
132     {
133         if ( super.handleTextInput(c) ) return true;
134 
135         _line.insert( c.to!dstring );
136         return true;
137     }
138 
139     ///
140     override void handleFocused ( bool status )
141     {
142         if ( !status ) {
143             _line.deselect();
144             requestRedraw();
145         }
146         super.handleFocused( status );
147     }
148 
149     ///
150     override @property const(Cursor) cursor ()
151     {
152         return Cursor.IBeam;
153     }
154 
155     ///
156     this ()
157     {
158         super();
159 
160         _line            = new TextLine;
161         _cursorPos       = 0;
162         _scrollLength    = 0;
163         _selectionLength = 0;
164 
165         _cursorElm    = new RectElement;
166         _selectionElm = new RectElement;
167 
168         _chainedButton = null;
169 
170         _line.onTextChange = ( dstring v ) {
171             loadText(v);
172         };
173         _line.onCursorMove = ( long i ) {
174             _cursorPos    = retrievePosFromIndex(i);
175             _scrollLength = retrieveScrollLength();
176             updateSelectionRect();
177             requestRedraw();
178         };
179 
180         parseColorSetsFromFile!"colorset/lineinput.yaml"( style );
181         style.box.size.width  = Scalar.None;
182         style.box.borderWidth = Rect( 1.pixel );
183         style.box.paddings    = Rect( 1.mm );
184         style.box.margins     = Rect(1.mm);
185     }
186 
187     protected @property lineHeight ()
188     {
189         enforce( _font, "Font is not specified." );
190         return _font.size.y;
191     }
192 
193     protected long retrieveIndexFromAbsPos ( float pos )
194     {
195         const r_pos = pos - style.clientLeftTop.x + _scrollLength;
196         return retrieveIndexFromPos( r_pos );
197     }
198     protected long retrieveIndexFromPos ( float pos )
199     {
200         foreach ( i,poly; _textElm.polys ) {
201             const border = vec2(poly.pos).x + poly.length/2;
202             if ( border >= pos ) {
203                 return i;
204             }
205         }
206         return _text.length;
207     }
208     protected float retrievePosFromIndex ( long i )
209     {
210         i = i.clamp( 0, _line.text.length );
211 
212         if ( i == 0 ) {
213             return 0;
214         } else if ( i == _text.length ) {
215             auto poly = _textElm.polys[$-1];
216             return vec2(poly.pos).x + poly.length;
217         } else {
218             return vec2(_textElm.polys[i.to!size_t].pos).x;
219         }
220     }
221 
222     /// Changes the character of password field.
223     void changePasswordChar ( dchar c = dchar.init )
224     {
225         _passwordChar = c;
226         loadText( text );
227     }
228     ///
229     override void loadText ( dstring text, FontFace font = null )
230     {
231         auto display = text;
232         if ( isPasswordField ) {
233             display = passwordChar.
234                 repeat( text.length ).to!dstring;
235         }
236         super.loadText( display, font );
237 
238         _line.setText( text );
239         onTextChange.call( text );
240 
241         if ( font ) {
242             style.box.size.height = lineHeight.pixel;
243             _cursorElm.resize( vec2(1,lineHeight) );
244         }
245     }
246 
247     /// Locks editing.
248     void lock ()
249     {
250         _line.lock();
251     }
252     /// Unlocks editing.
253     void unlock ()
254     {
255         _line.unlock();
256     }
257 
258     /// Chains the button.
259     /// Chained button will be handled when Enter is pressed.
260     void chainButton ( ButtonWidget btn )
261     {
262         _chainedButton = btn;
263     }
264 
265     protected float retrieveScrollLength ()
266     {
267         auto size = style.box.clientSize;
268         if ( _scrollLength >= _cursorPos ) {
269             auto index = max( _line.cursorIndex-1, 0 );
270             return retrievePosFromIndex(index);
271 
272         } else if ( _scrollLength+size.x <= _cursorPos ) {
273             auto index = min( _line.cursorIndex+1, _line.text.length );
274             return retrievePosFromIndex(index) - size.x;
275         }
276         return _scrollLength;
277     }
278     protected void updateSelectionRect ()
279     {
280         if ( !_line.isSelecting ) return;
281 
282         const selectionPos = retrievePosFromIndex( _line.selectionIndex );
283         const newLength    = _cursorPos - selectionPos;
284         if ( newLength == _selectionLength ) return;
285 
286         _selectionLength = newLength;
287 
288         const size = vec2( _selectionLength.abs, lineHeight );
289         _selectionElm.resize( size );
290     }
291 
292     protected override void drawText ( Window w )
293     {
294         auto pos = style.box.
295             borderInsideLeftTop + style.translate;
296         w.clip.pushRect( pos, style.box.borderInsideSize );
297 
298         super.drawText( w );
299         if ( isFocused ) {
300             drawCursor( w );
301         }
302         if ( _line.isSelecting ) {
303             drawSelectionRect( w );
304         }
305 
306         w.clip.popRect();
307     }
308     protected void drawCursor ( Window w )
309     {
310         auto  shader = w.shaders.fill3;
311         const saver  = ShaderStateSaver( shader );
312         auto  late   = vec2(_cursorPos, lineHeight/2);
313         late       += style.clientLeftTop;
314 
315         shader.use();
316         shader.matrix.late = vec3( late, 0 );
317         shader.color = colorset.foreground;
318         _cursorElm.draw( shader );
319     }
320     protected void drawSelectionRect ( Window w )
321     {
322         auto  shader = w.shaders.fill3;
323         const saver  = ShaderStateSaver( shader );
324         auto  late   = vec2(_cursorPos, lineHeight/2);
325         late       += style.clientLeftTop;
326         late.x     -= _selectionLength/2;
327 
328         shader.use();
329         shader.matrix.late = vec3( late, 0 );
330         shader.color = colorset.border;
331         _selectionElm.draw( shader );
332     }
333 
334     ///
335     override @property bool trackable () { return true; }
336     ///
337     override @property bool focusable () { return true; }
338 }