/* ** window.cpp ** ** This file is part of mkxp. ** ** Copyright (C) 2013 Jonas Kulla ** ** mkxp is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 2 of the License, or ** (at your option) any later version. ** ** mkxp is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with mkxp. If not, see . */ #include "window.h" #include "viewport.h" #include "globalstate.h" #include "bitmap.h" #include "etc.h" #include "etc-internal.h" #include "tilequad.h" #include "gl-util.h" #include "quad.h" #include "quadarray.h" #include "texpool.h" #include "glstate.h" #include "sigc++/connection.h" #include template struct Sides { T l, r, t, b; }; template struct Corners { T tl, tr, bl, br; }; static IntRect backgroundSrc(0, 0, 128, 128); static IntRect cursorSrc(128, 64, 32, 32); static IntRect pauseAniSrc[] = { IntRect(160, 64, 16, 16), IntRect(176, 64, 16, 16), IntRect(160, 80, 16, 16), IntRect(176, 80, 16, 16) }; static Sides bordersSrc = { IntRect(128, 16, 16, 32), IntRect(176, 16, 16, 32), IntRect(144, 0, 32, 16), IntRect(144, 48, 32, 16) }; static Corners cornersSrc = { IntRect(128, 0, 16, 16), IntRect(176, 0, 16, 16), IntRect(128, 48, 16, 16), IntRect(176, 48, 16, 16) }; static Sides scrollArrowSrc = { IntRect(144, 24, 8, 16), IntRect(168, 24, 8, 16), IntRect(152, 16, 16, 8), IntRect(152, 40, 16, 8) }; /* Cycling */ static unsigned char cursorAniAlpha[] = { /* Fade out */ 0xFF, 0xF7, 0xEF, 0xE7, 0xDF, 0xD7, 0xCF, 0xC7, 0xBF, 0xB7, 0xAF, 0xA7, 0x9F, 0x97, 0x8F, 0x87, /* Fade in */ 0x7F, 0x87, 0x8F, 0x97, 0x9F, 0xA7, 0xAF, 0xB7, 0xBF, 0xC7, 0xCF, 0xD7, 0xDF, 0xE7, 0xEF, 0xF7 }; static elementsN(cursorAniAlpha); /* Cycling */ static unsigned char pauseAniQuad[] = { 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3 }; static elementsN(pauseAniQuad); /* No cycle */ static unsigned char pauseAniAlpha[] = { 0x00, 0x20, 0x40, 0x60, 0x80, 0xA0, 0xC0, 0xE0, 0xFF }; static elementsN(pauseAniAlpha); /* Points to an array of quads which it doesn't own. * Useful for setting alpha of quads stored inside * bigger arrays */ struct QuadChunk { Vertex *vert; int count; /* In quads */ QuadChunk() : vert(0), count(0) {} void setAlpha(float value) { for (int i = 0; i < count*4; ++i) vert[i].color.w = value; } }; /* Vocabulary: * * Base: 'Base' layer of window; includes background and borders. * Drawn at z+0. * * Controls: 'Controls' layer of window; includes scroll arrows, * pause animation, cursor rectangle and contents bitmap. * Drawn at z+2. * * Scroll arrows: Arrows that appear automatically when a part of * the contents bitmap is not visible in either upper, lower, left * or right direction. * * Pause: Animation that displays an animating icon in the bottom * center of the window, usually indicating user input is awaited, * such as when text is displayed. * * Cursor: Blinking rectangle that usually displays a selection to * the user. * * Contents: User settable bitmap that is drawn inside the window, * clipped to a 16 pixel smaller rectangle. Position is adjusted * with OX/OY. * * BaseTex: If the window has an opacity <255, we have to prerender * the base to a texture and draw that. Otherwise, we can draw the * quad array directly to the screen. */ struct WindowPrivate { Bitmap *windowskin; Bitmap *contents; bool bgStretch; Rect *cursorRect; bool active; bool pause; sigc::connection cursorRectCon; Vec2i sceneOffset; Vec2i position; Vec2i size; Vec2i contentsOffset; NormValue opacity; NormValue backOpacity; NormValue contentsOpacity; bool baseVertDirty; bool opacityDirty; bool baseTexDirty; ColorQuadArray baseQuadArray; /* Used when opacity < 255 */ TEXFBO baseTex; bool useBaseTex; QuadChunk backgroundVert; Quad baseTexQuad; struct WindowControls : public ViewportElement { WindowPrivate *p; WindowControls(WindowPrivate *p, Viewport *viewport = 0) : ViewportElement(viewport), p(p) { setZ(2); } void draw() { p->drawControls(); } void release() { unlink(); } }; WindowControls controlsElement; ColorQuadArray controlsQuadArray; int controlsQuadCount; Quad contentsQuad; QuadChunk pauseAniVert; QuadChunk cursorVert; unsigned char cursorAniAlphaIdx; unsigned char pauseAniAlphaIdx; unsigned char pauseAniQuadIdx; bool controlsVertDirty; EtcTemps tmp; sigc::connection prepareCon; WindowPrivate(Viewport *viewport = 0) : windowskin(0), contents(0), bgStretch(true), cursorRect(&tmp.rect), active(true), pause(false), opacity(255), backOpacity(255), contentsOpacity(255), baseVertDirty(true), opacityDirty(true), baseTexDirty(true), controlsElement(this, viewport), cursorAniAlphaIdx(0), pauseAniAlphaIdx(0), pauseAniQuadIdx(0), controlsVertDirty(true) { refreshCursorRectCon(); controlsQuadArray.resize(14); TileQuads::buildFrameSource(cursorSrc, controlsQuadArray.vertices.data()); cursorVert.count = 9; pauseAniVert.count = 1; prepareCon = gState->prepareDraw.connect (sigc::mem_fun(this, &WindowPrivate::prepare)); } ~WindowPrivate() { gState->texPool().release(baseTex); cursorRectCon.disconnect(); prepareCon.disconnect(); } void onCursorRectChange() { controlsVertDirty = true; } void refreshCursorRectCon() { cursorRectCon.disconnect(); cursorRectCon = cursorRect->valueChanged.connect (sigc::mem_fun(this, &WindowPrivate::onCursorRectChange)); } void buildBaseVert() { int w = size.x; int h = size.y; IntRect bgRect(2, 2, w - 4, h - 4); Sides borderRects; borderRects.l = IntRect(0, 8, 16, h-16); borderRects.r = IntRect(w-16, 8, 16, h-16); borderRects.t = IntRect(8, 0, w-16, 16 ); borderRects.b = IntRect(8, h-16, w-16, 16 ); Corners cornerRects; cornerRects.tl = IntRect(0, 0, 16, 16); cornerRects.tr = IntRect(w-16, 0, 16, 16); cornerRects.bl = IntRect(0, h-16, 16, 16); cornerRects.br = IntRect(w-16, h-16, 16, 16); /* Required quad count */ int count = 0; /* Background */ if (bgStretch) backgroundVert.count = 1; else backgroundVert.count = TileQuads::twoDimCount(128, 128, bgRect.w, bgRect.h); count += backgroundVert.count; /* Borders (sides) */ count += TileQuads::oneDimCount(32, w-16) * 2; count += TileQuads::oneDimCount(32, h-16) * 2; /* Corners */ count += 4; /* Our vertex array */ baseQuadArray.resize(count); Vertex *vert = baseQuadArray.vertices.data(); int i = 0; backgroundVert.vert = &vert[i]; /* Background */ if (bgStretch) { Quad::setTexRect(&vert[i*4], backgroundSrc); Quad::setPosRect(&vert[i*4], bgRect); i += 1; } else { i += TileQuads::build(backgroundSrc, bgRect, &vert[i*4]); } /* Borders */ i += TileQuads::buildH(bordersSrc.t, w-16, 8, 0, &vert[i*4]); i += TileQuads::buildH(bordersSrc.b, w-16, 8, h-16, &vert[i*4]); i += TileQuads::buildV(bordersSrc.l, h-16, 0, 8, &vert[i*4]); i += TileQuads::buildV(bordersSrc.r, h-16, w-16, 8, &vert[i*4]); /* Corners */ i += Quad::setTexPosRect(&vert[i*4], cornersSrc.tl, cornerRects.tl); i += Quad::setTexPosRect(&vert[i*4], cornersSrc.tr, cornerRects.tr); i += Quad::setTexPosRect(&vert[i*4], cornersSrc.bl, cornerRects.bl); i += Quad::setTexPosRect(&vert[i*4], cornersSrc.br, cornerRects.br); for (int j = 0; j < count*4; ++j) vert[j].color = Vec4(1, 1, 1, 1); FloatRect texRect = FloatRect(0, 0, size.x, size.y); baseTexQuad.setTexPosRect(texRect, texRect); opacityDirty = true; baseTexDirty = true; } void updateBaseAlpha() { /* This is always applied unconditionally */ backgroundVert.setAlpha(backOpacity.norm); baseTexQuad.setColor(Vec4(1, 1, 1, opacity.norm)); baseTexDirty = true; } void ensureBaseTexReady() { /* Make sure texture is big enough */ int newW = baseTex.width; int newH = baseTex.height; bool resizeNeeded = false; if (size.x > baseTex.width) { newW = findNextPow2(size.x); resizeNeeded = true; } if (size.y > baseTex.height) { newH = findNextPow2(size.y); resizeNeeded = true; } if (!resizeNeeded) return; gState->texPool().release(baseTex); baseTex = gState->texPool().request(newW, newH); baseTexDirty = true; } void redrawBaseTex() { /* Discard old buffer */ TEX::bind(baseTex.tex); TEX::allocEmpty(baseTex.width, baseTex.height); TEX::unbind(); FBO::bind(baseTex.fbo, FBO::Draw); glState.viewport.pushSet(IntRect(0, 0, baseTex.width, baseTex.height)); glState.clearColor.pushSet(Vec4()); SimpleAlphaShader &shader = gState->simpleAlphaShader(); shader.bind(); shader.applyViewportProj(); shader.setTranslation(Vec2i()); /* Clear texture */ glClear(GL_COLOR_BUFFER_BIT); /* Repaint base */ windowskin->bindTex(shader); TEX::setSmooth(true); /* We need to blit the background without blending, * because we want to retain its correct alpha value. * Otherwise it would be mutliplied by the backgrounds 0 alpha */ glState.blendMode.pushSet(BlendNone); baseQuadArray.draw(0, backgroundVert.count); /* Now draw the rest (ie. the frame) with blending */ glState.blendMode.set(BlendNormal); baseQuadArray.draw(backgroundVert.count, baseQuadArray.count()-backgroundVert.count); glState.clearColor.pop(); glState.blendMode.pop(); glState.viewport.pop(); TEX::setSmooth(false); } void buildControlsVert() { int i = 0; Vertex *vert = controlsQuadArray.vertices.data(); /* Cursor */ if (!cursorRect->isEmpty()) { /* Effective cursor rect has 16 xy offset to window */ IntRect effectRect(cursorRect->x+16, cursorRect->y+16, cursorRect->width, cursorRect->height); cursorVert.vert = &vert[i*4]; i += TileQuads::buildFrame(effectRect, &vert[i*4]); } /* Scroll arrows */ int scrollLRY = (size.y - 16) / 2; int scrollTBX = (size.x - 16) / 2; Sides scrollArrows; scrollArrows.l = IntRect(4, scrollLRY, 8, 16); scrollArrows.r = IntRect(size.x - 12, scrollLRY, 8, 16); scrollArrows.t = IntRect(scrollTBX, 4, 16, 8); scrollArrows.b = IntRect(scrollTBX, size.y - 12, 16, 8); if (contents) { if (contentsOffset.x > 0) i += Quad::setTexPosRect(&vert[i*4], scrollArrowSrc.l, scrollArrows.l); if (contentsOffset.y > 0) i += Quad::setTexPosRect(&vert[i*4], scrollArrowSrc.t, scrollArrows.t); if ((size.x - 32) < (contents->width() - contentsOffset.x)) i += Quad::setTexPosRect(&vert[i*4], scrollArrowSrc.r, scrollArrows.r); if ((size.y - 32) < (contents->height() - contentsOffset.y)) i += Quad::setTexPosRect(&vert[i*4], scrollArrowSrc.b, scrollArrows.b); } /* Pause animation */ if (pause) { pauseAniVert.vert = &vert[i*4]; i += Quad::setTexPosRect(&vert[i*4], pauseAniSrc[pauseAniQuad[pauseAniQuadIdx]], FloatRect((size.x - 16) / 2, size.y - 16, 16, 16)); } controlsQuadArray.commit(); controlsQuadCount = i; } void prepare() { bool updateBaseQuadArray = false; if (windowskin) windowskin->flush(); if (contents) contents->flush(); if (baseVertDirty) { buildBaseVert(); baseVertDirty = false; updateBaseQuadArray = true; } if (opacityDirty) { updateBaseAlpha(); opacityDirty = false; updateBaseQuadArray = true; } if (updateBaseQuadArray) baseQuadArray.commit(); /* If opacity has effect, we must prerender to a texture * and then draw this texture instead of the quad array */ useBaseTex = opacity < 255; if (useBaseTex) { ensureBaseTexReady(); if (baseTexDirty) { redrawBaseTex(); baseTexDirty = false; } } } void drawBase() { if (!windowskin) return; if (size == Vec2i(0, 0)) return; Vec2i trans(position.x + sceneOffset.x, position.y + sceneOffset.y); SimpleAlphaShader &shader = gState->simpleAlphaShader(); shader.bind(); shader.applyViewportProj(); shader.setTranslation(trans); if (useBaseTex) { shader.setTexSize(Vec2i(baseTex.width, baseTex.height)); TEX::bind(baseTex.tex); baseTexQuad.draw(); } else { windowskin->bindTex(shader); TEX::setSmooth(true); baseQuadArray.draw(); TEX::setSmooth(false); } } void drawControls() { if (!windowskin) return; if (size == Vec2i(0, 0)) return; if (controlsVertDirty) { buildControlsVert(); updateControls(); controlsVertDirty = false; } /* Actual on screen coordinates */ int effectX = position.x + sceneOffset.x; int effectY = position.y + sceneOffset.y; IntRect windowRect(effectX, effectY, size.x, size.y); IntRect contentsRect(effectX+16, effectY+16, size.x-32, size.y-32); glState.scissorTest.pushSet(true); glState.scissorBox.push(); glState.scissorBox.setIntersect(windowRect); SimpleAlphaShader &shader = gState->simpleAlphaShader(); shader.bind(); shader.applyViewportProj(); shader.setTranslation(Vec2i(effectX, effectY)); /* Draw arrows / cursors */ windowskin->bindTex(shader); controlsQuadArray.draw(0, controlsQuadCount); if (contents) { /* Draw contents bitmap */ glState.scissorBox.setIntersect(contentsRect); effectX += 16-contentsOffset.x; effectY += 16-contentsOffset.y; shader.setTranslation(Vec2i(effectX, effectY)); contents->bindTex(shader); contentsQuad.draw(); } glState.scissorBox.pop(); glState.scissorTest.pop(); } void updateControls() { bool updateArray = false; if (active && cursorVert.vert) { float alpha = cursorAniAlpha[cursorAniAlphaIdx] / 255.0; cursorVert.setAlpha(alpha); updateArray = true; } if (pause && pauseAniVert.vert) { float alpha = pauseAniAlpha[pauseAniAlphaIdx] / 255.0; FloatRect frameRect = pauseAniSrc[pauseAniQuad[pauseAniQuadIdx]]; pauseAniVert.setAlpha(alpha); Quad::setTexRect(pauseAniVert.vert, frameRect); updateArray = true; } if (updateArray) controlsQuadArray.commit(); } void stepAnimations() { if (++cursorAniAlphaIdx == cursorAniAlphaN) cursorAniAlphaIdx = 0; if (pauseAniAlphaIdx < pauseAniAlphaN-1) ++pauseAniAlphaIdx; if (++pauseAniQuadIdx == pauseAniQuadN) pauseAniQuadIdx = 0; } }; Window::Window(Viewport *viewport) : ViewportElement(viewport) { p = new WindowPrivate(viewport); onGeometryChange(scene->getGeometry()); } Window::~Window() { dispose(); } void Window::update() { p->updateControls(); p->stepAnimations(); } #define DISP_CLASS_NAME "window" DEF_ATTR_SIMPLE(Window, X, int, p->position.x) DEF_ATTR_SIMPLE(Window, Y, int, p->position.y) DEF_ATTR_RD_SIMPLE(Window, Windowskin, Bitmap*, p->windowskin) DEF_ATTR_RD_SIMPLE(Window, Contents, Bitmap*, p->contents) DEF_ATTR_RD_SIMPLE(Window, Stretch, bool, p->bgStretch) DEF_ATTR_RD_SIMPLE(Window, CursorRect, Rect*, p->cursorRect) DEF_ATTR_RD_SIMPLE(Window, Active, bool, p->active) DEF_ATTR_RD_SIMPLE(Window, Pause, bool, p->pause) DEF_ATTR_RD_SIMPLE(Window, Width, int, p->size.x) DEF_ATTR_RD_SIMPLE(Window, Height, int, p->size.y) DEF_ATTR_RD_SIMPLE(Window, OX, int, p->contentsOffset.x) DEF_ATTR_RD_SIMPLE(Window, OY, int, p->contentsOffset.y) DEF_ATTR_RD_SIMPLE(Window, Opacity, int, p->opacity) DEF_ATTR_RD_SIMPLE(Window, BackOpacity, int, p->backOpacity) DEF_ATTR_RD_SIMPLE(Window, ContentsOpacity, int, p->contentsOpacity) void Window::setWindowskin(Bitmap *value) { GUARD_DISPOSED; p->windowskin = value; if (!value) return; value->ensureNonMega(); } void Window::setContents(Bitmap *value) { GUARD_DISPOSED; if (p->contents == value) return; p->contents = value; p->controlsVertDirty = true; if (!value) return; value->ensureNonMega(); p->contentsQuad.setTexPosRect(value->rect(), value->rect()); } void Window::setStretch(bool value) { GUARD_DISPOSED if (value == p->bgStretch) return; p->bgStretch = value; p->baseVertDirty = true; } void Window::setCursorRect(Rect *value) { GUARD_DISPOSED if (p->cursorRect == value) return; p->cursorRect = value; p->refreshCursorRectCon(); p->onCursorRectChange(); } void Window::setActive(bool value) { GUARD_DISPOSED if (p->active == value) return; p->active = value; p->cursorAniAlphaIdx = 0; } void Window::setPause(bool value) { GUARD_DISPOSED if (p->pause == value) return; p->pause = value; p->pauseAniAlphaIdx = 0; p->pauseAniQuadIdx = 0; p->controlsVertDirty = true; } void Window::setWidth(int value) { GUARD_DISPOSED if (p->size.x == value) return; p->size.x = value; p->baseVertDirty = true; } void Window::setHeight(int value) { GUARD_DISPOSED if (p->size.y == value) return; p->size.y = value; p->baseVertDirty = true; } void Window::setOX(int value) { GUARD_DISPOSED if (p->contentsOffset.x == value) return; p->contentsOffset.x = value; p->controlsVertDirty = true; } void Window::setOY(int value) { GUARD_DISPOSED if (p->contentsOffset.y == value) return; p->contentsOffset.y = value; p->controlsVertDirty = true; } void Window::setOpacity(int value) { GUARD_DISPOSED if (p->opacity == value) return; p->opacity = value; p->opacityDirty = true; } void Window::setBackOpacity(int value) { GUARD_DISPOSED if (p->backOpacity == value) return; p->backOpacity = value; p->opacityDirty = true; } void Window::setContentsOpacity(int value) { GUARD_DISPOSED if (p->contentsOpacity == value) return; p->contentsOpacity = value; p->contentsQuad.setColor(Vec4(1, 1, 1, p->contentsOpacity.norm)); } void Window::draw() { p->drawBase(); } void Window::onGeometryChange(const Scene::Geometry &geo) { p->sceneOffset.x = geo.rect.x - geo.xOrigin; p->sceneOffset.y = geo.rect.y - geo.yOrigin; } void Window::setZ(int value) { ViewportElement::setZ(value); p->controlsElement.setZ(value + 2); } void Window::setVisible(bool value) { ViewportElement::setVisible(value); p->controlsElement.setVisible(value); } void Window::onViewportChange() { p->controlsElement.setScene(*this->scene); } void Window::releaseResources() { p->controlsElement.release(); unlink(); delete p; }