From d842d67557d0a55e25de170b065ae25df19ffd66 Mon Sep 17 00:00:00 2001 From: root Date: Mon, 13 Aug 2012 13:32:53 -0500 Subject: [PATCH] Tons of new theming features! Check out THEMES.md for more info. --- CREDITS.md | 24 ++++++ README.md | 17 +--- THEMES.md | 74 ++++++++++++++++ changelog.txt | 5 ++ src/Renderer_draw.cpp | 20 ++++- src/components/GuiGameList.cpp | 17 ++-- src/components/GuiImage.cpp | 153 +++++++++++++++++++++++++-------- src/components/GuiImage.h | 8 +- src/components/GuiList.cpp | 11 ++- src/components/GuiList.h | 3 + src/components/GuiTheme.cpp | 88 +++++++++++++++++-- src/components/GuiTheme.h | 13 +++ src/main.cpp | 2 + 13 files changed, 364 insertions(+), 71 deletions(-) create mode 100644 CREDITS.md create mode 100644 THEMES.md diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 000000000..1e7f5c61f --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,24 @@ +Programming + Alec "Aloshi" Lofquist - http://www.aloshi.com + + +Resources +========= + +LinLibertine.ttf + The Libertine Font Project - http://www.linuxlibertine.org/ + +PugiXML + http://pugixml.org/ + +SDL 1.2 + http://www.libsdl.org/ + +SDL TTF + http://www.libsdl.org/projects/SDL_ttf/ + +SDL_image + http://www.libsdl.org/projects/SDL_image/ + +SDL_gfx + http://sourceforge.net/projects/sdlgfx/ diff --git a/README.md b/README.md index f081cf03c..48f5bebd5 100644 --- a/README.md +++ b/README.md @@ -55,23 +55,8 @@ The switch `--gamelist-only` can be used to skip automatic searching, and only d Themes ====== -At the moment, theming is still in flux. But if you want to play around with what's here, feel free. ES will first check a system's search directory for a file named theme.xml. If that's not found, it'll check $HOME/.emulationstation/es_theme.xml. -Themes are drawn before the rest of the game list. Here's the example I've been using to test a background: +If you want to know more about themes, read THEMES.md! -``` - - - image - /home/aloshi/EmulationStation/theme/background.png - 0 0 - 1 1 - - -``` - -You can add more than one component. You can use more than one component and components can be nested for your own personal use (but they won't inherit positions or anything). The only type thus far is image. Pos is short for position and dim is short for dimensions. Both work in screen percentages - a decimal from 0 to 1. A single space separates X/Y or width/height. -At the moment the X position is the horizontal center point for the image and Y is the top of the image. -Variable support is present, but the only variable defined right now is $headerHeight. You should be able to use addition/subtraction/multiplication/division. -Aloshi http://www.aloshi.com diff --git a/THEMES.md b/THEMES.md new file mode 100644 index 000000000..51b76a4ab --- /dev/null +++ b/THEMES.md @@ -0,0 +1,74 @@ +Themes +====== + +EmulationStation allows each system to have its own "theme." A theme is a collection of display settings and images defined in an XML document. + +ES will check two places for a theme: first, the system's search directory for theme.xml. Then, if that's not found, $HOME/.emulationstation/es_theme.xml. + +Almost all positions, dimensions, etc. work in percentages - that is, they are a decimal between 0 and 1, representing the percentage of the screen on that axis to use. +This ensures that themes look similar at every resolution. + + +Example +======= + +Here's a simple theme that defines some colors and displays a background: +``` + + 0000FF + 00FF00 + + + image + ./theme/background.png + 0 0 + 1 1 + 0 0 + + +``` + +All themes must be enclosed in a `` tag. + + +Components +========== +A theme is made up of components, which have various types. At the moment, the only type is `image`. Components can be nested for your own organization. In the future, you may be able to get data from a parent. + + +The "image" component +===================== +Used to display an image. + +`` - path to the image file. Most common file types are supported, and . and ~ are properly expanded. +`` - the position, as two screen percentage, at which to display the image. +`` - the dimensions, as two screen percentages, that the image will be resized to. Leave one percentage 0 to keep the aspect ratio. +`` - the point on the image that defines, as an image percentage. "0.5 0.5", the center of the image, by default. + +`` - if present, the image is tiled instead of resized. Tiling isn't exact at the moment, but good enough for backgrounds. +`` - if present, the image will not be stripped of its alpha channel. It will render much slower, but should have transparency. + + +Display tags +============ +Display tags must be at the root of the tree - for example, they can't be inside a component tag. They are not required. + +`` - the hex font color to use for games on the GuiGameList. +`` - the hex font color to use for folders on the GuiGameList. +`` - the hex font color to use for the description on the GuiGameList. +`` - the hex color to use for the "selector bar" on the GuiGameList. +`` - if present, the system name header won't be displayed (useful for replacing it with an image). +`` - if present, the divider between games on the detailed GuiGameList won't be displayed. + + +List of variables +================= + +Variables can be used in position and dimension definitions. They can be added, subtracted, multiplied, and divided. Parenthesis are valid. + +Currently, there's only one variable defined: +`$headerHeight` - height of the system name header. + + +-Aloshi +http://www.aloshi.com diff --git a/changelog.txt b/changelog.txt index 026a3c059..f10073102 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,8 @@ +August 13 +-Tons of new theming features! +-I've added a THEMES.md file for documentation on theming. +-A CREDITS.md file. + August 12 -If a theme.xml is not found in a system's directory, ES will now check for $HOME/.emulationstation/es_theme.xml. If present, it will load that. -Themes can now be used without the detailed GuiGameList. diff --git a/src/Renderer_draw.cpp b/src/Renderer_draw.cpp index fa50daa3e..8073f3dd1 100644 --- a/src/Renderer_draw.cpp +++ b/src/Renderer_draw.cpp @@ -10,6 +10,21 @@ bool Renderer::loadedFonts = false; TTF_Font* Renderer::fonts[3]; int Renderer::fontHeight[3]; + +SDL_Color getSDLColor(int& color) +{ + char* c = (char*)(&color); + + SDL_Color ret; + ret.r = *(c + 2); + ret.g = *(c + 1); + ret.b = *(c + 0); + + return ret; +} + + + void Renderer::drawRect(int x, int y, int h, int w, int color) { SDL_Rect rect = {x, y, h, w}; @@ -67,9 +82,10 @@ void Renderer::drawText(std::string text, int x, int y, int color, FontSize font //SDL_Color is a struct of four bytes, with the first three being colors. An int is four bytes. //So, we can just pretend the int is an SDL_Color. - SDL_Color* sdlcolor = (SDL_Color*)&color; + //SDL_Color* sdlcolor = (SDL_Color*)&color; + SDL_Color sdlcolor = getSDLColor(color); - SDL_Surface* textSurf = TTF_RenderText_Blended(font, text.c_str(), *sdlcolor); + SDL_Surface* textSurf = TTF_RenderText_Blended(font, text.c_str(), sdlcolor); if(textSurf == NULL) { std::cerr << "Error - could not render text \"" << text << "\" to surface!\n"; diff --git a/src/components/GuiGameList.cpp b/src/components/GuiGameList.cpp index 65b4004a5..947c52ff1 100644 --- a/src/components/GuiGameList.cpp +++ b/src/components/GuiGameList.cpp @@ -24,6 +24,7 @@ GuiGameList::GuiGameList(bool useDetail) mList = new GuiList(Renderer::getScreenWidth() * 0.4, Renderer::getFontHeight(Renderer::LARGE) + 2); mScreenshot = new GuiImage(Renderer::getScreenWidth() * 0.2, Renderer::getFontHeight(Renderer::LARGE) + 2, "", Renderer::getScreenWidth() * 0.3); + mScreenshot->setOrigin(0.5, 0.0); addChild(mScreenshot); }else{ mList = new GuiList(0, Renderer::getFontHeight(Renderer::LARGE) + 2); @@ -96,12 +97,14 @@ void GuiGameList::onRender() Renderer::drawText(fps, 0, 0, 0x00FF00); #endif - - Renderer::drawCenteredText(mSystem->getName(), 0, 1, 0x0000FF, Renderer::LARGE); + //header + if(!mTheme->getHeaderHidden()) + Renderer::drawCenteredText(mSystem->getName(), 0, 1, 0xFF0000, Renderer::LARGE); if(mDetailed) { - Renderer::drawRect(Renderer::getScreenWidth() * 0.4, Renderer::getFontHeight(Renderer::LARGE) + 2, 8, Renderer::getScreenHeight(), 0x0000FF); + if(!mTheme->getDividersHidden()) + Renderer::drawRect(Renderer::getScreenWidth() * 0.4, Renderer::getFontHeight(Renderer::LARGE) + 2, 8, Renderer::getScreenHeight(), 0x0000FF); //if we have selected a non-folder if(mList->getSelectedObject() && !mList->getSelectedObject()->isFolder()) @@ -111,7 +114,7 @@ void GuiGameList::onRender() //todo: cache this std::string desc = game->getDescription(); if(!desc.empty()) - Renderer::drawWrappedText(desc, 2, Renderer::getFontHeight(Renderer::LARGE) + mScreenshot->getHeight() + 10, Renderer::getScreenWidth() * 0.4, 0xFF0000, Renderer::SMALL); + Renderer::drawWrappedText(desc, 2, Renderer::getFontHeight(Renderer::LARGE) + mScreenshot->getHeight() + 10, Renderer::getScreenWidth() * 0.4, mTheme->getDescColor(), Renderer::SMALL); } } } @@ -178,9 +181,9 @@ void GuiGameList::updateList() FileData* file = mFolder->getFile(i); if(file->isFolder()) - mList->addObject(file->getName(), file, 0x00C000); + mList->addObject(file->getName(), file, mTheme->getSecondaryColor()); else - mList->addObject(file->getName(), file); + mList->addObject(file->getName(), file, mTheme->getPrimaryColor()); } } @@ -199,6 +202,8 @@ void GuiGameList::updateTheme() mTheme->readXML(defaultPath); else mTheme->readXML(""); //clears any current theme + + mList->setSelectorColor(mTheme->getSelectorColor()); } void GuiGameList::updateDetailData() diff --git a/src/components/GuiImage.cpp b/src/components/GuiImage.cpp index f0af5720f..a670bdcdf 100644 --- a/src/components/GuiImage.cpp +++ b/src/components/GuiImage.cpp @@ -14,9 +14,17 @@ GuiImage::GuiImage(int offsetX, int offsetY, std::string path, unsigned int maxW mOffsetX = offsetX; mOffsetY = offsetY; + //default origin (center of image) + mOriginX = 0; + mOriginY = 0; + + mTiled = false; + mMaxWidth = maxWidth; mMaxHeight = maxHeight; + mResizeExact = resizeExact; + mUseAlpha = false; if(!path.empty()) setImage(path); @@ -43,38 +51,15 @@ void GuiImage::loadImage(std::string path) } - //resize it - if(mResizeExact) - { - double scaleX = (double)mMaxWidth / (double)newSurf->w; - double scaleY = (double)mMaxHeight / (double)newSurf->h; - - SDL_Surface* resSurf = zoomSurface(newSurf, scaleX, scaleY, SMOOTHING_OFF); - SDL_FreeSurface(newSurf); - newSurf = resSurf; - }else{ - if(mMaxWidth && newSurf->w > mMaxWidth) - { - double scale = (double)mMaxWidth / (double)newSurf->w; - - SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF); - SDL_FreeSurface(newSurf); - newSurf = resSurf; - } - - if(mMaxHeight && newSurf->h > mMaxHeight) - { - double scale = (double)mMaxHeight / (double)newSurf->h; - - SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF); - SDL_FreeSurface(newSurf); - newSurf = resSurf; - } - } - + resizeSurface(&newSurf); //convert it into display format for faster rendering - SDL_Surface* dispSurf = SDL_DisplayFormat(newSurf); + SDL_Surface* dispSurf; + if(mUseAlpha) + dispSurf = SDL_DisplayFormatAlpha(newSurf); + else + dispSurf = SDL_DisplayFormat(newSurf); + SDL_FreeSurface(newSurf); newSurf = dispSurf; @@ -85,16 +70,56 @@ void GuiImage::loadImage(std::string path) mSurface = newSurf; - //Also update the rect - mRect.x = mOffsetX - (mSurface->w / 2); - mRect.y = mOffsetY; - mRect.w = mSurface->w; - mRect.h = mSurface->h; + updateRect(); + }else{ std::cerr << "File \"" << path << "\" not found!\n"; } } +//enjoy this overly-complicated pointer stuff that results from splitting a function too late +void GuiImage::resizeSurface(SDL_Surface** surfRef) +{ + if(mTiled) + return; + + SDL_Surface* newSurf = *surfRef; + if(mResizeExact) + { + double scaleX = (double)mMaxWidth / (double)newSurf->w; + double scaleY = (double)mMaxHeight / (double)newSurf->h; + + if(scaleX == 0) + scaleX = scaleY; + if(scaleY == 0) + scaleY = scaleX; + + SDL_Surface* resSurf = zoomSurface(newSurf, scaleX, scaleY, SMOOTHING_OFF); + SDL_FreeSurface(newSurf); + newSurf = resSurf; + }else{ + if(mMaxWidth && newSurf->w > mMaxWidth) + { + double scale = (double)mMaxWidth / (double)newSurf->w; + + SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF); + SDL_FreeSurface(newSurf); + newSurf = resSurf; + } + + if(mMaxHeight && newSurf->h > mMaxHeight) + { + double scale = (double)mMaxHeight / (double)newSurf->h; + + SDL_Surface* resSurf = zoomSurface(newSurf, scale, scale, SMOOTHING_OFF); + SDL_FreeSurface(newSurf); + newSurf = resSurf; + } + } + + *surfRef = newSurf; +} + void GuiImage::setImage(std::string path) { if(mPath == path) @@ -113,8 +138,64 @@ void GuiImage::setImage(std::string path) } +void GuiImage::updateRect() +{ + mRect.x = mOffsetX /*- mSurface->w*/ - (mSurface->w * mOriginX); + mRect.y = mOffsetY + (mSurface->h * mOriginY); + mRect.w = mSurface->w; + mRect.h = mSurface->h; +} + +void GuiImage::setOrigin(float originX, float originY) +{ + mOriginX = originX; + mOriginY = originY; + + if(mSurface) + updateRect(); +} + +void GuiImage::setTiling(bool tile) +{ + mTiled = tile; + + if(mTiled) + mResizeExact = false; +} + +void GuiImage::setAlpha(bool useAlpha) +{ + mUseAlpha = useAlpha; + + if(mSurface) + { + SDL_FreeSurface(mSurface); + mSurface = NULL; + loadImage(mPath); + } +} + +bool dbg = false; + void GuiImage::onRender() { if(mSurface) - SDL_BlitSurface(mSurface, NULL, Renderer::screen, &mRect); + { + if(mTiled) + { + SDL_Rect rect = mRect; + for(int x = 0; x < mMaxWidth / mSurface->w + 0.5; x++) + { + for(int y = 0; y < mMaxHeight / mSurface->h + 0.5; y++) + { + SDL_BlitSurface(mSurface, NULL, Renderer::screen, &rect); + rect.y += mSurface->h; + } + rect.x += mSurface->w; + rect.y = mRect.y; + } + }else{ + SDL_BlitSurface(mSurface, NULL, Renderer::screen, &mRect); + } + } } diff --git a/src/components/GuiImage.h b/src/components/GuiImage.h index 4d4d22432..9ca600488 100644 --- a/src/components/GuiImage.h +++ b/src/components/GuiImage.h @@ -13,6 +13,9 @@ public: ~GuiImage(); void setImage(std::string path); + void setOrigin(float originX, float originY); + void setTiling(bool tile); + void setAlpha(bool useAlpha); int getWidth(); int getHeight(); @@ -21,9 +24,12 @@ public: private: int mMaxWidth, mMaxHeight; - bool mResizeExact; + float mOriginX, mOriginY; + bool mResizeExact, mTiled, mUseAlpha; void loadImage(std::string path); + void resizeSurface(SDL_Surface** surfRef); + void updateRect(); std::string mPath; diff --git a/src/components/GuiList.cpp b/src/components/GuiList.cpp index f659e98d9..3820d9daa 100644 --- a/src/components/GuiList.cpp +++ b/src/components/GuiList.cpp @@ -13,6 +13,7 @@ GuiList::GuiList(int offsetX, int offsetY, Renderer::FontSize fontsize mOffsetY = offsetY; mFont = fontsize; + mSelectorColor = 0x000000; InputManager::registerComponent(this); } @@ -48,7 +49,7 @@ void GuiList::onRender() if(mRowVector.size() == 0) { - Renderer::drawCenteredText("The list is empty.", 0, y, 0xFF0000, mFont); + Renderer::drawCenteredText("The list is empty.", 0, y, 0x0000FF, mFont); return; } @@ -60,7 +61,7 @@ void GuiList::onRender() { if(mSelection == i) { - Renderer::drawRect(mOffsetX, y, Renderer::getScreenWidth(), Renderer::getFontHeight(mFont), 0x000000); + Renderer::drawRect(mOffsetX, y, Renderer::getScreenWidth(), Renderer::getFontHeight(mFont), mSelectorColor); } ListRow row = mRowVector.at((unsigned int)i); @@ -188,3 +189,9 @@ void GuiList::onResume() { InputManager::registerComponent(this); } + +template +void GuiList::setSelectorColor(int selectorColor) +{ + mSelectorColor = selectorColor; +} diff --git a/src/components/GuiList.h b/src/components/GuiList.h index b2beb7236..7d2056e68 100644 --- a/src/components/GuiList.h +++ b/src/components/GuiList.h @@ -31,11 +31,14 @@ public: std::string getSelectedName(); listType getSelectedObject(); int getSelection(); + + void setSelectorColor(int selectorColor); private: int mScrollDir, mScrollAccumulator; bool mScrolling; Renderer::FontSize mFont; + int mSelectorColor; int mOffsetX, mOffsetY; diff --git a/src/components/GuiTheme.cpp b/src/components/GuiTheme.cpp index 7c6a13957..08e86bc20 100644 --- a/src/components/GuiTheme.cpp +++ b/src/components/GuiTheme.cpp @@ -3,9 +3,24 @@ #include #include "GuiImage.h" #include +#include + +int GuiTheme::getPrimaryColor() { return mListPrimaryColor; } +int GuiTheme::getSecondaryColor() { return mListSecondaryColor; } +int GuiTheme::getSelectorColor() { return mListSelectorColor; } +int GuiTheme::getDescColor() { return mDescColor; } +bool GuiTheme::getHeaderHidden() { return mHideHeader; } +bool GuiTheme::getDividersHidden() { return mHideDividers; } GuiTheme::GuiTheme(std::string path) { + mListPrimaryColor = 0x0000FF; + mListSecondaryColor = 0x00FF00; + mListSelectorColor = 0x000000; + mDescColor = 0x0000FF; + mHideHeader = false; + mHideDividers = false; + if(!path.empty()) readXML(path); } @@ -55,6 +70,14 @@ void GuiTheme::readXML(std::string path) pugi::xml_node root = doc.child("theme"); + //load non-component theme stuff + mListPrimaryColor = resolveColor(root.child("listPrimaryColor").text().get(), 0x0000FF); + mListSecondaryColor = resolveColor(root.child("listSecondaryColor").text().get(), 0x00FF00); + mListSelectorColor = resolveColor(root.child("listSelectorColor").text().get(), 0x000000); + mDescColor = resolveColor(root.child("descColor").text().get(), 0x0000FF); + mHideHeader = root.child("hideHeader"); + mHideDividers = root.child("hideDividers"); + for(pugi::xml_node data = root.child("component"); data; data = data.next_sibling("component")) { createElement(data, this); @@ -79,17 +102,22 @@ GuiComponent* GuiTheme::createElement(pugi::xml_node data, GuiComponent* parent) std::string pos = data.child("pos").text().get(); std::string dim = data.child("dim").text().get(); + std::string origin = data.child("origin").text().get(); + + bool useAlpha = data.child("useAlpha"); + bool tiled = data.child("tiled"); //split position and dimension information - size_t posSplit = pos.find(' '); - std::string posX = pos.substr(0, posSplit); - std::string posY = pos.substr(posSplit + 1, pos.length() - posSplit - 1); + std::string posX, posY; + splitString(pos, ' ', &posX, &posY); - size_t dimSplit = dim.find(' '); - std::string dimW = dim.substr(0, dimSplit); - std::string dimH = dim.substr(dimSplit + 1, dim.length() - dimSplit - 1); + std::string dimW, dimH; + splitString(dim, ' ', &dimW, &dimH); - std::cout << "image, x: " << posX << " y: " << posY << " w: " << dimW << " h: " << dimH << "\n"; + std::string originX, originY; + splitString(origin, ' ', &originX, &originY); + + std::cout << "image, x: " << posX << " y: " << posY << " w: " << dimW << " h: " << dimH << " ox: " << originX << " oy: " << originY << " alpha: " << useAlpha << " tiled: " << tiled << "\n"; //resolve to pixels from percentages/variables int x = resolveExp(posX) * Renderer::getScreenWidth(); @@ -97,9 +125,17 @@ GuiComponent* GuiTheme::createElement(pugi::xml_node data, GuiComponent* parent) int w = resolveExp(dimW) * Renderer::getScreenWidth(); int h = resolveExp(dimH) * Renderer::getScreenHeight(); + int ox = strToInt(originX); + int oy = strToInt(originY); + std::cout << "w: " << w << "px, h: " << h << "px\n"; - GuiComponent* comp = new GuiImage(x, y, path, w, h, true); + GuiImage* comp = new GuiImage(x, y, "", w, h, true); + comp->setOrigin(ox, oy); + comp->setAlpha(useAlpha); + comp->setTiling(tiled); + comp->setImage(path); + parent->addChild(comp); mComponentVector.push_back(comp); return comp; @@ -130,3 +166,39 @@ float GuiTheme::resolveExp(std::string str) return exp.eval(); } + +int GuiTheme::resolveColor(std::string str, int defaultColor) +{ + if(str.empty()) + return defaultColor; + + int ret; + std::stringstream ss; + ss << std::hex << str; + ss >> ret; + + std::cout << "resolved " << str << " to " << ret << "\n"; + return ret; +} + +void GuiTheme::splitString(std::string str, char delim, std::string* before, std::string* after) +{ + if(str.empty()) + return; + + size_t split = str.find(delim); + *before = str.substr(0, split); + *after = str.substr(split + 1, str.length() - split - 1); +} + +int GuiTheme::strToInt(std::string str) +{ + if(str.empty()) + return 0; + + int ret; + std::stringstream ss; + ss << str; + ss >> ret; + return ret; +} diff --git a/src/components/GuiTheme.h b/src/components/GuiTheme.h index 254484495..f26ef397f 100644 --- a/src/components/GuiTheme.h +++ b/src/components/GuiTheme.h @@ -12,14 +12,27 @@ public: void readXML(std::string path); + int getPrimaryColor(); + int getSecondaryColor(); + int getSelectorColor(); + int getDescColor(); + bool getHeaderHidden(); + bool getDividersHidden(); private: void deleteComponents(); GuiComponent* createElement(pugi::xml_node data, GuiComponent* parent); + + //utility functions std::string expandPath(std::string path); float resolveExp(std::string str); + int resolveColor(std::string str, int defaultColor = 0x000000); + void splitString(std::string str, char delim, std::string* before, std::string* after); + int strToInt(std::string str); std::vector mComponentVector; std::string mPath; + int mListPrimaryColor, mListSecondaryColor, mListSelectorColor, mDescColor; + bool mHideHeader, mHideDividers; }; #endif diff --git a/src/main.cpp b/src/main.cpp index e10b13d28..93526bc79 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,6 +17,8 @@ int main(int argc, char* argv[]) { bool running = true; + //by the way, if anyone ever tries to port this to a different renderer but leave SDL as input - + //KEEP INITIALIZING VIDEO. It starts SDL's event system, and without it, input won't work. if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_JOYSTICK) != 0) { std::cerr << "Error - could not initialize SDL!\n";