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. |
239 | function SliderElement:addElement(element) |
240 | SliderElement:superClass().addElement(self, element) |
241 | |
242 | if self.hasButtons then |
243 | if table.getn(self.elements) == 1 then |
244 | -- up button |
245 | self.upButtonElement = element |
246 | element.target = self |
247 | element.onClickCallback = SliderElement.onScrollUp |
248 | |
249 | if self.direction == SliderElement.DIRECTION_Y then |
250 | element.onClickCallback = SliderElement.onScrollDown |
251 | end |
252 | |
253 | self:setDisabled(self.disabled) |
254 | elseif table.getn(self.elements) == 2 then |
255 | -- down button |
256 | self.downButtonElement = element |
257 | element.target = self |
258 | element.onClickCallback = SliderElement.onScrollDown |
259 | |
260 | if self.direction == SliderElement.DIRECTION_Y then |
261 | element.onClickCallback = SliderElement.onScrollUp |
262 | end |
263 | |
264 | self:setDisabled(self.disabled) |
265 | end |
266 | end |
267 | end |
156 | function SliderElement:copyAttributes(src) |
157 | SliderElement:superClass().copyAttributes(self, src) |
158 | |
159 | GuiOverlay.copyOverlay(self.overlay, src.overlay) |
160 | GuiOverlay.copyOverlay(self.sliderOverlay, src.sliderOverlay) |
161 | |
162 | self.direction = src.direction |
163 | self.hasButtons = src.hasButtons |
164 | self.minValue = src.minValue |
165 | self.maxValue = src.maxValue |
166 | self.currentValue = src.currentValue |
167 | self.stepSize = src.stepSize |
168 | self.sliderOffset = src.sliderOffset |
169 | self.sliderSize = ListUtil.copyTable(src.sliderSize) |
170 | |
171 | self.dataElementId = src.dataElementId |
172 | self.dataElementName = src.dataElementName |
173 | self.textElementId = src.textElementId |
174 | |
175 | self.onClickCallback = src.onClickCallback |
176 | self.onChangedCallback = src.onChangedCallback |
177 | |
178 | GuiMixin.cloneMixin(PlaySampleMixin, src, self) |
179 | end |
535 | function SliderElement:draw() |
536 | local state = GuiOverlay.STATE_NORMAL |
537 | if self.disabled then |
538 | state = GuiOverlay.STATE_DISABLED |
539 | end |
540 | |
541 | GuiOverlay.renderOverlay(self.overlay, self.absPosition[1], self.absPosition[2], self.size[1], self.size[2], state) |
542 | if self.isSliderVisible then |
543 | if self.needsSlider then |
544 | GuiOverlay.renderOverlay(self.sliderOverlay, self.sliderPosition[1], self.sliderPosition[2], self.sliderSize[1], self.sliderSize[2], state) |
545 | end |
546 | end |
547 | |
548 | SliderElement:superClass().draw(self) |
549 | end |
88 | function SliderElement:loadFromXML(xmlFile, key) |
89 | SliderElement:superClass().loadFromXML(self, xmlFile, key) |
90 | |
91 | GuiOverlay.loadOverlay(self, self.overlay, "image", self.imageSize, nil, xmlFile, key) |
92 | GuiOverlay.loadOverlay(self, self.sliderOverlay, "sliderImage", self.imageSize, nil, xmlFile, key) |
93 | |
94 | local direction = getXMLString(xmlFile, key.."#direction") |
95 | if direction ~= nil then |
96 | if direction == "y" then |
97 | self.direction = SliderElement.DIRECTION_Y |
98 | elseif direction == "x" then |
99 | self.direction = SliderElement.DIRECTION_X |
100 | end |
101 | end |
102 | |
103 | self:addCallback(xmlFile, key.."#onClick", "onClickCallback") |
104 | self:addCallback(xmlFile, key.."#onChanged", "onChangedCallback") |
105 | |
106 | self.hasButtons = Utils.getNoNil(getXMLBool(xmlFile, key.."#hasButtons"), self.hasButtons) |
107 | self.minValue = Utils.getNoNil(getXMLFloat(xmlFile, key.."#minValue"), self.minValue) |
108 | self.maxValue = Utils.getNoNil(getXMLFloat(xmlFile, key.."#maxValue"), self.maxValue) |
109 | self.currentValue = Utils.getNoNil(getXMLFloat(xmlFile, key.."#currentValue"), self.currentValue) |
110 | self.stepSize = Utils.getNoNil(getXMLFloat(xmlFile, key.."#stepSize"), self.stepSize) |
111 | |
112 | self.sliderOffset = unpack(GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#sliderOffset"), {self.outputSize[2]}, {self.sliderOffset})) |
113 | self.sliderSize = GuiUtils.getNormalizedValues(getXMLString(xmlFile, key.."#sliderSize"), self.outputSize, self.sliderSize) |
114 | |
115 | self.dataElementId = getXMLString(xmlFile, key.."#dataElementId") |
116 | self.dataElementName = getXMLString(xmlFile, key.."#dataElementName") |
117 | self.textElementId = getXMLString(xmlFile, key.."#textElementId") |
118 | |
119 | GuiOverlay.createOverlay(self.overlay) |
120 | GuiOverlay.createOverlay(self.sliderOverlay) |
121 | end |
125 | function SliderElement:loadProfile(profile, applyProfile) |
126 | SliderElement:superClass().loadProfile(self, profile, applyProfile) |
127 | |
128 | GuiOverlay.loadOverlay(self, self.overlay, "image", self.imageSize, profile, nil, nil) |
129 | GuiOverlay.loadOverlay(self, self.sliderOverlay, "sliderImage", self.imageSize, profile, nil, nil) |
130 | |
131 | local direction = profile:getValue("direction") |
132 | if direction ~= nil then |
133 | if direction == "y" then |
134 | self.direction = SliderElement.DIRECTION_Y |
135 | elseif direction == "x" then |
136 | self.direction = SliderElement.DIRECTION_X |
137 | end |
138 | end |
139 | |
140 | self.hasButtons = profile:getBool("hasButtons", self.hasButtons) |
141 | self.minValue = profile:getNumber("minValue", self.minValue) |
142 | self.maxValue = profile:getNumber("maxValue", self.maxValue) |
143 | self.currentValue = profile:getNumber("currentValue", self.currentValue) |
144 | self.stepSize = profile:getNumber("stepSize", self.stepSize) |
145 | |
146 | self.sliderOffset = unpack(GuiUtils.getNormalizedValues(profile:getValue("sliderOffset"), {self.outputSize[2]}, {self.sliderOffset})) |
147 | self.sliderSize = GuiUtils.getNormalizedValues(profile:getValue("sliderSize"), self.outputSize, self.sliderSize) |
148 | |
149 | if applyProfile then |
150 | self:applySliderAspectScale() |
151 | end |
152 | end |
427 | function SliderElement:mouseEvent(posX, posY, isDown, isUp, button, eventUsed) |
428 | if self:getIsActive() then |
429 | if SliderElement:superClass().mouseEvent(self, posX, posY, isDown, isUp, button, eventUsed) then |
430 | eventUsed = true |
431 | end |
432 | |
433 | if self.mouseDown and isUp and button == Input.MOUSE_BUTTON_LEFT then |
434 | eventUsed = true |
435 | self.clickedOnSlider = false |
436 | self.mouseDown = false |
437 | self:raiseCallback("onClickCallback", self.currentValue) |
438 | end |
439 | |
440 | if not eventUsed and (GuiUtils.checkOverlayOverlap(posX, posY, self.absPosition[1], self.absPosition[2], self.size[1], self.size[2]) or GuiUtils.checkOverlayOverlap(posX, posY, self.sliderPosition[1], self.sliderPosition[2], self.sliderSize[1], self.sliderSize[2])) then |
441 | eventUsed = true |
442 | if Input.isMouseButtonPressed(Input.MOUSE_BUTTON_WHEEL_UP) then |
443 | self:setValue(self.currentValue - self.stepSize) |
444 | end |
445 | |
446 | if Input.isMouseButtonPressed(Input.MOUSE_BUTTON_WHEEL_DOWN) then |
447 | self:setValue(self.currentValue + self.stepSize) |
448 | end |
449 | |
450 | if isDown and button == Input.MOUSE_BUTTON_LEFT then |
451 | if not self.mouseDown and GuiUtils.checkOverlayOverlap(posX, posY, self.sliderPosition[1], self.sliderPosition[2], self.sliderSize[1], self.sliderSize[2]) then |
452 | self.clickedOnSlider = true |
453 | self.lastMousePosX = posX |
454 | self.lastMousePosY = posY |
455 | self.lastSliderPosX = self.sliderPosition[1] |
456 | self.lastSliderPosY = self.sliderPosition[2] |
457 | end |
458 | self.mouseDown = true |
459 | end |
460 | end |
461 | |
462 | if self.mouseDown then |
463 | eventUsed = true |
464 | -- calculate slider value according to current mouse position |
465 | local newValue = 0 |
466 | local mousePos = posX |
467 | if self.direction == SliderElement.DIRECTION_Y then |
468 | mousePos = posY |
469 | if self.clickedOnSlider then |
470 | local deltaY = posY - self.lastMousePosY |
471 | mousePos = self.lastSliderPosY + deltaY |
472 | newValue = self.minValue + (1-((mousePos - self.minAbsSliderPos) / (self.maxAbsSliderPos - self.minAbsSliderPos))) * (self.maxValue - self.minValue) |
473 | else |
474 | if mousePos > self.sliderPosition[2]+self.sliderSize[2] then |
475 | mousePos = mousePos - self.sliderSize[2] |
476 | end |
477 | newValue = self.minValue + (1-((mousePos - self.minAbsSliderPos) / (self.maxAbsSliderPos - self.minAbsSliderPos))) * (self.maxValue - self.minValue) |
478 | end |
479 | else |
480 | if self.clickedOnSlider then |
481 | local deltaX = posX - self.lastMousePosX |
482 | mousePos = self.lastSliderPosX + deltaX |
483 | newValue = self.minValue + ((mousePos - self.minAbsSliderPos) / (self.maxAbsSliderPos - self.minAbsSliderPos)) * (self.maxValue - self.minValue) |
484 | else |
485 | if mousePos > self.sliderPosition[1]+self.sliderSize[1] then |
486 | mousePos = mousePos - self.sliderSize[1] |
487 | end |
488 | end |
489 | |
490 | newValue = self.minValue + ((mousePos - self.minAbsSliderPos) / (self.maxAbsSliderPos - self.minAbsSliderPos)) * (self.maxValue - self.minValue) |
491 | end |
492 | self:setValue(newValue) |
493 | end |
494 | end |
495 | |
496 | return eventUsed |
497 | 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 | |
57 | self.overlay = {} |
58 | self.sliderOverlay = {} |
59 | |
60 | self.sliderOffset = -0.012 |
61 | self.sliderSize = {1,1} |
62 | self.sliderPosition = {0,0} |
63 | self.adjustSliderSize = true |
64 | |
65 | self.textElement = nil |
66 | self.dataElementId = nil |
67 | self.textElementId = nil |
68 | |
69 | self.minAbsSliderPos = 0.08 |
70 | self.maxAbsSliderPos = 0.92 |
71 | |
72 | self.isSliderVisible = true |
73 | self.needsSlider = true |
74 | |
75 | return self |
76 | end |
207 | function SliderElement:onGuiSetupFinished() |
208 | SliderElement:superClass().onGuiSetupFinished(self) |
209 | |
210 | if self.textElementId ~= nil then |
211 | if self.target[self.textElementId] ~= nil then |
212 | self.textElement = self.target[self.textElementId] |
213 | else |
214 | print("Warning: TextElementId '"..self.textElementId.."' not found for '"..self.target.name.."'!") |
215 | end |
216 | end |
217 | |
218 | if self.dataElementId ~= nil then |
219 | if self.target[self.dataElementId] ~= nil then |
220 | local dataElement = self.target[self.dataElementId] |
221 | self:setDataElement(dataElement) |
222 | else |
223 | print("Warning: DataElementId '"..self.dataElementId.."' not found for '"..self.target.name.."'!") |
224 | end |
225 | elseif self.dataElementName ~= nil and self.parent then |
226 | local findDataElement = function(element) return element.name and element.name == self.dataElementName end |
227 | local dataElement = self.parent:getFirstDescendant(findDataElement) |
228 | |
229 | if dataElement then |
230 | self:setDataElement(dataElement) |
231 | else |
232 | print("Warning: DataElementName '"..self.dataElementName.."' not found as descendant of '"..tostring(self.parent).."'!") |
233 | end |
234 | end |
235 | end |
289 | function SliderElement:setValue(newValue, doNotUpdateDataElement) |
290 | self.sliderValue = math.min(math.max(newValue, self.minValue), self.maxValue) |
291 | self:updateSliderPosition() |
292 | local rem = (newValue-self.minValue) % self.stepSize |
293 | |
294 | -- round to the next step |
295 | if rem >= self.stepSize - rem then |
296 | newValue = newValue + self.stepSize - rem |
297 | else |
298 | newValue = newValue - rem |
299 | end |
300 | |
301 | newValue = math.min(math.max(newValue, self.minValue), self.maxValue) |
302 | |
303 | -- round to 5 decimal places |
304 | local numDecimalPlaces = 5 |
305 | local mult = 10^numDecimalPlaces |
306 | newValue = math.floor(newValue * mult + 0.5) / mult |
307 | |
308 | if newValue ~= self.currentValue then |
309 | self.currentValue = newValue |
310 | if self.textElement ~= nil then |
311 | self.textElement:setText(self.currentValue) |
312 | end |
313 | |
314 | self:callOnChanged() |
315 | for _, element in pairs(self.elements) do |
316 | if element.onSliderValueChanged ~= nil then |
317 | element:onSliderValueChanged(self, newValue) |
318 | end |
319 | end |
320 | |
321 | if self.dataElement ~= nil and (doNotUpdateDataElement == nil or not doNotUpdateDataElement) then |
322 | self.dataElement:onSliderValueChanged(self, newValue) |
323 | end |
324 | |
325 | -- Disable buttons if needed |
326 | self:updateSliderButtons() |
327 | |
328 | return true |
329 | end |
330 | |
331 | return false |
332 | end |
501 | function SliderElement:updateSliderPosition() |
502 | local state = (self.sliderValue - self.minValue) / (self.maxValue - self.minValue) |
503 | |
504 | if self.direction == SliderElement.DIRECTION_Y then |
505 | self.sliderPosition[1] = self.absPosition[1] + self.sliderOffset |
506 | self.sliderPosition[2] = MathUtil.lerp(self.minAbsSliderPos, self.maxAbsSliderPos, 1-state) |
507 | else |
508 | self.sliderPosition[1] = MathUtil.lerp(self.minAbsSliderPos, self.maxAbsSliderPos, state) |
509 | self.sliderPosition[2] = self.absPosition[2] + self.sliderOffset |
510 | end |
511 | |
512 | self:updateSliderButtons() |
513 | end |