1 /// 2 module glui.style; 3 4 import raylib; 5 6 import std.math; 7 import std.range; 8 import std..string; 9 import std.typecons; 10 import std.algorithm; 11 12 import glui.utils; 13 14 public import glui.style_macros; 15 16 @safe: 17 18 /// Node theme. 19 alias StyleKeyPtr = immutable(StyleKey)*; 20 alias Theme = Style[StyleKeyPtr]; 21 22 /// An empty struct used to create unique style type identifiers. 23 struct StyleKey { } 24 25 /// Create a new style initialized with given D code. 26 /// 27 /// raylib and std.string are accessible inside by default. 28 /// 29 /// Note: It is recommended to create a root style node defining font parameters and then inherit other styles from it. 30 /// 31 /// Params: 32 /// init = D code to use. 33 /// parents = Styles to inherit from. See `Style.this` documentation for more info. 34 /// data = Data to pass to the code as the context. All fields of the struct will be within the style's scope. 35 Style style(string init, Data)(Data data, Style[] parents...) { 36 37 auto result = new Style; 38 39 with (data) with (result) mixin(init); 40 41 return result; 42 43 } 44 45 /// Ditto. 46 Style style(string init)(Style[] parents...) { 47 48 auto result = new Style(parents); 49 result.update!init; 50 51 return result; 52 53 } 54 55 /// Contains a style for a node. 56 class Style { 57 58 enum Side { 59 60 left, right, top, bottom, 61 62 } 63 64 // Internal use only, can't be private because it's used in mixins. 65 static { 66 67 Theme _currentTheme; 68 Style[] _styleStack; 69 70 } 71 72 // Text options 73 struct { 74 75 /// Font to be used for the text. 76 Font font; 77 78 /// Font size (height) in pixels. 79 float fontSize; 80 81 /// Line height, as a fraction of `fontSize`. 82 float lineHeight; 83 84 /// Space between characters, relative to font size. 85 float charSpacing; 86 87 /// Space between words, relative to the font size. 88 float wordSpacing; 89 90 /// Text color. 91 Color textColor; 92 93 } 94 95 // Background 96 struct { 97 98 /// Background color of the node. 99 Color backgroundColor; 100 101 } 102 103 // Spacing 104 struct { 105 106 /// Margin (outer margin) of the node. `[left, right, top, bottom]`. 107 /// 108 /// Tip: You can directly set all margins with eg. `margin = 6;` 109 /// 110 /// See: enum `Side` 111 uint[4] margin; 112 113 /// Padding (inner margin) of the node. `[left, right, top, bottom]`. 114 /// 115 /// See: enum `Side` 116 uint[4] padding; 117 118 } 119 120 // Misc 121 struct { 122 123 /// Cursor icon to use while this node is hovered. 124 /// 125 /// Custom image cursors are not supported yet. 126 MouseCursor mouseCursor; 127 128 } 129 130 this() { } 131 132 /// Create a style by copying params of others. 133 /// 134 /// Multiple styles can be set, so if one field is set to `typeof(field).init`, it will be taken from the previous 135 /// style from the list — that is, settings from the last style override previous ones. 136 this(Style[] styles...) { 137 138 import std.meta, std.traits; 139 140 // Check each style 141 foreach (i, style; styles) { 142 143 // Inherit each field 144 static foreach (field; FieldNameTuple!(typeof(this))) {{ 145 146 auto inheritedField = mixin("style." ~ field); 147 148 // Ignore if it's set to init (unless it's the first style) 149 if (i == 0 || inheritedField != typeof(inheritedField).init) { 150 151 mixin("this." ~ field) = inheritedField; 152 153 } 154 155 }} 156 157 } 158 159 } 160 161 /// Get the default, empty style. 162 static Style init() { 163 164 static Style val; 165 if (val is null) val = new Style; 166 return val; 167 168 } 169 170 /// Update the style with given D code. 171 /// 172 /// This allows each init code to have a consistent default scope, featuring `glui`, `raylib` and chosen `std` 173 /// modules. 174 /// 175 /// Params: 176 /// init = Code to update the style with. 177 /// T = An compile-time object to update the scope with. 178 void update(string init)() { 179 180 import glui; 181 182 // Wrap init content in brackets to allow imports 183 // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com 184 // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple 185 // statements is annoyingly treated as multiple separate mixins. 186 mixin(init.format!"{ %s }"); 187 188 } 189 190 /// Ditto. 191 void update(string init, T)() { 192 193 import glui; 194 195 with (T) mixin(init.format!"{ %s }"); 196 197 } 198 199 /// Get the current font 200 inout(Font) getFont() inout @trusted { 201 202 return cast(inout) (font.recs ? font : GetFontDefault); 203 204 } 205 206 /// Measure space given text will use. 207 /// 208 /// Params: 209 /// availableSpace = Space available for drawing. 210 /// text = Text to draw. 211 /// wrap = If true (default), the text will be wrapped to match available space, unless the space is 212 /// empty. 213 /// Returns: 214 /// If `availableSpace` is a vector, returns the result as a vector. 215 /// 216 /// If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position 217 /// of the given rectangle. 218 Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const { 219 220 auto wrapped = wrapText(availableSpace.x, text, !wrap || availableSpace.x == 0); 221 222 return Vector2( 223 wrapped.map!"a.width".maxElement, 224 wrapped.length * fontSize * lineHeight, 225 ); 226 227 } 228 229 /// Ditto 230 Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const { 231 232 const vec = measureText( 233 Vector2(availableSpace.width, availableSpace.height), 234 text, wrap 235 ); 236 237 return Rectangle( 238 availableSpace.x, availableSpace.y, 239 vec.x, vec.y 240 ); 241 242 } 243 244 /// Draw text using the same params as `measureText`. 245 void drawText(Rectangle rect, string text, bool wrap = true) const { 246 247 // Text position from top, relative to given rect 248 size_t top; 249 250 const totalLineHeight = fontSize * lineHeight; 251 252 // Draw each line 253 foreach (line; wrapText(rect.width, text, !wrap)) { 254 255 scope (success) top += cast(size_t) ceil(lineHeight * fontSize); 256 257 // Stop if drawing below rect 258 if (top > rect.height) break; 259 260 // Text position from left 261 size_t left; 262 263 const margin = (totalLineHeight - fontSize)/2; 264 265 foreach (word; line.words) { 266 267 const position = Vector2(rect.x + left, rect.y + top + margin); 268 269 () @trusted { 270 271 // cast(): raylib doesn't mutate the font. The parameter would probably be defined `const`, but 272 // since it's not transistive in C, and font is a struct with a pointer inside, it only matters 273 // in D. 274 275 DrawTextEx(cast() getFont, word.text.toStringz, position, fontSize, fontSize * charSpacing, 276 textColor); 277 278 }(); 279 280 left += cast(size_t) ceil(word.width + fontSize * wordSpacing); 281 282 } 283 284 } 285 286 } 287 288 /// Split the text into multiple lines in order to fit within given width. 289 /// 290 /// Params: 291 /// width = Container width the text should fit in. 292 /// text = Text to wrap. 293 /// lineFeedsOnly = If true, this should only wrap the text on line feeds. 294 TextLine[] wrapText(double width, string text, bool lineFeedsOnly) const { 295 296 const spaceSize = cast(size_t) ceil(fontSize * wordSpacing); 297 298 auto result = [TextLine()]; 299 300 /// Get width of the given word. 301 float wordWidth(string wordText) @trusted { 302 303 // See drawText for cast() 304 return MeasureTextEx(cast() getFont, wordText.toStringz, fontSize, fontSize * charSpacing).x; 305 306 } 307 308 TextLine.Word[] words; 309 310 auto whitespaceSplit = text[] 311 .splitter!((a, string b) => [' ', '\n'].canFind(a), Yes.keepSeparators)(" "); 312 313 // Pass 1: split on words, calculate minimum size 314 foreach (chunk; whitespaceSplit.chunks(2)) { 315 316 const wordText = chunk.front; 317 const size = cast(size_t) wordWidth(wordText).ceil; 318 319 chunk.popFront; 320 const feed = chunk.empty 321 ? false 322 : chunk.front == "\n"; 323 324 // Push the word 325 words ~= TextLine.Word(wordText, size, feed); 326 327 // Update minimum size 328 if (size > width) width = size; 329 330 } 331 332 // Pass 2: calculate total size 333 foreach (word; words) { 334 335 scope (success) { 336 337 // Start a new line if this words is followed by a line feed 338 if (word.lineFeed) result ~= TextLine(); 339 340 } 341 342 auto lastLine = &result[$-1]; 343 344 // If last line is empty 345 if (lastLine.words == []) { 346 347 // Append to it immediately 348 lastLine.words ~= word; 349 lastLine.width += word.width; 350 continue; 351 352 } 353 354 355 // Check if this word can fit 356 if (lineFeedsOnly || lastLine.width + spaceSize + word.width <= width) { 357 358 // Push it to this line 359 lastLine.words ~= word; 360 lastLine.width += spaceSize + word.width; 361 362 } 363 364 // It can't 365 else { 366 367 // Push it to a new line 368 result ~= TextLine([word], word.width); 369 370 } 371 372 } 373 374 return result; 375 376 } 377 378 /// Draw the background 379 void drawBackground(Rectangle rect) const @trusted { 380 381 DrawRectangleRec(rect, backgroundColor); 382 383 } 384 385 /// Remove padding from the vector representing size of a box. 386 Vector2 contentBox(Vector2 size) const { 387 388 size.x = max(0, size.x - padding[0] - padding[1]); 389 size.y = max(0, size.y - padding[2] - padding[3]); 390 391 return size; 392 393 } 394 395 /// Remove padding from the given rect. 396 Rectangle contentBox(Rectangle rect) const { 397 398 rect.x += padding[0]; 399 rect.y += padding[2]; 400 401 const size = contentBox(Vector2(rect.w, rect.h)); 402 rect.width = size.x; 403 rect.height = size.y; 404 405 return rect; 406 407 } 408 409 } 410 411 /// `wrapText` result. 412 struct TextLine { 413 414 struct Word { 415 416 string text; 417 size_t width; 418 bool lineFeed; // Word is followed by a line feed. 419 420 } 421 422 /// Words on this line. 423 Word[] words; 424 425 /// Width of the line (including spaces). 426 size_t width = 0; 427 428 } 429 430 ref uint sideLeft(return ref uint[4] sides) { 431 432 return sides[Style.Side.left]; 433 434 } 435 ref uint sideRight(return ref uint[4] sides) { 436 437 return sides[Style.Side.right]; 438 439 } 440 ref uint sideTop(return ref uint[4] sides) { 441 442 return sides[Style.Side.top]; 443 444 } 445 446 ref uint sideBottom(return ref uint[4] sides) { 447 448 return sides[Style.Side.bottom]; 449 450 } 451 452 ref uint[2] sideX(return ref uint[4] sides) { 453 454 const start = Style.Side.left; 455 return sides[start .. start + 2]; 456 457 } 458 459 ref uint[2] sideY(return ref uint[4] sides) { 460 461 const start = Style.Side.top; 462 return sides[start .. start + 2]; 463 464 }