1 module evael.graphics.gui.controls.TextBox; 2 3 import std.math : round; 4 import std.array : insertInPlace; 5 import std.range; 6 import std.string : toStringz; 7 import std.conv : to; 8 9 import evael.graphics.gui.controls.Control; 10 11 import evael.system.Input; 12 13 import evael.utils.Math; 14 import evael.utils.Size; 15 import evael.utils.Color; 16 17 extern (Windows) short GetKeyState(int nVirtKey); 18 19 class TextBox : Control 20 { 21 /// Text 22 private wstring m_text; 23 24 private wstring m_visibleText; 25 26 /// Text position 27 private vec2 m_textPosition; 28 29 private int m_textAlign; 30 31 /// Caret position 32 private vec2 m_caretPosition; 33 34 /// Caret index in text 35 private uint m_caretIndex; 36 37 /// Characters limit 38 private uint m_maxCharactersCount; 39 40 private bool m_inSelectMode; 41 42 private float m_textWidth; 43 44 private float m_padding; 45 46 public this(in float x, in float y, in int width, in int height) 47 { 48 this(vec2(x, y,), Size!int(width, height)); 49 } 50 51 public this()(in auto ref vec2 position, in auto ref Size!int size) 52 { 53 super(position, size); 54 55 this.m_name = "textBox"; 56 this.m_padding = 4.0f; 57 this.m_textPosition = vec2(0.0f, 0.0f); 58 this.m_caretPosition = vec2(this.m_padding, 3.0f); 59 this.m_inSelectMode = false; 60 this.m_maxCharactersCount = 500; 61 this.m_textAlign = NVGalign.NVG_ALIGN_LEFT | NVGalign.NVG_ALIGN_TOP; 62 this.m_isFocusable = true; 63 } 64 65 /** 66 * Renders the textbox 67 */ 68 public override void draw(in float deltaTime) 69 { 70 if(!this.m_isVisible) 71 { 72 return; 73 } 74 75 super.draw(deltaTime); 76 77 immutable x = this.m_realPosition.x; 78 immutable y = this.m_realPosition.y; 79 immutable w = this.m_size.width; 80 immutable h = this.m_size.height; 81 82 auto vg = this.m_nvg; 83 84 if(this.m_theme.borderType == Theme.BorderType.Solid) 85 { 86 nvgBeginPath(vg); 87 nvgRoundedRect(vg, x, y, w, h, this.m_theme.cornerRadius); 88 nvgStrokeColor(vg, this.m_theme.borderColor.asNvg); 89 nvgStroke(vg); 90 } 91 92 nvgSave(vg); 93 94 nvgIntersectScissor(vg, x + this.m_padding, y, this.m_size.width - (this.m_padding * 2), this.m_size.height); 95 96 this.m_theme.font.draw(this.m_text, vec2(x + this.m_padding, y) + this.m_textPosition, 97 this.m_theme.fontColor, this.m_theme.fontSize, this.m_theme.drawTextShadow); 98 99 nvgRestore(vg); 100 101 // Caret 102 if(this.m_hasFocus) 103 { 104 nvgBeginPath(vg); 105 nvgMoveTo(vg, this.m_caretPosition.x + x, this.m_caretPosition.y + y); 106 nvgLineTo(vg, this.m_caretPosition.x + x, this.m_caretPosition.y + y + h - 5.5f); 107 nvgStrokeColor(vg, this.m_theme.fontColor.asNvg); 108 nvgStroke(vg); 109 } 110 } 111 112 /** 113 * Char event 114 * Params: 115 */ 116 public override void onText(in int key) 117 { 118 if(this.m_maxCharactersCount != 0 && this.m_text.length >= this.m_maxCharactersCount) 119 return; 120 121 super.onText(key); 122 123 immutable newChar = cast(char)key; 124 125 // Return button 126 if(key == 13) 127 { 128 return; 129 } 130 131 this.m_text.insertInPlace(this.m_caretIndex, newChar); 132 this.moveCaretToRight(); 133 } 134 135 /** 136 * Key pressed event 137 * Params: 138 * key : key pressed 139 */ 140 public override void onKey(in int key) 141 { 142 super.onKey(key); 143 144 if(this.m_text.length == 0) 145 { 146 return; 147 } 148 149 switch(key) 150 { 151 // Delete 152 case Key.Back: 153 154 if(this.m_inSelectMode) 155 { 156 this.clear(); 157 } 158 else 159 { 160 if(this.m_caretIndex == 0) 161 { 162 return; 163 } 164 165 this.moveCaretToLeft(); 166 167 auto rest = this.m_text.save(); 168 rest.popFrontN(this.m_caretIndex + 1); 169 this.m_text = this.m_text[0 .. this.m_caretIndex] ~ rest; 170 } 171 172 break; 173 174 // Left 175 case Key.Left: 176 if(this.m_caretIndex == 0) 177 { 178 return; 179 } 180 181 if(this.m_inSelectMode) 182 { 183 this.m_inSelectMode = false; 184 } 185 186 this.moveCaretToLeft(); 187 188 break; 189 190 // Right 191 case Key.Right: 192 193 if(this.m_caretIndex == this.m_text.length) 194 { 195 return; 196 } 197 198 if(this.m_inSelectMode) 199 { 200 this.m_inSelectMode = false; 201 } 202 203 this.moveCaretToRight(); 204 205 break; 206 207 // A 208 case Key.A: 209 if(GetKeyState(0x11) < 0) 210 { 211 // TODO: 212 /*this.m_inSelectMode = true; 213 214 this.m_buffer.bind(); 215 216 ubyte[16] data; 217 218 this.initializeArrayFromColor(data.ptr, Color.red); 219 220 glBufferSubData(GL_ARRAY_BUFFER, 48, data.sizeof, data.ptr);*/ 221 } 222 223 break; 224 225 // Delete 226 case Key.Delete: 227 228 break; 229 230 // Home 231 case Key.Home: 232 this.m_caretIndex = 0; 233 this.m_textPosition.x = 0; 234 this.m_caretPosition.x = this.m_padding; 235 break; 236 237 // End 238 case Key.End: 239 this.m_caretIndex = this.m_text.length; 240 this.m_textPosition.x = 0; 241 this.m_caretPosition.x = this.m_size.width - this.m_padding; 242 break; 243 244 default: 245 break; 246 } 247 } 248 249 /** 250 * Mouse click 251 */ 252 public override void onMouseClick(in MouseButton mouseButton, in ref vec2 mousePosition) 253 { 254 super.onMouseClick(mouseButton, mousePosition); 255 this.switchState!(State.Clicked); 256 257 // this.m_theme.borderColor = Color.Orange; 258 259 this.m_hasFocus = true; 260 } 261 262 /** 263 * Mouse enters control's rect 264 * Params: 265 * mousePosition : mouse's position 266 */ 267 public override void onMouseMove(in ref vec2 mousePosition) 268 { 269 this.switchState!(State.Hovered); 270 } 271 272 /** 273 * Mouse leaves control's rect 274 */ 275 public override void onMouseLeave() 276 { 277 this.switchState!(State.Normal); 278 } 279 280 281 /** 282 * Moves caret to left 283 */ 284 private void moveCaretToLeft() 285 { 286 auto i = --this.m_caretIndex; 287 288 auto glyph = this.m_theme.font.getGlyphPosition(i, this.m_padding, this.m_text, this.m_theme.fontSize); 289 290 // Explanation of : glyph.x - (-this.m_textPosition.x); 291 // We get glyph position with x = 0 292 // If the text position has been moved, glyph.x become invalid cause text is not at coord x = 0 anymore, but we still get glyph with x = 0 293 // So we substract this value from glyph.x 294 this.m_caretPosition.x = glyph.x - (-this.m_textPosition.x); 295 296 // We check if the new displayed character is gonna be displayed outside of textbox 297 if(this.m_caretPosition.x < this.m_padding) 298 { 299 // Yes, we need to move text position to the right 300 // We just need to add abs(caretPosition.x) to text.x 301 this.m_textPosition.x = this.m_textPosition.x + (- this.m_caretPosition.x ) + this.m_padding; 302 303 this.m_caretPosition.x = this.m_padding; 304 } 305 } 306 307 /** 308 * Moves caret to right 309 */ 310 private void moveCaretToRight() 311 { 312 auto i = ++this.m_caretIndex; 313 314 NVGglyphPosition glyph; 315 316 if(this.m_caretIndex < this.m_text.length) 317 { 318 glyph = this.m_theme.font.getGlyphPosition(i, this.m_padding, this.m_text, this.m_theme.fontSize); 319 // Explanation of : glyph.x - (-this.m_textPosition.x); 320 // We get glyph position with x = 0 (if padding = 0) 321 // If the text position has been moved, glyph.x become invalid cause text is not at coord x = 0 anymore, but we still get glyph with x = 0 322 // So we substract this value from glyph.x 323 this.m_caretPosition.x = glyph.x - (-this.m_textPosition.x); 324 } 325 else 326 { 327 glyph = this.m_theme.font.getGlyphPosition(i - 1, this.m_padding, this.m_text, this.m_theme.fontSize); 328 329 // Adding text at the end, we need to do that 330 glyph.x = glyph.maxx; 331 332 this.m_caretPosition.x = glyph.maxx; 333 } 334 335 immutable w = this.m_size.width - this.m_padding; 336 337 // We check if the new displayed character is gonna be displayed outside of textbox 338 if(this.m_caretPosition.x > w) 339 { 340 // Yes, we need to move text position to the left 341 // TextBox width = 120 342 // if glyph.x = 123, then we move text to -(123 - 120) 343 this.m_textPosition.x = -(glyph.x - w); 344 345 this.m_caretPosition.x = w; 346 } 347 } 348 349 public override void initialize() 350 { 351 super.initialize(); 352 353 float[4] bounds = this.m_theme.font.getTextBounds(this.m_text, 0, this.m_theme.fontSize); 354 355 immutable float h = bounds[3] - bounds[1]; 356 357 if(h > this.m_size.height) 358 { 359 this.m_size.height = cast(int)h + 5; 360 } 361 362 import std.math : round; 363 364 this.m_textPosition.y = round(this.m_size.halfHeight - (h / 2)); 365 } 366 367 /** 368 * Clear the textbox 369 */ 370 public void clear() nothrow 371 { 372 this.m_text = ""; 373 374 this.m_textPosition.x = 0.0f; 375 this.m_caretPosition.x = 0.0f; 376 this.m_caretIndex = 0; 377 378 this.m_inSelectMode = false; 379 380 this.switchState!(State.Hovered); 381 } 382 383 @property 384 { 385 public string text() const nothrow 386 { 387 import std.utf; 388 return toUTF8(this.m_text); 389 } 390 391 public wstring textw() const nothrow @nogc 392 { 393 return this.m_text; 394 } 395 396 public void text(in wstring value) nothrow @nogc 397 { 398 this.m_text = value; 399 this.m_visibleText = value; 400 } 401 402 public void maxCharactersCount(in ushort value) nothrow @nogc 403 { 404 this.m_maxCharactersCount = value; 405 } 406 } 407 408 }