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 import fluid.container; 16 17 public import fluid.scroll_input; 18 19 20 @safe: 21 22 23 alias ScrollFrame = Scrollable!Frame; 24 alias Scrollable(T : Space) = Scrollable!(T, "directionHorizontal"); 25 26 /// Create a new vertical scroll frame. 27 alias vscrollFrame = simpleConstructor!ScrollFrame; 28 29 /// Create a new horizontal scroll frame. 30 alias hscrollFrame = simpleConstructor!(ScrollFrame, (a) { 31 32 a.directionHorizontal = true; 33 34 }); 35 36 /// Create a new scrollable node. 37 alias vscrollable(alias T) = simpleConstructor!(ApplyRight!(ScrollFrame, "false"), T); 38 39 /// Create a new horizontally scrollable node. 40 alias hscrollable(alias T) = simpleConstructor!(ApplyRight!(ScrollFrame, "true"), T); 41 42 /// Implement scrolling for the given node. 43 /// 44 /// This only supports scrolling in one axis. 45 class Scrollable(T : Node, string horizontalExpression) : T { 46 47 mixin DefineStyles; 48 49 public { 50 51 /// Scrollbar for the frame. Can be replaced with a customized one. 52 ScrollInput scrollBar; 53 54 } 55 56 private { 57 58 /// minSize including the padding. 59 Vector2 paddingBoxSize; 60 61 } 62 63 this(T...)(T args) { 64 65 super(args); 66 this.scrollBar = .vscrollInput(); 67 68 } 69 70 /// Distance the node is scrolled by. 71 @property 72 ref inout(size_t) scroll() inout { return scrollBar.position; } 73 74 /// Check if the underlying node is horizontal. 75 private bool isHorizontal() const { 76 77 return mixin(horizontalExpression); 78 79 } 80 81 /// Scroll to the beginning of the node. 82 void scrollStart() { 83 84 scroll = 0; 85 86 } 87 88 /// Scroll to the end of the node, requires the node to be drawn at least once. 89 void scrollEnd() { 90 91 scroll = scrollMax; 92 93 } 94 95 /// Set the scroll to a value clamped between start and end. 96 void setScroll(ptrdiff_t value) { 97 98 scrollBar.setScroll(value); 99 100 } 101 102 /// Get the maximum value this container can be scrolled to. Requires at least one draw. 103 size_t scrollMax() const { 104 105 return scrollBar.scrollMax(); 106 107 } 108 109 static if (is(typeof(this) : FluidContainer)) 110 override Rectangle shallowScrollTo(const Node, Vector2, Rectangle parentBox, Rectangle childBox) { 111 112 struct Position { 113 114 float* start; 115 float end; 116 float viewportStart, viewportEnd; 117 118 } 119 120 // Get the data for the node 121 scope position = isHorizontal 122 ? Position( 123 &childBox.x, childBox.x + childBox.width, 124 parentBox.x, parentBox.x + parentBox.width 125 ) 126 : Position( 127 &childBox.y, childBox.y + childBox.height, 128 parentBox.y, parentBox.y + parentBox.height 129 ); 130 131 auto scrollBefore = scroll(); 132 133 // Calculate the offset 134 auto offset 135 136 // Need to scroll towards the end 137 = *position.start > position.viewportStart && position.end > position.viewportEnd 138 ? to!ptrdiff_t(position.end - position.viewportEnd) 139 140 // Need to scroll towards the start 141 : *position.start < position.viewportStart && position.end < position.viewportEnd 142 ? to!ptrdiff_t(*position.start - position.viewportStart) 143 144 // Already in viewport 145 : 0; 146 147 // Perform the scroll 148 setScroll(scroll.to!ptrdiff_t + offset); 149 150 // Adjust the offset 151 offset = scroll.to!ptrdiff_t - scrollBefore; 152 153 // Apply child position 154 *position.start += offset; 155 156 return childBox; 157 158 } 159 160 override void resizeImpl(Vector2 space) { 161 162 assert(scrollBar !is null, "No scrollbar has been set for FluidScrollable"); 163 assert(theme !is null); 164 assert(tree !is null); 165 166 /// Padding represented as a vector. This sums the padding on each axis. 167 const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum); 168 169 /// Space with padding included 170 const paddingSpace = space + paddingVector; 171 172 // Resize the scrollbar 173 with (scrollBar) { 174 175 horizontal = isHorizontal; 176 layout = .layout!(1, "fill"); 177 resize(this.tree, this.theme, paddingSpace); 178 179 } 180 181 /// Space without the scrollbar 182 const contentSpace = isHorizontal 183 ? space - Vector2(0, scrollBar.minSize.y) 184 : space - Vector2(scrollBar.minSize.x, 0); 185 186 // Resize the frame while reserving some space for the scrollbar 187 super.resizeImpl(contentSpace); 188 189 // Calculate the expected padding box size 190 paddingBoxSize = minSize + paddingVector; 191 192 // Set scrollbar size and add the scrollbar to the result 193 if (isHorizontal) { 194 195 scrollBar.availableSpace = cast(size_t) paddingBoxSize.x; 196 minSize.y += scrollBar.minSize.y; 197 198 } 199 200 else { 201 202 scrollBar.availableSpace = cast(size_t) paddingBoxSize.y; 203 minSize.x += scrollBar.minSize.x; 204 205 } 206 207 } 208 209 override void drawImpl(Rectangle outer, Rectangle inner) { 210 211 // Note: Mouse input detection is primitive, awaiting #13 and #14 to help better identify when should the mouse 212 // affect this frame. 213 214 // This node doesn't use InputNode because it doesn't take focus, and we don't want to cause related 215 // accessibility issues. It can function perfectly without it, or at least until above note gets fixed. 216 // Then, a "FluidHoverable" interface could possibly become a thing. 217 218 // TODO Is the above still true? 219 220 scrollBar.horizontal = isHorizontal; 221 222 auto scrollBarRect = outer; 223 224 if (isHovered) inputImpl(); 225 226 // Scroll the given rectangle horizontally 227 if (isHorizontal) { 228 229 // Calculate fake box sizes 230 outer.width = max(outer.width, paddingBoxSize.x); 231 inner = style.contentBox(outer); 232 233 static foreach (rect; AliasSeq!(outer, inner)) { 234 235 // Perform the scroll 236 rect.x -= scroll; 237 238 // Reduce both rects by scrollbar size 239 rect.height -= scrollBar.minSize.y; 240 241 } 242 243 scrollBarRect.y += outer.height; 244 scrollBarRect.height = scrollBar.minSize.y; 245 246 } 247 248 // Vertically 249 else { 250 251 // Calculate fake box sizes 252 outer.height = max(outer.height, paddingBoxSize.y); 253 inner = style.contentBox(outer); 254 255 static foreach (rect; AliasSeq!(outer, inner)) { 256 257 // Perform the scroll 258 rect.y -= scroll; 259 260 // Reduce both rects by scrollbar size 261 rect.width -= scrollBar.minSize.x; 262 263 } 264 265 scrollBarRect.x += outer.width; 266 scrollBarRect.width = scrollBar.minSize.x; 267 268 } 269 270 // Draw the scrollbar 271 scrollBar.draw(scrollBarRect); 272 273 // Draw the frame 274 super.drawImpl(outer, inner); 275 276 } 277 278 /// Implementation of mouse input 279 private void inputImpl() @trusted { 280 281 // TODO do this via input actions somehow https://git.samerion.com/Samerion/Fluid/issues/89 282 const speed = scrollBar.scrollSpeed; 283 const value = isHorizontal 284 ? io.scroll.x 285 : io.scroll.y; 286 const move = speed * value; 287 // io.deltaTime is irrelevant here 288 289 // TODO NO ptrdiff_t 290 scrollBar.setScroll(scroll.to!ptrdiff_t + move.to!ptrdiff_t); 291 292 } 293 294 }