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 Rectangle shallowScrollTo(const Node child, Vector2, Rectangle parentBox, Rectangle childBox) { 123 124 return shallowScrollTo(child, parentBox, childBox); 125 126 } 127 128 /// Scroll to the given node. 129 Rectangle shallowScrollTo(const Node, Rectangle parentBox, Rectangle childBox) { 130 131 struct Position { 132 133 float* start; 134 float end; 135 float viewportStart, viewportEnd; 136 137 } 138 139 // Get the data for the node 140 scope position = isHorizontal 141 ? Position( 142 &childBox.x, childBox.x + childBox.width, 143 parentBox.x, parentBox.x + parentBox.width 144 ) 145 : Position( 146 &childBox.y, childBox.y + childBox.height, 147 parentBox.y, parentBox.y + parentBox.height 148 ); 149 150 auto scrollBefore = scroll(); 151 152 // Calculate the offset 153 auto offset 154 155 // Need to scroll towards the end 156 = *position.start > position.viewportStart && position.end > position.viewportEnd 157 ? position.end - position.viewportEnd 158 159 // Need to scroll towards the start 160 : *position.start < position.viewportStart && position.end < position.viewportEnd 161 ? *position.start - position.viewportStart 162 163 // Already in viewport 164 : 0; 165 166 // Perform the scroll 167 setScroll(scroll + offset); 168 169 // Adjust the offset 170 offset = scroll - scrollBefore; 171 172 // Apply child position 173 *position.start -= offset; 174 175 return childBox; 176 177 } 178 179 override void resizeImpl(Vector2 space) { 180 181 assert(scrollBar !is null, "No scrollbar has been set for FluidScrollable"); 182 assert(tree !is null); 183 184 /// Padding represented as a vector. This sums the padding on each axis. 185 const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum); 186 187 /// Space with padding included 188 const paddingSpace = space + paddingVector; 189 190 // Resize the scrollbar 191 scrollBar.isHorizontal = this.isHorizontal; 192 scrollBar.resize(this.tree, this.theme, paddingSpace); 193 194 /// Space without the scrollbar 195 const contentSpace = isHorizontal 196 ? space - Vector2(0, scrollBar.minSize.y) 197 : space - Vector2(scrollBar.minSize.x, 0); 198 199 // Resize the frame while reserving some space for the scrollbar 200 super.resizeImpl(contentSpace); 201 202 // Calculate the expected padding box size 203 paddingBoxSize = minSize + paddingVector; 204 205 // Set scrollbar size and add the scrollbar to the result 206 if (isHorizontal) { 207 208 scrollBar.availableSpace = paddingBoxSize.x; 209 minSize.y += scrollBar.minSize.y; 210 211 } 212 213 else { 214 215 scrollBar.availableSpace = paddingBoxSize.y; 216 minSize.x += scrollBar.minSize.x; 217 218 } 219 220 } 221 222 override void drawImpl(Rectangle outer, Rectangle inner) { 223 224 auto scrollBarRect = outer; 225 226 scrollBar.horizontal = isHorizontal; 227 228 // Scroll the given rectangle horizontally 229 if (isHorizontal) { 230 231 // Calculate fake box sizes 232 outer.width = max(outer.width, paddingBoxSize.x); 233 inner = style.contentBox(outer); 234 235 static foreach (rect; AliasSeq!(outer, inner)) { 236 237 // Perform the scroll 238 rect.x -= scroll; 239 240 // Reduce both rects by scrollbar size 241 rect.height -= scrollBar.minSize.y; 242 243 } 244 245 scrollBarRect.y += outer.height; 246 scrollBarRect.height = scrollBar.minSize.y; 247 248 } 249 250 // Vertically 251 else { 252 253 // Calculate fake box sizes 254 outer.height = max(outer.height, paddingBoxSize.y); 255 inner = style.contentBox(outer); 256 257 static foreach (rect; AliasSeq!(outer, inner)) { 258 259 // Perform the scroll 260 rect.y -= scroll; 261 262 // Reduce both rects by scrollbar size 263 rect.width -= scrollBar.minSize.x; 264 265 } 266 267 scrollBarRect.x += outer.width; 268 scrollBarRect.width = scrollBar.minSize.x; 269 270 } 271 272 // Draw the scrollbar 273 scrollBar.draw(scrollBarRect); 274 275 // Draw the frame 276 super.drawImpl(outer, inner); 277 278 } 279 280 bool canScroll(Vector2 valueVec) const { 281 282 const speed = scrollBar.scrollSpeed; 283 const value = isHorizontal 284 ? valueVec.x 285 : valueVec.y; 286 const move = speed * value; 287 288 return move.to!ptrdiff_t != 0; 289 290 } 291 292 void scrollImpl(Vector2 valueVec) { 293 294 const speed = scrollBar.scrollSpeed; 295 const value = isHorizontal 296 ? valueVec.x 297 : valueVec.y; 298 const move = speed * value; 299 // io.deltaTime is irrelevant here 300 301 scrollBar.setScroll(scroll + move); 302 303 } 304 305 }