Fractional GUI scale (0.5 step), fit menu to window, fix settings pages

- display.gui-scale is now a number: 0 (auto) or 0.5..4 in 0.5 steps
- explicit scale is no longer limited by window size (tiled windows);
  only an open menu page is shrunk to fit the window
- snap Batch2D vertices to pixels on fractional scale (fixes dots above text)
- pass app to settings_common via options (not visible inside modules)
- gui.get_scale() returns a number
This commit is contained in:
loki5512344 2026-10-03 15:28:03 +02:00
parent 33e132a231
commit 48d5d54b77
13 changed files with 76 additions and 41 deletions

View file

@ -44,6 +44,10 @@ void Batch2D::vertex(
float u, float v,
float r, float g, float b, float a
) {
if (pixelSnap > 0.0f) {
x = std::round(x * pixelSnap) / pixelSnap;
y = std::round(y * pixelSnap) / pixelSnap;
}
buffer[index].position = {x, y};
buffer[index].uv = {u * region.getWidth() + region.u1, v * region.getHeight() + region.v1};
buffer[index].color = {r, g, b, a};
@ -54,6 +58,9 @@ void Batch2D::vertex(
glm::vec2 uvpoint,
float r, float g, float b, float a
) {
if (pixelSnap > 0.0f) {
point = glm::round(point * pixelSnap) / pixelSnap;
}
buffer[index].position = point;
buffer[index].uv = {uvpoint.x * region.getWidth() + region.u1, uvpoint.y * region.getHeight() + region.v1};
buffer[index].color = {r, g, b, a};
@ -83,6 +90,10 @@ void Batch2D::setRegion(UVRegion region) {
this->region = region;
}
void Batch2D::setPixelSnap(float pixelsPerUnit) {
pixelSnap = pixelsPerUnit;
}
void Batch2D::point(float x, float y, float r, float g, float b, float a){
if (index + 6 >= capacity)
flush();

View file

@ -33,6 +33,8 @@ class Batch2D : public Flushable {
const Texture* currentTexture;
DrawPrimitive primitive = DrawPrimitive::triangle;
UVRegion region {0.0f, 0.0f, 1.0f, 1.0f};
/// @brief Vertex positions are rounded to 1/pixelSnap (0 - disabled)
float pixelSnap = 0.0f;
void setPrimitive(DrawPrimitive primitive);
@ -56,6 +58,11 @@ public:
void texture(const Texture* texture);
void untexture();
void setRegion(UVRegion region);
/// @brief Align vertices to the pixel grid (fixes artifacts of textured
/// quads on fractional UI scale)
/// @param pixelsPerUnit window pixels per one unit, 0 - disable
void setPixelSnap(float pixelsPerUnit);
void sprite(float x, float y, float w, float h, const UVRegion& region, glm::vec4 tint);
void sprite(float x, float y, float w, float h, int atlasRes, int index, glm::vec4 tint);
void sprite(float x, float y, float w, float h, float skew, int atlasRes, int index, glm::vec4 tint);

View file

@ -1,6 +1,7 @@
#include "GUI.hpp"
#include <algorithm>
#include <cmath>
#include <utility>
#include "assets/Assets.hpp"
@ -103,16 +104,28 @@ int GUI::calcMaxScale(const glm::uvec2& viewport) {
return maxFit;
}
int GUI::calcScale(const glm::uvec2& viewport) const {
int maxFit = calcMaxScale(viewport);
int value = engine.getSettings().display.guiScale.get();
if (value <= 0) {
return maxFit; // auto
float GUI::calcScale(const glm::uvec2& viewport) const {
float value = engine.getSettings().display.guiScale.get();
float result = value <= 0.0f
? static_cast<float>(calcMaxScale(viewport)) // auto
: value;
// while a menu page is open, it must fit the window
auto& page = menu->getCurrent();
if (page.panel) {
auto size = page.panel->getSize();
if (size.x > 0.0f && size.y > 0.0f) {
float fit = std::min(viewport.x / size.x, viewport.y / size.y);
fit = std::floor(fit * 2.0f) / 2.0f; // step 0.5
result = std::min(result, std::max(fit, 0.5f));
}
}
return std::min(value, maxFit);
// keep UI size at least 1 unit
float limit = static_cast<float>(std::min(viewport.x, viewport.y));
return std::max(0.5f, std::min(result, std::max(limit, 0.5f)));
}
int GUI::getScale() const {
float GUI::getScale() const {
return scale;
}
@ -122,8 +135,8 @@ int GUI::getMaxScale() const {
CursorState GUI::getCursor() const {
auto cursor = input.getCursor();
cursor.pos /= static_cast<float>(scale);
cursor.delta /= static_cast<float>(scale);
cursor.pos /= scale;
cursor.delta /= scale;
return cursor;
}
@ -282,7 +295,7 @@ void GUI::actFocused() {
void GUI::act(float delta, const glm::uvec2& vp) {
scale = calcScale(vp);
container->setSize(glm::vec2(vp) / static_cast<float>(scale));
container->setSize(glm::vec2(vp) / scale);
for (auto& pair : frames) {
pair.second->act(delta);
}
@ -318,11 +331,11 @@ void GUI::postAct() {
void GUI::draw(const DrawContext& pctx, Assets& assets) {
auto ctx = pctx.sub(batch2D.get());
ctx.setUiScale(static_cast<float>(scale));
ctx.setUiScale(scale);
// viewport in UI units
const glm::vec2 viewport =
glm::vec2(ctx.getViewport()) / static_cast<float>(scale);
glm::vec2(ctx.getViewport()) / scale;
auto& page = menu->getCurrent();
if (page.panel) {
@ -341,6 +354,9 @@ void GUI::draw(const DrawContext& pctx, Assets& assets) {
uishader->uniformMatrix("u_projview", uicamera->getProjView());
batch2D->begin();
// fractional scale puts quad edges at pixel centers, which makes
// atlas sampling bleed into neighbour glyphs
batch2D->setPixelSnap(scale != std::floor(scale) ? scale : 0.0f);
for (auto& [outputTexture, frame] : frames) {
frame->updateOutput(assets);
frame->draw(ctx, assets);
@ -382,6 +398,7 @@ void GUI::draw(const DrawContext& pctx, Assets& assets) {
batch2D->setColor(0, 255, 0);
batch2D->lineRect(pos.x, pos.y, size.x-1, size.y-1);
}
batch2D->setPixelSnap(0.0f);
}
std::shared_ptr<UINode> GUI::getFocused() const {

View file

@ -87,7 +87,7 @@ namespace gui {
vec2supplier cursorLocator;
/// @brief Current GUI scale (UI units -> window pixels)
int scale = 1;
float scale = 1.0f;
float tooltipTimer = 0.0f;
float doubleClickTimer = 0.0f;
@ -96,8 +96,7 @@ namespace gui {
bool debug = false;
static int calcMaxScale(const glm::uvec2& viewport);
int calcScale(const glm::uvec2& viewport) const;
CursorState getScaledCursor() const;
float calcScale(const glm::uvec2& viewport) const;
void actMouse(Frame& frame, float delta, const CursorState& cursor);
void actFocused();
@ -133,9 +132,9 @@ namespace gui {
void postAct();
/// @brief Get current GUI scale: window pixels per one UI unit
int getScale() const;
float getScale() const;
/// @brief Get maximal GUI scale that fits the current window size
/// @brief Get GUI scale used in auto mode (fits the current window size)
int getMaxScale() const;
/// @brief Get cursor state in UI coordinates

View file

@ -1068,7 +1068,7 @@ static int l_gui_getviewport(lua::State* L) {
}
static int l_gui_get_scale(lua::State* L) {
return lua::pushinteger(L, engine->getGUI().getScale());
return lua::pushnumber(L, engine->getGUI().getScale());
}
static int l_gui_get_max_scale(lua::State* L) {

View file

@ -41,8 +41,8 @@ struct DisplaySettings {
FlagSetting limitFpsIconified {false};
/// @brief Adaptive framerate in menu (experimental)
FlagSetting adaptiveFpsInMenu {false};
/// @brief GUI scale factor (0 - auto, 1..4 - integer scale limited by window size)
IntegerSetting guiScale {0, 0, 4};
/// @brief GUI scale factor (0 - auto, 0.5..4 - explicit scale)
NumberSetting guiScale {0.0f, 0.0f, 4.0f};
};
struct ChunksSettings {