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.style.widget,
12        w4d.task.window,
13        w4d.util.clipping,
14        w4d.util.textline,
15        w4d.widget.button,
16        w4d.widget.text,
17        w4d.event,
18        w4d.exception;
19 import g4d.element.shape.rect,
20        g4d.ft.font,
21        g4d.glfw.cursor,
22        g4d.shader.base;
23 import gl3n.linalg;
24 import std.algorithm,
25        std.conv,
26        std.math,
27        std.range;
28 
29 /// A handler that handles chainging text.
30 alias TextChangeHandler = EventHandler!( void, dstring );
31 
32 /// A widget of line input.
33 class LineInputWidget : TextWidget
34 {
35     protected TextLine _line;
36     protected float    _cursorPos;
37     protected float    _scrollLength;
38     protected float    _selectionLength;
39 
40     override @property dstring text () { return _line.text; }
41 
42     protected dchar    _passwordChar;
43     /// Character of password filed.
44     @property passwordChar () { return _passwordChar; }
45     /// Checks if the line input is password field.
46     @property isPasswordField ()
47     {
48         return _passwordChar != dchar.init;
49     }
50 
51     protected RectElement _cursorElm;
52     protected RectElement _selectionElm;
53 
54     protected ButtonWidget _chainedButton;
55 
56     ///
57     TextChangeHandler onTextChange;
58 
59     ///
60     override bool handleMouseMove ( vec2 pos )
61     {
62         if ( super.handleMouseMove( pos ) ) {
63             return true;
64         }
65         if ( isTracked ) {
66             _line.moveCursorTo( retrieveIndexFromAbsPos( pos.x ), true );
67             return true;
68         }
69         return false;
70     }
71     ///
72     override bool handleMouseButton ( MouseButton btn, bool status, vec2 pos )
73     {
74         if ( super.handleMouseButton( btn, status, pos ) ) {
75             return true;
76         }
77         if ( btn == MouseButton.Left && status ) {
78             auto selecting = _context.shift && isFocused;
79             _line.moveCursorTo( retrieveIndexFromAbsPos( pos.x ), selecting );
80             focus();
81             return true;
82         }
83         return false;
84     }
85     ///
86     override bool handleKey ( Key key, KeyState status )
87     {
88         if ( super.handleKey( key, status ) ) return true;
89 
90         const pressing = ( status != KeyState.Release );
91 
92         if ( key == Key.Backspace && pressing ) {
93             _line.backspace();
94 
95         } else if ( key == Key.Delete && pressing ) {
96             _line.del();
97 
98         } else if ( key == Key.Left && pressing ) {
99             _line.left( _context.shift );
100         } else if ( key == Key.Right && pressing ) {
101             _line.right( _context.shift );
102         } else if ( key == Key.Home && pressing ) {
103             _line.home( _context.shift );
104         } else if ( key == Key.End && pressing ) {
105             _line.end( _context.shift );
106 
107         } else if ( key == Key.A && pressing && _context.ctrl ) {
108             _line.selectAll();
109         } else if ( key == Key.D && pressing && _context.ctrl ) {
110             _line.deselect();
111             requestRedraw();
112 
113         } else if ( key == Key.C && pressing && _context.ctrl ) {
114             Window.setClipboard( _line.selectedText );
115         } else if ( key == Key.V && pressing && _context.ctrl ) {
116             _line.insert( Window.getClipboard() );
117 
118         } else if ( key == Key.Enter && pressing ) {
119             if ( _chainedButton ) {
120                 _chainedButton.onButtonPress.call();
121             }
122 
123         } else {
124             return false;
125         }
126         return true;
127     }
128 
129     ///
130     override bool handleTextInput ( dchar c )
131     {
132         if ( super.handleTextInput(c) ) return true;
133 
134         _line.insert( c.to!dstring );
135         return true;
136     }
137 
138     ///
139     override void handleFocused ( bool status )
140     {
141         if ( !status ) {
142             _line.deselect();
143             requestRedraw();
144         }
145         super.handleFocused( status );
146     }
147 
148     ///
149     override void handleChangeStatus ( WidgetState s, bool e )
150     {
151         e? _line.lock(): _line.unlock();
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     /// Chains the button.
253     /// Chained button will be handled when Enter is pressed.
254     void chainButton ( ButtonWidget btn )
255     {
256         _chainedButton = btn;
257     }
258 
259     protected float retrieveScrollLength ()
260     {
261         auto size = style.box.clientSize;
262         if ( _scrollLength >= _cursorPos ) {
263             auto index = max( _line.cursorIndex-1, 0 );
264             return retrievePosFromIndex(index);
265 
266         } else if ( _scrollLength+size.x <= _cursorPos ) {
267             auto index = min( _line.cursorIndex+1, _line.text.length );
268             return retrievePosFromIndex(index) - size.x;
269         }
270         return _scrollLength;
271     }
272     protected void updateSelectionRect ()
273     {
274         if ( !_line.isSelecting ) return;
275 
276         const selectionPos = retrievePosFromIndex( _line.selectionIndex );
277         const newLength    = _cursorPos - selectionPos;
278         if ( newLength == _selectionLength ) return;
279 
280         _selectionLength = newLength;
281 
282         const size = vec2( _selectionLength.abs, lineHeight );
283         _selectionElm.resize( size );
284     }
285 
286     protected override void drawText ( Window w, float xshift = 0 )
287     {
288         auto pos = style.box.
289             borderInsideLeftTop + style.translate;
290         w.clip.pushRect( pos, style.box.borderInsideSize );
291 
292         super.drawText( w, -_scrollLength+xshift );
293         if ( isFocused ) {
294             drawCursor( w );
295         }
296         if ( _line.isSelecting ) {
297             drawSelectionRect( w );
298         }
299 
300         w.clip.popRect();
301     }
302     protected void drawCursor ( Window w )
303     {
304         auto  shader = w.shaders.fill3;
305         const saver  = ShaderStateSaver( shader );
306         auto  late   = vec2(_cursorPos, lineHeight/2);
307         late        += style.clientLeftTop;
308         late.x      -= _scrollLength;
309 
310         shader.use();
311         shader.matrix.late = vec3( late, 0 );
312         shader.color = colorset.foreground;
313         _cursorElm.draw( shader );
314     }
315     protected void drawSelectionRect ( Window w )
316     {
317         auto  shader = w.shaders.fill3;
318         const saver  = ShaderStateSaver( shader );
319         auto  late   = vec2(_cursorPos, lineHeight/2);
320         late        += style.clientLeftTop;
321         late.x      -= _scrollLength + _selectionLength/2;
322 
323         shader.use();
324         shader.matrix.late = vec3( late, 0 );
325         shader.color = colorset.border;
326         _selectionElm.draw( shader );
327     }
328 
329     ///
330     override @property bool trackable () { return true; }
331     ///
332     override @property bool focusable () { return true; }
333 }