GIX is a high-productivity way to write Scene2D layouts and components. You write the layout, some logic, and data binding in HTML. The reason for this is that productivity is much higher for some types of projects. GIX supports a component system and hot reloading.
This means that you can edit your views and save, and the game, or UI, is automatically updated without a restart. This only applies to the HTML - the components' Java code is not hot reloaded (yet).
Templates are "type safe" in that they are validated at parse time. You can't pass an object to
a Label constructor and have it silently fail - you get a nice error message. You can then fix, save,
and continue - without restarting.
All LibGDX and vis-ui components should be supported (see How it Works). This also means you can write
any Java class that extends Actor and it can become a <Component> used in your template!
Let's write our template:
<VisWindownew="Todo App" setFillParent="true"><VisTable><Repeatwith={todos}as="todo"><VisTable:row><VisLabelnew={todo.name}></VisLabel><FlowGroupnew="false"><VisTextButtonnew="Delete" addListener={todo.deleteListener}></VisTextButton></FlowGroup></VisTable:row></Repeat></VisTable><VisTable><VisTable:row><VisTextFieldid="field"></VisTextField><VisTextButtonnew="Add" addListener={addTodoListener}></VisTextButton></VisTable:row></VisTable></VisWindow>We're using vis-ui here but regular Scene2D components work too.
Now let's write a corresponding class which will define the state passed to this template:
publicclassTodoComponentextendsGIXComponent<TodoComponent.TodoState> {
publicTodoComponent(GIXParentparent) {
super(parent, Gdx.files.internal("todo.html"));
setState(newTodoState());
}
publicclassTodoState {
Array<Todo> todos = Array.with(newTodo("Buy Eggs"), newTodo("Write Java"));
ClickListeneraddTodoListener = newClickListener() {
publicvoidclicked(InputEventevent, floatx, floaty) {
VisTextFieldfield = ((VisTextField) getById("field"));
todos.add(newTodo(field.getText()));
field.setText("");
setState(TodoState.this);
}
};
publicclassTodo {
publicStringname;
Todo(Stringname) {
this.name = name;
}
ClickListenerdeleteListener = newClickListener() {
publicvoidclicked(InputEventevent, floatx, floaty) {
todos.removeValue(Todo.this, true);
setState(TodoState.this);
}
};
}
}
}Now let's use it (just add to stage, but GIXParent can also accept another component):
publicclassTodoAppextendsApplicationAdapter {
privateStagestage;
@Overridepublicvoidcreate() {
stage = newStage(newScreenViewport());
Gdx.input.setInputProcessor(stage);
GIXComponent.addComponentClassPaths('com.yourapp.components'); // what package will your components be in?GIXComponent.setDevMode(true); // for hot reloadnewTodoComponent(newGIXParent(stage));
}
@Overridepublicvoidrender() {
ScreenUtils.clear(1, 1, 1, 1);
stage.act(Math.min(Gdx.graphics.getDeltaTime(), 1 / 30f));
GIXComponent.tickComponents(); // for hot reloadstage.draw();
}
}Children work this way:
publicclassChildComponentextendsGIXComponent<ChildComponent.ChildState> {
publicChildComponent(GIXParentparent) {
super(parent, Gdx.files.internal("child.html"));
setState(newChildState());
}
publicChildComponent(GIXNodeparent) {
this(newGIXParent(parent));
}
publicclassChildState {
}
publicvoidsetCustomData(booleansomeData) {
// do something with data from parent
}
}Now you can use <ChildComponent customData="true"></ChildComponent> in the parent.
- XHTML tags correspond to a Class.
- Tag attributes become method calls/setters.
- Children are added to parents by searching for an
add()oraddActor()method on the parent. - Quoted values are passed literally (cast/mapped to correct type based on target).
- Values
{like_this}come from the passed in state. Expressions are not supported - only values. This keeps things simple and faster. - Table rows etc can be used via
<Table:row></Table:row>notation. setState()re-renders the whole component.- For more CPU-intensive work you can manipulate components directly by calling
getById()instead ofsetState()all the time. For example, for a large data table, you probably don't want to re-render the whole table when adding an item.
You can install the library from Maven Central:
implementation 'com.winricklabs:gix:1.0.1'
Constructor arguments are invoked this way:
<ConstructorTestingWidget new="true" id="single-constructor-arg"></ConstructorTestingWidget>
Constructor params must be first.
You can also have multiple parameters:
<ConstructorTestingWidget new:testArgA="true" new:testArgB="true" id="multi-constructor-arg"></ConstructorTestingWidget>
Let's say I have a Widget with a setLeft(int) method. I could call it these ways:
<Widget left="0">
<Widget setLeft="0">
If the method takes no arguments, like goLeft() then we can just do:
<Widget goLeft>
All widgets in LibGDX Scene2D and vis-ui should be supported, and if not it is a bug. Custom widgets/components are also supported.
AKA calling a method on a component that returns an actor that child components can be added to.
Table has a Actor row() method, so we can do:
<Table>
<Table:row>
<Label new="Hi!"></Label>
</Table:row>
</Table>
GIX reads your templates via an absolute path to make dev mode work without making you setup
hot reloading in libgdx. See the setDevMode and tickComponents methods above.
When the templates change it re-renders them with the current state. So, you can enter input in your UI, navigate it, change things, and write your templates as you go.
For this to work you should probably define your app state objects and components first to limit context switching, and then go through and write your templates.
You can take this a step further and make your components' Java code hot reloadable too by setting up LibGDX hot reloading.
We can enable debug mode in live dev mode by just adding:
<Table debug>
The debug attribute where we want.
setState re-creates the whole UI with the given state. This would be slow if you have a large UI (hundreds/thousands of elements in a component).
The suggested course of action in this case is to use GIX to do the layout and initial rendering, and then
add your event handlers and reactively update elements manually like you would with normal Scene2D.
Also ensure you have dev mode off in prod as currently it hits the file system on the main thread twice a second.
Try setFillParent="true" on your root table. Also try debug.
Contributions welcome! Some low-hanging fruit I could get help with:
- We need a runnable demo project in this repo.
- Integration tests (define HTML, assert resulting view structure. I would suggest a snapshot approach like Jest snapshots - turn the resulting UI into a JSON tree).
- Performance optimizations.
- Performance tests.
- Any "TODOs" I've put in the code in the initial release.
- Hot reloading templates does not work on mobile yet. It only works on Desktop.
I offer paid development support and feature/bugfix priority. Contact: winrid [at] gmail.com.
Apache-2.0