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 
11 @safe:
12 
13 
14 /// Progress bar node for communicating the program is actively working on something, and needs time to process. The
15 /// progress bar draws a styleable `ProgressBarFill` node inside, spanning a fraction of its content, usually starting
16 /// from left. Text is drawn over the node to display current progress.
17 ///
18 /// The progress bar will use text height for its own height, but it needs extra horizontal space to be functional. To
19 /// make sure it displays, ensure its horizontal alignment is always set to "fill" — this is the default, if the layout
20 /// is not changed.
21 ///
22 /// ## Styling
23 ///
24 /// The `progressBar` component is split into two nodes, `ProgressBar` and `ProgressBarFill`. When styling, the
25 /// former defines the background, greyed out part of the node, and the latter defines the foreground, the fill
26 /// that appears as progress is accumulated. Usually, the background for `ProgressBar` will have low saturation or
27 /// be grayed out, and `ProgressBarFill` will be colorful.
28 ///
29 /// Text currently uses the `ProgressBar` color, but it's possible it will blend the colors of both sides in the
30 /// future.
31 ///
32 /// ---
33 /// Theme(
34 ///     rule!ProgressBar(
35 ///         backgroundColor = color("#eee"),
36 ///         textColor = color("#000"),
37 ///     ),
38 ///     rule!ProgressBarFill(
39 ///         backgroundColor = color("#17b117"),
40 ///     )
41 /// )
42 /// ---
43 ///
44 /// ## Text format
45 ///
46 /// `ProgressBar` does not currently offer the possibility to change text format, but it can be accomplished by
47 /// subclassing and overriding the `buildText` method, like so:
48 ///
49 /// ---
50 /// class MyProgressBar : ProgressBar {
51 ///
52 ///     override string buildText() const {
53 ///
54 ///         return format!"%s/%s"(value, maxValue);
55 ///
56 ///     }
57 ///
58 /// }
59 /// ---
60 ///
61 /// Importantly, should `buildText` return an empty string, the progress bar will disappear, since its size depends on
62 /// the text itself. If text is not desired, one can set `textColor` to a transparent value like `color("#0000")`.
63 /// Alternatively `resizeImpl` can also be overrided to change the sizing behavior.
64 alias progressBar = simpleConstructor!ProgressBar;
65 
66 /// ditto
67 class ProgressBar : Node {
68 
69     public {
70 
71         /// `value`, along with `maxValue` indicate the current progress, defined as the fraction of `value` over
72         /// `maxValue`. If 0, the progress bar is empty. If equal to `maxValue`, the progress bar is full.
73         int value;
74 
75         /// ditto.
76         int maxValue;
77 
78         /// Text used by the node.
79         Text text;
80 
81         /// Node used as the filling for this progress bar.
82         ProgressBarFill fill;
83 
84     }
85 
86     /// Set the `value` and `maxValue` of the progressBar.
87     ///
88     /// If initialized with no arguments, the progress bar starts empty, with `maxValue` set to 100.
89     ///
90     /// Params:
91     ///     value = Current value. Defaults to 0, making the progress bar empty.
92     ///     maxValue = Maximum value for the progress bar.
93     this(int value, int maxValue) {
94 
95         this.layout = .layout!("fill", "start");
96         this.value = value;
97         this.maxValue = maxValue;
98         this.fill = new ProgressBarFill(this);
99         this.text = Text(this, "");
100 
101     }
102 
103     /// ditto
104     this(int maxValue = 100) {
105 
106         this(0, maxValue);
107 
108     }
109 
110     override void resizeImpl(Vector2 space) {
111 
112         text = buildText();
113         text.resize();
114         fill.resize(tree, theme, space);
115         minSize = text.size;
116 
117     }
118 
119     override void drawImpl(Rectangle paddingBox, Rectangle contentBox) {
120 
121         auto style = pickStyle();
122         style.drawBackground(io, paddingBox);
123 
124         // Draw the filling
125         fill.draw(contentBox);
126 
127         // Draw the text
128         const textPosition = center(contentBox) - text.size / 2;
129         text.draw(style, textPosition);
130 
131     }
132 
133     /// Get text that displays on top of the progress bar.
134     ///
135     /// This function can be overrided to adjust the text and its formatting, or to remove the text completely. Keep in
136     /// mind that since `ProgressBar` uses the text as reference for its own size, if the text is removed, the progress
137     /// bar will disappear — `minSize` has to be adjusted accordingly.
138     string buildText() const {
139 
140         import std.format : format;
141 
142         const int percentage = 100 * value / maxValue;
143 
144         return format!"%s%%"(percentage);
145 
146     }
147 
148 }
149 
150 ///
151 unittest {
152 
153     const steps = 24;
154 
155     // Create a progress bar.
156     auto bar = progressBar(steps);
157 
158     // Keep the user updated on the progress.
159     foreach (i; 0 .. steps) {
160 
161         bar.value = i;
162         bar.updateSize();
163 
164     }
165 
166 }
167 
168 unittest {
169 
170     import fluid.theme;
171     import fluid.default_theme;
172 
173     const steps = 24;
174 
175     auto io = new HeadlessBackend;
176     auto theme = nullTheme.derive(
177         rule!ProgressBar(
178             backgroundColor = color("#eee"),
179             textColor = color("#000"),
180         ),
181         rule!ProgressBarFill(
182             backgroundColor = color("#17b117"),
183         )
184     );
185     auto bar = progressBar(theme, steps);
186 
187     bar.io = io;
188     bar.draw();
189 
190     assert(bar.text == "0%");
191     io.assertRectangle(Rectangle(0, 0, 800, 27), color("#eee"));
192     io.assertRectangle(Rectangle(0, 0, 0, 27), color("#17b117"));
193     io.assertTexture(bar.text.texture.chunks[0].texture, Vector2(387, 0), color("#fff"));
194 
195     io.nextFrame;
196     bar.value = 2;
197     bar.updateSize();
198     bar.draw();
199 
200     assert(bar.text == "8%");
201     io.assertRectangle(Rectangle(0, 0, 800, 27), color("#eee"));
202     io.assertRectangle(Rectangle(0, 0, 66.66, 27), color("#17b117"));
203     io.assertTexture(bar.text.texture.chunks[0].texture, Vector2(387.5, 0), color("#fff"));
204 
205     io.nextFrame;
206     bar.value = steps;
207     bar.updateSize();
208     bar.draw();
209 
210     assert(bar.text == "100%");
211     io.assertRectangle(Rectangle(0, 0, 800, 27), color("#eee"));
212     io.assertRectangle(Rectangle(0, 0, 800, 27), color("#17b117"));
213     io.assertTexture(bar.text.texture.chunks[0].texture, Vector2(377, 0), color("#fff"));
214 
215 }
216 
217 unittest {
218 
219     import fluid.style;
220     import fluid.theme;
221 
222     auto io = new HeadlessBackend;
223     auto theme = nullTheme.derive(
224         rule!ProgressBar(
225             backgroundColor = color("#eee"),
226         ),
227         rule!ProgressBarFill(
228             backgroundColor = color("#17b117"),
229         )
230     );
231     auto bar = new class ProgressBar {
232 
233         override void resizeImpl(Vector2 space) {
234 
235             super.resizeImpl(space);
236             minSize = Vector2(0, 4);
237 
238         }
239 
240         override string buildText() const {
241 
242             return "";
243 
244         }
245 
246     };
247 
248     bar.io = io;
249     bar.theme = theme;
250     bar.maxValue = 20;
251     bar.draw();
252 
253     assert(bar.text == "");
254     io.assertRectangle(Rectangle(0, 0, 800, 4), color("#eee"));
255     io.assertRectangle(Rectangle(0, 0, 0, 4), color("#17b117"));
256 
257     io.nextFrame;
258     bar.value = 2;
259     bar.updateSize();
260     bar.draw();
261 
262     assert(bar.text == "");
263     io.assertRectangle(Rectangle(0, 0, 800, 4), color("#eee"));
264     io.assertRectangle(Rectangle(0, 0, 80, 4), color("#17b117"));
265 
266 }
267 
268 /// Content for the progress bar. Used for styling. See `ProgressBar` for usage instructions.
269 class ProgressBarFill : Node {
270 
271     public {
272 
273         /// Progress bar the node belongs to.
274         ProgressBar bar;
275 
276     }
277 
278     this(ProgressBar bar) {
279 
280         this.layout = .layout!"fill";
281         this.bar = bar;
282 
283     }
284 
285     override void resizeImpl(Vector2 space) {
286 
287         minSize = Vector2(0, 0);
288 
289     }
290 
291     override void drawImpl(Rectangle paddingBox, Rectangle contentBox) {
292 
293         // Use a fraction of the padding box corresponding to the fill value
294         paddingBox.width *= cast(float) bar.value / bar.maxValue;
295 
296         auto style = pickStyle();
297         style.drawBackground(io, paddingBox);
298 
299     }
300 
301 }