Jump to content
Sign in to follow this  
Kered13

OptionsGadgets: A library to make writing Options easier. (UPDATED 2015-09-19)

Recommended Posts

I've started working on a framework to make writing options easier. I'd have called it Widgets, but the name was already taken :P

The core idea is to have containers which can hold gadgets (including primitive options UI elements and other containers) and can calculate their own size. Then instead of having to calculate x and y coordinates manually, it can be automatically laid out. Additionally, containers return the input values of their nested elements in an easy to use manner, and can be given optional smart functionality to process aggregated values before returning them.

Features:
-Automatic layout.
-Easy extraction of values.
-Can optionally specify padding and alignment to fine-tune layout.
-Easily extensible by both composition and inheritance of existing elements.
-Programmable, gadgets can be controlled by external code. Allows for simple animation and more complex behavior.
-Extensively documented.

Up to date code for the library, along with a simple test widget, can be found here (you'll need to download both the Utils and OptionsGadgets folders).

Supported gadgets:

​Containers:
-VerticalContainer
-HorizontalContainer
-FreeContainer
-TabbedContainer
-TableContainer

Basic gadgets:

-EmptyGadget
-UiEditBoxGadget
-NumberEditBoxGadget
-UiSliderGadget
-UiButtonGadget
-UiButtonVerticalGadget
-UiCheckBoxGadget
-UiComboBoxGadget
-UiScrollSelectGadget
-UiColorPickerGadget
-TextGadget (supports text formatting)
-TextBoxGadget
-DividerGadget
-BindGadget

Advanced gadgets:
-ToolTipGadget
-EditableSliderGadget
-RadioButtonGadget
-TogglableButtonGadget
-SimpleColorGadget
-DrawableGadget
-HideableGadget
-CollapsibleGadget
-ScrollableGadget
-ConVarGadget (automatically updates convars)
-OrderingGadget (drag and drop to order a list)
-WindowGadget (can be opened and closed and moved)
-OkCancelWindowGadget (ok to save changes, cancel to discard)

If you find any bugs, contact me and I will try to get them fixed.

An example widget using the code:

-- KeredOptionsGadgetTest
-- Author: Kered13

require "base/internal/ui/Kered13/OptionsGadgets/OptionsGadgets"
require "base/internal/ui/Kered13/Utils/ConVars"
require "base/internal/ui/Kered13/Utils/Utils"
require "base/internal/ui/reflexcore"

KeredOptionsGadgetTest = {};
registerWidget("KeredOptionsGadgetTest");

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function KeredOptionsGadgetTest:initialize()
	consolePrint("Initializing KeredOptionsGadgetTest...");
	
	-- load data stored in engine.
	self.userData = loadUserData();
	
	-- ensure it has what we need
	CheckSetDefaultValue(self, "userData", "table", {});
	CheckSetDefaultValue(self.userData, "str", "string", "foo");
	CheckSetDefaultValue(self.userData, "num", "number", 10);
	CheckSetDefaultValue(self.userData, "bool", "boolean", false);
	CheckSetDefaultValue(self.userData, "check", "boolean", false);
	CheckSetDefaultValue(self.userData, "combo", "string", "One");
	CheckSetDefaultValue(self.userData, "scroll", "string", "A");
	CheckSetDefaultValue(self.userData, "color", "table", Color(145, 20, 190));
	CheckSetDefaultValue(self.userData, "radio", "number", 1);
	
	fixedCreateConsoleVariable("test", "string");
	
	self:defineDrawOptions();
end

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function KeredOptionsGadgetTest:draw()
	TextGadget(toString(self.userData)):draw(-700, -200)
end

--------------------------------------------------------------------------------
-- Using this function, which is only called at initialization, to create the UI
-- gadgets allows us to carry gadget state over from frame to frame.
--------------------------------------------------------------------------------
function KeredOptionsGadgetTest:defineDrawOptions()
	local user = self.userData;
	local tablePage = TableContainer(
		{
			TextGadget("Sample"),
			TextGadget("More samples"),
		},
		{
			TextGadget("Another row"),
			TextGadget("With longer text"),
		}
	);
	self.window = OkCancelWindowGadget("This is a window",
		ConVarGadget("test", UiEditBoxGadget(200):setValue(ConVars.test or "")));
	self.optionsUi = TabbedContainer(
		{"First", VerticalContainer(
			TextGadget("Hello"):setStyle({fontSize=40, fontColor=Color(255, 150, 0, 128)}),
			TextGadget("World"):setStyle({fontFace=FONT_TEXT2_BOLD}),
			DrawableGadget(200, 200, function() self:draw() end):setClipOutOfBounds(false),
			{1, UiScrollSelectionGadget({"A", "B", "C"}, 150, 75):setValue(user.scroll)},
			FreeContainer(
				{{x=0, y=20}, TextGadget("Label:"):setWidth(100)},
				{"editBox", {x=20, y=0}, UiEditBoxGadget(100):setValue(user.str)}
			):setHeight(100)
		):setWidth(500)},
		{"page", "Second", VerticalContainer(
			{"slider", EditableSliderGadget(400, 80, 0, 100):setValue(user.num)},
			EmptyGadget(0, 100),
			HorizontalContainer(
				{"checkbox", UiCheckBoxGadget("A checkbox."):setValue(user.check)},
				{"button", ToolTipGadget("This is a tooltip!",
					TogglableButtonGadget("Button", nil, 100, 40):setValue(user.bool))}
			),
			{"combobox", UiComboBoxGadget({"One", "Two", "Three"}, 80):setValue(user.combo)},
			ScrollableGadget(200, VerticalContainer(
				{"radio", RadioButtonGadget({"One", "Two", "Three"}):setValue(user.radio)},
				{"colorpicker", CollapsibleGadget("Color (click to open):", UiColorPickerGadget():setValue(user.color)):setOpen(true)}
			))
		)},
		{"Third", VerticalContainer(
			tablePage,
			TextGadget("This is multi-\nline text."),
			TextBoxGadget(200, "This is long text that will be wrapped by the text box. It has a " ..
							   "veryveryveryveryveryveryveryveryveryverylongword. And a bit more text."),
			UiButtonGadget("Show window", nil, 140, 40):setOnValueChange(function(value)
				if value then
					self.window:setVisibility(true);
				end
			end),
			self.window
		)}
	);
end

------------------------------------------
------------------------------------------
function KeredOptionsGadgetTest:drawOptions(x, y)
	local user = self.userData;
	local result = self.optionsUi:draw(x, y);
	
	user.scroll = result[1];
	user.str = result.editBox;
	user.num = result.page.slider;
	user.check = result.page.checkbox;
	user.bool = result.page.button;
	user.combo = result.page.combobox;
	user.radio = result.page.radio;
	user.color = result.page.colorpicker;
	saveUserData(user);
	
	self.optionsHeight = self.optionsUi:getHeight();
end

--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
function KeredOptionsGadgetTest:getOptionsHeight()
	return self.optionsHeight;
end
Edited by Kered13

Share this post


Link to post
Share on other sites

I have all the basic gadget implemented now and a few useful additional ones, such as a CollapsableGadget that can hide a section of the options until it's opened. I have some ideas for a few more gadgets still, like TabbedGadget that allows you to have multiple organized pages of options (like how the entire options menu is organized). I've also slightly restructured parts of the design to make it work better. The link to the up to date source is in the top post.

But the reason I made this post is because I've noticed a bug, I'm not sure if it's my bug or a library bug, but I want to try to fix it. When the options get long enough to start scrolling, some elements aren't being cutoff. You can see them scrolling above and below the options window. It's usable, but kind of annoying. From looking at the options menu code I think the problem might have something to do with nvgScissor and nvgRestore, which I think control the viewable area, but I don't know how they work. Can someone either point to some documentation on these nvg function, or describe how they work at a high level to me?

EDIT: Found documentation here, and found the problem. There are a couple bugs in the reflexcore library. First, uiScrollSelection sets nvgScissor when it should set nvgIntersectScissor so that it behaves correctly in nested scrolling. It also should call nvgSave and nvgRestore instead of nvgResetScissor. Second, uiColorDisplay calls nvgSave then nvgRestore twice, which messes up the nvg stack for everything after it.

Share this post


Link to post
Share on other sites

After putting a lot of work into it for the last week or so, I think I finally have a stable version of OptionsGadgets. It's changed a lot since my last post, I've added a lot more functionality made it a lot nicer to use. If you tried using this before, you'll probably need to update your code, but it will be worth it. You can find the latest version in the source link in the OP, which I've updated.

Share this post


Link to post
Share on other sites

#grandpasWarStories

 

theoretically you could do ex.

local Tables = require "base/internal/ui/myLib/Tables"

in your Tables.lua you make everything local and just have a

return Tables

at the end of the file and nothing had ever to be exposed to the global namespace ( _G ), but that the module system was accidentally nuked by the security fix.

 

oh and fun side fact require is not caseInsensitive so require "a" and require "A" would load file `a` two times

and unlike as in node.js you cant do relative requires

Share this post


Link to post
Share on other sites

I've added two new gadgets:

BindGadget: This captures inputs, such as for creating binds. Despite the name, it doesn't actually create the binds. This is so that users of the gadgets can do more complex processing if necessary.

ConVarGadget: This gadget synchronizes a console variable with another gadget, which is wraps. Changes to the console variable will change the wrapped gadget, and changes to the gadget will change the console variable.

Share this post


Link to post
Share on other sites

EDIT: I think I've tracked it down to something to do with cvars. If I delete the cvars section from game.cfg (see below) or give test a value, the user data loads correctly. However, when I close and reload the game, I get this message in the console:

unknown command "ui_keredoptionsgadgettest_test asdf"

Note that if I run the above command manually in the console it works correctly. The next time I close the game, the test cvar is saved as empty, which causes user data to not load on the next restart. So it seems like there are two parts to this:

1. Cvars are not loading properly from the config.

2. Empty cvars are saved, but prevent userdata from loading correctly.

Of course, it's possible that I'm using cvars incorrectly, but if so I'm not sure how. Perhaps something to do with the order of loading versus calling widgetCreateConsoleVariable? But I'm not sure at what stage cvars are loaded.

I've encountered a bug in my test widget, KeredOptionsGadgetTest, that has me stumped. At some point in the last few days of working, the widget stopped saving it's user data. However, there are no error messages and when I examine the user data right after calling saveUserData(), it is entirely correct. But when I reload the widget, loadUserData returns nil. All my other widgets work fine, despite doing essentially the same thing. I can look at game.cfg and see that all the data is saved correctly, but it won't load. Since I can't examine the behavior of loadUserData any closer, I'm stuck unless someone can point out something stupid I'm overlooking.

The complete code is here. Relevant snippets below.

 

function KeredOptionsGadgetTest:initialize()
	consolePrint("Initializing KeredOptionsGadgetTest...");
	
	-- load data stored in engine.
	self.userData = loadUserData();
	fixedPrint(toString(self.userData));  -- userData is nil after loading data.
	
	-- ensure it has what we need
	CheckSetDefaultValue(self, "userData", "table", {});
	CheckSetDefaultValue(self.userData, "str", "string", "foo");
	CheckSetDefaultValue(self.userData, "num", "number", 10);
	CheckSetDefaultValue(self.userData, "bool", "boolean", false);
	CheckSetDefaultValue(self.userData, "check", "boolean", false);
	CheckSetDefaultValue(self.userData, "combo", "string", "One");
	CheckSetDefaultValue(self.userData, "scroll", "string", "A");
	CheckSetDefaultValue(self.userData, "color", "table", Color(145, 20, 190));
	CheckSetDefaultValue(self.userData, "radio", "number", 1);
	
	self:defineDrawOptions();
end

...

function KeredOptionsGadgetTest:drawOptions(x, y)
	local user = self.userData;
	local result = self.optionsUi:draw(x, y);
	
	user.scroll = result[4];
	user.str = result.editBox;
	user.num = result.page.slider;
	user.check = result.page.checkbox;
	user.bool = result.page.button;
	user.combo = result.page.combobox;
	user.radio = result.page.radio;
	user.color = result.page.colorpicker;
	saveUserData(user);
	fixedPrint(toString(user));  -- This shows that user is exactly as expected. Changes in the options menu are reflected here.
	fixedPrint(toString(loadUserData());  -- This returns nil.
	
	self.optionsHeight = self.optionsUi:getHeight();
end
game.cfg:

...
ui_define KeredOptionsGadgetTest = 
{
	offset = { x = 0.000000, y = 0.000000 };
	anchor = { x = 0, y = 0 };
	zIndex = 0.000000;
	scale = 1.000000;
	visible = false;
	userData = 
	{
		bool = false;
		radio = 1;
		num = 10;
		str = "asdf";
		color = 
		{
			r = 145;
			b = 190;
			a = 255;
			g = 20;
		};
		scroll = "B";
		combo = "One";
		check = false;
	};
	cvars =
	{
		test = ;
	};
}
...

Share this post


Link to post
Share on other sites

I found the with convars, it's a bug relating to how string convars are saved. I've reported it and written a work-around for the time being.

In other additions, I've added two new gadgets:
-TextBoxGadget, which I had wanted to write before, but the necessary NVG function wasn't available. Instead, I implemented text wrapping myself. Remarkably, it worked in the first try.
-OrderingGadget, which allows drag-and-drop ordering of items (which can themselves be gadgets).

Now I've about run out of good ideas for gadgets, so if anyone has an interesting suggestion, feel free to share.

Edited by Kered13

Share this post


Link to post
Share on other sites

I just pushed a big update with a bunch of backend changes. New features are the addition of windows that can be open and closed and moved around and an onValueChange function, which is useful for event driven programming. For example, you could make a window appear when a button is pressed without having to ever read the value of the button. There's also some changes to how containers work (no implicit keys anymore).

Due to the size of this update it wouldn't surprise me if there are some bugs I still need to work out.

In the near future there's probably going to be more clean up changes, because I'm really unhappy with a few parts of the code right now.

Edited by Kered13

Share this post


Link to post
Share on other sites

The timer still doesn't work for me for some reason, same issue! 

Tryed some of the others to, all of them does the same thing! 

Edit: 
Haha nvm. I did put them in the widgets folder, didnt know i needed to create new folders! :D 

Edited by I4N

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×