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 }