You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bastion/libs/StdUi/widgets/Scroll.lua

598 lines
16 KiB

--- @type StdUi
local StdUi = LibStub and LibStub('StdUi', true);
if not StdUi then
return
end
local module, version = 'Scroll', 6;
if not StdUi:UpgradeNeeded(module, version) then
return
end
local round = function(num)
return math.floor(num + .5);
end
----------------------------------------------------
--- ScrollFrame
----------------------------------------------------
StdUi.ScrollBarEvents = {
UpDownButtonOnClick = function(self)
local scrollBar = self.scrollBar;
local scrollFrame = scrollBar.scrollFrame;
local scrollStep = scrollBar.scrollStep or (scrollFrame:GetHeight() / 2);
if self.direction == 1 then
scrollBar:SetValue(scrollBar:GetValue() - scrollStep);
else
scrollBar:SetValue(scrollBar:GetValue() + scrollStep);
end
end,
OnValueChanged = function(self, value)
self.scrollFrame:SetVerticalScroll(value);
end
};
StdUi.ScrollFrameEvents = {
OnMouseWheel = function(self, value, scrollBar)
scrollBar = scrollBar or self.scrollBar;
local scrollStep = scrollBar.scrollStep or scrollBar:GetHeight() / 2;
if value > 0 then
scrollBar:SetValue(scrollBar:GetValue() - scrollStep);
else
scrollBar:SetValue(scrollBar:GetValue() + scrollStep);
end
end,
OnScrollRangeChanged = function(self, _, yRange)
-- xRange
local scrollbar = self.scrollBar;
if not yRange then
yRange = self:GetVerticalScrollRange();
end
-- Accounting for very small ranges
yRange = math.floor(yRange);
local value = math.min(scrollbar:GetValue(), yRange);
scrollbar:SetMinMaxValues(0, yRange);
scrollbar:SetValue(value);
local scrollDownButton = scrollbar.ScrollDownButton;
local scrollUpButton = scrollbar.ScrollUpButton;
local thumbTexture = scrollbar.ThumbTexture;
if yRange == 0 then
if self.scrollBarHideable then
scrollbar:Hide();
scrollDownButton:Hide();
scrollUpButton:Hide();
thumbTexture:Hide();
else
scrollDownButton:Disable();
scrollUpButton:Disable();
scrollDownButton:Show();
scrollUpButton:Show();
if (not self.noScrollThumb) then
thumbTexture:Show();
end
end
else
scrollDownButton:Show();
scrollUpButton:Show();
scrollbar:Show();
if not self.noScrollThumb then
thumbTexture:Show();
end
-- The 0.005 is to account for precision errors
if yRange - value > 0.005 then
scrollDownButton:Enable();
else
scrollDownButton:Disable();
end
end
end,
OnVerticalScroll = function(self, offset)
local scrollBar = self.scrollBar;
scrollBar:SetValue(offset);
local _, max = scrollBar:GetMinMaxValues();
scrollBar.ScrollUpButton:SetEnabled(offset ~= 0);
scrollBar.ScrollDownButton:SetEnabled((scrollBar:GetValue() - max) ~= 0);
end
}
StdUi.ScrollFrameMethods = {
SetScrollStep = function(self, scrollStep)
scrollStep = round(scrollStep);
self.scrollBar.scrollStep = scrollStep;
self.scrollBar:SetValueStep(scrollStep);
end,
GetChildFrames = function(self)
return self.scrollBar, self.scrollChild, self.scrollBar.ScrollUpButton, self.scrollBar.ScrollDownButton;
end,
UpdateSize = function(self, newWidth, newHeight)
self:SetSize(newWidth, newHeight);
self.scrollFrame:ClearAllPoints();
-- scrollbar width and margins
self.scrollFrame:SetSize(newWidth - self.scrollBarWidth - 5, newHeight - 4);
self.stdUi:GlueAcross(self.scrollFrame, self, 2, -2, -self.scrollBarWidth - 2, 2);
-- panel of scrollBar
self.scrollBar.panel:SetPoint('TOPRIGHT', self, 'TOPRIGHT', -2, -2);
self.scrollBar.panel:SetPoint('BOTTOMRIGHT', self, 'BOTTOMRIGHT', -2, 2);
if self.scrollChild then
self.scrollChild:SetWidth(self.scrollFrame:GetWidth());
self.scrollChild:SetHeight(self.scrollFrame:GetHeight());
end
end
};
function StdUi:ScrollFrame(parent, width, height, scrollChild)
local panel = self:Panel(parent, width, height);
panel.stdUi = self;
panel.offset = 0;
panel.scrollBarWidth = 16;
local scrollFrame = CreateFrame('ScrollFrame', nil, panel);
local scrollBar = self:ScrollBar(panel, panel.scrollBarWidth);
scrollBar:SetMinMaxValues(0, 0);
scrollBar:SetValue(0);
scrollBar:SetScript('OnValueChanged', self.ScrollBarEvents.OnValueChanged);
scrollBar.ScrollUpButton.direction = 1;
scrollBar.ScrollDownButton.direction = -1;
scrollBar.ScrollDownButton:SetScript('OnClick', self.ScrollBarEvents.UpDownButtonOnClick);
scrollBar.ScrollUpButton:SetScript('OnClick', self.ScrollBarEvents.UpDownButtonOnClick);
scrollBar.ScrollDownButton:Disable();
scrollBar.ScrollUpButton:Disable();
if self.noScrollThumb then
scrollBar.ThumbTexture:Hide();
end
scrollBar.scrollFrame = scrollFrame;
scrollFrame.scrollBar = scrollBar;
scrollFrame.panel = panel;
panel.scrollBar = scrollBar;
panel.scrollFrame = scrollFrame;
for k, v in pairs(self.ScrollFrameMethods) do
panel[k] = v;
end
for k, v in pairs(self.ScrollFrameEvents) do
scrollFrame:SetScript(k, v);
end
if not scrollChild then
scrollChild = CreateFrame('Frame', nil, scrollFrame);
scrollChild:SetWidth(scrollFrame:GetWidth());
scrollChild:SetHeight(scrollFrame:GetHeight());
else
scrollChild:SetParent(scrollFrame);
end
panel.scrollChild = scrollChild;
panel:UpdateSize(width, height);
scrollFrame:SetScrollChild(scrollChild);
scrollFrame:EnableMouse(true);
scrollFrame:SetClampedToScreen(true);
scrollFrame:SetClipsChildren(true);
scrollChild:SetPoint('RIGHT', scrollFrame, 'RIGHT', 0, 0);
scrollFrame.scrollChild = scrollChild;
return panel;
end
----------------------------------------------------
--- FauxScrollFrame
----------------------------------------------------
StdUi.FauxScrollFrameMethods = {
GetOffset = function(self)
return self.offset or 0;
end,
--- Performs vertical scroll
DoVerticalScroll = function(self, value, itemHeight, updateFunction)
local scrollBar = self.scrollBar;
itemHeight = itemHeight or self.lineHeight;
scrollBar:SetValue(value);
self.offset = floor((value / itemHeight) + 0.5);
if updateFunction then
updateFunction(self);
end
end,
--- Redraws items in case of manual update from outside without changing parameters
Redraw = function(self)
self:Update(
self.itemCount or #self.scrollChild.items,
self.displayCount,
self.lineHeight
);
end,
UpdateItemsCount = function(self, newCount)
self.itemCount = newCount;
self:Update(
newCount,
self.displayCount,
self.lineHeight
);
end,
Update = function(self, numItems, numToDisplay, buttonHeight)
local scrollBar, scrollChildFrame, scrollUpButton, scrollDownButton = self:GetChildFrames();
local showScrollBar;
if numItems == nil or numToDisplay == nil then
return;
end
if numItems > numToDisplay then
showScrollBar = 1;
else
scrollBar:SetValue(0);
end
if self:IsShown() then
local scrollFrameHeight = 0;
local scrollChildHeight = 0;
if numItems > 0 then
scrollFrameHeight = (numItems - numToDisplay) * buttonHeight;
scrollChildHeight = numItems * buttonHeight;
if (scrollFrameHeight < 0) then
scrollFrameHeight = 0;
end
scrollChildFrame:Show();
else
scrollChildFrame:Hide();
end
local maxRange = (numItems - numToDisplay) * buttonHeight;
if maxRange < 0 then
maxRange = 0;
end
scrollBar:SetMinMaxValues(0, maxRange);
self:SetScrollStep(buttonHeight);
scrollBar:SetStepsPerPage(numToDisplay - 1);
scrollChildFrame:SetHeight(scrollChildHeight);
-- Arrow button handling
if scrollBar:GetValue() == 0 then
scrollUpButton:Disable();
else
scrollUpButton:Enable();
end
if (scrollBar:GetValue() - scrollFrameHeight) == 0 then
scrollDownButton:Disable();
else
scrollDownButton:Enable();
end
end
return showScrollBar;
end,
};
local OnVerticalScrollUpdate = function(self)
self:Redraw();
end;
StdUi.FauxScrollFrameEvents = {
OnVerticalScroll = function(self, value)
value = round(value);
local panel = self.panel;
panel:DoVerticalScroll(
value,
panel.lineHeight,
OnVerticalScrollUpdate
);
end
};
--- Works pretty much the same as scroll frame however it does not have smooth scroll and only display a certain amount
--- of items
function StdUi:FauxScrollFrame(parent, width, height, displayCount, lineHeight, scrollChild)
local panel = self:ScrollFrame(parent, width, height, scrollChild);
panel.lineHeight = lineHeight;
panel.displayCount = displayCount;
for k, v in pairs(self.FauxScrollFrameMethods) do
panel[k] = v;
end
for k, v in pairs(self.FauxScrollFrameEvents) do
panel.scrollFrame:SetScript(k, v);
end
return panel;
end
----------------------------------------------------
--- HybridScrollFrame
----------------------------------------------------
StdUi.HybridScrollFrameMethods = {
Update = function(self, totalHeight)
local range = floor(totalHeight - self.scrollChild:GetHeight() + 0.5);
if range > 0 and self.scrollBar then
local _, maxVal = self.scrollBar:GetMinMaxValues();
if math.floor(self.scrollBar:GetValue()) >= math.floor(maxVal) then
self.scrollBar:SetMinMaxValues(0, range);
if range < maxVal then
if math.floor(self.scrollBar:GetValue()) ~= math.floor(range) then
self.scrollBar:SetValue(range);
else
-- If we've scrolled to the bottom, we need to recalculate the offset.
self:SetOffset(self, range);
end
end
else
self.scrollBar:SetMinMaxValues(0, range)
end
self.scrollBar:Enable();
self:UpdateScrollBarState();
self.scrollBar:Show();
elseif self.scrollBar then
self.scrollBar:SetValue(0);
if self.scrollBar.doNotHide then
self.scrollBar:Disable();
self.scrollBar.ScrollUpButton:Disable();
self.scrollBar.ScrollDownButton:Disable();
self.scrollBar.ThumbTexture:Hide();
else
self.scrollBar:Hide();
end
end
self.range = range;
self.totalHeight = totalHeight;
self.scrollFrame:UpdateScrollChildRect();
end,
SetData = function(self, data)
self.data = data;
end,
SetUpdateFunction = function(self, updateFn)
self.updateFn = updateFn;
end,
UpdateScrollBarState = function(self, currValue)
if not currValue then
currValue = self.scrollBar:GetValue();
end
self.scrollBar.ScrollUpButton:Enable();
self.scrollBar.ScrollDownButton:Enable();
local minVal, maxVal = self.scrollBar:GetMinMaxValues();
if currValue >= maxVal then
self.scrollBar.ThumbTexture:Show();
if self.scrollBar.ScrollDownButton then
self.scrollBar.ScrollDownButton:Disable()
end
end
if currValue <= minVal then
self.scrollBar.ThumbTexture:Show();
if self.scrollBar.ScrollUpButton then
self.scrollBar.ScrollUpButton:Disable();
end
end
end,
GetOffset = function(self)
return math.floor(self.offset or 0), (self.offset or 0);
end,
SetOffset = function(self, offset)
local items = self.items;
local itemHeight = self.itemHeight;
local element, overflow;
local scrollHeight = 0;
if self.dynamic then
--This is for frames where items will have different heights
if offset < itemHeight then
-- a little optimization
element, scrollHeight = 0, offset;
else
element, scrollHeight = self.dynamic(offset);
end
else
element = offset / itemHeight;
overflow = element - math.floor(element);
scrollHeight = overflow * itemHeight;
end
if math.floor(self.offset or 0) ~= math.floor(element) and self.updateFn then
self.offset = element;
self:UpdateItems();
else
self.offset = element;
end
self.scrollFrame:SetVerticalScroll(scrollHeight);
end,
CreateItems = function(self, data, create, update, padding, oX, oY)
local scrollChild = self.scrollChild;
local itemHeight = 0;
local numItems = #data;
--initialPoint = initialPoint or 'TOPLEFT';
--initialRelative = initialRelative or 'TOPLEFT';
--point = point or 'TOPLEFT';
--relativePoint = relativePoint or 'BOTTOMLEFT';
--offsetX = offsetX or 0;
--offsetY = offsetY or 0;
if not self.items then
self.items = {};
end
self.data = data;
self.createFn = create;
self.updateFn = update;
self.itemPadding = padding;
self.stdUi:ObjectList(scrollChild, self.items, create, update, data, padding, oX, oY,
function (i, totalHeight, lih)
return totalHeight > self:GetHeight() + lih;
end);
if self.items[1] then
itemHeight = round(self.items[1]:GetHeight() + padding);
end
self.itemHeight = itemHeight;
local totalHeight = numItems * itemHeight;
self.scrollFrame:SetVerticalScroll(0);
local scrollBar = self.scrollBar;
scrollBar:SetMinMaxValues(0, totalHeight);
scrollBar.itemHeight = itemHeight;
self:SetScrollStep(itemHeight / 2);
-- one additional item was added above. Need to remove that,
-- and one more to make the current bottom the new top (and vice versa)
scrollBar:SetStepsPerPage(numItems - 2);
scrollBar:SetValue(0);
self:Update(totalHeight);
end,
UpdateItems = function(self)
local count = #self.data;
local offset = self:GetOffset();
local items = self:GetItems();
for i = 1, #items do
local item = items[i];
local index = offset + i;
if index <= count then
self.updateFn(self.scrollChild, item, self.data[index], index, i);
end
end
local firstButton = items[1];
local totalHeight = 0;
if firstButton then
totalHeight = count * (firstButton:GetHeight() + self.itemPadding);
end
self:Update(totalHeight, self:GetHeight());
end,
GetItems = function(self)
return self.items;
end,
SetDoNotHideScrollBar = function(self, doNotHide)
if not self.scrollBar or self.scrollBar.doNotHide == doNotHide then
return ;
end
self.scrollBar.doNotHide = doNotHide;
self:Update(self.totalHeight or 0, self.scrollChild:GetHeight());
end,
ScrollToIndex = function(self, index, getHeightFunc)
local totalHeight = 0;
local scrollFrameHeight = self:GetHeight();
for i = 1, index do
local entryHeight = getHeightFunc(i);
if i == index then
local offset = 0;
-- we don't need to do anything if the entry is fully displayed with the scroll all the way up
if totalHeight + entryHeight > scrollFrameHeight then
if (entryHeight > scrollFrameHeight) then
-- this entry is larger than the entire scrollframe, put it at the top
offset = totalHeight;
else
-- otherwise place it in the center
local diff = scrollFrameHeight - entryHeight;
offset = totalHeight - diff / 2;
end
-- because of valuestep our positioning might change
-- we'll do the adjustment ourselves to make sure the entry ends up above the center rather than below
local valueStep = self.scrollBar:GetValueStep();
offset = offset + valueStep - mod(offset, valueStep);
-- but if we ended up moving the entry so high up that its top is not visible, move it back down
if offset > totalHeight then
offset = offset - valueStep;
end
end
self.scrollBar:SetValue(offset);
break;
end
totalHeight = totalHeight + entryHeight;
end
end
};
local HybridScrollBarOnValueChanged = function(self, value)
local widget = self.scrollFrame.panel;
value = round(value);
widget:SetOffset(value);
widget:UpdateScrollBarState(value);
end
function StdUi:HybridScrollFrame(parent, width, height, scrollChild)
local panel = self:ScrollFrame(parent, width, height, scrollChild);
panel.scrollBar:SetScript('OnValueChanged', HybridScrollBarOnValueChanged);
panel.scrollFrame:SetScript('OnScrollRangeChanged', nil);
panel.scrollFrame:SetScript('OnVerticalScroll', nil);
for k, v in pairs(self.HybridScrollFrameMethods) do
panel[k] = v;
end
--for k, v in pairs(self.HybridScrollFrameEvents) do
-- panel.scrollFrame:SetScript(k, v);
--end
return panel;
end
StdUi:RegisterModule(module, version);