1 /// 2 module glui.node; 3 4 import raylib; 5 6 import std.math; 7 import std.traits; 8 import std..string; 9 10 import glui.style; 11 import glui.utils; 12 import glui.structs; 13 14 @safe: 15 16 private interface Styleable { 17 18 /// Reload styles for the node. Triggered when the theme is changed. 19 /// 20 /// Use `mixin DefineStyles` to generate the styles. 21 final void reloadStyles() { 22 23 // First load what we're given 24 reloadStylesImpl(); 25 26 // Then load the defaults 27 loadDefaultStyles(); 28 29 } 30 31 // Internal: 32 33 protected void reloadStylesImpl(); 34 protected void loadDefaultStyles(); 35 36 } 37 38 /// Represents a Glui node. 39 abstract class GluiNode : Styleable { 40 41 /// This node defines a single style, `style`, which also works as a default style for all other nodes. However, 42 /// rather than for that, the purpose of this style is to define the convention of `style` being the node's default, 43 /// idle style. 44 /// 45 /// It should be noted the default `style` is the only style that affects a node's sizing — as the tree would have 46 /// to be resized in case they changed and secondary styles are assumed to change frequently (for example, on 47 /// hover). In practice, resizing the tree on those changes usually ends up horrible for the user, so it's advised 48 /// to stick to constant sizing in order to not hurt the accessibility. 49 mixin DefineStyles!( 50 "style", q{ Style.init }, 51 ); 52 53 public { 54 55 /// Tree data for the node. Note: requires at least one draw before this will work. 56 LayoutTree* tree; 57 58 /// Layout for this node. 59 Layout layout; 60 61 /// If true, this node will be removed from the tree on the next draw. 62 bool toRemove; 63 64 } 65 66 /// Minimum size of the node. 67 protected auto minSize = Vector2(0, 0); 68 69 private { 70 71 /// If true, this node must update its size. 72 bool _requiresResize = true; 73 74 /// If true, this node is hidden and won't be rendered. 75 bool _hidden; 76 77 /// If true, this node is currently hovered. 78 bool _hovered; 79 80 /// Theme of this node. 81 Theme _theme; 82 83 } 84 85 @property { 86 87 /// Get the current theme. 88 pragma(inline) 89 const(Theme) theme() const { return _theme; } 90 91 /// Set the theme. 92 const(Theme) theme(const Theme value) @trusted { 93 94 _theme = cast(Theme) value; 95 reloadStyles(); 96 return _theme; 97 98 } 99 100 } 101 102 @property { 103 104 /// Check if the node is hidden. 105 bool hidden() const { return _hidden; } 106 107 /// Set the visibility 108 bool hidden(bool value) { 109 110 // If changed, trigger resize 111 if (_hidden != value) updateSize(); 112 113 return _hidden = value; 114 115 } 116 117 } 118 119 /// Params: 120 /// layout = Layout for this node. 121 /// theme = Theme of this node. 122 this(Layout layout = Layout.init, const Theme theme = null) { 123 124 this.layout = layout; 125 this.theme = theme; 126 this.tree = new LayoutTree(this); 127 128 } 129 130 /// Ditto 131 this(const Theme theme = null, Layout layout = Layout.init) { 132 133 this(layout, theme); 134 135 } 136 137 /// Ditto 138 this() { 139 140 this(Layout.init, null); 141 142 } 143 144 /// Show the node. 145 final GluiNode show() { 146 147 hidden = false; 148 return this; 149 150 } 151 152 /// Hide the node. 153 final GluiNode hide() { 154 155 hidden = true; 156 return this; 157 158 } 159 160 /// Toggle the node's visibility. 161 final void toggleShow() { hidden = !hidden; } 162 163 /// Remove this node from the tree before the next draw. 164 final void remove() { 165 166 hidden = true; 167 toRemove = true; 168 169 } 170 171 /// Check if this node is hovered. 172 @property 173 bool hovered() const { return _hovered; } 174 175 /// Recalculate the window size before next draw. 176 /// 177 /// Note: should be called or root; in case of children, will only work after the first draw. 178 final void updateSize() { 179 180 tree.root._requiresResize = true; 181 182 } 183 184 /// Draw this node as a root node. 185 final void draw() @trusted { 186 187 // No theme set, set the default 188 if (!theme) { 189 190 import glui.default_theme; 191 theme = gluiDefaultTheme; 192 193 } 194 195 const space = Vector2(GetScreenWidth, GetScreenHeight); 196 197 // Clear mouse hover if LMB is up 198 if (!isLMBHeld) tree.hover = null; 199 200 201 // Resize if required 202 if (IsWindowResized || _requiresResize) { 203 204 resize(space); 205 _requiresResize = false; 206 207 } 208 209 // Draw this node 210 draw(Rectangle(0, 0, space.x, space.y)); 211 212 213 // Set mouse cursor to match hovered node 214 if (tree.hover) { 215 216 if (auto style = tree.hover.pickStyle) { 217 218 SetMouseCursor(style.mouseCursor); 219 220 } 221 222 } 223 224 225 // Note: pressed, not released; released activates input events, pressed activates focus 226 const mousePressed = IsMouseButtonPressed(MouseButton.MOUSE_LEFT_BUTTON); 227 228 // Mouse is hovering an input node 229 if (auto hoverInput = cast(GluiFocusable) tree.hover) { 230 231 // Pass the input to it 232 hoverInput.mouseImpl(); 233 234 // If the left mouse button is pressed down, let it have focus 235 if (mousePressed && !hoverInput.isFocused) hoverInput.focus(); 236 237 } 238 239 // Mouse pressed over a non-focusable node, remove focus 240 else if (mousePressed) tree.focus = null; 241 242 243 // Pass keyboard input to the currently focused node 244 if (tree.focus) tree.keyboardHandled = tree.focus.keyboardImpl(); 245 else tree.keyboardHandled = false; 246 247 } 248 249 /// Draw this node at specified location. 250 final protected void draw(Rectangle space) @trusted { 251 252 // Given "space" is the amount of space we're given and what we should use at max. 253 // Within this function, we deduce how much of the space we should actually use, and align the node 254 // within the space. 255 256 import std.algorithm : all, min, max; 257 258 assert(!toRemove, "A toRemove child wasn't removed from container."); 259 260 // If hidden, don't draw anything 261 if (hidden) return; 262 263 const spaceV = Vector2(space.width, space.height); 264 265 // No style set? Reload styles, the theme might've been set through CTFE 266 if (!style) reloadStyles(); 267 268 // Get parameters 269 const size = Vector2( 270 layout.nodeAlign[0] == NodeAlign.fill ? space.width : min(space.width, minSize.x), 271 layout.nodeAlign[1] == NodeAlign.fill ? space.height : min(space.height, minSize.y), 272 ); 273 const position = position(space, size); 274 275 // Calculate the margin 276 const margin = style 277 ? Rectangle( 278 style.margin[0], style.margin[2], 279 style.margin[0] + style.margin[1], style.margin[2] + style.margin[3] 280 ) 281 : Rectangle(0, 0, 0, 0); 282 283 // Get the rectangle this node should occupy within the given space 284 const paddingBox = Rectangle( 285 position.x + margin.x, position.y + margin.y, 286 size.x - margin.w, size.y - margin.h, 287 ); 288 289 // Get the visible part of the padding box — so overflowed content doesn't get mouse focus 290 const visibleBox = tree.intersectScissors(paddingBox); 291 292 // Subtract padding to get the content box. 293 const contentBox = style.contentBox(paddingBox); 294 295 // Check if hovered 296 _hovered = hoveredImpl(visibleBox, GetMousePosition); 297 298 // Update global hover unless mouse is being held down 299 if (_hovered && !isLMBHeld) tree.hover = this; 300 301 assert( 302 [size.tupleof].all!isFinite, 303 format!"Node %s resulting size is invalid: %s; given space = %s, minSize = %s"( 304 typeid(this), size, space, minSize 305 ), 306 ); 307 assert( 308 [paddingBox.tupleof, contentBox.tupleof].all!isFinite, 309 format!"Node %s size is invalid: paddingBox = %s, contentBox = %s"( 310 typeid(this), paddingBox, contentBox 311 ) 312 ); 313 314 tree.pushScissors(paddingBox); 315 scope (exit) tree.popScissors(); 316 317 // Draw the node 318 drawImpl(paddingBox, contentBox); 319 320 } 321 322 /// Recalculate the minimum node size and update the `minSize` property. 323 /// Params: 324 /// space = Available space. 325 protected final void resize(Vector2 space) { 326 327 // The node is hidden, reset size 328 if (hidden) minSize = Vector2(0, 0); 329 330 // Otherwise perform like normal 331 else { 332 333 import std.range, std.algorithm; 334 335 const spacingX = style ? chain(style.margin[0..2], style.padding[0..2]).sum : 0; 336 const spacingY = style ? chain(style.margin[2..4], style.padding[2..4]).sum : 0; 337 338 // Reduce space by margins 339 space.x = max(0, space.x - spacingX); 340 space.y = max(0, space.y - spacingY); 341 342 // Resize the node 343 resizeImpl(space); 344 345 // Add margins 346 minSize.x += spacingX; 347 minSize.y += spacingY; 348 349 } 350 351 assert( 352 minSize.x.isFinite && minSize.y.isFinite, 353 format!"Node %s returned invalid minSize %s"(typeid(this), minSize) 354 ); 355 356 } 357 358 /// Ditto 359 /// 360 /// This is the implementation of resizing to be provided by children. 361 /// 362 /// If style margins/paddings are non-zero, they are automatically subtracted from space, so they are handled 363 /// automatically. 364 protected abstract void resizeImpl(Vector2 space); 365 366 /// Draw this node. 367 /// 368 /// Note: Instead of directly accessing `style`, use `pickStyle` to enable temporarily changing styles as visual 369 /// feedback. `resize` should still use the normal style. 370 /// 371 /// Params: 372 /// paddingBox = Area which should be used by the node. It should include styling elements such as background, 373 /// but no content. 374 /// contentBox = Area which should be filled with content of the node, such as child nodes, text, etc. 375 protected abstract void drawImpl(Rectangle paddingBox, Rectangle contentBox); 376 377 /// Check if the node is hovered. 378 /// 379 /// This will be called right before drawImpl for each node in order to determine the which node should handle mouse 380 /// input. 381 /// 382 /// If your node fills the rectangle area its given in `drawImpl`, you may use `mixin ImplHoveredRect` to implement 383 /// this automatically. 384 /// 385 /// Params: 386 /// rect = Area the node should be drawn in, as provided by drawImpl. 387 /// mousePosition = Current mouse position within the window. 388 protected abstract bool hoveredImpl(Rectangle rect, Vector2 mousePosition) const; 389 390 protected mixin template ImplHoveredRect() { 391 392 private import raylib : Rectangle, Vector2; 393 394 protected override bool hoveredImpl(Rectangle rect, Vector2 mousePosition) const { 395 396 import glui.utils : contains; 397 398 return rect.contains(mousePosition); 399 400 } 401 402 } 403 404 /// Get the current style. 405 protected abstract const(Style) pickStyle() const; 406 407 /// Get the node position. 408 private Vector2 position(Rectangle space, Vector2 usedSpace) const { 409 410 float positionImpl(NodeAlign align_, lazy float spaceLeft) { 411 412 with (NodeAlign) 413 final switch (align_) { 414 415 case start, fill: return 0; 416 case center: return spaceLeft / 2; 417 case end: return spaceLeft; 418 419 } 420 421 } 422 423 return Vector2( 424 space.x + positionImpl(layout.nodeAlign[0], space.width - usedSpace.x), 425 space.y + positionImpl(layout.nodeAlign[1], space.height - usedSpace.y), 426 ); 427 428 } 429 430 private bool isLMBHeld() @trusted { 431 432 const lmb = MouseButton.MOUSE_LEFT_BUTTON; 433 return IsMouseButtonDown(lmb) || IsMouseButtonReleased(lmb); 434 435 } 436 437 override string toString() const { 438 439 return format!"%s(%s)"(typeid(this), layout); 440 441 } 442 443 }