1 ///
2 module fluid.progress_bar;
3 
4 import fluid.text;
5 import fluid.node;
6 import fluid.utils;
7 import fluid.backend;
8 import fluid.structs;
9 
10 import fluid.io.canvas;
11 
12 @safe:
13 
14 
15 /// Progress bar node for communicating the program is actively working on something, and needs time to process. The
16 /// progress bar draws a styleable `ProgressBarFill` node inside, spanning a fraction of its content, usually starting
17 /// from left. Text is drawn over the node to display current progress.
18 ///
19 /// The progress bar will use text height for its own height, but it needs extra horizontal space to be functional. To
20 /// make sure it displays, ensure its horizontal alignment is always set to "fill" — this is the default, if the layout
21 /// is not changed.
22 ///
23 /// ## Styling
24 ///
25 /// The `progressBar` component is split into two nodes, `ProgressBar` and `ProgressBarFill`. When styling, the
26 /// former defines the background, greyed out part of the node, and the latter defines the foreground, the fill
27 /// that appears as progress is accumulated. Usually, the background for `ProgressBar` will have low saturation or
28 /// be grayed out, and `ProgressBarFill` will be colorful.
29 ///
30 /// Text currently uses the `ProgressBar` color, but it's possible it will blend the colors of both sides in the
31 /// future.
32 ///
33 /// ---
34 /// Theme(
35 ///     rule!ProgressBar(
36 ///         backgroundColor = color("#eee"),
37 ///         textColor = color("#000"),
38 ///     ),
39 ///     rule!ProgressBarFill(
40 ///         backgroundColor = color("#17b117"),
41 ///     )
42 /// )
43 /// ---
44 ///
45 /// ## Text format
46 ///
47 /// `ProgressBar` does not currently offer the possibility to change text format, but it can be accomplished by
48 /// subclassing and overriding the `buildText` method, like so:
49 ///
50 /// ---
51 /// class MyProgressBar : ProgressBar {
52 ///
53 ///     override string buildText() const {
54 ///
55 ///         return format!"%s/%s"(value, maxValue);
56 ///
57 ///     }
58 ///
59 /// }
60 /// ---
61 ///
62 /// Importantly, should `buildText` return an empty string, the progress bar will disappear, since its size depends on
63 /// the text itself. If text is not desired, one can set `textColor` to a transparent value like `color("#0000")`.
64 /// Alternatively `resizeImpl` can also be overrided to change the sizing behavior.
65 alias progressBar = simpleConstructor!ProgressBar;
66 
67 /// ditto
68 class ProgressBar : Node {
69 
70     CanvasIO canvasIO;
71 
72     public {
73 
74         /// `value`, along with `maxValue` indicate the current progress, defined as the fraction of `value` over
75         /// `maxValue`. If 0, the progress bar is empty. If equal to `maxValue`, the progress bar is full.
76         int value;
77 
78         /// ditto.
79         int maxValue;
80 
81         /// Text used by the node.
82         Text text;
83 
84         /// Node used as the filling for this progress bar.
85         ProgressBarFill fill;
86 
87     }
88 
89     /// Set the `value` and `maxValue` of the progressBar.
90     ///
91     /// If initialized with no arguments, the progress bar starts empty, with `maxValue` set to 100.
92     ///
93     /// Params:
94     ///     value = Current value. Defaults to 0, making the progress bar empty.
95     ///     maxValue = Maximum value for the progress bar.
96     this(int value, int maxValue) {
97 
98         this.layout = .layout!("fill", "start");
99         this.value = value;
100         this.maxValue = maxValue;
101         this.fill = new ProgressBarFill(this);
102         this.text = Text(this, "");
103 
104     }
105 
106     /// ditto
107     this(int maxValue = 100) {
108 
109         this(0, maxValue);
110 
111     }
112 
113     override void resizeImpl(Vector2 space) {
114 
115         use(canvasIO);
116 
117         text = buildText();
118         text.resize(canvasIO);
119         resizeChild(fill, space);
120         minSize = text.size;
121 
122     }
123 
124     override void drawImpl(Rectangle paddingBox, Rectangle contentBox) {
125 
126         auto style = pickStyle();
127         style.drawBackground(io, canvasIO, paddingBox);
128 
129         // Draw the filling
130         drawChild(fill, contentBox);
131 
132         // Draw the text
133         const textPosition = center(contentBox) - text.size / 2;
134 
135         text.draw(canvasIO, style, textPosition);
136 
137     }
138 
139     /// Get text that displays on top of the progress bar.
140     ///
141     /// This function ;can be overrided to adjust the text and its formatting, or to remove the text completely. Keep in
142     /// mind that since `ProgressBar` uses the text as reference for its own size, if the text is removed, the progress
143     /// bar will disappear — `minSize` has to be adjusted accordingly.
144     string buildText() const {
145 
146         import std.format : format;
147 
148         const int percentage = 100 * value / maxValue;
149 
150         return format!"%s%%"(percentage);
151 
152     }
153 
154 }
155 
156 ///
157 unittest {
158 
159     const steps = 24;
160 
161     // Create a progress bar.
162     auto bar = progressBar(steps);
163 
164     // Keep the user updated on the progress.
165     foreach (i; 0 .. steps) {
166 
167         bar.value = i;
168         bar.updateSize();
169 
170     }
171 
172 }
173 
174 /// Content for the progress bar. Used for styling. See `ProgressBar` for usage instructions.
175 class ProgressBarFill : Node {
176 
177     CanvasIO canvasIO;
178 
179     public {
180 
181         /// Progress bar the node belongs to.
182         ProgressBar bar;
183 
184     }
185 
186     this(ProgressBar bar) {
187 
188         this.layout = .layout!"fill";
189         this.bar = bar;
190 
191     }
192 
193     override void resizeImpl(Vector2 space) {
194 
195         use(canvasIO);
196         minSize = Vector2(0, 0);
197 
198     }
199 
200     override void drawImpl(Rectangle paddingBox, Rectangle contentBox) {
201 
202         // Use a fraction of the padding box corresponding to the fill value
203         paddingBox.width *= cast(float) bar.value / bar.maxValue;
204 
205         auto style = pickStyle();
206         style.drawBackground(io, canvasIO, paddingBox);
207 
208     }
209 
210 }