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 }