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