/* Copyright (C) 2007 - 2016 by Mark de Wever Part of the Battle for Wesnoth Project http://www.wesnoth.org/ This program 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. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY. See the COPYING file for more details. */ /** * @file * Implementation of canvas.hpp. */ #define GETTEXT_DOMAIN "wesnoth-lib" #include "gui/core/canvas.hpp" #include "config.hpp" #include "font/text.hpp" #include "formatter.hpp" #include "gettext.hpp" #include "image.hpp" #include "gui/auxiliary/typed_formula.hpp" #include "gui/core/log.hpp" #include "gui/widgets/helper.hpp" #include "sdl/rect.hpp" #include "video.hpp" #include "wml_exception.hpp" namespace gui2 { namespace { /*WIKI * @page = GUICanvasWML * * {{Autogenerated}} * * = Canvas = * * A canvas is a blank drawing area on which the user can draw several shapes. * The drawing is done by adding WML structures to the canvas. */ /*WIKI * @page = GUICanvasWML * @begin{parent}{name="generic/state/draw/"} * * == Pre commit == * @begin{tag}{name="pre_commit"}{min="0"}{max="1"} * * This section contains the pre commit functions. These functions will be * executed before the drawn canvas is applied on top of the normal * background. There should only be one pre commit section and its order * regarding the other shapes doesn't matter. The function has effect on the * entire canvas, it's not possible to affect only a small part of the canvas. * * The section can have one of the following subsections. * * === Blur === * @begin{tag}{name="blur"}{min="0"}{max="1"} * * Blurs the background before applying the canvas. This doesn't make sense * if the widget isn't semi-transparent. * * Keys: * @begin{table}{config} * depth & unsigned & 0 & The depth to blur. $ * @end{table} * @end{tag}{name="blur"} * @end{tag}{name="pre_commit"} */ /***** ***** ***** ***** ***** DRAWING PRIMITIVES ***** ***** ***** ***** *****/ static void set_renderer_color(SDL_Renderer* renderer, color_t color) { SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); } /** * Draws a line on a surface. * * @pre The caller needs to make sure the entire line fits on * the @p surface. * @pre @p x2 >= @p x1 * @pre The @p surface is locked. * * @param canvas The canvas to draw upon, the caller should lock the * surface before calling. * @param color The color of the line to draw. * @param x1 The start x coordinate of the line to draw. * @param y1 The start y coordinate of the line to draw. * @param x2 The end x coordinate of the line to draw. * @param y2 The end y coordinate of the line to draw. */ static void draw_line(surface& canvas, SDL_Renderer* renderer, color_t color, unsigned x1, unsigned y1, const unsigned x2, unsigned y2) { unsigned w = canvas->w; DBG_GUI_D << "Shape: draw line from " << x1 << ',' << y1 << " to " << x2 << ',' << y2 << " canvas width " << w << " canvas height " << canvas->h << ".\n"; assert(static_cast(x1) < canvas->w); assert(static_cast(x2) < canvas->w); assert(static_cast(y1) < canvas->h); assert(static_cast(y2) < canvas->h); set_renderer_color(renderer, color); if(x1 == x2 && y1 == y2) { // Handle single-pixel lines properly SDL_RenderDrawPoint(renderer, x1, y1); } else { SDL_RenderDrawLine(renderer, x1, y1, x2, y2); } } /** * Draws a circle on a surface. * * @pre The circle must fit on the canvas. * @pre The @p surface is locked. * * @param canvas The canvas to draw upon, the caller should lock the * surface before calling. * @param color The color of the circle to draw. * @param x_center The x coordinate of the center of the circle to draw. * @param y_center The y coordinate of the center of the circle to draw. * @param radius The radius of the circle to draw. */ static void draw_circle(surface& canvas, SDL_Renderer* renderer, color_t color, const int x_center, const int y_center, const int radius) { unsigned w = canvas->w; DBG_GUI_D << "Shape: draw circle at " << x_center << ',' << y_center << " with radius " << radius << " canvas width " << w << " canvas height " << canvas->h << ".\n"; assert((x_center + radius) < canvas->w); assert((x_center - radius) >= 0); assert((y_center + radius) < canvas->h); assert((y_center - radius) >= 0); set_renderer_color(renderer, color); // Algorithm based on // http://de.wikipedia.org/wiki/Rasterung_von_Kreisen#Methode_von_Horn // version of 2011.02.07. int d = -static_cast(radius); int x = radius; int y = 0; std::vector points; while(!(y > x)) { points.push_back({x_center + x, y_center + y}); points.push_back({x_center + x, y_center - y}); points.push_back({x_center - x, y_center + y}); points.push_back({x_center - x, y_center - y}); points.push_back({x_center + y, y_center + x}); points.push_back({x_center + y, y_center - x}); points.push_back({x_center - y, y_center + x}); points.push_back({x_center - y, y_center - x}); d += 2 * y + 1; ++y; if(d > 0) { d += -2 * x + 2; --x; } } SDL_RenderDrawPoints(renderer, points.data(), points.size()); } /***** ***** ***** ***** ***** LINE ***** ***** ***** ***** *****/ /** Definition of a line shape. */ class line_shape : public canvas::shape { public: /** * Constructor. * * @param cfg The config object to define the line see * http://www.wesnoth.org/wiki/GUICanvasWML#Line * for more information. */ explicit line_shape(const config& cfg); /** Implement shape::draw(). */ void draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables); private: typed_formula x1_, /**< The start x coordinate of the line. */ y1_, /**< The start y coordinate of the line. */ x2_, /**< The end x coordinate of the line. */ y2_, /**< The end y coordinate of the line. */ alpha_; /**< Alpha value override computed as a formula. */ /** The color of the line. */ color_t color_; /** * The thickness of the line. * * if the value is odd the x and y are the middle of the line. * if the value is even the x and y are the middle of a line * with width - 1. (0 is special case, does nothing.) */ unsigned thickness_; }; /*WIKI * @page = GUICanvasWML * * == Line == * @begin{tag}{name="line"}{min="0"}{max="-1"} * Definition of a line. When drawing a line it doesn't get blended on the * surface but replaces the pixels instead. A blitting flag might be added later * if needed. * * Keys: * @begin{table}{config} * x1 & f_unsigned & 0 & The x coordinate of the startpoint. $ * y1 & f_unsigned & 0 & The y coordinate of the startpoint. $ * x2 & f_unsigned & 0 & The x coordinate of the endpoint. $ * y2 & f_unsigned & 0 & The y coordinate of the endpoint. $ * color & color & "" & The color of the line. $ * thickness & unsigned & 0 & The thickness of the line if 0 nothing * is drawn. $ * debug & string & "" & Debug message to show upon creation * this message is not stored. $ * @end{table} * @end{tag}{name="line"} * * Variables:. * @begin{table}{formula} * width & unsigned & The width of the canvas. $ * height & unsigned & The height of the canvas. $ * text & tstring & The text to render on the widget. $ * text_maximum_width & unsigned & The maximum width available for the * text on the widget. $ * text_maximum_height & unsigned & The maximum height available for the * text on the widget. $ * text_wrap_mode & int & When the text doesn't fit in the * available width there are several ways * to fix that. This variable holds the * best method. (NOTE this is a 'hidden' * variable meant to copy state from a * widget to its canvas so there's no * reason to use this variable and thus * its values are not listed and might * change without further notice.) $ * text_alignment & h_align & The way the text is aligned inside the * canvas. $ *@end{table} * * The size variables are copied to the window and will be determined at * runtime. This is needed since the main window can be resized and the dialog * needs to resize accordingly. The following variables are available: * @begin{table}{formula} * screen_width & unsigned & The usable width of the Wesnoth main * window. $ * screen_height & unsigned & The usable height of the Wesnoth main * window. $ * gamemapx_offset & unsigned & The distance between left edge of the * screen and the game map. $ * gamemap_width & unsigned & The usable width of the Wesnoth gamemap, * if no gamemap shown it's the same value * as screen_width. $ * gamemap_height & unsigned & The usable height of the Wesnoth * gamemap, if no gamemap shown it's the * same value as screen_height. $ * * mouse_x & unsigned & The x coordinate of the mouse pointer. $ * mouse_y & unsigned & The y coordinate of the mouse pointer. $ * * window_width & unsigned & The window width. This value has two * meanings during the layout phase. This * only applies if automatic placement is * not enabled. * - When set to 0 it should return the * wanted maximum width. If no maximum * is wanted it should be set to the * '"(screen_width)"'. * - When not equal to 0 its value is the * best width for the window. When the * size should remain unchanged it * should be set to '"(window_width)"'. * $ * * window_height & unsigned & The window height. This value has two * meanings during the layout phase. This * only applies if automatic placement is * not enabled. * - When set to 0 it should return the * wanted maximum height. If no maximum * is wanted it should be set to the * '"(screen_height)"'. * - When not equal to 0 its value is the * best height for the window. When the * size should remain unchanged it * should be set to '"(window_height)"'. * $ * * size_request_mode & string & A field foo: * - maximum * - size * * @end{table} * * Note when drawing the valid coordinates are:
* 0 -> width - 1
* 0 -> height -1 * * Drawing outside this area will result in unpredictable results including * crashing. (That should be fixed, when encountered.) */ /*WIKI - unclassified * This code can be used by a parser to generate the wiki page * structure * [tag name] * param type_info description * * param Name of the parameter. * * type_info = ( type = default_value) The info about a optional parameter. * type_info = ( type ) The info about a mandatory parameter * type_info = [ type_info ] The info about a conditional parameter * description should explain the reason. * * description Description of the parameter. * * * * * Formulas are a function between brackets, that way the engine can see whether * there is standing a plain number or a formula eg: * 0 A value of zero * (0) A formula returning zero * * When formulas are available the text should state the available variables * which are available in that function. */ /*WIKI * @page = GUIVariable * * {{Autogenerated}} * * = Variables = * * In various parts of the GUI there are several variables types in use. This * page describes them. * * == Simple types == * * The simple types are types which have one value or a short list of options. * * @begin{table}{variable_types} * unsigned & Unsigned number (positive whole numbers * and zero). $ * f_unsigned & Unsigned number or formula returning an * unsigned number. $ * int & Signed number (whole numbers). $ * f_int & Signed number or formula returning an * signed number. $ * bool & A boolean value accepts the normal * values as the rest of the game. $ * f_bool & Boolean value or a formula returning a * boolean value. $ * string & A text. $ * tstring & A translatable string. $ * f_tstring & Formula returning a translatable string. * $ * function & A string containing a set of function * definition for the formula language. $ * * color & A string which contains the color, this * a group of 4 numbers between 0 and 255 * separated by a comma. The numbers are red * component, green component, blue * component and alpha. A color of 0 is not * available. An alpha of 255 is fully * transparent. Omitted values are set to * 0. $ * * font_style & A string which contains the style of the * font: * @* normal normal font * @* bold bold font * @* italic italic font * @* underline underlined font * @-Since SDL has problems combining these * styles only one can be picked. Once SDL * will allow multiple options, this type * will be transformed to a comma separated * list. If empty we default to the normal * style. Since the render engine is * replaced by Pango markup this field will * change later on. Note widgets that allow * marked up text can use markup to change * the font style. $ * * v_align & Vertical alignment; how an item is * aligned vertically in the available * space. Possible values: * @* top aligned at the top * @* bottom aligned at the bottom * @* center centered * @-When nothing is set or an another * value as in the list the item is * centered. $ * * h_align & Horizontal alignment; how an item is * aligned horizontal in the available * space. Possible values: * @* left aligned at the left side * @* right aligned at the right side * @* center centered $ * * f_h_align & A horizontal alignment or a formula * returning a horizontal alignment. $ * * border & Comma separated list of borders to use. * Possible values: * @* left border at the left side * @* right border at the right side * @* top border at the top * @* bottom border at the bottom * @* all alias for "left, right, top, * bottom" $ * * scrollbar_mode & How to show the scrollbar of a widget. * Possible values: * @* always The scrollbar is always * shown, regardless whether it's required * or not. * @* never The scrollbar is never * shown, even not when needed. (Note when * setting this mode dialogs might * not properly fit anymore). * @* auto Shows the scrollbar when * needed. The widget will reserve space for * the scrollbar, but only show when needed. * @* initial_auto Like auto, but when the * scrollbar is not needed the space is not * reserved. * @-Use auto when the list can be changed * dynamically eg the game list in the * lobby. For optimization you can also * use auto when you really expect a * scrollbar, but don't want it to be shown * when not needed eg the language list * will need a scrollbar on most screens. $ * * resize_mode & Determines how an image is resized. * Possible values: * @* scale The image is scaled. * @* stretch The first row or column * of pixels is copied over the entire * image. (Can only be used to scale resize * in one direction, else falls * back to scale.) * @* tile The image is placed * several times until the entire surface * is filled. The last images are * truncated. $ * * grow_direction & Determines how an image is resized. * Possible values: * @* scale The image is scaled. * @* stretch The first row or column * of pixels is copied over the entire * image. (Can only be used to scale resize * in one direction, else falls * back to scale.) * @* tile The image is placed * several times until the entire surface * is filled. The last images are * truncated. $ * @end{table} * @allow{type}{name="unsigned"}{value="^\d+$"} * @allow{type}{name="f_unsigned"}{value="^.+$"} * @allow{type}{name="int"}{value="^-?\d+$"} * @allow{type}{name="f_int"}{value="^.*$"} * @allow{type}{name="bool"}{value="^true|false|yes|no$"} * @allow{type}{name="f_bool"}{value="^.*$"} * @allow{type}{name="string"}{value="^.*$"} * @allow{type}{name="t_string"}{value="^_?.*$"} * @allow{type}{name="f_string"}{value="^.*$"} * @allow{type}{name="f_tstring"}{value="^_?.*$"} * @allow{type}{name="function"}{value="^_?.*$"} * * @allow{type}{name="color"}{value="^(?:2[0-5][0-5]|[01]?\d?\d)[.,]\s*(?:2[0-5][0-5]|[01]?\d?\d)[.,]\s*(?:2[0-5][0-5]|[01]?\d?\d)[.,]\s*(?:2[0-5][0-5]|[01]?\d?\d)$"} * * @allow{type}{name="font_style"}{value="^(normal|bold|italic|underline)?$"} * @allow{type}{name="v_align"}{value="^top|bottom|center$"} * @allow{type}{name="h_align"}{value="^left|right|center$"} * @allow{type}{name="f_h_align"}{value="^.*$"} * @allow{type}{name="border"}{value="^(top|bottom|left|right|all)?(,\s*(top|bottom|left|right|all))*$"} * @allow{type}{name="scrollbar_mode"}{value="^always|never|auto|initial_auto$"} * @allow{type}{name="resize_mode"}{value="^scale|stretch|tile$"} * @allow{type}{name="grow_direction"}{value="^horizontal|vertical$"} * * @remove{type}{name="section"} * @remove{type}{name="config"} * @remove{type}{name="grid"} * == Section types == * * For more complex parts, there are sections. Sections contain of several * lines of WML and can have sub sections. For example a grid has sub sections * which contain various widgets. Here's the list of sections. * * @begin{table}{variable_types} * section & A generic section. The documentation * about the section should describe the * section in further detail. $ * * grid & A grid contains several widgets. (TODO * add link to generic grid page.) $ * @end{table} */ line_shape::line_shape(const config& cfg) : x1_(cfg["x1"]) , y1_(cfg["y1"]) , x2_(cfg["x2"]) , y2_(cfg["y2"]) , alpha_(cfg["alpha"]) , color_(decode_color(cfg["color"])) , thickness_(cfg["thickness"]) { const std::string& debug = (cfg["debug"]); if(!debug.empty()) { DBG_GUI_P << "Line: found debug message '" << debug << "'.\n"; } } void line_shape::draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables) { /** * @todo formulas are now recalculated every draw cycle which is a bit silly * unless there has been a resize. So to optimize we should use an extra * flag or do the calculation in a separate routine. */ const unsigned x1 = x1_(variables); const unsigned y1 = y1_(variables); const unsigned x2 = x2_(variables); const unsigned y2 = y2_(variables); const unsigned alpha = alpha_(variables); // Override alpha from color with formula. if(alpha_.has_formula()) { color_.a = alpha; } DBG_GUI_D << "Line: draw from " << x1 << ',' << y1 << " to " << x2 << ',' << y2 << " canvas size " << canvas->w << ',' << canvas->h << ".\n"; VALIDATE(static_cast(x1) < canvas->w && static_cast(x2) < canvas->w && static_cast(y1) < canvas->h && static_cast(y2) < canvas->h, _("Line doesn't fit on canvas.")); // @todo FIXME respect the thickness. // lock the surface surface_lock locker(canvas); draw_line(canvas, renderer, color_, x1, y1, x2, y2); } /***** ***** ***** ***** ***** Rectangle ***** ***** ***** ***** *****/ /** Definition of a rectangle shape. */ class rectangle_shape : public canvas::shape { public: /** * Constructor. * * @param cfg The config object to define the rectangle see * http://www.wesnoth.org/wiki/GUICanvasWML#Rectangle * for more information. */ explicit rectangle_shape(const config& cfg); /** Implement shape::draw(). */ void draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables); private: typed_formula x_, /**< The x coordinate of the rectangle. */ y_, /**< The y coordinate of the rectangle. */ w_, /**< The width of the rectangle. */ h_; /**< The height of the rectangle. */ /** * Border thickness. * * If 0 the fill color is used for the entire widget. */ int border_thickness_; /** * The border color of the rectangle. * * If the color is fully transparent the border isn't drawn. */ color_t border_color_; /** * The border color of the rectangle. * * If the color is fully transparent the rectangle won't be filled. */ color_t fill_color_; }; /*WIKI * @page = GUICanvasWML * * == Rectangle == * @begin{tag}{name="rectangle"}{min="0"}{max="-1"} * * Definition of a rectangle. When drawing a rectangle it doesn't get blended on * the surface but replaces the pixels instead. A blitting flag might be added * later if needed. * * Keys: * @begin{table}{config} * x & f_unsigned & 0 & The x coordinate of the top left corner. * $ * y & f_unsigned & 0 & The y coordinate of the top left corner. * $ * w & f_unsigned & 0 & The width of the rectangle. $ * h & f_unsigned & 0 & The height of the rectangle. $ * border_thickness & unsigned & 0 & * The thickness of the border if the * thickness is zero it's not drawn. $ * border_color & color & "" & The color of the border if empty it's * not drawn. $ * fill_color & color & "" & The color of the interior if omitted * it's not drawn. $ * debug & string & "" & Debug message to show upon creation * this message is not stored. $ * @end{table} * @end{tag}{name="rectangle"} * Variables: * See [[#general_variables|Line]]. * */ rectangle_shape::rectangle_shape(const config& cfg) : x_(cfg["x"]) , y_(cfg["y"]) , w_(cfg["w"]) , h_(cfg["h"]) , border_thickness_(cfg["border_thickness"]) , border_color_(decode_color(cfg["border_color"])) , fill_color_(decode_color(cfg["fill_color"])) { if(border_color_.null()) { border_thickness_ = 0; } const std::string& debug = (cfg["debug"]); if(!debug.empty()) { DBG_GUI_P << "Rectangle: found debug message '" << debug << "'.\n"; } } void rectangle_shape::draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables) { /** * @todo formulas are now recalculated every draw cycle which is a bit * silly unless there has been a resize. So to optimize we should use an * extra flag or do the calculation in a separate routine. */ const int x = x_(variables); const int y = y_(variables); const int w = w_(variables); const int h = h_(variables); DBG_GUI_D << "Rectangle: draw from " << x << ',' << y << " width " << w << " height " << h << " canvas size " << canvas->w << ',' << canvas->h << ".\n"; VALIDATE(x < canvas->w && x + w <= canvas->w && y < canvas->h && y + h <= canvas->h, _("Rectangle doesn't fit on canvas.")); surface_lock locker(canvas); // Draw the border for(int i = 0; i < border_thickness_; ++i) { SDL_Rect dimensions { x + i, y + i, w - (i * 2), h - (i * 2) }; set_renderer_color(renderer, border_color_); SDL_RenderDrawRect(renderer, &dimensions); } // Fill the background, if applicable if(!fill_color_.null() && w && h) { set_renderer_color(renderer, fill_color_); SDL_Rect area { x + border_thickness_, y + border_thickness_, w - (border_thickness_ * 2), h - (border_thickness_ * 2) }; SDL_RenderFillRect(renderer, &area); } } /***** ***** ***** ***** ***** CIRCLE ***** ***** ***** ***** *****/ /** Definition of a circle shape. */ class circle_shape : public canvas::shape { public: /** * Constructor. * * @param cfg The config object to define the circle see * http://www.wesnoth.org/wiki/GUICanvasWML#Circle * for more information. */ explicit circle_shape(const config& cfg); /** Implement shape::draw(). */ void draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables); private: typed_formula x_, /**< The center x coordinate of the circle. */ y_, /**< The center y coordinate of the circle. */ radius_; /**< The radius of the circle. */ /** The color of the circle. */ color_t color_; }; /*WIKI * @page = GUICanvasWML * * == Circle == * @begin{tag}{name="circle"}{min="0"}{max="-1"} * * Definition of a circle. When drawing a circle it doesn't get blended on * the surface but replaces the pixels instead. A blitting flag might be * added later if needed. * * Keys: * @begin{table}{config} * x & f_unsigned & 0 & The x coordinate of the center. $ * y & f_unsigned & 0 & The y coordinate of the center. $ * radius & f_unsigned & 0 & The radius of the circle if 0 nothing is * drawn. $ * color & color & "" & The color of the circle. $ * debug & string & "" & Debug message to show upon creation this * message is not stored. $ * @end{table} * @end{tag}{name="circle"} * Variables: * See [[#general_variables|Line]]. * * Drawing outside the area will result in unpredictable results including * crashing. (That should be fixed, when encountered.) */ circle_shape::circle_shape(const config& cfg) : x_(cfg["x"]) , y_(cfg["y"]) , radius_(cfg["radius"]) , color_(decode_color(cfg["color"])) { const std::string& debug = (cfg["debug"]); if(!debug.empty()) { DBG_GUI_P << "Circle: found debug message '" << debug << "'.\n"; } } void circle_shape::draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables) { /** * @todo formulas are now recalculated every draw cycle which is a bit * silly unless there has been a resize. So to optimize we should use an * extra flag or do the calculation in a separate routine. */ const unsigned x = x_(variables); const unsigned y = y_(variables); const unsigned radius = radius_(variables); DBG_GUI_D << "Circle: drawn at " << x << ',' << y << " radius " << radius << " canvas size " << canvas->w << ',' << canvas->h << ".\n"; VALIDATE_WITH_DEV_MESSAGE( static_cast(x - radius) >= 0, _("Circle doesn't fit on canvas."), formatter() << "x = " << x << ", radius = " << radius); VALIDATE_WITH_DEV_MESSAGE( static_cast(y - radius) >= 0, _("Circle doesn't fit on canvas."), formatter() << "y = " << y << ", radius = " << radius); VALIDATE_WITH_DEV_MESSAGE( static_cast(x + radius) < canvas->w, _("Circle doesn't fit on canvas."), formatter() << "x = " << x << ", radius = " << radius << "', canvas width = " << canvas->w << "."); VALIDATE_WITH_DEV_MESSAGE( static_cast(y + radius) < canvas->h, _("Circle doesn't fit on canvas."), formatter() << "y = " << y << ", radius = " << radius << "', canvas height = " << canvas->h << "."); // lock the surface surface_lock locker(canvas); draw_circle(canvas, renderer, color_, x, y, radius); } /***** ***** ***** ***** ***** IMAGE ***** ***** ***** ***** *****/ /** Definition of an image shape. */ class image_shape : public canvas::shape { public: /** * Constructor. * * @param cfg The config object to define the image see * http://www.wesnoth.org/wiki/GUICanvasWML#Image * for more information. */ explicit image_shape(const config& cfg); /** Implement shape::draw(). */ void draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables); private: typed_formula x_, /**< The x coordinate of the image. */ y_, /**< The y coordinate of the image. */ w_, /**< The width of the image. */ h_; /**< The height of the image. */ /** Contains the size of the image. */ SDL_Rect src_clip_; /** The image is cached in this surface. */ surface image_; /** * Name of the image. * * This value is only used when the image name is a formula. If it isn't a * formula the image will be loaded in the constructor. If it's a formula it * will be loaded every draw cycles. This allows 'changing' images. */ typed_formula image_name_; /** * Determines the way an image will be resized. * * If the image is smaller is needed it needs to resized, how is determined * by the value of this enum. */ enum resize_mode { scale, stretch, tile }; /** Converts a string to a resize mode. */ resize_mode get_resize_mode(const std::string& resize_mode); /** The resize mode for an image. */ resize_mode resize_mode_; /** Mirror the image over the vertical axis. */ typed_formula vertical_mirror_; }; /*WIKI * @page = GUICanvasWML * * == Image == * @begin{tag}{name="image"}{min="0"}{max="-1"} * Definition of an image. * * Keys: * @begin{table}{config} * x & f_unsigned & 0 & The x coordinate of the top left corner. * $ * y & f_unsigned & 0 & The y coordinate of the top left corner. * $ * w & f_unsigned & 0 & The width of the image, if not zero the * image will be scaled to the desired * width. $ * h & f_unsigned & 0 & The height of the image, if not zero the * image will be scaled to the desired * height. $ * resize_mode & resize_mode & scale & * Determines how an image is scaled to fit * the wanted size. $ * vertical_mirror & f_bool & false & * Mirror the image over the vertical axis. * $ * name & f_string & "" & The name of the image. $ * debug & string & "" & Debug message to show upon creation * this message is not stored. $ * * @end{table} * @end{tag}{name="image"} * Variables: * @begin{table}{formula} * image_width & unsigned & The width of the image, either the * requested width or the natural width of * the image. This value can be used to set * the x (or y) value of the image. (This * means x and y are evaluated after the * width and height.) $ * image_height & unsigned & The height of the image, either the * requested height or the natural height * of the image. This value can be used to * set the y (or x) value of the image. * (This means x and y are evaluated after * the width and height.) $ * image_original_width & unsigned & * The width of the image as stored on * disk, can be used to set x or w * (also y and h can be set). $ * image_original_height & unsigned & * The height of the image as stored on * disk, can be used to set y or h * (also x and y can be set). $ * @end{table} * Also the general variables are available, see [[#general_variables|Line]]. */ image_shape::image_shape(const config& cfg) : x_(cfg["x"]) , y_(cfg["y"]) , w_(cfg["w"]) , h_(cfg["h"]) , src_clip_() , image_() , image_name_(cfg["name"]) , resize_mode_(get_resize_mode(cfg["resize_mode"])) , vertical_mirror_(cfg["vertical_mirror"]) { const std::string& debug = (cfg["debug"]); if(!debug.empty()) { DBG_GUI_P << "Image: found debug message '" << debug << "'.\n"; } } void image_shape::draw(surface& canvas, SDL_Renderer* /*renderer*/, const game_logic::map_formula_callable& variables) { DBG_GUI_D << "Image: draw.\n"; /** * @todo formulas are now recalculated every draw cycle which is a bit * silly unless there has been a resize. So to optimize we should use an * extra flag or do the calculation in a separate routine. */ const std::string& name = image_name_(variables); if(name.empty()) { DBG_GUI_D << "Image: formula returned no value, will not be drawn.\n"; return; } /* * The locator might return a different surface for every call so we can't * cache the output, also not if no formula is used. */ surface tmp(image::get_image(image::locator(name))); if(!tmp) { ERR_GUI_D << "Image: '" << name << "' not found and won't be drawn." << std::endl; return; } image_.assign(make_neutral_surface(tmp)); assert(image_); src_clip_ = sdl::create_rect(0, 0, image_->w, image_->h); game_logic::map_formula_callable local_variables(variables); local_variables.add("image_original_width", variant(image_->w)); local_variables.add("image_original_height", variant(image_->h)); unsigned w = w_(local_variables); VALIDATE_WITH_DEV_MESSAGE(static_cast(w) >= 0, _("Image doesn't fit on canvas."), formatter() << "Image '" << name << "', w = " << static_cast(w) << "."); unsigned h = h_(local_variables); VALIDATE_WITH_DEV_MESSAGE(static_cast(h) >= 0, _("Image doesn't fit on canvas."), formatter() << "Image '" << name << "', h = " << static_cast(h) << "."); local_variables.add("image_width", variant(w ? w : image_->w)); local_variables.add("image_height", variant(h ? h : image_->h)); const unsigned clip_x = x_(local_variables); VALIDATE_WITH_DEV_MESSAGE(static_cast(clip_x) >= 0, _("Image doesn't fit on canvas."), formatter() << "Image '" << name << "', x = " << static_cast(clip_x) << "."); const unsigned clip_y = y_(local_variables); VALIDATE_WITH_DEV_MESSAGE(static_cast(clip_y) >= 0, _("Image doesn't fit on canvas."), formatter() << "Image '" << name << "', y = " << static_cast(clip_y) << "."); // Copy the data to local variables to avoid overwriting the originals. SDL_Rect src_clip = src_clip_; SDL_Rect dst_clip = sdl::create_rect(clip_x, clip_y, 0, 0); surface surf; // Test whether we need to scale and do the scaling if needed. if(w || h) { bool done = false; bool stretch_image = (resize_mode_ == stretch) && (!!w ^ !!h); if(!w) { if(stretch_image) { DBG_GUI_D << "Image: vertical stretch from " << image_->w << ',' << image_->h << " to a height of " << h << ".\n"; surf = stretch_surface_vertical(image_, h, false); done = true; } w = image_->w; } if(!h) { if(stretch_image) { DBG_GUI_D << "Image: horizontal stretch from " << image_->w << ',' << image_->h << " to a width of " << w << ".\n"; surf = stretch_surface_horizontal(image_, w, false); done = true; } h = image_->h; } if(!done) { if(resize_mode_ == tile) { DBG_GUI_D << "Image: tiling from " << image_->w << ',' << image_->h << " to " << w << ',' << h << ".\n"; const int columns = (w + image_->w - 1) / image_->w; const int rows = (h + image_->h - 1) / image_->h; surf = create_neutral_surface(w, h); for(int x = 0; x < columns; ++x) { for(int y = 0; y < rows; ++y) { const SDL_Rect dest = sdl::create_rect( x * image_->w, y * image_->h, 0, 0); blit_surface(image_, nullptr, surf, &dest); } } } else { if(resize_mode_ == stretch) { ERR_GUI_D << "Image: failed to stretch image, " "fall back to scaling.\n"; } DBG_GUI_D << "Image: scaling from " << image_->w << ',' << image_->h << " to " << w << ',' << h << ".\n"; surf = scale_surface(image_, w, h, false); } } src_clip.w = w; src_clip.h = h; } else { surf = image_; } if(vertical_mirror_(local_variables)) { surf = flip_surface(surf, false); } blit_surface(surf, &src_clip, canvas, &dst_clip); } image_shape::resize_mode image_shape::get_resize_mode(const std::string& resize_mode) { if(resize_mode == "tile") { return image_shape::tile; } else if(resize_mode == "stretch") { return image_shape::stretch; } else { if(!resize_mode.empty() && resize_mode != "scale") { ERR_GUI_E << "Invalid resize mode '" << resize_mode << "' falling back to 'scale'.\n"; } return image_shape::scale; } } /***** ***** ***** ***** ***** TEXT ***** ***** ***** ***** *****/ /** Definition of a text shape. */ class text_shape : public canvas::shape { public: /** * Constructor. * * @param cfg The config object to define the text see * http://www.wesnoth.org/wiki/GUICanvasWML#Text * for more information. */ explicit text_shape(const config& cfg); /** Implement shape::draw(). */ void draw(surface& canvas, SDL_Renderer* renderer, const game_logic::map_formula_callable& variables); private: typed_formula x_, /**< The x coordinate of the text. */ y_, /**< The y coordinate of the text. */ w_, /**< The width of the text. */ h_; /**< The height of the text. */ /** The text font family. */ font::family_class font_family_; /** The font size of the text. */ unsigned font_size_; /** The style of the text. */ font::pango_text::FONT_STYLE font_style_; /** The alignment of the text. */ typed_formula text_alignment_; /** The color of the text. */ color_t color_; /** The text to draw. */ typed_formula text_; /** The text markup switch of the text. */ typed_formula text_markup_; /** The link aware switch of the text. */ typed_formula link_aware_; /** The link color of the text. */ typed_formula link_color_; /** The maximum width for the text. */ typed_formula maximum_width_; /** The number of characters per line. */ unsigned characters_per_line_; /** The maximum height for the text. */ typed_formula maximum_height_; }; /*WIKI * @page = GUICanvasWML * * == Text == * @begin{tag}{name="text"}{min="0"}{max="-1"} * Definition of text. * * Keys: * @begin{table}{config} * x & f_unsigned & 0 & The x coordinate of the top left corner. * $ * y & f_unsigned & 0 & The y coordinate of the top left corner. * $ * w & f_unsigned & 0 & The width of the text's bounding * rectangle. $ * h & f_unsigned & 0 & The height of the text's bounding * rectangle. $ * font_family & font_family & "sans" & * The font family used for the text. $ * font_size & unsigned & & The size of the text font. $ * font_style & font_style & "" & The style of the text. $ * text_alignment & f_h_align & "left" & * The alignment of the text. $ * color & color & "" & The color of the text. $ * text & f_tstring & "" & The text to draw (translatable). $ * text_markup & f_bool & false & Can the text have mark-up? $ * text_link_aware & f_bool & false & * Is the text link aware? $ * text_link_color & f_string & "#ffff00" & * The color of links in the text $ * maximum_width & f_int & -1 & The maximum width the text is allowed to * be. $ * maximum_height & f_int & -1 & The maximum height the text is allowed * to be. $ * debug & string & "" & Debug message to show upon creation * this message is not stored. $ * @end{table} * @end{tag}{name="text"} * NOTE alignment could only be done with the formulas, but now with the * text_alignment flag as well, older widgets might still use the formulas and * not all widgets may expose the text alignment yet and when exposed not use * it yet. * * Variables: * @begin{table}{formula} * text_width & unsigned & The width of the rendered text. $ * text_height & unsigned & The height of the rendered text. $ * @end{table} * Also the general variables are available, see [[#general_variables|Line]]. * @end{parent}{name="generic/state/draw/"} */ text_shape::text_shape(const config& cfg) : x_(cfg["x"]) , y_(cfg["y"]) , w_(cfg["w"]) , h_(cfg["h"]) , font_family_(font::str_to_family_class(cfg["font_family"])) , font_size_(cfg["font_size"]) , font_style_(decode_font_style(cfg["font_style"])) , text_alignment_(cfg["text_alignment"]) , color_(decode_color(cfg["color"])) , text_(cfg["text"]) , text_markup_(cfg["text_markup"], false) , link_aware_(cfg["text_link_aware"], false) , link_color_(cfg["text_link_color"], color_t::from_hex_string("ffff00")) , maximum_width_(cfg["maximum_width"], -1) , characters_per_line_(cfg["text_characters_per_line"]) , maximum_height_(cfg["maximum_height"], -1) { VALIDATE(font_size_, _("Text has a font size of 0.")); const std::string& debug = (cfg["debug"]); if(!debug.empty()) { DBG_GUI_P << "Text: found debug message '" << debug << "'.\n"; } } void text_shape::draw(surface& canvas, SDL_Renderer* /*renderer*/, const game_logic::map_formula_callable& variables) { assert(variables.has_key("text")); // We first need to determine the size of the text which need the rendered // text. So resolve and render the text first and then start to resolve // the other formulas. const t_string text = text_(variables); if(text.empty()) { DBG_GUI_D << "Text: no text to render, leave.\n"; return; } static font::pango_text text_renderer; text_renderer.set_link_aware(link_aware_(variables)) .set_link_color(link_color_(variables)); text_renderer.set_text(text, text_markup_(variables)); text_renderer .set_family_class(font_family_) .set_font_size(font_size_) .set_font_style(font_style_) .set_alignment(text_alignment_(variables)) .set_foreground_color(color_) .set_maximum_width(maximum_width_(variables)) .set_maximum_height(maximum_height_(variables), true) .set_ellipse_mode( variables.has_key("text_wrap_mode") ? static_cast( variables.query_value("text_wrap_mode") .as_int()) : PANGO_ELLIPSIZE_END) .set_characters_per_line(characters_per_line_); surface& surf = text_renderer.render(); if(surf->w == 0) { DBG_GUI_D << "Text: Rendering '" << text << "' resulted in an empty canvas, leave.\n"; return; } game_logic::map_formula_callable local_variables(variables); local_variables.add("text_width", variant(surf->w)); local_variables.add("text_height", variant(surf->h)); /* std::cerr << "Text: drawing text '" << text << " maximum width " << maximum_width_(variables) << " maximum height " << maximum_height_(variables) << " text width " << surf->w << " text height " << surf->h; */ ///@todo formulas are now recalculated every draw cycle which is a // bit silly unless there has been a resize. So to optimize we should // use an extra flag or do the calculation in a separate routine. const unsigned x = x_(local_variables); const unsigned y = y_(local_variables); const unsigned w = w_(local_variables); const unsigned h = h_(local_variables); DBG_GUI_D << "Text: drawing text '" << text << "' drawn from " << x << ',' << y << " width " << w << " height " << h << " canvas size " << canvas->w << ',' << canvas->h << ".\n"; VALIDATE(static_cast(x) < canvas->w && static_cast(y) < canvas->h, _("Text doesn't start on canvas.")); // A text might be to long and will be clipped. if(surf->w > static_cast(w)) { WRN_GUI_D << "Text: text is too wide for the " "canvas and will be clipped.\n"; } if(surf->h > static_cast(h)) { WRN_GUI_D << "Text: text is too high for the " "canvas and will be clipped.\n"; } SDL_Rect dst = sdl::create_rect(x, y, canvas->w, canvas->h); blit_surface(surf, 0, canvas, &dst); } } // namespace /***** ***** ***** ***** ***** CANVAS ***** ***** ***** ***** *****/ canvas::canvas() : shapes_() , blur_depth_(0) , w_(0) , h_(0) , canvas_() , renderer_(nullptr) , variables_() , is_dirty_(true) { } canvas::~canvas() { SDL_DestroyRenderer(renderer_); } void canvas::draw(const bool force) { log_scope2(log_gui_draw, "Canvas: drawing."); if(!is_dirty_ && !force) { DBG_GUI_D << "Canvas: nothing to draw.\n"; return; } if(is_dirty_) { get_screen_size_variables(variables_); variables_.add("width", variant(w_)); variables_.add("height", variant(h_)); } // create surface DBG_GUI_D << "Canvas: create new empty canvas.\n"; canvas_.assign(create_neutral_surface(w_, h_)); SDL_DestroyRenderer(renderer_); renderer_ = SDL_CreateSoftwareRenderer(canvas_); SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); // draw items for(auto& shape : shapes_) { lg::scope_logger inner_scope_logging_object__(log_gui_draw, "Canvas: draw shape."); shape->draw(canvas_, renderer_, variables_); } SDL_RenderPresent(renderer_); is_dirty_ = false; } void canvas::blit(surface& surf, SDL_Rect rect) { draw(); if(blur_depth_) { /* * If the surf is the video surface the blurring seems to stack, this * can be seen in the title screen. So also use the not 32 bpp method * for this situation. */ if(surf != CVideo::get_singleton().getSurface() && is_neutral(surf)) { blur_surface(surf, rect, blur_depth_); } else { // Can't directly blur the surface if not 32 bpp. SDL_Rect r = rect; surface s = get_surface_portion(surf, r); s = blur_surface(s, blur_depth_, false); sdl_blit(s, nullptr, surf, &r); } } sdl_blit(canvas_, nullptr, surf, &rect); } void canvas::parse_cfg(const config& cfg) { log_scope2(log_gui_parse, "Canvas: parsing config."); shapes_.clear(); for(const auto & shape : cfg.all_children_range()) { const std::string& type = shape.key; const config& data = shape.cfg; DBG_GUI_P << "Canvas: found shape of the type " << type << ".\n"; if(type == "line") { shapes_.push_back(std::make_shared(data)); } else if(type == "rectangle") { shapes_.push_back(std::make_shared(data)); } else if(type == "circle") { shapes_.push_back(std::make_shared(data)); } else if(type == "image") { shapes_.push_back(std::make_shared(data)); } else if(type == "text") { shapes_.push_back(std::make_shared(data)); } else if(type == "pre_commit") { /* note this should get split if more preprocessing is used. */ for(const auto & function : data.all_children_range()) { if(function.key == "blur") { blur_depth_ = function.cfg["depth"]; } else { ERR_GUI_P << "Canvas: found a pre commit function" << " of an invalid type " << type << ".\n"; } } } else { ERR_GUI_P << "Canvas: found a shape of an invalid type " << type << ".\n"; assert(false); } } } /***** ***** ***** ***** ***** SHAPE ***** ***** ***** ***** *****/ } // namespace gui2 /*WIKI * @page = GUICanvasWML * @order = ZZZZZZ_footer * * [[Category: WML Reference]] * [[Category: GUI WML Reference]] * */ /*WIKI * @page = GUIVariable * @order = ZZZZZZ_footer * * [[Category: WML Reference]] * [[Category: GUI WML Reference]] */