This project shows an example of using lua as a scriptable ui logic component.
The binding is done via luabind and the dependency injection using wallaroo.
Starting from lul_example.cpp:
Catalog catalog;
lul::CreateSomeUI("MainUI",catalog);
lul::CreateTestLogic("TestLogic",catalog);
catalog
is here the named object pool that can contain instances of the interfaces defined in lul/iui.h. There you will find an IView
and an ILogic
simple interfaces. The two lul::Create*
calls instantiate and register the named objects that implement the interfaces. As you can see, the knowledge of the concrete types is not required here.
The implementation of the UI contains a placeholder for the logic component as a Wallaroo 'plug':
class SomeUI : public lul::iui::IView
{
public:
SomeUI(): logic( "logic", RegistrationToken() ) {}
// ...
private:
wallaroo::Plug<lul::iui::ILogic> logic;
};
Now, the components are linked together using the special Wallaroo syntax that is quite natural language - readable:
wallaroo_within(catalog)
{
use("TestLogic").as("logic").of("MainUI");
}
All objects are specified by their name, so you need discipline, error handling or a fool-proof configuration to resolve all the necessary dependencies. The 'TestLogic' logic implementation does nothing apart from echoing events to the console. Hence, when you call the following,
Using the interface instances
std::shared_ptr<IView> MainUI = catalog["MainUI"];
MainUI->ReceiveEvent("test");
the UI forwards the call to it's logic meber that echoes SomeUILogic received event: test
.
The main strength of Wallaroo is perhaps the ability to reconfigure the dependency resolutions at runtime. Thus, calling
wallaroo_within(catalog)
{
use("LuaCounterLogic").as("logic").of("MainUI");
use("MainUI").as("view").of("LuaCounterLogic");
use("LuaCounterContext").as("context").of("LuaCounterLogic");
}
updates the dependency of 'MainUI', and adds a new dependency of the dependency - the LuaCounterContext
. What exactly is runtime-linked to what can be read in plain English from the code itself.
Now, the behavior of the UI is totally changed, and it uses two more components.
The logic component implemented in Lua just as an example. The Lua script text is embedded into the c++ executable for demo purposes. Any other kind of script deployment can replace this one. The logic component knows of the UI interface and of the state interface, which is provided in example/iluacontext.h. The interface is extermely simple - it can only accept a Lua state, into which it binds a C++ context class, which will be instantiated from Lua. The state is a simple counter with an upper and a lower limit.
The logic component exposes only three functions to the Lua state: UnknownEvent
, Alert
and Updated
that are conveniently bound to the logic instance using LuaBind:
luaL_openlibs(L);
module(L) [
def("UnknownEvent",
tag_function<void(std::string)>(
boost::bind(&LuaLogic::UnknownEvent,this,_1)
)),
//...
];
The events from the outside are forwarded to the state machine actions implementation, which is defined in context.lua.
Coming back to the UI: the example program consists of a chain of calls to the ReceiveEvent
method of the UI, simulating external input events. These are forwarded exactly to the logic, as the primary concern of the UI might be presentation solely.
Since the state machine is implemented in Lua, lua functions for the named actions are called if the event name is present. These, in turn, evaluate the state of the state machine, which is implemented in C++ and is bound to the Lua state.
During the action, the script calls some functions that report the update back to the UI. I.e. Updated
and Alert
:
function Increment()
if context.state < context.upper_limit then
context.state = context.state+context.increment;
Updated( 'counter' , tostring(context.state) )
else
Alert( [[Upper limit of ]] .. context.upper_limit .. [[ reached]] )
end
end
These updates are then forwarded back to the UI via the IView
interface:
void Updated(std::string key,std::string value) const {
if (view)
view->ShowValue(key,value);
}
This example is not intended as a recipe for implementing UIs with logic and state. It rather tries to demonstrate that, given independent components, you are much more free to restructure and reimplement your software architecture. For example, as in this example, having a state in C++ is not really needed need. To move that concern to the Lua side would be very simple - delete the binding and define one Lua table with number values in the script. There's no message passing strategy involved in the example, but incorporating it should not be a huge refactoring, since the interfaces are rather lean and mimic message passing.
No specific license is attached. Use at your own risk.