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 static Font loadFont(string file, int fontSize, dchar[] fontChars = null) @trusted { 171 172 const realSize = fontSize * hidpiScale.y; 173 174 return LoadFontEx(file.toStringz, cast(int) realSize.ceil, cast(int*) fontChars.ptr, 175 cast(int) fontChars.length); 176 177 } 178 179 /// Update the style with given D code. 180 /// 181 /// This allows each init code to have a consistent default scope, featuring `glui`, `raylib` and chosen `std` 182 /// modules. 183 /// 184 /// Params: 185 /// init = Code to update the style with. 186 /// T = An compile-time object to update the scope with. 187 void update(string init)() { 188 189 import glui; 190 191 // Wrap init content in brackets to allow imports 192 // See: https://forum.dlang.org/thread/nl4vse$egk$1@digitalmars.com 193 // The thread mentions mixin templates but it's the same for string mixins too; and a mixin with multiple 194 // statements is annoyingly treated as multiple separate mixins. 195 mixin(init.format!"{ %s }"); 196 197 } 198 199 /// Ditto. 200 void update(string init, T)() { 201 202 import glui; 203 204 with (T) mixin(init.format!"{ %s }"); 205 206 } 207 208 /// Get the current font 209 inout(Font) getFont() inout @trusted { 210 211 return cast(inout) (font.recs ? font : GetFontDefault); 212 213 } 214 215 /// Measure space given text will use. 216 /// 217 /// Params: 218 /// availableSpace = Space available for drawing. 219 /// text = Text to draw. 220 /// wrap = If true (default), the text will be wrapped to match available space, unless the space is 221 /// empty. 222 /// Returns: 223 /// If `availableSpace` is a vector, returns the result as a vector. 224 /// 225 /// If `availableSpace` is a rectangle, returns a rectangle of the size of the result, offset to the position 226 /// of the given rectangle. 227 Vector2 measureText(Vector2 availableSpace, string text, bool wrap = true) const { 228 229 auto wrapped = wrapText(availableSpace.x, text, !wrap || availableSpace.x == 0); 230 231 return Vector2( 232 wrapped.map!"a.width".maxElement, 233 wrapped.length * fontSize * lineHeight, 234 ); 235 236 } 237 238 /// Ditto 239 Rectangle measureText(Rectangle availableSpace, string text, bool wrap = true) const { 240 241 const vec = measureText( 242 Vector2(availableSpace.width, availableSpace.height), 243 text, wrap 244 ); 245 246 return Rectangle( 247 availableSpace.x, availableSpace.y, 248 vec.x, vec.y 249 ); 250 251 } 252 253 /// Draw text using the same params as `measureText`. 254 void drawText(Rectangle rect, string text, bool wrap = true) const { 255 256 // Text position from top, relative to given rect 257 size_t top; 258 259 const totalLineHeight = fontSize * lineHeight; 260 261 // Draw each line 262 foreach (line; wrapText(rect.width, text, !wrap)) { 263 264 scope (success) top += cast(size_t) ceil(lineHeight * fontSize); 265 266 // Stop if drawing below rect 267 if (top > rect.height) break; 268 269 // Text position from left 270 size_t left; 271 272 const margin = (totalLineHeight - fontSize)/2; 273 274 foreach (word; line.words) { 275 276 const position = Vector2(rect.x + left, rect.y + top + margin); 277 278 () @trusted { 279 280 // cast(): raylib doesn't mutate the font. The parameter would probably be defined `const`, but 281 // since it's not transistive in C, and font is a struct with a pointer inside, it only matters 282 // in D. 283 284 DrawTextEx(cast() getFont, word.text.toStringz, position, fontSize, fontSize * charSpacing, 285 textColor); 286 287 }(); 288 289 left += cast(size_t) ceil(word.width + fontSize * wordSpacing); 290 291 } 292 293 } 294 295 } 296 297 /// Split the text into multiple lines in order to fit within given width. 298 /// 299 /// Params: 300 /// width = Container width the text should fit in. 301 /// text = Text to wrap. 302 /// lineFeedsOnly = If true, this should only wrap the text on line feeds. 303 TextLine[] wrapText(double width, string text, bool lineFeedsOnly) const { 304 305 const spaceSize = cast(size_t) ceil(fontSize * wordSpacing); 306 307 auto result = [TextLine()]; 308 309 /// Get width of the given word. 310 float wordWidth(string wordText) @trusted { 311 312 // See drawText for cast() 313 return MeasureTextEx(cast() getFont, wordText.toStringz, fontSize, fontSize * charSpacing).x; 314 315 } 316 317 TextLine.Word[] words; 318 319 auto whitespaceSplit = text[] 320 .splitter!((a, string b) => [' ', '\n'].canFind(a), Yes.keepSeparators)(" "); 321 322 // Pass 1: split on words, calculate minimum size 323 foreach (chunk; whitespaceSplit.chunks(2)) { 324 325 const wordText = chunk.front; 326 const size = cast(size_t) wordWidth(wordText).ceil; 327 328 chunk.popFront; 329 const feed = chunk.empty 330 ? false 331 : chunk.front == "\n"; 332 333 // Push the word 334 words ~= TextLine.Word(wordText, size, feed); 335 336 // Update minimum size 337 if (size > width) width = size; 338 339 } 340 341 // Pass 2: calculate total size 342 foreach (word; words) { 343 344 scope (success) { 345 346 // Start a new line if this words is followed by a line feed 347 if (word.lineFeed) result ~= TextLine(); 348 349 } 350 351 auto lastLine = &result[$-1]; 352 353 // If last line is empty 354 if (lastLine.words == []) { 355 356 // Append to it immediately 357 lastLine.words ~= word; 358 lastLine.width += word.width; 359 continue; 360 361 } 362 363 364 // Check if this word can fit 365 if (lineFeedsOnly || lastLine.width + spaceSize + word.width <= width) { 366 367 // Push it to this line 368 lastLine.words ~= word; 369 lastLine.width += spaceSize + word.width; 370 371 } 372 373 // It can't 374 else { 375 376 // Push it to a new line 377 result ~= TextLine([word], word.width); 378 379 } 380 381 } 382 383 return result; 384 385 } 386 387 /// Draw the background 388 void drawBackground(Rectangle rect) const @trusted { 389 390 DrawRectangleRec(rect, backgroundColor); 391 392 } 393 394 /// Remove padding from the vector representing size of a box. 395 Vector2 contentBox(Vector2 size) const { 396 397 size.x = max(0, size.x - padding[0] - padding[1]); 398 size.y = max(0, size.y - padding[2] - padding[3]); 399 400 return size; 401 402 } 403 404 /// Remove padding from the given rect. 405 Rectangle contentBox(Rectangle rect) const { 406 407 rect.x += padding[0]; 408 rect.y += padding[2]; 409 410 const size = contentBox(Vector2(rect.w, rect.h)); 411 rect.width = size.x; 412 rect.height = size.y; 413 414 return rect; 415 416 } 417 418 } 419 420 /// `wrapText` result. 421 struct TextLine { 422 423 struct Word { 424 425 string text; 426 size_t width; 427 bool lineFeed; // Word is followed by a line feed. 428 429 } 430 431 /// Words on this line. 432 Word[] words; 433 434 /// Width of the line (including spaces). 435 size_t width = 0; 436 437 } 438 439 ref uint sideLeft(return ref uint[4] sides) { 440 441 return sides[Style.Side.left]; 442 443 } 444 ref uint sideRight(return ref uint[4] sides) { 445 446 return sides[Style.Side.right]; 447 448 } 449 ref uint sideTop(return ref uint[4] sides) { 450 451 return sides[Style.Side.top]; 452 453 } 454 455 ref uint sideBottom(return ref uint[4] sides) { 456 457 return sides[Style.Side.bottom]; 458 459 } 460 461 ref inout(uint[2]) sideX(return ref inout uint[4] sides) { 462 463 const start = Style.Side.left; 464 return sides[start .. start + 2]; 465 466 } 467 468 ref inout(uint[2]) sideY(return ref inout uint[4] sides) { 469 470 const start = Style.Side.top; 471 return sides[start .. start + 2]; 472 473 }