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 }