diff --git a/doc/en/scripting/builtins/libgui.md b/doc/en/scripting/builtins/libgui.md index 3dc10612b..e6febc9db 100644 --- a/doc/en/scripting/builtins/libgui.md +++ b/doc/en/scripting/builtins/libgui.md @@ -16,6 +16,17 @@ gui.str(text: str, context: str) -> str -- Returns size of the main container (window). gui.get_viewport() -> {int, int} +-- Returns current GUI scale: window pixels per one UI unit (0.5..4). +-- Set by the display.gui-scale setting (auto mode fits the window size). +gui.get_scale() -> number + +-- Returns GUI scale used in auto mode (fits the current window size). +gui.get_max_scale() -> int + +-- Returns cursor position in UI units (unlike input.get_mouse_pos, +-- which returns window pixels). Use it to position UI elements. +gui.get_cursor_pos() -> {number, number} + -- Returns environment (global variables table) of the specified document. gui.get_env(document: str) -> table ``` diff --git a/doc/en/scripting/builtins/libinput.md b/doc/en/scripting/builtins/libinput.md index 9cf754d33..c2f7c858d 100644 --- a/doc/en/scripting/builtins/libinput.md +++ b/doc/en/scripting/builtins/libinput.md @@ -55,7 +55,8 @@ end, document.root) input.get_mouse_pos() --> {int, int} ``` -Returns cursor screen position. +Returns cursor screen position in window pixels. +To get position in UI units (affected by GUI scale) use `gui.get_cursor_pos()`. ```lua input.get_mouse_delta() --> {int, int} diff --git a/doc/ru/scripting/builtins/libgui.md b/doc/ru/scripting/builtins/libgui.md index 51ac7307f..0085501c2 100644 --- a/doc/ru/scripting/builtins/libgui.md +++ b/doc/ru/scripting/builtins/libgui.md @@ -31,6 +31,17 @@ gui.str(text: string, context: string) -> string -- Возвращает размер главного контейнера (окна). gui.get_viewport() -> {number, number} +-- Возвращает текущий масштаб интерфейса: пикселей окна в одной единице UI (0.5..4). +-- Задаётся настройкой display.gui-scale (в режиме Авто подбирается под размер окна). +gui.get_scale() -> number + +-- Возвращает масштаб интерфейса, который выбирается в режиме Авто (помещается в текущее окно). +gui.get_max_scale() -> int + +-- Возвращает позицию курсора в единицах UI (в отличие от input.get_mouse_pos, +-- возвращающей пиксели окна). Используйте для позиционирования элементов UI. +gui.get_cursor_pos() -> {number, number} + -- Возвращает окружение (глобальные переменные) указанного документа. gui.get_env(document: string) -> table diff --git a/doc/ru/scripting/builtins/libinput.md b/doc/ru/scripting/builtins/libinput.md index f3ac4b699..d1ef05af8 100644 --- a/doc/ru/scripting/builtins/libinput.md +++ b/doc/ru/scripting/builtins/libinput.md @@ -29,7 +29,8 @@ input.set_enabled(bindname: string, flag: boolean) ## Курсор мыши ```lua --- Возвращает позицию курсора на экране. +-- Возвращает позицию курсора на экране в пикселях окна. +-- Для позиции в единицах UI (с учётом масштаба) используйте gui.get_cursor_pos(). input.get_mouse_pos() -> {number, number} -- Возращает дельту позиции курсора. diff --git a/res/layouts/pages/settings_audio.xml.lua b/res/layouts/pages/settings_audio.xml.lua index 97628d1e8..d27c3c4e1 100644 --- a/res/layouts/pages/settings_audio.xml.lua +++ b/res/layouts/pages/settings_audio.xml.lua @@ -1,28 +1,7 @@ -function create_trackbar_setting(id, name, step, postfix) - local info = app.get_setting_info(id) - postfix = postfix or "" - document.root:add(gui.template("track_setting", { - id=id, - name=gui.str(name, "settings"), - value=app.get_setting(id), - min=info.min, - max=info.max, - step=step, - postfix=postfix - })) - update_trackbar_label(app.get_setting(id), id, name, postfix) -end - -function update_trackbar_label(x, id, name, postfix) - app.set_setting(id, x) - -- updating label - document[id..".L"].text = string.format( - "%s: %s%s", - gui.str(name, "settings"), - app.str_setting(id), - postfix - ) -end +local settings = require "core:settings_common".new(document, {app=app}) +-- templates call these functions by name, so they must be global +create_trackbar_setting = settings.create_trackbar_setting +update_trackbar_label = settings.update_trackbar_label function update_checkbox_setting(id, value) app.set_setting(id, value) @@ -41,12 +20,8 @@ function update_checkbox_setting(id, value) end end -function create_checkbox(id, name, tooltip) - tooltip = tooltip or '' - document.root:add(string.format( - "%s", - id, app.str_setting(id), gui.str(tooltip, "settings"), gui.str(name, "settings") - )) +local function create_audio_checkbox(id, name, tooltip) + settings.create_checkbox(id, name, tooltip, "update_checkbox_setting") update_checkbox_setting(id, app.get_setting(id)) end @@ -78,6 +53,7 @@ function on_open() create_trackbar_setting("audio.volume-ui", "UI Sounds", 0.01) create_trackbar_setting("audio.volume-ambient", "Ambient", 0.01) create_trackbar_setting("audio.volume-music", "Music", 0.01) + create_trackbar_setting("audio.volume-contrast", "Volume Contrast", 0.01, "", "audio.volume-contrast.tooltip") document.root:add("") document.root:add("".. diff --git a/res/layouts/pages/settings_graphics.xml.lua b/res/layouts/pages/settings_graphics.xml.lua index ed249b0f3..6e645b184 100644 --- a/res/layouts/pages/settings_graphics.xml.lua +++ b/res/layouts/pages/settings_graphics.xml.lua @@ -1,39 +1,8 @@ -function create_trackbar_setting(id, name, step, postfix, tooltip, changeonrelease) - local info = app.get_setting_info(id) - postfix = postfix or "" - tooltip = tooltip or "" - changeonrelease = changeonrelease or "" - document.root:add(gui.template("track_setting", { - id=id, - name=gui.str(name, "settings"), - value=app.get_setting(id), - min=info.min, - max=info.max, - step=step, - postfix=postfix, - tooltip=tooltip, - changeonrelease=changeonrelease - })) - update_trackbar_label(app.get_setting(id), id, name, postfix) -end - -function update_trackbar_label(x, id, name, postfix) - -- updating label - document[id..".L"].text = string.format( - "%s: %s%s", - gui.str(name, "settings"), - app.str_setting(id), - postfix - ) -end - -function create_checkbox(id, name, tooltip) - tooltip = tooltip or '' - document.root:add(string.format( - "%s", - id, app.str_setting(id), gui.str(tooltip, "settings"), gui.str(name, "settings") - )) -end +local settings = require "core:settings_common".new(document, {app=app}) +-- templates call these functions by name, so they must be global +create_trackbar_setting = settings.create_trackbar_setting +update_trackbar_label = settings.update_trackbar_label +create_checkbox = settings.create_checkbox function on_open() create_trackbar_setting("chunks.load-distance", "Load Distance", 1) diff --git a/res/layouts/pages/settings_reset.xml.lua b/res/layouts/pages/settings_reset.xml.lua index 59583eefa..efba528d9 100644 --- a/res/layouts/pages/settings_reset.xml.lua +++ b/res/layouts/pages/settings_reset.xml.lua @@ -20,6 +20,7 @@ function reset_audio() reset_setting("audio.volume-ui") reset_setting("audio.volume-ambient") reset_setting("audio.volume-music") + reset_setting("audio.volume-contrast") end function reset_display() diff --git a/res/modules/settings_common.lua b/res/modules/settings_common.lua new file mode 100644 index 000000000..886c4bfc8 --- /dev/null +++ b/res/modules/settings_common.lua @@ -0,0 +1,87 @@ +-- Common helpers for settings pages (res/layouts/pages/settings_*.xml.lua). +-- +-- Usage in a page script: +-- +-- local settings = require "core:settings_common".new(document, { +-- app = app, +-- tostring_overrides = {["display.framerate"] = function(x) ... end} +-- }) +-- -- templates call these functions by name, so they must be global +-- create_trackbar_setting = settings.create_trackbar_setting +-- update_trackbar_label = settings.update_trackbar_label +-- create_checkbox = settings.create_checkbox + +local settings_common = {} + +--- Create helpers bound to the page document. +--- @param document the page document +--- @param options table (optional) with fields: +--- app: the page's `app` library (not visible inside modules) +--- tostring_overrides: {[setting_id] = function(value) -> string} +function settings_common.new(document, options) + options = options or {} + local app = options.app + local tostring_overrides = options.tostring_overrides or {} + local this = {} + + --- Update trackbar label (called by the track_setting template) + function this.update_trackbar_label(x, id, name, postfix) + local str + local func = tostring_overrides[id] + if func then + str = func(x) + else + str = app.str_setting(id) + end + document[id..".L"].text = string.format( + "%s: %s%s", + gui.str(name, "settings"), + str, + postfix + ) + end + + --- Add a trackbar bound to the setting + --- @param changeonrelease apply value only when mouse is released + function this.create_trackbar_setting( + id, name, step, postfix, tooltip, changeonrelease + ) + local info = app.get_setting_info(id) + postfix = postfix or "" + tooltip = tooltip or "" + changeonrelease = changeonrelease or "" + document.root:add(gui.template("track_setting", { + id=id, + name=gui.str(name, "settings"), + value=app.get_setting(id), + min=info.min, + max=info.max, + step=step, + postfix=postfix, + tooltip=tooltip, + changeonrelease=changeonrelease + })) + this.update_trackbar_label(app.get_setting(id), id, name, postfix) + end + + --- Add a checkbox bound to the setting + --- @param consumer name of a global page function(id, value) called on + --- change instead of just setting the value (optional) + function this.create_checkbox(id, name, tooltip, consumer) + tooltip = tooltip or "" + local action + if consumer then + action = string.format("%s(\"%s\", x)", consumer, id) + else + action = string.format("app.set_setting(\"%s\", x)", id) + end + document.root:add(string.format( + "%s", + action, app.str_setting(id), gui.str(tooltip, "settings"), gui.str(name, "settings") + )) + end + + return this +end + +return settings_common diff --git a/res/texts/en_US.txt b/res/texts/en_US.txt index 294758a7e..476663adb 100644 --- a/res/texts/en_US.txt +++ b/res/texts/en_US.txt @@ -18,7 +18,9 @@ devtools.output=Output # Tooltips audio.acoustic-effects.tooltip=Acoustic effects may not be supported by your system +audio.volume-contrast.tooltip=Lower value - loud sounds are lowered to the level of quiet ones (night mode) audio.recording-enabled.tooltip=Enables audio recording features +display.gui-scale.tooltip=GUI size: Auto or 0.5-4. In auto mode the menu is reduced to fit the window graphics.enable-fog.tooltip=Enables fog graphics.gamma.tooltip=Lighting brightness curve graphics.backlight.tooltip=Backlight to prevent total darkness diff --git a/res/texts/ru_RU.txt b/res/texts/ru_RU.txt index 295d467af..e78147d26 100644 --- a/res/texts/ru_RU.txt +++ b/res/texts/ru_RU.txt @@ -12,6 +12,7 @@ Dependencies=Зависимости Description=Описание Converting world...=Выполняется конвертация мира... Unlimited=Неограниченно +Auto=Авто Chat=Чат Console=Консоль Log=Лог @@ -39,7 +40,9 @@ pack.remove-confirm=Удалить весь поставляемый паком/ # Подсказки audio.acoustic-effects.tooltip=Акустические эффекты могут не поддерживаться системой +audio.volume-contrast.tooltip=Меньше - громкие звуки приглушаются до уровня тихих (ночной режим) audio.recording-enabled.tooltip=Включает функции записи звука +display.gui-scale.tooltip=Размер интерфейса: Авто или 0.5-4. В режиме Авто меню уменьшается, если не помещается в окно graphics.enable-fog.tooltip=Отображать туман / дымку graphics.gamma.tooltip=Кривая яркости освещения graphics.backlight.tooltip=Подсветка, предотвращающая полную темноту @@ -95,6 +98,7 @@ settings.Fog Curve=Кривая Тумана settings.FOV=Поле Зрения settings.Fullscreen=Полный экран settings.Framerate=Частота кадров +settings.GUI Scale=Масштаб интерфейса settings.Gamma=Гамма settings.Language=Язык settings.Load Distance=Дистанция Загрузки @@ -102,6 +106,7 @@ settings.Load Speed=Скорость Загрузки settings.Master Volume=Общая Громкость settings.Mouse Sensitivity=Чувствительность Мыши settings.Music=Музыка +settings.Volume Contrast=Контраст громкости settings.Regular Sounds=Обычные Звуки settings.UI Sounds=Звуки Интерфейса settings.V-Sync=Вертикальная Синхронизация diff --git a/src/audio/audio.cpp b/src/audio/audio.cpp index ee35abeeb..4621bcb7e 100644 --- a/src/audio/audio.cpp +++ b/src/audio/audio.cpp @@ -1,5 +1,6 @@ #include "audio.hpp" +#include #include #include #include @@ -25,6 +26,64 @@ namespace { util::ObjectsKeeper objects_keeper {}; std::unique_ptr input_device = nullptr; static bool input_enabled = false; + + /// @brief Volume contrast setting value (1.0 - no compression) + float volume_contrast = 1.0f; + /// @brief RMS level (0..1) that louder sounds are lowered to + /// when contrast is 0 + constexpr float REFERENCE_RMS = 0.1f; + + struct LoudnessEntry { + std::weak_ptr pcm; + float rms; + }; + std::unordered_map loudness_cache; +} + +/// @brief Calculate root mean square level of PCM data +/// @return level in range [0.0, 1.0] +static float calc_rms(const PCM& pcm) { + double sum = 0.0; + size_t count = 0; + if (pcm.bitsPerSample == 16) { + count = pcm.data.size() / sizeof(int16_t); + auto samples = reinterpret_cast(pcm.data.data()); + for (size_t i = 0; i < count; i++) { + double value = samples[i] / 32768.0; + sum += value * value; + } + } else { + count = pcm.data.size(); + for (size_t i = 0; i < count; i++) { + double value = (static_cast(pcm.data[i]) - 128) / 128.0; + sum += value * value; + } + } + return count ? static_cast(std::sqrt(sum / count)) : 0.0f; +} + +/// @brief Get volume multiplier for a sound according to volume contrast +/// setting. Loud sounds are lowered, quiet ones are never amplified +static float get_contrast_gain(const Sound* sound) { + if (volume_contrast >= 1.0f) { + return 1.0f; + } + auto pcm = sound->getPCM(); + if (pcm == nullptr) { + return 1.0f; + } + float rms; + auto found = loudness_cache.find(sound); + if (found != loudness_cache.end() && found->second.pcm.lock() == pcm) { + rms = found->second.rms; + } else { + rms = calc_rms(*pcm); + loudness_cache[sound] = {pcm, rms}; + } + if (rms <= REFERENCE_RMS) { + return 1.0f; + } + return std::pow(REFERENCE_RMS / rms, 1.0f - volume_contrast); } Channel::Channel(std::string name, bool effects) @@ -196,6 +255,9 @@ void audio::initialize( audio::get_channel(channel.name)->setVolume(value * value); }, true)); } + objects_keeper.keepAlive(settings.volumeContrast.observe([](auto value) { + volume_contrast = value; + }, true)); objects_keeper.keepAlive(settings.acousticEffects.observe([=](bool value) { if (value) return; backend->setAcoustics(audio::Acoustics {}); @@ -395,7 +457,7 @@ speakerid_t audio::play( speakerid_t id = nextId++; speakers.try_emplace(id, std::move(speaker_ptr)); speaker->setPosition(position); - speaker->setVolume(volume); + speaker->setVolume(volume * get_contrast_gain(sound)); speaker->setPitch(pitch); speaker->setLoop(loop); speaker->setRelative(relative); diff --git a/src/frontend/hud.cpp b/src/frontend/hud.cpp index 6751b885c..38c1e4a58 100644 --- a/src/frontend/hud.cpp +++ b/src/frontend/hud.cpp @@ -370,7 +370,7 @@ void Hud::update(bool visible) { element.getNode()->setVisible(visible); } - const auto& windowSize = engine.getWindow().getSize(); + const auto windowSize = gui.getContainer()->getSize(); glm::vec2 caSize = contentAccessPanel->getSize(); contentAccessPanel->setVisible(inventoryView != nullptr && showContentPanel); contentAccessPanel->setSize(glm::vec2(caSize.x, windowSize.y)); @@ -626,7 +626,8 @@ void Hud::setDebug(bool flag) { void Hud::draw(const DrawContext& ctx){ const auto& viewport = ctx.getViewport(); - updateElementsPosition(viewport); + // UI layout is calculated in GUI units (window size / GUI scale) + updateElementsPosition(gui.getContainer()->getSize()); uicamera->setFov(viewport.y); uicamera->setAspectRatio(viewport.x / static_cast(viewport.y)); @@ -653,7 +654,7 @@ void Hud::draw(const DrawContext& ctx){ } } -void Hud::updateElementsPosition(const glm::uvec2& viewport) { +void Hud::updateElementsPosition(const glm::vec2& viewport) { if (inventoryOpen) { float caWidth = inventoryView && showContentPanel ? contentAccess->getSize().x @@ -698,7 +699,7 @@ void Hud::updateElementsPosition(const glm::uvec2& viewport) { } } if (exchangeSlot != nullptr) { - exchangeSlot->setPos(input.getCursor().pos); + exchangeSlot->setPos(gui.getCursor().pos); } hotbarView->setPos(glm::vec2(viewport.x / 2, viewport.y - 65)); hotbarView->setSelected(player.getChosenSlot()); diff --git a/src/frontend/hud.hpp b/src/frontend/hud.hpp index 6b5d62130..02fe62a62 100644 --- a/src/frontend/hud.hpp +++ b/src/frontend/hud.hpp @@ -130,7 +130,7 @@ class Hud : public util::ObjectsKeeper { std::shared_ptr createHotbar(); void processInput(bool visible); - void updateElementsPosition(const glm::uvec2& viewport); + void updateElementsPosition(const glm::vec2& viewport); void updateHotbarControl(); void cleanup(); diff --git a/src/graphics/core/Batch2D.cpp b/src/graphics/core/Batch2D.cpp index 33265b49f..ab09955fe 100644 --- a/src/graphics/core/Batch2D.cpp +++ b/src/graphics/core/Batch2D.cpp @@ -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(); diff --git a/src/graphics/core/Batch2D.hpp b/src/graphics/core/Batch2D.hpp index ec181a6fb..9154c9212 100644 --- a/src/graphics/core/Batch2D.hpp +++ b/src/graphics/core/Batch2D.hpp @@ -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); diff --git a/src/graphics/core/DrawContext.cpp b/src/graphics/core/DrawContext.cpp index 86e2131b1..81b7a4ab1 100644 --- a/src/graphics/core/DrawContext.cpp +++ b/src/graphics/core/DrawContext.cpp @@ -157,8 +157,12 @@ void DrawContext::setBlendMode(BlendMode mode) { set_blend_mode(mode); } +void DrawContext::setUiScale(float scale) { + uiScale = scale; +} + void DrawContext::setScissors(const glm::vec4& area) { - window.pushScissor(area); + window.pushScissor(area * uiScale); scissorsCount++; } diff --git a/src/graphics/core/DrawContext.hpp b/src/graphics/core/DrawContext.hpp index 7e568e337..e2e884d80 100644 --- a/src/graphics/core/DrawContext.hpp +++ b/src/graphics/core/DrawContext.hpp @@ -24,6 +24,7 @@ class DrawContext { BlendMode blendMode = BlendMode::normal; int scissorsCount = 0; float lineWidth = 1.0f; + float uiScale = 1.0f; std::array textures {}; public: DrawContext( @@ -46,5 +47,9 @@ public: void setBlendMode(BlendMode mode); void setScissors(const glm::vec4& area); void setLineWidth(float width); + + /// @brief Set scale of UI coordinates (UI units -> window pixels). + /// Affects setScissors only + void setUiScale(float scale); void useTexture(int target, const Bindable* texture); }; diff --git a/src/graphics/ui/GUI.cpp b/src/graphics/ui/GUI.cpp index a5f3010ff..d74c5bda6 100644 --- a/src/graphics/ui/GUI.cpp +++ b/src/graphics/ui/GUI.cpp @@ -1,6 +1,7 @@ #include "GUI.hpp" #include +#include #include #include "assets/Assets.hpp" @@ -88,13 +89,65 @@ void GUI::onAssetsLoad(Assets* assets) { assets->store(rootDocument, "core:root"); } +/// @brief Minimal UI size (in UI units) that GUI scale must keep, so +/// menus always fit the window. Larger scale values are limited to it +static constexpr glm::uvec2 MIN_UI_SIZE {640, 480}; +static constexpr int MAX_SCALE = 4; + +int GUI::calcMaxScale(const glm::uvec2& viewport) { + int maxFit = 1; + while (maxFit < MAX_SCALE && + viewport.x / (maxFit + 1) >= MIN_UI_SIZE.x && + viewport.y / (maxFit + 1) >= MIN_UI_SIZE.y) { + maxFit++; + } + return maxFit; +} + +float GUI::calcScale(const glm::uvec2& viewport) const { + float value = engine.getSettings().display.guiScale.get(); + float result = value <= 0.0f + ? static_cast(calcMaxScale(viewport)) // auto + : value; + + // explicitly set scale is applied as is; in auto mode an open menu page + // must fit the window + auto& page = menu->getCurrent(); + if (value <= 0.0f && 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)); + } + } + // keep UI size at least 1 unit + float limit = static_cast(std::min(viewport.x, viewport.y)); + return std::max(0.5f, std::min(result, std::max(limit, 0.5f))); +} + +float GUI::getScale() const { + return scale; +} + +int GUI::getMaxScale() const { + return calcMaxScale(engine.getWindow().getSize()); +} + +CursorState GUI::getCursor() const { + auto cursor = input.getCursor(); + cursor.pos /= scale; + cursor.delta /= scale; + return cursor; +} + void GUI::resetTooltip() { tooltipTimer = 0.0f; tooltip->setVisible(false); } void GUI::updateTooltip(float delta) { - const auto& cursor = input.getCursor(); + const auto cursor = getCursor(); if (hover == nullptr || !hover->isInside(cursor.pos)) { return resetTooltip(); } @@ -229,7 +282,7 @@ void GUI::actFocused() { focus->keyPressed(key); } - const auto& cursor = input.getCursor(); + const auto cursor = getCursor(); if (!cursor.locked) { if (input.clicked(Mousecode::BUTTON_1) && (input.jclicked(Mousecode::BUTTON_1) || cursor.delta.x || @@ -242,7 +295,8 @@ void GUI::actFocused() { } void GUI::act(float delta, const glm::uvec2& vp) { - container->setSize(vp); + scale = calcScale(vp); + container->setSize(glm::vec2(vp) / scale); for (auto& pair : frames) { pair.second->act(delta); } @@ -250,7 +304,7 @@ void GUI::act(float delta, const glm::uvec2& vp) { updateTooltip(delta); - const auto& cursor = input.getCursor(); + const auto cursor = getCursor(); if (!cursor.locked && activeFrame) { actMouse(*activeFrame, delta, cursor); } else { @@ -278,8 +332,11 @@ void GUI::postAct() { void GUI::draw(const DrawContext& pctx, Assets& assets) { auto ctx = pctx.sub(batch2D.get()); + ctx.setUiScale(scale); - auto& viewport = ctx.getViewport(); + // viewport in UI units + const glm::vec2 viewport = + glm::vec2(ctx.getViewport()) / scale; auto& page = menu->getCurrent(); if (page.panel) { @@ -289,15 +346,18 @@ void GUI::draw(const DrawContext& pctx, Assets& assets) { panel->cropToContent(); } } - menu->setPos((glm::vec2(viewport) - menu->getSize()) / 2.0f); + menu->setPos((viewport - menu->getSize()) / 2.0f); uicamera->setFov(viewport.y); - uicamera->setAspectRatio(viewport.x / static_cast(viewport.y)); + uicamera->setAspectRatio(viewport.x / viewport.y); auto uishader = assets.get("ui"); uishader->use(); 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); @@ -339,6 +399,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 GUI::getFocused() const { diff --git a/src/graphics/ui/GUI.hpp b/src/graphics/ui/GUI.hpp index 997b67bea..4a0d610f9 100644 --- a/src/graphics/ui/GUI.hpp +++ b/src/graphics/ui/GUI.hpp @@ -86,12 +86,18 @@ namespace gui { std::unordered_map> frames; vec2supplier cursorLocator; + /// @brief Current GUI scale (UI units -> window pixels) + float scale = 1.0f; + float tooltipTimer = 0.0f; float doubleClickTimer = 0.0f; float doubleClickDelay = 0.5f; bool doubleClicked = false; bool debug = false; + static int calcMaxScale(const glm::uvec2& viewport); + float calcScale(const glm::uvec2& viewport) const; + void actMouse(Frame& frame, float delta, const CursorState& cursor); void actFocused(); void updateTooltip(float delta); @@ -125,6 +131,15 @@ namespace gui { void postAct(); + /// @brief Get current GUI scale: window pixels per one UI unit + float getScale() const; + + /// @brief Get GUI scale used in auto mode (fits the current window size) + int getMaxScale() const; + + /// @brief Get cursor state in UI coordinates + CursorState getCursor() const; + /// @brief Add element to the main container /// @param node UI element void add(std::shared_ptr node); diff --git a/src/graphics/ui/elements/InventoryView.cpp b/src/graphics/ui/elements/InventoryView.cpp index 614d4c913..cc385309f 100644 --- a/src/graphics/ui/elements/InventoryView.cpp +++ b/src/graphics/ui/elements/InventoryView.cpp @@ -78,7 +78,7 @@ void InventoryBuilder::addGrid( if (addpanel) { auto panel = std::make_shared(gui, glm::vec2(width, height)); - view->setColor(glm::vec4(0.122f, 0.122f, 0.122f, 0.878f)); + view->setColor(InventoryView::DEFAULT_COLOR); view->add(panel, pos); } diff --git a/src/graphics/ui/elements/InventoryView.hpp b/src/graphics/ui/elements/InventoryView.hpp index fbfe3eee9..c58fdf0ee 100644 --- a/src/graphics/ui/elements/InventoryView.hpp +++ b/src/graphics/ui/elements/InventoryView.hpp @@ -160,6 +160,8 @@ namespace gui { static const int SLOT_INTERVAL = 4; static const int SLOT_SIZE = ITEM_ICON_SIZE; + /// @brief Default background color (can be overridden by 'color' attribute) + static inline const glm::vec4 DEFAULT_COLOR {0.122f, 0.122f, 0.122f, 0.878f}; }; class InventoryBuilder { diff --git a/src/graphics/ui/elements/ModelViewer.cpp b/src/graphics/ui/elements/ModelViewer.cpp index 435f3340d..6e27614bd 100644 --- a/src/graphics/ui/elements/ModelViewer.cpp +++ b/src/graphics/ui/elements/ModelViewer.cpp @@ -57,7 +57,7 @@ void ModelViewer::act(float delta) { grabbing = true; } if (grabbing && input.clicked(Mousecode::BUTTON_3)) { - auto cursor = input.getCursor(); + auto cursor = gui.getCursor(); if (input.pressed(Keycode::LEFT_SHIFT)) { center -= camera.right * (cursor.delta.x / size.x) * distance; center += camera.up * (cursor.delta.y / size.y) * distance; diff --git a/src/graphics/ui/gui_xml.cpp b/src/graphics/ui/gui_xml.cpp index 41a603cea..8ad9f9c5a 100644 --- a/src/graphics/ui/gui_xml.cpp +++ b/src/graphics/ui/gui_xml.cpp @@ -860,7 +860,7 @@ static std::shared_ptr read_inventory( UiXmlReader& reader, const xml::xmlelement& element ) { auto view = std::make_shared(reader.getGUI()); - view->setColor(glm::vec4(0.122f, 0.122f, 0.122f, 0.878f)); // TODO: fixme + view->setColor(InventoryView::DEFAULT_COLOR); reader.addIgnore("slot"); reader.addIgnore("slots-grid"); reader.readUINode(reader, element, *view); diff --git a/src/io/settings_io.cpp b/src/io/settings_io.cpp index e72cb009d..07e3d5f86 100644 --- a/src/io/settings_io.cpp +++ b/src/io/settings_io.cpp @@ -44,6 +44,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) { builder.add("volume-ui", &settings.audio.volumeUI); builder.add("volume-ambient", &settings.audio.volumeAmbient); builder.add("volume-music", &settings.audio.volumeMusic); + builder.add("volume-contrast", &settings.audio.volumeContrast); builder.add("input-device", &settings.audio.inputDevice); builder.add("acoustic-effects", &settings.audio.acousticEffects); @@ -55,6 +56,7 @@ SettingsHandler::SettingsHandler(EngineSettings& settings) { builder.add("limit-fps-iconified", &settings.display.limitFpsIconified); builder.add("window-mode", &settings.display.windowMode); builder.add("adaptive-menu-fps", &settings.display.adaptiveFpsInMenu); + builder.add("gui-scale", &settings.display.guiScale); builder.addSection("camera"); builder.add("sensitivity", &settings.camera.sensitivity); diff --git a/src/logic/scripting/lua/libs/libgui.cpp b/src/logic/scripting/lua/libs/libgui.cpp index dc4806d00..340afd918 100644 --- a/src/logic/scripting/lua/libs/libgui.cpp +++ b/src/logic/scripting/lua/libs/libgui.cpp @@ -1067,6 +1067,20 @@ static int l_gui_getviewport(lua::State* L) { return lua::pushvec2(L, engine->getGUI().getContainer()->getSize()); } +static int l_gui_get_scale(lua::State* L) { + return lua::pushnumber(L, engine->getGUI().getScale()); +} + +static int l_gui_get_max_scale(lua::State* L) { + return lua::pushinteger(L, engine->getGUI().getMaxScale()); +} + +static int l_gui_get_cursor_pos(lua::State* L) { + if (engine->isHeadless()) + return 0; + return lua::pushvec2(L, engine->getGUI().getCursor().pos); +} + static int l_gui_clear_markup(lua::State* L) { auto lang = lua::require_string(L, 1); std::string text = lua::require_string(L, 2); @@ -1247,6 +1261,9 @@ static int l_screenshot(lua::State* L) { const luaL_Reg guilib[] = { {"screenshot", lua::wrap}, {"get_viewport", lua::wrap}, + {"get_scale", lua::wrap}, + {"get_max_scale", lua::wrap}, + {"get_cursor_pos", lua::wrap}, {"getattr", lua::wrap}, {"setattr", lua::wrap}, {"get_env", lua::wrap}, diff --git a/src/settings.hpp b/src/settings.hpp index 320d0fc8d..88567f386 100644 --- a/src/settings.hpp +++ b/src/settings.hpp @@ -17,6 +17,9 @@ struct AudioSettings { NumberSetting volumeUI {1.0f, 0.0f, 1.0f, setting_format::percent}; NumberSetting volumeAmbient {1.0f, 0.0f, 1.0f, setting_format::percent}; NumberSetting volumeMusic {1.0f, 0.0f, 1.0f, setting_format::percent}; + /// @brief Volume contrast: 100% - sounds are played as is, + /// 0% - loud sounds are lowered to the level of quiet ones ("night mode") + NumberSetting volumeContrast {1.0f, 0.0f, 1.0f, setting_format::percent}; StringSetting inputDevice {""}; @@ -38,6 +41,8 @@ struct DisplaySettings { FlagSetting limitFpsIconified {false}; /// @brief Adaptive framerate in menu (experimental) FlagSetting adaptiveFpsInMenu {false}; + /// @brief GUI scale factor (0 - auto, 0.5..4 - explicit scale) + NumberSetting guiScale {0.0f, 0.0f, 4.0f}; }; struct ChunksSettings {