GuiElement#direction | string [optional] Slider orientation, defaults to "x". Valid values: "x" for horizontal, "y" for vertical. |
GuiElement#hasButtons | bool [optional] If false, will not look for buttons in child elements of this slider. |
GuiElement#minValue | float [optional] Minimum slider value when at the top or left, defaults to 0. |
GuiElement#maxValue | float [optional] Maximum slider value when at the bottom or right, defaults to 100. |
GuiElement#currentValue | float [optional] Initial slider value, defaults to 0. Should be set to fall within the range defined by #minValue and #maxValue. |
GuiElement#stepSize | float [optional] Step size which all newly set values will be rounded to, defaults to 1. |
GuiElement#sliderOffset | string [optional] Pixel offset of slider handle in slider direction in reference resolution from the slider's origin. Format: "[offset]px" |
GuiElement#sliderSize | string Pixel size of slider handle in reference resolution. Format: "[width]px [height]px" |
GuiElement#dataElementId | string [optional] Element ID of target data element which receives slider state update callback onSliderValueChanged(element, newValue) with this element and the new value as arguments. |
GuiElement#dataElementName | string [optional] Element name-based variant of #dataElementId for cases where IDs are not available (e.g. cloning pages). Use sparingly, because the #name attribute is not guaranteed to be unique and can lead to conflicts. |
GuiElement#textElementId | string [optional] Element ID of target text element whose text gets set to the current slider value on change. |
GuiElement#onClick | callback [optional] onClick(currentValue) Called when this element is clicked. Receives the current slider value. |
GuiElement#onChanged | callback [optional] onChanged(currentValue) Called when the slider value changes. Receives the current slider value. |
288 | function SliderElement:addElement(element) |
289 | SliderElement:superClass().addElement(self, element) |
290 | |
291 | if self.hasButtons then |
292 | if table.getn(self.elements) == 1 then |
293 | -- up button |
294 | self.upButtonElement = element |
295 | element.target = self |
296 | |
297 | if self.direction == SliderElement.DIRECTION_Y then |
298 | element:setCallback("onClickCallback", "onScrollDown") |
299 | else |
300 | element:setCallback("onClickCallback", "onScrollUp") |
301 | end |
302 | |
303 | self:setDisabled(self.disabled) |
304 | elseif table.getn(self.elements) == 2 then |
305 | -- down button |
306 | self.downButtonElement = element |
307 | element.target = self |
308 | |
309 | if self.direction == SliderElement.DIRECTION_Y then |
310 | element:setCallback("onClickCallback", "onScrollUp") |
311 | else |
312 | element:setCallback("onClickCallback", "onScrollDown") |
313 | end |
314 | |
315 | self:setDisabled(self.disabled) |
316 | end |
317 | end |
318 | end |
188 | function SliderElement:copyAttributes(src) |
189 | SliderElement:superClass().copyAttributes(self, src) |
190 | |
191 | GuiOverlay.copyOverlay(self.overlay, src.overlay) |
192 | GuiOverlay.copyOverlay(self.sliderOverlay, src.sliderOverlay) |
193 | GuiOverlay.copyOverlay(self.startOverlay, src.startOverlay) |
194 | GuiOverlay.copyOverlay(self.endOverlay, src.endOverlay) |
195 | |
196 | self.direction = src.direction |
197 | self.hasButtons = src.hasButtons |
198 | self.minValue = src.minValue |
199 | self.maxValue = src.maxValue |
200 | self.currentValue = src.currentValue |
201 | self.stepSize = src.stepSize |
202 | self.sliderOffset = src.sliderOffset |
203 | self.sliderSize = table.copy(src.sliderSize) |
204 | self.isThreePartBitmap = src.isThreePartBitmap |
205 | self.hideParentWhenEmpty = src.hideParentWhenEmpty |
206 | self.useStepRounding = src.useStepRounding |
207 | |
208 | self.startSize = table.copy(src.startSize) |
209 | self.midSize = table.copy(src.midSize) |
210 | self.endSize = table.copy(src.endSize) |
211 | |
212 | self.dataElementId = src.dataElementId |
213 | self.dataElementName = src.dataElementName |
214 | self.textElementId = src.textElementId |
215 | |
216 | self.onClickCallback = src.onClickCallback |
217 | self.onChangedCallback = src.onChangedCallback |
218 | |
219 | GuiMixin.cloneMixin(PlaySampleMixin, src, self) |
220 | end |
589 | function SliderElement:draw(clipX1, clipY1, clipX2, clipY2) |
590 | local state = GuiOverlay.STATE_NORMAL |
591 | if self.disabled then |
592 | state = GuiOverlay.STATE_DISABLED |
593 | end |
594 | |
595 | GuiOverlay.renderOverlay(self.overlay, self.absPosition[1], self.absPosition[2], self.size[1], self.size[2], state, clipX1, clipY1, clipX2, clipY2) |
596 | |
597 | if self.isSliderVisible and self.needsSlider then |
598 | if self.isThreePartBitmap then |
599 | local x, y = self.sliderPosition[1], self.sliderPosition[2] |
600 | if self.direction == SliderElement.DIRECTION_X then |
601 | GuiOverlay.renderOverlay(self.startOverlay, x, y, self.startSize[1], self.sliderSize[2], state, clipX1, clipY1, clipX2, clipY2) |
602 | GuiOverlay.renderOverlay(self.sliderOverlay, x + self.startSize[1], y, self.sliderSize[1] - self.startSize[1] - self.endSize[1], self.sliderSize[2], state, clipX1, clipY1, clipX2, clipY2) |
603 | GuiOverlay.renderOverlay(self.endOverlay, x + self.sliderSize[1] - self.endSize[1], y, self.endSize[1], self.sliderSize[2], state, clipX1, clipY1, clipX2, clipY2) |
604 | else |
605 | GuiOverlay.renderOverlay(self.startOverlay, x, y + self.sliderSize[2] - self.startSize[2], self.sliderSize[1], self.startSize[2], state, clipX1, clipY1, clipX2, clipY2) |
606 | GuiOverlay.renderOverlay(self.sliderOverlay, x, y + self.endSize[2], self.sliderSize[1], self.sliderSize[2] - self.startSize[2] - self.endSize[2], state, clipX1, clipY1, clipX2, clipY2) |
607 | GuiOverlay.renderOverlay(self.endOverlay, x, y, self.sliderSize[1], self.endSize[2], state, clipX1, clipY1, clipX2, clipY2) |
608 | end |
609 | else |
610 | GuiOverlay.renderOverlay(self.sliderOverlay, self.sliderPosition[1], self.sliderPosition[2], self.sliderSize[1], self.sliderSize[2], state, clipX1, clipY1, clipX2, clipY2) |
611 | end |
612 | end |
613 | |
614 | SliderElement:superClass().draw(self, clipX1, clipY1, clipX2, clipY2) |
615 | end |
100 | function SliderElement:loadFromXML(xmlFile, key) |
101 | SliderElement:superClass().loadFromXML(self, xmlFile, key) |
102 | |
103 | GuiOverlay.loadOverlay(self, self.overlay, "image", self.imageSize, nil, xmlFile, key) |
104 | GuiOverlay.loadOverlay(self, self.sliderOverlay, "sliderImage", self.imageSize, nil, xmlFile, key) |
105 | GuiOverlay.loadOverlay(self, self.startOverlay, "startImage", self.imageSize, nil, xmlFile, key) |
106 | GuiOverlay.loadOverlay(self, self.endOverlay, "endImage", self.imageSize, nil, xmlFile, key) |
107 | |
108 | local direction = getXMLString(xmlFile, key.."#direction") |
109 | if direction ~= nil then |
110 | if direction == "y" then |
111 | self.direction = SliderElement.DIRECTION_Y |
112 | elseif direction == "x" then |
113 | self.direction = SliderElement.DIRECTION_X |
114 | end |
115 | end |
116 | |
117 | self:addCallback(xmlFile, key.."#onClick", "onClickCallback") |
118 | self:addCallback(xmlFile, key.."#onChanged", "onChangedCallback") |
119 | |
120 | self.hasButtons = Utils.getNoNil(getXMLBool(xmlFile, key.."#hasButtons"), self.hasButtons) |
121 | self.minValue = Utils.getNoNil(getXMLFloat(xmlFile, key.."#minValue"), self.minValue) |
122 | self.maxValue = Utils.getNoNil(getXMLFloat(xmlFile, key.."#maxValue"), self.maxValue) |
123 | self.currentValue = Utils.getNoNil(getXMLFloat(xmlFile, key.."#currentValue"), self.currentValue) |
124 | self.stepSize = Utils.getNoNil(getXMLFloat(xmlFile, key.."#stepSize"), self.stepSize) |
125 | self.isThreePartBitmap = Utils.getNoNil(getXMLBool(xmlFile, key.."#isThreePartBitmap"), self.isThreePartBitmap) |
126 | self.hideParentWhenEmpty = Utils.getNoNil(getXMLBool(xmlFile, key.."#hideParentWhenEmpty"), self.hideParentWhenEmpty) |
127 | self.useStepRounding = Utils.getNoNil(getXMLBool(xmlFile, key.."#useStepRounding"), self.useStepRounding) |
128 | |
129 | self.sliderOffset = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#sliderOffset"), {self.outputSize[2]}, {self.sliderOffset})) |
130 | self.sliderSize = GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#sliderSize"), self.outputSize, self.sliderSize) |
131 | |
132 | self.startSize = GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#startImageSize"), self.outputSize, self.startSize) |
133 | self.midSize = GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#midImageSize"), self.outputSize, self.midSize) |
134 | self.endSize = GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#endImageSize"), self.outputSize, self.endSize) |
135 | |
136 | self.dataElementId = getXMLString(xmlFile, key.."#dataElementId") |
137 | self.dataElementName = getXMLString(xmlFile, key.."#dataElementName") |
138 | self.textElementId = getXMLString(xmlFile, key.."#textElementId") |
139 | |
140 | GuiOverlay.createOverlay(self.overlay) |
141 | GuiOverlay.createOverlay(self.sliderOverlay) |
142 | GuiOverlay.createOverlay(self.startOverlay) |
143 | GuiOverlay.createOverlay(self.endOverlay) |
144 | end |
148 | function SliderElement:loadProfile(profile, applyProfile) |
149 | SliderElement:superClass().loadProfile(self, profile, applyProfile) |
150 | |
151 | GuiOverlay.loadOverlay(self, self.overlay, "image", self.imageSize, profile, nil, nil) |
152 | GuiOverlay.loadOverlay(self, self.sliderOverlay, "sliderImage", self.imageSize, profile, nil, nil) |
153 | GuiOverlay.loadOverlay(self, self.startOverlay, "startImage", self.imageSize, profile, nil, nil) |
154 | GuiOverlay.loadOverlay(self, self.endOverlay, "endImage", self.imageSize, profile, nil, nil) |
155 | |
156 | local direction = profile:getValue("direction") |
157 | if direction ~= nil then |
158 | if direction == "y" then |
159 | self.direction = SliderElement.DIRECTION_Y |
160 | elseif direction == "x" then |
161 | self.direction = SliderElement.DIRECTION_X |
162 | end |
163 | end |
164 | |
165 | self.hasButtons = profile:getBool("hasButtons", self.hasButtons) |
166 | self.minValue = profile:getNumber("minValue", self.minValue) |
167 | self.maxValue = profile:getNumber("maxValue", self.maxValue) |
168 | self.currentValue = profile:getNumber("currentValue", self.currentValue) |
169 | self.stepSize = profile:getNumber("stepSize", self.stepSize) |
170 | self.isThreePartBitmap = profile:getBool("isThreePartBitmap", self.isThreePartBitmap) |
171 | self.hideParentWhenEmpty = profile:getBool("hideParentWhenEmpty", self.hideParentWhenEmpty) |
172 | self.useStepRounding = profile:getBool("useStepRounding", self.useStepRounding) |
173 | |
174 | self.sliderOffset = unpack(GuiUtils.getNormalizedValues(profile:getValue("sliderOffset"), {self.outputSize[2]}, {self.sliderOffset})) |
175 | self.sliderSize = GuiUtils.getNormalizedValues(profile:getValue("sliderSize"), self.outputSize, self.sliderSize) |
176 | |
177 | self.startSize = GuiUtils.getNormalizedValues(profile:getValue("startImageSize"), self.outputSize, self.startSize) |
178 | self.midSize = GuiUtils.getNormalizedValues(profile:getValue("midImageSize"), self.outputSize, self.midSize) |
179 | self.endSize = GuiUtils.getNormalizedValues(profile:getValue("endImageSize"), self.outputSize, self.endSize) |
180 | |
181 | if applyProfile then |
182 | self:applySliderAspectScale() |
183 | end |
184 | end |
481 | function SliderElement:mouseEvent(posX, posY, isDown, isUp, button, eventUsed) |
482 | if self:getIsActive() then |
483 | if SliderElement:superClass().mouseEvent(self, posX, posY, isDown, isUp, button, eventUsed) then |
484 | eventUsed = true |
485 | end |
486 | |
487 | if self.mouseDown and isUp and button == Input.MOUSE_BUTTON_LEFT then |
488 | eventUsed = true |
489 | self.clickedOnSlider = false |
490 | self.mouseDown = false |
491 | |
492 | self:raiseCallback("onClickCallback", self.currentValue) |
493 | end |
494 | |
495 | if not eventUsed and (GuiUtils.checkOverlayOverlap(posX, posY, self.absPosition[1], self.absPosition[2], self.absSize[1], self.absSize[2]) or GuiUtils.checkOverlayOverlap(posX, posY, self.sliderPosition[1], self.sliderPosition[2], self.sliderSize[1], self.sliderSize[2])) then |
496 | eventUsed = true |
497 | if Input.isMouseButtonPressed(Input.MOUSE_BUTTON_WHEEL_UP) then |
498 | self:setValue(self.currentValue - self.stepSize, nil, false) |
499 | end |
500 | |
501 | if Input.isMouseButtonPressed(Input.MOUSE_BUTTON_WHEEL_DOWN) then |
502 | self:setValue(self.currentValue + self.stepSize, nil, false) |
503 | end |
504 | |
505 | if isDown and button == Input.MOUSE_BUTTON_LEFT then |
506 | if not self.mouseDown and GuiUtils.checkOverlayOverlap(posX, posY, self.sliderPosition[1], self.sliderPosition[2], self.sliderSize[1], self.sliderSize[2]) then |
507 | self.clickedOnSlider = true |
508 | self.lastMousePosX = posX |
509 | self.lastMousePosY = posY |
510 | self.lastSliderPosX = self.sliderPosition[1] |
511 | self.lastSliderPosY = self.sliderPosition[2] |
512 | end |
513 | self.mouseDown = true |
514 | end |
515 | end |
516 | |
517 | if self.mouseDown then |
518 | eventUsed = true |
519 | -- calculate slider value according to current mouse position |
520 | local newValue |
521 | local mousePos = posX |
522 | if self.direction == SliderElement.DIRECTION_Y then |
523 | mousePos = posY |
524 | if self.clickedOnSlider then |
525 | local deltaY = posY - self.lastMousePosY |
526 | mousePos = self.lastSliderPosY + deltaY |
527 | newValue = self.minValue + (1-((mousePos - self.minAbsSliderPos) / (self.maxAbsSliderPos - self.minAbsSliderPos))) * (self.maxValue - self.minValue) |
528 | else |
529 | if mousePos > self.sliderPosition[2]+self.sliderSize[2] then |
530 | mousePos = mousePos - self.sliderSize[2] |
531 | end |
532 | newValue = self.minValue + (1-((mousePos - self.minAbsSliderPos) / (self.maxAbsSliderPos - self.minAbsSliderPos))) * (self.maxValue - self.minValue) |
533 | end |
534 | else |
535 | if self.clickedOnSlider then |
536 | local deltaX = posX - self.lastMousePosX |
537 | mousePos = self.lastSliderPosX + deltaX |
538 | else |
539 | if mousePos > self.sliderPosition[1]+self.sliderSize[1] then |
540 | mousePos = mousePos - self.sliderSize[1] |
541 | end |
542 | end |
543 | |
544 | newValue = self.minValue + ((mousePos - self.minAbsSliderPos) / (self.maxAbsSliderPos - self.minAbsSliderPos)) * (self.maxValue - self.minValue) |
545 | end |
546 | self:setValue(newValue, nil, true) |
547 | end |
548 | end |
549 | |
550 | return eventUsed |
551 | end |
42 | function SliderElement.new(target, custom_mt) |
43 | local self = GuiElement.new(target, custom_mt or SliderElement_mt) |
44 | self:include(PlaySampleMixin) -- add sound playing |
45 | |
46 | self.mouseDown = false |
47 | |
48 | self.minValue = 0 |
49 | self.maxValue = 100 |
50 | self.currentValue = 0 |
51 | self.sliderValue = 0 |
52 | self.stepSize = 1.0 |
53 | self.direction = SliderElement.DIRECTION_X |
54 | |
55 | self.hasButtons = true |
56 | self.isThreePartBitmap = false |
57 | |
58 | self.overlay = {} |
59 | self.sliderOverlay = {} |
60 | self.startOverlay = {} |
61 | self.endOverlay = {} |
62 | |
63 | self.startSize = {0, 0} |
64 | self.midSize = {0, 0} |
65 | self.endSize = {0, 0} |
66 | self.sliderOffset = -0.012 |
67 | self.sliderSize = {1,1} |
68 | self.sliderPosition = {0,0} |
69 | self.adjustSliderSize = true |
70 | |
71 | self.textElement = nil |
72 | self.dataElementId = nil |
73 | self.textElementId = nil |
74 | |
75 | self.minAbsSliderPos = 0.08 |
76 | self.maxAbsSliderPos = 0.92 |
77 | |
78 | self.isSliderVisible = true |
79 | self.needsSlider = true |
80 | self.useStepRounding = false |
81 | |
82 | self.hideParentWhenEmpty = false |
83 | |
84 | return self |
85 | end |
705 | function SliderElement:onBindUpdate(element) |
706 | if element:isa(ListElement) then |
707 | local list = element |
708 | |
709 | local numItems = list:getItemCount() |
710 | local numVisibleItems = list:getVisibleItemCount() |
711 | |
712 | self.useStepRounding = true |
713 | |
714 | self:setMinValue(1) |
715 | self:setMaxValue(math.ceil(numItems - numVisibleItems) / list:getItemFactor() + 1) |
716 | self:setSliderSize(numVisibleItems, numItems) |
717 | self:setValue(math.max(list.firstVisibleItem, 1), true, true) |
718 | self.needsSlider = numItems > numVisibleItems |
719 | elseif element:isa(ScrollingLayoutElement) then |
720 | self:setMinValue(1) |
721 | |
722 | self.useStepRounding = true |
723 | |
724 | if element:getNeedsScrolling() then |
725 | self:setMaxValue(element.contentSize / element.absSize[2] * 20) |
726 | self:setSliderSize(element.absSize[2], element.contentSize) |
727 | self.needsSlider = true |
728 | else |
729 | self:setMaxValue(1) |
730 | self:setSliderSize(10, 100) |
731 | self.needsSlider = false |
732 | end |
733 | -- self:setValue(element.firstVisibleY element.contentSize) |
734 | elseif element:isa(SmoothListElement) then |
735 | -- round values on pixel level to avoid floating point issues |
736 | local base = element.lengthAxis == 1 and g_screenWidth or g_screenHeight |
737 | local contentSize = MathUtil.round(element.contentSize * base) |
738 | local scrollViewOffsetDelta = MathUtil.round(element.scrollViewOffsetDelta * base) |
739 | local size = MathUtil.round(element.absSize[element.lengthAxis] * base) |
740 | |
741 | local numStepsTotal = math.ceil(contentSize / scrollViewOffsetDelta) |
742 | local numStepsVisible = math.floor(size / scrollViewOffsetDelta) |
743 | local scrollSteps = math.max(numStepsTotal - numStepsVisible, 0) |
744 | |
745 | self:setMinValue(1) |
746 | self:setMaxValue(scrollSteps + 1) |
747 | |
748 | local viewSize = element.absSize[element.lengthAxis] |
749 | self:setSliderSize(viewSize, element.contentSize) |
750 | self.needsSlider = element.contentSize > viewSize |
751 | |
752 | self:setValue(element:getViewOffsetPercentage() * (self.maxValue - self.minValue) + self.minValue, true, true) |
753 | end |
754 | |
755 | if self.hideParentWhenEmpty then |
756 | self.parent:setVisible(self.needsSlider) |
757 | end |
758 | end |
256 | function SliderElement:onGuiSetupFinished() |
257 | SliderElement:superClass().onGuiSetupFinished(self) |
258 | |
259 | if self.textElementId ~= nil then |
260 | if self.target[self.textElementId] ~= nil then |
261 | self.textElement = self.target[self.textElementId] |
262 | else |
263 | print("Warning: TextElementId '"..self.textElementId.."' not found for '"..self.target.name.."'!") |
264 | end |
265 | end |
266 | |
267 | if self.dataElementId ~= nil then |
268 | if self.target[self.dataElementId] ~= nil then |
269 | local dataElement = self.target[self.dataElementId] |
270 | self:setDataElement(dataElement) |
271 | else |
272 | print("Warning: DataElementId '"..self.dataElementId.."' not found for '"..self.target.name.."'!") |
273 | end |
274 | elseif self.dataElementName ~= nil and self.parent then |
275 | local findDataElement = function(element) return element.name and element.name == self.dataElementName end |
276 | local dataElement = self.parent:getFirstDescendant(findDataElement) |
277 | |
278 | if dataElement then |
279 | self:setDataElement(dataElement) |
280 | else |
281 | print("Warning: DataElementName '"..self.dataElementName.."' not found as descendant of '"..tostring(self.parent).."'!") |
282 | end |
283 | end |
284 | end |
340 | function SliderElement:setValue(newValue, doNotUpdateDataElement, immediateMode) |
341 | self.sliderValue = math.min(math.max(newValue, self.minValue), self.maxValue) |
342 | self:updateSliderPosition() |
343 | |
344 | if self.useStepRounding then |
345 | local rem = (newValue-self.minValue) % self.stepSize |
346 | |
347 | -- round to the next step |
348 | if rem >= self.stepSize - rem then |
349 | newValue = newValue + self.stepSize - rem |
350 | else |
351 | newValue = newValue - rem |
352 | end |
353 | |
354 | newValue = math.min(math.max(newValue, self.minValue), self.maxValue) |
355 | end |
356 | |
357 | -- round to 5 decimal places |
358 | local numDecimalPlaces = 5 |
359 | local mult = 10^numDecimalPlaces |
360 | newValue = math.floor(newValue * mult + 0.5) / mult |
361 | |
362 | if newValue ~= self.currentValue then |
363 | self.currentValue = newValue |
364 | if self.textElement ~= nil then |
365 | self.textElement:setText(self.currentValue) |
366 | end |
367 | |
368 | self:callOnChanged() |
369 | for _, element in pairs(self.elements) do |
370 | if element.onSliderValueChanged ~= nil then |
371 | element:onSliderValueChanged(self, newValue, immediateMode) |
372 | end |
373 | end |
374 | |
375 | if self.dataElement ~= nil and (doNotUpdateDataElement == nil or not doNotUpdateDataElement) then |
376 | self.dataElement:onSliderValueChanged(self, newValue, immediateMode) |
377 | end |
378 | |
379 | -- Disable buttons if needed |
380 | self:updateSliderButtons() |
381 | |
382 | return true |
383 | end |
384 | |
385 | return false |
386 | end |