1 /// 2 module glui.text_input; 3 4 import raylib; 5 6 import glui.node; 7 import glui.input; 8 import glui.label; 9 import glui.style; 10 import glui.utils; 11 import glui.scroll; 12 import glui.structs; 13 14 alias textInput = simpleConstructor!GluiTextInput; 15 16 /// Raylib: Get pressed char 17 private extern (C) int GetCharPressed() nothrow @nogc; 18 19 @safe: 20 21 /// Text input field. 22 /// 23 /// Styles: $(UL 24 /// $(LI `style` = Default style for the input.) 25 /// $(LI `focusStyle` = Style for when the input is focused.) 26 /// $(LI `emptyStyle` = Style for when the input is empty, i.e. the placeholder is visible. Text should usually be 27 /// grayed out.) 28 /// ) 29 class GluiTextInput : GluiInput!GluiNode { 30 31 mixin DefineStyles!( 32 "emptyStyle", q{ style }, 33 ); 34 mixin ImplHoveredRect; 35 36 /// Time in seconds before the cursor toggles visibility. 37 static immutable float blinkTime = 1; 38 39 public { 40 41 /// Size of the field. 42 auto size = Vector2(200, 0); 43 44 /// Value of the field. 45 string value; 46 47 /// A placeholder text for the field, displayed when the field is empty. Style using `emptyStyle`. 48 string placeholder; 49 50 /// TODO. If true, this input accepts multiple lines. 51 bool multiline; 52 53 } 54 55 /// Underlying label controlling the content. Needed to properly adjust it to scroll. 56 private GluiScrollable!(TextImpl, "true") contentLabel; 57 58 static foreach (index; 0 .. BasicNodeParamLength) { 59 60 /// Create a text input. 61 /// Params: 62 /// sup = Node parameters. 63 /// placeholder = Placeholder text for the field. 64 /// submitted = Callback for when the field is submitted (enter pressed, ctrl+enter if multiline). 65 this(BasicNodeParam!index sup, string placeholder = "", void delegate() @trusted submitted = null) { 66 67 super(sup); 68 this.placeholder = placeholder; 69 this.submitted = submitted; 70 71 // Create the label 72 this.contentLabel = new typeof(contentLabel)(.layout!(1, "fill")); 73 74 with (this.contentLabel) { 75 76 scrollBar.width = 0; 77 disableWrap = true; 78 ignoreMouse = true; 79 80 } 81 82 } 83 84 } 85 86 protected override void resizeImpl(Vector2 area) { 87 88 import std.algorithm : max; 89 90 // Set the size 91 minSize = size; 92 93 // Single line 94 if (!multiline) { 95 96 // Set height to at least the font size 97 minSize.y = max(minSize.y, style.fontSize * style.lineHeight); 98 99 } 100 101 // Set the label text 102 contentLabel.text = (value == "") ? placeholder : value; 103 104 // Inherit main style 105 const theme = [ 106 107 &TextImpl.styleKey: cast(const Style) style, 108 109 ]; 110 111 // Resize the label 112 contentLabel.resize(tree, theme, Vector2(0, minSize.y)); 113 114 } 115 116 protected override void drawImpl(Rectangle outer, Rectangle inner) @trusted { 117 118 // Note: We're drawing the label in `outer` as the presence of the label is meant to be transparent. 119 120 import std.algorithm : min, max; 121 122 const style = pickStyle(); 123 const scrollOffset = max(0, contentLabel.scrollMax - inner.w); 124 125 // Fill the background 126 style.drawBackground(outer); 127 128 // Copy the style to the label 129 contentLabel.activeStyle = style; 130 131 // Set the scroll 132 contentLabel.scroll = cast(size_t) scrollOffset; 133 134 // If the box isn't focused 135 if (!isFocused) { 136 137 // Just draw the text 138 contentLabel.draw(outer); 139 return; 140 141 } 142 143 // Draw the label 144 contentLabel.draw(outer); 145 146 // Add a blinking caret 147 if (GetTime % (blinkTime*2) < blinkTime) { 148 149 const lineHeight = style.fontSize * style.lineHeight; 150 const margin = style.fontSize / 10f; 151 152 // Put the caret at the start if the placeholder is shown 153 const textWidth = value.length 154 ? min(contentLabel.scrollMax, inner.w) 155 : 0; 156 157 // Get caret position 158 const end = Vector2( 159 inner.x + textWidth, 160 inner.y + inner.height, 161 ); 162 163 // Draw the caret 164 DrawLineV( 165 end - Vector2(0, lineHeight - margin), 166 end - Vector2(0, margin), 167 style.textColor 168 ); 169 170 } 171 172 } 173 174 // Do nothing, we take mouse focus while drawing. 175 protected override void mouseImpl() @trusted { 176 177 // Update status 178 if (IsMouseButtonDown(MouseButton.MOUSE_LEFT_BUTTON)) { 179 180 isFocused = true; 181 182 } 183 184 } 185 186 protected override bool keyboardImpl() @trusted { 187 188 import std.uni : isAlpha, isWhite; 189 import std.range : back; 190 import std.string : chop; 191 192 bool backspace = false; 193 string input; 194 195 // Get pressed key 196 while (true) { 197 198 // Backspace 199 if (value.length && IsKeyPressed(KeyboardKey.KEY_BACKSPACE)) { 200 201 /// If true, delete whole words 202 const word = IsKeyDown(KeyboardKey.KEY_LEFT_CONTROL); 203 204 // Remove the last character 205 do { 206 207 const lastChar = value.back; 208 value = value.chop; 209 210 backspace = true; 211 212 // Stop instantly if there are no characters left 213 if (value.length == 0) break; 214 215 216 // Whitespace, continue deleting 217 if (lastChar.isWhite) continue; 218 219 // Matching alpha, continue deleting 220 else if (value.back.isAlpha == lastChar.isAlpha) continue; 221 222 // Break in other cases 223 break; 224 225 } 226 227 // Repeat only if requested to delete whole words 228 while (word); 229 230 } 231 232 // Submit 233 if (!multiline && IsKeyPressed(KeyboardKey.KEY_ENTER)) { 234 235 isFocused = false; 236 if (submitted) submitted(); 237 238 return true; 239 240 } 241 242 243 // Read text 244 if (const key = GetCharPressed()) { 245 246 // Append to char arrays 247 input ~= cast(dchar) key; 248 249 } 250 251 // Stop if nothing left 252 else break; 253 254 } 255 256 value ~= input; 257 258 // Trigger callback 259 if ((input.length || backspace) && changed) { 260 261 // Trigger change 262 changed(); 263 264 // Update the size of the input 265 updateSize(); 266 267 return true; 268 269 } 270 271 // Even if nothing changed, user might have held the key for a while which this function probably wouldn't have 272 // caught, so we'd be returning false-positives all the time. 273 // The safest way is to just return true always, text input really is complex enough we can assume we did take 274 // any input there could be. 275 return true; 276 277 } 278 279 override const(Style) pickStyle() const { 280 281 // Disabled 282 if (isDisabledInherited) return disabledStyle; 283 284 // Focused 285 else if (isFocused) return focusStyle; 286 287 // Empty text (display placeholder) 288 else if (value == "") return emptyStyle; 289 290 // Other styles 291 else return super.pickStyle(); 292 293 } 294 295 } 296 297 private class TextImpl : GluiLabel { 298 299 mixin DefineStyles!( 300 "activeStyle", q{ style } 301 ); 302 303 this(T...)(T args) { 304 305 super(args); 306 307 } 308 309 // Same as parent, but doesn't draw background 310 override void drawImpl(Rectangle outer, Rectangle inner) { 311 312 const style = pickStyle(); 313 style.drawText(inner, text); 314 315 } 316 317 override const(Style) pickStyle() const { 318 319 return activeStyle; 320 321 } 322 323 }