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 // TODO: move keyboard input to FluidScrollBar. 50 51 public { 52 53 /// Scrollbar for the frame. Can be replaced with a customized one. 54 ScrollInput scrollBar; 55 56 } 57 58 private { 59 60 /// minSize including the padding. 61 Vector2 paddingBoxSize; 62 63 } 64 65 this(T...)(T args) { 66 67 super(args); 68 this.scrollBar = .vscrollInput(); 69 70 } 71 72 /// Distance the node is scrolled by. 73 @property 74 ref inout(size_t) scroll() inout { return scrollBar.position; } 75 76 /// Check if the underlying node is horizontal. 77 private bool isHorizontal() const { 78 79 return mixin(horizontalExpression); 80 81 } 82 83 /// Scroll to the beginning of the node. 84 void scrollStart() { 85 86 scroll = 0; 87 88 } 89 90 /// Scroll to the end of the node, requires the node to be drawn at least once. 91 void scrollEnd() { 92 93 scroll = scrollMax; 94 95 } 96 97 /// Set the scroll to a value clamped between start and end. 98 void setScroll(ptrdiff_t value) { 99 100 scrollBar.setScroll(value); 101 102 } 103 104 /// Get the maximum value this container can be scrolled to. Requires at least one draw. 105 size_t scrollMax() const { 106 107 return scrollBar.scrollMax(); 108 109 } 110 111 static if (is(typeof(this) : FluidContainer)) 112 override Rectangle shallowScrollTo(const Node, Vector2, Rectangle parentBox, Rectangle childBox) { 113 114 struct Position { 115 116 float* start; 117 float end; 118 float viewportStart, viewportEnd; 119 120 } 121 122 // Get the data for the node 123 scope position = isHorizontal 124 ? Position( 125 &childBox.x, childBox.x + childBox.width, 126 parentBox.x, parentBox.x + parentBox.width 127 ) 128 : Position( 129 &childBox.y, childBox.y + childBox.height, 130 parentBox.y, parentBox.y + parentBox.height 131 ); 132 133 auto scrollBefore = scroll(); 134 135 // Calculate the offset 136 auto offset 137 138 // Need to scroll towards the end 139 = *position.start > position.viewportStart && position.end > position.viewportEnd 140 ? to!ptrdiff_t(position.end - position.viewportEnd) 141 142 // Need to scroll towards the start 143 : *position.start < position.viewportStart && position.end < position.viewportEnd 144 ? to!ptrdiff_t(*position.start - position.viewportStart) 145 146 // Already in viewport 147 : 0; 148 149 // Perform the scroll 150 setScroll(scroll.to!ptrdiff_t + offset); 151 152 // Adjust the offset 153 offset = scroll.to!ptrdiff_t - scrollBefore; 154 155 // Apply child position 156 *position.start += offset; 157 158 return childBox; 159 160 } 161 162 override void resizeImpl(Vector2 space) { 163 164 assert(scrollBar !is null, "No scrollbar has been set for FluidScrollable"); 165 assert(theme !is null); 166 assert(tree !is null); 167 168 /// Padding represented as a vector. This sums the padding on each axis. 169 const paddingVector = Vector2(style.padding.sideX[].sum, style.padding.sideY[].sum); 170 171 /// Space with padding included 172 const paddingSpace = space + paddingVector; 173 174 // Resize the scrollbar 175 with (scrollBar) { 176 177 horizontal = isHorizontal; 178 layout = .layout!(1, "fill"); 179 resize(this.tree, this.theme, paddingSpace); 180 181 } 182 183 /// Space without the scrollbar 184 const contentSpace = isHorizontal 185 ? space - Vector2(0, scrollBar.minSize.y) 186 : space - Vector2(scrollBar.minSize.x, 0); 187 188 // Resize the frame while reserving some space for the scrollbar 189 super.resizeImpl(contentSpace); 190 191 // Calculate the expected padding box size 192 paddingBoxSize = minSize + paddingVector; 193 194 // Set scrollbar size and add the scrollbar to the result 195 if (isHorizontal) { 196 197 scrollBar.availableSpace = cast(size_t) paddingBoxSize.x; 198 minSize.y += scrollBar.minSize.y; 199 200 } 201 202 else { 203 204 scrollBar.availableSpace = cast(size_t) paddingBoxSize.y; 205 minSize.x += scrollBar.minSize.x; 206 207 } 208 209 } 210 211 override void drawImpl(Rectangle outer, Rectangle inner) { 212 213 // Note: Mouse input detection is primitive, awaiting #13 and #14 to help better identify when should the mouse 214 // affect this frame. 215 216 // This node doesn't use InputNode because it doesn't take focus, and we don't want to cause related 217 // accessibility issues. It can function perfectly without it, or at least until above note gets fixed. 218 // Then, a "FluidHoverable" interface could possibly become a thing. 219 220 // TODO Is the above still true? 221 222 scrollBar.horizontal = isHorizontal; 223 224 auto scrollBarRect = outer; 225 226 if (isHovered) inputImpl(); 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 /// Implementation of mouse input 281 private void inputImpl() @trusted { 282 283 // TODO do this via input actions somehow https://git.samerion.com/Samerion/Fluid/issues/89 284 const isPlus = isHorizontal 285 ? io.isReleased(MouseButton.scrollRight) 286 : io.isReleased(MouseButton.scrollDown); 287 const isMinus = isHorizontal 288 ? io.isReleased(MouseButton.scrollLeft) 289 : io.isReleased(MouseButton.scrollUp); 290 291 const speed = scrollBar.scrollSpeed; 292 const move 293 = isPlus ? +speed 294 : isMinus ? -speed 295 : 0; 296 297 scrollBar.setScroll(scroll.to!ptrdiff_t + move.to!ptrdiff_t); 298 299 } 300 301 }