1 /// 2 module fluid.space; 3 4 import std.math; 5 import std.range; 6 import std.string; 7 import std.traits; 8 import std.algorithm; 9 10 import fluid.node; 11 import fluid.style; 12 import fluid.utils; 13 import fluid.backend; 14 import fluid.children; 15 16 17 @safe: 18 19 20 /// This is a space, a basic container for other nodes. 21 /// 22 /// Nodes are laid in a column (`vframe`) or in a row (`hframe`). 23 /// 24 /// Space only acts as a container and doesn't implement styles and doesn't take focus. It's very useful as a helper for 25 /// building layout, while `Frame` remains to provide styling. 26 alias vspace = simpleConstructor!Space; 27 28 /// ditto 29 alias hspace = simpleConstructor!(Space, (a) { 30 31 a.directionHorizontal = true; 32 33 }); 34 35 /// ditto 36 class Space : Node { 37 38 public { 39 40 /// Children of this frame. 41 Children children; 42 43 /// Defines in what directions children of this frame should be placed. 44 /// 45 /// If true, children are placed horizontally, if false, vertically. 46 bool isHorizontal; 47 48 alias horizontal = isHorizontal; 49 alias directionHorizontal = horizontal; 50 51 } 52 53 private { 54 55 /// Denominator for content sizing. 56 uint denominator; 57 58 /// Space reserved for shrinking elements. 59 float reservedSpace; 60 61 } 62 63 /// Create the space and fill it with given nodes. 64 this(Node[] nodes...) { 65 66 this.children ~= nodes; 67 68 } 69 70 /// Create the space using nodes from the given range. 71 this(Range)(Range range) 72 if (isInputRange!Range) 73 do { 74 75 this.children ~= range; 76 77 } 78 79 /// Add children. 80 pragma(inline, true) 81 void opOpAssign(string operator : "~", T)(T nodes) { 82 83 children ~= nodes; 84 85 } 86 87 protected override void resizeImpl(Vector2 available) { 88 89 import std.algorithm : max, map, fold; 90 91 // Now that we're recalculating the layout, we can remove the dirty flag from children 92 children.clearDirty; 93 94 // Reset size 95 minSize = Vector2(0, 0); 96 reservedSpace = 0; 97 denominator = 0; 98 99 // Ignore the rest if there's no children 100 if (!children.length) return; 101 102 Vector2 maxExpandSize; 103 104 // Collect expanding children in a separate array 105 Node[] expandChildren; 106 size_t visibleChildren; 107 foreach (child; children) { 108 109 visibleChildren += !child.isHidden; 110 111 // This node expands and isn't hidden 112 if (child.layout.expand && !child.isHidden) { 113 114 // Make it happen later 115 expandChildren ~= child; 116 117 // Add to the denominator 118 denominator += child.layout.expand; 119 120 } 121 122 // Check non-expand nodes now 123 else { 124 125 resizeChild(child, childSpace(child, available, false)); 126 minSize = addSize(child.minSize, minSize); 127 128 // Reserve space for this node 129 reservedSpace += directionHorizontal 130 ? child.minSize.x 131 : child.minSize.y; 132 133 } 134 135 } 136 137 const gapSpace 138 = visibleChildren == 0 ? 0 139 : isHorizontal ? style.gap.sideX * (visibleChildren - 1u) 140 : style.gap.sideY * (visibleChildren - 1u); 141 142 // Reserve space for gaps 143 reservedSpace += gapSpace; 144 145 if (isHorizontal) 146 minSize.x += gapSpace; 147 else 148 minSize.y += gapSpace; 149 150 // Calculate the size of expanding children last 151 foreach (child; expandChildren) { 152 153 // Resize the child 154 resizeChild(child, childSpace(child, available, false)); 155 156 const childSize = child.minSize; 157 const childExpand = child.layout.expand; 158 159 const segmentSize = horizontal 160 ? Vector2(childSize.x / childExpand, childSize.y) 161 : Vector2(childSize.x, childSize.y / childExpand); 162 163 // Reserve expand space 164 maxExpandSize.x = max(maxExpandSize.x, segmentSize.x); 165 maxExpandSize.y = max(maxExpandSize.y, segmentSize.y); 166 167 } 168 169 const expandSize = horizontal 170 ? Vector2(maxExpandSize.x * denominator, maxExpandSize.y) 171 : Vector2(maxExpandSize.x, maxExpandSize.y * denominator); 172 173 // Add the expand space 174 minSize = addSize(expandSize, minSize); 175 176 } 177 178 protected override void drawImpl(Rectangle, Rectangle area) { 179 180 assertClean(children, "Children were changed without calling updateSize()."); 181 182 auto position = start(area); 183 184 foreach (child; filterChildren) { 185 186 // Ignore if this child is not visible 187 if (child.isHidden) continue; 188 189 // Get params 190 const size = childSpace(child, size(area), true); 191 const rect = Rectangle( 192 position.x, position.y, 193 size.x, size.y 194 ); 195 196 // Draw the child 197 drawChild(child, rect); 198 199 // Offset position 200 position = childOffset(position, size); 201 202 } 203 204 } 205 206 /// List children in the space, removing all nodes queued for deletion beforehand. 207 protected auto filterChildren() { 208 209 struct ChildIterator { 210 211 Space node; 212 213 int opApply(int delegate(Node) @safe fun) @trusted { 214 215 foreach (_, node; this) { 216 217 if (auto result = fun(node)) { 218 return result; 219 } 220 221 } 222 return 0; 223 224 } 225 226 int opApply(int delegate(size_t index, Node) @safe fun) @trusted { 227 228 node.children.lock(); 229 scope (exit) node.children.unlock(); 230 231 size_t destinationIndex = 0; 232 int end = 0; 233 234 // Iterate through all children. When we come upon ones that are queued for deletion, 235 foreach (sourceIndex, child; node.children) { 236 237 const toRemove = child.toRemove; 238 child.toRemove = false; 239 240 // Ignore children that are to be removed 241 if (toRemove) continue; 242 243 // Yield the child 244 if (!end) 245 end = fun(destinationIndex, child); 246 247 // Move the child if needed 248 if (sourceIndex != destinationIndex) { 249 250 node.children.forceMutable[destinationIndex] = child; 251 252 } 253 254 // Stop iteration if requested — and if there's nothing to move 255 else if (end) return end; 256 257 // Set space for next nodes 258 destinationIndex++; 259 260 261 } 262 263 // Adjust length 264 node.children.forceMutable.length = destinationIndex; 265 266 return 0; 267 268 } 269 270 } 271 272 return ChildIterator(this); 273 274 } 275 276 /// Space does not take hover; isHovered is always false. 277 protected override bool hoveredImpl(Rectangle, Vector2) { 278 279 return false; 280 281 } 282 283 /// Params: 284 /// child = Child size to add. 285 /// previous = Previous position. 286 private Vector2 addSize(Vector2 child, Vector2 previous) const { 287 288 import std.algorithm : max; 289 290 // Horizontal 291 if (directionHorizontal) { 292 293 return Vector2( 294 previous.x + child.x, 295 max(minSize.y, child.y), 296 ); 297 298 } 299 300 // Vertical 301 else return Vector2( 302 max(minSize.x, child.x), 303 previous.y + child.y, 304 ); 305 306 } 307 308 /// Calculate the offset for the next node, given the `childSpace` result for its previous sibling. 309 protected Vector2 childOffset(Vector2 currentOffset, Vector2 childSpace) { 310 311 if (isHorizontal) 312 return currentOffset + Vector2(childSpace.x + style.gap.sideX, 0); 313 else 314 return currentOffset + Vector2(0, childSpace.y + style.gap.sideY); 315 316 } 317 318 /// Get space for a child. 319 /// Params: 320 /// child = Child to place 321 /// available = Available space 322 protected Vector2 childSpace(const Node child, Vector2 available, bool stateful = true) const 323 in( 324 child.isHidden || child.layout.expand <= denominator, 325 format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"( 326 typeid(this), typeid(child), child.layout.expand, denominator, 327 ) 328 ) 329 out( 330 r; only(r.tupleof).all!isFinite, 331 format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"( 332 typeid(child), r, available, child.layout.expand, denominator, reservedSpace 333 ) 334 ) 335 do { 336 337 // Hidden, give it no space 338 if (child.isHidden) return Vector2(); 339 340 // Horizontal 341 if (directionHorizontal) { 342 343 const avail = (available.x - reservedSpace); 344 const minSize = stateful 345 ? child.minSize.x 346 : available.x; 347 348 return Vector2( 349 child.layout.expand 350 ? avail * child.layout.expand / denominator 351 : minSize, 352 available.y, 353 ); 354 355 } 356 357 // Vertical 358 else { 359 360 const avail = (available.y - reservedSpace); 361 const minSize = stateful 362 ? child.minSize.y 363 : available.y; 364 365 return Vector2( 366 available.x, 367 child.layout.expand 368 ? avail * child.layout.expand / denominator 369 : minSize, 370 ); 371 372 } 373 374 } 375 376 } 377 378 /// 379 unittest { 380 381 import fluid; 382 383 // A vspace will align all its content in a column 384 vspace( 385 label("First entry"), 386 label("Second entry"), 387 label("Third entry"), 388 ); 389 390 // hspace will lay out the nodes in a row 391 hspace( 392 label("One, "), 393 label("Two, "), 394 label("Three!"), 395 ); 396 397 // Combine them to quickly build layouts! 398 vspace( 399 label("Are you sure you want to proceed?"), 400 hspace( 401 button("Yes", delegate { }), 402 button("Cancel", delegate { }), 403 ), 404 ); 405 406 }