1 module fluid.scroll; 2 3 import std.meta; 4 import std.conv; 5 import std.algorithm; 6 7 import fluid.node; 8 import fluid.frame; 9 import fluid.space; 10 import fluid.utils; 11 import fluid.input; 12 import fluid.style; 13 import fluid.backend; 14 import fluid.structs; 15 16 public import fluid.scroll_input; 17 18 19 @safe: 20 21 22 alias ScrollFrame = Scrollable!Frame; 23 alias Scrollable(T : Space) = Scrollable!(T, "directionHorizontal"); 24 25 /// Create a new vertical scroll frame. 26 alias vscrollFrame = simpleConstructor!ScrollFrame; 27 28 /// Create a new horizontal scroll frame. 29 alias hscrollFrame = simpleConstructor!(ScrollFrame, (a) { 30 31 a.directionHorizontal = true; 32 33 }); 34 35 /// Create a new scrollable node. 36 alias vscrollable(alias T) = simpleConstructor!(ApplyRight!(ScrollFrame, "false"), T); 37 38 /// Create a new horizontally scrollable node. 39 alias hscrollable(alias T) = simpleConstructor!(ApplyRight!(ScrollFrame, "true"), T); 40 41 /// Implement scrolling for the given node. 42 /// 43 /// This only supports scrolling in one axis. 44 class Scrollable(T : Node, string horizontalExpression) : T, FluidScrollable { 45 46 public { 47 48 /// Scrollbar for the frame. Can be replaced with a customized one. 49 ScrollInput 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 = .vscrollInput(.layout!(1, "fill")); 64 65 } 66 67 /// Distance the node is scrolled by. 68 ref inout(float) scroll() inout { 69 70 return scrollBar.position; 71 72 } 73 74 float scroll() const { 75 76 return scrollBar.position; 77 78 } 79 80 float scroll(float value) { 81 82 setScroll(value); 83 return value; 84 85 } 86 87 /// Check if the underlying node is horizontal. 88 private bool isHorizontal() const { 89 90 return mixin(horizontalExpression); 91 92 } 93 94 /// Scroll to the beginning of the node. 95 void scrollStart() { 96 97 scroll = 0; 98 99 } 100 101 /// Scroll to the end of the node, requires the node to be drawn at least once. 102 void scrollEnd() { 103 104 scroll = scrollMax; 105 106 } 107 108 /// Set the scroll to a value clamped between start and end. 109 void setScroll(float value) { 110 111 scrollBar.setScroll(value); 112 113 } 114 115 /// Get the maximum value this container can be scrolled to. Requires at least one draw. 116 float scrollMax() const { 117 118 return scrollBar.scrollMax(); 119 120 } 121 122 deprecated("shallowScrollTo with a Vector2 argument has been deprecated and will be removed in Fluid 0.8.0.") 123 Rectangle shallowScrollTo(const Node child, Vector2, Rectangle parentBox, Rectangle childBox) { 124 125 return shallowScrollTo(child, parentBox, childBox); 126 127 } 128 129 /// Scroll to the given node. 130 Rectangle shallowScrollTo(const Node, Rectangle parentBox, Rectangle childBox) { 131 132 struct Position { 133 134 float* start; 135 float end; 136 float viewportStart, viewportEnd; 137 138 } 139 140 // Get the data for the node 141 scope position = isHorizontal 142 ? Position( 143 &childBox.x, childBox.x + childBox.width, 144 parentBox.x, parentBox.x + parentBox.width 145 ) 146 : Position( 147 &childBox.y, childBox.y + childBox.height, 148 parentBox.y, parentBox.y + parentBox.height 149 ); 150 151 auto scrollBefore = scroll(); 152 153 // Calculate the offset 154 auto offset 155 156 // Need to scroll towards the end 157 = *position.start > position.viewportStart && position.end > position.viewportEnd 158 ? position.end - position.viewportEnd 159 160 // Need to scroll towards the start 161 : *position.start < position.viewportStart && position.end < position.viewportEnd 162 ? *position.start - position.viewportStart 163 164 // Already in viewport 165 : 0; 166 167 // Perform the scroll 168 setScroll(scroll + offset); 169 170 // Adjust the offset 171 offset = scroll - scrollBefore; 172 173 // Apply child position 174 *position.start -= offset; 175 176 return childBox; 177 178 } 179 180 override void resizeImpl(Vector2 space) { 181 182 assert(scrollBar !is null, "No scrollbar has been set for FluidScrollable"); 183 assert(tree !is null); 184 185 /// Padding represented as a vector. This sums the padding on each axis. 186 const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum); 187 188 /// Space with padding included 189 const paddingSpace = space + paddingVector; 190 191 // Resize the scrollbar 192 scrollBar.isHorizontal = this.isHorizontal; 193 scrollBar.resize(this.tree, this.theme, paddingSpace); 194 195 /// Space without the scrollbar 196 const contentSpace = isHorizontal 197 ? space - Vector2(0, scrollBar.minSize.y) 198 : space - Vector2(scrollBar.minSize.x, 0); 199 200 // Resize the frame while reserving some space for the scrollbar 201 super.resizeImpl(contentSpace); 202 203 // Calculate the expected padding box size 204 paddingBoxSize = minSize + paddingVector; 205 206 // Set scrollbar size and add the scrollbar to the result 207 if (isHorizontal) { 208 209 scrollBar.availableSpace = paddingBoxSize.x; 210 minSize.y += scrollBar.minSize.y; 211 212 } 213 214 else { 215 216 scrollBar.availableSpace = paddingBoxSize.y; 217 minSize.x += scrollBar.minSize.x; 218 219 } 220 221 } 222 223 override void drawImpl(Rectangle mainOuter, Rectangle inner) { 224 225 auto outer = mainOuter; 226 auto scrollBarRect = outer; 227 228 scrollBar.horizontal = isHorizontal; 229 230 // Scroll the given rectangle horizontally 231 if (isHorizontal) { 232 233 // Calculate fake box sizes 234 outer.width = max(outer.width, paddingBoxSize.x); 235 inner = style.contentBox(outer); 236 237 static foreach (rect; AliasSeq!(outer, inner)) { 238 239 // Perform the scroll 240 rect.x -= scroll; 241 242 // Reduce both rects by scrollbar size 243 rect.height -= scrollBar.minSize.y; 244 245 } 246 247 scrollBarRect.y += outer.height; 248 scrollBarRect.height = scrollBar.minSize.y; 249 mainOuter.height -= scrollBarRect.width; 250 251 } 252 253 // Vertically 254 else { 255 256 // Calculate fake box sizes 257 outer.height = max(outer.height, paddingBoxSize.y); 258 inner = style.contentBox(outer); 259 260 static foreach (rect; AliasSeq!(outer, inner)) { 261 262 // Perform the scroll 263 rect.y -= scroll; 264 265 // Reduce both rects by scrollbar size 266 rect.width -= scrollBar.minSize.x; 267 268 } 269 270 scrollBarRect.x += outer.width; 271 scrollBarRect.width = scrollBar.minSize.x; 272 mainOuter.width -= scrollBarRect.width; 273 274 } 275 276 // Draw the scrollbar 277 scrollBar.draw(scrollBarRect); 278 279 // Draw the frame 280 super.drawImpl(mainOuter, inner); 281 282 } 283 284 bool canScroll(Vector2 valueVec) const { 285 286 const speed = scrollBar.scrollSpeed; 287 const value = isHorizontal 288 ? valueVec.x 289 : valueVec.y; 290 const move = speed * value; 291 292 return move.to!ptrdiff_t != 0; 293 294 } 295 296 void scrollImpl(Vector2 valueVec) { 297 298 const speed = scrollBar.scrollSpeed; 299 const value = isHorizontal 300 ? valueVec.x 301 : valueVec.y; 302 const move = speed * value; 303 // io.deltaTime is irrelevant here 304 305 scrollBar.setScroll(scroll + move); 306 307 } 308 309 }