Update: Translations from eints
[openttd-github.git] / src / gfx.cpp
blobf6c3ceb51a80516f4574b5cdb7b54cd1e7d66456
1 /*
2 * This file is part of OpenTTD.
3 * OpenTTD 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, version 2.
4 * OpenTTD 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.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>.
6 */
8 /** @file gfx.cpp Handling of drawing text and other gfx related stuff. */
10 #include "stdafx.h"
11 #include "gfx_layout.h"
12 #include "progress.h"
13 #include "zoom_func.h"
14 #include "blitter/factory.hpp"
15 #include "video/video_driver.hpp"
16 #include "strings_func.h"
17 #include "settings_type.h"
18 #include "network/network.h"
19 #include "network/network_func.h"
20 #include "window_gui.h"
21 #include "window_func.h"
22 #include "newgrf_debug.h"
23 #include "core/backup_type.hpp"
24 #include "core/container_func.hpp"
25 #include "core/geometry_func.hpp"
26 #include "viewport_func.h"
28 #include "table/string_colours.h"
29 #include "table/sprites.h"
30 #include "table/control_codes.h"
32 #include "safeguards.h"
34 uint8_t _dirkeys; ///< 1 = left, 2 = up, 4 = right, 8 = down
35 bool _fullscreen;
36 uint8_t _support8bpp;
37 CursorVars _cursor;
38 bool _ctrl_pressed; ///< Is Ctrl pressed?
39 bool _shift_pressed; ///< Is Shift pressed?
40 uint16_t _game_speed = 100; ///< Current game-speed; 100 is 1x, 0 is infinite.
41 bool _left_button_down; ///< Is left mouse button pressed?
42 bool _left_button_clicked; ///< Is left mouse button clicked?
43 bool _right_button_down; ///< Is right mouse button pressed?
44 bool _right_button_clicked; ///< Is right mouse button clicked?
45 DrawPixelInfo _screen;
46 bool _screen_disable_anim = false; ///< Disable palette animation (important for 32bpp-anim blitter during giant screenshot)
47 std::atomic<bool> _exit_game;
48 GameMode _game_mode;
49 SwitchMode _switch_mode; ///< The next mainloop command.
50 PauseMode _pause_mode;
51 GameSessionStats _game_session_stats; ///< Statistics about the current session.
53 static uint8_t _stringwidth_table[FS_END][224]; ///< Cache containing width of often used characters. @see GetCharacterWidth()
54 DrawPixelInfo *_cur_dpi;
56 static void GfxMainBlitterViewport(const Sprite *sprite, int x, int y, BlitterMode mode, const SubSprite *sub = nullptr, SpriteID sprite_id = SPR_CURSOR_MOUSE);
57 static void GfxMainBlitter(const Sprite *sprite, int x, int y, BlitterMode mode, const SubSprite *sub = nullptr, SpriteID sprite_id = SPR_CURSOR_MOUSE, ZoomLevel zoom = ZOOM_LVL_MIN);
59 static ReusableBuffer<uint8_t> _cursor_backup;
61 ZoomLevel _gui_zoom = ZOOM_LVL_NORMAL; ///< GUI Zoom level
62 ZoomLevel _font_zoom = _gui_zoom; ///< Sprite font Zoom level (not clamped)
63 int _gui_scale = MIN_INTERFACE_SCALE; ///< GUI scale, 100 is 100%.
64 int _gui_scale_cfg; ///< GUI scale in config.
66 /**
67 * The rect for repaint.
69 * This rectangle defines the area which should be repaint by the video driver.
71 * @ingroup dirty
73 static Rect _invalid_rect;
74 static const uint8_t *_colour_remap_ptr;
75 static uint8_t _string_colourremap[3]; ///< Recoloursprite for stringdrawing. The grf loader ensures that #SpriteType::Font sprites only use colours 0 to 2.
77 static const uint DIRTY_BLOCK_HEIGHT = 8;
78 static const uint DIRTY_BLOCK_WIDTH = 64;
80 static uint _dirty_bytes_per_line = 0;
81 static uint8_t *_dirty_blocks = nullptr;
82 extern uint _dirty_block_colour;
84 void GfxScroll(int left, int top, int width, int height, int xo, int yo)
86 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
88 if (xo == 0 && yo == 0) return;
90 if (_cursor.visible) UndrawMouseCursor();
92 if (_networking) NetworkUndrawChatMessage();
94 blitter->ScrollBuffer(_screen.dst_ptr, left, top, width, height, xo, yo);
95 /* This part of the screen is now dirty. */
96 VideoDriver::GetInstance()->MakeDirty(left, top, width, height);
101 * Applies a certain FillRectMode-operation to a rectangle [left, right] x [top, bottom] on the screen.
103 * @pre dpi->zoom == ZOOM_LVL_MIN, right >= left, bottom >= top
104 * @param left Minimum X (inclusive)
105 * @param top Minimum Y (inclusive)
106 * @param right Maximum X (inclusive)
107 * @param bottom Maximum Y (inclusive)
108 * @param colour A 8 bit palette index (FILLRECT_OPAQUE and FILLRECT_CHECKER) or a recolour spritenumber (FILLRECT_RECOLOUR)
109 * @param mode
110 * FILLRECT_OPAQUE: Fill the rectangle with the specified colour
111 * FILLRECT_CHECKER: Like FILLRECT_OPAQUE, but only draw every second pixel (used to grey out things)
112 * FILLRECT_RECOLOUR: Apply a recolour sprite to every pixel in the rectangle currently on screen
114 void GfxFillRect(int left, int top, int right, int bottom, int colour, FillRectMode mode)
116 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
117 const DrawPixelInfo *dpi = _cur_dpi;
118 void *dst;
119 const int otop = top;
120 const int oleft = left;
122 if (dpi->zoom != ZOOM_LVL_MIN) return;
123 if (left > right || top > bottom) return;
124 if (right < dpi->left || left >= dpi->left + dpi->width) return;
125 if (bottom < dpi->top || top >= dpi->top + dpi->height) return;
127 if ( (left -= dpi->left) < 0) left = 0;
128 right = right - dpi->left + 1;
129 if (right > dpi->width) right = dpi->width;
130 right -= left;
131 assert(right > 0);
133 if ( (top -= dpi->top) < 0) top = 0;
134 bottom = bottom - dpi->top + 1;
135 if (bottom > dpi->height) bottom = dpi->height;
136 bottom -= top;
137 assert(bottom > 0);
139 dst = blitter->MoveTo(dpi->dst_ptr, left, top);
141 switch (mode) {
142 default: // FILLRECT_OPAQUE
143 blitter->DrawRect(dst, right, bottom, (uint8_t)colour);
144 break;
146 case FILLRECT_RECOLOUR:
147 blitter->DrawColourMappingRect(dst, right, bottom, GB(colour, 0, PALETTE_WIDTH));
148 break;
150 case FILLRECT_CHECKER: {
151 uint8_t bo = (oleft - left + dpi->left + otop - top + dpi->top) & 1;
152 do {
153 for (int i = (bo ^= 1); i < right; i += 2) blitter->SetPixel(dst, i, 0, (uint8_t)colour);
154 dst = blitter->MoveTo(dst, 0, 1);
155 } while (--bottom > 0);
156 break;
161 typedef std::pair<Point, Point> LineSegment;
164 * Make line segments from a polygon defined by points, translated by an offset.
165 * Entirely horizontal lines (start and end at same Y coordinate) are skipped, as they are irrelevant to scanline conversion algorithms.
166 * Generated line segments always have the lowest Y coordinate point first, i.e. original direction is lost.
167 * @param shape The polygon to convert.
168 * @param offset Offset vector subtracted from all coordinates in the shape.
169 * @return Vector of undirected line segments.
171 static std::vector<LineSegment> MakePolygonSegments(const std::vector<Point> &shape, Point offset)
173 std::vector<LineSegment> segments;
174 if (shape.size() < 3) return segments; // fewer than 3 will always result in an empty polygon
175 segments.reserve(shape.size());
177 /* Connect first and last point by having initial previous point be the last */
178 Point prev = shape.back();
179 prev.x -= offset.x;
180 prev.y -= offset.y;
181 for (Point pt : shape) {
182 pt.x -= offset.x;
183 pt.y -= offset.y;
184 /* Create segments for all non-horizontal lines in the polygon.
185 * The segments always have lowest Y coordinate first. */
186 if (prev.y > pt.y) {
187 segments.emplace_back(pt, prev);
188 } else if (prev.y < pt.y) {
189 segments.emplace_back(prev, pt);
191 prev = pt;
194 return segments;
198 * Fill a polygon with colour.
199 * The odd-even winding rule is used, i.e. self-intersecting polygons will have holes in them.
200 * Left and top edges are inclusive, right and bottom edges are exclusive.
201 * @note For rectangles the GfxFillRect function will be faster.
202 * @pre dpi->zoom == ZOOM_LVL_MIN
203 * @param shape List of points on the polygon.
204 * @param colour An 8 bit palette index (FILLRECT_OPAQUE and FILLRECT_CHECKER) or a recolour spritenumber (FILLRECT_RECOLOUR).
205 * @param mode
206 * FILLRECT_OPAQUE: Fill the polygon with the specified colour.
207 * FILLRECT_CHECKER: Fill every other pixel with the specified colour, in a checkerboard pattern.
208 * FILLRECT_RECOLOUR: Apply a recolour sprite to every pixel in the polygon.
210 void GfxFillPolygon(const std::vector<Point> &shape, int colour, FillRectMode mode)
212 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
213 const DrawPixelInfo *dpi = _cur_dpi;
214 if (dpi->zoom != ZOOM_LVL_MIN) return;
216 std::vector<LineSegment> segments = MakePolygonSegments(shape, Point{ dpi->left, dpi->top });
218 /* Remove segments appearing entirely above or below the clipping area. */
219 segments.erase(std::remove_if(segments.begin(), segments.end(), [dpi](const LineSegment &s) { return s.second.y <= 0 || s.first.y >= dpi->height; }), segments.end());
221 /* Check that this wasn't an empty shape (all points on a horizontal line or outside clipping.) */
222 if (segments.empty()) return;
224 /* Sort the segments by first point Y coordinate. */
225 std::sort(segments.begin(), segments.end(), [](const LineSegment &a, const LineSegment &b) { return a.first.y < b.first.y; });
227 /* Segments intersecting current scanline. */
228 std::vector<LineSegment> active;
229 /* Intersection points with a scanline.
230 * Kept outside loop to avoid repeated re-allocations. */
231 std::vector<int> intersections;
232 /* Normal, reasonable polygons don't have many intersections per scanline. */
233 active.reserve(4);
234 intersections.reserve(4);
236 /* Scan through the segments and paint each scanline. */
237 int y = segments.front().first.y;
238 std::vector<LineSegment>::iterator nextseg = segments.begin();
239 while (!active.empty() || nextseg != segments.end()) {
240 /* Clean up segments that have ended. */
241 active.erase(std::remove_if(active.begin(), active.end(), [y](const LineSegment &s) { return s.second.y == y; }), active.end());
243 /* Activate all segments starting on this scanline. */
244 while (nextseg != segments.end() && nextseg->first.y == y) {
245 active.push_back(*nextseg);
246 ++nextseg;
249 /* Check clipping. */
250 if (y < 0) {
251 ++y;
252 continue;
254 if (y >= dpi->height) return;
256 /* Intersect scanline with all active segments. */
257 intersections.clear();
258 for (const LineSegment &s : active) {
259 const int sdx = s.second.x - s.first.x;
260 const int sdy = s.second.y - s.first.y;
261 const int ldy = y - s.first.y;
262 const int x = s.first.x + sdx * ldy / sdy;
263 intersections.push_back(x);
266 /* Fill between pairs of intersections. */
267 std::sort(intersections.begin(), intersections.end());
268 for (size_t i = 1; i < intersections.size(); i += 2) {
269 /* Check clipping. */
270 const int x1 = std::max(0, intersections[i - 1]);
271 const int x2 = std::min(intersections[i], dpi->width);
272 if (x2 < 0) continue;
273 if (x1 >= dpi->width) continue;
275 /* Fill line y from x1 to x2. */
276 void *dst = blitter->MoveTo(dpi->dst_ptr, x1, y);
277 switch (mode) {
278 default: // FILLRECT_OPAQUE
279 blitter->DrawRect(dst, x2 - x1, 1, (uint8_t)colour);
280 break;
281 case FILLRECT_RECOLOUR:
282 blitter->DrawColourMappingRect(dst, x2 - x1, 1, GB(colour, 0, PALETTE_WIDTH));
283 break;
284 case FILLRECT_CHECKER:
285 /* Fill every other pixel, offset such that the sum of filled pixels' X and Y coordinates is odd.
286 * This creates a checkerboard effect. */
287 for (int x = (x1 + y) & 1; x < x2 - x1; x += 2) {
288 blitter->SetPixel(dst, x, 0, (uint8_t)colour);
290 break;
294 /* Next line */
295 ++y;
300 * Check line clipping by using a linear equation and draw the visible part of
301 * the line given by x/y and x2/y2.
302 * @param video Destination pointer to draw into.
303 * @param x X coordinate of first point.
304 * @param y Y coordinate of first point.
305 * @param x2 X coordinate of second point.
306 * @param y2 Y coordinate of second point.
307 * @param screen_width With of the screen to check clipping against.
308 * @param screen_height Height of the screen to check clipping against.
309 * @param colour Colour of the line.
310 * @param width Width of the line.
311 * @param dash Length of dashes for dashed lines. 0 means solid line.
313 static inline void GfxDoDrawLine(void *video, int x, int y, int x2, int y2, int screen_width, int screen_height, uint8_t colour, int width, int dash = 0)
315 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
317 assert(width > 0);
319 if (y2 == y || x2 == x) {
320 /* Special case: horizontal/vertical line. All checks already done in GfxPreprocessLine. */
321 blitter->DrawLine(video, x, y, x2, y2, screen_width, screen_height, colour, width, dash);
322 return;
325 int grade_y = y2 - y;
326 int grade_x = x2 - x;
328 /* Clipping rectangle. Slightly extended so we can ignore the width of the line. */
329 int extra = (int)CeilDiv(3 * width, 4); // not less then "width * sqrt(2) / 2"
330 Rect clip = { -extra, -extra, screen_width - 1 + extra, screen_height - 1 + extra };
332 /* prevent integer overflows. */
333 int margin = 1;
334 while (INT_MAX / abs(grade_y) < std::max(abs(clip.left - x), abs(clip.right - x))) {
335 grade_y /= 2;
336 grade_x /= 2;
337 margin *= 2; // account for rounding errors
340 /* Imagine that the line is infinitely long and it intersects with
341 * infinitely long left and right edges of the clipping rectangle.
342 * If both intersection points are outside the clipping rectangle
343 * and both on the same side of it, we don't need to draw anything. */
344 int left_isec_y = y + (clip.left - x) * grade_y / grade_x;
345 int right_isec_y = y + (clip.right - x) * grade_y / grade_x;
346 if ((left_isec_y > clip.bottom + margin && right_isec_y > clip.bottom + margin) ||
347 (left_isec_y < clip.top - margin && right_isec_y < clip.top - margin)) {
348 return;
351 /* It is possible to use the line equation to further reduce the amount of
352 * work the blitter has to do by shortening the effective line segment.
353 * However, in order to get that right and prevent the flickering effects
354 * of rounding errors so much additional code has to be run here that in
355 * the general case the effect is not noticeable. */
357 blitter->DrawLine(video, x, y, x2, y2, screen_width, screen_height, colour, width, dash);
361 * Align parameters of a line to the given DPI and check simple clipping.
362 * @param dpi Screen parameters to align with.
363 * @param x X coordinate of first point.
364 * @param y Y coordinate of first point.
365 * @param x2 X coordinate of second point.
366 * @param y2 Y coordinate of second point.
367 * @param width Width of the line.
368 * @return True if the line is likely to be visible, false if it's certainly
369 * invisible.
371 static inline bool GfxPreprocessLine(DrawPixelInfo *dpi, int &x, int &y, int &x2, int &y2, int width)
373 x -= dpi->left;
374 x2 -= dpi->left;
375 y -= dpi->top;
376 y2 -= dpi->top;
378 /* Check simple clipping */
379 if (x + width / 2 < 0 && x2 + width / 2 < 0 ) return false;
380 if (y + width / 2 < 0 && y2 + width / 2 < 0 ) return false;
381 if (x - width / 2 > dpi->width && x2 - width / 2 > dpi->width ) return false;
382 if (y - width / 2 > dpi->height && y2 - width / 2 > dpi->height) return false;
383 return true;
386 void GfxDrawLine(int x, int y, int x2, int y2, int colour, int width, int dash)
388 DrawPixelInfo *dpi = _cur_dpi;
389 if (GfxPreprocessLine(dpi, x, y, x2, y2, width)) {
390 GfxDoDrawLine(dpi->dst_ptr, x, y, x2, y2, dpi->width, dpi->height, colour, width, dash);
394 void GfxDrawLineUnscaled(int x, int y, int x2, int y2, int colour)
396 DrawPixelInfo *dpi = _cur_dpi;
397 if (GfxPreprocessLine(dpi, x, y, x2, y2, 1)) {
398 GfxDoDrawLine(dpi->dst_ptr,
399 UnScaleByZoom(x, dpi->zoom), UnScaleByZoom(y, dpi->zoom),
400 UnScaleByZoom(x2, dpi->zoom), UnScaleByZoom(y2, dpi->zoom),
401 UnScaleByZoom(dpi->width, dpi->zoom), UnScaleByZoom(dpi->height, dpi->zoom), colour, 1);
406 * Draws the projection of a parallelepiped.
407 * This can be used to draw boxes in world coordinates.
409 * @param x Screen X-coordinate of top front corner.
410 * @param y Screen Y-coordinate of top front corner.
411 * @param dx1 Screen X-length of first edge.
412 * @param dy1 Screen Y-length of first edge.
413 * @param dx2 Screen X-length of second edge.
414 * @param dy2 Screen Y-length of second edge.
415 * @param dx3 Screen X-length of third edge.
416 * @param dy3 Screen Y-length of third edge.
418 void DrawBox(int x, int y, int dx1, int dy1, int dx2, int dy2, int dx3, int dy3)
420 /* ....
421 * .. ....
422 * .. ....
423 * .. ^
424 * <--__(dx1,dy1) /(dx2,dy2)
425 * : --__ / :
426 * : --__ / :
427 * : *(x,y) :
428 * : | :
429 * : | ..
430 * .... |(dx3,dy3)
431 * .... | ..
432 * ....V.
435 static const uint8_t colour = PC_WHITE;
437 GfxDrawLineUnscaled(x, y, x + dx1, y + dy1, colour);
438 GfxDrawLineUnscaled(x, y, x + dx2, y + dy2, colour);
439 GfxDrawLineUnscaled(x, y, x + dx3, y + dy3, colour);
441 GfxDrawLineUnscaled(x + dx1, y + dy1, x + dx1 + dx2, y + dy1 + dy2, colour);
442 GfxDrawLineUnscaled(x + dx1, y + dy1, x + dx1 + dx3, y + dy1 + dy3, colour);
443 GfxDrawLineUnscaled(x + dx2, y + dy2, x + dx2 + dx1, y + dy2 + dy1, colour);
444 GfxDrawLineUnscaled(x + dx2, y + dy2, x + dx2 + dx3, y + dy2 + dy3, colour);
445 GfxDrawLineUnscaled(x + dx3, y + dy3, x + dx3 + dx1, y + dy3 + dy1, colour);
446 GfxDrawLineUnscaled(x + dx3, y + dy3, x + dx3 + dx2, y + dy3 + dy2, colour);
450 * Draw the outline of a Rect
451 * @param r Rect to draw.
452 * @param colour Colour of the outline.
453 * @param width Width of the outline.
454 * @param dash Length of dashes for dashed lines. 0 means solid lines.
456 void DrawRectOutline(const Rect &r, int colour, int width, int dash)
458 GfxDrawLine(r.left, r.top, r.right, r.top, colour, width, dash);
459 GfxDrawLine(r.left, r.top, r.left, r.bottom, colour, width, dash);
460 GfxDrawLine(r.right, r.top, r.right, r.bottom, colour, width, dash);
461 GfxDrawLine(r.left, r.bottom, r.right, r.bottom, colour, width, dash);
465 * Set the colour remap to be for the given colour.
466 * @param colour the new colour of the remap.
468 static void SetColourRemap(TextColour colour)
470 if (colour == TC_INVALID) return;
472 /* Black strings have no shading ever; the shading is black, so it
473 * would be invisible at best, but it actually makes it illegible. */
474 bool no_shade = (colour & TC_NO_SHADE) != 0 || colour == TC_BLACK;
475 bool raw_colour = (colour & TC_IS_PALETTE_COLOUR) != 0;
476 colour &= ~(TC_NO_SHADE | TC_IS_PALETTE_COLOUR | TC_FORCED);
478 _string_colourremap[1] = raw_colour ? (uint8_t)colour : _string_colourmap[colour];
479 _string_colourremap[2] = no_shade ? 0 : 1;
480 _colour_remap_ptr = _string_colourremap;
484 * Drawing routine for drawing a laid out line of text.
485 * @param line String to draw.
486 * @param y The top most position to draw on.
487 * @param left The left most position to draw on.
488 * @param right The right most position to draw on.
489 * @param align The alignment of the string when drawing left-to-right. In the
490 * case a right-to-left language is chosen this is inverted so it
491 * will be drawn in the right direction.
492 * @param underline Whether to underline what has been drawn or not.
493 * @param truncation Whether to perform string truncation or not.
494 * @param default_colour Colour of text if not specified within string.
496 * @return In case of left or center alignment the right most pixel we have drawn to.
497 * In case of right alignment the left most pixel we have drawn to.
499 static int DrawLayoutLine(const ParagraphLayouter::Line &line, int y, int left, int right, StringAlignment align, bool underline, bool truncation, TextColour default_colour)
501 if (line.CountRuns() == 0) return 0;
503 int w = line.GetWidth();
504 int h = line.GetLeading();
507 * The following is needed for truncation.
508 * Depending on the text direction, we either remove bits at the rear
509 * or the front. For this we shift the entire area to draw so it fits
510 * within the left/right bounds and the side we do not truncate it on.
511 * Then we determine the truncation location, i.e. glyphs that fall
512 * outside of the range min_x - max_x will not be drawn; they are thus
513 * the truncated glyphs.
515 * At a later step we insert the dots.
518 int max_w = right - left + 1; // The maximum width.
520 int offset_x = 0; // The offset we need for positioning the glyphs
521 int min_x = left; // The minimum x position to draw normal glyphs on.
522 int max_x = right; // The maximum x position to draw normal glyphs on.
524 truncation &= max_w < w; // Whether we need to do truncation.
525 int dot_width = 0; // Cache for the width of the dot.
526 const Sprite *dot_sprite = nullptr; // Cache for the sprite of the dot.
527 bool dot_has_shadow = false; // Whether the dot's font requires shadows.
529 if (truncation) {
531 * Assumption may be made that all fonts of a run are of the same size.
532 * In any case, we'll use these dots for the abbreviation, so even if
533 * another size would be chosen it won't have truncated too little for
534 * the truncation dots.
536 FontCache *fc = line.GetVisualRun(0).GetFont()->fc;
537 dot_has_shadow = fc->GetDrawGlyphShadow();
538 GlyphID dot_glyph = fc->MapCharToGlyph('.');
539 dot_width = fc->GetGlyphWidth(dot_glyph);
540 dot_sprite = fc->GetGlyph(dot_glyph);
542 if (_current_text_dir == TD_RTL) {
543 min_x += 3 * dot_width;
544 offset_x = w - 3 * dot_width - max_w;
545 } else {
546 max_x -= 3 * dot_width;
549 w = max_w;
552 /* In case we have a RTL language we swap the alignment. */
553 if (!(align & SA_FORCE) && _current_text_dir == TD_RTL && (align & SA_HOR_MASK) != SA_HOR_CENTER) align ^= SA_RIGHT;
555 /* right is the right most position to draw on. In this case we want to do
556 * calculations with the width of the string. In comparison right can be
557 * seen as lastof(todraw) and width as lengthof(todraw). They differ by 1.
558 * So most +1/-1 additions are to move from lengthof to 'indices'.
560 switch (align & SA_HOR_MASK) {
561 case SA_LEFT:
562 /* right + 1 = left + w */
563 right = left + w - 1;
564 break;
566 case SA_HOR_CENTER:
567 left = RoundDivSU(right + 1 + left - w, 2);
568 /* right + 1 = left + w */
569 right = left + w - 1;
570 break;
572 case SA_RIGHT:
573 left = right + 1 - w;
574 break;
576 default:
577 NOT_REACHED();
580 const uint shadow_offset = ScaleGUITrad(1);
582 /* Draw shadow, then foreground */
583 for (bool do_shadow : { true, false }) {
584 bool colour_has_shadow = false;
585 for (int run_index = 0; run_index < line.CountRuns(); run_index++) {
586 const ParagraphLayouter::VisualRun &run = line.GetVisualRun(run_index);
587 const auto &glyphs = run.GetGlyphs();
588 const auto &positions = run.GetPositions();
589 const Font *f = run.GetFont();
591 FontCache *fc = f->fc;
592 TextColour colour = f->colour;
593 if (colour == TC_INVALID || HasFlag(default_colour, TC_FORCED)) colour = default_colour;
594 colour_has_shadow = (colour & TC_NO_SHADE) == 0 && colour != TC_BLACK;
595 SetColourRemap(do_shadow ? TC_BLACK : colour); // the last run also sets the colour for the truncation dots
596 if (do_shadow && (!fc->GetDrawGlyphShadow() || !colour_has_shadow)) continue;
598 DrawPixelInfo *dpi = _cur_dpi;
599 int dpi_left = dpi->left;
600 int dpi_right = dpi->left + dpi->width - 1;
602 for (int i = 0; i < run.GetGlyphCount(); i++) {
603 GlyphID glyph = glyphs[i];
605 /* Not a valid glyph (empty) */
606 if (glyph == 0xFFFF) continue;
608 int begin_x = positions[i].left + left - offset_x;
609 int end_x = positions[i].right + left - offset_x;
610 int top = positions[i].top + y;
612 /* Truncated away. */
613 if (truncation && (begin_x < min_x || end_x > max_x)) continue;
615 const Sprite *sprite = fc->GetGlyph(glyph);
616 /* Check clipping (the "+ 1" is for the shadow). */
617 if (begin_x + sprite->x_offs > dpi_right || begin_x + sprite->x_offs + sprite->width /* - 1 + 1 */ < dpi_left) continue;
619 if (do_shadow && (glyph & SPRITE_GLYPH) != 0) continue;
621 GfxMainBlitter(sprite, begin_x + (do_shadow ? shadow_offset : 0), top + (do_shadow ? shadow_offset : 0), BM_COLOUR_REMAP);
625 if (truncation && (!do_shadow || (dot_has_shadow && colour_has_shadow))) {
626 int x = (_current_text_dir == TD_RTL) ? left : (right - 3 * dot_width);
627 for (int i = 0; i < 3; i++, x += dot_width) {
628 GfxMainBlitter(dot_sprite, x + (do_shadow ? shadow_offset : 0), y + (do_shadow ? shadow_offset : 0), BM_COLOUR_REMAP);
633 if (underline) {
634 GfxFillRect(left, y + h, right, y + h + WidgetDimensions::scaled.bevel.top - 1, _string_colourremap[1]);
637 return (align & SA_HOR_MASK) == SA_RIGHT ? left : right;
641 * Draw string, possibly truncated to make it fit in its allocated space
643 * @param left The left most position to draw on.
644 * @param right The right most position to draw on.
645 * @param top The top most position to draw on.
646 * @param str String to draw.
647 * @param colour Colour used for drawing the string, for details see _string_colourmap in
648 * table/palettes.h or docs/ottd-colourtext-palette.png or the enum TextColour in gfx_type.h
649 * @param align The alignment of the string when drawing left-to-right. In the
650 * case a right-to-left language is chosen this is inverted so it
651 * will be drawn in the right direction.
652 * @param underline Whether to underline what has been drawn or not.
653 * @param fontsize The size of the initial characters.
654 * @return In case of left or center alignment the right most pixel we have drawn to.
655 * In case of right alignment the left most pixel we have drawn to.
657 int DrawString(int left, int right, int top, std::string_view str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
659 /* The string may contain control chars to change the font, just use the biggest font for clipping. */
660 int max_height = std::max({GetCharacterHeight(FS_SMALL), GetCharacterHeight(FS_NORMAL), GetCharacterHeight(FS_LARGE), GetCharacterHeight(FS_MONO)});
662 /* Funny glyphs may extent outside the usual bounds, so relax the clipping somewhat. */
663 int extra = max_height / 2;
665 if (_cur_dpi->top + _cur_dpi->height + extra < top || _cur_dpi->top > top + max_height + extra ||
666 _cur_dpi->left + _cur_dpi->width + extra < left || _cur_dpi->left > right + extra) {
667 return 0;
670 Layouter layout(str, INT32_MAX, fontsize);
671 if (layout.empty()) return 0;
673 return DrawLayoutLine(*layout.front(), top, left, right, align, underline, true, colour);
677 * Draw string, possibly truncated to make it fit in its allocated space
679 * @param left The left most position to draw on.
680 * @param right The right most position to draw on.
681 * @param top The top most position to draw on.
682 * @param str String to draw.
683 * @param colour Colour used for drawing the string, for details see _string_colourmap in
684 * table/palettes.h or docs/ottd-colourtext-palette.png or the enum TextColour in gfx_type.h
685 * @param align The alignment of the string when drawing left-to-right. In the
686 * case a right-to-left language is chosen this is inverted so it
687 * will be drawn in the right direction.
688 * @param underline Whether to underline what has been drawn or not.
689 * @param fontsize The size of the initial characters.
690 * @return In case of left or center alignment the right most pixel we have drawn to.
691 * In case of right alignment the left most pixel we have drawn to.
693 int DrawString(int left, int right, int top, StringID str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
695 return DrawString(left, right, top, GetString(str), colour, align, underline, fontsize);
699 * Calculates height of string (in pixels). The string is changed to a multiline string if needed.
700 * @param str string to check
701 * @param maxw maximum string width
702 * @return height of pixels of string when it is drawn
704 int GetStringHeight(std::string_view str, int maxw, FontSize fontsize)
706 assert(maxw > 0);
707 Layouter layout(str, maxw, fontsize);
708 return layout.GetBounds().height;
712 * Calculates height of string (in pixels). The string is changed to a multiline string if needed.
713 * @param str string to check
714 * @param maxw maximum string width
715 * @return height of pixels of string when it is drawn
717 int GetStringHeight(StringID str, int maxw)
719 return GetStringHeight(GetString(str), maxw);
723 * Calculates number of lines of string. The string is changed to a multiline string if needed.
724 * @param str string to check
725 * @param maxw maximum string width
726 * @return number of lines of string when it is drawn
728 int GetStringLineCount(StringID str, int maxw)
730 Layouter layout(GetString(str), maxw);
731 return (uint)layout.size();
735 * Calculate string bounding box for multi-line strings.
736 * @param str String to check.
737 * @param suggestion Suggested bounding box.
738 * @return Bounding box for the multi-line string, may be bigger than \a suggestion.
740 Dimension GetStringMultiLineBoundingBox(StringID str, const Dimension &suggestion)
742 Dimension box = {suggestion.width, (uint)GetStringHeight(str, suggestion.width)};
743 return box;
747 * Calculate string bounding box for multi-line strings.
748 * @param str String to check.
749 * @param suggestion Suggested bounding box.
750 * @return Bounding box for the multi-line string, may be bigger than \a suggestion.
752 Dimension GetStringMultiLineBoundingBox(std::string_view str, const Dimension &suggestion)
754 Dimension box = {suggestion.width, (uint)GetStringHeight(str, suggestion.width)};
755 return box;
759 * Draw string, possibly over multiple lines.
761 * @param left The left most position to draw on.
762 * @param right The right most position to draw on.
763 * @param top The top most position to draw on.
764 * @param bottom The bottom most position to draw on.
765 * @param str String to draw.
766 * @param colour Colour used for drawing the string, for details see _string_colourmap in
767 * table/palettes.h or docs/ottd-colourtext-palette.png or the enum TextColour in gfx_type.h
768 * @param align The horizontal and vertical alignment of the string.
769 * @param underline Whether to underline all strings
770 * @param fontsize The size of the initial characters.
772 * @return If \a align is #SA_BOTTOM, the top to where we have written, else the bottom to where we have written.
774 int DrawStringMultiLine(int left, int right, int top, int bottom, std::string_view str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
776 int maxw = right - left + 1;
777 int maxh = bottom - top + 1;
779 /* It makes no sense to even try if it can't be drawn anyway, or
780 * do we really want to support fonts of 0 or less pixels high? */
781 if (maxh <= 0) return top;
783 Layouter layout(str, maxw, fontsize);
784 int total_height = layout.GetBounds().height;
785 int y;
786 switch (align & SA_VERT_MASK) {
787 case SA_TOP:
788 y = top;
789 break;
791 case SA_VERT_CENTER:
792 y = RoundDivSU(bottom + top - total_height, 2);
793 break;
795 case SA_BOTTOM:
796 y = bottom - total_height;
797 break;
799 default: NOT_REACHED();
802 int last_line = top;
803 int first_line = bottom;
805 for (const auto &line : layout) {
807 int line_height = line->GetLeading();
808 if (y >= top && y + line_height - 1 <= bottom) {
809 last_line = y + line_height;
810 if (first_line > y) first_line = y;
812 DrawLayoutLine(*line, y, left, right, align, underline, false, colour);
814 y += line_height;
817 return ((align & SA_VERT_MASK) == SA_BOTTOM) ? first_line : last_line;
821 * Draw string, possibly over multiple lines.
823 * @param left The left most position to draw on.
824 * @param right The right most position to draw on.
825 * @param top The top most position to draw on.
826 * @param bottom The bottom most position to draw on.
827 * @param str String to draw.
828 * @param colour Colour used for drawing the string, for details see _string_colourmap in
829 * table/palettes.h or docs/ottd-colourtext-palette.png or the enum TextColour in gfx_type.h
830 * @param align The horizontal and vertical alignment of the string.
831 * @param underline Whether to underline all strings
832 * @param fontsize The size of the initial characters.
834 * @return If \a align is #SA_BOTTOM, the top to where we have written, else the bottom to where we have written.
836 int DrawStringMultiLine(int left, int right, int top, int bottom, StringID str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
838 return DrawStringMultiLine(left, right, top, bottom, GetString(str), colour, align, underline, fontsize);
842 * Return the string dimension in pixels. The height and width are returned
843 * in a single Dimension value. TINYFONT, BIGFONT modifiers are only
844 * supported as the first character of the string. The returned dimensions
845 * are therefore a rough estimation correct for all the current strings
846 * but not every possible combination
847 * @param str string to calculate pixel-width
848 * @param start_fontsize Fontsize to start the text with
849 * @return string width and height in pixels
851 Dimension GetStringBoundingBox(std::string_view str, FontSize start_fontsize)
853 Layouter layout(str, INT32_MAX, start_fontsize);
854 return layout.GetBounds();
858 * Get bounding box of a string. Uses parameters set by #SetDParam if needed.
859 * Has the same restrictions as #GetStringBoundingBox(std::string_view str, FontSize start_fontsize).
860 * @param strid String to examine.
861 * @return Width and height of the bounding box for the string in pixels.
863 Dimension GetStringBoundingBox(StringID strid, FontSize start_fontsize)
865 return GetStringBoundingBox(GetString(strid), start_fontsize);
869 * Get maximum width of a list of strings.
870 * @param list List of strings.
871 * @param fontsize Font size to use.
872 * @return Width of longest string within the list.
874 uint GetStringListWidth(std::span<const StringID> list, FontSize fontsize)
876 uint width = 0;
877 for (auto str : list) {
878 width = std::max(width, GetStringBoundingBox(str, fontsize).width);
880 return width;
884 * Get maximum dimension of a list of strings.
885 * @param list List of strings, terminated by INVALID_STRING_ID.
886 * @param fontsize Font size to use.
887 * @return Dimension of highest and longest string within the list.
889 Dimension GetStringListBoundingBox(std::span<const StringID> list, FontSize fontsize)
891 Dimension d{0, 0};
892 for (auto str : list) {
893 d = maxdim(d, GetStringBoundingBox(str, fontsize));
895 return d;
899 * Draw single character horizontally centered around (x,y)
900 * @param c Character (glyph) to draw
901 * @param r Rectangle to draw character within
902 * @param colour Colour to use, for details see _string_colourmap in
903 * table/palettes.h or docs/ottd-colourtext-palette.png or the enum TextColour in gfx_type.h
905 void DrawCharCentered(char32_t c, const Rect &r, TextColour colour)
907 SetColourRemap(colour);
908 GfxMainBlitter(GetGlyph(FS_NORMAL, c),
909 CenterBounds(r.left, r.right, GetCharacterWidth(FS_NORMAL, c)),
910 CenterBounds(r.top, r.bottom, GetCharacterHeight(FS_NORMAL)),
911 BM_COLOUR_REMAP);
915 * Get the size of a sprite.
916 * @param sprid Sprite to examine.
917 * @param[out] offset Optionally returns the sprite position offset.
918 * @param zoom The zoom level applicable to the sprite.
919 * @return Sprite size in pixels.
920 * @note The size assumes (0, 0) as top-left coordinate and ignores any part of the sprite drawn at the left or above that position.
922 Dimension GetSpriteSize(SpriteID sprid, Point *offset, ZoomLevel zoom)
924 const Sprite *sprite = GetSprite(sprid, SpriteType::Normal);
926 if (offset != nullptr) {
927 offset->x = UnScaleByZoom(sprite->x_offs, zoom);
928 offset->y = UnScaleByZoom(sprite->y_offs, zoom);
931 Dimension d;
932 d.width = std::max<int>(0, UnScaleByZoom(sprite->x_offs + sprite->width, zoom));
933 d.height = std::max<int>(0, UnScaleByZoom(sprite->y_offs + sprite->height, zoom));
934 return d;
938 * Helper function to get the blitter mode for different types of palettes.
939 * @param pal The palette to get the blitter mode for.
940 * @return The blitter mode associated with the palette.
942 static BlitterMode GetBlitterMode(PaletteID pal)
944 switch (pal) {
945 case PAL_NONE: return BM_NORMAL;
946 case PALETTE_CRASH: return BM_CRASH_REMAP;
947 case PALETTE_ALL_BLACK: return BM_BLACK_REMAP;
948 default: return BM_COLOUR_REMAP;
953 * Draw a sprite in a viewport.
954 * @param img Image number to draw
955 * @param pal Palette to use.
956 * @param x Left coordinate of image in viewport, scaled by zoom
957 * @param y Top coordinate of image in viewport, scaled by zoom
958 * @param sub If available, draw only specified part of the sprite
960 void DrawSpriteViewport(SpriteID img, PaletteID pal, int x, int y, const SubSprite *sub)
962 SpriteID real_sprite = GB(img, 0, SPRITE_WIDTH);
963 if (HasBit(img, PALETTE_MODIFIER_TRANSPARENT)) {
964 pal = GB(pal, 0, PALETTE_WIDTH);
965 _colour_remap_ptr = GetNonSprite(pal, SpriteType::Recolour) + 1;
966 GfxMainBlitterViewport(GetSprite(real_sprite, SpriteType::Normal), x, y, pal == PALETTE_TO_TRANSPARENT ? BM_TRANSPARENT : BM_TRANSPARENT_REMAP, sub, real_sprite);
967 } else if (pal != PAL_NONE) {
968 if (HasBit(pal, PALETTE_TEXT_RECOLOUR)) {
969 SetColourRemap((TextColour)GB(pal, 0, PALETTE_WIDTH));
970 } else {
971 _colour_remap_ptr = GetNonSprite(GB(pal, 0, PALETTE_WIDTH), SpriteType::Recolour) + 1;
973 GfxMainBlitterViewport(GetSprite(real_sprite, SpriteType::Normal), x, y, GetBlitterMode(pal), sub, real_sprite);
974 } else {
975 GfxMainBlitterViewport(GetSprite(real_sprite, SpriteType::Normal), x, y, BM_NORMAL, sub, real_sprite);
980 * Draw a sprite, not in a viewport
981 * @param img Image number to draw
982 * @param pal Palette to use.
983 * @param x Left coordinate of image in pixels
984 * @param y Top coordinate of image in pixels
985 * @param sub If available, draw only specified part of the sprite
986 * @param zoom Zoom level of sprite
988 void DrawSprite(SpriteID img, PaletteID pal, int x, int y, const SubSprite *sub, ZoomLevel zoom)
990 SpriteID real_sprite = GB(img, 0, SPRITE_WIDTH);
991 if (HasBit(img, PALETTE_MODIFIER_TRANSPARENT)) {
992 pal = GB(pal, 0, PALETTE_WIDTH);
993 _colour_remap_ptr = GetNonSprite(pal, SpriteType::Recolour) + 1;
994 GfxMainBlitter(GetSprite(real_sprite, SpriteType::Normal), x, y, pal == PALETTE_TO_TRANSPARENT ? BM_TRANSPARENT : BM_TRANSPARENT_REMAP, sub, real_sprite, zoom);
995 } else if (pal != PAL_NONE) {
996 if (HasBit(pal, PALETTE_TEXT_RECOLOUR)) {
997 SetColourRemap((TextColour)GB(pal, 0, PALETTE_WIDTH));
998 } else {
999 _colour_remap_ptr = GetNonSprite(GB(pal, 0, PALETTE_WIDTH), SpriteType::Recolour) + 1;
1001 GfxMainBlitter(GetSprite(real_sprite, SpriteType::Normal), x, y, GetBlitterMode(pal), sub, real_sprite, zoom);
1002 } else {
1003 GfxMainBlitter(GetSprite(real_sprite, SpriteType::Normal), x, y, BM_NORMAL, sub, real_sprite, zoom);
1008 * The code for setting up the blitter mode and sprite information before finally drawing the sprite.
1009 * @param sprite The sprite to draw.
1010 * @param x The X location to draw.
1011 * @param y The Y location to draw.
1012 * @param mode The settings for the blitter to pass.
1013 * @param sub Whether to only draw a sub set of the sprite.
1014 * @param zoom The zoom level at which to draw the sprites.
1015 * @param dst Optional parameter for a different blitting destination.
1016 * @tparam ZOOM_BASE The factor required to get the sub sprite information into the right size.
1017 * @tparam SCALED_XY Whether the X and Y are scaled or unscaled.
1019 template <int ZOOM_BASE, bool SCALED_XY>
1020 static void GfxBlitter(const Sprite * const sprite, int x, int y, BlitterMode mode, const SubSprite * const sub, SpriteID sprite_id, ZoomLevel zoom, const DrawPixelInfo *dst = nullptr)
1022 const DrawPixelInfo *dpi = (dst != nullptr) ? dst : _cur_dpi;
1023 Blitter::BlitterParams bp;
1025 if (SCALED_XY) {
1026 /* Scale it */
1027 x = ScaleByZoom(x, zoom);
1028 y = ScaleByZoom(y, zoom);
1031 /* Move to the correct offset */
1032 x += sprite->x_offs;
1033 y += sprite->y_offs;
1035 if (sub == nullptr) {
1036 /* No clipping. */
1037 bp.skip_left = 0;
1038 bp.skip_top = 0;
1039 bp.width = UnScaleByZoom(sprite->width, zoom);
1040 bp.height = UnScaleByZoom(sprite->height, zoom);
1041 } else {
1042 /* Amount of pixels to clip from the source sprite */
1043 int clip_left = std::max(0, -sprite->x_offs + sub->left * ZOOM_BASE );
1044 int clip_top = std::max(0, -sprite->y_offs + sub->top * ZOOM_BASE );
1045 int clip_right = std::max(0, sprite->width - (-sprite->x_offs + (sub->right + 1) * ZOOM_BASE));
1046 int clip_bottom = std::max(0, sprite->height - (-sprite->y_offs + (sub->bottom + 1) * ZOOM_BASE));
1048 if (clip_left + clip_right >= sprite->width) return;
1049 if (clip_top + clip_bottom >= sprite->height) return;
1051 bp.skip_left = UnScaleByZoomLower(clip_left, zoom);
1052 bp.skip_top = UnScaleByZoomLower(clip_top, zoom);
1053 bp.width = UnScaleByZoom(sprite->width - clip_left - clip_right, zoom);
1054 bp.height = UnScaleByZoom(sprite->height - clip_top - clip_bottom, zoom);
1056 x += ScaleByZoom(bp.skip_left, zoom);
1057 y += ScaleByZoom(bp.skip_top, zoom);
1060 /* Copy the main data directly from the sprite */
1061 bp.sprite = sprite->data;
1062 bp.sprite_width = sprite->width;
1063 bp.sprite_height = sprite->height;
1064 bp.top = 0;
1065 bp.left = 0;
1067 bp.dst = dpi->dst_ptr;
1068 bp.pitch = dpi->pitch;
1069 bp.remap = _colour_remap_ptr;
1071 assert(sprite->width > 0);
1072 assert(sprite->height > 0);
1074 if (bp.width <= 0) return;
1075 if (bp.height <= 0) return;
1077 y -= SCALED_XY ? ScaleByZoom(dpi->top, zoom) : dpi->top;
1078 int y_unscaled = UnScaleByZoom(y, zoom);
1079 /* Check for top overflow */
1080 if (y < 0) {
1081 bp.height -= -y_unscaled;
1082 if (bp.height <= 0) return;
1083 bp.skip_top += -y_unscaled;
1084 y = 0;
1085 } else {
1086 bp.top = y_unscaled;
1089 /* Check for bottom overflow */
1090 y += SCALED_XY ? ScaleByZoom(bp.height - dpi->height, zoom) : ScaleByZoom(bp.height, zoom) - dpi->height;
1091 if (y > 0) {
1092 bp.height -= UnScaleByZoom(y, zoom);
1093 if (bp.height <= 0) return;
1096 x -= SCALED_XY ? ScaleByZoom(dpi->left, zoom) : dpi->left;
1097 int x_unscaled = UnScaleByZoom(x, zoom);
1098 /* Check for left overflow */
1099 if (x < 0) {
1100 bp.width -= -x_unscaled;
1101 if (bp.width <= 0) return;
1102 bp.skip_left += -x_unscaled;
1103 x = 0;
1104 } else {
1105 bp.left = x_unscaled;
1108 /* Check for right overflow */
1109 x += SCALED_XY ? ScaleByZoom(bp.width - dpi->width, zoom) : ScaleByZoom(bp.width, zoom) - dpi->width;
1110 if (x > 0) {
1111 bp.width -= UnScaleByZoom(x, zoom);
1112 if (bp.width <= 0) return;
1115 assert(bp.skip_left + bp.width <= UnScaleByZoom(sprite->width, zoom));
1116 assert(bp.skip_top + bp.height <= UnScaleByZoom(sprite->height, zoom));
1118 /* We do not want to catch the mouse. However we also use that spritenumber for unknown (text) sprites. */
1119 if (_newgrf_debug_sprite_picker.mode == SPM_REDRAW && sprite_id != SPR_CURSOR_MOUSE) {
1120 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
1121 void *topleft = blitter->MoveTo(bp.dst, bp.left, bp.top);
1122 void *bottomright = blitter->MoveTo(topleft, bp.width - 1, bp.height - 1);
1124 void *clicked = _newgrf_debug_sprite_picker.clicked_pixel;
1126 if (topleft <= clicked && clicked <= bottomright) {
1127 uint offset = (((size_t)clicked - (size_t)topleft) / (blitter->GetScreenDepth() / 8)) % bp.pitch;
1128 if (offset < (uint)bp.width) {
1129 include(_newgrf_debug_sprite_picker.sprites, sprite_id);
1134 BlitterFactory::GetCurrentBlitter()->Draw(&bp, mode, zoom);
1138 * Draws a sprite to a new RGBA buffer (see Colour union) instead of drawing to the screen.
1140 * @param spriteId The sprite to draw.
1141 * @param zoom The zoom level at which to draw the sprites.
1142 * @return Pixel buffer, or nullptr if an 8bpp blitter is being used.
1144 std::unique_ptr<uint32_t[]> DrawSpriteToRgbaBuffer(SpriteID spriteId, ZoomLevel zoom)
1146 /* Invalid zoom level requested? */
1147 if (zoom < _settings_client.gui.zoom_min || zoom > _settings_client.gui.zoom_max) return nullptr;
1149 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
1150 if (blitter->GetScreenDepth() != 8 && blitter->GetScreenDepth() != 32) return nullptr;
1152 /* Gather information about the sprite to write, reserve memory */
1153 const SpriteID real_sprite = GB(spriteId, 0, SPRITE_WIDTH);
1154 const Sprite *sprite = GetSprite(real_sprite, SpriteType::Normal);
1155 Dimension dim = GetSpriteSize(real_sprite, nullptr, zoom);
1156 size_t dim_size = static_cast<size_t>(dim.width) * dim.height;
1157 std::unique_ptr<uint32_t[]> result = std::make_unique<uint32_t[]>(dim_size);
1159 /* Prepare new DrawPixelInfo - Normally this would be the screen but we want to draw to another buffer here.
1160 * Normally, pitch would be scaled screen width, but in our case our "screen" is only the sprite width wide. */
1161 DrawPixelInfo dpi;
1162 dpi.dst_ptr = result.get();
1163 dpi.pitch = dim.width;
1164 dpi.left = 0;
1165 dpi.top = 0;
1166 dpi.width = dim.width;
1167 dpi.height = dim.height;
1168 dpi.zoom = zoom;
1170 dim_size = static_cast<size_t>(dim.width) * dim.height;
1172 /* If the current blitter is a paletted blitter, we have to render to an extra buffer and resolve the palette later. */
1173 std::unique_ptr<uint8_t[]> pal_buffer{};
1174 if (blitter->GetScreenDepth() == 8) {
1175 pal_buffer = std::make_unique<uint8_t[]>(dim_size);
1176 dpi.dst_ptr = pal_buffer.get();
1179 /* Temporarily disable screen animations while blitting - This prevents 40bpp_anim from writing to the animation buffer. */
1180 Backup<bool> disable_anim(_screen_disable_anim, true);
1181 GfxBlitter<1, true>(sprite, 0, 0, BM_NORMAL, nullptr, real_sprite, zoom, &dpi);
1182 disable_anim.Restore();
1184 if (blitter->GetScreenDepth() == 8) {
1185 /* Resolve palette. */
1186 uint32_t *dst = result.get();
1187 const uint8_t *src = pal_buffer.get();
1188 for (size_t i = 0; i < dim_size; ++i) {
1189 *dst++ = _cur_palette.palette[*src++].data;
1193 return result;
1196 static void GfxMainBlitterViewport(const Sprite *sprite, int x, int y, BlitterMode mode, const SubSprite *sub, SpriteID sprite_id)
1198 GfxBlitter<ZOOM_BASE, false>(sprite, x, y, mode, sub, sprite_id, _cur_dpi->zoom);
1201 static void GfxMainBlitter(const Sprite *sprite, int x, int y, BlitterMode mode, const SubSprite *sub, SpriteID sprite_id, ZoomLevel zoom)
1203 GfxBlitter<1, true>(sprite, x, y, mode, sub, sprite_id, zoom);
1207 * Initialize _stringwidth_table cache
1208 * @param monospace Whether to load the monospace cache or the normal fonts.
1210 void LoadStringWidthTable(bool monospace)
1212 ClearFontCache();
1214 for (FontSize fs = monospace ? FS_MONO : FS_BEGIN; fs < (monospace ? FS_END : FS_MONO); fs++) {
1215 for (uint i = 0; i != 224; i++) {
1216 _stringwidth_table[fs][i] = GetGlyphWidth(fs, i + 32);
1222 * Return width of character glyph.
1223 * @param size Font of the character
1224 * @param key Character code glyph
1225 * @return Width of the character glyph
1227 uint8_t GetCharacterWidth(FontSize size, char32_t key)
1229 /* Use _stringwidth_table cache if possible */
1230 if (key >= 32 && key < 256) return _stringwidth_table[size][key - 32];
1232 return GetGlyphWidth(size, key);
1236 * Return the maximum width of single digit.
1237 * @param size Font of the digit
1238 * @return Width of the digit.
1240 uint8_t GetDigitWidth(FontSize size)
1242 uint8_t width = 0;
1243 for (char c = '0'; c <= '9'; c++) {
1244 width = std::max(GetCharacterWidth(size, c), width);
1246 return width;
1250 * Determine the broadest digits for guessing the maximum width of a n-digit number.
1251 * @param[out] front Broadest digit, which is not 0. (Use this digit as first digit for numbers with more than one digit.)
1252 * @param[out] next Broadest digit, including 0. (Use this digit for all digits, except the first one; or for numbers with only one digit.)
1253 * @param size Font of the digit
1255 void GetBroadestDigit(uint *front, uint *next, FontSize size)
1257 int width = -1;
1258 for (char c = '9'; c >= '0'; c--) {
1259 int w = GetCharacterWidth(size, c);
1260 if (w > width) {
1261 width = w;
1262 *next = c - '0';
1263 if (c != '0') *front = c - '0';
1268 void ScreenSizeChanged()
1270 _dirty_bytes_per_line = CeilDiv(_screen.width, DIRTY_BLOCK_WIDTH);
1271 _dirty_blocks = ReallocT<uint8_t>(_dirty_blocks, static_cast<size_t>(_dirty_bytes_per_line) * CeilDiv(_screen.height, DIRTY_BLOCK_HEIGHT));
1273 /* check the dirty rect */
1274 if (_invalid_rect.right >= _screen.width) _invalid_rect.right = _screen.width;
1275 if (_invalid_rect.bottom >= _screen.height) _invalid_rect.bottom = _screen.height;
1277 /* screen size changed and the old bitmap is invalid now, so we don't want to undraw it */
1278 _cursor.visible = false;
1281 void UndrawMouseCursor()
1283 /* Don't undraw mouse cursor if it is handled by the video driver. */
1284 if (VideoDriver::GetInstance()->UseSystemCursor()) return;
1286 /* Don't undraw the mouse cursor if the screen is not ready */
1287 if (_screen.dst_ptr == nullptr) return;
1289 if (_cursor.visible) {
1290 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
1291 _cursor.visible = false;
1292 blitter->CopyFromBuffer(blitter->MoveTo(_screen.dst_ptr, _cursor.draw_pos.x, _cursor.draw_pos.y), _cursor_backup.GetBuffer(), _cursor.draw_size.x, _cursor.draw_size.y);
1293 VideoDriver::GetInstance()->MakeDirty(_cursor.draw_pos.x, _cursor.draw_pos.y, _cursor.draw_size.x, _cursor.draw_size.y);
1297 void DrawMouseCursor()
1299 /* Don't draw mouse cursor if it is handled by the video driver. */
1300 if (VideoDriver::GetInstance()->UseSystemCursor()) return;
1302 /* Don't draw the mouse cursor if the screen is not ready */
1303 if (_screen.dst_ptr == nullptr) return;
1305 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
1307 /* Redraw mouse cursor but only when it's inside the window */
1308 if (!_cursor.in_window) return;
1310 /* Don't draw the mouse cursor if it's already drawn */
1311 if (_cursor.visible) {
1312 if (!_cursor.dirty) return;
1313 UndrawMouseCursor();
1316 /* Determine visible area */
1317 int left = _cursor.pos.x + _cursor.total_offs.x;
1318 int width = _cursor.total_size.x;
1319 if (left < 0) {
1320 width += left;
1321 left = 0;
1323 if (left + width > _screen.width) {
1324 width = _screen.width - left;
1326 if (width <= 0) return;
1328 int top = _cursor.pos.y + _cursor.total_offs.y;
1329 int height = _cursor.total_size.y;
1330 if (top < 0) {
1331 height += top;
1332 top = 0;
1334 if (top + height > _screen.height) {
1335 height = _screen.height - top;
1337 if (height <= 0) return;
1339 _cursor.draw_pos.x = left;
1340 _cursor.draw_pos.y = top;
1341 _cursor.draw_size.x = width;
1342 _cursor.draw_size.y = height;
1344 uint8_t *buffer = _cursor_backup.Allocate(blitter->BufferSize(_cursor.draw_size.x, _cursor.draw_size.y));
1346 /* Make backup of stuff below cursor */
1347 blitter->CopyToBuffer(blitter->MoveTo(_screen.dst_ptr, _cursor.draw_pos.x, _cursor.draw_pos.y), buffer, _cursor.draw_size.x, _cursor.draw_size.y);
1349 /* Draw cursor on screen */
1350 _cur_dpi = &_screen;
1351 for (const auto &cs : _cursor.sprites) {
1352 DrawSprite(cs.image.sprite, cs.image.pal, _cursor.pos.x + cs.pos.x, _cursor.pos.y + cs.pos.y);
1355 VideoDriver::GetInstance()->MakeDirty(_cursor.draw_pos.x, _cursor.draw_pos.y, _cursor.draw_size.x, _cursor.draw_size.y);
1357 _cursor.visible = true;
1358 _cursor.dirty = false;
1362 * Repaints a specific rectangle of the screen.
1364 * @param left,top,right,bottom The area of the screen that needs repainting
1365 * @pre The rectangle should have been previously marked dirty with \c AddDirtyBlock.
1366 * @see AddDirtyBlock
1367 * @see DrawDirtyBlocks
1368 * @ingroup dirty
1371 void RedrawScreenRect(int left, int top, int right, int bottom)
1373 assert(right <= _screen.width && bottom <= _screen.height);
1374 if (_cursor.visible) {
1375 if (right > _cursor.draw_pos.x &&
1376 left < _cursor.draw_pos.x + _cursor.draw_size.x &&
1377 bottom > _cursor.draw_pos.y &&
1378 top < _cursor.draw_pos.y + _cursor.draw_size.y) {
1379 UndrawMouseCursor();
1383 if (_networking) NetworkUndrawChatMessage();
1385 DrawOverlappedWindowForAll(left, top, right, bottom);
1387 VideoDriver::GetInstance()->MakeDirty(left, top, right - left, bottom - top);
1391 * Repaints the rectangle blocks which are marked as 'dirty'.
1393 * @see AddDirtyBlock
1395 * @ingroup dirty
1397 void DrawDirtyBlocks()
1399 uint8_t *b = _dirty_blocks;
1400 const int w = Align(_screen.width, DIRTY_BLOCK_WIDTH);
1401 const int h = Align(_screen.height, DIRTY_BLOCK_HEIGHT);
1402 int x;
1403 int y;
1405 y = 0;
1406 do {
1407 x = 0;
1408 do {
1409 if (*b != 0) {
1410 int left;
1411 int top;
1412 int right = x + DIRTY_BLOCK_WIDTH;
1413 int bottom = y;
1414 uint8_t *p = b;
1415 int h2;
1417 /* First try coalescing downwards */
1418 do {
1419 *p = 0;
1420 p += _dirty_bytes_per_line;
1421 bottom += DIRTY_BLOCK_HEIGHT;
1422 } while (bottom != h && *p != 0);
1424 /* Try coalescing to the right too. */
1425 h2 = (bottom - y) / DIRTY_BLOCK_HEIGHT;
1426 assert(h2 > 0);
1427 p = b;
1429 while (right != w) {
1430 uint8_t *p2 = ++p;
1431 int i = h2;
1432 /* Check if a full line of dirty flags is set. */
1433 do {
1434 if (!*p2) goto no_more_coalesc;
1435 p2 += _dirty_bytes_per_line;
1436 } while (--i != 0);
1438 /* Wohoo, can combine it one step to the right!
1439 * Do that, and clear the bits. */
1440 right += DIRTY_BLOCK_WIDTH;
1442 i = h2;
1443 p2 = p;
1444 do {
1445 *p2 = 0;
1446 p2 += _dirty_bytes_per_line;
1447 } while (--i != 0);
1449 no_more_coalesc:
1451 left = x;
1452 top = y;
1454 if (left < _invalid_rect.left ) left = _invalid_rect.left;
1455 if (top < _invalid_rect.top ) top = _invalid_rect.top;
1456 if (right > _invalid_rect.right ) right = _invalid_rect.right;
1457 if (bottom > _invalid_rect.bottom) bottom = _invalid_rect.bottom;
1459 if (left < right && top < bottom) {
1460 RedrawScreenRect(left, top, right, bottom);
1464 } while (b++, (x += DIRTY_BLOCK_WIDTH) != w);
1465 } while (b += -(int)(w / DIRTY_BLOCK_WIDTH) + _dirty_bytes_per_line, (y += DIRTY_BLOCK_HEIGHT) != h);
1467 ++_dirty_block_colour;
1468 _invalid_rect.left = w;
1469 _invalid_rect.top = h;
1470 _invalid_rect.right = 0;
1471 _invalid_rect.bottom = 0;
1475 * Extend the internal _invalid_rect rectangle to contain the rectangle
1476 * defined by the given parameters. Note the point (0,0) is top left.
1478 * @param left The left edge of the rectangle
1479 * @param top The top edge of the rectangle
1480 * @param right The right edge of the rectangle
1481 * @param bottom The bottom edge of the rectangle
1482 * @see DrawDirtyBlocks
1483 * @ingroup dirty
1486 void AddDirtyBlock(int left, int top, int right, int bottom)
1488 uint8_t *b;
1489 int width;
1490 int height;
1492 if (left < 0) left = 0;
1493 if (top < 0) top = 0;
1494 if (right > _screen.width) right = _screen.width;
1495 if (bottom > _screen.height) bottom = _screen.height;
1497 if (left >= right || top >= bottom) return;
1499 if (left < _invalid_rect.left ) _invalid_rect.left = left;
1500 if (top < _invalid_rect.top ) _invalid_rect.top = top;
1501 if (right > _invalid_rect.right ) _invalid_rect.right = right;
1502 if (bottom > _invalid_rect.bottom) _invalid_rect.bottom = bottom;
1504 left /= DIRTY_BLOCK_WIDTH;
1505 top /= DIRTY_BLOCK_HEIGHT;
1507 b = _dirty_blocks + top * _dirty_bytes_per_line + left;
1509 width = ((right - 1) / DIRTY_BLOCK_WIDTH) - left + 1;
1510 height = ((bottom - 1) / DIRTY_BLOCK_HEIGHT) - top + 1;
1512 assert(width > 0 && height > 0);
1514 do {
1515 int i = width;
1517 do b[--i] = 0xFF; while (i != 0);
1519 b += _dirty_bytes_per_line;
1520 } while (--height != 0);
1524 * This function mark the whole screen as dirty. This results in repainting
1525 * the whole screen. Use this with care as this function will break the
1526 * idea about marking only parts of the screen as 'dirty'.
1527 * @ingroup dirty
1529 void MarkWholeScreenDirty()
1531 AddDirtyBlock(0, 0, _screen.width, _screen.height);
1535 * Set up a clipping area for only drawing into a certain area. To do this,
1536 * Fill a DrawPixelInfo object with the supplied relative rectangle, backup
1537 * the original (calling) _cur_dpi and assign the just returned DrawPixelInfo
1538 * _cur_dpi. When you are done, give restore _cur_dpi's original value
1539 * @param *n the DrawPixelInfo that will be the clipping rectangle box allowed
1540 * for drawing
1541 * @param left,top,width,height the relative coordinates of the clipping
1542 * rectangle relative to the current _cur_dpi. This will most likely be the
1543 * offset from the calling window coordinates
1544 * @return return false if the requested rectangle is not possible with the
1545 * current dpi pointer. Only continue of the return value is true, or you'll
1546 * get some nasty results
1548 bool FillDrawPixelInfo(DrawPixelInfo *n, int left, int top, int width, int height)
1550 Blitter *blitter = BlitterFactory::GetCurrentBlitter();
1551 const DrawPixelInfo *o = _cur_dpi;
1553 n->zoom = ZOOM_LVL_MIN;
1555 assert(width > 0);
1556 assert(height > 0);
1558 if ((left -= o->left) < 0) {
1559 width += left;
1560 if (width <= 0) return false;
1561 n->left = -left;
1562 left = 0;
1563 } else {
1564 n->left = 0;
1567 if (width > o->width - left) {
1568 width = o->width - left;
1569 if (width <= 0) return false;
1571 n->width = width;
1573 if ((top -= o->top) < 0) {
1574 height += top;
1575 if (height <= 0) return false;
1576 n->top = -top;
1577 top = 0;
1578 } else {
1579 n->top = 0;
1582 n->dst_ptr = blitter->MoveTo(o->dst_ptr, left, top);
1583 n->pitch = o->pitch;
1585 if (height > o->height - top) {
1586 height = o->height - top;
1587 if (height <= 0) return false;
1589 n->height = height;
1591 return true;
1595 * Update cursor dimension.
1596 * Called when changing cursor sprite resp. reloading grfs.
1598 void UpdateCursorSize()
1600 /* Ignore setting any cursor before the sprites are loaded. */
1601 if (GetMaxSpriteID() == 0) return;
1603 bool first = true;
1604 for (const auto &cs : _cursor.sprites) {
1605 const Sprite *p = GetSprite(GB(cs.image.sprite, 0, SPRITE_WIDTH), SpriteType::Normal);
1606 Point offs, size;
1607 offs.x = UnScaleGUI(p->x_offs) + cs.pos.x;
1608 offs.y = UnScaleGUI(p->y_offs) + cs.pos.y;
1609 size.x = UnScaleGUI(p->width);
1610 size.y = UnScaleGUI(p->height);
1612 if (first) {
1613 /* First sprite sets the total. */
1614 _cursor.total_offs = offs;
1615 _cursor.total_size = size;
1616 first = false;
1617 } else {
1618 /* Additional sprites expand the total. */
1619 int right = std::max(_cursor.total_offs.x + _cursor.total_size.x, offs.x + size.x);
1620 int bottom = std::max(_cursor.total_offs.y + _cursor.total_size.y, offs.y + size.y);
1621 if (offs.x < _cursor.total_offs.x) _cursor.total_offs.x = offs.x;
1622 if (offs.y < _cursor.total_offs.y) _cursor.total_offs.y = offs.y;
1623 _cursor.total_size.x = right - _cursor.total_offs.x;
1624 _cursor.total_size.y = bottom - _cursor.total_offs.y;
1628 _cursor.dirty = true;
1632 * Switch cursor to different sprite.
1633 * @param cursor Sprite to draw for the cursor.
1634 * @param pal Palette to use for recolouring.
1636 static void SetCursorSprite(CursorID cursor, PaletteID pal)
1638 if (_cursor.sprites.size() == 1 && _cursor.sprites[0].image.sprite == cursor && _cursor.sprites[0].image.pal == pal) return;
1640 _cursor.sprites.clear();
1641 _cursor.sprites.emplace_back(cursor, pal, 0, 0);
1643 UpdateCursorSize();
1646 static void SwitchAnimatedCursor()
1648 const AnimCursor *cur = _cursor.animate_cur;
1650 if (cur == nullptr || cur->sprite == AnimCursor::LAST) cur = _cursor.animate_list;
1652 assert(!_cursor.sprites.empty());
1653 SetCursorSprite(cur->sprite, _cursor.sprites[0].image.pal);
1655 _cursor.animate_timeout = cur->display_time;
1656 _cursor.animate_cur = cur + 1;
1659 void CursorTick()
1661 if (_cursor.animate_timeout != 0 && --_cursor.animate_timeout == 0) {
1662 SwitchAnimatedCursor();
1667 * Set or unset the ZZZ cursor.
1668 * @param busy Whether to show the ZZZ cursor.
1670 void SetMouseCursorBusy(bool busy)
1672 assert(!_cursor.sprites.empty());
1673 if (busy) {
1674 if (_cursor.sprites[0].image.sprite == SPR_CURSOR_MOUSE) SetMouseCursor(SPR_CURSOR_ZZZ, PAL_NONE);
1675 } else {
1676 if (_cursor.sprites[0].image.sprite == SPR_CURSOR_ZZZ) SetMouseCursor(SPR_CURSOR_MOUSE, PAL_NONE);
1681 * Assign a single non-animated sprite to the cursor.
1682 * @param sprite Sprite to draw for the cursor.
1683 * @param pal Palette to use for recolouring.
1684 * @see SetAnimatedMouseCursor
1686 void SetMouseCursor(CursorID sprite, PaletteID pal)
1688 /* Turn off animation */
1689 _cursor.animate_timeout = 0;
1690 /* Set cursor */
1691 SetCursorSprite(sprite, pal);
1695 * Assign an animation to the cursor.
1696 * @param table Array of animation states.
1697 * @see SetMouseCursor
1699 void SetAnimatedMouseCursor(const AnimCursor *table)
1701 assert(!_cursor.sprites.empty());
1702 _cursor.animate_list = table;
1703 _cursor.animate_cur = nullptr;
1704 _cursor.sprites[0].image.pal = PAL_NONE;
1705 SwitchAnimatedCursor();
1709 * Update cursor position based on a relative change.
1711 * @param delta_x How much change in the X position.
1712 * @param delta_y How much change in the Y position.
1714 void CursorVars::UpdateCursorPositionRelative(int delta_x, int delta_y)
1716 assert(this->fix_at);
1718 this->delta.x = delta_x;
1719 this->delta.y = delta_y;
1723 * Update cursor position on mouse movement.
1724 * @param x New X position.
1725 * @param y New Y position.
1726 * @return true, if the OS cursor position should be warped back to this->pos.
1728 bool CursorVars::UpdateCursorPosition(int x, int y)
1730 this->delta.x = x - this->pos.x;
1731 this->delta.y = y - this->pos.y;
1733 if (this->fix_at) {
1734 return this->delta.x != 0 || this->delta.y != 0;
1735 } else if (this->pos.x != x || this->pos.y != y) {
1736 this->dirty = true;
1737 this->pos.x = x;
1738 this->pos.y = y;
1741 return false;
1744 bool ChangeResInGame(int width, int height)
1746 return (_screen.width == width && _screen.height == height) || VideoDriver::GetInstance()->ChangeResolution(width, height);
1749 bool ToggleFullScreen(bool fs)
1751 bool result = VideoDriver::GetInstance()->ToggleFullscreen(fs);
1752 if (_fullscreen != fs && _resolutions.empty()) {
1753 Debug(driver, 0, "Could not find a suitable fullscreen resolution");
1755 return result;
1758 void SortResolutions()
1760 std::sort(_resolutions.begin(), _resolutions.end());
1762 /* Remove any duplicates from the list. */
1763 auto last = std::unique(_resolutions.begin(), _resolutions.end());
1764 _resolutions.erase(last, _resolutions.end());
1768 * Resolve GUI zoom level, if auto-suggestion is requested.
1770 void UpdateGUIZoom()
1772 /* Determine real GUI zoom to use. */
1773 if (_gui_scale_cfg == -1) {
1774 _gui_scale = VideoDriver::GetInstance()->GetSuggestedUIScale();
1775 } else {
1776 _gui_scale = Clamp(_gui_scale_cfg, MIN_INTERFACE_SCALE, MAX_INTERFACE_SCALE);
1779 int8_t new_zoom = ScaleGUITrad(1) <= 1 ? ZOOM_LVL_NORMAL : ScaleGUITrad(1) >= 4 ? ZOOM_LVL_IN_4X : ZOOM_LVL_IN_2X;
1780 /* Font glyphs should not be clamped to min/max zoom. */
1781 _font_zoom = static_cast<ZoomLevel>(new_zoom);
1782 /* Ensure the gui_zoom is clamped between min/max. */
1783 new_zoom = Clamp(new_zoom, _settings_client.gui.zoom_min, _settings_client.gui.zoom_max);
1784 _gui_zoom = static_cast<ZoomLevel>(new_zoom);
1788 * Resolve GUI zoom level and adjust GUI to new zoom, if auto-suggestion is requested.
1789 * @param automatic Set if the change is occuring due to OS DPI scaling being changed.
1790 * @returns true when the zoom level has changed, caller must call ReInitAllWindows(true)
1791 * after resizing the application's window/buffer.
1793 bool AdjustGUIZoom(bool automatic)
1795 ZoomLevel old_gui_zoom = _gui_zoom;
1796 ZoomLevel old_font_zoom = _font_zoom;
1797 int old_scale = _gui_scale;
1798 UpdateGUIZoom();
1799 if (old_scale == _gui_scale && old_gui_zoom == _gui_zoom) return false;
1801 /* Update cursors if sprite zoom level has changed. */
1802 if (old_gui_zoom != _gui_zoom) {
1803 VideoDriver::GetInstance()->ClearSystemSprites();
1804 UpdateCursorSize();
1806 if (old_font_zoom != _font_zoom) {
1807 GfxClearFontSpriteCache();
1809 ClearFontCache();
1810 LoadStringWidthTable();
1812 SetupWidgetDimensions();
1813 UpdateAllVirtCoords();
1815 /* Adjust all window sizes to match the new zoom level, so that they don't appear
1816 to move around when the application is moved to a screen with different DPI. */
1817 auto zoom_shift = old_gui_zoom - _gui_zoom;
1818 for (Window *w : Window::Iterate()) {
1819 if (automatic) {
1820 w->left = (w->left * _gui_scale) / old_scale;
1821 w->top = (w->top * _gui_scale) / old_scale;
1823 if (w->viewport != nullptr) {
1824 w->viewport->zoom = static_cast<ZoomLevel>(Clamp(w->viewport->zoom - zoom_shift, _settings_client.gui.zoom_min, _settings_client.gui.zoom_max));
1828 return true;
1831 void ChangeGameSpeed(bool enable_fast_forward)
1833 if (enable_fast_forward) {
1834 _game_speed = _settings_client.gui.fast_forward_speed_limit;
1835 } else {
1836 _game_speed = 100;