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 }