1 /// This showcase is a set of examples and tutorials designed to illustrate core features of Fluid and provide a quick 2 /// start guide to developing applications using Fluid. 3 /// 4 /// This module is the central piece of the showcase, gluing it together. It loads and parses each module to display 5 /// it as a document. It's not trivial; other modules from this package are designed to offer better guidance on Fluid 6 /// usage, but it might also be useful to people intending to implement similar functionality. 7 /// 8 /// To get started with the showcase, use `dub run fluid:showcase`, which should compile and run the showcase. The 9 /// program explains different components of the library and provides code examples, but you're free to browse through 10 /// its files if you like! introduction.d might be a good start. I hope this directory proves as a useful learning 11 /// resource. 12 module fluid.showcase; 13 14 import fluid; 15 import dparse.ast; 16 17 import std.string; 18 import std.traits; 19 import std.algorithm; 20 21 22 /// Maximum content width, used for code samples, since they require more space. 23 enum maxContentSize = .sizeLimitX(1000); 24 25 /// Reduced content width, used for document text. 26 enum contentSize = .sizeLimitX(800); 27 28 Theme mainTheme; 29 Theme headingTheme; 30 Theme subheadingTheme; 31 Theme exampleListTheme; 32 Theme codeTheme; 33 Theme previewWrapperTheme; 34 Theme highlightBoxTheme; 35 Theme warningTheme; 36 37 enum Chapter { 38 @"Introduction" introduction, 39 @"Frames" frames, 40 @"Buttons & mutability" buttons, 41 @"Node slots" slots, 42 @"Themes" themes, 43 }; 44 45 /// The entrypoint prepares themes and the window. The UI is build in `createUI()`. 46 void main(string[] args) { 47 48 // Prepare themes 49 mainTheme = makeTheme!q{ 50 Frame.styleAdd!q{ 51 margin.sideX = 12; 52 margin.sideY = 16; 53 Grid.styleAdd.margin.sideY = 0; 54 GridRow.styleAdd.margin = 0; 55 ScrollFrame.styleAdd.margin = 0; 56 }; 57 Label.styleAdd!q{ 58 margin.sideX = 12; 59 margin.sideY = 7; 60 Button!().styleAdd; 61 }; 62 }; 63 64 headingTheme = mainTheme.makeTheme!q{ 65 Label.styleAdd!q{ 66 typeface = Style.loadTypeface(20); 67 margin.sideTop = 20; 68 margin.sideBottom = 10; 69 }; 70 }; 71 72 subheadingTheme = mainTheme.makeTheme!q{ 73 Label.styleAdd!q{ 74 typeface = Style.loadTypeface(16); 75 margin.sideTop = 16; 76 margin.sideBottom = 8; 77 }; 78 }; 79 80 exampleListTheme = mainTheme.makeTheme!q{ 81 Button!().styleAdd!q{ 82 padding.sideX = 8; 83 padding.sideY = 16; 84 margin = 2; 85 }; 86 }; 87 88 highlightBoxTheme = makeTheme!q{ 89 border = 1; 90 borderStyle = colorBorder(color!"#e62937"); 91 }; 92 93 codeTheme = mainTheme.makeTheme!q{ 94 import std.file, std.path; 95 96 typeface = Style.loadTypeface(thisExePath.dirName.buildPath("../examples/ibm-plex-mono.ttf"), 12); 97 backgroundColor = color!"#dedede"; 98 99 Frame.styleAdd!q{ 100 padding = 0; 101 }; 102 Label.styleAdd!q{ 103 margin = 0; 104 padding.sideX = 12; 105 padding.sideY = 16; 106 }; 107 }; 108 109 previewWrapperTheme = mainTheme.makeTheme!q{ 110 NodeSlot!Node.styleAdd!q{ 111 border = 1; 112 borderStyle = colorBorder(color!"#dedede"); 113 }; 114 }; 115 116 warningTheme = mainTheme.makeTheme!q{ 117 Label.styleAdd!q{ 118 padding.sideX = 16; 119 padding.sideY = 6; 120 border = 1; 121 borderStyle = colorBorder(color!"#ffc30f"); 122 backgroundColor = color!"#ffe186"; 123 textColor = color!"#000"; 124 }; 125 }; 126 127 // Create the UI — pass the first argument to load a chapter under the given name 128 auto ui = args.length > 1 129 ? createUI(args[1]) 130 : createUI(); 131 132 /// Start the window. 133 startWindow(ui); 134 135 } 136 137 /// Raylib entrypoint. 138 version (Have_raylib_d) 139 void startWindow(Node ui) { 140 141 import raylib; 142 143 // Prepare the window 144 SetConfigFlags(ConfigFlags.FLAG_WINDOW_RESIZABLE); 145 SetTraceLogLevel(TraceLogLevel.LOG_WARNING); 146 InitWindow(1000, 750, "Fluid showcase"); 147 SetTargetFPS(60); 148 scope (exit) CloseWindow(); 149 150 // Event loop 151 while (!WindowShouldClose) { 152 153 BeginDrawing(); 154 scope (exit) EndDrawing(); 155 156 ClearBackground(color!"fff"); 157 158 // Fluid is by default configured to work with Raylib, so all you need to make them work together is a single 159 // call 160 ui.draw(); 161 162 } 163 164 } 165 166 else version (Have_arsd_official_simpledisplay) 167 void startWindow(Node ui) { 168 169 import arsd.simpledisplay; 170 171 SimpledisplayBackend backend; 172 173 // Create the window 174 auto window = new SimpleWindow(1000, 750, "Fluid showcase", 175 OpenGlOptions.yes, 176 Resizeability.allowResizing); 177 178 // Setup the backend 179 ui.backend = backend = new SimpledisplayBackend(window); 180 181 // Simpledisplay's design is more sophisticated and requires more config than Raylib 182 window.redrawOpenGlScene = { 183 ui.draw(); 184 backend.poll(); 185 }; 186 187 // 1 frame every 16 ms ≈ 60 FPS 188 window.eventLoop(16, { 189 window.redrawOpenGlSceneSoon(); 190 }); 191 192 } 193 194 Space createUI(string initialChapter = null) @safe { 195 196 import std.conv; 197 198 Chapter currentChapter; 199 ScrollFrame root; 200 Space navigationBar; 201 Label titleLabel; 202 Button!() leftButton, rightButton; 203 204 auto content = nodeSlot!Node(.layout!(1, "fill")); 205 206 void changeChapter(Chapter chapter) { 207 208 // Change the content root and show the back button 209 currentChapter = chapter; 210 content = render(chapter); 211 titleLabel.text = title(chapter); 212 213 // Show navigation 214 navigationBar.show(); 215 leftButton.isHidden = chapter == 0; 216 rightButton.isHidden = chapter == Chapter.max; 217 218 // Scroll back to top 219 root.scrollStart(); 220 221 } 222 223 // All content is scrollable 224 root = vscrollFrame( 225 .layout!"fill", 226 .mainTheme, 227 sizeLock!vspace( 228 .layout!(1, "center", "start"), 229 .maxContentSize, 230 231 // Back button 232 navigationBar = sizeLock!hspace( 233 .layout!"center", 234 .contentSize, 235 button("← Back to navigation", delegate { 236 content = exampleList(&changeChapter); 237 navigationBar.hide(); 238 leftButton.hide(); 239 rightButton.hide(); 240 }), 241 titleLabel = label(""), 242 ).hide(), 243 244 // Content 245 content = exampleList(&changeChapter), 246 247 sizeLock!hframe( 248 .layout!"center", 249 .contentSize, 250 251 // Left button 252 leftButton = button("Previous chapter", delegate { 253 changeChapter(to!Chapter(currentChapter-1)); 254 }).hide(), 255 256 // Right button 257 rightButton = button(.layout!(1, "end"), "Next chapter", delegate { 258 changeChapter(to!Chapter(currentChapter+1)); 259 }).hide(), 260 ), 261 ) 262 ); 263 264 if (initialChapter) { 265 changeChapter(to!Chapter(initialChapter)); 266 } 267 268 return root; 269 270 } 271 272 Space exampleList(void delegate(Chapter) @safe changeChapter) @safe { 273 274 import std.array; 275 import std.range; 276 277 auto chapterGrid = grid( 278 .layout!"fill", 279 .segments(3), 280 ); 281 282 // TODO This should be easier 283 auto rows = only(EnumMembers!Chapter) 284 285 // Create a button for each chapter 286 .map!(a => button( 287 .layout!"fill", 288 title(a), 289 () => changeChapter(a) 290 )) 291 292 // Split them into chunks of three 293 .chunks(3); 294 295 foreach (row; rows) { 296 chapterGrid.addRow(row.array); 297 } 298 299 return sizeLock!vspace( 300 .layout!"center", 301 .exampleListTheme, 302 .contentSize, 303 label(.layout!"center", .headingTheme, "Hello, World!"), 304 label("Pick a chapter of the tutorial to get started. Start with the first one or browse the chapters that " 305 ~ "interest you! Output previews are shown next to code samples to help you understand the content."), 306 label(.layout!"fill", .warningTheme, "While this tutorial covers the most important parts of Fluid, it's still " 307 ~ "incomplete. Content will be added in further updates of Fluid. Contributions are welcome."), 308 chapterGrid, 309 ); 310 311 } 312 313 /// Create a code block 314 Space showcaseCode(string code) { 315 316 return vframe( 317 .layout!"fill", 318 .codeTheme, 319 sizeLock!label( 320 .layout!"center", 321 .contentSize, 322 code, 323 ), 324 ); 325 326 } 327 328 /// Showcase code and its result. 329 Space showcaseCode(string code, Node node, Theme theme = null) { 330 331 // Make the node inherit the default theme rather than the one we set 332 if (node.theme is null) { 333 node.theme = either(theme, fluidDefaultTheme); 334 } 335 336 return hframe( 337 .layout!"fill", 338 339 hscrollable!label( 340 .layout!(1, "fill"), 341 .codeTheme, 342 code, 343 ).disableWrap(), 344 nodeSlot!Node( 345 .layout!(1, "fill"), 346 .previewWrapperTheme, 347 node, 348 ), 349 ); 350 351 } 352 353 /// Get the title of the given chapter. 354 string title(Chapter query) @safe { 355 356 import std.traits; 357 358 switch (query) { 359 360 static foreach (chapter; EnumMembers!Chapter) { 361 362 case chapter: 363 return __traits(getAttributes, chapter)[0]; 364 365 } 366 367 default: return null; 368 369 } 370 371 } 372 373 /// Render the given chapter. 374 Space render(Chapter query) @safe { 375 376 switch (query) { 377 378 static foreach (chapter; EnumMembers!Chapter) { 379 380 case chapter: 381 return render!chapter; 382 383 } 384 385 default: return null; 386 387 } 388 389 } 390 391 /// ditto 392 Space render(Chapter chapter)() @trusted { 393 394 import std.file; 395 import std.path; 396 import std.conv; 397 import std.meta; 398 import std.traits; 399 import dparse.lexer; 400 import dparse.parser : parseModule; 401 import dparse.rollback_allocator : RollbackAllocator; 402 403 LexerConfig config; 404 RollbackAllocator rba; 405 406 enum name = chapter.to!string; 407 408 // Import the module 409 mixin("import fluid.showcase.", name, ";"); 410 alias mod = mixin("fluid.showcase.", name); 411 412 // Get the module filename 413 const sourceDirectory = thisExePath.dirName.buildPath("../examples"); 414 const filename = buildPath(sourceDirectory, name ~ ".d"); 415 416 // Load the file 417 auto sourceCode = readText(filename); 418 auto cache = StringCache(StringCache.defaultBucketCount); 419 auto tokens = getTokensForParser(sourceCode, config, &cache); 420 421 // Parse it 422 auto m = parseModule(tokens, filename, &rba); 423 auto visitor = new FunctionVisitor(sourceCode.splitLines); 424 visitor.visit(m); 425 426 // Begin creating the document 427 auto document = vspace(.layout!"fill"); 428 429 // Check each member 430 static foreach (member; __traits(allMembers, mod)) {{ 431 432 // Limit to memberrs that end with "Example" 433 // Note we cannot properly support overloads 434 static if (member.endsWith("Example")) { 435 436 alias memberSymbol = __traits(getMember, mod, member); 437 438 auto documentation = sizeLock!vspace(.layout!"center", .contentSize); 439 auto code = visitor.functions[member]; 440 auto theme = fluidDefaultTheme; 441 442 // Load documentation attributes 443 static foreach (uda; __traits(getAttributes, memberSymbol)) { 444 445 // Node 446 static if (is(typeof(uda()) : Node)) 447 documentation ~= uda(); 448 449 // Theme 450 else static if (is(typeof(uda()) : Theme)) 451 theme = uda(); 452 453 } 454 455 // Insert the documentation 456 document ~= documentation; 457 458 // Add and run a code example if it returns a node 459 static if (is(ReturnType!memberSymbol : Node)) 460 document ~= showcaseCode(code, memberSymbol(), theme); 461 462 // Otherwise, show just the code 463 else if (code != "") 464 document ~= showcaseCode(code); 465 466 } 467 468 }} 469 470 return document; 471 472 } 473 474 class FunctionVisitor : ASTVisitor { 475 476 int indentLevel; 477 478 /// Source code divided by lines. 479 string[] sourceLines; 480 481 /// Mapping of function names to their bodies. 482 string[string] functions; 483 484 this(string[] sourceLines) { 485 486 this.sourceLines = sourceLines; 487 488 } 489 490 alias visit = ASTVisitor.visit; 491 492 override void visit(const FunctionDeclaration decl) { 493 494 import std.array; 495 import std.range; 496 import std.string; 497 import dparse.lexer; 498 import dparse.formatter; 499 500 static struct Location { 501 size_t line; 502 size_t column; 503 504 this(T)(T t) { 505 this.line = t.line - 1; 506 this.column = t.column - 1; 507 } 508 } 509 510 // Get function boundaries 511 auto content = decl.functionBody.specifiedFunctionBody.blockStatement; 512 auto tokens = content.tokens; 513 514 // Convert to 0-indexing 515 auto start = Location(content.tokens[0]); 516 auto end = Location(content.tokens[$-1]); 517 auto rangeLines = sourceLines[start.line..end.line+1]; 518 519 // Extract the text from original source code to preserve original formatting and comments 520 auto output = rangeLines 521 .enumerate 522 .map!((value) { 523 524 auto i = value[0], line = value[1]; 525 526 // One line code 527 if (rangeLines.length == 1) return line[start.column+1..end.column]; 528 529 // First line, skip past "{" 530 if (i == 0) return line[start.column+1..$]; 531 532 // Middle line, write whole 533 else if (i+1 != rangeLines.length) return line; 534 535 // Last line, end before "}" 536 else return line[0..end.column]; 537 538 }) 539 .join("\n"); 540 541 // Save the result 542 functions[decl.name.text] = output[].outdent.strip; 543 decl.accept(this); 544 545 } 546 547 }