From 6a1d22b2c516abbb9ce9670dedff47451317706f Mon Sep 17 00:00:00 2001 From: grorp Date: Sun, 24 Nov 2024 11:33:39 +0100 Subject: [PATCH] Implement an editor to customize the touchscreen controls (#14933) - The editor is accessible via the pause menu and the settings menu. - Buttons can be moved via drag & drop. - Buttons can be added/removed. The grid menu added by #14918 is used to show all buttons not included in the layout. - Custom layouts are responsive and adapt to changed screen size / DPI / hud_scaling. - The layout is saved as JSON in the "touch_layout" setting. --- builtin/mainmenu/settings/dlg_settings.lua | 18 + doc/menu_lua_api.md | 1 + src/client/game.cpp | 21 +- src/client/hud.cpp | 1 + src/defaultsettings.cpp | 1 + src/gui/CMakeLists.txt | 2 + src/gui/mainmenumanager.h | 7 + src/gui/touchcontrols.cpp | 203 ++++------- src/gui/touchcontrols.h | 81 ++--- src/gui/touchscreeneditor.cpp | 404 +++++++++++++++++++++ src/gui/touchscreeneditor.h | 81 +++++ src/gui/touchscreenlayout.cpp | 345 ++++++++++++++++++ src/gui/touchscreenlayout.h | 104 ++++++ src/irr_gui_ptr.h | 28 ++ src/script/lua_api/l_mainmenu.cpp | 18 + src/script/lua_api/l_mainmenu.h | 2 + textures/base/pack/down.png | Bin 1618 -> 3647 bytes textures/base/pack/jump_btn.png | Bin 1633 -> 3625 bytes 18 files changed, 1122 insertions(+), 195 deletions(-) create mode 100644 src/gui/touchscreeneditor.cpp create mode 100644 src/gui/touchscreeneditor.h create mode 100644 src/gui/touchscreenlayout.cpp create mode 100644 src/gui/touchscreenlayout.h create mode 100644 src/irr_gui_ptr.h diff --git a/builtin/mainmenu/settings/dlg_settings.lua b/builtin/mainmenu/settings/dlg_settings.lua index ef28398b1..f1861879a 100644 --- a/builtin/mainmenu/settings/dlg_settings.lua +++ b/builtin/mainmenu/settings/dlg_settings.lua @@ -123,6 +123,22 @@ local function load() end, } + local touchscreen_layout = { + query_text = "Touchscreen layout", + requires = { + touchscreen = true, + }, + get_formspec = function(self, avail_w) + local btn_w = math.min(avail_w, 6) + return ("button[0,0;%f,0.8;btn_touch_layout;%s]"):format(btn_w, fgettext("Touchscreen layout")), 0.8 + end, + on_submit = function(self, fields) + if fields.btn_touch_layout then + core.show_touchscreen_layout() + end + end, + } + add_page({ id = "accessibility", title = fgettext_ne("Accessibility"), @@ -151,6 +167,8 @@ local function load() load_settingtypes() table.insert(page_by_id.controls_keyboard_and_mouse.content, 1, change_keys) + -- insert after "touch_controls" + table.insert(page_by_id.controls_touchscreen.content, 2, touchscreen_layout) do local content = page_by_id.graphics_and_audio_effects.content local idx = table.indexof(content, "enable_dynamic_shadows") diff --git a/doc/menu_lua_api.md b/doc/menu_lua_api.md index ae4afd998..b17e4b1c8 100644 --- a/doc/menu_lua_api.md +++ b/doc/menu_lua_api.md @@ -223,6 +223,7 @@ GUI * `core.set_clouds()` * `core.set_topleft_text(text)` * `core.show_keys_menu()` +* `core.show_touchscreen_layout()` * `core.show_path_select_dialog(formname, caption, is_file_select)` * shows a path select dialog * `formname` is base name of dialog response returned in fields diff --git a/src/client/game.cpp b/src/client/game.cpp index f9f175d1b..dc13095d1 100644 --- a/src/client/game.cpp +++ b/src/client/game.cpp @@ -26,6 +26,7 @@ #include "client/event_manager.h" #include "fontengine.h" #include "gui/touchcontrols.h" +#include "gui/touchscreeneditor.h" #include "itemdef.h" #include "log.h" #include "log_internal.h" @@ -144,6 +145,11 @@ struct LocalFormspecHandler : public TextDest return; } + if (fields.find("btn_touchscreen_layout") != fields.end()) { + g_gamecallback->touchscreenLayout(); + return; + } + if (fields.find("btn_exit_menu") != fields.end()) { g_gamecallback->disconnect(); return; @@ -1844,6 +1850,12 @@ inline bool Game::handleCallbacks() g_gamecallback->keyconfig_requested = false; } + if (g_gamecallback->touchscreenlayout_requested) { + (new GUITouchscreenLayout(guienv, guiroot, -1, + &g_menumgr, texture_src))->drop(); + g_gamecallback->touchscreenlayout_requested = false; + } + if (!g_gamecallback->show_open_url_dialog.empty()) { (void)make_irr(guienv, guiroot, -1, &g_menumgr, texture_src, g_gamecallback->show_open_url_dialog); @@ -4510,9 +4522,14 @@ void Game::showPauseMenu() << strgettext("Sound Volume") << "]"; } #endif - os << "button_exit[4," << (ypos++) << ";3,0.5;btn_key_config;" - << strgettext("Controls") << "]"; #endif + if (g_touchcontrols) { + os << "button_exit[4," << (ypos++) << ";3,0.5;btn_touchscreen_layout;" + << strgettext("Touchscreen Layout") << "]"; + } else { + os << "button_exit[4," << (ypos++) << ";3,0.5;btn_key_config;" + << strgettext("Controls") << "]"; + } os << "button_exit[4," << (ypos++) << ";3,0.5;btn_exit_menu;" << strgettext("Exit to Menu") << "]"; os << "button_exit[4," << (ypos++) << ";3,0.5;btn_exit_os;" diff --git a/src/client/hud.cpp b/src/client/hud.cpp index 6929d4c9e..e49327d30 100644 --- a/src/client/hud.cpp +++ b/src/client/hud.cpp @@ -24,6 +24,7 @@ #include "wieldmesh.h" #include "client/renderingengine.h" #include "client/minimap.h" +#include "client/texturesource.h" #include "gui/touchcontrols.h" #include "util/enriched_string.h" #include "irrlicht_changes/CGUITTFont.h" diff --git a/src/defaultsettings.cpp b/src/defaultsettings.cpp index 2142615e2..345e4a07b 100644 --- a/src/defaultsettings.cpp +++ b/src/defaultsettings.cpp @@ -542,6 +542,7 @@ void set_default_settings() settings->setDefault("keymap_sneak", "KEY_SHIFT"); #endif + settings->setDefault("touch_layout", ""); settings->setDefault("touchscreen_sensitivity", "0.2"); settings->setDefault("touchscreen_threshold", "20"); settings->setDefault("touch_long_tap_delay", "400"); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 04a03609d..876acab74 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -25,5 +25,7 @@ set(gui_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/modalMenu.cpp ${CMAKE_CURRENT_SOURCE_DIR}/profilergraph.cpp ${CMAKE_CURRENT_SOURCE_DIR}/touchcontrols.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/touchscreenlayout.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/touchscreeneditor.cpp PARENT_SCOPE ) diff --git a/src/gui/mainmenumanager.h b/src/gui/mainmenumanager.h index d5f43796d..9d10e3960 100644 --- a/src/gui/mainmenumanager.h +++ b/src/gui/mainmenumanager.h @@ -21,6 +21,7 @@ public: virtual void changeVolume() = 0; virtual void showOpenURLDialog(const std::string &url) = 0; virtual void signalKeyConfigChange() = 0; + virtual void touchscreenLayout() = 0; }; extern gui::IGUIEnvironment *guienv; @@ -133,6 +134,11 @@ public: keyconfig_changed = true; } + void touchscreenLayout() override + { + touchscreenlayout_requested = true; + } + void showOpenURLDialog(const std::string &url) override { show_open_url_dialog = url; @@ -142,6 +148,7 @@ public: bool changepassword_requested = false; bool changevolume_requested = false; bool keyconfig_requested = false; + bool touchscreenlayout_requested = false; bool shutdown_requested = false; bool keyconfig_changed = false; std::string show_open_url_dialog = ""; diff --git a/src/gui/touchcontrols.cpp b/src/gui/touchcontrols.cpp index d7e46787b..17352264d 100644 --- a/src/gui/touchcontrols.cpp +++ b/src/gui/touchcontrols.cpp @@ -5,6 +5,7 @@ // Copyright (C) 2024 grorp, Gregor Parzefall #include "touchcontrols.h" +#include "touchscreenlayout.h" #include "gettime.h" #include "irr_v2d.h" @@ -14,8 +15,10 @@ #include "client/guiscalingfilter.h" #include "client/keycode.h" #include "client/renderingengine.h" +#include "client/texturesource.h" #include "util/numeric.h" -#include "gettext.h" +#include "irr_gui_ptr.h" +#include "IGUIImage.h" #include "IGUIStaticText.h" #include "IGUIFont.h" #include @@ -26,59 +29,6 @@ TouchControls *g_touchcontrols; -static const char *button_image_names[] = { - "jump_btn.png", - "down.png", - "zoom.png", - "aux1_btn.png", - "overflow_btn.png", - - "fly_btn.png", - "noclip_btn.png", - "fast_btn.png", - "debug_btn.png", - "camera_btn.png", - "rangeview_btn.png", - "minimap_btn.png", - "", - - "chat_btn.png", - "inventory_btn.png", - "drop_btn.png", - "exit_btn.png", - - "joystick_off.png", - "joystick_bg.png", - "joystick_center.png", -}; - -// compare with GUIKeyChangeMenu::init_keys -static const char *button_titles[] = { - N_("Jump"), - N_("Sneak"), - N_("Zoom"), - N_("Aux1"), - N_("Overflow menu"), - - N_("Toggle fly"), - N_("Toggle noclip"), - N_("Toggle fast"), - N_("Toggle debug"), - N_("Change camera"), - N_("Range select"), - N_("Toggle minimap"), - N_("Toggle chat log"), - - N_("Chat"), - N_("Inventory"), - N_("Drop"), - N_("Exit"), - - N_("Joystick"), - N_("Joystick"), - N_("Joystick"), -}; - static void load_button_texture(IGUIImage *gui_button, const std::string &path, const recti &button_rect, ISimpleTextureSource *tsrc, video::IVideoDriver *driver) { @@ -268,10 +218,22 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc) m_long_tap_delay = g_settings->getU16("touch_long_tap_delay"); m_fixed_joystick = g_settings->getBool("fixed_virtual_joystick"); m_joystick_triggers_aux1 = g_settings->getBool("virtual_joystick_triggers_aux1"); + m_screensize = m_device->getVideoDriver()->getScreenSize(); - m_button_size = MYMIN(m_screensize.Y / 4.5f, - RenderingEngine::getDisplayDensity() * 65.0f * - g_settings->getFloat("hud_scaling")); + m_button_size = ButtonLayout::getButtonSize(m_screensize); + applyLayout(ButtonLayout::loadFromSettings()); +} + +void TouchControls::applyLayout(const ButtonLayout &layout) +{ + m_layout = layout; + + m_buttons.clear(); + m_overflow_btn = nullptr; + m_overflow_bg = nullptr; + m_overflow_buttons.clear(); + m_overflow_button_titles.clear(); + m_overflow_button_rects.clear(); // Initialize joystick display "button". // Joystick is placed on the bottom left of screen. @@ -298,47 +260,21 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc) m_joystick_btn_center = grab_gui_element(makeButtonDirect(joystick_center_id, recti(0, 0, m_button_size, m_button_size), false)); - // init jump button - addButton(m_buttons, jump_id, button_image_names[jump_id], - recti(m_screensize.X - 1.75f * m_button_size, - m_screensize.Y - m_button_size, - m_screensize.X - 0.25f * m_button_size, - m_screensize.Y)); + for (const auto &[id, meta] : m_layout.layout) { + if (!mayAddButton(id)) + continue; - // init sneak button - addButton(m_buttons, sneak_id, button_image_names[sneak_id], - recti(m_screensize.X - 3.25f * m_button_size, - m_screensize.Y - m_button_size, - m_screensize.X - 1.75f * m_button_size, - m_screensize.Y)); - - // init zoom button - addButton(m_buttons, zoom_id, button_image_names[zoom_id], - recti(m_screensize.X - 1.25f * m_button_size, - m_screensize.Y - 4 * m_button_size, - m_screensize.X - 0.25f * m_button_size, - m_screensize.Y - 3 * m_button_size)); - - // init aux1 button - if (!m_joystick_triggers_aux1) - addButton(m_buttons, aux1_id, button_image_names[aux1_id], - recti(m_screensize.X - 1.25f * m_button_size, - m_screensize.Y - 2.5f * m_button_size, - m_screensize.X - 0.25f * m_button_size, - m_screensize.Y - 1.5f * m_button_size)); - - // init overflow button - m_overflow_btn = grab_gui_element(makeButtonDirect(overflow_id, - recti(m_screensize.X - 1.25f * m_button_size, - m_screensize.Y - 5.5f * m_button_size, - m_screensize.X - 0.25f * m_button_size, - m_screensize.Y - 4.5f * m_button_size), true)); - - const static touch_gui_button_id overflow_buttons[] { - chat_id, inventory_id, drop_id, exit_id, - fly_id, noclip_id, fast_id, debug_id, camera_id, range_id, minimap_id, - toggle_chat_id, - }; + recti rect = m_layout.getRect(id, m_screensize, m_button_size, m_texturesource); + if (id == toggle_chat_id) + // Chat is shown by default, so chat_hide_btn.png is shown first. + addToggleButton(m_buttons, id, "chat_hide_btn.png", + "chat_show_btn.png", rect, true); + else if (id == overflow_id) + m_overflow_btn = grab_gui_element( + makeButtonDirect(id, rect, true)); + else + addButton(m_buttons, id, button_image_names[id], rect, true); + } IGUIStaticText *background = m_guienv->addStaticText(L"", recti(v2s32(0, 0), dimension2du(m_screensize))); @@ -346,32 +282,17 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc) background->setVisible(false); m_overflow_bg = grab_gui_element(background); - s32 cols = 4; - s32 rows = 3; - f32 screen_aspect = (f32)m_screensize.X / (f32)m_screensize.Y; - while ((s32)ARRLEN(overflow_buttons) > cols * rows) { - f32 aspect = (f32)cols / (f32)rows; - if (aspect > screen_aspect) - rows++; - else - cols++; - } - - v2s32 size(m_button_size, m_button_size); - v2s32 spacing(m_screensize.X / (cols + 1), m_screensize.Y / (rows + 1)); - v2s32 pos(spacing); - - for (auto id : overflow_buttons) { - if (id_to_keycode(id) == KEY_UNKNOWN) - continue; - - recti rect(pos - size / 2, dimension2du(size.X, size.Y)); - if (rect.LowerRightCorner.X > (s32)m_screensize.X) { - pos.X = spacing.X; - pos.Y += spacing.Y; - rect = recti(pos - size / 2, dimension2du(size.X, size.Y)); - } + auto overflow_buttons = m_layout.getMissingButtons(); + overflow_buttons.erase(std::remove_if( + overflow_buttons.begin(), overflow_buttons.end(), + [&](touch_gui_button_id id) { + // There's no sense in adding the overflow button to the overflow + // menu (also, it's impossible since it doesn't have a keycode). + return !mayAddButton(id) || id == overflow_id; + }), overflow_buttons.end()); + layout_button_grid(m_screensize, m_texturesource, overflow_buttons, + [&] (touch_gui_button_id id, v2s32 pos, recti rect) { if (id == toggle_chat_id) // Chat is shown by default, so chat_hide_btn.png is shown first. addToggleButton(m_overflow_buttons, id, "chat_hide_btn.png", @@ -379,27 +300,23 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc) else addButton(m_overflow_buttons, id, button_image_names[id], rect, false); - std::wstring str = wstrgettext(button_titles[id]); - IGUIStaticText *text = m_guienv->addStaticText(str.c_str(), recti()); - IGUIFont *font = text->getActiveFont(); - dimension2du dim = font->getDimension(str.c_str()); - dim = dimension2du(dim.Width * 1.25f, dim.Height * 1.25f); // avoid clipping - text->setRelativePosition(recti(pos.X - dim.Width / 2, pos.Y + size.Y / 2, - pos.X + dim.Width / 2, pos.Y + size.Y / 2 + dim.Height)); - text->setTextAlignment(EGUIA_CENTER, EGUIA_UPPERLEFT); + IGUIStaticText *text = m_guienv->addStaticText(L"", recti()); + make_button_grid_title(text, id, pos, rect); text->setVisible(false); m_overflow_button_titles.push_back(grab_gui_element(text)); rect.addInternalPoint(text->getRelativePosition().UpperLeftCorner); rect.addInternalPoint(text->getRelativePosition().LowerRightCorner); m_overflow_button_rects.push_back(rect); - - pos.X += spacing.X; - } + }); m_status_text = grab_gui_element( m_guienv->addStaticText(L"", recti(), false, false)); m_status_text->setVisible(false); + + // applyLayout can be called at any time, also e.g. while the overflow menu + // is open, so this is necessary to restore correct visibility. + updateVisibility(); } TouchControls::~TouchControls() @@ -407,6 +324,17 @@ TouchControls::~TouchControls() releaseAll(); } +bool TouchControls::mayAddButton(touch_gui_button_id id) +{ + if (!ButtonLayout::isButtonAllowed(id)) + return false; + if (id == aux1_id && m_joystick_triggers_aux1) + return false; + if (id != overflow_id && id_to_keycode(id) == KEY_UNKNOWN) + return false; + return true; +} + void TouchControls::addButton(std::vector &buttons, touch_gui_button_id id, const std::string &image, const recti &rect, bool visible) { @@ -701,6 +629,15 @@ void TouchControls::applyJoystickStatus() void TouchControls::step(float dtime) { + v2u32 screensize = m_device->getVideoDriver()->getScreenSize(); + s32 button_size = ButtonLayout::getButtonSize(screensize); + + if (m_screensize != screensize || m_button_size != button_size) { + m_screensize = screensize; + m_button_size = button_size; + applyLayout(m_layout); + } + // simulate keyboard repeats buttons_step(m_buttons, dtime, m_device->getVideoDriver(), m_receiver, m_texturesource); buttons_step(m_overflow_buttons, dtime, m_device->getVideoDriver(), m_receiver, m_texturesource); diff --git a/src/gui/touchcontrols.h b/src/gui/touchcontrols.h index cb3e8e8bf..701baba42 100644 --- a/src/gui/touchcontrols.h +++ b/src/gui/touchcontrols.h @@ -5,11 +5,8 @@ #pragma once -#include "IGUIStaticText.h" #include "irrlichttypes.h" -#include -#include -#include +#include "IEventReceiver.h" #include #include @@ -17,41 +14,29 @@ #include #include "itemdef.h" -#include "client/game.h" +#include "touchscreenlayout.h" #include "util/basic_macros.h" -#include "client/texturesource.h" namespace irr { class IrrlichtDevice; + namespace gui + { + class IGUIEnvironment; + class IGUIImage; + class IGUIStaticText; + } + namespace video + { + class IVideoDriver; + } } +class ISimpleTextureSource; using namespace irr::core; using namespace irr::gui; -// We cannot use irr_ptr for Irrlicht GUI elements we own. -// Option 1: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr -// constructor. -// -> We steal the reference owned by IGUIEnvironment and drop it later, -// causing the IGUIElement to be deleted while IGUIEnvironment still -// references it. -// Option 2: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr::grab. -// -> We add another reference and drop it later, but since IGUIEnvironment -// still references the IGUIElement, it is never deleted. -// To make IGUIEnvironment drop its reference to the IGUIElement, we have to call -// IGUIElement::remove, so that's what we'll do. -template -std::shared_ptr grab_gui_element(T *element) -{ - static_assert(std::is_base_of_v, - "grab_gui_element only works for IGUIElement"); - return std::shared_ptr(element, [](T *e) { - e->remove(); - }); -} - - enum class TapState { None, @@ -59,36 +44,6 @@ enum class TapState LongTap, }; -enum touch_gui_button_id -{ - jump_id = 0, - sneak_id, - zoom_id, - aux1_id, - overflow_id, - - // usually in the "settings bar" - fly_id, - noclip_id, - fast_id, - debug_id, - camera_id, - range_id, - minimap_id, - toggle_chat_id, - - // usually in the "rare controls bar" - chat_id, - inventory_id, - drop_id, - exit_id, - - // the joystick - joystick_off_id, - joystick_bg_id, - joystick_center_id, -}; - #define BUTTON_REPEAT_DELAY 0.5f #define BUTTON_REPEAT_INTERVAL 0.333f @@ -168,6 +123,9 @@ public: bool isStatusTextOverriden() { return m_overflow_open; } IGUIStaticText *getStatusText() { return m_status_text.get(); } + ButtonLayout getLayout() { return m_layout; } + void applyLayout(const ButtonLayout &layout); + private: IrrlichtDevice *m_device = nullptr; IGUIEnvironment *m_guienv = nullptr; @@ -233,13 +191,14 @@ private: void releaseAll(); // initialize a button + bool mayAddButton(touch_gui_button_id id); void addButton(std::vector &buttons, touch_gui_button_id id, const std::string &image, - const recti &rect, bool visible=true); + const recti &rect, bool visible); void addToggleButton(std::vector &buttons, touch_gui_button_id id, const std::string &image_1, const std::string &image_2, - const recti &rect, bool visible=true); + const recti &rect, bool visible); IGUIImage *makeButtonDirect(touch_gui_button_id id, const recti &rect, bool visible); @@ -268,6 +227,8 @@ private: bool m_place_pressed = false; u64 m_place_pressed_until = 0; + + ButtonLayout m_layout; }; extern TouchControls *g_touchcontrols; diff --git a/src/gui/touchscreeneditor.cpp b/src/gui/touchscreeneditor.cpp new file mode 100644 index 000000000..b30ce5a20 --- /dev/null +++ b/src/gui/touchscreeneditor.cpp @@ -0,0 +1,404 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 grorp, Gregor Parzefall + +#include "touchscreeneditor.h" +#include "touchcontrols.h" +#include "touchscreenlayout.h" + +#include "client/renderingengine.h" +#include "gettext.h" +#include "irr_gui_ptr.h" +#include "settings.h" + +#include "IGUIButton.h" +#include "IGUIFont.h" +#include "IGUIImage.h" +#include "IGUIStaticText.h" + +GUITouchscreenLayout::GUITouchscreenLayout(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr, ISimpleTextureSource *tsrc +): + GUIModalMenu(env, parent, id, menumgr), + m_tsrc(tsrc) +{ + if (g_touchcontrols) + m_layout = g_touchcontrols->getLayout(); + else + m_layout = ButtonLayout::loadFromSettings(); + + m_gui_help_text = grab_gui_element(Environment->addStaticText( + L"", core::recti(), false, false, this, -1)); + m_gui_help_text->setTextAlignment(EGUIA_CENTER, EGUIA_CENTER); + + m_gui_add_btn = grab_gui_element(Environment->addButton( + core::recti(), this, -1, wstrgettext("Add button").c_str())); + m_gui_reset_btn = grab_gui_element(Environment->addButton( + core::recti(), this, -1, wstrgettext("Reset").c_str())); + m_gui_done_btn = grab_gui_element(Environment->addButton( + core::recti(), this, -1, wstrgettext("Done").c_str())); + + m_gui_remove_btn = grab_gui_element(Environment->addButton( + core::recti(), this, -1, wstrgettext("Remove").c_str())); +} + +GUITouchscreenLayout::~GUITouchscreenLayout() +{ + ButtonLayout::clearTextureCache(); +} + +void GUITouchscreenLayout::regenerateGui(v2u32 screensize) +{ + DesiredRect = core::recti(0, 0, screensize.X, screensize.Y); + recalculateAbsolutePosition(false); + + s32 button_size = ButtonLayout::getButtonSize(screensize); + if (m_last_screensize != screensize || m_button_size != button_size) { + m_last_screensize = screensize; + m_button_size = button_size; + // Prevent interpolation when the layout scales. + clearGUIImages(); + } + + // Discard invalid selection. May happen when... + // 1. a button is removed. + // 2. adding a button fails and it disappears from the layout again. + if (m_selected_btn != touch_gui_button_id_END && + m_layout.layout.count(m_selected_btn) == 0) + m_selected_btn = touch_gui_button_id_END; + + if (m_mode == Mode::Add) + regenerateGUIImagesAddMode(screensize); + else + regenerateGUIImagesRegular(screensize); + regenerateMenu(screensize); +} + +void GUITouchscreenLayout::clearGUIImages() +{ + m_gui_images.clear(); + m_gui_images_target_pos.clear(); + + m_add_layout.layout.clear(); + m_add_button_titles.clear(); +} + +void GUITouchscreenLayout::regenerateGUIImagesRegular(v2u32 screensize) +{ + assert(m_mode != Mode::Add); + + auto old_gui_images = m_gui_images; + clearGUIImages(); + + for (const auto &[btn, meta] : m_layout.layout) { + core::recti rect = m_layout.getRect(btn, screensize, m_button_size, m_tsrc); + std::shared_ptr img; + + if (old_gui_images.count(btn) > 0) { + img = old_gui_images.at(btn); + // Update size, keep position. Position is interpolated in interpolateGUIImages. + img->setRelativePosition(core::recti( + img->getRelativePosition().UpperLeftCorner, rect.getSize())); + } else { + img = grab_gui_element(Environment->addImage(rect, this, -1)); + img->setImage(ButtonLayout::getTexture(btn, m_tsrc)); + img->setScaleImage(true); + } + + m_gui_images[btn] = img; + m_gui_images_target_pos[btn] = rect.UpperLeftCorner; + } +} + +void GUITouchscreenLayout::regenerateGUIImagesAddMode(v2u32 screensize) +{ + assert(m_mode == Mode::Add); + + clearGUIImages(); + + auto missing_buttons = m_layout.getMissingButtons(); + + layout_button_grid(screensize, m_tsrc, missing_buttons, + [&] (touch_gui_button_id btn, v2s32 pos, core::recti rect) { + auto img = grab_gui_element(Environment->addImage(rect, this, -1)); + img->setImage(ButtonLayout::getTexture(btn, m_tsrc)); + img->setScaleImage(true); + m_gui_images[btn] = img; + + ButtonMeta meta; + meta.setPos(pos, screensize, m_button_size); + m_add_layout.layout[btn] = meta; + + IGUIStaticText *text = Environment->addStaticText(L"", core::recti(), + false, false,this, -1); + make_button_grid_title(text, btn, pos, rect); + m_add_button_titles.push_back(grab_gui_element(text)); + }); +} + +void GUITouchscreenLayout::interpolateGUIImages() +{ + if (m_mode == Mode::Add) + return; + + for (auto &[btn, gui_image] : m_gui_images) { + bool interpolate = m_mode != Mode::Dragging || m_selected_btn != btn; + + v2s32 cur_pos_int = gui_image->getRelativePosition().UpperLeftCorner; + v2s32 tgt_pos_int = m_gui_images_target_pos.at(btn); + v2f cur_pos(cur_pos_int.X, cur_pos_int.Y); + v2f tgt_pos(tgt_pos_int.X, tgt_pos_int.Y); + + if (interpolate && cur_pos.getDistanceFrom(tgt_pos) > 2.0f) { + v2f pos = cur_pos.getInterpolated(tgt_pos, 0.5f); + gui_image->setRelativePosition(v2s32(core::round32(pos.X), core::round32(pos.Y))); + } else { + gui_image->setRelativePosition(tgt_pos_int); + } + } +} + +static void layout_menu_row(v2u32 screensize, + const std::vector> &row, + const std::vector> &full_row, bool bottom) +{ + s32 spacing = RenderingEngine::getDisplayDensity() * 4.0f; + + s32 btn_w = 0; + s32 btn_h = 0; + for (const auto &btn : full_row) { + IGUIFont *font = btn->getActiveFont(); + core::dimension2du dim = font->getDimension(btn->getText()); + btn_w = std::max(btn_w, (s32)(dim.Width * 1.5f)); + btn_h = std::max(btn_h, (s32)(dim.Height * 2.5f)); + } + + const s32 row_width = ((btn_w + spacing) * row.size()) - spacing; + s32 x = screensize.X / 2 - row_width / 2; + const s32 y = bottom ? screensize.Y - spacing - btn_h : spacing; + + for (const auto &btn : row) { + btn->setRelativePosition(core::recti( + v2s32(x, y), core::dimension2du(btn_w, btn_h))); + x += btn_w + spacing; + } +} + +void GUITouchscreenLayout::regenerateMenu(v2u32 screensize) +{ + bool have_selection = m_selected_btn != touch_gui_button_id_END; + + if (m_mode == Mode::Add) + m_gui_help_text->setText(wstrgettext("Start dragging a button to add. Tap outside to cancel.").c_str()); + else if (!have_selection) + m_gui_help_text->setText(wstrgettext("Tap a button to select it. Drag a button to move it.").c_str()); + else + m_gui_help_text->setText(wstrgettext("Tap outside to deselect.").c_str()); + + IGUIFont *font = m_gui_help_text->getActiveFont(); + core::dimension2du dim = font->getDimension(m_gui_help_text->getText()); + s32 height = dim.Height * 2.5f; + s32 pos_y = (m_mode == Mode::Add || have_selection) ? 0 : screensize.Y - height; + m_gui_help_text->setRelativePosition(core::recti( + v2s32(0, pos_y), + core::dimension2du(screensize.X, height))); + + bool normal_buttons_visible = m_mode != Mode::Add && !have_selection; + bool add_visible = normal_buttons_visible && !m_layout.getMissingButtons().empty(); + + m_gui_add_btn->setVisible(add_visible); + m_gui_reset_btn->setVisible(normal_buttons_visible); + m_gui_done_btn->setVisible(normal_buttons_visible); + + if (normal_buttons_visible) { + std::vector row1{m_gui_add_btn, m_gui_reset_btn, m_gui_done_btn}; + if (add_visible) { + layout_menu_row(screensize, row1, row1, false); + } else { + std::vector row1_reduced{m_gui_reset_btn, m_gui_done_btn}; + layout_menu_row(screensize, row1_reduced, row1, false); + } + } + + bool remove_visible = m_mode != Mode::Add && have_selection && + !ButtonLayout::isButtonRequired(m_selected_btn); + + m_gui_remove_btn->setVisible(remove_visible); + + if (remove_visible) { + std::vector row2{m_gui_remove_btn}; + layout_menu_row(screensize, row2, row2, true); + } +} + +void GUITouchscreenLayout::drawMenu() +{ + video::IVideoDriver *driver = Environment->getVideoDriver(); + + video::SColor bgcolor(140, 0, 0, 0); + video::SColor selection_color(255, 128, 128, 128); + video::SColor error_color(255, 255, 0, 0); + + driver->draw2DRectangle(bgcolor, AbsoluteRect, &AbsoluteClippingRect); + + // Done here instead of in OnPostRender to avoid drag&drop lagging behind + // input by one frame. + // Must be done before drawing selection rectangle. + interpolateGUIImages(); + + bool draw_selection = m_gui_images.count(m_selected_btn) > 0; + if (draw_selection) + driver->draw2DRectangle(selection_color, + m_gui_images.at(m_selected_btn)->getAbsolutePosition(), + &AbsoluteClippingRect); + + if (m_mode == Mode::Dragging) { + for (const auto &rect : m_error_rects) + driver->draw2DRectangle(error_color, rect, &AbsoluteClippingRect); + } + + IGUIElement::draw(); +} + +void GUITouchscreenLayout::updateDragState(v2u32 screensize, v2s32 mouse_movement) +{ + assert(m_mode == Mode::Dragging); + + core::recti rect = m_layout.getRect(m_selected_btn, screensize, m_button_size, m_tsrc); + rect += mouse_movement; + rect.constrainTo(core::recti(v2s32(0, 0), core::dimension2du(screensize))); + + ButtonMeta &meta = m_layout.layout.at(m_selected_btn); + meta.setPos(rect.getCenter(), screensize, m_button_size); + + rect = m_layout.getRect(m_selected_btn, screensize, m_button_size, m_tsrc); + + m_error_rects.clear(); + for (const auto &[other_btn, other_meta] : m_layout.layout) { + if (other_btn == m_selected_btn) + continue; + core::recti other_rect = m_layout.getRect(other_btn, screensize, m_button_size, m_tsrc); + if (other_rect.isRectCollided(rect)) + m_error_rects.push_back(other_rect); + } + if (m_error_rects.empty()) + m_last_good_layout = m_layout; +} + +bool GUITouchscreenLayout::OnEvent(const SEvent& event) +{ + if (event.EventType == EET_KEY_INPUT_EVENT) { + if (event.KeyInput.Key == KEY_ESCAPE && event.KeyInput.PressedDown) { + quitMenu(); + return true; + } + } + + core::dimension2du screensize = Environment->getVideoDriver()->getScreenSize(); + + if (event.EventType == EET_MOUSE_INPUT_EVENT) { + v2s32 mouse_pos = v2s32(event.MouseInput.X, event.MouseInput.Y); + + switch (event.MouseInput.Event) { + case EMIE_LMOUSE_PRESSED_DOWN: { + m_mouse_down = true; + m_last_mouse_pos = mouse_pos; + + IGUIElement *el = Environment->getRootGUIElement()->getElementFromPoint(mouse_pos); + // Clicking on nothing deselects. + m_selected_btn = touch_gui_button_id_END; + for (const auto &[btn, gui_image] : m_gui_images) { + if (el == gui_image.get()) { + m_selected_btn = btn; + break; + } + } + + if (m_mode == Mode::Add) { + if (m_selected_btn != touch_gui_button_id_END) { + m_mode = Mode::Dragging; + m_last_good_layout = m_layout; + m_layout.layout[m_selected_btn] = m_add_layout.layout.at(m_selected_btn); + updateDragState(screensize, v2s32(0, 0)); + } else { + // Clicking on nothing quits add mode without adding a button. + m_mode = Mode::Default; + } + } + + regenerateGui(screensize); + return true; + } + case EMIE_MOUSE_MOVED: { + if (m_mouse_down && m_selected_btn != touch_gui_button_id_END) { + if (m_mode != Mode::Dragging) { + m_mode = Mode::Dragging; + m_last_good_layout = m_layout; + } + updateDragState(screensize, mouse_pos - m_last_mouse_pos); + + regenerateGui(screensize); + } + + m_last_mouse_pos = mouse_pos; + return true; + } + case EMIE_LMOUSE_LEFT_UP: { + m_mouse_down = false; + + if (m_mode == Mode::Dragging) { + m_mode = Mode::Default; + if (!m_error_rects.empty()) + m_layout = m_last_good_layout; + + regenerateGui(screensize); + } + + return true; + } + default: + break; + } + } + + if (event.EventType == EET_GUI_EVENT) { + switch (event.GUIEvent.EventType) { + case EGET_BUTTON_CLICKED: { + if (event.GUIEvent.Caller == m_gui_add_btn.get()) { + m_mode = Mode::Add; + regenerateGui(screensize); + return true; + } + + if (event.GUIEvent.Caller == m_gui_reset_btn.get()) { + m_layout = ButtonLayout::predefined; + regenerateGui(screensize); + return true; + } + + if (event.GUIEvent.Caller == m_gui_done_btn.get()) { + if (g_touchcontrols) + g_touchcontrols->applyLayout(m_layout); + std::ostringstream oss; + m_layout.serializeJson(oss); + g_settings->set("touch_layout", oss.str()); + quitMenu(); + return true; + } + + if (event.GUIEvent.Caller == m_gui_remove_btn.get()) { + m_layout.layout.erase(m_selected_btn); + regenerateGui(screensize); + return true; + } + + break; + } + default: + break; + } + } + + return Parent ? Parent->OnEvent(event) : false; +} diff --git a/src/gui/touchscreeneditor.h b/src/gui/touchscreeneditor.h new file mode 100644 index 000000000..dc06fb224 --- /dev/null +++ b/src/gui/touchscreeneditor.h @@ -0,0 +1,81 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 grorp, Gregor Parzefall + +#pragma once + +#include "touchscreenlayout.h" +#include "modalMenu.h" + +#include +#include + +class ISimpleTextureSource; +namespace irr::gui +{ + class IGUIImage; +} + +class GUITouchscreenLayout : public GUIModalMenu +{ +public: + GUITouchscreenLayout(gui::IGUIEnvironment* env, + gui::IGUIElement* parent, s32 id, + IMenuManager *menumgr, ISimpleTextureSource *tsrc); + ~GUITouchscreenLayout(); + + void regenerateGui(v2u32 screensize); + void drawMenu(); + bool OnEvent(const SEvent& event); + +protected: + std::wstring getLabelByID(s32 id) { return L""; } + std::string getNameByID(s32 id) { return ""; } + +private: + ISimpleTextureSource *m_tsrc; + + ButtonLayout m_layout; + v2u32 m_last_screensize; + s32 m_button_size; + + enum class Mode { + Default, + Dragging, + Add, + }; + Mode m_mode = Mode::Default; + + std::unordered_map> m_gui_images; + // unused if m_mode == Mode::Add + std::unordered_map m_gui_images_target_pos; + void clearGUIImages(); + void regenerateGUIImagesRegular(v2u32 screensize); + void regenerateGUIImagesAddMode(v2u32 screensize); + void interpolateGUIImages(); + + // interaction state + bool m_mouse_down = false; + v2s32 m_last_mouse_pos; + touch_gui_button_id m_selected_btn = touch_gui_button_id_END; + + // dragging + ButtonLayout m_last_good_layout; + std::vector m_error_rects; + void updateDragState(v2u32 screensize, v2s32 mouse_movement); + + // add mode + ButtonLayout m_add_layout; + std::vector> m_add_button_titles; + + // Menu GUI elements + std::shared_ptr m_gui_help_text; + + std::shared_ptr m_gui_add_btn; + std::shared_ptr m_gui_reset_btn; + std::shared_ptr m_gui_done_btn; + + std::shared_ptr m_gui_remove_btn; + + void regenerateMenu(v2u32 screensize); +}; diff --git a/src/gui/touchscreenlayout.cpp b/src/gui/touchscreenlayout.cpp new file mode 100644 index 000000000..0a88f5dd5 --- /dev/null +++ b/src/gui/touchscreenlayout.cpp @@ -0,0 +1,345 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 grorp, Gregor Parzefall + +#include "touchscreenlayout.h" +#include "client/renderingengine.h" +#include "client/texturesource.h" +#include "convert_json.h" +#include "gettext.h" +#include "settings.h" +#include + +#include "IGUIFont.h" +#include "IGUIStaticText.h" + +const char *button_names[] = { + "jump", + "sneak", + "zoom", + "aux1", + "overflow", + + "chat", + "inventory", + "drop", + "exit", + + "fly", + "fast", + "noclip", + "debug", + "camera", + "range", + "minimap", + "toggle_chat", + + "joystick_off", + "joystick_bg", + "joystick_center", +}; + +// compare with GUIKeyChangeMenu::init_keys +const char *button_titles[] = { + N_("Jump"), + N_("Sneak"), + N_("Zoom"), + N_("Aux1"), + N_("Overflow menu"), + + N_("Chat"), + N_("Inventory"), + N_("Drop"), + N_("Exit"), + + N_("Toggle fly"), + N_("Toggle fast"), + N_("Toggle noclip"), + N_("Toggle debug"), + N_("Change camera"), + N_("Range select"), + N_("Toggle minimap"), + N_("Toggle chat log"), + + N_("Joystick"), + N_("Joystick"), + N_("Joystick"), +}; + +const char *button_image_names[] = { + "jump_btn.png", + "down.png", + "zoom.png", + "aux1_btn.png", + "overflow_btn.png", + + "chat_btn.png", + "inventory_btn.png", + "drop_btn.png", + "exit_btn.png", + + "fly_btn.png", + "fast_btn.png", + "noclip_btn.png", + "debug_btn.png", + "camera_btn.png", + "rangeview_btn.png", + "minimap_btn.png", + // toggle button: switches between "chat_hide_btn.png" and "chat_show_btn.png" + "chat_hide_btn.png", + + "joystick_off.png", + "joystick_bg.png", + "joystick_center.png", +}; + +v2s32 ButtonMeta::getPos(v2u32 screensize, s32 button_size) const +{ + return v2s32((position.X * screensize.X) + (offset.X * button_size), + (position.Y * screensize.Y) + (offset.Y * button_size)); +} + +void ButtonMeta::setPos(v2s32 pos, v2u32 screensize, s32 button_size) +{ + v2s32 third(screensize.X / 3, screensize.Y / 3); + + if (pos.X < third.X) + position.X = 0.0f; + else if (pos.X < 2 * third.X) + position.X = 0.5f; + else + position.X = 1.0f; + + if (pos.Y < third.Y) + position.Y = 0.0f; + else if (pos.Y < 2 * third.Y) + position.Y = 0.5f; + else + position.Y = 1.0f; + + offset.X = (pos.X - (position.X * screensize.X)) / button_size; + offset.Y = (pos.Y - (position.Y * screensize.Y)) / button_size; +} + +bool ButtonLayout::isButtonAllowed(touch_gui_button_id id) +{ + return id != joystick_off_id && id != joystick_bg_id && id != joystick_center_id && + id != touch_gui_button_id_END; +} + +bool ButtonLayout::isButtonRequired(touch_gui_button_id id) +{ + return id == overflow_id; +} + +s32 ButtonLayout::getButtonSize(v2u32 screensize) +{ + return std::min(screensize.Y / 4.5f, + RenderingEngine::getDisplayDensity() * 65.0f * + g_settings->getFloat("hud_scaling")); +} + +const ButtonLayout ButtonLayout::predefined {{ + {jump_id, { + v2f(1.0f, 1.0f), + v2f(-1.0f, -0.5f), + }}, + {sneak_id, { + v2f(1.0f, 1.0f), + v2f(-2.5f, -0.5f), + }}, + {zoom_id, { + v2f(1.0f, 1.0f), + v2f(-0.75f, -3.5f), + }}, + {aux1_id, { + v2f(1.0f, 1.0f), + v2f(-0.75f, -2.0f), + }}, + {overflow_id, { + v2f(1.0f, 1.0f), + v2f(-0.75f, -5.0f), + }}, +}}; + +ButtonLayout ButtonLayout::loadFromSettings() +{ + bool restored = false; + ButtonLayout layout; + + std::string str = g_settings->get("touch_layout"); + if (!str.empty()) { + std::istringstream iss(str); + try { + layout.deserializeJson(iss); + restored = true; + } catch (const Json::Exception &e) { + warningstream << "Could not parse touchscreen layout: " << e.what() << std::endl; + } + } + + if (!restored) + return predefined; + + return layout; +} + +std::unordered_map> ButtonLayout::texture_cache; + +video::ITexture *ButtonLayout::getTexture(touch_gui_button_id btn, ISimpleTextureSource *tsrc) +{ + if (texture_cache.count(btn) > 0) + return texture_cache.at(btn).get(); + + video::ITexture *tex = tsrc->getTexture(button_image_names[btn]); + if (!tex) + // necessary in the mainmenu + tex = tsrc->getTexture(porting::path_share + "/textures/base/pack/" + + button_image_names[btn]); + irr_ptr ptr; + ptr.grab(tex); + texture_cache[btn] = ptr; + return tex; +} + +void ButtonLayout::clearTextureCache() +{ + texture_cache.clear(); +} + +core::recti ButtonLayout::getRect(touch_gui_button_id btn, + v2u32 screensize, s32 button_size, ISimpleTextureSource *tsrc) +{ + const ButtonMeta &meta = layout.at(btn); + v2s32 pos = meta.getPos(screensize, button_size); + + v2u32 orig_size = getTexture(btn, tsrc)->getOriginalSize(); + v2s32 size((button_size * orig_size.X) / orig_size.Y, button_size); + + return core::recti(pos - size / 2, core::dimension2di(size)); +} + +std::vector ButtonLayout::getMissingButtons() +{ + std::vector missing_buttons; + for (u8 i = 0; i < touch_gui_button_id_END; i++) { + touch_gui_button_id btn = (touch_gui_button_id)i; + if (isButtonAllowed(btn) && layout.count(btn) == 0) + missing_buttons.push_back(btn); + } + return missing_buttons; +} + +void ButtonLayout::serializeJson(std::ostream &os) const +{ + Json::Value root = Json::objectValue; + root["layout"] = Json::objectValue; + + for (const auto &[id, meta] : layout) { + Json::Value button = Json::objectValue; + button["position_x"] = meta.position.X; + button["position_y"] = meta.position.Y; + button["offset_x"] = meta.offset.X; + button["offset_y"] = meta.offset.Y; + + root["layout"][button_names[id]] = button; + } + + fastWriteJson(root, os); +} + +static touch_gui_button_id button_name_to_id(const std::string &name) +{ + for (u8 i = 0; i < touch_gui_button_id_END; i++) { + if (name == button_names[i]) + return (touch_gui_button_id)i; + } + return touch_gui_button_id_END; +} + +void ButtonLayout::deserializeJson(std::istream &is) +{ + layout.clear(); + + Json::Value root; + is >> root; + + if (!root["layout"].isObject()) + throw Json::RuntimeError("invalid type for layout"); + + Json::Value &obj = root["layout"]; + Json::ValueIterator iter; + for (iter = obj.begin(); iter != obj.end(); iter++) { + touch_gui_button_id id = button_name_to_id(iter.name()); + if (!isButtonAllowed(id)) + throw Json::RuntimeError("invalid button name"); + + Json::Value &value = *iter; + if (!value.isObject()) + throw Json::RuntimeError("invalid type for button metadata"); + + ButtonMeta meta; + + if (!value["position_x"].isNumeric() || !value["position_y"].isNumeric()) + throw Json::RuntimeError("invalid type for position_x or position_y in button metadata"); + meta.position.X = value["position_x"].asFloat(); + meta.position.Y = value["position_y"].asFloat(); + + if (!value["offset_x"].isNumeric() || !value["offset_y"].isNumeric()) + throw Json::RuntimeError("invalid type for offset_x or offset_y in button metadata"); + meta.offset.X = value["offset_x"].asFloat(); + meta.offset.Y = value["offset_y"].asFloat(); + + layout.emplace(id, meta); + } +} + +void layout_button_grid(v2u32 screensize, ISimpleTextureSource *tsrc, + const std::vector &buttons, + // pos refers to the center of the button + const std::function &callback) +{ + s32 cols = 4; + s32 rows = 3; + f32 screen_aspect = (f32)screensize.X / (f32)screensize.Y; + while ((s32)buttons.size() > cols * rows) { + f32 aspect = (f32)cols / (f32)rows; + if (aspect > screen_aspect) + rows++; + else + cols++; + } + + s32 button_size = ButtonLayout::getButtonSize(screensize); + v2s32 spacing(screensize.X / (cols + 1), screensize.Y / (rows + 1)); + v2s32 pos(spacing); + + for (touch_gui_button_id btn : buttons) { + v2u32 orig_size = ButtonLayout::getTexture(btn, tsrc)->getOriginalSize(); + v2s32 size((button_size * orig_size.X) / orig_size.Y, button_size); + + core::recti rect(pos - size / 2, core::dimension2di(size)); + + if (rect.LowerRightCorner.X > (s32)screensize.X) { + pos.X = spacing.X; + pos.Y += spacing.Y; + rect = core::recti(pos - size / 2, core::dimension2di(size)); + } + + callback(btn, pos, rect); + + pos.X += spacing.X; + } +} + +void make_button_grid_title(gui::IGUIStaticText *text, touch_gui_button_id btn, v2s32 pos, core::recti rect) +{ + std::wstring str = wstrgettext(button_titles[btn]); + text->setText(str.c_str()); + gui::IGUIFont *font = text->getActiveFont(); + core::dimension2du dim = font->getDimension(str.c_str()); + dim = core::dimension2du(dim.Width * 1.25f, dim.Height * 1.25f); // avoid clipping + text->setRelativePosition(core::recti(pos.X - dim.Width / 2, rect.LowerRightCorner.Y, + pos.X + dim.Width / 2, rect.LowerRightCorner.Y + dim.Height)); + text->setTextAlignment(gui::EGUIA_CENTER, gui::EGUIA_UPPERLEFT); +} diff --git a/src/gui/touchscreenlayout.h b/src/gui/touchscreenlayout.h new file mode 100644 index 000000000..9c7d72fbf --- /dev/null +++ b/src/gui/touchscreenlayout.h @@ -0,0 +1,104 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 grorp, Gregor Parzefall + +#pragma once + +#include "irr_ptr.h" +#include "irrlichttypes_bloated.h" +#include "rect.h" +#include +#include + +class ISimpleTextureSource; +namespace irr::gui +{ + class IGUIStaticText; +} +namespace irr::video +{ + class ITexture; +} + +enum touch_gui_button_id : u8 +{ + jump_id = 0, + sneak_id, + zoom_id, + aux1_id, + overflow_id, + + // formerly "rare controls bar" + chat_id, + inventory_id, + drop_id, + exit_id, + + // formerly "settings bar" + fly_id, + fast_id, + noclip_id, + debug_id, + camera_id, + range_id, + minimap_id, + toggle_chat_id, + + // the joystick + joystick_off_id, + joystick_bg_id, + joystick_center_id, + + touch_gui_button_id_END, +}; + +extern const char *button_names[]; +extern const char *button_titles[]; +extern const char *button_image_names[]; + +struct ButtonMeta { + // Position, specified as a percentage of the screensize in the range [0,1]. + // The editor currently writes the values 0, 0.5 and 1. + v2f position; + // Offset, multiplied by the global button size before it is applied. + // Together, position and offset define the position of the button's center. + v2f offset; + + // Returns the button's effective center position in pixels. + v2s32 getPos(v2u32 screensize, s32 button_size) const; + // Sets the button's effective center position in pixels. + void setPos(v2s32 pos, v2u32 screensize, s32 button_size); +}; + +struct ButtonLayout { + static bool isButtonAllowed(touch_gui_button_id id); + static bool isButtonRequired(touch_gui_button_id id); + static s32 getButtonSize(v2u32 screensize); + static ButtonLayout loadFromSettings(); + + static video::ITexture *getTexture(touch_gui_button_id btn, ISimpleTextureSource *tsrc); + static void clearTextureCache(); + + std::unordered_map layout; + + core::recti getRect(touch_gui_button_id btn, + v2u32 screensize, s32 button_size, ISimpleTextureSource *tsrc); + + std::vector getMissingButtons(); + + void serializeJson(std::ostream &os) const; + void deserializeJson(std::istream &is); + + static const ButtonLayout predefined; + +private: + static std::unordered_map> texture_cache; +}; + +void layout_button_grid(v2u32 screensize, ISimpleTextureSource *tsrc, + const std::vector &buttons, + // pos refers to the center of the button. + const std::function &callback); + +void make_button_grid_title(gui::IGUIStaticText *text, + touch_gui_button_id btn,v2s32 pos, core::recti rect); diff --git a/src/irr_gui_ptr.h b/src/irr_gui_ptr.h new file mode 100644 index 000000000..8c16a5b1e --- /dev/null +++ b/src/irr_gui_ptr.h @@ -0,0 +1,28 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2024 grorp, Gregor Parzefall + +#pragma once +#include +#include "IGUIElement.h" + +// We cannot use irr_ptr for Irrlicht GUI elements we own. +// Option 1: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr +// constructor. +// -> We steal the reference owned by IGUIEnvironment and drop it later, +// causing the IGUIElement to be deleted while IGUIEnvironment still +// references it. +// Option 2: Pass IGUIElement* returned by IGUIEnvironment::add* into irr_ptr::grab. +// -> We add another reference and drop it later, but since IGUIEnvironment +// still references the IGUIElement, it is never deleted. +// To make IGUIEnvironment drop its reference to the IGUIElement, we have to call +// IGUIElement::remove, so that's what we'll do. +template +std::shared_ptr grab_gui_element(T *element) +{ + static_assert(std::is_base_of_v, + "grab_gui_element only works for IGUIElement"); + return std::shared_ptr(element, [](T *e) { + e->remove(); + }); +} diff --git a/src/script/lua_api/l_mainmenu.cpp b/src/script/lua_api/l_mainmenu.cpp index e8c969268..0353efe1d 100644 --- a/src/script/lua_api/l_mainmenu.cpp +++ b/src/script/lua_api/l_mainmenu.cpp @@ -11,6 +11,7 @@ #include "gui/guiMainMenu.h" #include "gui/guiKeyChangeMenu.h" #include "gui/guiPathSelectMenu.h" +#include "gui/touchscreeneditor.h" #include "version.h" #include "porting.h" #include "filesys.h" @@ -535,6 +536,22 @@ int ModApiMainMenu::l_show_keys_menu(lua_State *L) return 0; } +/******************************************************************************/ +int ModApiMainMenu::l_show_touchscreen_layout(lua_State *L) +{ + GUIEngine *engine = getGuiEngine(L); + sanity_check(engine != NULL); + + GUITouchscreenLayout *gui = new GUITouchscreenLayout( + engine->m_rendering_engine->get_gui_env(), + engine->m_parent, + -1, + engine->m_menumanager, + engine->m_texture_source.get()); + gui->drop(); + return 0; +} + /******************************************************************************/ int ModApiMainMenu::l_create_world(lua_State *L) { @@ -1080,6 +1097,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top) API_FCT(start); API_FCT(close); API_FCT(show_keys_menu); + API_FCT(show_touchscreen_layout); API_FCT(create_world); API_FCT(delete_world); API_FCT(set_background); diff --git a/src/script/lua_api/l_mainmenu.h b/src/script/lua_api/l_mainmenu.h index d0f72b6c4..704924bf3 100644 --- a/src/script/lua_api/l_mainmenu.h +++ b/src/script/lua_api/l_mainmenu.h @@ -69,6 +69,8 @@ private: static int l_show_keys_menu(lua_State *L); + static int l_show_touchscreen_layout(lua_State *L); + static int l_show_path_select_dialog(lua_State *L); static int l_set_topleft_text(lua_State *L); diff --git a/textures/base/pack/down.png b/textures/base/pack/down.png index 7183ee5364c4f54d0f3dff7a6908ef36fe5a47a8..594863dba86ea4e9cbc563f9e8c574ac5830d2f5 100644 GIT binary patch literal 3647 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 literal 1618 zcmbV_Ydq5n7{~v1Msf+IGm*>Dl|!gWE;}WorVN|4!Zg*Q879j$S-Eu5bfsx-Wek(c zsb;x5r=)`{mp?_QCTkeZ+9vnoyf`n;i}R-Mo9FpGzvuJ&UGqX8(p1w|0{}o1br^{O z0HxJc2~dTu4n;gs0stzTP)KK=m;uQUih?8Qz5J^WQ^dpkUcE1{+~!oM4SJsswgsLR zq2{*Uw(X8`b}Ej~)D%}!RQ-Chh~=(ql~39Xy`qvA?i0{oH*~tTq#ciW9}Gk;6 zv+A*z6q2>_)7+5qGL zAXRP78k34f!#e~3py@0CqC>3PCtF=y@VLjG70T=8o+)W5Olfa^OGh0 z zn@qpyPjzuBxPoREZ6lE6<5dqIkBEn@cX=1$X+Ndg_R$&x;2`aK=T{*euNQ_XOo~e$ z)9237FIoG1jg~t!Oj(_O!<;Y_&VnbN#;_dvLYjSAy)C7ZH$kP=lBBl8$sSAiysmA1 zfN}QJBNbY0{toJ0g&z8Yr~HZUOw#~Z;(15|T=3`5UDQvHdoR;bRPQPhw}R!k%It9k z4N#x*t;p9Xma3O2x8gda{&Eq~oO3L6&*xY#(bkbgH$)&8^m9%)%XLebfBw{$itm;* z5l$lG!NQjLS1}&(Zg*Z+;+zj@ZX77fc0sf_2nmukwYzH+{0V~a?%mWXPul%PJ4FBZ zks#gykfUkTootl$ktUi@#`f{Y7qV@|+cWPBSr8sazWIGf;4X)gJmKkJ7Hqm9(%1)v-H6S8|OjU_6nE7*LtU zx3>f)AUV>Ft*#h*Sj`&YvsbSRi0TYJ%1Z1%Ehs_>I8oW{Nq0t#X(ViYvvDS8xy14M zdX@U)1?zsSdu(=w-dCy5XsHEb5{|w|A$BDKYW#A9vCWU6=|fgyG~;^>$G$v!k*uR{4gpPKiL&naw25 z=|V)6-s+;7Gv+$fZ!*H`vCOpr$zp4W0$+NMxBRkAT<{cos6ee zn9Yq$oypk4(3X8EdldcgeiN|~M9@rDRN?2|%5YUf)mnIU8QCH}qi3!2S;xO5ST)Mb z%+HX}fq*Fs^x8=GMT@+vIy)ovJ?&mNmS&+xY?|+U3$TW zofDBlj5QN-#%42}+(^%_(J%D$-AX;WGPIY60jo@jhIXWkJ)gAwi?!OrR*bIhZ)0?9 zSv{u92}9O5v-2osbCgHxuTI32Z>ky25|}L!~g&Q diff --git a/textures/base/pack/jump_btn.png b/textures/base/pack/jump_btn.png index 17b7cb586dbb602e8f37fac463e5630b316826e0..dbb455b9598328eda2b68d4e3b903fbc29ace1c1 100644 GIT binary patch literal 3625 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 elePpK7F+}k^M@G+J9DQ10000