Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement GUI scaling for HighDPI displays and accessibility (SDL2 only) #1594

Merged
merged 31 commits into from
Aug 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
8e216d6
Add type GuiScale
falbrechtskirchinger Jul 23, 2023
00ba706
Implement GUI scaling (backend-agnostic)
falbrechtskirchinger Jul 5, 2023
870167d
Implement GUI scaling (SDL2)
falbrechtskirchinger Jul 5, 2023
060f8cb
Add setGuiScalePercent() stub to WinAPI backend
falbrechtskirchinger Jul 5, 2023
cf2b043
Implement setGuiScalePercent() in mockup driver
falbrechtskirchinger Jul 23, 2023
db5edaf
Add GUI scale setting
falbrechtskirchinger Jul 5, 2023
22aec23
Add GUI scale to the options screen
falbrechtskirchinger Jul 5, 2023
8ec48c6
Enable changing GUI scale using Ctrl+mouse wheel
falbrechtskirchinger Jul 5, 2023
f65589f
Apply saved GUI scale on game start
falbrechtskirchinger Jul 5, 2023
a47a3b2
Store unscaled render size
falbrechtskirchinger Jul 6, 2023
fb59f77
Add function for calculating GUI scale range
falbrechtskirchinger Jul 6, 2023
de80e43
Generate GUI scale percentages dynamically
falbrechtskirchinger Jul 6, 2023
d4a6273
Add automatic GUI scale selection
falbrechtskirchinger Jul 6, 2023
c7c8b73
Move cursor after GUI scale change
falbrechtskirchinger Jul 8, 2023
9b4cc5d
Add function returning DPI scale
falbrechtskirchinger Jul 9, 2023
a8f34ac
Decouple GUI scaling from game world scaling
falbrechtskirchinger Jul 9, 2023
c89d5d5
Enable HighDPI support in SDL2
falbrechtskirchinger Jul 9, 2023
d0efcbc
Show actual GUI scale percentage next to "Auto"
falbrechtskirchinger Jul 9, 2023
cf8b6d1
Fix spurious hover events
falbrechtskirchinger Jul 9, 2023
3532cb7
Hardcode minimum GUI scale of 100%
falbrechtskirchinger Jul 9, 2023
aa6a993
Generate GUI scale percentages in fixed increments
falbrechtskirchinger Aug 12, 2023
e5d6315
Fix cropped screenshots
falbrechtskirchinger Jul 14, 2023
2ce1ca0
Fix line drawing not honoring GUI scale
falbrechtskirchinger Jul 14, 2023
13af85d
Remove setGuiScaleInternal()
falbrechtskirchinger Jul 23, 2023
08a606d
Warn user if GUI scaling isn't supported
falbrechtskirchinger Jul 23, 2023
5d60752
Refactor handling of GUI scale and size changes
falbrechtskirchinger Aug 10, 2023
75b58c9
Add test for GUI scale translations
falbrechtskirchinger Jul 6, 2023
731d33d
Add test for GUI scale range calculation
falbrechtskirchinger Jul 6, 2023
d40cf72
Test functional equivalence of GuiScale overloads
falbrechtskirchinger Aug 10, 2023
868a7f4
Add unit test for getDpiScale()
falbrechtskirchinger Aug 12, 2023
0a39833
Add unit test for re-calculation of auto GUI scale
falbrechtskirchinger Aug 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions extras/videoDrivers/SDL2/VideoSDL2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,20 @@ bool VideoSDL2::CreateScreen(const std::string& title, const VideoMode& size, bo
int wndPos = SDL_WINDOWPOS_CENTERED;

const auto requestedSize = fullscreen ? FindClosestVideoMode(size) : size;
unsigned commonFlags = SDL_WINDOW_OPENGL;

#if SDL_VERSION_ATLEAST(2, 0, 1)
commonFlags |= SDL_WINDOW_ALLOW_HIGHDPI;
#endif

window = SDL_CreateWindow(title.c_str(), wndPos, wndPos, requestedSize.width, requestedSize.height,
SDL_WINDOW_OPENGL | (fullscreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_RESIZABLE));
commonFlags | (fullscreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_RESIZABLE));

// Fallback to non-fullscreen
if(!window && fullscreen)
{
window = SDL_CreateWindow(title.c_str(), wndPos, wndPos, requestedSize.width, requestedSize.height,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
commonFlags | SDL_WINDOW_RESIZABLE);
}

if(!window)
Expand Down Expand Up @@ -352,7 +357,7 @@ bool VideoSDL2::MessageLoop()
break;
}
case SDL_MOUSEBUTTONDOWN:
mouse_xy.pos = Position(ev.button.x, ev.button.y);
mouse_xy.pos = getGuiScale().screenToView(Position(ev.button.x, ev.button.y));

if(/*!mouse_xy.ldown && */ ev.button.button == SDL_BUTTON_LEFT)
{
Expand All @@ -366,7 +371,7 @@ bool VideoSDL2::MessageLoop()
}
break;
case SDL_MOUSEBUTTONUP:
mouse_xy.pos = Position(ev.button.x, ev.button.y);
mouse_xy.pos = getGuiScale().screenToView(Position(ev.button.x, ev.button.y));

if(/*mouse_xy.ldown &&*/ ev.button.button == SDL_BUTTON_LEFT)
{
Expand All @@ -393,7 +398,7 @@ bool VideoSDL2::MessageLoop()
}
break;
case SDL_MOUSEMOTION:
mouse_xy.pos = Position(ev.motion.x, ev.motion.y);
mouse_xy.pos = getGuiScale().screenToView(Position(ev.motion.x, ev.motion.y));
CallBack->Msg_MouseMove(mouse_xy);
break;
}
Expand Down Expand Up @@ -433,8 +438,9 @@ OpenGL_Loader_Proc VideoSDL2::GetLoaderFunction() const

void VideoSDL2::SetMousePos(Position pos)
{
const auto screenPos = getGuiScale().viewToScreen(pos);
mouse_xy.pos = pos;
SDL_WarpMouseInWindow(window, pos.x, pos.y);
SDL_WarpMouseInWindow(window, screenPos.x, screenPos.y);
}

KeyEvent VideoSDL2::GetModKeyState() const
Expand Down
3 changes: 3 additions & 0 deletions extras/videoDrivers/WinAPI/WinAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ class VideoWinAPI final : public VideoDriver
/// Funktion zum Setzen der Mauskoordinaten.
void SetMousePos(Position pos) override;

// GUI scaling is not implemented by this backend
void setGuiScalePercent(unsigned /* percent */) override {}

/// Get state of the modifier keys
KeyEvent GetModKeyState() const override;

Expand Down
69 changes: 69 additions & 0 deletions libs/driver/include/driver/GuiScale.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (C) 2005 - 2023 Settlers Freaks (sf-team at siedler25.org)
//
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include "Point.h"
#include "helpers/mathFuncs.h"
#include <type_traits>

/// Holds the range of supported GUI scale percentages
struct GuiScaleRange
{
unsigned minPercent; ///< Minimum supported GUI scale in percent
unsigned maxPercent; ///< Maximum supported GUI scale in percent
unsigned recommendedPercent; ///< Recommended GUI scale in percent
};

/// Represents the scale factors – precomputed from a percentage – to translate between screen and view dimensions
class GuiScale
{
public:
constexpr GuiScale(unsigned percent) noexcept
: percent_(percent), scaleViewToScreen_(percent_ / 100.f), scaleScreenToView_(1.f / scaleViewToScreen_)
{}

constexpr unsigned percent() const noexcept { return percent_; }
constexpr float scaleFactor() const noexcept { return scaleViewToScreen_; }
constexpr float invScaleFactor() const noexcept { return scaleScreenToView_; }

/// Translate a scalar from screen to view space
template<typename R = float>
constexpr R screenToView(float val) const noexcept
{
if constexpr(std::is_integral_v<R>)
return helpers::iround<R>(val * scaleScreenToView_);
else
return static_cast<R>(val * scaleScreenToView_);
}

/// Translate a point from screen to view space
template<typename R = Position, typename T>
constexpr R screenToView(Point<T> pt) const noexcept
{
return R(PointF(pt) * scaleScreenToView_);
}

/// Translate a scalar from view to screen space
template<typename R = float>
constexpr R viewToScreen(float val) const noexcept
{
if constexpr(std::is_integral_v<R>)
return helpers::iround<R>(val * scaleViewToScreen_);
else
return static_cast<R>(val * scaleViewToScreen_);
}

/// Translate a point from view to screen space
template<typename R = Position, typename T>
constexpr R viewToScreen(Point<T> pt) const noexcept
{
return R(PointF(pt) * scaleViewToScreen_);
}

private:
unsigned percent_; ///< GUI scaling factor in percent
float scaleViewToScreen_; ///< Decimal GUI scaling factor
float scaleScreenToView_; ///< Inverse of the decimal GUI scaling factor
};
18 changes: 15 additions & 3 deletions libs/driver/include/driver/VideoDriver.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ class VideoDriver : public IVideoDriver
bool GetMouseStateR() const override;

VideoMode GetWindowSize() const override final { return windowSize_; }
Extent GetRenderSize() const override final { return renderSize_; }
Extent GetRenderSize() const override final { return scaledRenderSize_; }
bool IsFullscreen() const override final { return isFullscreen_; }

float getDpiScale() const override final { return dpiScale_; }

const GuiScale& getGuiScale() const override final { return guiScale_; }
void setGuiScalePercent(unsigned percent) override;

GuiScaleRange getGuiScaleRange() const override;

/// prüft auf Initialisierung.
bool IsInitialized() const override final { return initialized; }
bool IsOpenGL() const override { return true; }
Expand All @@ -46,6 +53,11 @@ class VideoDriver : public IVideoDriver
bool isFullscreen_; /// Vollbild an/aus?
private:
// cached as possibly used often
VideoMode windowSize_;
Extent renderSize_;
VideoMode windowSize_; ///< Size of the window or fullscreen resolution
Extent renderSize_; ///< Size of the renderable surface
Extent scaledRenderSize_; ///< Size of the projection clipping area

float dpiScale_; ///< Scale factor required to convert "normal" DPI to the display DPI
GuiScale guiScale_; ///< Scale factor applied to the user interface
bool autoGuiScale_; ///< Whether the recommended GUI scale is used
};
13 changes: 13 additions & 0 deletions libs/driver/include/driver/VideoInterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#pragma once

#include "GuiScale.h"
#include "KeyEvent.h"
#include "Point.h"
#include "VideoMode.h"
Expand Down Expand Up @@ -63,6 +64,18 @@ class BOOST_SYMBOL_VISIBLE IVideoDriver
virtual Extent GetRenderSize() const = 0;
virtual bool IsFullscreen() const = 0;

/// Get the factor required to scale "normal" DPI to the display DPI
virtual float getDpiScale() const = 0;

/// Get the scale applied to the user interface
virtual const GuiScale& getGuiScale() const = 0;

/// Set the scale applied to the user interface in percent
virtual void setGuiScalePercent(unsigned percent) = 0;

/// Get minimum, maximum, and recommended GUI scale percentages for the current window and render size
virtual GuiScaleRange getGuiScaleRange() const = 0;

/// Get state of the modifier keys
virtual KeyEvent GetModKeyState() const = 0;

Expand Down
47 changes: 46 additions & 1 deletion libs/driver/src/VideoDriver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "driver/VideoDriver.h"
#include "commonDefines.h"
#include "driver/VideoDriverLoaderInterface.h"
#include "helpers/mathFuncs.h"
#include <algorithm>
#include <stdexcept>

Expand All @@ -17,7 +19,8 @@ IVideoDriver::~IVideoDriver() = default;
* @param[in] CallBack DriverCallback für Rückmeldungen.
*/
VideoDriver::VideoDriver(VideoDriverLoaderInterface* CallBack)
: CallBack(CallBack), initialized(false), isFullscreen_(false), renderSize_(0, 0)
: CallBack(CallBack), initialized(false), isFullscreen_(false), renderSize_(0, 0), scaledRenderSize_(0, 0),
dpiScale_(1.f), guiScale_(100), autoGuiScale_(false)
{
std::fill(keyboard.begin(), keyboard.end(), false);
}
Expand Down Expand Up @@ -73,4 +76,46 @@ void VideoDriver::SetNewSize(VideoMode windowSize, Extent renderSize)
{
windowSize_ = windowSize;
renderSize_ = renderSize;

const auto ratioXY = PointF(renderSize_) / PointF(windowSize_.width, windowSize_.height);
dpiScale_ = (ratioXY.x + ratioXY.y) / 2.f; // use the average ratio of both axes

if(autoGuiScale_)
guiScale_ = GuiScale(getGuiScaleRange().recommendedPercent);

scaledRenderSize_ = guiScale_.screenToView<Extent>(renderSize);
}

void VideoDriver::setGuiScalePercent(unsigned percent)
{
autoGuiScale_ = (percent == 0);
if(autoGuiScale_)
percent = getGuiScaleRange().recommendedPercent;

if(guiScale_.percent() == percent)
return;

// translate current mouse position to screen space
const auto screenPos = guiScale_.viewToScreen(mouse_xy.pos);

guiScale_ = GuiScale(percent);
scaledRenderSize_ = guiScale_.screenToView<Extent>(renderSize_);
CallBack->WindowResized();

// move cursor to new position in view space
// must happen after window resize event to avoid drawing spurious hover events
mouse_xy.pos = guiScale_.screenToView(screenPos);
CallBack->Msg_MouseMove(mouse_xy);
}

GuiScaleRange VideoDriver::getGuiScaleRange() const
{
constexpr auto min = 100u;
const auto recommended = std::max(min, helpers::iround<unsigned>(dpiScale_ * 100.f));
const auto maxScaleXY = renderSize_ / PointF(800.f, 600.f);
const auto maxScale = std::min(maxScaleXY.x, maxScaleXY.y);
// if the window shrinks below its minimum size of 800x600, max can be smaller than recommended
const auto max = std::max(helpers::iround<unsigned>(maxScale * 100.f), recommended);

return GuiScaleRange{min, max, recommended};
}
1 change: 1 addition & 0 deletions libs/s25main/GameManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ bool GameManager::Start()
return false;
videoDriver_.setTargetFramerate(settings_.video.framerate);
videoDriver_.SetMouseWarping(settings_.global.smartCursor);
videoDriver_.setGuiScalePercent(settings_.video.guiScale);

/// Audiodriver laden
if(!audioDriver_.LoadDriver(settings_.driver.audio))
Expand Down
3 changes: 3 additions & 0 deletions libs/s25main/Settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ void Settings::LoadDefaults()
video.framerate = 0; // Special value for HW vsync
video.vbo = true;
video.shared_textures = true;
video.guiScale = 0; // special value indicating automatic selection
// }

// language
Expand Down Expand Up @@ -228,6 +229,7 @@ void Settings::Load()
video.framerate = iniVideo->getValue("framerate", 0);
video.vbo = iniVideo->getBoolValue("vbo");
video.shared_textures = iniVideo->getBoolValue("shared_textures");
video.guiScale = iniVideo->getValue("gui_scale", 0);
// };

if(video.fullscreenSize.width == 0 || video.fullscreenSize.height == 0 || video.windowedSize.width == 0
Expand Down Expand Up @@ -412,6 +414,7 @@ void Settings::Save()
iniVideo->setValue("framerate", video.framerate);
iniVideo->setValue("vbo", video.vbo);
iniVideo->setValue("shared_textures", video.shared_textures);
iniVideo->setValue("gui_scale", video.guiScale);
// };

// language
Expand Down
1 change: 1 addition & 0 deletions libs/s25main/Settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class Settings : public Singleton<Settings, SingletonPolicies::WithLongevity>
bool fullscreen;
bool vbo;
bool shared_textures;
unsigned guiScale; ///< UI scaling in percent; 0 indicates automatic selection
} video;

struct
Expand Down
5 changes: 3 additions & 2 deletions libs/s25main/WindowManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -781,8 +781,9 @@ void WindowManager::SetActiveWindow(Window& wnd)

void WindowManager::TakeScreenshot() const
{
libsiedler2::PixelBufferBGRA buffer(curRenderSize.x, curRenderSize.y);
glReadPixels(0, 0, curRenderSize.x, curRenderSize.y, GL_BGRA, GL_UNSIGNED_BYTE, buffer.getPixelPtr());
const auto windowSize = VIDEODRIVER.GetWindowSize();
libsiedler2::PixelBufferBGRA buffer(windowSize.width, windowSize.height);
glReadPixels(0, 0, windowSize.width, windowSize.height, GL_BGRA, GL_UNSIGNED_BYTE, buffer.getPixelPtr());
flipVertical(buffer);
const bfs::path outFilepath =
RTTRCONFIG.ExpandPath(s25::folders::screenshots) / (s25util::Time::FormatTime("%Y-%m-%d_%H-%i-%s") + ".bmp");
Expand Down
Loading