ScrollBarImpl.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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, U"ScrollUp");
  27. this->scalableImage_scrollBottom = theme_getScalableImage(theme, U"ScrollDown");
  28. this->scalableImage_verticalScrollKnob = theme_getScalableImage(theme, U"VerticalScrollKnob");
  29. this->scalableImage_verticalScrollBackground = theme_getScalableImage(theme, U"VerticalScrollList");
  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. IRect ScrollBarImpl::getScrollBarLocation_excludingButtons(int32_t parentWidth, int32_t parentHeight) {
  59. return this->vertical ? IRect(parentWidth - this->scrollBarThickness, this->scrollButtonLength, this->scrollBarThickness, parentHeight - (this->scrollButtonLength * 2))
  60. : IRect(this->scrollButtonLength, parentHeight - this->scrollBarThickness, parentWidth - (this->scrollButtonLength * 2), this->scrollBarThickness);
  61. }
  62. IRect ScrollBarImpl::getKnobLocation(int32_t parentWidth, int32_t parentHeight) {
  63. // Eroded scroll-bar excluding buttons
  64. // The final knob is a sub-set of this region corresponding to the visibility
  65. IRect scrollBarRegion = this->getScrollBarLocation_excludingButtons(parentWidth, parentHeight);
  66. // The knob should represent the selected range within the total range.
  67. int64_t barLength = this->vertical ? scrollBarRegion.height() : scrollBarRegion.width();
  68. int64_t barThickness = this->vertical ? scrollBarRegion.width() : scrollBarRegion.height();
  69. int64_t knobLength = (barLength * this->scrollRange.visibleItems) / (this->scrollRange.maxValue + this->scrollRange.visibleItems);
  70. if (knobLength < barThickness) {
  71. knobLength = barThickness;
  72. }
  73. // Visual range for center
  74. int64_t scrollStart = (this->vertical ? scrollBarRegion.top() : scrollBarRegion.left()) + knobLength / 2;
  75. int64_t scrollDistance = barLength - knobLength;
  76. int64_t knobStart = scrollStart + ((this->value * scrollDistance) / this->scrollRange.maxValue) - (knobLength / 2);
  77. return this->vertical ? IRect(scrollBarRegion.left(), knobStart, barThickness, knobLength)
  78. : IRect(knobStart, scrollBarRegion.top(), knobLength, barThickness);
  79. }
  80. void ScrollBarImpl::draw(OrderedImageRgbaU8 &target, VisualTheme &theme, const ColorRgbI32 &color) {
  81. if (this->visible) {
  82. int32_t parentWidth = image_getWidth(target);
  83. int32_t parentHeight = image_getHeight(target);
  84. IRect whole = this->vertical ? IRect(parentWidth - this->scrollBarThickness, 0, this->scrollBarThickness, parentHeight)
  85. : IRect(0, parentHeight - this->scrollBarThickness, parentWidth, this->scrollBarThickness);
  86. IRect upper = this->vertical ? IRect(whole.left(), whole.top(), whole.width(), this->scrollButtonLength)
  87. : IRect(whole.left(), whole.top(), this->scrollButtonLength, whole.height());
  88. IRect lower = this->vertical ? IRect(whole.left(), whole.bottom() - this->scrollButtonLength, whole.width(), this->scrollButtonLength)
  89. : IRect(whole.right() - this->scrollButtonLength, whole.top(), this->scrollButtonLength, whole.height());
  90. IRect knob = this->getKnobLocation(parentWidth, parentHeight);
  91. // Only redraw the knob image if its dimensions changed
  92. if (!image_exists(this->scrollKnobImage)
  93. || image_getWidth(this->scrollKnobImage) != knob.width()
  94. || image_getHeight(this->scrollKnobImage) != knob.height()) {
  95. component_generateImage(theme, this->scalableImage_verticalScrollKnob, knob.width(), knob.height(), color.red, color.green, color.blue, 0)(this->scrollKnobImage);
  96. }
  97. // Only redraw the scroll list if its dimenstions changed
  98. if (!image_exists(this->verticalScrollBarImage)
  99. || image_getWidth(this->verticalScrollBarImage) != whole.width()
  100. || image_getHeight(this->verticalScrollBarImage) != whole.height()) {
  101. component_generateImage(theme, this->scalableImage_verticalScrollBackground, whole.width(), whole.height(), color.red, color.green, color.blue, 0)(this->verticalScrollBarImage);
  102. }
  103. // Draw the scroll-bar
  104. draw_alphaFilter(target, this->verticalScrollBarImage, whole.left(), whole.top());
  105. draw_alphaFilter(target, this->scrollKnobImage, knob.left(), knob.top());
  106. draw_alphaFilter(target, this->pressScrollUp ? this->scrollButtonTopImage_pressed : this->scrollButtonTopImage_normal, upper.left(), upper.top());
  107. draw_alphaFilter(target, this->pressScrollDown ? this->scrollButtonBottomImage_pressed : this->scrollButtonBottomImage_normal, lower.left(), lower.top());
  108. }
  109. }
  110. bool ScrollBarImpl::pressScrollBar(const IRect &parentLocation, int64_t localCoordinate) {
  111. int64_t oldValue = this->value;
  112. IRect knobLocation = this->getKnobLocation(parentLocation.width(), parentLocation.height());
  113. int64_t knobLength = this->vertical ? knobLocation.height() : knobLocation.width();
  114. int64_t endDistance = this->scrollButtonLength + knobLength / 2;
  115. int64_t barLength = (this->vertical ? parentLocation.height() : parentLocation.width()) - (endDistance * 2);
  116. this->value = ((localCoordinate - endDistance) * this->scrollRange.maxValue + (barLength / 2)) / barLength;
  117. this->limitScrolling(parentLocation);
  118. // Avoid expensive redrawing if the index did not change
  119. return this->value != oldValue;
  120. }
  121. bool ScrollBarImpl::receiveMouseEvent(const IRect &parentLocation, const MouseEvent& event) {
  122. bool intercepted = false;
  123. IVector2D localPosition = event.position - parentLocation.upperLeft();
  124. int64_t usedCoordinate = (this->vertical ? localPosition.y : localPosition.x);
  125. bool onScrollBar = this->visible && (this->vertical ? (localPosition.x >= parentLocation.width() - scrollBarThickness) : (localPosition.y >= parentLocation.height() - scrollBarThickness));
  126. if (event.mouseEventType == MouseEventType::MouseDown) {
  127. if (onScrollBar) {
  128. intercepted = true;
  129. if (this->vertical) {
  130. if (localPosition.y < this->scrollButtonLength) {
  131. // Upper scroll button
  132. this->pressScrollUp = true;
  133. this->value--;
  134. } else if (localPosition.y > parentLocation.height() - this->scrollButtonLength) {
  135. // Lower scroll button
  136. this->pressScrollDown = true;
  137. this->value++;
  138. } else {
  139. // Start scrolling with the mouse using the relative height on the scroll bar.
  140. IRect knobLocation = this->getKnobLocation(parentLocation.width(), parentLocation.height());
  141. int64_t halfKnobLength = knobLocation.height() / 2;
  142. this->knobHoldOffset = localPosition.y - (knobLocation.top() + halfKnobLength);
  143. if (this->knobHoldOffset < -halfKnobLength || this->knobHoldOffset > halfKnobLength) {
  144. // If pressing outside of the knob, pull it directly to the pressed location before pulling from the center.
  145. this->knobHoldOffset = 0;
  146. this->pressScrollBar(parentLocation, usedCoordinate - this->knobHoldOffset);
  147. }
  148. this->holdingScrollBar = true;
  149. }
  150. } else {
  151. if (localPosition.x < this->scrollButtonLength) {
  152. // Upper scroll button
  153. this->pressScrollUp = true;
  154. this->value--;
  155. } else if (localPosition.x > parentLocation.width() - this->scrollButtonLength) {
  156. // Lower scroll button
  157. this->pressScrollDown = true;
  158. this->value++;
  159. } else {
  160. // Start scrolling with the mouse using the relative width on the scroll bar.
  161. IRect knobLocation = this->getKnobLocation(parentLocation.width(), parentLocation.height());
  162. int64_t halfKnobLength = knobLocation.width() / 2;
  163. this->knobHoldOffset = localPosition.x - (knobLocation.width() + halfKnobLength);
  164. if (this->knobHoldOffset < -halfKnobLength || this->knobHoldOffset > halfKnobLength) {
  165. // If pressing outside of the knob, pull it directly to the pressed location before pulling from the center.
  166. this->knobHoldOffset = 0;
  167. this->pressScrollBar(parentLocation, usedCoordinate - this->knobHoldOffset);
  168. }
  169. this->holdingScrollBar = true;
  170. }
  171. }
  172. }
  173. this->limitScrolling(parentLocation);
  174. } else if (event.mouseEventType == MouseEventType::MouseUp) {
  175. this->pressScrollUp = false;
  176. this->pressScrollDown = false;
  177. this->holdingScrollBar = false;
  178. intercepted = true;
  179. } else if (event.mouseEventType == MouseEventType::Scroll) {
  180. if (this->vertical) {
  181. if (event.key == MouseKeyEnum::ScrollUp) {
  182. this->value--;
  183. } else if (event.key == MouseKeyEnum::ScrollDown) {
  184. this->value++;
  185. }
  186. this->limitScrolling(parentLocation);
  187. }
  188. this->holdingScrollBar = false;
  189. intercepted = true;
  190. } else if (event.mouseEventType == MouseEventType::MouseMove) {
  191. if (this->holdingScrollBar) {
  192. intercepted = this->pressScrollBar(parentLocation, usedCoordinate - this->knobHoldOffset);
  193. }
  194. }
  195. return intercepted;
  196. }