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     version (FluidTour_NewIO)
179         InitWindow(1000, 750, "Fluid tour (New I/O)");
180     else
181         InitWindow(1000, 750, "Fluid tour");
182     SetTargetFPS(60);
183     SetExitKey(0);
184     scope (exit) CloseWindow();
185 
186     // Create the UI — pass the first argument to load a chapter under the given name
187     auto ui = args.length > 1
188         ? createUI(args[1])
189         : createUI();
190 
191     // If new I/O is toggled on, disable the default backend
192     // and wrap the UI in the raylibStack root
193     version (FluidTour_NewIO) {
194         auto root = raylibStack.v5_5(ui);
195         root.io = new HeadlessBackend;
196     }
197     else {
198         auto root = ui;
199     }
200 
201     // Event loop
202     while (!WindowShouldClose) {
203 
204         BeginDrawing();
205         scope (exit) EndDrawing();
206 
207         ClearBackground(color!"fff");
208 
209         // Fluid is by default configured to work with Raylib, so all you need to make them work together is a single
210         // call
211         root.draw();
212 
213     }
214 
215 }
216 
217 Space createUI(string initialChapter = null) @safe {
218 
219     import std.conv;
220 
221     Chapter currentChapter;
222     Frame root;
223     ScrollFrame contentWrapper;
224     Space navigationBar;
225     Label titleLabel;
226     Button leftButton, rightButton;
227 
228     auto content = nodeSlot!Space(.layout!(1, "fill"));
229     auto outlineContent = vspace(.layout!"fill");
230     auto outline = vframe(
231         .layout!"fill",
232         button(
233             .layout!"fill",
234             "Top",
235             delegate {
236                 contentWrapper.scrollStart();
237             }
238         ),
239         outlineContent,
240     );
241     auto sidebar = sizeLock!switchSlot(
242         .layout!(1, "end", "start"),
243         .sidebarSize,
244         outline,
245         null,
246     );
247 
248     void changeChapter(Chapter chapter) {
249 
250         // Change the content root and show the back button
251         currentChapter = chapter;
252         content = render(chapter);
253         titleLabel.text = title(chapter);
254 
255         // Show navigation
256         navigationBar.show();
257         leftButton.isHidden = chapter == 0;
258         rightButton.isHidden = chapter == Chapter.max;
259 
260         // Scroll back to top
261         contentWrapper.scrollStart();
262 
263         // Collect all headings and build the outline
264         content.queueAction(new BuildOutline(outlineContent));
265 
266     }
267 
268     // All content is scrollable
269     root = onionFrame(
270         .layout!"fill",
271 
272         // Content
273         contentWrapper = vscrollFrame(
274             .layout!"fill",
275             .mainTheme,
276             sizeLock!vspace(
277                 .layout!(1, "center", "start"),
278                 .maxContentSize,
279 
280                 // Navigation
281                 navigationBar = sizeLock!hspace(
282                     .layout!"center",
283                     .contentSize,
284 
285                     // Back button
286                     button("← Back to navigation", delegate {
287                         content = exampleList(&changeChapter);
288                         navigationBar.hide();
289                         leftButton.hide();
290                         rightButton.hide();
291                         outlineContent.children = [];
292                     }),
293                     sidebar.retry(
294                         popupButton("Outline", outline),
295                     ),
296                     titleLabel = label(""),
297                 ).hide(),
298 
299                 // Content
300                 content = exampleList(&changeChapter),
301 
302                 sizeLock!hframe(
303                     .layout!"center",
304                     .contentSize,
305 
306                     // Left button
307                     leftButton = button("Previous chapter", delegate {
308                         changeChapter(to!Chapter(currentChapter-1));
309                     }).hide(),
310 
311                     // Right button
312                     rightButton = button(.layout!(1, "end"), "Next chapter", delegate {
313                         changeChapter(to!Chapter(currentChapter+1));
314                     }).hide(),
315                 ),
316             ),
317         ),
318 
319         // Add sidebar on the left
320         hspace(
321             .layout!"fill",
322             sidebar,
323 
324             // Reserve space for content
325             sizeLock!vspace(.maxContentSize),
326 
327             // Balance the sidebar to center the content
328             vspace(.layout!1),
329         ),
330 
331 
332     );
333 
334     if (initialChapter) {
335         changeChapter(to!Chapter(initialChapter));
336     }
337 
338     return root;
339 
340 }
341 
342 Space exampleList(void delegate(Chapter) @safe changeChapter) @safe {
343 
344     import std.meta;
345     import std.array;
346     import std.range;
347 
348     auto chapterGrid = gridFrame(
349         .layout!"fill",
350         .segments(3),
351     );
352 
353     // TODO This should be easier
354     auto rows = only(EnumMembers!Chapter)
355 
356         // Create a button for each chapter
357         .map!(a => button(
358             .layout!"fill",
359             title(a),
360             delegate { changeChapter(a); }
361         ))
362 
363         // Split them into chunks of three
364         .chunks(3);
365 
366     foreach (row; rows) {
367         chapterGrid.addRow(row.array);
368     }
369 
370     return sizeLock!vspace(
371         .layout!"center",
372         .exampleListTheme,
373         .contentSize,
374         label(.layout!"center", .tags!(Tags.heading), "Hello, World!"),
375         label("Pick a chapter of the tutorial to get started. Start with the first one or browse the chapters that "
376             ~ "interest you! Output previews are shown next to code samples to help you understand the content."),
377         label(.layout!"fill", .tags!(FluidTag.warning), "While this tutorial covers the most important parts of Fluid, "
378             ~ "it's still incomplete. Content will be added in further updates of Fluid. Contributions are welcome."),
379         chapterGrid,
380         hseparator(),
381         imageView(.layout!"center", "logo.png", Vector2(249.5, 120)),
382     );
383 
384 }
385 
386 /// Create a code block
387 Space showcaseCode(string code) {
388 
389     return vframe(
390         .layout!"fill",
391         .codeTheme,
392         sizeLock!label(
393             .layout!"center",
394             .contentSize,
395             code,
396         ),
397     );
398 
399 }
400 
401 /// Showcase code and its result.
402 Space showcaseCode(string code, Node node, Theme theme = Theme.init) {
403 
404     CodeInput editor;
405 
406     // Make the node inherit the default theme rather than the one we set
407     if (!node.theme) {
408         node.theme = either(theme, fluidDefaultTheme);
409     }
410 
411     // Reset code editor text.
412     void reset() {
413 
414         editor.value = code;
415 
416     }
417 
418     scope (success) reset();
419 
420     return hframe(
421         .layout!"fill",
422 
423         editor = codeInput(
424             .layout!(1, "fill"),
425             .codeTheme,
426         ),
427         vframe(
428             .layout!(1, "fill"),
429             .previewWrapperTheme,
430             nodeSlot!Node(
431                 .layout!(1, "fill"),
432                 node,
433             ),
434         )
435     );
436 
437 }
438 
439 /// Get the title of the given chapter.
440 string title(Chapter query) @safe {
441 
442     import std.traits;
443 
444     switch (query) {
445 
446         static foreach (chapter; EnumMembers!Chapter) {
447 
448             case chapter:
449                 return getUDAs!(chapter, string)[0];
450 
451         }
452 
453         default: return null;
454 
455     }
456 
457 }
458 
459 /// Render the given chapter.
460 Space render(Chapter query) @safe {
461 
462     switch (query) {
463 
464         static foreach (chapter; EnumMembers!Chapter) {
465 
466             case chapter:
467                 return render!chapter;
468 
469         }
470 
471         default: return null;
472 
473     }
474 
475 }
476 
477 /// ditto
478 Space render(Chapter chapter)() @trusted {
479 
480     import std.file;
481     import std.path;
482     import std.conv;
483     import std.meta;
484     import std.traits;
485     import dparse.lexer;
486     import dparse.parser : parseModule;
487     import dparse.rollback_allocator : RollbackAllocator;
488 
489     LexerConfig config;
490     RollbackAllocator rba;
491 
492     enum name = chapter.to!string;
493 
494     // Import the module
495     mixin("import fluid.tour.", name, ";");
496     alias mod = mixin("fluid.tour.", name);
497 
498     // Get the module filename
499     const sourceDirectory = thisExePath.dirName.buildPath("../tour");
500     const filename = buildPath(sourceDirectory, name ~ ".d");
501 
502     // Use moduleView for rendering its module
503     if (chapter == Chapter.module_view) {
504 
505         import std.path;
506         import fluid.theme;
507         import fluid.module_view;
508 
509         auto compiler = DlangCompiler.findAny();
510 
511         return moduleViewFile(
512             .layout!"fill",
513             mainTheme.derive(
514                 rule!Frame(
515                     padding = 0,
516                     margin = 0,
517                     gap = 4,
518                 ),
519                 rule!(Frame, FluidTag.warning)(
520                     warningRule,
521                     margin.sideX = 12,
522                     children!Label(
523                         margin = 0,
524                     ),
525                 ),
526                 rule!Button(
527                     margin = 0,
528                 ),
529             ),
530             compiler,
531             filename,
532         );
533 
534     }
535 
536     // Load the file
537     auto sourceCode = readText(filename);
538     auto cache = StringCache(StringCache.defaultBucketCount);
539     auto tokens = getTokensForParser(sourceCode, config, &cache);
540 
541     // Parse it
542     auto m = parseModule(tokens, filename, &rba);
543     auto visitor = new FunctionVisitor(sourceCode.splitLines);
544     visitor.visit(m);
545 
546     // Begin creating the document
547     auto document = vspace(.layout!"fill");
548 
549     // Check each member
550     static foreach (member; __traits(allMembers, mod)) {{
551 
552         // Limit to memberrs that end with "Example"
553         // Note we cannot properly support overloads
554         static if (member.endsWith("Example")) {
555 
556             alias memberSymbol = __traits(getMember, mod, member);
557 
558             auto documentation = sizeLock!vspace(.layout!"center", .contentSize);
559             auto code = visitor.functions[member];
560             auto theme = fluidDefaultTheme;
561 
562             // Load documentation attributes
563             static foreach (uda; __traits(getAttributes, memberSymbol)) {
564 
565                 // Node
566                 static if (is(typeof(uda()) : Node))
567                     documentation ~= uda();
568 
569                 // Theme
570                 else static if (is(typeof(uda()) : Theme))
571                     theme = uda();
572 
573             }
574 
575             // Insert the documentation
576             document ~= documentation;
577 
578             // Add and run a code example if it returns a node
579             static if (is(ReturnType!memberSymbol : Node))
580                 document ~= showcaseCode(code, memberSymbol(), theme);
581 
582             // Otherwise, show just the code
583             else if (code != "")
584                 document ~= showcaseCode(code);
585 
586         }
587 
588     }}
589 
590     return document;
591 
592 }
593 
594 class FunctionVisitor : ASTVisitor {
595 
596     int indentLevel;
597 
598     /// Source code divided by lines.
599     string[] sourceLines;
600 
601     /// Mapping of function names to their bodies.
602     string[string] functions;
603 
604     this(string[] sourceLines) {
605 
606         this.sourceLines = sourceLines;
607 
608     }
609 
610     alias visit = ASTVisitor.visit;
611 
612     override void visit(const FunctionDeclaration decl) {
613 
614         import std.array;
615         import std.range;
616         import std.string;
617         import dparse.lexer;
618         import dparse.formatter;
619 
620         static struct Location {
621             size_t line;
622             size_t column;
623 
624             this(T)(T t) {
625                 this.line = t.line - 1;
626                 this.column = t.column - 1;
627             }
628         }
629 
630         // Get function boundaries
631         auto content = decl.functionBody.specifiedFunctionBody.blockStatement;
632         auto tokens = content.tokens;
633 
634         // Convert to 0-indexing
635         auto start = Location(content.tokens[0]);
636         auto end = Location(content.tokens[$-1]);
637         auto rangeLines = sourceLines[start.line..end.line+1];
638 
639         // Extract the text from original source code to preserve original formatting and comments
640         auto output = rangeLines
641             .enumerate
642             .map!((value) {
643 
644                 auto i = value[0], line = value[1];
645 
646                 // One line code
647                 if (rangeLines.length == 1) return line[start.column+1..end.column];
648 
649                 // First line, skip past "{"
650                 if (i == 0) return line[start.column+1..$];
651 
652                 // Middle line, write whole
653                 else if (i+1 != rangeLines.length) return line;
654 
655                 // Last line, end before "}"
656                 else return line[0..end.column];
657 
658             })
659             .join("\n");
660 
661         // Save the result
662         functions[decl.name.text] = output[].outdent.strip;
663         decl.accept(this);
664 
665     }
666 
667 }
668 
669 class BuildOutline : TreeAction {
670 
671     Space outline;
672     Children children;
673 
674     this(Space outline) @safe {
675 
676         this.outline = outline;
677         outline.children = [];
678 
679     }
680 
681     override void beforeResize(Node node, Vector2) @safe {
682 
683         auto headingTags = .tags!(Tags.heading, Tags.subheading);
684         const isHeading = !node.tags.intersect(headingTags).empty;
685 
686         // Headings only
687         if (!isHeading) return;
688 
689         // Add a button to the outline
690         if (auto label = cast(Label) node) {
691 
692             children ~= button(
693                 .layout!"fill",
694                 label.text,
695                 delegate {
696                     label.scrollToTop();
697                 }
698             );
699 
700         }
701 
702     }
703 
704     override void afterTree() @safe {
705 
706         super.afterTree();
707         outline.children = children;
708         outline.updateSize();
709 
710     }
711 
712 }