Unify scrollable list code

The goal of this patch set is to eliminate the code duplication
caused by copy/paste of the code in the file selector, listbox,
and partition list GUI elements. Those classes will now utilize a
single GUIScrollList class that will handle rendering and
scrolling.

Change-Id: I0cb98ab36cf47178296034293435225658c779cd
diff --git a/gui/scrolllist.cpp b/gui/scrolllist.cpp
new file mode 100644
index 0000000..9e8db4c
--- /dev/null
+++ b/gui/scrolllist.cpp
@@ -0,0 +1,709 @@
+/*
+	Copyright 2013 bigbiff/Dees_Troy TeamWin
+	This file is part of TWRP/TeamWin Recovery Project.
+
+	TWRP is free software: you can redistribute it and/or modify
+	it under the terms of the GNU General Public License as published by
+	the Free Software Foundation, either version 3 of the License, or
+	(at your option) any later version.
+
+	TWRP is distributed in the hope that it will be useful,
+	but WITHOUT ANY WARRANTY; without even the implied warranty of
+	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+	GNU General Public License for more details.
+
+	You should have received a copy of the GNU General Public License
+	along with TWRP.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <string.h>
+
+extern "C" {
+#include "../twcommon.h"
+#include "../minuitwrp/minui.h"
+}
+
+#include "rapidxml.hpp"
+#include "objects.hpp"
+#include "../data.hpp"
+
+const int SCROLLING_SPEED_DECREMENT = 12; // friction
+const int SCROLLING_FLOOR = 10;	// minimum pixels for scrolling to start or stop
+const int SCROLLING_MULTIPLIER = 2; // initial speed of kinetic scrolling
+const float SCROLLING_SPEED_LIMIT = 2.5; // maximum number of items to scroll per update
+
+GUIScrollList::GUIScrollList(xml_node<>* node) : GUIObject(node)
+{
+	xml_attribute<>* attr;
+	xml_node<>* child;
+	int header_separator_color_specified = 0, header_separator_height_specified = 0, header_text_color_specified = 0, header_background_color_specified = 0;
+
+	firstDisplayedItem = mItemSpacing = mFontHeight = mSeparatorH = y_offset = scrollingSpeed = 0;
+	maxIconWidth = maxIconHeight =  mHeaderIconHeight = mHeaderIconWidth = 0;
+	mHeaderSeparatorH = mHeaderIsStatic = mHeaderH = actualItemHeight = 0;
+	mBackground = mFont = mHeaderIcon = NULL;
+	mBackgroundW = mBackgroundH = 0;
+	mFastScrollW = mFastScrollLineW = mFastScrollRectW = mFastScrollRectH = 0;
+	lastY = last2Y = fastScroll = 0;
+	mUpdate = 0;
+	touchDebounce = 6;
+	ConvertStrToColor("black", &mBackgroundColor);
+	ConvertStrToColor("black", &mHeaderBackgroundColor);
+	ConvertStrToColor("black", &mSeparatorColor);
+	ConvertStrToColor("black", &mHeaderSeparatorColor);
+	ConvertStrToColor("white", &mFontColor);
+	ConvertStrToColor("white", &mHeaderFontColor);
+	ConvertStrToColor("white", &mFastScrollLineColor);
+	ConvertStrToColor("white", &mFastScrollRectColor);
+	hasHighlightColor = false;
+	hasFontHighlightColor = false;
+	selectedItem = NO_ITEM;
+
+	// Load header text
+	child = node->first_node("header");
+	if (child)
+	{
+		attr = child->first_attribute("icon");
+		if (attr)
+			mHeaderIcon = PageManager::FindResource(attr->value());
+
+		attr = child->first_attribute("background");
+		if (attr)
+		{
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mHeaderBackgroundColor);
+			header_background_color_specified = -1;
+		}
+		attr = child->first_attribute("textcolor");
+		if (attr)
+		{
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mHeaderFontColor);
+			header_text_color_specified = -1;
+		}
+		attr = child->first_attribute("separatorcolor");
+		if (attr)
+		{
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mHeaderSeparatorColor);
+			header_separator_color_specified = -1;
+		}
+		attr = child->first_attribute("separatorheight");
+		if (attr) {
+			string parsevalue = gui_parse_text(attr->value());
+			mHeaderSeparatorH = atoi(parsevalue.c_str());
+			header_separator_height_specified = -1;
+		}
+	}
+	child = node->first_node("text");
+	if (child)  mHeaderText = child->value();
+
+	memset(&mHighlightColor, 0, sizeof(COLOR));
+	child = node->first_node("highlight");
+	if (child) {
+		attr = child->first_attribute("color");
+		if (attr) {
+			hasHighlightColor = true;
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mHighlightColor);
+		}
+	}
+
+	// Simple way to check for static state
+	mLastHeaderValue = gui_parse_text(mHeaderText);
+	if (mLastHeaderValue != mHeaderText)
+		mHeaderIsStatic = 0;
+	else
+		mHeaderIsStatic = -1;
+
+	child = node->first_node("background");
+	if (child)
+	{
+		attr = child->first_attribute("resource");
+		if (attr)
+			mBackground = PageManager::FindResource(attr->value());
+		attr = child->first_attribute("color");
+		if (attr)
+		{
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mBackgroundColor);
+			if (!header_background_color_specified)
+				ConvertStrToColor(color, &mHeaderBackgroundColor);
+		}
+	}
+
+	// Load the placement
+	LoadPlacement(node->first_node("placement"), &mRenderX, &mRenderY, &mRenderW, &mRenderH);
+	SetActionPos(mRenderX, mRenderY, mRenderW, mRenderH);
+
+	// Load the font, and possibly override the color
+	child = node->first_node("font");
+	if (child)
+	{
+		attr = child->first_attribute("resource");
+		if (attr)
+			mFont = PageManager::FindResource(attr->value());
+
+		attr = child->first_attribute("color");
+		if (attr)
+		{
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mFontColor);
+			if (!header_text_color_specified)
+				ConvertStrToColor(color, &mHeaderFontColor);
+		}
+
+		attr = child->first_attribute("spacing");
+		if (attr) {
+			string parsevalue = gui_parse_text(attr->value());
+			mItemSpacing = atoi(parsevalue.c_str());
+		}
+
+		attr = child->first_attribute("highlightcolor");
+		memset(&mFontHighlightColor, 0, sizeof(COLOR));
+		if (attr)
+		{
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mFontHighlightColor);
+			hasFontHighlightColor = true;
+		}
+	}
+
+	// Load the separator if it exists
+	child = node->first_node("separator");
+	if (child)
+	{
+		attr = child->first_attribute("color");
+		if (attr)
+		{
+			std::string color = attr->value();
+			ConvertStrToColor(color, &mSeparatorColor);
+			if (!header_separator_color_specified)
+				ConvertStrToColor(color, &mHeaderSeparatorColor);
+		}
+
+		attr = child->first_attribute("height");
+		if (attr) {
+			string parsevalue = gui_parse_text(attr->value());
+			mSeparatorH = atoi(parsevalue.c_str());
+			if (!header_separator_height_specified)
+				mHeaderSeparatorH = mSeparatorH;
+		}
+	}
+
+	// Fast scroll colors
+	child = node->first_node("fastscroll");
+	if (child)
+	{
+		attr = child->first_attribute("linecolor");
+		if(attr)
+			ConvertStrToColor(attr->value(), &mFastScrollLineColor);
+
+		attr = child->first_attribute("rectcolor");
+		if(attr)
+			ConvertStrToColor(attr->value(), &mFastScrollRectColor);
+
+		attr = child->first_attribute("w");
+		if (attr) {
+			string parsevalue = gui_parse_text(attr->value());
+			mFastScrollW = atoi(parsevalue.c_str());
+		}
+
+		attr = child->first_attribute("linew");
+		if (attr) {
+			string parsevalue = gui_parse_text(attr->value());
+			mFastScrollLineW = atoi(parsevalue.c_str());
+		}
+
+		attr = child->first_attribute("rectw");
+		if (attr) {
+			string parsevalue = gui_parse_text(attr->value());
+			mFastScrollRectW = atoi(parsevalue.c_str());
+		}
+
+		attr = child->first_attribute("recth");
+		if (attr) {
+			string parsevalue = gui_parse_text(attr->value());
+			mFastScrollRectH = atoi(parsevalue.c_str());
+		}
+	}
+
+	// Retrieve the line height
+	mFontHeight = gr_getMaxFontHeight(mFont ? mFont->GetResource() : NULL);
+	mHeaderH = mFontHeight;
+
+	if (mHeaderIcon && mHeaderIcon->GetResource())
+	{
+		mHeaderIconWidth = gr_get_width(mHeaderIcon->GetResource());
+		mHeaderIconHeight = gr_get_height(mHeaderIcon->GetResource());
+		if (mHeaderIconHeight > mHeaderH)
+			mHeaderH = mHeaderIconHeight;
+		if (mHeaderIconWidth > maxIconWidth)
+			maxIconWidth = mHeaderIconWidth;
+	}
+
+	mHeaderH += mItemSpacing + mHeaderSeparatorH;
+	actualItemHeight = mFontHeight + mItemSpacing + mSeparatorH;
+	if (mHeaderH < actualItemHeight)
+		mHeaderH = actualItemHeight;
+
+	if (actualItemHeight / 3 > 6)
+		touchDebounce = actualItemHeight / 3;
+
+	if (mBackground && mBackground->GetResource())
+	{
+		mBackgroundW = gr_get_width(mBackground->GetResource());
+		mBackgroundH = gr_get_height(mBackground->GetResource());
+	}
+}
+
+GUIScrollList::~GUIScrollList()
+{
+	delete mHeaderIcon;
+	delete mBackground;
+	delete mFont;
+}
+
+void GUIScrollList::SetMaxIconSize(int w, int h)
+{
+	if (w > maxIconWidth)
+		maxIconWidth = w;
+	if (h > maxIconHeight)
+		maxIconHeight = h;
+	if (maxIconHeight > mFontHeight) {
+		actualItemHeight = maxIconHeight + mItemSpacing + mSeparatorH;
+		if (actualItemHeight > mHeaderH)
+			mHeaderH = actualItemHeight;
+	}
+}
+
+void GUIScrollList::SetVisibleListLocation(size_t list_index)
+{
+	// This will make sure that the item indicated by list_index is visible on the screen
+	size_t lines = GetDisplayItemCount(), listSize = GetItemCount();
+
+	if (list_index <= (unsigned)firstDisplayedItem) {
+		// list_index is above the currently displayed items, put the selected item at the very top
+		firstDisplayedItem = list_index;
+		y_offset = 0;
+	} else if (list_index >= firstDisplayedItem + lines) {
+		// list_index is below the currently displayed items, put the selected item at the very bottom
+		firstDisplayedItem = list_index - lines + 1;
+		if (GetDisplayRemainder() != 0) {
+			// There's a partial row displayed, set the scrolling offset so that the selected item really is at the very bottom
+			firstDisplayedItem--;
+			y_offset = GetDisplayRemainder() - actualItemHeight;
+		} else {
+			// There's no partial row so zero out the offset
+			y_offset = 0;
+		}
+	}
+	scrollingSpeed = 0; // stop kinetic scrolling on setting visible location
+	mUpdate = 1;
+}
+
+int GUIScrollList::Render(void)
+{
+	if(!isConditionTrue())
+		return 0;
+
+	// First step, fill background
+	gr_color(mBackgroundColor.red, mBackgroundColor.green, mBackgroundColor.blue, 255);
+	gr_fill(mRenderX, mRenderY + mHeaderH, mRenderW, mRenderH - mHeaderH);
+
+	// Next, render the background resource (if it exists)
+	if (mBackground && mBackground->GetResource())
+	{
+		int mBackgroundX = mRenderX + ((mRenderW - mBackgroundW) / 2);
+		int mBackgroundY = mRenderY + ((mRenderH - mBackgroundH) / 2);
+		gr_blit(mBackground->GetResource(), 0, 0, mBackgroundW, mBackgroundH, mBackgroundX, mBackgroundY);
+	}
+
+	// This tells us how many lines we can actually render
+	size_t lines = GetDisplayItemCount();
+
+	size_t listSize = GetItemCount();
+	int listW = mRenderW;
+
+	if (listSize <= lines) {
+		hasScroll = false;
+		scrollingSpeed = 0;
+		lines = listSize;
+		y_offset = 0;
+	} else {
+		hasScroll = true;
+		listW -= mFastScrollW; // space for fast scroll
+		lines++;
+		if (lines < listSize)
+			lines++;
+	}
+
+	void* fontResource = NULL;
+	if (mFont)  fontResource = mFont->GetResource();
+
+	int yPos = mRenderY + mHeaderH + y_offset;
+	int fontOffsetY = (int)((actualItemHeight - mFontHeight) / 2);
+
+	// render all visible items
+	for (size_t line = 0; line < lines; line++)
+	{
+		size_t itemindex = line + firstDisplayedItem;
+		if (itemindex >= listSize)
+			break;
+
+		// get item data
+		Resource* icon;
+		std::string label;
+		if (GetListItem(itemindex, icon, label))
+			break;
+
+		if (hasHighlightColor && itemindex == selectedItem) {
+			// Highlight the item background of the selected item
+			gr_color(mHighlightColor.red, mHighlightColor.green, mHighlightColor.blue, 255);
+			int HighlightHeight = actualItemHeight;
+			if (yPos + HighlightHeight > mRenderY + mRenderH) {
+				HighlightHeight = mRenderY + mRenderH - yPos;
+			}
+			gr_fill(mRenderX, yPos, mRenderW, HighlightHeight);
+		}
+
+		if (hasFontHighlightColor && itemindex == selectedItem) {
+			// Use the highlight color for the font
+			gr_color(mFontHighlightColor.red, mFontHighlightColor.green, mFontHighlightColor.blue, 255);
+		} else {
+			// Set the color for the font
+			gr_color(mFontColor.red, mFontColor.green, mFontColor.blue, 255);
+		}
+
+		if (icon && icon->GetResource()) {
+			int currentIconHeight = gr_get_height(icon->GetResource());
+			int currentIconWidth = gr_get_width(icon->GetResource());
+			int currentIconOffsetY = (int)((actualItemHeight - currentIconHeight) / 2);
+			int currentIconOffsetX = (maxIconWidth - currentIconWidth) / 2;
+			int rect_y = 0, image_y = (yPos + currentIconOffsetY);
+			if (image_y + currentIconHeight > mRenderY + mRenderH)
+				rect_y = mRenderY + mRenderH - image_y;
+			else
+				rect_y = currentIconHeight;
+			gr_blit(icon->GetResource(), 0, 0, currentIconWidth, rect_y, mRenderX + currentIconOffsetX, image_y);
+		}
+
+		gr_textExWH(mRenderX + maxIconWidth + 5, yPos + fontOffsetY, label.c_str(), fontResource, mRenderX + listW, mRenderY + mRenderH);
+
+		// Add the separator
+		if (yPos + actualItemHeight < mRenderH + mRenderY) {
+			gr_color(mSeparatorColor.red, mSeparatorColor.green, mSeparatorColor.blue, 255);
+			gr_fill(mRenderX, yPos + actualItemHeight - mSeparatorH, listW, mSeparatorH);
+		}
+
+		// Move the yPos
+		yPos += actualItemHeight;
+	}
+
+	// Render the Header (last so that it overwrites the top most row for per pixel scrolling)
+	// First step, fill background
+	gr_color(mHeaderBackgroundColor.red, mHeaderBackgroundColor.green, mHeaderBackgroundColor.blue, 255);
+	gr_fill(mRenderX, mRenderY, mRenderW, mHeaderH);
+
+	// Now, we need the header (icon + text)
+	yPos = mRenderY;
+	{
+		Resource* headerIcon;
+		int mIconOffsetX = 0;
+
+		// render the icon if it exists
+		headerIcon = mHeaderIcon;
+		if (headerIcon && headerIcon->GetResource())
+		{
+			gr_blit(headerIcon->GetResource(), 0, 0, mHeaderIconWidth, mHeaderIconHeight, mRenderX + ((mHeaderIconWidth - maxIconWidth) / 2), (yPos + (int)((mHeaderH - mHeaderIconHeight) / 2)));
+			mIconOffsetX = maxIconWidth;
+		}
+
+		// render the text
+		gr_color(mHeaderFontColor.red, mHeaderFontColor.green, mHeaderFontColor.blue, 255);
+		gr_textExWH(mRenderX + mIconOffsetX + 5, yPos + (int)((mHeaderH - mFontHeight) / 2), mLastHeaderValue.c_str(), fontResource, mRenderX + mRenderW, mRenderY + mRenderH);
+
+		// Add the separator
+		gr_color(mHeaderSeparatorColor.red, mHeaderSeparatorColor.green, mHeaderSeparatorColor.blue, 255);
+		gr_fill(mRenderX, yPos + mHeaderH - mHeaderSeparatorH, mRenderW, mHeaderSeparatorH);
+	}
+
+	// render fast scroll
+	lines = GetDisplayItemCount();
+	if (hasScroll) {
+		int startX = listW + mRenderX;
+		int fWidth = mRenderW - listW;
+		int fHeight = mRenderH - mHeaderH;
+
+		// line
+		gr_color(mFastScrollLineColor.red, mFastScrollLineColor.green, mFastScrollLineColor.blue, 255);
+		gr_fill(startX + fWidth/2, mRenderY + mHeaderH, mFastScrollLineW, mRenderH - mHeaderH);
+
+		// rect
+		int pct = 0;
+		if (GetDisplayRemainder() != 0) {
+			// Properly handle the percentage if a partial line is present
+			int partial_line_size = actualItemHeight - GetDisplayRemainder();
+			pct = ((firstDisplayedItem*actualItemHeight - y_offset)*100)/(listSize*actualItemHeight-((lines + 1)*actualItemHeight) + partial_line_size);
+		} else {
+			pct = ((firstDisplayedItem*actualItemHeight - y_offset)*100)/(listSize*actualItemHeight-lines*actualItemHeight);
+		}
+		int mFastScrollRectX = startX + (fWidth - mFastScrollRectW)/2;
+		int mFastScrollRectY = mRenderY+mHeaderH + ((fHeight - mFastScrollRectH)*pct)/100;
+
+		gr_color(mFastScrollRectColor.red, mFastScrollRectColor.green, mFastScrollRectColor.blue, 255);
+		gr_fill(mFastScrollRectX, mFastScrollRectY, mFastScrollRectW, mFastScrollRectH);
+	}
+	mUpdate = 0;
+	return 0;
+}
+
+int GUIScrollList::Update(void)
+{
+	if(!isConditionTrue())
+		return 0;
+
+	if (!mHeaderIsStatic) {
+		std::string newValue = gui_parse_text(mHeaderText);
+		if (mLastHeaderValue != newValue) {
+			mLastHeaderValue = newValue;
+			mUpdate = 1;
+		}
+	}
+
+	// Handle kinetic scrolling
+	int maxScrollDistance = actualItemHeight * SCROLLING_SPEED_LIMIT;
+	if (scrollingSpeed == 0) {
+		// Do nothing
+		return 0;
+	} else if (scrollingSpeed > 0) {
+		if (scrollingSpeed < maxScrollDistance)
+			y_offset += scrollingSpeed;
+		else
+			y_offset += maxScrollDistance;
+		scrollingSpeed -= SCROLLING_SPEED_DECREMENT;
+	} else if (scrollingSpeed < 0) {
+		if (abs(scrollingSpeed) < maxScrollDistance)
+			y_offset += scrollingSpeed;
+		else
+			y_offset -= maxScrollDistance;
+		scrollingSpeed += SCROLLING_SPEED_DECREMENT;
+	}
+	if (abs(scrollingSpeed) < SCROLLING_FLOOR)
+		scrollingSpeed = 0;
+	HandleScrolling();
+	mUpdate = 1;
+
+	return 0;
+}
+
+size_t GUIScrollList::HitTestItem(int x, int y)
+{
+	// We only care about y position
+	if (y < mRenderY || y - mRenderY <= mHeaderH || y - mRenderY > mRenderH)
+		return NO_ITEM;
+
+	int startSelection = (y - mRenderY - mHeaderH);
+
+	// Locate the correct item
+	size_t actualSelection = firstDisplayedItem;
+	int selectY = y_offset;
+	while (selectY + actualItemHeight < startSelection) {
+		selectY += actualItemHeight;
+		actualSelection++;
+	}
+
+	if (actualSelection < GetItemCount())
+		return actualSelection;
+
+	return NO_ITEM;
+}
+
+int GUIScrollList::NotifyTouch(TOUCH_STATE state, int x, int y)
+{
+	if(!isConditionTrue())
+		return -1;
+
+	switch (state)
+	{
+	case TOUCH_START:
+		if (hasScroll && x >= mRenderX + mRenderW - mFastScrollW)
+			fastScroll = 1; // Initial touch is in the fast scroll region
+		if (scrollingSpeed != 0) {
+			selectedItem = NO_ITEM; // this allows the user to tap the list to stop the scrolling without selecting the item they tap
+			scrollingSpeed = 0; // stop scrolling on a new touch
+		} else if (!fastScroll) {
+			// find out which item the user touched
+			selectedItem = HitTestItem(x, y);
+		}
+		if (selectedItem != NO_ITEM)
+			mUpdate = 1;
+		lastY = last2Y = y;
+		break;
+
+	case TOUCH_DRAG:
+		if (fastScroll)
+		{
+			int pct = ((y-mRenderY-mHeaderH)*100)/(mRenderH-mHeaderH);
+			int totalSize = GetItemCount();
+			int lines = GetDisplayItemCount();
+
+			float l = float((totalSize-lines)*pct)/100;
+			if(l + lines >= totalSize)
+			{
+				firstDisplayedItem = totalSize - lines;
+				if (GetDisplayRemainder() != 0) {
+					// There's a partial row displayed, set the scrolling offset so that the last item really is at the very bottom
+					firstDisplayedItem--;
+					y_offset = GetDisplayRemainder() - actualItemHeight;
+				} else {
+					// There's no partial row so zero out the offset
+					y_offset = 0;
+				}
+			}
+			else
+			{
+				if (l < 0)
+					l = 0;
+				firstDisplayedItem = l;
+				y_offset = -(l - int(l))*actualItemHeight;
+				if (GetDisplayRemainder() != 0) {
+					// There's a partial row displayed, make sure y_offset doesn't go past the max
+					if (firstDisplayedItem == totalSize - lines - 1 && y_offset < GetDisplayRemainder() - actualItemHeight)
+						y_offset = GetDisplayRemainder() - actualItemHeight;
+				} else if (firstDisplayedItem == totalSize - lines)
+					y_offset = 0;
+			}
+
+			selectedItem = NO_ITEM;
+			mUpdate = 1;
+			scrollingSpeed = 0; // prevent kinetic scrolling when using fast scroll
+			break;
+		}
+
+		// Provide some debounce on initial touches
+		if (selectedItem != NO_ITEM && abs(y - lastY) < touchDebounce) {
+			mUpdate = 1;
+			break;
+		}
+
+		selectedItem = NO_ITEM; // nothing is selected because we dragged too far
+		// Handle scrolling
+		if (hasScroll) {
+			y_offset += y - lastY; // adjust the scrolling offset based on the difference between the starting touch and the current touch
+			last2Y = lastY; // keep track of previous y locations so that we can tell how fast to scroll for kinetic scrolling
+			lastY = y; // update last touch to the current touch so we can tell how far and what direction we scroll for the next touch event
+
+			HandleScrolling();
+		} else
+			y_offset = 0;
+		mUpdate = 1;
+		break;
+
+	case TOUCH_RELEASE:
+		fastScroll = 0;
+		if (selectedItem != NO_ITEM) {
+			// We've selected an item!
+			NotifySelect(selectedItem);
+			mUpdate = 1;
+
+			DataManager::Vibrate("tw_button_vibrate");
+			selectedItem = NO_ITEM;
+		} else {
+			// Start kinetic scrolling
+			scrollingSpeed = lastY - last2Y;
+			if (abs(scrollingSpeed) > SCROLLING_FLOOR)
+				scrollingSpeed *= SCROLLING_MULTIPLIER;
+			else
+				scrollingSpeed = 0;
+		}
+	case TOUCH_REPEAT:
+	case TOUCH_HOLD:
+		break;
+	}
+	return 0;
+}
+
+void GUIScrollList::HandleScrolling()
+{
+	// handle dragging downward, scrolling upward
+	// the offset should always be <= 0 and > -actualItemHeight, adjust the first display row and offset as needed
+	while(firstDisplayedItem && y_offset > 0) {
+		firstDisplayedItem--;
+		y_offset -= actualItemHeight;
+	}
+	if (firstDisplayedItem == 0 && y_offset > 0)
+		y_offset = 0; // user kept dragging downward past the top of the list, so always reset the offset to 0 since we can't scroll any further in this direction
+
+	// handle dragging upward, scrolling downward
+	int totalSize = GetItemCount();
+	int lines = GetDisplayItemCount(); // number of full lines our list can display at once
+	int bottom_offset = GetDisplayRemainder() - actualItemHeight; // extra display area that can display a partial line for per pixel scrolling
+
+	// the offset should always be <= 0 and > -actualItemHeight, adjust the first display row and offset as needed
+	while (firstDisplayedItem + lines + (bottom_offset ? 1 : 0) < totalSize && abs(y_offset) > actualItemHeight) {
+		firstDisplayedItem++;
+		y_offset += actualItemHeight;
+	}
+	// Check if we dragged too far, set the list at the bottom and adjust offset as needed
+	if (bottom_offset != 0 && firstDisplayedItem + lines + 1 >= totalSize && y_offset <= bottom_offset) {
+		firstDisplayedItem = totalSize - lines - 1;
+		y_offset = bottom_offset;
+	} else if (firstDisplayedItem + lines >= totalSize && y_offset < 0) {
+		firstDisplayedItem = totalSize - lines;
+		y_offset = 0;
+	}
+}
+
+int GUIScrollList::GetDisplayItemCount()
+{
+	return (mRenderH - mHeaderH) / (actualItemHeight);
+}
+
+int GUIScrollList::GetDisplayRemainder()
+{
+	return (mRenderH - mHeaderH) % actualItemHeight;
+}
+
+int GUIScrollList::NotifyVarChange(const std::string& varName, const std::string& value)
+{
+	GUIObject::NotifyVarChange(varName, value);
+
+	if(!isConditionTrue())
+		return 0;
+
+	if (!mHeaderIsStatic) {
+		std::string newValue = gui_parse_text(mHeaderText);
+		if (mLastHeaderValue != newValue) {
+			mLastHeaderValue = newValue;
+			firstDisplayedItem = 0;
+			y_offset = 0;
+			scrollingSpeed = 0; // stop kinetic scrolling on variable changes
+			mUpdate = 1;
+		}
+	}
+	return 0;
+}
+
+int GUIScrollList::SetRenderPos(int x, int y, int w /* = 0 */, int h /* = 0 */)
+{
+	mRenderX = x;
+	mRenderY = y;
+	if (w || h)
+	{
+		mRenderW = w;
+		mRenderH = h;
+	}
+	SetActionPos(mRenderX, mRenderY, mRenderW, mRenderH);
+	mUpdate = 1;
+	return 0;
+}
+
+void GUIScrollList::SetPageFocus(int inFocus)
+{
+	if (inFocus) {
+		NotifyVarChange("", ""); // This forces a check for the header text
+		scrollingSpeed = 0; // stop kinetic scrolling on page changes
+		mUpdate = 1;
+	}
+}