1 module glui.scroll; 2 3 import raylib; 4 5 import std.meta; 6 import std.conv; 7 import std.algorithm; 8 9 import glui.node; 10 import glui.frame; 11 import glui.space; 12 import glui.utils; 13 import glui.input; 14 import glui.style; 15 import glui.structs; 16 import glui.scrollbar; 17 18 private extern(C) float GetMouseWheelMove(); 19 20 21 @safe: 22 23 24 alias GluiScrollFrame = GluiScrollable!GluiFrame; 25 alias GluiScrollable(T : GluiSpace) = GluiScrollable!(T, "directionHorizontal"); 26 27 /// Create a new vertical scroll frame. 28 alias vscrollFrame = simpleConstructor!GluiScrollFrame; 29 30 /// Create a new horizontal scroll frame. 31 alias hscrollFrame = simpleConstructor!(GluiScrollFrame, (a) { 32 33 a.directionHorizontal = true; 34 35 }); 36 37 /// Implement scrolling for the given node. 38 /// 39 /// This only supports scrolling in one side. 40 class GluiScrollable(T : GluiNode, string horizontalExpression) : T { 41 42 mixin DefineStyles; 43 44 // TODO: move keyboard input to GluiScrollBar. 45 46 public { 47 48 /// Scrollbar for the frame. Can be replaced with a customized one. 49 GluiScrollBar scrollBar; 50 51 } 52 53 private { 54 55 /// minSize including the padding. 56 Vector2 paddingBoxSize; 57 58 } 59 60 this(T...)(T args) { 61 62 super(args); 63 this.scrollBar = .vscrollBar(); 64 65 } 66 67 /// Distance the node is scrolled by. 68 @property 69 ref inout(size_t) scroll() inout { return scrollBar.position; } 70 71 /// Check if the underlying node is horizontal. 72 private bool isHorizontal() const { 73 74 return mixin(horizontalExpression); 75 76 } 77 78 /// Scroll to the beginning of the node. 79 void scrollStart() { 80 81 scroll = 0; 82 83 } 84 85 /// Scroll to the end of the node, requires the node to be drawn at least once. 86 void scrollEnd() { 87 88 scroll = scrollMax; 89 90 } 91 92 /// Set the scroll to a value clamped between start and end. 93 void setScroll(ptrdiff_t value) { 94 95 scrollBar.setScroll(value); 96 97 } 98 99 /// Get the maximum value this container can be scrolled to. Requires at least one draw. 100 size_t scrollMax() const { 101 102 return scrollBar.scrollMax(); 103 104 } 105 106 override void resizeImpl(Vector2 space) { 107 108 assert(scrollBar !is null, "No scrollbar has been set for GluiScrollable"); 109 assert(theme !is null); 110 assert(tree !is null); 111 112 /// Padding represented as a vector. This sums the padding on each axis. 113 const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum); 114 115 /// Space with padding included 116 const paddingSpace = space + paddingVector; 117 118 // Resize the scrollbar 119 with (scrollBar) { 120 121 horizontal = isHorizontal; 122 layout = .layout!(1, "fill"); 123 resize(this.tree, this.theme, paddingSpace); 124 125 } 126 127 /// Space without the scrollbar 128 const contentSpace = isHorizontal 129 ? space - Vector2(0, scrollBar.minSize.y) 130 : space - Vector2(scrollBar.minSize.x, 0); 131 132 // Resize the frame while reserving some space for the scrollbar 133 super.resizeImpl(contentSpace); 134 135 // Calculate the expected padding box size 136 paddingBoxSize = minSize + paddingVector; 137 138 // Set scrollbar size and add the scrollbar to the result 139 if (isHorizontal) { 140 141 scrollBar.availableSpace = cast(size_t) paddingBoxSize.x; 142 minSize.y += scrollBar.minSize.y; 143 144 } 145 146 else { 147 148 scrollBar.availableSpace = cast(size_t) paddingBoxSize.y; 149 minSize.x += scrollBar.minSize.x; 150 151 } 152 153 } 154 155 override void drawImpl(Rectangle outer, Rectangle inner) { 156 157 // Note: Mouse input detection is primitive, awaiting #13 and #14 to help better identify when should the mouse 158 // affect this frame. 159 160 // This node doesn't use GluiInput because it doesn't take focus, and we don't want to cause related 161 // accessibility issues. It can function perfectly without it, or at least until above note gets fixed. 162 // Then, a "GluiHoverable" interface could possibly become a thing. 163 164 // TODO Is the above still true? 165 166 scrollBar.horizontal = isHorizontal; 167 168 auto scrollBarRect = outer; 169 170 if (isHovered) inputImpl(); 171 172 // Scroll the given rectangle horizontally 173 if (isHorizontal) { 174 175 // Calculate fake box sizes 176 outer.width = max(outer.width, paddingBoxSize.x); 177 inner = style.contentBox(outer); 178 179 static foreach (rect; AliasSeq!(outer, inner)) { 180 181 // Perform the scroll 182 rect.x -= scroll; 183 184 // Reduce both rects by scrollbar size 185 rect.height -= scrollBar.minSize.y; 186 187 } 188 189 scrollBarRect.y += outer.height; 190 scrollBarRect.height = scrollBar.minSize.y; 191 192 } 193 194 // Vertically 195 else { 196 197 // Calculate fake box sizes 198 outer.height = max(outer.height, paddingBoxSize.y); 199 inner = style.contentBox(outer); 200 201 static foreach (rect; AliasSeq!(outer, inner)) { 202 203 // Perform the scroll 204 rect.y -= scroll; 205 206 // Reduce both rects by scrollbar size 207 rect.width -= scrollBar.minSize.x; 208 209 } 210 211 scrollBarRect.x += outer.width; 212 scrollBarRect.width = scrollBar.minSize.x; 213 214 } 215 216 // Draw the scrollbar 217 scrollBar.draw(scrollBarRect); 218 219 // Draw the frame 220 super.drawImpl(outer, inner); 221 222 } 223 224 /// Implementation of mouse input 225 private void inputImpl() @trusted { 226 227 const float move = -GetMouseWheelMove; 228 const float totalChange = move * scrollBar.scrollSpeed; 229 230 scrollBar.setScroll(scroll.to!ptrdiff_t + totalChange.to!ptrdiff_t); 231 232 } 233 234 }