1 /// This tour 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 tour, 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 tour, use `dub run fluid:tour`, which should compile and run the tour. The program explains 9 /// different parts of the library and provides code examples, but you're free to browse through its files if you 10 /// like! introduction.d might be a good start. I hope this directory proves as a useful learning resource. 11 module fluid.tour; 12 13 import fluid; 14 import dparse.ast; 15 16 import std.string; 17 import std.traits; 18 import std.algorithm; 19 20 21 /// Maximum content width, used for code samples, since they require more space. 22 enum maxContentSize = .sizeLimitX(1000); 23 24 /// Reduced content width, used for document text. 25 enum contentSize = .sizeLimitX(800); 26 27 /// Sidebar width 28 enum sidebarSize = .sizeLimitX(220); 29 30 Theme mainTheme; 31 Theme exampleListTheme; 32 Theme codeTheme; 33 Theme previewWrapperTheme; 34 Theme highlightBoxTheme; 35 36 static this() { 37 38 import fluid.theme; 39 import std.file, std.path; 40 41 auto monospace = Style.loadTypeface(thisExePath.dirName.buildPath("../tour/ibm-plex-mono.ttf")); 42 43 mainTheme = Theme( 44 rule!Frame( 45 margin.sideX = 12, 46 margin.sideY = 16, 47 ), 48 rule!Label( 49 margin.sideX = 12, 50 margin.sideY = 7, 51 ), 52 rule!Button( 53 margin.sideX = 12, 54 margin.sideY = 7, 55 ), 56 rule!GridFrame(margin.sideY = 0), 57 rule!GridRow(margin = 0), 58 rule!ScrollFrame(margin = 0), 59 rule!PopupFrame( 60 padding.sideX = 2, 61 padding.sideY = 4, 62 ), 63 64 /// Code input 65 rule!CodeInput( 66 margin = 0, 67 typeface = monospace, 68 fontSize = 11.pt, 69 backgroundColor = color!"#dedede", 70 padding.sideX = 12, 71 padding.sideY = 16, 72 73 when!"a.isDisabled"( 74 backgroundColor = color!"#dedede", 75 ), 76 77 // TODO These colors are "borrowed" from Tree-sitter CLI, how about making our own? 78 when!`a.token.startsWith("keyword")` (textColor = color("#5f00d7")), 79 when!`a.token.startsWith("attribute")` (textColor = color("#af0000")), 80 when!`a.token.startsWith("property")` (textColor = color("#af0000")), 81 when!`a.token.startsWith("punctuation")`(textColor = color("#4e4e4e")), 82 when!`a.token.startsWith("type")` (textColor = color("#005f5f")), 83 when!`a.token.startsWith("operator")` (textColor = color("#50228a")), 84 when!`a.token.startsWith("comment")` (textColor = color("#8a8a8a")), 85 when!`a.token.startsWith("number")` (textColor = color("#875f00")), 86 when!`a.token.startsWith("string")` (textColor = color("#008700")), 87 when!`a.token.startsWith("constant")` (textColor = color("#875f00")), 88 when!`a.token.startsWith("variable")` (textColor = color("#875f00")), 89 when!`a.token.startsWith("function")` (textColor = color("#005fd7")), 90 when!`a.token.startsWith("module")` (textColor = color("#af8700")), 91 ), 92 93 // Heading 94 rule!(Label, Tags.heading)( 95 typeface = Style.defaultTypeface, 96 fontSize = 20.pt, 97 margin.sideTop = 20, 98 margin.sideBottom = 10, 99 ), 100 rule!(Label, Tags.subheading)( 101 typeface = Style.defaultTypeface, 102 fontSize = 16.pt, 103 margin.sideTop = 16, 104 margin.sideBottom = 8, 105 ), 106 ); 107 108 exampleListTheme = mainTheme.derive( 109 rule!Button( 110 padding.sideX = 8, 111 padding.sideY = 16, 112 margin = 2, 113 ), 114 ); 115 116 highlightBoxTheme = Theme( 117 rule!Node( 118 border = 1, 119 borderStyle = colorBorder(color!"#e62937"), 120 ), 121 ); 122 123 codeTheme = mainTheme.derive( 124 125 rule!Node( 126 typeface = monospace, 127 fontSize = 11.pt, 128 ), 129 rule!Frame( 130 padding = 0, 131 ), 132 rule!Label( 133 margin = 0, 134 backgroundColor = color!"#dedede", 135 padding.sideX = 12, 136 padding.sideY = 16, 137 ), 138 ); 139 140 previewWrapperTheme = mainTheme.derive( 141 rule!Frame( 142 margin = 0, 143 border = 1, 144 padding = 0, 145 borderStyle = colorBorder(color!"#dedede"), 146 ), 147 ); 148 149 } 150 151 enum Chapter { 152 @"Introduction" introduction, 153 @"Frames" frames, 154 @"Buttons & mutability" buttons, 155 @"Node slots" slots, 156 @"Themes" themes, 157 @"Margin, padding and border" margins, 158 @"Writing forms" forms, 159 @"moduleView" module_view, 160 // @"Popups" popups, 161 // @"Drag and drop" drag_and_drop, 162 } 163 164 @NodeTag 165 enum Tags { 166 heading, 167 subheading, 168 } 169 170 /// The entrypoint prepares themes and the window. 171 void main(string[] args) { 172 173 import raylib; 174 175 // Prepare the window 176 SetConfigFlags(ConfigFlags.FLAG_WINDOW_RESIZABLE); 177 SetTraceLogLevel(TraceLogLevel.LOG_WARNING); 178 InitWindow(1000, 750, "Fluid tour"); 179 SetTargetFPS(60); 180 SetExitKey(0); 181 scope (exit) CloseWindow(); 182 183 // Create the UI — pass the first argument to load a chapter under the given name 184 auto ui = args.length > 1 185 ? createUI(args[1]) 186 : createUI(); 187 188 // Event loop 189 while (!WindowShouldClose) { 190 191 BeginDrawing(); 192 scope (exit) EndDrawing(); 193 194 ClearBackground(color!"fff"); 195 196 // Fluid is by default configured to work with Raylib, so all you need to make them work together is a single 197 // call 198 ui.draw(); 199 200 } 201 202 } 203 204 Space createUI(string initialChapter = null) @safe { 205 206 import std.conv; 207 208 Chapter currentChapter; 209 Frame root; 210 ScrollFrame contentWrapper; 211 Space navigationBar; 212 Label titleLabel; 213 Button leftButton, rightButton; 214 215 auto content = nodeSlot!Space(.layout!(1, "fill")); 216 auto outlineContent = vspace(.layout!"fill"); 217 auto outline = vframe( 218 .layout!"fill", 219 button( 220 .layout!"fill", 221 "Top", 222 delegate { 223 contentWrapper.scrollStart(); 224 } 225 ), 226 outlineContent, 227 ); 228 auto sidebar = sizeLock!switchSlot( 229 .layout!(1, "end", "start"), 230 .sidebarSize, 231 outline, 232 null, 233 ); 234 235 void changeChapter(Chapter chapter) { 236 237 // Change the content root and show the back button 238 currentChapter = chapter; 239 content = render(chapter); 240 titleLabel.text = title(chapter); 241 242 // Show navigation 243 navigationBar.show(); 244 leftButton.isHidden = chapter == 0; 245 rightButton.isHidden = chapter == Chapter.max; 246 247 // Scroll back to top 248 contentWrapper.scrollStart(); 249 250 // Collect all headings and build the outline 251 content.queueAction(new BuildOutline(outlineContent)); 252 253 } 254 255 // All content is scrollable 256 root = onionFrame( 257 .layout!"fill", 258 259 // Content 260 contentWrapper = vscrollFrame( 261 .layout!"fill", 262 .mainTheme, 263 sizeLock!vspace( 264 .layout!(1, "center", "start"), 265 .maxContentSize, 266 267 // Navigation 268 navigationBar = sizeLock!hspace( 269 .layout!"center", 270 .contentSize, 271 272 // Back button 273 button("← Back to navigation", delegate { 274 content = exampleList(&changeChapter); 275 navigationBar.hide(); 276 leftButton.hide(); 277 rightButton.hide(); 278 outlineContent.children = []; 279 }), 280 sidebar.retry( 281 popupButton("Outline", outline), 282 ), 283 titleLabel = label(""), 284 ).hide(), 285 286 // Content 287 content = exampleList(&changeChapter), 288 289 sizeLock!hframe( 290 .layout!"center", 291 .contentSize, 292 293 // Left button 294 leftButton = button("Previous chapter", delegate { 295 changeChapter(to!Chapter(currentChapter-1)); 296 }).hide(), 297 298 // Right button 299 rightButton = button(.layout!(1, "end"), "Next chapter", delegate { 300 changeChapter(to!Chapter(currentChapter+1)); 301 }).hide(), 302 ), 303 ), 304 ), 305 306 // Add sidebar on the left 307 hspace( 308 .layout!"fill", 309 sidebar, 310 311 // Reserve space for content 312 sizeLock!vspace(.maxContentSize), 313 314 // Balance the sidebar to center the content 315 vspace(.layout!1), 316 ), 317 318 319 ); 320 321 if (initialChapter) { 322 changeChapter(to!Chapter(initialChapter)); 323 } 324 325 return root; 326 327 } 328 329 Space exampleList(void delegate(Chapter) @safe changeChapter) @safe { 330 331 import std.meta; 332 import std.array; 333 import std.range; 334 335 auto chapterGrid = gridFrame( 336 .layout!"fill", 337 .segments(3), 338 ); 339 340 // TODO This should be easier 341 auto rows = only(EnumMembers!Chapter) 342 343 // Create a button for each chapter 344 .map!(a => button( 345 .layout!"fill", 346 title(a), 347 delegate { changeChapter(a); } 348 )) 349 350 // Split them into chunks of three 351 .chunks(3); 352 353 foreach (row; rows) { 354 chapterGrid.addRow(row.array); 355 } 356 357 return sizeLock!vspace( 358 .layout!"center", 359 .exampleListTheme, 360 .contentSize, 361 label(.layout!"center", .tags!(Tags.heading), "Hello, World!"), 362 label("Pick a chapter of the tutorial to get started. Start with the first one or browse the chapters that " 363 ~ "interest you! Output previews are shown next to code samples to help you understand the content."), 364 label(.layout!"fill", .tags!(FluidTag.warning), "While this tutorial covers the most important parts of Fluid, " 365 ~ "it's still incomplete. Content will be added in further updates of Fluid. Contributions are welcome."), 366 chapterGrid, 367 ); 368 369 } 370 371 /// Create a code block 372 Space showcaseCode(string code) { 373 374 return vframe( 375 .layout!"fill", 376 .codeTheme, 377 sizeLock!label( 378 .layout!"center", 379 .contentSize, 380 code, 381 ), 382 ); 383 384 } 385 386 /// Showcase code and its result. 387 Space showcaseCode(string code, Node node, Theme theme = Theme.init) { 388 389 CodeInput editor; 390 391 // Make the node inherit the default theme rather than the one we set 392 if (!node.theme) { 393 node.theme = either(theme, fluidDefaultTheme); 394 } 395 396 // Reset code editor text. 397 void reset() { 398 399 editor.value = code; 400 401 } 402 403 scope (success) reset(); 404 405 return hframe( 406 .layout!"fill", 407 408 editor = codeInput( 409 .layout!(1, "fill"), 410 .codeTheme, 411 ), 412 vframe( 413 .layout!(1, "fill"), 414 .previewWrapperTheme, 415 nodeSlot!Node( 416 .layout!(1, "fill"), 417 node, 418 ), 419 ) 420 ); 421 422 } 423 424 /// Get the title of the given chapter. 425 string title(Chapter query) @safe { 426 427 import std.traits; 428 429 switch (query) { 430 431 static foreach (chapter; EnumMembers!Chapter) { 432 433 case chapter: 434 return getUDAs!(chapter, string)[0]; 435 436 } 437 438 default: return null; 439 440 } 441 442 } 443 444 /// Render the given chapter. 445 Space render(Chapter query) @safe { 446 447 switch (query) { 448 449 static foreach (chapter; EnumMembers!Chapter) { 450 451 case chapter: 452 return render!chapter; 453 454 } 455 456 default: return null; 457 458 } 459 460 } 461 462 /// ditto 463 Space render(Chapter chapter)() @trusted { 464 465 import std.file; 466 import std.path; 467 import std.conv; 468 import std.meta; 469 import std.traits; 470 import dparse.lexer; 471 import dparse.parser : parseModule; 472 import dparse.rollback_allocator : RollbackAllocator; 473 474 LexerConfig config; 475 RollbackAllocator rba; 476 477 enum name = chapter.to!string; 478 479 // Import the module 480 mixin("import fluid.tour.", name, ";"); 481 alias mod = mixin("fluid.tour.", name); 482 483 // Get the module filename 484 const sourceDirectory = thisExePath.dirName.buildPath("../tour"); 485 const filename = buildPath(sourceDirectory, name ~ ".d"); 486 487 // Use moduleView for rendering its module 488 if (chapter == Chapter.module_view) { 489 490 import std.path; 491 import fluid.theme; 492 import fluid.module_view; 493 494 auto compiler = DlangCompiler.findAny(); 495 496 return moduleViewFile( 497 .layout!"fill", 498 mainTheme.derive( 499 rule!Frame( 500 padding = 0, 501 margin = 0, 502 gap = 4, 503 ), 504 rule!(Frame, FluidTag.warning)( 505 warningRule, 506 margin.sideX = 12, 507 children!Label( 508 margin = 0, 509 ), 510 ), 511 rule!Button( 512 margin = 0, 513 ), 514 ), 515 compiler, 516 filename, 517 ); 518 519 } 520 521 // Load the file 522 auto sourceCode = readText(filename); 523 auto cache = StringCache(StringCache.defaultBucketCount); 524 auto tokens = getTokensForParser(sourceCode, config, &cache); 525 526 // Parse it 527 auto m = parseModule(tokens, filename, &rba); 528 auto visitor = new FunctionVisitor(sourceCode.splitLines); 529 visitor.visit(m); 530 531 // Begin creating the document 532 auto document = vspace(.layout!"fill"); 533 534 // Check each member 535 static foreach (member; __traits(allMembers, mod)) {{ 536 537 // Limit to memberrs that end with "Example" 538 // Note we cannot properly support overloads 539 static if (member.endsWith("Example")) { 540 541 alias memberSymbol = __traits(getMember, mod, member); 542 543 auto documentation = sizeLock!vspace(.layout!"center", .contentSize); 544 auto code = visitor.functions[member]; 545 auto theme = fluidDefaultTheme; 546 547 // Load documentation attributes 548 static foreach (uda; __traits(getAttributes, memberSymbol)) { 549 550 // Node 551 static if (is(typeof(uda()) : Node)) 552 documentation ~= uda(); 553 554 // Theme 555 else static if (is(typeof(uda()) : Theme)) 556 theme = uda(); 557 558 } 559 560 // Insert the documentation 561 document ~= documentation; 562 563 // Add and run a code example if it returns a node 564 static if (is(ReturnType!memberSymbol : Node)) 565 document ~= showcaseCode(code, memberSymbol(), theme); 566 567 // Otherwise, show just the code 568 else if (code != "") 569 document ~= showcaseCode(code); 570 571 } 572 573 }} 574 575 return document; 576 577 } 578 579 class FunctionVisitor : ASTVisitor { 580 581 int indentLevel; 582 583 /// Source code divided by lines. 584 string[] sourceLines; 585 586 /// Mapping of function names to their bodies. 587 string[string] functions; 588 589 this(string[] sourceLines) { 590 591 this.sourceLines = sourceLines; 592 593 } 594 595 alias visit = ASTVisitor.visit; 596 597 override void visit(const FunctionDeclaration decl) { 598 599 import std.array; 600 import std.range; 601 import std.string; 602 import dparse.lexer; 603 import dparse.formatter; 604 605 static struct Location { 606 size_t line; 607 size_t column; 608 609 this(T)(T t) { 610 this.line = t.line - 1; 611 this.column = t.column - 1; 612 } 613 } 614 615 // Get function boundaries 616 auto content = decl.functionBody.specifiedFunctionBody.blockStatement; 617 auto tokens = content.tokens; 618 619 // Convert to 0-indexing 620 auto start = Location(content.tokens[0]); 621 auto end = Location(content.tokens[$-1]); 622 auto rangeLines = sourceLines[start.line..end.line+1]; 623 624 // Extract the text from original source code to preserve original formatting and comments 625 auto output = rangeLines 626 .enumerate 627 .map!((value) { 628 629 auto i = value[0], line = value[1]; 630 631 // One line code 632 if (rangeLines.length == 1) return line[start.column+1..end.column]; 633 634 // First line, skip past "{" 635 if (i == 0) return line[start.column+1..$]; 636 637 // Middle line, write whole 638 else if (i+1 != rangeLines.length) return line; 639 640 // Last line, end before "}" 641 else return line[0..end.column]; 642 643 }) 644 .join("\n"); 645 646 // Save the result 647 functions[decl.name.text] = output[].outdent.strip; 648 decl.accept(this); 649 650 } 651 652 } 653 654 class BuildOutline : TreeAction { 655 656 Space outline; 657 Children children; 658 659 this(Space outline) @safe { 660 661 this.outline = outline; 662 outline.children = []; 663 664 } 665 666 override void beforeResize(Node node, Vector2) @safe { 667 668 auto headingTags = .tags!(Tags.heading, Tags.subheading); 669 const isHeading = !node.tags.intersect(headingTags).empty; 670 671 // Headings only 672 if (!isHeading) return; 673 674 // Add a button to the outline 675 if (auto label = cast(Label) node) { 676 677 children ~= button( 678 .layout!"fill", 679 label.text, 680 delegate { 681 label.scrollToTop(); 682 } 683 ); 684 685 } 686 687 } 688 689 override void afterTree() @safe { 690 691 super.afterTree(); 692 outline.children = children; 693 outline.updateSize(); 694 695 } 696 697 }