ScrollBarImpl.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // zlib open source license
  2. //
  3. // Copyright (c) 2020 to 2022 David Forsgren Piuva
  4. //
  5. // This software is provided 'as-is', without any express or implied
  6. // warranty. In no event will the authors be held liable for any damages
  7. // arising from the use of this software.
  8. //
  9. // Permission is granted to anyone to use this software for any purpose,
  10. // including commercial applications, and to alter it and redistribute it
  11. // freely, subject to the following restrictions:
  12. //
  13. // 1. The origin of this software must not be misrepresented; you must not
  14. // claim that you wrote the original software. If you use this software
  15. // in a product, an acknowledgment in the product documentation would be
  16. // appreciated but is not required.
  17. //
  18. // 2. Altered source versions must be plainly marked as such, and must not be
  19. // misrepresented as being the original software.
  20. //
  21. // 3. This notice may not be removed or altered from any source
  22. // distribution.
  23. #include "ScrollBarImpl.h"
  24. using namespace dsr;
  25. void ScrollBarImpl::loadTheme(VisualTheme theme, const ColorRgbI32 &color) {
  26. this->scalableImage_scrollTop = theme_getScalableImage(theme, this->vertical ? U"ScrollUp" : U"ScrollLeft");
  27. this->scalableImage_scrollBottom = theme_getScalableImage(theme, this->vertical ? U"ScrollDown" : U"ScrollRight");
  28. this->scalableImage_scrollKnob = theme_getScalableImage(theme, this->vertical ? U"VerticalScrollKnob" : U"HorizontalScrollKnob");
  29. this->scalableImage_scrollBackground = theme_getScalableImage(theme, this->vertical ? U"VerticalScrollList" : U"HorizontalScrollList");
  30. component_generateImage(theme, this->scalableImage_scrollTop, this->scrollBarThickness, this->scrollButtonLength, color.red, color.green, color.blue, 0)(this->scrollButtonTopImage_normal);
  31. component_generateImage(theme, this->scalableImage_scrollTop, this->scrollBarThickness, this->scrollButtonLength, color.red, color.green, color.blue, 1)(this->scrollButtonTopImage_pressed);
  32. component_generateImage(theme, this->scalableImage_scrollBottom, this->scrollBarThickness, this->scrollButtonLength, color.red, color.green, color.blue, 0)(this->scrollButtonBottomImage_normal);
  33. component_generateImage(theme, this->scalableImage_scrollBottom, this->scrollBarThickness, this->scrollButtonLength, color.red, color.green, color.blue, 1)(this->scrollButtonBottomImage_pressed);
  34. }
  35. void ScrollBarImpl::updateScrollRange(const ScrollRange &range) {
  36. this->scrollRange = range;
  37. }
  38. void ScrollBarImpl::setValue(int64_t newValue) {
  39. this->value = newValue;
  40. }
  41. int64_t ScrollBarImpl::getValue() {
  42. return this->value;
  43. }
  44. void ScrollBarImpl::limitScrolling(const IRect &parentLocation, bool keepPinValueInRange, int64_t pinValue) {
  45. // Big enough list to need scrolling but big enough list-box to fit two buttons inside
  46. this->visible = this->scrollRange.minValue < this->scrollRange.maxValue && parentLocation.width() >= scrollBarThickness * 2 && parentLocation.height() >= this->scrollButtonLength * 3;
  47. if (keepPinValueInRange) {
  48. // Constrain to keep pinValue in range.
  49. int64_t maxScroll = pinValue;
  50. int64_t minScroll = pinValue + 1 - this->scrollRange.visibleItems;
  51. if (this->value > maxScroll) this->value = maxScroll;
  52. if (this->value < minScroll) this->value = minScroll;
  53. }
  54. // Constrain value to be within the inclusive minValue..maxValue interval, in case that pinValue is out of bound.
  55. if (this->value > scrollRange.maxValue) this->value = scrollRange.maxValue;
  56. if (this->value < scrollRange.minValue) this->value = scrollRange.minValue;
  57. }
  58. // Fill from the right side if vertical and bottom if horizontal.
  59. static IRect getWallSide(int32_t parentWidth, int32_t parentHeight, int32_t thickness, bool vertical) {
  60. return vertical ? IRect(std::max(1, parentWidth - thickness), 0, thickness, parentHeight)
  61. : IRect(0, std::max(1, parentHeight - thickness), parentWidth, thickness);
  62. }
  63. // Get the upper part if vertical and left part if horizontal.
  64. static IRect getStartRect(const IRect &original, int32_t startLength, bool vertical) {
  65. return vertical ? IRect(original.left(), original.top(), original.width(), startLength)
  66. : IRect(original.left(), original.top(), startLength, original.height());
  67. }
  68. // Get the bottom part if vertical and right part if horizontal.
  69. static IRect getEndRect(const IRect &original, int32_t endLength, bool vertical) {
  70. return vertical ? IRect(original.left(), original.bottom() - endLength, original.width(), endLength)
  71. : IRect(original.right() - endLength, original.top(), endLength, original.height());
  72. }
  73. static IRect getMiddleRect(const IRect &original, int32_t startCropping, int32_t endCropping, bool vertical) {
  74. return vertical ? IRect(original.left(), original.top() + startCropping, original.width(), std::max(1, original.height() - startCropping - endCropping))
  75. : IRect(original.left() + startCropping, original.top(), std::max(1, original.width() - startCropping - endCropping), original.height());
  76. }
  77. static int32_t getStart(const IRect &rect, bool vertical) {
  78. return vertical ? rect.top() : rect.left();
  79. }
  80. static int32_t getEnd(const IRect &rect, bool vertical) {
  81. return vertical ? rect.bottom() : rect.right();
  82. }
  83. static int32_t getLength(const IRect &rect, bool vertical) {
  84. return vertical ? rect.height() : rect.width();
  85. }
  86. static int32_t getThickness(const IRect &rect, bool vertical) {
  87. return vertical ? rect.width() : rect.height();
  88. }
  89. IRect ScrollBarImpl::getScrollBarLocation(int32_t parentWidth, int32_t parentHeight) {
  90. IRect whole = getWallSide(parentWidth, parentHeight, this->scrollBarThickness, this->vertical);
  91. return getMiddleRect(whole, reservedStart, reservedEnd, this->vertical);
  92. }
  93. IRect ScrollBarImpl::getScrollRegion(const IRect &scrollBarLocation) {
  94. return getMiddleRect(scrollBarLocation, this->scrollButtonLength, this->scrollButtonLength, this->vertical);
  95. }
  96. IRect ScrollBarImpl::getDecreaseButton(const IRect &scrollBarLocation) {
  97. return getStartRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
  98. }
  99. IRect ScrollBarImpl::getIncreaseButton(const IRect &scrollBarLocation) {
  100. return getEndRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
  101. }
  102. IRect ScrollBarImpl::getKnobLocation(const IRect &scrollBarLocation) {
  103. // Eroded scroll-bar excluding buttons
  104. // The final knob is a sub-set of this region corresponding to the visibility
  105. IRect scrollRegion = getMiddleRect(scrollBarLocation, this->scrollButtonLength, this->scrollButtonLength, this->vertical);
  106. // The knob should represent the selected range within the total range.
  107. int64_t barLength = getLength(scrollRegion, this->vertical);
  108. int64_t barThickness = getThickness(scrollRegion, this->vertical);
  109. int64_t knobLength = (barLength * this->scrollRange.visibleItems) / (this->scrollRange.maxValue + this->scrollRange.visibleItems);
  110. if (knobLength < barThickness) {
  111. knobLength = barThickness;
  112. }
  113. // Visual range for center
  114. int64_t scrollStart = (this->vertical ? scrollRegion.top() : scrollRegion.left()) + knobLength / 2;
  115. int64_t scrollDistance = barLength - knobLength;
  116. int64_t knobStart = scrollStart + ((this->value * scrollDistance) / this->scrollRange.maxValue) - (knobLength / 2);
  117. return this->vertical ? IRect(scrollRegion.left(), knobStart, barThickness, knobLength)
  118. : IRect(knobStart, scrollRegion.top(), knobLength, barThickness);
  119. }
  120. void ScrollBarImpl::draw(OrderedImageRgbaU8 &target, VisualTheme &theme, const ColorRgbI32 &color) {
  121. if (this->visible) {
  122. int32_t parentWidth = image_getWidth(target);
  123. int32_t parentHeight = image_getHeight(target);
  124. IRect scrollBarLocation = this->getScrollBarLocation(parentWidth, parentHeight);
  125. IRect upper = getStartRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
  126. IRect lower = getEndRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
  127. IRect knob = this->getKnobLocation(scrollBarLocation);
  128. // Only redraw the knob image if its dimensions changed
  129. if (!image_exists(this->scrollKnobImage_normal)
  130. || image_getWidth(this->scrollKnobImage_normal) != knob.width()
  131. || image_getHeight(this->scrollKnobImage_normal) != knob.height()) {
  132. component_generateImage(theme, this->scalableImage_scrollKnob, knob.width(), knob.height(), color.red, color.green, color.blue, 0)(this->scrollKnobImage_normal);
  133. component_generateImage(theme, this->scalableImage_scrollKnob, knob.width(), knob.height(), color.red, color.green, color.blue, 1)(this->scrollKnobImage_pressed);
  134. }
  135. // Only redraw the scroll list if its dimenstions changed
  136. if (!image_exists(this->scrollBarImage)
  137. || image_getWidth(this->scrollBarImage) != scrollBarLocation.width()
  138. || image_getHeight(this->scrollBarImage) != scrollBarLocation.height()) {
  139. component_generateImage(theme, this->scalableImage_scrollBackground, scrollBarLocation.width(), scrollBarLocation.height(), color.red, color.green, color.blue, 0)(this->scrollBarImage);
  140. }
  141. // Draw the scroll-bar
  142. draw_alphaFilter(target, this->scrollBarImage, scrollBarLocation.left(), scrollBarLocation.top());
  143. draw_alphaFilter(target, this->holdingScrollBar ? this->scrollKnobImage_pressed : this->scrollKnobImage_normal, knob.left(), knob.top());
  144. draw_alphaFilter(target, this->pressScrollUp ? this->scrollButtonTopImage_pressed : this->scrollButtonTopImage_normal, upper.left(), upper.top());
  145. draw_alphaFilter(target, this->pressScrollDown ? this->scrollButtonBottomImage_pressed : this->scrollButtonBottomImage_normal, lower.left(), lower.top());
  146. }
  147. }
  148. bool ScrollBarImpl::pressScrollBar(const IRect &parentLocation, int64_t localCoordinate) {
  149. int64_t oldValue = this->value;
  150. IRect scrollBarLocation = this->getScrollBarLocation(parentLocation.width(), parentLocation.height());
  151. IRect scrollRegion = this->getScrollRegion(scrollBarLocation);
  152. IRect knobLocation = this->getKnobLocation(scrollBarLocation);
  153. int64_t knobLength = getLength(knobLocation, this->vertical);
  154. int64_t minimumAt = getStart(scrollRegion, this->vertical) + knobLength / 2;
  155. int64_t maximumAt = getEnd(scrollRegion, this->vertical) - knobLength / 2;
  156. int64_t pixelRange = maximumAt - minimumAt;
  157. int64_t valueRange = this->scrollRange.maxValue - this->scrollRange.minValue;
  158. this->value = this->scrollRange.minValue;
  159. if (pixelRange > 0) {
  160. this->value += ((localCoordinate - minimumAt) * valueRange + pixelRange / 2) / pixelRange;
  161. }
  162. this->limitScrolling(parentLocation);
  163. // Avoid expensive redrawing if the index did not change
  164. return this->value != oldValue;
  165. }
  166. bool ScrollBarImpl::receiveMouseEvent(const IRect &parentLocation, const MouseEvent& event) {
  167. bool intercepted = false;
  168. IVector2D localPosition = event.position - parentLocation.upperLeft();
  169. IRect scrollBarLocation = this->getScrollBarLocation(parentLocation.width(), parentLocation.height());
  170. IRect cursorLocation = IRect(localPosition.x, localPosition.y, 1, 1);
  171. int64_t usedCoordinate = (this->vertical ? localPosition.y : localPosition.x);
  172. if (event.mouseEventType == MouseEventType::MouseDown) {
  173. if (IRect::touches(scrollBarLocation, cursorLocation)) {
  174. IRect upperLocation = getStartRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
  175. IRect lowerLocation = getEndRect(scrollBarLocation, this->scrollButtonLength, this->vertical);
  176. IRect knobLocation = this->getKnobLocation(scrollBarLocation);
  177. intercepted = true;
  178. if (IRect::touches(upperLocation, cursorLocation)) {
  179. // Upper scroll button
  180. this->pressScrollUp = true;
  181. this->value--;
  182. } else if (IRect::touches(lowerLocation, cursorLocation)) {
  183. // Lower scroll button
  184. this->pressScrollDown = true;
  185. this->value++;
  186. } else {
  187. // Start scrolling with the mouse using the relative height on the scroll bar.
  188. IRect knobLocation = this->getKnobLocation(scrollBarLocation);
  189. int64_t halfKnobLength = getLength(knobLocation, this->vertical) / 2;
  190. this->knobHoldOffset = usedCoordinate - (getStart(knobLocation, this->vertical) + halfKnobLength);
  191. if (this->knobHoldOffset < -halfKnobLength || this->knobHoldOffset > halfKnobLength) {
  192. // If pressing outside of the knob, pull it directly to the pressed location before pulling from the center.
  193. this->knobHoldOffset = 0;
  194. this->pressScrollBar(parentLocation, usedCoordinate - this->knobHoldOffset);
  195. }
  196. this->holdingScrollBar = true;
  197. }
  198. }
  199. this->limitScrolling(parentLocation);
  200. } else if (event.mouseEventType == MouseEventType::MouseUp) {
  201. this->pressScrollUp = false;
  202. this->pressScrollDown = false;
  203. this->holdingScrollBar = false;
  204. intercepted = true;
  205. } else if (event.mouseEventType == MouseEventType::Scroll) {
  206. if (this->vertical) {
  207. if (event.key == MouseKeyEnum::ScrollUp) {
  208. this->value--;
  209. } else if (event.key == MouseKeyEnum::ScrollDown) {
  210. this->value++;
  211. }
  212. this->limitScrolling(parentLocation);
  213. }
  214. this->holdingScrollBar = false;
  215. intercepted = true;
  216. } else if (event.mouseEventType == MouseEventType::MouseMove) {
  217. if (this->holdingScrollBar) {
  218. intercepted = this->pressScrollBar(parentLocation, usedCoordinate - this->knobHoldOffset);
  219. }
  220. }
  221. return intercepted;
  222. }