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