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 7183ee536..594863dba 100644 Binary files a/textures/base/pack/down.png and b/textures/base/pack/down.png differ diff --git a/textures/base/pack/jump_btn.png b/textures/base/pack/jump_btn.png index 17b7cb586..dbb455b95 100644 Binary files a/textures/base/pack/jump_btn.png and b/textures/base/pack/jump_btn.png differ