Newer
Older
Import / applications / HighwayDash / ports / Framework / Dialog.h
#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