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.C && pressing && _context.ctrl ) {
120             Window.setClipboard( _line.selectedText );
121         } else if ( key == Key.V && pressing && _context.ctrl ) {
122             _line.insert( Window.getClipboard() );
123 
124         } else if ( key == Key.Enter && pressing ) {
125             if ( _chainedButton ) {
126                 _chainedButton.onButtonPress.call();
127             }
128 
129         } else {
130             return false;
131         }
132         return true;
133     }
134 
135     ///
136     override bool handleTextInput ( dchar c )
137     {
138         if ( super.handleTextInput(c) ) return true;
139 
140         _line.insert( c.to!dstring );
141         return true;
142     }
143 
144     ///
145     override void handleFocused ( bool status )
146     {
147         if ( !status ) {
148             _line.deselect();
149             requestRedraw();
150         }
151         super.handleFocused( status );
152     }
153 
154     ///
155     override @property const(Cursor) cursor ()
156     {
157         return Cursor.IBeam;
158     }
159 
160     ///
161     this ()
162     {
163         super();
164 
165         _line            = new TextLine;
166         _cursorPos       = 0;
167         _scrollLength    = 0;
168         _selectionLength = 0;
169 
170         _cursorElm    = new RectElement;
171         _selectionElm = new RectElement;
172 
173         _chainedButton = null;
174 
175         _line.onTextChange = ( dstring v ) {
176             loadText(v);
177         };
178         _line.onCursorMove = ( long i ) {
179             _cursorPos    = retrievePosFromIndex(i);
180             _scrollLength = retrieveScrollLength();
181             updateSelectionRect();
182             requestRedraw();
183         };
184 
185         parseColorSetsFromFile!"colorset/lineinput.yaml"( style );
186         style.box.size.width  = Scalar.None;
187         style.box.borderWidth = Rect( 1.pixel );
188         style.box.paddings    = Rect( 1.mm );
189         style.box.margins     = Rect(1.mm);
190     }
191 
192     protected @property lineHeight ()
193     {
194         enforce( _font, "Font is not specified." );
195         return _font.size.y;
196     }
197 
198     protected long retrieveIndexFromAbsPos ( float pos )
199     {
200         const r_pos = pos - style.clientLeftTop.x + _scrollLength;
201         return retrieveIndexFromPos( r_pos );
202     }
203     protected long retrieveIndexFromPos ( float pos )
204     {
205         foreach ( i,poly; _textElm.polys ) {
206             const border = vec2(poly.pos).x + poly.length/2;
207             if ( border >= pos ) {
208                 return i;
209             }
210         }
211         return _text.length;
212     }
213     protected float retrievePosFromIndex ( long i )
214     {
215         i = i.clamp( 0, _line.text.length );
216 
217         if ( i == 0 ) {
218             return 0;
219         } else if ( i == _text.length ) {
220             auto poly = _textElm.polys[$-1];
221             return vec2(poly.pos).x + poly.length;
222         } else {
223             return vec2(_textElm.polys[i.to!size_t].pos).x;
224         }
225     }
226 
227     /// Changes the character of password field.
228     void changePasswordChar ( dchar c = dchar.init )
229     {
230         _passwordChar = c;
231         loadText( text );
232     }
233     ///
234     override void loadText ( dstring text, FontFace font = null )
235     {
236         auto display = text;
237         if ( isPasswordField ) {
238             display = passwordChar.
239                 repeat( text.length ).to!dstring;
240         }
241         super.loadText( display, font );
242 
243         _line.setText( text );
244         onTextChange.call( text );
245 
246         if ( font ) {
247             style.box.size.height = lineHeight.pixel;
248             _cursorElm.resize( vec2(1,lineHeight) );
249         }
250     }
251 
252     /// Locks editing.
253     void lock ()
254     {
255         _line.lock();
256     }
257     /// Unlocks editing.
258     void unlock ()
259     {
260         _line.unlock();
261     }
262 
263     /// Chains the button.
264     /// Chained button will be handled when Enter is pressed.
265     void chainButton ( ButtonWidget btn )
266     {
267         _chainedButton = btn;
268     }
269 
270     protected float retrieveScrollLength ()
271     {
272         auto size = style.box.clientSize;
273         if ( _scrollLength >= _cursorPos ) {
274             auto index = max( _line.cursorIndex-1, 0 );
275             return retrievePosFromIndex(index);
276 
277         } else if ( _scrollLength+size.x <= _cursorPos ) {
278             auto index = min( _line.cursorIndex+1, _line.text.length );
279             return retrievePosFromIndex(index) - size.x;
280         }
281         return _scrollLength;
282     }
283     protected void updateSelectionRect ()
284     {
285         if ( !_line.isSelecting ) return;
286 
287         const selectionPos = retrievePosFromIndex( _line.selectionIndex );
288         const newLength    = _cursorPos - selectionPos;
289         if ( newLength == _selectionLength ) return;
290 
291         _selectionLength = newLength;
292 
293         const size = vec2( _selectionLength.abs, lineHeight );
294         _selectionElm.resize( size );
295     }
296 
297     protected override void drawText ( Window w, float xshift = 0 )
298     {
299         auto pos = style.box.
300             borderInsideLeftTop + style.translate;
301         w.clip.pushRect( pos, style.box.borderInsideSize );
302 
303         super.drawText( w, -_scrollLength+xshift );
304         if ( isFocused ) {
305             drawCursor( w );
306         }
307         if ( _line.isSelecting ) {
308             drawSelectionRect( w );
309         }
310 
311         w.clip.popRect();
312     }
313     protected void drawCursor ( Window w )
314     {
315         auto  shader = w.shaders.fill3;
316         const saver  = ShaderStateSaver( shader );
317         auto  late   = vec2(_cursorPos, lineHeight/2);
318         late        += style.clientLeftTop;
319         late.x      -= _scrollLength;
320 
321         shader.use();
322         shader.matrix.late = vec3( late, 0 );
323         shader.color = colorset.foreground;
324         _cursorElm.draw( shader );
325     }
326     protected void drawSelectionRect ( Window w )
327     {
328         auto  shader = w.shaders.fill3;
329         const saver  = ShaderStateSaver( shader );
330         auto  late   = vec2(_cursorPos, lineHeight/2);
331         late        += style.clientLeftTop;
332         late.x      -= _scrollLength + _selectionLength/2;
333 
334         shader.use();
335         shader.matrix.late = vec3( late, 0 );
336         shader.color = colorset.border;
337         _selectionElm.draw( shader );
338     }
339 
340     ///
341     override @property bool trackable () { return true; }
342     ///
343     override @property bool focusable () { return true; }
344 }