Browse Source

Luainvaders sample: In high scores, replace datagrid usage with data binding and RCSS table. In options, demonstrate usage of data bindings combined with Lua scripting.

Michael Ragazzon 5 years ago
parent
commit
5438be5a35

+ 71 - 50
Samples/luainvaders/data/high_score.rml

@@ -1,45 +1,40 @@
 <rml>
-	<head>
-		<title>High Scores</title>
-		<link type="text/template" href="window.rml" />
-		<style>
-			body
-			{
-				width: 440px;
-				height: 440px;
-				
-				margin: auto;
-			}
+<head>
+	<title>High Scores</title>
+	<link type="text/template" href="window.rml" />
+	<style>
+		body
+		{
+			width: 440px;
+			height: 440px;
 			
-			div#title_bar div#icon
-			{
-				decorator: image( icon-hiscore );
-			}
+			margin: auto;
+		}
+		
+		div#title_bar div#icon
+		{
+			decorator: image( icon-hiscore );
+		}
+		defender
+		{
+			display: inline-block;
+			width: 64px;
+			height: 16px;
 			
-			datagrid
-			{
-				margin-bottom: 20px;
-			}
-			
-			datagrid datagridbody
-			{
-				min-height: 200px;
-			}
-			datagrid datagridrow
-			{
-				padding-top: 7px;
-				padding-bottom: 7px;
-			}
-			defender
-			{
-				display: block;
-				width: 64px;
-				height: 16px;
-				
-				decorator: defender( high_scores_defender.tga );
-			}
-		</style>
-		<script>
+			decorator: defender( high_scores_defender.tga );
+		}
+		tbody tr {
+			height: 30px;
+		}
+		tbody td
+		{
+			padding-top: 5px;
+			height: 30px;
+			white-space: nowrap;
+			overflow: hidden;
+		}
+	</style>
+	<script>
 HighScore = HighScore or {}
 
 function HighScore.OnKeyDown(event)
@@ -47,15 +42,41 @@ function HighScore.OnKeyDown(event)
 		Game.SetHighScoreName(Element.As.ElementFormControlInput(event.current_element).value)
 	end
 end
-		</script>
-	</head>
-	<body template="luawindow" onload="Window.OnWindowLoad(document) Game.SubmitHighScore()">
-		<datagrid id="datagrid" source="high_scores.scores">
-			<col fields="name,name_required" formatter="name" width="40%">Pilot</col>
-			<col fields="colour" formatter="ship" width="20%">Ship</col>
-			<col fields="wave" width="20%">Wave</col>
-			<col fields="score" width="20%">Score</col>
-		</datagrid>
-		<button onclick="Game.SetHighScoreName('Anon') Window.LoadMenu('main_menu',document)">Main Menu</button>
-	</body>
+	</script>
+</head>
+<body template="luawindow" onload="Window.OnWindowLoad(document) Game.SubmitHighScore()">
+	<table data-model="high_scores">
+		<thead>
+			<tr>
+				<td style="width: 200%; margin-left: 10px;">Pilot</td>
+				<td style="min-width: 64px;">Ship</td>
+				<td>Wave</td>
+				<td style="min-width: 64px;">Score</td>
+			</tr>
+		</thead>
+		<tbody>
+			<tr data-for="score : scores">
+				<td data-if="score.name_required">
+					<input id="player_input" type="text" name="name" onkeydown="HighScore.OnKeyDown(event)" autofocus/>
+				</td>
+				<td data-if="!score.name_required">
+					{{score.name}}
+				</td>
+				<td>
+					<defender data-style-color="score.colour"/>
+				</td>
+				<td>
+					{{score.wave}}
+				</td>
+				<td>
+					{{score.score}}
+				</td>
+			</tr>
+			<tr data-if="scores.size == 0">
+				<td colspan="4"><em>No scores recorded. Go play!</em></td>
+			</tr>
+		</tbody>
+	</table>
+	<button onclick="Game.SetHighScoreName('Anon') Window.LoadMenu('main_menu',document)">Main Menu</button>
+</body>
 </rml>

+ 3 - 3
Samples/luainvaders/data/main_menu.rml

@@ -27,9 +27,9 @@ function MainMenu.CloseLogo(document)
 end
 		</script>
 	</head>
-	<body data-model="menu" template="luawindow" onload="Window.OnWindowLoad(document) document.context:LoadDocument('luainvaders/data/logo.rml'):Show()" onunload="MainMenu.CloseLogo(document)">
-		<button onclick="document.context:LoadDocument('luainvaders/data/start_game.rml'):Show() document:Close()">{{button1}}</button><br />
-		<button onclick="Window.LoadMenu('high_score',document)">{{button2}}</button><br />
+	<body template="luawindow" onload="Window.OnWindowLoad(document) document.context:LoadDocument('luainvaders/data/logo.rml'):Show()" onunload="MainMenu.CloseLogo(document)">
+		<button onclick="document.context:LoadDocument('luainvaders/data/start_game.rml'):Show() document:Close()">Start Game</button><br />
+		<button onclick="Window.LoadMenu('high_score',document)">High Scores</button><br />
 		<button onclick="Window.LoadMenu('options',document)">Options</button><br />
 		<button onclick="Window.LoadMenu('help',document)">Help</button><br />
 		<button onclick="Game.Shutdown()">Exit</button>

+ 27 - 14
Samples/luainvaders/data/options.rml

@@ -26,6 +26,14 @@
 		<script>
 Options = Options or {}
 
+if Options.datamodel == nil then
+	-- Create a new data model
+	Options.datamodel = rmlui.contexts["main"]:OpenDataModel("options", {
+		graphics = 'ok',
+		options_changed = false,
+	})
+end
+
 function Options.Serialize(filename,options)
     local file,message = io.open(filename,'w+') --w+ will erase the previous data
     if file == nil then Log.Message(Log.logtype.error, "Error saving options in options.rml: " .. message) return end
@@ -66,6 +74,7 @@ function Options.LoadOptions(document)
     --because everything is loaded as a string, we have to fool around with the boolean variables
     AsInput(document:GetElementById('reverb')).checked = (options['reverb'] == 'true')
     AsInput(document:GetElementById('3d')).checked = (options['3d'] == 'true')
+	Options.datamodel.options_changed = false
 end
 
 function Options.SaveOptions(event)
@@ -83,34 +92,38 @@ function Options.SaveOptions(event)
     
     Options.Serialize('options.dat',options)
 end
-		
-function Options.DisplayBadGraphics(document, display)
-	if display then
-		document:GetElementById('bad_warning').style.display = 'block'
-	else
-		document:GetElementById('bad_warning').style.display = 'none'
+
+function Options.OnChange(event)
+	Options.datamodel.options_changed = true
+	
+	local name = event.target_element.attributes.name
+	local value = event.parameters.value
+	
+	-- When a radio button gets checked, it sets the value parameter. When it gets unchecked, the value is nil.
+	if name == 'graphics' and value ~= nil then
+		Options.datamodel.graphics = value
 	end
 end
 
 	</script>
 	</head>
 	<body template="luawindow" onload="Window.OnWindowLoad(document) Options.LoadOptions(document)">
-		<form onsubmit="Options.SaveOptions(event) Window.LoadMenu('main_menu',document)">
+		<form data-model="options" onsubmit="Options.SaveOptions(event) Window.LoadMenu('main_menu',document)" onchange="Options.OnChange(event)">
 			<div>
 				<p>
 					Graphics:<br />
-					<input id="good" type="radio" name="graphics" value="good" onchange="Options.DisplayBadGraphics(document, false)"/> Good<br />
-					<input id="ok" type="radio" name="graphics" value="ok" checked="true" onchange="Options.DisplayBadGraphics(document, false)"/> OK<br />
-					<input id="bad" type="radio" name="graphics" value="bad" onchange="Options.DisplayBadGraphics(document, true)" /> Bad<br />
+					<input id="good" type="radio" name="graphics" value="good" /> Good<br />
+					<input id="ok" type="radio" name="graphics" value="ok" checked /> OK<br />
+					<input id="bad" type="radio" name="graphics" value="bad" /> Bad<br />
 				</p>
-				<p id="bad_warning" style="display: none;">Are you sure about this? Bad graphics are just plain <em>bad.</em></p>
+				<p data-if="graphics == 'bad'">Are you sure about this? Bad graphics are just plain <em>bad.</em></p>
 				<p>
 					Audio:<br />
-					<input id="reverb" type="checkbox" name="reverb" value="true" checked="true" /> Reverb<br />
-					<input id="3d" type="checkbox" name="3d" value="false" checked="false" /> 3D Spatialisation
+					<input id="reverb" type="checkbox" name="reverb" checked /> Reverb<br />
+					<input id="3d" type="checkbox" name="3d" /> 3D Spatialisation
 				</p>
 			</div>
-			<input type="submit" name="button" value="accept">Accept</input>
+			<input type="submit" name="button" value="accept" data-attrif-disabled="!options_changed">Accept</input>
 			<input type="submit" name="button" value="cancel">Cancel</input>
 		</form>
 	</body>

+ 1 - 1
Samples/luainvaders/data/pause.rml

@@ -20,7 +20,7 @@
 	<body template="luawindow" onload="Window.OnWindowLoad(document) Game.SetPaused(true)" onunload="Game.SetPaused(false)">
 		<br />
 		<p>Are you sure you want to end this game?</p>
-		<button onclick="Window.LoadMenu('high_score',document) document.context.documents['game_window']:Close()">Yes</button>
+		<button onclick="Window.LoadMenu('high_score',document) document.context.documents['game_window']:Close()" autofocus>Yes</button>
 		<button onclick="document:Close()">No!</button>
 	</body>
 </rml>

+ 15 - 7
Samples/luainvaders/data/start_game.rml

@@ -21,6 +21,14 @@
 				width: 200px;
 				margin: auto;
 			}
+			color
+			{
+				display: inline-block;
+				width: 9px;
+				height: 9px;
+				border: 1px #666;
+				margin-right: 0.5em;
+			}
 		</style>
 		<script>
 StartGame = StartGame or {}
@@ -54,13 +62,13 @@ end
 				<p>
 					Colour:<br />
 					<select name="colour">
-						<option value="233,116,81">Burnt Sienna</option>
-						<option value="127,255,0">Chartreuse</option>
-						<option value="21,96,189">Denim</option>
-						<option value="246,74,138">French Rose</option>
-						<option value="255,0,255">Fuschia</option>
-						<option value="218,165,32">Goldenrod</option>
-						<option selected value="255,255,240">Ivory</option>
+						<option value="233,116,81"><color style="background: rgb(233,116,81)"/>Burnt Sienna</option>
+						<option value="127,255,0"><color style="background: rgb(127,255,0)"/>Chartreuse</option>
+						<option value="21,96,189"><color style="background: rgb(21,96,189)"/>Denim</option>
+						<option value="246,74,138"><color style="background: rgb(246,74,138)"/>French Rose</option>
+						<option value="255,0,255"><color style="background: rgb(255,0,255)"/>Fuchsia</option>
+						<option value="218,165,32"><color style="background: rgb(218,165,32)"/>Goldenrod</option>
+						<option selected value="255,255,240"><color style="background: rgb(255,255,240)"/>Ivory</option>
 					</select>
 				</p>
 			</div>

+ 0 - 38
Samples/luainvaders/lua/start.lua

@@ -1,43 +1,5 @@
-
-Formatters = Formatters or {} --table to hold the formatters so that they don't get GC'd
-
---this will use two different ways to show how to create DataFormatter objects
---first way is to set the FormatData function explicitly
-local formatter = DataFormatter.new("name")
-formatter.FormatData = function(raw_data)
-    --[[ 
-    Data format:
-    raw_data[0] is the name.
-    raw_data[1] is a bool - True means the name has to be entered. False means the name has been entered already.
-    ]]
-    
-    formatted_data = ""
-    
-    if (raw_data[1] == "1") then
-        --because we know that it is only used in the high_score.rml file, use that namespace for the OnKeyDown function
-        formatted_data = "<input id=\"player_input\" type=\"text\" name=\"name\" onkeydown=\"HighScore.OnKeyDown(event)\" autofocus/>"
-    else
-        formatted_data = raw_data[0]
-    end
-        
-    return formatted_data
-end
-Formatters["name"] = formatter --using "name" as the key only for convenience
-
---second example uses a previously defined function, and passes in as a parameter for 'new'
-function SecondFormatData(raw_data)
-    return "<defender style=\"color: rgba(" .. raw_data[0] .. ");\" />"
-end
-Formatters["ship"] = DataFormatter.new("ship",SecondFormatData)
-
-
 function Startup()
 	maincontext = rmlui.contexts["main"]
-	datamodel = maincontext:OpenDataModel("menu", {
-		button1 = "1",
-		button2 = "High Scores",
-	})
-	datamodel.button1 = "Start Game"	-- Change button1
 	maincontext:LoadDocument("luainvaders/data/background.rml"):Show()
 	maincontext:LoadDocument("luainvaders/data/main_menu.rml"):Show()
 end

+ 47 - 98
Samples/luainvaders/src/HighScores.cpp

@@ -28,20 +28,37 @@
 
 #include "HighScores.h"
 #include <RmlUi/Core/TypeConverter.h>
+#include <RmlUi/Core/Context.h>
+#include <RmlUi/Core/DataModelHandle.h>
 #include <stdio.h>
+#include <algorithm>
 
 HighScores* HighScores::instance = nullptr;
 
-HighScores::HighScores() : Rml::DataSource("high_scores")
+HighScores::HighScores(Rml::Context* context)
 {
 	RMLUI_ASSERT(instance == nullptr);
 	instance = this;
 
-	for (int i = 0; i < NUM_SCORES; i++)
+	Rml::DataModelConstructor constructor = context->CreateDataModel("high_scores");
+	if (!constructor)
+		return;
+
+	if (auto score_handle = constructor.RegisterStruct<Score>())
 	{
-		scores[i].score = -1;
+		score_handle.RegisterMember("name_required", &Score::name_required);
+		score_handle.RegisterMember("name", &Score::name);
+		score_handle.RegisterMemberFunc("colour", &Score::GetColour);
+		score_handle.RegisterMember("wave", &Score::wave);
+		score_handle.RegisterMember("score", &Score::score);
 	}
 
+	constructor.RegisterArray<ScoreList>();
+
+	constructor.Bind("scores", &scores);
+
+	model_handle = constructor.GetModelHandle();
+
 	LoadScores();
 }
 
@@ -54,9 +71,9 @@ HighScores::~HighScores()
 	instance = nullptr;
 }
 
-void HighScores::Initialise()
+void HighScores::Initialise(Rml::Context* context)
 {
-	new HighScores();
+	new HighScores(context);
 }
 
 void HighScores::Shutdown()
@@ -64,62 +81,10 @@ void HighScores::Shutdown()
 	delete instance;
 }
 
-void HighScores::GetRow(Rml::StringList& row, const Rml::String& table, int row_index, const Rml::StringList& columns)
-{
-	if (table == "scores")
-	{
-		for (size_t i = 0; i < columns.size(); i++)
-		{
-			if (columns[i] == "name")
-			{
-				row.push_back(scores[row_index].name);
-			}
-			else if (columns[i] == "name_required")
-			{
-				row.push_back(Rml::CreateString(4, "%d", scores[row_index].name_required));
-			}
-			else if (columns[i] == "score")
-			{
-				row.push_back(Rml::CreateString(32, "%d", scores[row_index].score));
-			}
-			else if (columns[i] == "colour")
-			{
-				Rml::String colour_string;
-				Rml::TypeConverter< Rml::Colourb, Rml::String >::Convert(scores[row_index].colour, colour_string);
-				row.push_back(colour_string);
-			}
-			else if (columns[i] == "wave")
-			{
-				row.push_back(Rml::CreateString(8, "%d", scores[row_index].wave));
-			}
-		}
-	}
-}
-
-int HighScores::GetNumRows(const Rml::String& table)
-{
-	if (table == "scores")
-	{
-		for (int i = 0; i < NUM_SCORES; i++)
-		{
-			if (scores[i].score == -1)
-			{
-				return i;
-			}
-		}
-
-		return NUM_SCORES;
-	}
-
-	return 0;
-}
-
 int HighScores::GetHighScore()
 {
-	if (instance->GetNumRows("scores") == 0)
-	{
+	if (instance->scores.empty())
 		return 0;
-	}
 
 	return instance->scores[0].score;
 }
@@ -137,53 +102,37 @@ void HighScores::SubmitScore(const Rml::Colourb& colour, int wave, int score)
 // Sets the name of the last player to submit their score.
 void HighScores::SubmitName(const Rml::String& name)
 {
-	for (int i = 0; i < instance->GetNumRows("scores"); i++)
+	for (Score& score : instance->scores)
 	{
-		if (instance->scores[i].name_required)
+		if (score.name_required)
 		{
-			instance->scores[i].name = name;
-			instance->scores[i].name_required = false;
-			instance->NotifyRowChange("scores", i, 1);
+			score.name = name;
+			score.name_required = false;
+
+			instance->model_handle.DirtyVariable("scores");
 		}
 	}
 }
 
 void HighScores::SubmitScore(const Rml::String& name, const Rml::Colourb& colour, int wave, int score, bool name_required)
 {
-	for (int i = 0; i < NUM_SCORES; i++)
-	{
-		if (score > scores[i].score)
-		{
-			// If we've already got the maximum number of scores, then we have
-			// to send a RowsRemoved message as we're going to delete the last
-			// row from the data source.
-			bool max_rows = scores[NUM_SCORES - 1].score != -1;
+	Score entry;
+	entry.name = name;
+	entry.colour = colour;
+	entry.wave = wave;
+	entry.score = score;
+	entry.name_required = name_required;
 
-			// Push down all the other scores.
-			for (int j = NUM_SCORES - 1; j > i; j--)
-			{
-				scores[j] = scores[j - 1];
-			}
+	auto it = std::find_if(scores.begin(), scores.end(), [score](const Score& other) { return score > other.score; });
 
-			// Insert our new score.
-			scores[i].name = name;
-			scores[i].colour = colour;
-			scores[i].wave = wave;
-			scores[i].score = score;
-			scores[i].name_required = name_required;
+	scores.insert(it, std::move(entry));
 
-			// Send the row removal message (if necessary).
-			if (max_rows)
-			{
-				NotifyRowRemove("scores", NUM_SCORES - 1, 1);
-			}
+	constexpr size_t MaxNumberScores = 10;
 
-			// Then send the rows added message.
-			NotifyRowAdd("scores", i, 1);
+	if (scores.size() > MaxNumberScores)
+		scores.pop_back();
 
-			return;
-		}
-	}
+	model_handle.DirtyVariable("scores");
 }
 
 void HighScores::LoadScores()
@@ -204,7 +153,7 @@ void HighScores::LoadScores()
 				int wave;
 				int score;
 
-				if (Rml::TypeConverter< Rml::String , Rml::Colourb >::Convert(score_parts[1], colour) &&
+				if (Rml::TypeConverter< Rml::String, Rml::Colourb >::Convert(score_parts[1], colour) &&
 					Rml::TypeConverter< Rml::String, int >::Convert(score_parts[2], wave) &&
 					Rml::TypeConverter< Rml::String, int >::Convert(score_parts[3], score))
 				{
@@ -223,13 +172,13 @@ void HighScores::SaveScores()
 
 	if (scores_file)
 	{
-		for (int i = 0; i < GetNumRows("scores"); i++)
+		for (const Score& score : scores)
 		{
 			Rml::String colour_string;
-			Rml::TypeConverter< Rml::Colourb, Rml::String >::Convert(scores[i].colour, colour_string);
-      
-			Rml::String score = Rml::CreateString(1024, "%s\t%s\t%d\t%d\n", scores[i].name.c_str(), colour_string.c_str(), scores[i].wave, scores[i].score);
-			fputs(score.c_str(), scores_file);		
+			Rml::TypeConverter< Rml::Colourb, Rml::String >::Convert(score.colour, colour_string);
+
+			Rml::String score_str = Rml::CreateString(1024, "%s\t%s\t%d\t%d\n", score.name.c_str(), colour_string.c_str(), score.wave, score.score);
+			fputs(score_str.c_str(), scores_file);
 		}
 
 		fclose(scores_file);

+ 11 - 10
Samples/luainvaders/src/HighScores.h

@@ -29,20 +29,15 @@
 #ifndef RMLUI_LUAINVADERS_HIGHSCORES_H
 #define RMLUI_LUAINVADERS_HIGHSCORES_H
 
-#include <RmlUi/Core/Elements/DataSource.h>
 #include <RmlUi/Core/Types.h>
+#include <RmlUi/Core/DataModelHandle.h>
 
-const int NUM_SCORES = 10;
-
-class HighScores : public Rml::DataSource
+class HighScores
 {
 public:
-	static void Initialise();
+	static void Initialise(Rml::Context* context);
 	static void Shutdown();
 
-	void GetRow(Rml::StringList& row, const Rml::String& table, int row_index, const Rml::StringList& columns);
-	int GetNumRows(const Rml::String& table);
-
 	static int GetHighScore();
 
 	/// Two functions to add a score to the chart.
@@ -54,7 +49,7 @@ public:
 	static void SubmitName(const Rml::String& name);
 
 private:
-	HighScores();
+	HighScores(Rml::Context* context);
 	~HighScores();
 
 	static HighScores* instance;
@@ -70,9 +65,15 @@ private:
 		Rml::Colourb colour;
 		int score;
 		int wave;
+
+		void GetColour(Rml::Variant& variant) {
+			variant = "rgba(" + Rml::ToString(colour) + ')';
+		}
 	};
+	using ScoreList = Rml::Vector< Score >;
+	ScoreList scores;
 
-	Score scores[NUM_SCORES];
+	Rml::DataModelHandle model_handle;
 };
 
 #endif

+ 1 - 1
Samples/luainvaders/src/main.cpp

@@ -115,7 +115,7 @@ int main(int, char**)
 	Rml::Factory::RegisterDecoratorInstancer("defender", &decorator_defender);
 
 	// Construct the game singletons.
-	HighScores::Initialise();
+	HighScores::Initialise(context);
 
 	// Fire off the startup script.
     LuaInterface::Initialise(Rml::Lua::Interpreter::GetLuaState()); //the tables/functions defined in the samples