1 /// 2 module glui.space; 3 4 import raylib; 5 6 import std.math; 7 import std.range; 8 import std.string; 9 import std.traits; 10 import std.algorithm; 11 12 import glui.node; 13 import glui.style; 14 import glui.utils; 15 import glui.children; 16 17 @safe: 18 19 /// Make a new vertical space 20 GluiSpace vspace(T...)(T args) { 21 22 return new GluiSpace(args); 23 24 } 25 26 /// Make a new horizontal space 27 GluiSpace hspace(T...)(T args) { 28 29 auto frame = new GluiSpace(args); 30 frame.directionHorizontal = true; 31 32 return frame; 33 34 } 35 36 /// This is a space, basic container for other nodes. 37 /// 38 /// Space only acts as a container and doesn't implement styles and doesn't take focus. It can be very useful to build 39 /// overlaying nodes, eg. with `GluiOnionFrame`. 40 class GluiSpace : GluiNode { 41 42 mixin DefineStyles; 43 44 /// Children of this frame. 45 Children children; 46 47 /// Defines in what directions children of this frame should be placed. 48 /// 49 /// If true, children are placed horizontally, if false, vertically. 50 bool horizontal; 51 52 alias directionHorizontal = horizontal; 53 54 private { 55 56 /// Denominator for content sizing. 57 uint denominator; 58 59 /// Space reserved for shrinking elements. 60 uint reservedSpace; 61 62 } 63 64 // Generate constructors 65 static foreach (index; 0 .. BasicNodeParamLength) { 66 67 this(BasicNodeParam!index params, GluiNode[] nodes...) { 68 69 super(params); 70 this.children ~= nodes; 71 72 } 73 74 } 75 76 /// Add children. 77 pragma(inline, true) 78 void opOpAssign(string operator : "~", T)(T nodes) { 79 80 children ~= nodes; 81 82 } 83 84 protected override void resizeImpl(Vector2 available) { 85 86 import std.algorithm : max, map, fold; 87 88 // Reset size 89 minSize = Vector2(0, 0); 90 reservedSpace = 0; 91 denominator = 0; 92 93 // Ignore the rest if there's no children 94 if (!children.length) return; 95 96 Vector2 maxExpandSize; 97 98 // Collect expanding children in a separate array 99 GluiNode[] expandChildren; 100 foreach (child; children) { 101 102 // This node expands and isn't hidden 103 if (child.layout.expand && !child.hidden) { 104 105 // Make it happen later 106 expandChildren ~= child; 107 108 // Add to the denominator 109 denominator += child.layout.expand; 110 111 } 112 113 // Check non-expand nodes now 114 else { 115 116 child.resize(tree, theme, childSpace(child, available)); 117 minSize = childPosition(child.minSize, minSize); 118 119 // Reserve space for this node 120 reservedSpace += directionHorizontal 121 ? cast(uint) child.minSize.x 122 : cast(uint) child.minSize.y; 123 124 } 125 126 } 127 128 // Calculate the size of expanding children last 129 foreach (child; expandChildren) { 130 131 // Resize the child 132 child.resize(tree, theme, childSpace(child, available)); 133 134 const childSize = child.minSize; 135 const childExpand = child.layout.expand; 136 137 const segmentSize = horizontal 138 ? Vector2(childSize.x / childExpand, childSize.y) 139 : Vector2(childSize.x, childSize.y / childExpand); 140 141 // Reserve expand space 142 maxExpandSize.x = max(maxExpandSize.x, segmentSize.x); 143 maxExpandSize.y = max(maxExpandSize.y, segmentSize.y); 144 145 } 146 147 const expandSize = horizontal 148 ? Vector2(maxExpandSize.x * denominator, maxExpandSize.y) 149 : Vector2(maxExpandSize.x, maxExpandSize.y * denominator); 150 151 // Add the expand space 152 minSize = childPosition(expandSize, minSize); 153 154 } 155 156 protected override void drawImpl(Rectangle, Rectangle area) { 157 158 auto position = Vector2(area.x, area.y); 159 160 drawChildren((child) { 161 162 // Get params 163 const size = childSpace(child, Vector2(area.width, area.height)); 164 const rect = Rectangle( 165 position.x, position.y, 166 size.x, size.y 167 ); 168 169 // Draw the child 170 child.draw(rect); 171 172 // Offset position 173 if (directionHorizontal) position.x += cast(int) size.x; 174 else position.y += cast(int) size.y; 175 176 }); 177 178 } 179 180 /// Iterate over every child and perform the painting function. Will automatically remove nodes queued for removal. 181 protected void drawChildren(void delegate(GluiNode) @safe painter) { 182 183 GluiNode[] leftovers; 184 185 children.lock(); 186 scope (exit) children.unlock(); 187 188 // Draw each child and get rid of removed children 189 auto range = children[] 190 .filter!"!a.toRemove" 191 .tee!((node) => painter(node)); 192 193 () @trusted { 194 195 // Process the children and move them back to the original array 196 auto leftovers = range.moveAll(children.forceMutable); 197 198 // Adjust the array size 199 children.forceMutable.length -= leftovers.length; 200 201 }(); 202 203 } 204 205 protected override bool hoveredImpl(Rectangle, Vector2) const { 206 207 return false; 208 209 } 210 211 protected override const(Style) pickStyle() const { 212 213 return null; 214 215 } 216 217 /// Params: 218 /// child = Child size to add. 219 /// previous = Previous position. 220 private Vector2 childPosition(Vector2 child, Vector2 previous) const { 221 222 import std.algorithm : max; 223 224 // Horizontal 225 if (directionHorizontal) { 226 227 return Vector2( 228 previous.x + child.x, 229 max(minSize.y, child.y), 230 ); 231 232 } 233 234 // Vertical 235 else return Vector2( 236 max(minSize.x, child.x), 237 previous.y + child.y, 238 ); 239 240 } 241 242 /// Get space for a child. 243 /// Params: 244 /// child = Child to place 245 /// available = Available space 246 private Vector2 childSpace(const GluiNode child, Vector2 available) const 247 in( 248 child.hidden || child.layout.expand <= denominator, 249 format!"Nodes %s/%s sizes are out of date, call updateSize after updating the tree or layout (%s/%s)"( 250 typeid(this), typeid(child), child.layout.expand, denominator, 251 ) 252 ) 253 out( 254 r; [r.tupleof].all!isFinite, 255 format!"space: child %s given invalid size %s. available = %s, expand = %s, denominator = %s, reserved = %s"( 256 typeid(child), r, available, child.layout.expand, denominator, reservedSpace 257 ) 258 ) 259 do { 260 261 // Hidden, give it no space 262 if (child.hidden) return Vector2(); 263 264 // Horizontal 265 if (directionHorizontal) { 266 267 const avail = (available.x - reservedSpace); 268 269 return Vector2( 270 child.layout.expand 271 ? avail * child.layout.expand / denominator 272 : child.minSize.x, 273 available.y, 274 ); 275 276 } 277 278 // Vertical 279 else { 280 281 const avail = (available.y - reservedSpace); 282 283 return Vector2( 284 available.x, 285 child.layout.expand 286 ? avail * child.layout.expand / denominator 287 : child.minSize.y, 288 ); 289 290 } 291 292 } 293 294 }