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