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 }