1 module glui.scrollbar; 2 3 import raylib; 4 5 import std.algorithm; 6 7 import glui.node; 8 import glui.utils; 9 import glui.input; 10 import glui.style; 11 12 alias vscrollBar = simpleConstructor!GluiScrollBar; 13 14 @safe: 15 16 GluiScrollBar hscrollBar(Args...)(Args args) { 17 18 auto bar = vscrollBar(args); 19 bar.horizontal = true; 20 return bar; 21 22 } 23 24 /// 25 class GluiScrollBar : GluiInput!GluiNode { 26 27 /// Styles defined by this node: 28 /// 29 /// `backgroundStyle` — style defined for the background part of the scrollbar, 30 /// 31 /// `pressStyle` — style to activate while the scrollbar is pressed. 32 mixin DefineStyles!( 33 "backgroundStyle", q{ Style.init }, 34 "pressStyle", q{ style }, 35 ); 36 37 mixin ImplHoveredRect; 38 39 public { 40 41 /// If true, the scrollbar will be horizontal. 42 bool horizontal; 43 44 /// Amount of pixel the page is scrolled down. 45 size_t position; 46 47 /// Available space to scroll. 48 /// 49 /// Note: page length, and therefore scrollbar handle length, are determined from the space occupied by the 50 /// scrollbar. 51 size_t availableSpace; 52 53 /// Multipler of the scroll speed; applies to keyboard scroll only. 54 /// 55 /// This is actually number of pixels per mouse wheel event, as `GluiScrollable` determines mouse scroll speed 56 /// based on this. 57 enum scrollSpeed = 15.0; 58 59 } 60 61 protected { 62 63 /// If true, the inner part of the scrollbar is hovered. 64 bool innerHovered; 65 66 /// Length of the scrollbar as determined in drawImpl. 67 double scrollbarLength; 68 69 /// Length of the handle. 70 double handleLength; 71 72 /// Position of the scrollbar on the screen. 73 Vector2 scrollbarPosition; 74 75 /// Position where the mouse grabbed the scrollbar. 76 Vector2 grabPosition; 77 78 /// Start position of the mouse at the beginning of the grab. 79 size_t startPosition; 80 81 } 82 83 this(Args...)(Args args) { 84 85 super(args); 86 87 } 88 89 /// Set the scroll to a value clamped between start and end. 90 void setScroll(ptrdiff_t value) { 91 92 position = cast(size_t) value.clamp(0, scrollMax); 93 94 } 95 96 /// Ditto 97 void setScroll(float value) { 98 99 position = cast(size_t) value.clamp(0, scrollMax); 100 101 } 102 103 /// Get the maximum value this container can be scrolled to. Requires at least one draw. 104 size_t scrollMax() const { 105 106 return cast(size_t) max(0, availableSpace - scrollbarLength); 107 108 } 109 110 /// Set the total size of the scrollbar. Will always fill the available space in the target direction. 111 override protected void resizeImpl(Vector2 space) { 112 113 minSize = horizontal 114 ? Vector2(space.x, 10) 115 : Vector2(10, space.y); 116 117 } 118 119 override protected void drawImpl(Rectangle paddingBox, Rectangle contentBox) @trusted { 120 121 setScroll(position); 122 123 // Draw the background 124 backgroundStyle.drawBackground(paddingBox); 125 126 // Calculate the size of the scrollbar 127 scrollbarPosition = Vector2(contentBox.x, contentBox.y); 128 scrollbarLength = horizontal ? contentBox.width : contentBox.height; 129 handleLength = availableSpace 130 ? max(50, scrollbarLength^^2 / availableSpace) 131 : 0; 132 133 const handlePosition = (scrollbarLength - handleLength) * position / scrollMax; 134 135 // Now get the size of the inner rect 136 auto innerRect = contentBox; 137 138 if (horizontal) { 139 140 innerRect.x += handlePosition; 141 innerRect.w = handleLength; 142 143 } 144 145 else { 146 147 innerRect.y += handlePosition; 148 innerRect.h = handleLength; 149 150 } 151 152 // Check if the inner part is hovered 153 innerHovered = innerRect.contains(GetMousePosition); 154 155 // Get the inner style 156 const innerStyle = pickStyle(); 157 158 innerStyle.drawBackground(innerRect); 159 160 } 161 162 override protected const(Style) pickStyle() const { 163 164 const up = super.pickStyle(); 165 166 // The outer part is being hovered... 167 if (up is hoverStyle) { 168 169 // Check if the inner part is 170 return innerHovered 171 ? hoverStyle 172 : style; 173 174 } 175 176 return up; 177 178 } 179 180 override protected void mouseImpl() @trusted { 181 182 const triggerButton = MouseButton.MOUSE_LEFT_BUTTON; 183 184 // Ignore if we can't scroll 185 if (availableSpace == 0) return; 186 187 // Pressed the scrollbar 188 if (IsMouseButtonPressed(triggerButton)) { 189 190 // Remember the grab position 191 grabPosition = GetMousePosition; 192 scope (exit) startPosition = position; 193 194 // Didn't press the handle 195 if (!innerHovered) { 196 197 // Get the position 198 const posdir = horizontal ? scrollbarPosition.x : scrollbarPosition.y; 199 const grabdir = horizontal ? grabPosition.x : grabPosition.y; 200 const screenPos = grabdir - posdir - handleLength/2; 201 202 // Move it to this position 203 setScroll(screenPos * availableSpace / scrollbarLength); 204 205 } 206 207 } 208 209 // Mouse is held down 210 else if (IsMouseButtonDown(triggerButton)) { 211 212 const mouse = GetMousePosition; 213 214 const float move = horizontal 215 ? mouse.x - grabPosition.x 216 : mouse.y - grabPosition.y; 217 218 // Move the scrollbar 219 setScroll(startPosition + move * availableSpace / scrollbarLength); 220 221 } 222 223 } 224 225 override protected bool keyboardImpl() @trusted { 226 227 const plusKey = horizontal 228 ? KeyboardKey.KEY_RIGHT 229 : KeyboardKey.KEY_DOWN; 230 const minusKey = horizontal 231 ? KeyboardKey.KEY_LEFT 232 : KeyboardKey.KEY_UP; 233 234 const arrowSpeed = scrollSpeed * 20 * GetFrameTime; 235 const pageSpeed = scrollbarLength * 3/4; 236 237 const move = IsKeyPressed(KeyboardKey.KEY_PAGE_DOWN) ? +pageSpeed 238 : IsKeyPressed(KeyboardKey.KEY_PAGE_UP) ? -pageSpeed 239 : IsKeyDown(plusKey) ? +arrowSpeed 240 : IsKeyDown(minusKey) ? -arrowSpeed 241 : 0; 242 243 if (move != 0) { 244 245 setScroll(position + move); 246 247 if (changed) changed(); 248 249 return true; 250 251 } 252 253 return false; 254 255 } 256 257 }