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 }