#ifndef DIALOG_H
#define DIALOG_H
#include <vector>
#include <stack>
#include <functional>
#include <string>
#include <map>
#include <cmath>
#include "Animation.h"
#include "Lua.h"
#include "GLProgram.h"
#include "Math.h"
namespace GameUi {
struct Point
{
int x,y;
};
struct Rectangle
{
int x,y,w,h;
bool isInside(Point pt) const;
};
struct Color
{
Color(uint32_t a_c) { c = a_c; }
union {
uint32_t c;
#if __BYTE_ORDER == __LITTLE_ENDIAN
struct { uint8_t r,g,b,a; }; // LE
#elif __BYTE_ORDER == __BIG_ENDIAN
struct { uint8_t b,g,r,a; }; // BE
#else
# error "if on _WIN32, perhaps can assume LE"
#endif
};
};
DECLARE_ATTRIB_TYPE(vec2i, int16_t, x, y)
DECLARE_ATTRIB_TYPE(tex3i, uint8_t, u, v, texIdx)
DECLARE_VERTEX(UiVertex)
DECLARE_ATTRIB(vec2i, position, GL_FALSE)
DECLARE_ATTRIB(col4i, color, GL_TRUE)
DECLARE_ATTRIB(tex3i, textureCoords, GL_FALSE) // Fix the font and ui control elements in to a 256x256 texture
DECLARE_VERTEX_END
static_assert(sizeof(UiVertex) == 16, "Bad size");
struct TextureLoadJob
{
std::string assetName;
uint8_t texIdx;
};
class DrawItems
{
public:
DrawItems() { m_items.reserve(65536); }
void addQuad(Rectangle r, Rectangle tex, Color col, uint8_t texId=0);
void addIcon(int x, int y, char icon, Color col, int scale = 2);
void addChar(int x, int y, char ch, Color col, int scale = 2);
void addString(int x, int y, const char* a_text, Color col, int scale = 2);
void addContrastString(int x, int y, const char* a_text, int scale = 2);
void add9Slice(Rectangle r, Rectangle t, int radius, Color col);
uint8_t getTextureIdForAsset(const char* assetName);
//std::map<std::string, uint8_t> mappedTextureIds;
std::vector<TextureLoadJob> loadTexturesQueue;
std::vector<UiVertex> m_items;
std::map<std::string, std::string> m_variables; // Similar to environment variables
// name->value pairs for substitution when adding strings,
// eg: m_variables["BLAH"] = "foo"; addString("${BLAH}");
// This will result in "foo" being drawn instead of "${BLAH}" as m_variables maps it to this.
};
struct AnimationParameters
{
std::string m_event;
std::string m_parameter;
//std::string m_curve;
CurveFunction m_curve;
float m_timeScale = 1.0;
float m_timeOffset = 0.0;
bool m_loop = false;
int m_t0 = 0;
int m_t1 = 1;
};
struct MouseState
{
Point pos;
bool down;
};
class Ui;
typedef std::map<std::string,std::vector<std::string> > ActionMap;
typedef std::vector<AnimationParameters> AnimationList;
class UiControl
{
public:
virtual ~UiControl(){}
bool handleMouse(MouseState ms);
/*
enum MouseState {
ButtonState = 1,
InsideState = 2,
PositionState = 4
};
virtual void onMouseStateChange() {}
*/
// Other possible events:
// onHide/Close
// onShow/Open
// onKeyPress
// onKeyRelease
// onMinimize
// onMaximize
// onEvent // generic messaging system
// onHover
// onEnter
// onLeave
virtual void onMousePress() { genericDispatch("onMousePress"); }
virtual void onMouseRelease() { genericDispatch("onMouseRelease"); }
virtual void onMouseMove() { genericDispatch("onMouseMove"); }
virtual void onMouseEnter() { genericDispatch("onMouseEnter"); }
virtual void onMouseLeave() { genericDispatch("onMouseLeave"); }
virtual void onMouseClicked() { genericDispatch("onMouseClicked"); }
virtual void onUpdate(DrawItems&, float) {}
void update() { m_needsUpdate = true; }
Rectangle m_geometry;
bool m_needsUpdate = true;
bool m_mouseDownInside = false;
bool m_mouseDownLast = false;
bool m_mouseInsideLast = false;
MouseState m_mouseState;
std::string m_name = "null";
AnimationList m_animationList;
ActionMap m_actionMap;
Ui* m_ui = nullptr;
void genericDispatch(const char* event);
static std::string formattedText(DrawItems&, std::string text, ...);
};
class Label : public UiControl
{
public:
Label(int x, int y, int alignmentFlags, const char* a_text) : m_x(x), m_y(y), m_flags(alignmentFlags), m_text(a_text) {
m_geometry = Rectangle{x,y,uint16_t(m_text.size()*10),16};
m_name = "label";
}
void onUpdate(DrawItems& items, float a_elapsed) override {
std::string txt = formattedText(items, m_text);
if (m_flags) {
const int chWidth = 20;
int strWidth = (int)txt.size() * chWidth;
items.addContrastString(m_x - strWidth, m_y, txt.c_str());
} else
items.addContrastString(m_x, m_y, txt.c_str());
UiControl::onUpdate(items, a_elapsed);
}
private:
int m_x, m_y, m_flags;
std::string m_text;
};
class ButtonBase : public UiControl
{
public:
void onMousePress() override { if (!m_pressed) { m_pressed = true; update(); } UiControl::onMousePress(); }
void onMouseRelease() override { if (m_pressed) { m_pressed = false; update(); } UiControl::onMouseRelease(); }
void onMouseClicked() override { if (m_isToggle) { m_on = !m_on; update(); } UiControl::onMouseClicked(); }
//std::function<void()> m_clicked;
bool m_pressed = false;
bool m_isToggle = false;
bool m_on = false;
};
struct PixelData
{
uint32_t* m_data;
size_t m_stride;
size_t m_rows;
};
class PixmapEditor : public ButtonBase
{
public:
PixmapEditor(Rectangle screenGeometry, PixelData pixmap, Rectangle pixmapRect) {
m_geometry = screenGeometry;
m_pixmapSlice = pixmapRect;
m_pixmap = pixmap;
m_name = "pixmapeditor";
}
void onUpdate(DrawItems& items, float a_elapsed) override {
if (!m_pixmapSlice.w || !m_pixmapSlice.h)
return;
int pixW = m_geometry.w / m_pixmapSlice.w;
int pixH = m_geometry.h / m_pixmapSlice.h;
for (int j = 0; j < m_pixmapSlice.h; j++)
for (int i = 0; i < m_pixmapSlice.w; i++) {
int x = m_geometry.x + pixW * i;
int y = m_geometry.y + pixH * j;
size_t idx = (m_pixmapSlice.y + j) * m_pixmap.m_stride + m_pixmapSlice.x + i;
if (idx < m_pixmap.m_stride * m_pixmap.m_rows)
items.addQuad(Rectangle{x, y, pixW, pixH}, Rectangle{179,113,30,30}, m_pixmap.m_data[idx]);
}
UiControl::onUpdate(items, a_elapsed);
}
private:
PixelData m_pixmap;
Rectangle m_pixmapSlice;
};
class ToolButton : public ButtonBase
{
public:
ToolButton(int x, int y, int icon) : m_x(x), m_y(y), m_icon(icon) {
// , std::function<void()> clicked) : m_x(x), m_y(y), m_icon(icon) {
//m_clicked = clicked;
m_geometry = Rectangle{x,y,50,50};
m_name = "toolbutton";
}
void onUpdate(DrawItems& items, float a_elapsed) override {
// Tool Button
int state = (m_pressed) ? 31 : ((m_on) ? 61 : 1);
items.add9Slice(Rectangle{m_x, m_y, 50, 50}, Rectangle{state, 114, 30, 30}, 10, 0xaadd9999);
// Icon
items.addIcon(m_x+5, m_y+6+(m_pressed?1:0), m_icon, 0xFF772222);
//int tx1 = 1 + 20*m_icon, ty1 = 5*19;//64;
//items.addQuad(Rectangle{m_x+4, m_y+5+(m_pressed?1:0), 40, 38}, Rectangle{tx1, ty1, 20, 19}, 0xFF772222);
UiControl::onUpdate(items, a_elapsed);
}
private:
int m_x, m_y, m_icon;
};
class Button : public ButtonBase
{
public:
Button(int x, int y, const char* a_text) : m_x(x), m_y(y), m_text(a_text) {
// , std::function<void()> clicked) : m_x(x), m_y(y), m_text(a_text) {
//m_clicked = clicked;
m_geometry = Rectangle{x,y,250,50};
m_name = "button";
}
void onUpdate(DrawItems& items, float a_elapsed) override {
//printf("button update %ld\n", items.m_items.size());
std::string txt = formattedText(items, m_text);
int state = (m_pressed) ? 31 : ((m_on) ? 61 : 1);
items.add9Slice(Rectangle{m_x, m_y, 250, 50}, Rectangle{state, 114, 30, 30}, 10, 0xff550011);// 0xaa550000);
items.addContrastString(m_x+125-(int)txt.size()*10, m_y+8+(m_pressed?1:0), txt.c_str());
UiControl::onUpdate(items, a_elapsed);
}
private:
int m_x, m_y;
std::string m_text;
};
class CheckBox : public UiControl
{
public:
CheckBox(int x, int y, const char* a_text, std::string binding) : m_x(x), m_y(y), m_text(a_text), m_dataBinding(binding) {
m_geometry = Rectangle{x,y,250,50};
m_name = "checkbox";
m_mouseState.down = false;
m_lastMouseState.down = false;
}
void onUpdate(DrawItems& items, float a_elapsed) override {
if (!m_mouseState.down)
m_down = false;
else if (!m_down && !m_lastMouseState.down && m_geometry.isInside(m_mouseState.pos)) {
m_down = true;
m_on = !m_on;
}
m_lastMouseState = m_mouseState;
char icon = 10 + ((m_on) ? 1 : 0);
items.addIcon(m_x, m_y, icon, 0xff550011);
items.addContrastString(m_x+45, m_y, m_text.c_str());
items.m_variables[m_dataBinding] = (m_on) ? "on" : "off";
UiControl::onUpdate(items, a_elapsed);
}
private:
MouseState m_lastMouseState;
bool m_down = false;
bool m_on = false;
int m_x, m_y;
std::string m_text;
std::string m_dataBinding;
};
class Slider : public UiControl
{
public:
Slider(int x, int y, int height, std::string binding) : m_x(x), m_y(y), m_h(height), m_dataBinding(binding) {
m_geometry = Rectangle{x-15,y-20,50,50};
m_name = "slider";
m_mouseState.down = false;
m_lastMouseState.down = false;
m_down = false;
}
void onUpdate(DrawItems& items, float a_elapsed) override {
if (!m_mouseState.down)
m_down = false;
else if (!m_down && !m_lastMouseState.down && m_geometry.isInside(m_mouseState.pos))
m_down = true;
if (m_down && m_mouseState.down)
//m_geometry.y += m_mouseState.pos.y - m_lastMouseState.pos.y; // Usual mouse kind of control
m_geometry.y = m_mouseState.pos.y - 20; // Perhaps more touch screen style of control
m_lastMouseState = m_mouseState;
m_geometry.y = Math::clamp(m_geometry.y, m_y-20, m_y+m_h-20);
items.add9Slice(Rectangle{m_x, m_y, 20, m_h}, Rectangle{211, 114, 30, 30}, 10, 0xff550011);
int state = (m_down) ? 31 : 1;
items.add9Slice(m_geometry, Rectangle{state, 114, 30, 30}, 10, 0xff550011);
float percent = float(m_geometry.y - m_y + 20) / float(m_h);
items.m_variables[m_dataBinding] = std::to_string(percent);
UiControl::onUpdate(items, a_elapsed);
}
private:
MouseState m_lastMouseState;
bool m_down;
int m_x, m_y, m_h;
std::string m_dataBinding;
};
class Frame : public UiControl
{
public:
Frame(int x, int y, int w, int h, const char* a_text) : m_text(a_text) {
m_geometry = Rectangle{x,y,w,h};
m_name = "frame";
}
void onUpdate(DrawItems& items, float a_elapsed) override {
int x = m_geometry.x, y = m_geometry.y, w = m_geometry.w, h = m_geometry.h;
items.add9Slice({ x-3, y-35, 120, 50}, { 1, 114, 30, 30}, 10, 0xff553333);
items.add9Slice({ x+0, y+0, w+0, h+ 0}, { 90, 114, 30, 30}, 20, 0xff000000);
items.addQuad( { x+3, y+3, w-6, h- 6}, {150, 114, 30, 30}, 0xffffffff);
items.addContrastString( x+15, y-25, m_text.c_str(), 1);
UiControl::onUpdate(items, a_elapsed);
}
private:
std::string m_text;
};
class Panel : public UiControl
{
public:
Panel(int w, int h, Color col) : m_w(w), m_h(h), m_col(col) {
m_geometry = Rectangle{0,0,w,h};
m_name = "panel";
}
void onUpdate(DrawItems& items, float a_elapsed) override {
items.addQuad(Rectangle{ 0, 0, m_w, m_h}, Rectangle{150, 114, 30, 30}, 0x33333333);
items.add9Slice(Rectangle{ 50, 50, m_w-100, m_h-200}, Rectangle{90, 114, 30, 30}, 50, 0xdd223322);//0x77CF77);
UiControl::onUpdate(items, a_elapsed);
}
private:
int m_w, m_h;
Color m_col;
};
class RectangleShape : public UiControl
{
public:
RectangleShape(int x, int y, int w, int h, Color col) : m_col(col) {
m_geometry = Rectangle{x,y,w,h};
}
void onUpdate(DrawItems& items, float a_elapsed) override {
m_t += a_elapsed;
for (size_t i = 0; i < m_animationList.size(); i++) {
AnimationParameters params = m_animationList[i];
double tmp, ratio = m_t * 100.0 / params.m_timeScale;
if (ratio >= 1.0) {
//if (params.m_loop)
ratio = modf(ratio, &tmp);
//else
// ratio = 1.0;
}
int value = Math::lerp(params.m_t0, params.m_t1, params.m_curve(ratio));
//printf("applying anim param %s = %i\n", params.m_parameter.c_str(), value);
if (params.m_parameter == "x")
m_geometry.x = value;
else if (params.m_parameter == "y")
m_geometry.y = value;
else if (params.m_parameter == "w")
m_geometry.w = value;
else if (params.m_parameter == "h")
m_geometry.h = value;
else if (params.m_parameter == "r")
m_col.r = value;
else if (params.m_parameter == "g")
m_col.g = value;
else if (params.m_parameter == "b")
m_col.b = value;
else if (params.m_parameter == "a")
m_col.a = value;
}
items.addQuad(m_geometry, Rectangle{180+1, 114+1, 30-2, 30-2}, m_col);
UiControl::onUpdate(items, a_elapsed);
}
private:
float m_t;
Color m_col;
};
class ImageShape : public UiControl
{
public:
ImageShape(int x, int y, const char* assetName) {
int w = 255, h = 255;
m_asset = assetName;
//printf("image loading with asset: %s\n", assetName);
// load asset and set m_texIdx
m_geometry = Rectangle{x,y,w,h};
}
void onUpdate(DrawItems& items, float a_elapsed) override {
m_texIdx = items.getTextureIdForAsset(m_asset.c_str());
items.addQuad(m_geometry, Rectangle{0,0,m_geometry.w,m_geometry.h}, 0xff223344, m_texIdx);
UiControl::onUpdate(items, a_elapsed);
}
private:
std::string m_asset;
int m_texIdx = 1;
};
class View
{
public:
View(int x, int y, int w, int h, const char* view) : m_geometry{x,y,w,h}, m_view(view) {}
Rectangle m_geometry;
std::string m_view;
};
class Dialog
{
public:
Dialog() {}
Dialog(int w, int h) {
m_controls.push_back(new Panel(w,h,0xaa770000));
}
~Dialog() {
for (UiControl* ctrl : m_controls) {
delete ctrl;
}
}
bool handleMouse(MouseState ms, bool& consumed) {
m_needsUpdate = false;
// for (UiControl* ctrl : m_controls) {
for (long i = m_controls.size()-1; i >= 0; i--) {
UiControl* ctrl = m_controls[i];
consumed = ctrl->handleMouse(ms);
//if (ctrl->m_needsUpdate)
// printf("need update for ctrl %s\n", ctrl->m_name.c_str());
m_needsUpdate |= ctrl->m_needsUpdate;
if (consumed)
break; // Only let one control consume the mouse event (perhaps should do this in reverse ctrl order)
}
return m_needsUpdate;
}
void backgroundItems(DrawItems& items) {
for (UiControl* ctrl : m_background)
ctrl->onUpdate(items, 0.0);
}
void update(DrawItems& items, float a_elapsed) {
for (UiControl* ctrl : m_controls) {
//printf("update for ctrl %s\n", ctrl->m_name.c_str());
ctrl->onUpdate(items, a_elapsed);
ctrl->m_needsUpdate = false;
}
m_needsUpdate = false;
}
// void close() { m_onClose(); }
// std::function<void()> m_onClose;
std::vector<UiControl*> m_background;
std::vector<View*> m_views;
std::vector<UiControl*> m_controls;
bool m_needsUpdate = false;
ActionMap m_actionMap;
std::string m_name;
std::string m_stage; // expected c++ level
};
class Ui
{
public:
~Ui() {
// Probably need to remove then from the DialogMap, not the stack
//for (Dialog* dialog : m_stack)
// delete dialog;
}
void LoadDialogsFromAsset(const char* assetName);
void DialogRedirect(std::string redirect);
bool handleMouse(MouseState ms) { bool consumed = false;
if (ms.down != m_lastMouse.down) {
float dx = m_lastMouse.pos.x - ms.pos.x;
if (ms.down)
dialogDispatch("onTap");
else if (dx > 5)
dialogDispatch("onSwipeRight");
else if (dx < -5)
dialogDispatch("onSwipeLeft");
m_lastMouse = ms;
}
if (m_stack.size())
m_needsUpdate = current().handleMouse(ms, consumed);
return consumed;
}
void update(DrawItems& items, float a_elapsed) { if (m_stack.size()) { current().update(items, a_elapsed); } m_needsUpdate = false; }
void backgroundItems(DrawItems& items) { if (m_stack.size()) { current().backgroundItems(items); } }
void getCurrentViews(std::vector<View*>& views) { if (m_stack.size()) { views = current().m_views; } }
void pushDialog(Dialog* dialog) { m_stack.push_back(dialog); /* dialog->m_onClose = [this](){ pop(); }; */ m_needsUpdate = true; }
void pop() { m_stack.pop_back(); m_needsUpdate = true; }
void closeCurrent() { if (m_stack.size()) pop(); } //current().close(); }
void exitCurrent() { dialogDispatch("onExit"); }
std::string currentStage() { if (m_stack.size()) return current().m_stage; return "blank"; }
bool needsUpdate() { return m_needsUpdate; }
void onLaunch() { genericDispatch("onLaunch"); }
void onGameOver() { genericDispatch("onGameOver"); }
void bindFunctionToLua(const char* name, lua_CFunction fn) {
m_luaVM.BindCFunction(name, fn);
}
private:
MouseState m_lastMouse = {{0,0},false};
ActionMap m_gameFlowSteps;
void genericDispatch(const char* event); // events that are mapped to actions that are ui.json wide / game flow steps
void dialogDispatch(const char* event) { // events that are mapped to actions for particular dialogs
if (m_stack.size()) {
if (current().m_actionMap.count(event))
for (auto action : current().m_actionMap[event])
DialogRedirect(action);
}
}
Dialog& current() { return *m_stack.back(); }
std::vector<Dialog*> m_stack;
bool m_needsUpdate = true;
typedef std::map<std::string, Dialog*> DialogMap;
DialogMap m_dialogMap;
float m_t = 0.0;
LuaVM m_luaVM;
};
}
#endif // DIALOG_H