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 }