diff --git a/src/smallmap_gui.cpp b/src/smallmap_gui.cpp index 6f4b1da..7e8310e 100644 --- a/src/smallmap_gui.cpp +++ b/src/smallmap_gui.cpp @@ -33,6 +33,8 @@ enum SmallMapWindowWidgets { SM_WIDGET_MAP, SM_WIDGET_LEGEND, SM_WIDGET_BUTTONSPANEL, + SM_WIDGET_ZOOM_IN, + SM_WIDGET_ZOOM_OUT, SM_WIDGET_CONTOUR, SM_WIDGET_VEHICLES, SM_WIDGET_INDUSTRIES, @@ -53,8 +55,10 @@ static const Widget _smallmap_widgets[] = { { WWT_STICKYBOX, RESIZE_LR, COLOUR_BROWN, 338, 349, 0, 13, 0x0, STR_STICKY_BUTTON}, // SM_WIDGET_STICKYBOX { WWT_PANEL, RESIZE_RB, COLOUR_BROWN, 0, 349, 14, 157, 0x0, STR_NULL}, // SM_WIDGET_MAP_BORDER { WWT_INSET, RESIZE_RB, COLOUR_BROWN, 2, 347, 16, 155, 0x0, STR_NULL}, // SM_WIDGET_MAP -{ WWT_PANEL, RESIZE_RTB, COLOUR_BROWN, 0, 261, 158, 201, 0x0, STR_NULL}, // SM_WIDGET_LEGEND -{ WWT_PANEL, RESIZE_LRTB, COLOUR_BROWN, 262, 349, 158, 158, 0x0, STR_NULL}, // SM_WIDGET_BUTTONSPANEL +{ WWT_PANEL, RESIZE_RTB, COLOUR_BROWN, 0, 239, 158, 201, 0x0, STR_NULL}, // SM_WIDGET_LEGEND +{ WWT_PANEL, RESIZE_LRTB, COLOUR_BROWN, 240, 349, 158, 158, 0x0, STR_NULL}, // SM_WIDGET_BUTTONSPANEL +{ WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 240, 261, 158, 179, SPR_IMG_ZOOMIN, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_IN}, // SM_WIDGET_ZOOM_IN +{ WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 240, 261, 180, 201, SPR_IMG_ZOOMOUT, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_OUT}, // SM_WIDGET_ZOOM_OUT { WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 284, 305, 158, 179, SPR_IMG_SHOW_COUNTOURS, STR_SMALLMAP_TOOLTIP_SHOW_LAND_CONTOURS_ON_MAP}, // SM_WIDGET_CONTOUR { WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 306, 327, 158, 179, SPR_IMG_SHOW_VEHICLES, STR_SMALLMAP_TOOLTIP_SHOW_VEHICLES_ON_MAP}, // SM_WIDGET_VEHICLES { WWT_IMGBTN, RESIZE_LRTB, COLOUR_BROWN, 328, 349, 158, 179, SPR_IMG_INDUSTRY, STR_SMALLMAP_TOOLTIP_SHOW_INDUSTRIES_ON_MAP}, // SM_WIDGET_INDUSTRIES @@ -86,11 +90,13 @@ static const NWidgetPart _nested_smallmap_widgets[] = { EndContainer(), /* Panel. */ NWidget(NWID_HORIZONTAL), - NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_LEGEND), SetMinimalSize(262, 44), SetResize(1, 0), EndContainer(), + NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_LEGEND), SetMinimalSize(240, 44), SetResize(1, 0), EndContainer(), NWidget(NWID_LAYERED), NWidget(NWID_VERTICAL), /* Top button row. */ NWidget(NWID_HORIZONTAL), + NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_ZOOM_IN), SetMinimalSize(22, 22), + SetDataTip(SPR_IMG_ZOOMIN, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_IN), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_CENTERMAP), SetMinimalSize(22, 22), SetDataTip(SPR_IMG_SMALLMAP, STR_SMALLMAP_CENTER), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_CONTOUR), SetMinimalSize(22, 22), @@ -102,6 +108,8 @@ static const NWidgetPart _nested_smallmap_widgets[] = { EndContainer(), /* Bottom button row. */ NWidget(NWID_HORIZONTAL), + NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_ZOOM_OUT), SetMinimalSize(22, 22), + SetDataTip(SPR_IMG_ZOOMOUT, STR_TOOLBAR_TOOLTIP_ZOOM_THE_VIEW_OUT), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_TOGGLETOWNNAME), SetMinimalSize(22, 22), SetDataTip(SPR_IMG_TOWN, STR_SMALLMAP_TOOLTIP_TOGGLE_TOWN_NAMES_ON_OFF), NWidget(WWT_IMGBTN, COLOUR_BROWN, SM_WIDGET_ROUTES), SetMinimalSize(22, 22), @@ -113,7 +121,7 @@ static const NWidgetPart _nested_smallmap_widgets[] = { EndContainer(), EndContainer(), NWidget(NWID_VERTICAL), - NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_BUTTONSPANEL), SetMinimalSize(88, 1), SetFill(0, 0), EndContainer(), + NWidget(WWT_PANEL, COLOUR_BROWN, SM_WIDGET_BUTTONSPANEL), SetMinimalSize(110, 1), SetFill(0, 0), EndContainer(), NWidget(NWID_SPACER), SetFill(0, 1), EndContainer(), EndContainer(), @@ -497,19 +505,6 @@ static inline uint32 GetSmallMapOwnerPixels(TileIndex tile) return _owner_colours[o]; } - -static const uint32 _smallmap_mask_left[3] = { - MKCOLOUR(0xFF000000), - MKCOLOUR(0xFFFF0000), - MKCOLOUR(0xFFFFFF00), -}; - -static const uint32 _smallmap_mask_right[] = { - MKCOLOUR(0x000000FF), - MKCOLOUR(0x0000FFFF), - MKCOLOUR(0x00FFFFFF), -}; - /* each tile has 4 x pixels and 1 y pixel */ static GetSmallMapPixels *_smallmap_draw_procs[] = { @@ -554,32 +549,87 @@ class SmallMapWindow : public Window int32 scroll_x; int32 scroll_y; - int32 subscroll; + uint8 refresh; + /** + * zoom level of the smallmap. + * May be something between -ZOOM_LVL_MAX and +ZOOM_LVL_MAX. Negative zoom levels are zoom in. + */ + ZoomLevel zoom; + static const int COLUMN_WIDTH = 119; static const int MIN_LEGEND_HEIGHT = 6 * 7; + /** size of left and right borders of the smallmap window */ + static const int SPACING_SIDE = 2; + + /** size of top border (and title bar) of the smallmap window */ + static const int SPACING_TOP = 16; + + /* The order of calculations when remapping is _very_ important as it introduces rounding errors. + * Everything has to be done just like when drawing the background otherwise the rounding errors are + * different on the background and on the overlay which creates "jumping" behaviour. This means: + * 1. UnScaleByZoom + * 2. divide by TILE_SIZE + * 3. subtract or add things or RemapCoords + * Note: + * We can't divide scroll_{x|y} by TILE_SIZE before scaling as that would mean we can only scroll full tiles. + */ + /** - * Remap a map's tile X coordinate (TileX(TileIndex)) to - * a location on this smallmap. - * @param tile_x the tile's X coordinate. + * remap coordinates on the main map into coordinates on the smallmap + * @param pos_x X position on the main map + * @param pos_y Y position on the main map + * @return Point in the smallmap + */ + inline Point RemapPlainCoords(int pos_x, int pos_y) + { + return RemapCoords( + RemapX(pos_x), + RemapY(pos_y), + 0 + ); + } + + /** + * remap a tile coordinate into coordinates on the smallmap + * @param tile the tile to be remapped + * @return Point with coordinates of the tile's upper left corner in the smallmap + */ + inline Point RemapTileCoords(TileIndex tile) + { + return RemapPlainCoords(TileX(tile) * TILE_SIZE, TileY(tile) * TILE_SIZE); + } + + /** + * scale a coordinate from the main map into the smallmap dimension + * @param pos coordinate to be scaled + * @return scaled coordinate + */ + inline int UnScalePlainCoord(int pos) + { + return UnScaleByZoomLower(pos, this->zoom) / TILE_SIZE; + } + + /** + * Remap a map X coordinate to a location on this smallmap. + * @param pos_x the tile's X coordinate. * @return the X coordinate to draw on. */ - inline int RemapX(int tile_x) + inline int RemapX(int pos_x) { - return tile_x - this->scroll_x / TILE_SIZE; + return UnScalePlainCoord(pos_x) - UnScalePlainCoord(this->scroll_x); } /** - * Remap a map's tile Y coordinate (TileY(TileIndex)) to - * a location on this smallmap. - * @param tile_y the tile's Y coordinate. + * Remap a map Y coordinate to a location on this smallmap. + * @param pos_y the tile's Y coordinate. * @return the Y coordinate to draw on. */ - inline int RemapY(int tile_y) + inline int RemapY(int pos_y) { - return tile_y - this->scroll_y / TILE_SIZE; + return UnScalePlainCoord(pos_y) - UnScalePlainCoord(this->scroll_y); } /** @@ -591,11 +641,10 @@ class SmallMapWindow : public Window * @param yc The Y coordinate of the first tile in the column * @param pitch Number of pixels to advance in the screen buffer each time a pixel is written. * @param reps Number of lines to draw - * @param mask ? * @param proc Pointer to the colour function * @see GetSmallMapPixels(TileIndex) */ - inline void DrawSmallMapStuff(void *dst, uint xc, uint yc, int pitch, int reps, uint32 mask, GetSmallMapPixels *proc) + inline void DrawSmallMapStuff(void *dst, uint xc, uint yc, int pitch, int reps, GetSmallMapPixels *proc) { Blitter *blitter = BlitterFactoryBase::GetCurrentBlitter(); void *dst_ptr_abs_end = blitter->MoveTo(_screen.dst_ptr, 0, _screen.height); @@ -604,12 +653,14 @@ class SmallMapWindow : public Window do { /* check if the tile (xc,yc) is within the map range */ uint min_xy = _settings_game.construction.freeform_edges ? 1 : 0; - if (IsInsideMM(xc, min_xy, MapMaxX()) && IsInsideMM(yc, min_xy, MapMaxY())) { + uint x = ScaleByZoomLower(xc, this->zoom); + uint y = ScaleByZoomLower(yc, this->zoom); + if (IsInsideMM(x, min_xy, MapMaxX()) && IsInsideMM(y, min_xy, MapMaxY())) { /* check if the dst pointer points to a pixel inside the screen buffer */ if (dst < _screen.dst_ptr) continue; if (dst >= dst_ptr_abs_end) continue; - uint32 val = proc(TileXY(xc, yc)) & mask; + uint32 val = proc(TileXY(x, y)); uint8 *val8 = (uint8 *)&val; if (dst <= dst_ptr_end) { @@ -629,6 +680,51 @@ class SmallMapWindow : public Window } while (xc++, yc++, dst = blitter->MoveTo(dst, pitch, 0), --reps != 0); } + /** + * draws a vehivle in the smallmap if it's in the selected drawing area. + * @param dpi the part of the smallmap to be drawn into + * @param v the vehicle to be drawn + */ + void DrawVehicle(DrawPixelInfo *dpi, Vehicle *v) + { + Blitter *blitter = BlitterFactoryBase::GetCurrentBlitter(); + int scale = 1; + if (this->zoom < 0) { + scale = 1 << (-this->zoom); + } + + /* Remap into flat coordinates. */ + Point pt = RemapTileCoords(v->tile); + + int x = pt.x - dpi->left; + int y = pt.y - dpi->top; + + /* Check if rhombus is inside bounds */ + if ((x + 2 * scale < 0) || //left + (y + 2 * scale < 0) || //top + (x - 2 * scale >= dpi->width) || //right + (y - 2 * scale >= dpi->height)) { //bottom + return; + } + + byte colour = (this->map_type == SMT_VEHICLES) ? _vehicle_type_colours[v->type] : 0xF; + + /* Draw rhombus */ + for (int dy = 0; dy < scale; dy++) { + for (int dx = 0; dx < scale; dx++) { + pt = RemapCoords(-dx, -dy, 0); + if (IsInsideMM(y + pt.y, 0, dpi->height)) { + if (IsInsideMM(x + pt.x, 0, dpi->width)) { + blitter->SetPixel(dpi->dst_ptr, x + pt.x, y + pt.y, colour); + } + if (IsInsideMM(x + pt.x + 1, 0, dpi->width)) { + blitter->SetPixel(dpi->dst_ptr, x + pt.x + 1, y + pt.y, colour); + } + } + } + } + } + public: /** * Draws the small map. @@ -672,54 +768,63 @@ public: } } - int tile_x = this->scroll_x / TILE_SIZE; - int tile_y = this->scroll_y / TILE_SIZE; + int tile_x = UnScalePlainCoord(this->scroll_x); + int tile_y = UnScalePlainCoord(this->scroll_y); - int dx = dpi->left + this->subscroll; + int dx = dpi->left; tile_x -= dx / 4; tile_y += dx / 4; - dx &= 3; int dy = dpi->top; tile_x += dy / 2; tile_y += dy / 2; + /* prevent some artifacts when partially redrawing. + * I have no idea how this works. + */ + dx &= 3; + dx += 1; if (dy & 1) { tile_x++; dx += 2; - if (dx > 3) { - dx -= 4; - tile_x--; - tile_y++; - } } - void *ptr = blitter->MoveTo(dpi->dst_ptr, -dx - 4, 0); - int x = - dx - 4; + /** + * for some reason we have to start drawing at an X position <= -4 + * otherwise we get artifacts when partially redrawing. + * Make sure dx provides for that and update tile_x and tile_y accordingly. + */ + while(dx < 4) { + dx += 4; + tile_x++; + tile_y--; + } + + /* The map background is off by a little less than one tile in y direction compared to vehicles and signs. + * I have no idea why this is the case. + * on zoom levels >= ZOOM_LVL_NORMAL this isn't visible as only full tiles can be shown + */ + dy = 0; + if (this->zoom < ZOOM_LVL_NORMAL) { + dy = UnScaleByZoomLower(2, this->zoom) - 2; + } + + /* correct the various problems mentioned above by moving the initial drawing pointer a little */ + void *ptr = blitter->MoveTo(dpi->dst_ptr, -dx, -dy); + int x = -dx; int y = 0; for (;;) { - uint32 mask = 0xFFFFFFFF; - /* distance from left edge */ if (x >= -3) { - if (x < 0) { - /* mask to use at the left edge */ - mask = _smallmap_mask_left[x + 3]; - } /* distance from right edge */ - int t = dpi->width - x; - if (t < 4) { - if (t <= 0) break; // exit loop - /* mask to use at the right edge */ - mask &= _smallmap_mask_right[t - 1]; - } + if (dpi->width - x <= 0) break; /* number of lines */ - int reps = (dpi->height - y + 1) / 2; + int reps = (dpi->height + 1 + dy) / 2; if (reps > 0) { - this->DrawSmallMapStuff(ptr, tile_x, tile_y, dpi->pitch * 2, reps, mask, _smallmap_draw_procs[this->map_type]); + this->DrawSmallMapStuff(ptr, tile_x, tile_y, dpi->pitch * 2, reps, _smallmap_draw_procs[this->map_type]); } } @@ -743,41 +848,7 @@ public: FOR_ALL_VEHICLES(v) { if (v->type != VEH_EFFECT && (v->vehstatus & (VS_HIDDEN | VS_UNCLICKABLE)) == 0) { - /* Remap into flat coordinates. */ - Point pt = RemapCoords( - this->RemapX(v->x_pos / TILE_SIZE), - this->RemapY(v->y_pos / TILE_SIZE), - 0); - x = pt.x; - y = pt.y; - - /* Check if y is out of bounds? */ - y -= dpi->top; - if (!IsInsideMM(y, 0, dpi->height)) continue; - - /* Default is to draw both pixels. */ - bool skip = false; - - /* Offset X coordinate */ - x -= this->subscroll + 3 + dpi->left; - - if (x < 0) { - /* if x+1 is 0, that means we're on the very left edge, - * and should thus only draw a single pixel */ - if (++x != 0) continue; - skip = true; - } else if (x >= dpi->width - 1) { - /* Check if we're at the very right edge, and if so draw only a single pixel */ - if (x != dpi->width - 1) continue; - skip = true; - } - - /* Calculate pointer to pixel and the colour */ - byte colour = (this->map_type == SMT_VEHICLES) ? _vehicle_type_colours[v->type] : 0xF; - - /* And draw either one or two pixels depending on clipping */ - blitter->SetPixel(dpi->dst_ptr, x, y, colour); - if (!skip) blitter->SetPixel(dpi->dst_ptr, x + 1, y, colour); + DrawVehicle(dpi, v); } } } @@ -787,11 +858,8 @@ public: FOR_ALL_TOWNS(t) { /* Remap the town coordinate */ - Point pt = RemapCoords( - this->RemapX(TileX(t->xy)), - this->RemapY(TileY(t->xy)), - 0); - x = pt.x - this->subscroll + 3 - (t->sign.width_small >> 1); + Point pt = RemapTileCoords(t->xy); + x = pt.x - (t->sign.width_small >> 1); y = pt.y; /* Check if the town sign is within bounds */ @@ -812,15 +880,11 @@ public: /* Draw map indicators */ Point pt = RemapCoords(this->scroll_x, this->scroll_y, 0); - x = vp->virtual_left - pt.x; - y = vp->virtual_top - pt.y; - int x2 = (x + vp->virtual_width) / TILE_SIZE; - int y2 = (y + vp->virtual_height) / TILE_SIZE; - x /= TILE_SIZE; - y /= TILE_SIZE; - - x -= this->subscroll; - x2 -= this->subscroll; + /* UnScale everything separately to produce the same rounding errors as when drawing the background */ + x = UnScalePlainCoord(vp->virtual_left) - UnScalePlainCoord(pt.x); + y = UnScalePlainCoord(vp->virtual_top) - UnScalePlainCoord(pt.y); + int x2 = x + UnScalePlainCoord(vp->virtual_width); + int y2 = y + UnScalePlainCoord(vp->virtual_height); DrawVertMapIndicator(x, y, x, y2); DrawVertMapIndicator(x2, y, x2, y2); @@ -834,13 +898,47 @@ public: { ViewPort *vp = FindWindowById(WC_MAIN_WINDOW, 0)->viewport; - int x = ((vp->virtual_width - (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) * TILE_SIZE) / 2 + vp->virtual_left) / 4; - int y = ((vp->virtual_height - (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top ) * TILE_SIZE) / 2 + vp->virtual_top ) / 2 - TILE_SIZE * 2; - this->scroll_x = (y - x) & ~0xF; - this->scroll_y = (x + y) & ~0xF; + int zoomed_width = ScaleByZoom((this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) * TILE_SIZE, this->zoom); + int zoomed_height = ScaleByZoom((this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top) * TILE_SIZE, this->zoom); + int x = ((vp->virtual_width - zoomed_width) / 2 + vp->virtual_left); + int y = ((vp->virtual_height - zoomed_height) / 2 + vp->virtual_top); + this->scroll_x = (y * 2 - x) / 4; + this->scroll_y = (x + y * 2) / 4; this->SetDirty(); } + /** + * Zoom in the map by one level. + * @param cx horizontal coordinate of center point, relative to SM_WIDGET_MAP widget + * @param cy vertical coordinate of center point, relative to SM_WIDGET_MAP widget + */ + void ZoomIn(int cx, int cy) + { + if (this->zoom > -ZOOM_LVL_MAX) { + this->zoom--; + this->DoScroll(cx, cy); + this->SetWidgetDisabledState(SM_WIDGET_ZOOM_IN, this->zoom == -ZOOM_LVL_MAX); + this->EnableWidget(SM_WIDGET_ZOOM_OUT); + this->SetDirty(); + } + } + + /** + * Zoom out the map by one level. + * @param cx horizontal coordinate of center point, relative to SM_WIDGET_MAP widget + * @param cy vertical coordinate of center point, relative to SM_WIDGET_MAP widget + */ + void ZoomOut(int cx, int cy) + { + if (this->zoom < ZOOM_LVL_MAX) { + this->zoom++; + this->DoScroll(cx / -2, cy / -2); + this->EnableWidget(SM_WIDGET_ZOOM_IN); + this->SetWidgetDisabledState(SM_WIDGET_ZOOM_OUT, this->zoom == ZOOM_LVL_MAX); + this->SetDirty(); + } + } + void ResizeLegend() { Widget *legend = &this->widget[SM_WIDGET_LEGEND]; @@ -870,7 +968,7 @@ public: } } - SmallMapWindow(const WindowDesc *desc, int window_number) : Window(desc, window_number) + SmallMapWindow(const WindowDesc *desc, int window_number) : Window(desc, window_number), zoom(ZOOM_LVL_NORMAL) { this->LowerWidget(this->map_type + SM_WIDGET_CONTOUR); this->SetWidgetLoweredState(SM_WIDGET_TOGGLETOWNNAME, this->show_towns); @@ -952,12 +1050,28 @@ public: Point pt = RemapCoords(this->scroll_x, this->scroll_y, 0); Window *w = FindWindowById(WC_MAIN_WINDOW, 0); w->viewport->follow_vehicle = INVALID_VEHICLE; - w->viewport->dest_scrollpos_x = pt.x + ((_cursor.pos.x - this->left + 2) << 4) - (w->viewport->virtual_width >> 1); - w->viewport->dest_scrollpos_y = pt.y + ((_cursor.pos.y - this->top - 16) << 4) - (w->viewport->virtual_height >> 1); + int scaled_x_off = ScaleByZoom((_cursor.pos.x - this->left - this->SPACING_SIDE) * TILE_SIZE, this->zoom); + int scaled_y_off = ScaleByZoom((_cursor.pos.y - this->top - this->SPACING_TOP) * TILE_SIZE, this->zoom); + w->viewport->dest_scrollpos_x = pt.x + scaled_x_off - w->viewport->virtual_width / 2; + w->viewport->dest_scrollpos_y = pt.y + scaled_y_off - w->viewport->virtual_height / 2; this->SetDirty(); } break; + case SM_WIDGET_ZOOM_OUT: + this->ZoomOut( + (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) / 2, + (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top) / 2 + ); + SndPlayFx(SND_15_BEEP); + break; + case SM_WIDGET_ZOOM_IN: + this->ZoomIn( + (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) / 2, + (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top) / 2 + ); + SndPlayFx(SND_15_BEEP); + break; case SM_WIDGET_CONTOUR: // Show land contours case SM_WIDGET_VEHICLES: // Show vehicles case SM_WIDGET_INDUSTRIES: // Show industries @@ -1033,6 +1147,27 @@ public: } } + virtual void OnMouseWheel(int wheel) + { + /* Cursor position relative to window */ + int cx = _cursor.pos.x - this->left; + int cy = _cursor.pos.y - this->top; + + /* Is cursor over the map ? */ + if (IsInsideMM(cx, this->widget[SM_WIDGET_MAP].left, this->widget[SM_WIDGET_MAP].right + 1) && + IsInsideMM(cy, this->widget[SM_WIDGET_MAP].top, this->widget[SM_WIDGET_MAP].bottom + 1)) { + /* Cursor position relative to map */ + cx -= this->widget[SM_WIDGET_MAP].left; + cy -= this->widget[SM_WIDGET_MAP].top; + + if (wheel < 0) { + this->ZoomIn(cx, cy); + } else { + this->ZoomOut(cx, cy); + } + } + }; + virtual void OnRightClick(Point pt, int widget) { if (widget == SM_WIDGET_MAP) { @@ -1052,55 +1187,49 @@ public: virtual void OnScroll(Point delta) { _cursor.fix_at = true; + DoScroll(delta.x, delta.y); + this->SetDirty(); + } - int x = this->scroll_x; - int y = this->scroll_y; - - int sub = this->subscroll + delta.x; - - x -= (sub >> 2) << 4; - y += (sub >> 2) << 4; - sub &= 3; - - x += (delta.y >> 1) << 4; - y += (delta.y >> 1) << 4; - - if (delta.y & 1) { - x += TILE_SIZE; - sub += 2; - if (sub > 3) { - sub -= 4; - x -= TILE_SIZE; - y += TILE_SIZE; - } - } - - int hx = (this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left) / 2; - int hy = (this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top ) / 2; - int hvx = hx * -4 + hy * 8; - int hvy = hx * 4 + hy * 8; - if (x < -hvx) { - x = -hvx; - sub = 0; - } - if (x > (int)MapMaxX() * TILE_SIZE - hvx) { - x = MapMaxX() * TILE_SIZE - hvx; - sub = 0; - } - if (y < -hvy) { - y = -hvy; - sub = 0; + /** + * Do the actual scrolling, but don't fix the cursor or set the window dirty. + * @param dx x offset to scroll in screen dimension + * @param dy y offset to scroll in screen dimension + */ + void DoScroll(int dx, int dy) + { + /* divide as late as possible to avoid premature reduction to 0, which causes "jumpy" behaviour + * at the same time make sure this is the exact reverse function of the drawing methods in order to + * avoid map indicators shifting around: + * 1. add/subtract + * 2. * TILE_SIZE + * 3. scale + */ + int x = dy * 2 - dx; + int y = dx + dy * 2; + + /* round to next divisible by 4 to allow for smoother scrolling */ + int rem_x = abs(x % 4); + int rem_y = abs(y % 4); + if (rem_x != 0) { + x += x > 0 ? 4 - rem_x : rem_x - 4; } - if (y > (int)MapMaxY() * TILE_SIZE - hvy) { - y = MapMaxY() * TILE_SIZE - hvy; - sub = 0; + if (rem_y != 0) { + y += y > 0 ? 4 - rem_y : rem_y - 4; } - this->scroll_x = x; - this->scroll_y = y; - this->subscroll = sub; - - this->SetDirty(); + this->scroll_x += ScaleByZoomLower(x / 4 * TILE_SIZE, this->zoom); + this->scroll_y += ScaleByZoomLower(y / 4 * TILE_SIZE, this->zoom); + + /* enforce the screen limits */ + int hx = this->widget[SM_WIDGET_MAP].right - this->widget[SM_WIDGET_MAP].left; + int hy = this->widget[SM_WIDGET_MAP].bottom - this->widget[SM_WIDGET_MAP].top; + int hvx = ScaleByZoomLower(hy * 4 - hx * 2, this->zoom); + int hvy = ScaleByZoomLower(hx * 2 + hy * 4, this->zoom); + this->scroll_x = max(-hvx, this->scroll_x); + this->scroll_y = max(-hvy, this->scroll_y); + this->scroll_x = min(MapMaxX() * TILE_SIZE, this->scroll_x); + this->scroll_y = min(MapMaxY() * TILE_SIZE - hvy, this->scroll_y); } virtual void OnResize(Point delta)