Blob Blame History Raw

#include "menu.h"
#include "icons.h"


static int parseImgName(const char *imgname, int *rows, int *cols) {
  if (rows) *rows = 0;
  if (cols) *cols = 0;
  char* p = strrchr(imgname, '.');
  if (!p) return 0;
  int r = 0, c = 0;
  if ( 2 != sscanf(p, ".pz%dx%d", &c, &r)
    || r > 100 || c > 100 || r*c > 200
    || r < 1   || c < 1   || r*c < 2 ) return 0;
  if (rows) *rows = r;
  if (cols) *cols = c;
  return 1;
}


static Animation loadThumb(const char *imgname, Framebuffer fb, int w, int h) {
  char *imgpath = findImage(imgname);
  if (!imgpath)
    { ERR("loadThumb: image not found: %s", imgname); return NULL; }

  if (w < 1) w = 1;
  if (h < 1) h = 1;

  char *tpath = strprintf("%s/%s", thumbsPath(), filename(imgpath));
  if (fileExists(tpath) && filetime(imgpath) < filetime(tpath)) {
    Animation thumb = createAnimationEx(tpath, TRUE, FALSE, FALSE);
    if (abs(animationGetOrigWidth(thumb) - w) <= 2 || abs(animationGetOrigHeight(thumb) - h) <= 2) {
      free(tpath);
      free(imgpath);
      return thumb;
    }
    animationDestroy(thumb);
  }
  
  INF("loadThumb: create thumbnail: %s -> %s", imgpath, tpath);
  
  Animation full = createAnimationEx(imgpath, TRUE, FALSE, FALSE);
  int fw = animationGetOrigWidth(full);
  int fh = animationGetOrigHeight(full);
  
  int tw = w, th = h;
  if (fw*(double)h > fh*(double)w) th = (int)round(w/(double)fw*fh);
                              else tw = (int)round(h/(double)fh*fw);
  if (tw < 1) tw = 1; else if (tw > w) tw = w;
  if (th < 1) th = 1; else if (th > h) th = h;
  DBG("[%dx%d] -> [%dx%d]", fw, fh, tw, th);
  
  saveState();
  resetState();
  target(fb);
  background(COLOR_TRANSPARENT);
  clear();
  noStroke();
  rectTextured(full, 0, 0, tw, th);
  
  int iw, ih;
  unsigned char *pixels;
  Animation thumb = NULL;
  if (imageFromViewport(&iw, &ih, &pixels)) {
    if (ih > th) ih = th;
    if (iw > tw) {
      for(int i = 1; i < ih; ++i) memmove(pixels + i*tw*4, pixels + i*iw*4, tw*4);
      iw = tw;
    }
    if (iw != tw || ih != th) {
      WRN("loadThumb: thumbnail was truncated: %s, [%dx%d] -> [%dx%d]", imgpath, tw, th, iw, ih);
    } else
    if (!imageSave(tpath, iw, ih, pixels)) {
      WRN("loadThumb: cannot save thumbnail to: %s", tpath);
      thumb = createAnimationFromImageEx(iw, ih, pixels, FALSE, FALSE, TRUE, TRUE);
    } else {
      thumb = createAnimationEx(tpath, TRUE, FALSE, FALSE);
    }
    free(pixels);
  } else {
    WRN("loadThumb: cannot capture vieport: %s", imgpath);
    unsigned char px[4*4*4] = {};
    thumb = createAnimationFromImageEx(4, 4, px, FALSE, FALSE, TRUE, TRUE);
  }
  
  restoreState();
  free(tpath);
  free(imgpath);
  return thumb;
}


int menuItemInit(MenuItem *mi, const char *path, const char *imgname, Vec p0, Vec p1, Framebuffer fb, int w, int h) {
  memset(mi, 0, sizeof(*mi));

  DBG("%s, %s", path ? path : "null", imgname ? imgname : "null");
  
  Vec d = vsub(p1, p0);
  if (!(fabs(d.x) > PRECISION) || !(fabs(d.y) > PRECISION)) return 0;

  if (path) {
    GameInfo *gi = loadGameInfo(path);
    if (!gi) return 0;
    mi->image = loadThumb(gi->imgname, fb, w, h);
    if (!mi->image) { free(gi); return 0; }
    
    int cnt = gi->rows*gi->cols;
    mi->path = strprintf("%s", path);
    mi->imgname = strprintf("%s", gi->imgname);
    mi->percent = (cnt - gi->groups)/(cnt - 1.0);
    mi->t = filetime(path);
    free(gi);
  } else
  if (imgname) {
    mi->image = loadThumb(imgname, fb, w, h);
    if (!mi->image) return 0;
    mi->imgname = strprintf("%s", imgname);
    parseImgName(mi->imgname, &mi->rows, &mi->cols);
  } else {
    return 0;
  }

  Vec is = { animationGetOrigWidth(mi->image), animationGetOrigHeight(mi->image) };
  if (is.x < 1) is.x = 1;
  if (is.y < 1) is.y = 1;
  Vec k = vabs(vdivv(is, d));
  
  if (k.x > k.y) {
    double l = (1 - k.y/k.x)/2;
    mi->p0 = vec(p0.x, lintr(p0.y, p1.y, l));
    mi->p1 = vec(p1.x, lintr(p0.y, p1.y, 1-l));
  } else {
    double l = (1 - k.x/k.y)/2;
    mi->p0 = vec(lintr(p0.x, p1.x, l), p0.y);
    mi->p1 = vec(lintr(p0.x, p1.x, 1-l), p1.y);
  }
  
  return 1;
}


void menuItemFree(MenuItem *mi) {
  free(mi->path);
  free(mi->imgname);
  if (mi->image) animationDestroy(mi->image);
  memset(mi, 0, sizeof(*mi));
}


static void splitRect(Vec p0, Vec p1, double percent, Vec *v0, int *v0c, Vec *v1, int *v1c) {
  Vec s = vabs(vsub(p1, p0));
  if (s.x < s.y) {
    splitRect(vtranspose(p0), vtranspose(p1), percent, v0, v0c, v1, v1c);
    for(int i = 0; i < *v0c; ++i) v0[i] = vtranspose(v0[i]);
    for(int i = 0; i < *v1c; ++i) v1[i] = vtranspose(v1[i]);
    return;
  }
  if (percent > 0.5) {
    splitRect(p1, p0, 1 - percent - PRECISION, v1, v1c, v0, v0c);
    return;
  }
  
  if (!(percent > PRECISION) || !(s.x > PRECISION) || !(s.y > PRECISION))
    { v0[0] = p0; v0[1] = vec(p1.x, p0.y); v0[2] = p1; v0[3] = vec(p0.x, p1.y); *v0c = 4; *v1c = 0; return; }
  if (!(percent < 1-PRECISION))
    { v1[0] = p1; v1[1] = vec(p0.x, p1.y); v1[2] = p0; v1[3] = vec(p1.x, p0.y); *v0c = 0; *v1c = 4; return; }

  *v0c = *v1c = 0;
   v0[(*v0c)++] = p0;
   v1[(*v1c)++] = p1;

  double af = s.x*s.y;
  double ap = af*percent;
  double ac = s.y*s.y/2;
  if (ap < ac) {
    double ky = sqrt(ap/ac);
    Vec a = { lintr(p0.x, p1.x, ky*s.y/s.x), p0.y };
    Vec b = { p0.x, lintr(p0.y, p1.y, ky) };

    v0[(*v0c)++] = a;
    v0[(*v0c)++] = b;
     
    v1[(*v1c)++] = vec(p0.x, p1.y);
    v1[(*v1c)++] = b;
    v1[(*v1c)++] = a;
    v1[(*v1c)++] = vec(p1.x, p0.y);
  } else {
    double kx = (ap-ac)/(af-2*ac)*(s.x-s.y)/s.x;
    Vec b = { lintr(p0.x, p1.x, kx), p1.y };
    Vec a = { b.x + p1.y - p0.y, p0.y };
    
    v0[(*v0c)++] = a;
    v0[(*v0c)++] = b;
    v0[(*v0c)++] = vec(p0.x, p1.y);

    v1[(*v1c)++] = b;
    v1[(*v1c)++] = a;
    v1[(*v1c)++] = vec(p1.x, p0.y);
  }
}


void menuItemDraw(MenuItem *mi) {
  saveState();
  Vec d = vsub(mi->p1, mi->p0);
  if (mi->path) {
    noStroke();
    Vec v[2][5] = {};
    int vc[2] = {};
    splitRect(mi->p0, mi->p1, mi->percent, v[0], &vc[0], v[1], &vc[1]);
    noStroke();
    for(int i = 0; i < 2; ++i) {
      fill(colorByRGBA(1, 1, 1, (2-i)*0.5));
      fillTexture(mi->image, mi->p0.x, mi->p0.y, d.x, d.y, FALSE);
      moveTo(v[i][0].x, v[i][0].y);
      for(int j = 1; j < vc[i]; ++j) lineTo(v[i][j].x, v[i][j].y);
      closePath();
    }
    noFill();
    stroke(COLOR_BLUE);
    rect(mi->p0.x, mi->p0.y, d.x, d.y);
  } else {
    stroke(COLOR_BLACK);
    fill(COLOR_WHITE);
    rectTextured(mi->image, mi->p0.x, mi->p0.y, d.x, d.y);
  }
  restoreState();
}


static Animation loadIcon(const void *data, size_t size) {
  unsigned char *p = NULL; int w = 0, h = 0;
  if (!imageLoadFromMemory(data, size, &w, &h, &p))
    return createAnimationEmpty();
  Animation a = createAnimationFromImageEx(w, h, p, FALSE, FALSE, TRUE, TRUE);
  free(p);
  return a;
}


void menuInit(Menu *m) {
  memset(m, 0, sizeof(*m));
  m->contentSize = vone();
  #define LOADICON(x) loadIcon(dataIcon ## x, dataIcon ## x ## Size)
  m->iconBack     = LOADICON(Back);
  m->iconCount[0] = LOADICON(Count1);
  m->iconCount[1] = LOADICON(Count2);
  m->iconCount[2] = LOADICON(Count3);
  m->iconCount[3] = LOADICON(Count4);
  m->iconTurn[0]  = LOADICON(Noturn);
  m->iconTurn[1]  = LOADICON(Turn);
  #undef LOADICON
}


void menuFree(Menu *m) {
  if (gameStarted(&m->gm)) gameFree(&m->gm);
  for(int i = 0; i < m->itemsCnt; ++i) menuItemFree(&m->items[i]);
  free(m->items);
  if (m->iconBack) animationDestroy(m->iconBack);
  for(int i = 0; i < COUNTOF(m->iconCount); ++i)
    if (m->iconCount[i]) animationDestroy(m->iconCount[i]);
  for(int i = 0; i < COUNTOF(m->iconTurn); ++i)
    if (m->iconTurn[i]) animationDestroy(m->iconTurn[i]);
  memset(m, 0, sizeof(*m));
}


void menuUpdateItems(Menu* m) {
  // remove old items, but copy images to keep them in memory
  Animation animCopy = createAnimationEmpty();
  for(int i = 0; i < m->itemsCnt; ++i) {
    animationInsert(animCopy, -1, m->items[i].image);
    menuItemFree(&m->items[i]);
  }
  free(m->items);
  m->itemsCnt = 0;
  
  Vec border = {1, 1};
  Vec spacing = {1, 1};
  Vec thumb = {16, 12};
  int cols = 4;
  int tw = 256;
  int th = tw/4*3;
  
  Framebuffer fb = createFramebuffer(tw, tw);

  PathList *pls = saveFiles();
  PathList *pli = imageFiles();
  m->items = alloc((pls->cnt + pli->cnt)*ASIZEOF(*m->items));
  
  int c = 0;
  double y = border.y, ly = y;
  for(int i = 0; i < pls->cnt; ++i) {
    Vec p0 = { border.x + (thumb.x + spacing.x)*c, y };
    Vec p1 = vadd(p0, thumb);
    if (menuItemInit(&m->items[m->itemsCnt], pls->l[i], NULL, p0, p1, fb, tw, th)) {
      if (++c >= cols) { c = 0; y += thumb.y + spacing.y; }
      ++m->itemsCnt; ly = p1.y;
    }
  }

  if (c) { c = 0; y += thumb.y + spacing.y; }
  if (m->itemsCnt) y += spacing.y;
  
  for(int i = 0; i < pli->cnt; ++i) {
    Vec p0 = { border.x + (thumb.x + spacing.x)*c, y };
    Vec p1 = vadd(p0, thumb);
    char *imgname = filebase(pli->l[i]);
    if (menuItemInit(&m->items[m->itemsCnt], NULL, imgname, p0, p1, fb, tw, th)) {
      if (++c >= cols) { c = 0; y += thumb.y + spacing.y; }
      ++m->itemsCnt; ly = p1.y;
    }
    free(imgname);
  }
  
  animationDestroy(animCopy);
  free(pls);
  free(pli);
  
  framebufferDestroy(fb);
  m->contentSize = vec( 2*border.x + thumb.x*cols + spacing.x*(cols-1), ly + border.y );
}


int menuSave(Menu *m) {
  makeDirectoryRecursive(savesPath());
  const char *fn = settingsFile();
  FILE *f = fopen(fn, "w");
  if (!f) return ERR("menuSave: cannnot open file for write: %s", fn);
  fprintf(f, "puzzle1\n[%s]\n%d %d\n", m->gm.path ? m->gm.path : "", m->countMode, m->turnMode);
  fflush(f);
  if (ferror(f)) ERR("menuSave: cannnot write to file: %s", fn);
  fclose(f);
  return 1;
}


int menuLoad(Menu *m) {
  const char *fn = settingsFile();
  FILE *f = fopen(fn, "r");
  if (!f) return 0;

  char *l = trim(readline(f));
  if (strcmp(l, "puzzle1")) {
    ERR("menuLoad: wrong file version (expected: puzzle1): %s", l);
    free(l); fclose(f); return 0;
  }

  l = unquote(readline(f), '[', ']');
  int cm = -1, tm = -1;
  fscanf(f, "%d %d", &cm, &tm);
  fclose(f);

  if ( cm < 0 || cm > COUNTOF(m->iconCount) || tm < 0 || tm > COUNTOF(m->iconTurn))
    { ERR("menuLoad: cannot parse file: %s", fn); free(l); return 0; }

  m->countMode = cm;
  m->turnMode = tm;
  if (*l) gameLoad(&m->gm, l);
  free(l);
  return 1;
}


void menuChooseItem(Menu *m, MenuItem *mi) {
  if (mi->path) {
    gameLoad(&m->gm, mi->path);
    return;
  }
  
  int cnt = m->countMode == 0 ? 12
          : m->countMode == 1 ? 24
          : m->countMode == 2 ? 48 : 96;
  int rows = mi->rows;
  int cols = mi->cols;
  if (!mi->rows || !mi->cols) { rows = cnt; cols = 0; }
    
  char *path = NULL;
  for(int j = 0; j < 10000; ++j) {
    path = strprintf("%s/%s_%04d.puzzle", savesPath(), mi->imgname, j);
    if (fileExists(path)) { free(path); continue; }
    gameCreate(&m->gm, path, mi->imgname, rows, cols, m->turnMode, 0);
    free(path);
    break;
  }
}


void menuClick(Menu *m, Vec screenSize, double cornerSize) {
  double cs2 = cornerSize*cornerSize;
  if (vlen2(vsub(vec(screenSize.x, 0), m->mouseDownPos)) <= cs2)
    { m->countMode = (m->countMode+1) % COUNTOF(m->iconCount); return; }
  if (vlen2(vsub(screenSize, m->mouseDownPos)) <= cs2)
    { m->turnMode = (m->turnMode+1) % COUNTOF(m->iconTurn); return; }

  Vec p = m->mouseDownPos;
  p.y += m->scrollPos;
  for(int i = 0; i < m->itemsCnt; ++i) {
    MenuItem *mi = &m->items[i];
    if ( !(p.x >= mi->p0.x) || !(p.x <= mi->p1.x)
      || !(p.y >= mi->p0.y) || !(p.y <= mi->p1.y) ) continue;
    m->choosingItem = mi;
    m->choosingItemFrames = 4;
    return;
  }
}


void menuDraw(Menu *m) {
  if (m->choosingItem) {
    if (--m->choosingItemFrames > 0) return;
    MenuItem *mi = m->choosingItem;
    m->choosingItem = NULL;
    m->choosingItemFrames = 0;
    menuChooseItem(m, mi);
  }
  
  int w = windowGetWidth();
  int h = windowGetHeight();
  double dt = windowGetFrameTime();
  double k = w/m->contentSize.x;
  Vec screenSize = { w/k, h/k };

  double maxPath    = m->contentSize.x*0.1;
  double scrollMax  = m->contentSize.y - screenSize.y;
  double scrollBase = m->contentSize.x*dt*2;
  double scrollDecc = m->contentSize.x*dt*2;
  double scrollOver = m->contentSize.x*0.2;
  if (!(scrollMax > 0)) scrollMax = 0;

  saveState();
  zoom(k);
  strokeWidth(1/k);

  double cornerSizeX = screenSize.x/8;
  double cornerSizeY = screenSize.y/4;
  double cornerSize = cornerSizeX < cornerSizeY ? cornerSizeX : cornerSizeY;
  
  Vec mouse = { mouseTransformedX(), mouseTransformedY() };

  if (mouseWentDown("left")) {
    m->mouseDownPos = mouse;
    m->mouseDownTime = 0;
    m->mousePath = 0;
    m->mouseScroll = m->scrollPos;
    if (vlen2(mouse) <= cornerSize*cornerSize || gameWon(&m->gm))
    {
      if (gameStarted(&m->gm) && !gameWon(&m->gm)) gameSave(&m->gm);
      m->closing = 1;
    }
  }

  if (mouseWentUp("left")) m->closing = 0;

  if (mouseDown("left")) {
    m->mousePath += vlen(vsub(mouse, m->mouse));
    m->mouseDownTime += dt;
    if (!(m->mousePath < maxPath)) m->closing = 0;
  }
  
  if (!gameStarted(&m->gm)) {
    if (mouseWentDown("left"))
      memset(m->scrollHist, 0, sizeof(m->scrollHist));
    
    if (mouseWentUp("left")) {
      if ( m->mousePath < maxPath
        && m->mouseDownTime < 1
        && vlen(vsub(mouse, m->mouseDownPos)) < maxPath )
      {
        m->scrollVel = 0;
        menuClick(m, screenSize, cornerSize);
      } else {    
        double p1 = m->scrollPos, p0 = p1, t = 0;
        for(int i = 0; i < MS_CNT; ++i) {
          if (m->scrollHist[i][1] > PRECISION) {
            p0 = m->scrollHist[i][0];
            t += m->scrollHist[i][1];
            if (i >= 3 && t > 0.2) break;
          }
        }
        m->scrollVel = t > PRECISION ? (p1 - p0)/t : 0;
      }
    }

    if (mouseDown("left")) {
      m->scrollPos = m->mouseScroll + m->mouseDownPos.y - mouse.y;
      memmove(m->scrollHist + 1, m->scrollHist, (MS_CNT-1)*sizeof(*m->scrollHist));
      m->scrollHist[0][0] = m->scrollPos;
      m->scrollHist[0][1] = dt;
    } else {
      if (m->scrollPos < -scrollOver && m->scrollVel < 0) m->scrollVel = 0;
      if (m->scrollPos > scrollMax+scrollOver && m->scrollVel > 0) m->scrollVel = 0;
      m->scrollPos += m->scrollVel*dt;
      
      if (m->scrollVel < 0) {
        m->scrollVel += scrollDecc;
        if (!(m->scrollVel < 0)) m->scrollVel = 0;
      } else {
        m->scrollVel -= scrollDecc;
        if (!(m->scrollVel > 0)) m->scrollVel = 0;
      }
        
      if (m->scrollPos < 0) {
        m->scrollPos += scrollBase;
        if (!(m->scrollPos < 0)) m->scrollPos = 0;
      } else
      if (m->scrollPos > scrollMax) {
        m->scrollPos -= scrollBase;
        if (!(m->scrollPos > scrollMax)) m->scrollPos = scrollMax;
      }
    }
 
    saveState();
    translate(0, -m->scrollPos);
    for(int i = 0; i < m->itemsCnt; ++i)
      menuItemDraw(&m->items[i]);
    restoreState();
  }
  
  m->mouse = mouse;

  if (gameStarted(&m->gm)) {
    int wasGroups = m->gm.pz.groups;
    int wasWon = gameWon(&m->gm);
    
    saveState();
    zoom(1/k);
    gameDraw(&m->gm);
    restoreState();
    
    if (wasGroups != m->gm.pz.groups && !gameWon(&m->gm))
      gameSave(&m->gm);
    if (wasWon && gameWon(&m->gm) && fileExists(m->gm.path) && remove(m->gm.path))
      ERR("cannot remove save-file for the won game: %s", m->gm.path);
  }
 
  saveState();
  noStroke();
  fill(colorByRGBA(1, 1, 1, 0.25));
  rectTextured(m->iconBack, 0, 0, cornerSize, cornerSize);
  if (!gameStarted(&m->gm)) {
    rectTextured(m->iconCount[m->countMode], screenSize.x-cornerSize, 0, cornerSize, cornerSize);
    rectTextured(m->iconTurn[m->turnMode], screenSize.x-cornerSize, screenSize.y-cornerSize, cornerSize, cornerSize);
  }
  restoreState();
  
  if (m->closing) {
    m->closingTransition += dt;
    if (m->closingTransition > 1) {
      if (gameStarted(&m->gm)) {
        gameFree(&m->gm);
        m->closing = 0;
        m->closingTransition = 0;
        menuUpdateItems(m);
      } else {
        m->closingTransition = 1;
        windowStop();
      }
    }
  } else {
    m->closingTransition -= dt;
    if (!(m->closingTransition > 0)) m->closingTransition = 0;
  }
  
  if (m->closingTransition > 0) {
    saveState();
    noStroke();
    fill(gameStarted(&m->gm) ? colorByRGBA(0.05, 0.05, 0.05, 1) : COLOR_BLACK);
    Vec c = m->mouseDownPos;
    
    Vec d = vsub(screenSize, c);
    double rmax2 = maxd( maxd(vlen2(vec(c.x, d.y)), vlen2(c)),
                         maxd(vlen2(vec(d.x, c.y)), vlen2(d)) );
    double r0 = sqrt(rmax2) * (1 - m->closingTransition);
    double r1 = r0 + vlen(screenSize);
    arcPath(c.x-r0, c.y-r0, 2*r0, 2*r0, 180, -180);
    arcPath(c.x-r1, c.y-r1, 2*r1, 2*r1, -180, 180);
    closePath();
    restoreState();
  }
  
  restoreState();
}