diff --git a/games/devtest/mods/testformspec/formspec.lua b/games/devtest/mods/testformspec/formspec.lua index f2f632fa0f..0d9cf7751c 100644 --- a/games/devtest/mods/testformspec/formspec.lua +++ b/games/devtest/mods/testformspec/formspec.lua @@ -323,6 +323,34 @@ local scroll_fs = --style_type[label;border=;bgcolor=] --label[0.75,2;Reset] +local autoscroll_fs = + "label[0.5,0.5;Auto-Scroll Test - Tab through buttons to test auto-scroll centering]" .. + "label[0.5,1;Vertical scroll container:]" .. + "scroll_container[0.5,1.5;5.5,5;autoscroll_v;vertical]" .. + "button[0,0;5,1;asv_btn1;Button 1]" .. + "button[0,1;5,1;asv_btn2;Button 2]" .. + "button[0,2;5,1;asv_btn3;Button 3]" .. + "button[0,3;5,1;asv_btn4;Button 4]" .. + "button[0,4;5,1;asv_btn5;Button 5]" .. + "button[0,5;5,1;asv_btn6;Button 6]" .. + "button[0,6;5,1;asv_btn7;Button 7]" .. + "button[0,7;5,1;asv_btn8;Button 8]" .. + "button[0,8;5,1;asv_btn9;Button 9]" .. + "button[0,9;5,1;asv_btn10;Button 10]" .. + "scroll_container_end[]" .. + "scrollbaroptions[max=50]" .. + "scrollbar[5.8,1.5;0.3,5;vertical;autoscroll_v;0]" .. + "label[7,1;Horizontal scroll container:]" .. + "scroll_container[7,1.5;4.5,2;autoscroll_h;horizontal]" .. + "button[0,0;3,1;ash_btn1;Btn1]" .. + "button[3,0;3,1;ash_btn2;Btn2]" .. + "button[6,0;3,1;ash_btn3;Btn3]" .. + "button[9,0;3,1;ash_btn4;Btn4]" .. + "button[12,0;3,1;ash_btn5;Btn5]" .. + "scroll_container_end[]" .. + "scrollbaroptions[max=105]" .. + "scrollbar[7,2.7;4.5,0.3;horizontal;autoscroll_h;0]" + local window = { sizex = 12, sizey = 13, @@ -477,6 +505,10 @@ mouse control = true] "formspec_version[7]size[12,13]" .. scroll_fs, + -- Autoscroll + "formspec_version[7]size[12,13]" .. + autoscroll_fs, + -- Sound [[ formspec_version[3] @@ -528,7 +560,7 @@ local function show_test_formspec(pname) page = page() end - local fs = page .. "tabheader[0,0;11,0.65;maintabs;Real Coord,Styles,Noclip,Table,Hypertext,Tabs,Invs,Window,Anim,Model,ScrollC,Sound,Background,Unsized;" .. page_id .. ";false;false]" + local fs = page .. "tabheader[0,0;11,0.65;maintabs;Real Coord,Styles,Noclip,Table,Hypertext,Tabs,Invs,Window,Anim,Model,ScrollC,Autoscroll,Sound,Background,Unsized;" .. page_id .. ";false;false]" core.show_formspec(pname, "testformspec:formspec", fs) end diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index f6fbaf4ca4..eb1f36b4ce 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -3482,6 +3482,9 @@ void GUIFormSpecMenu::drawMenu() updateSelectedItem(); + // Auto-scroll to center focused element when Tab enables focus tracking + autoScroll(); + video::IVideoDriver* driver = Environment->getVideoDriver(); /* @@ -3613,6 +3616,27 @@ void GUIFormSpecMenu::drawMenu() cursor_control->setActiveIcon(ECI_NORMAL); } + // Draw white outline around keyboard-focused form elements. + const gui::IGUIElement *focused = Environment->getFocus(); + if (focused && m_show_focus) { + core::rect rect = focused->getAbsoluteClippingRect(); + const video::SColor white(255, 255, 255, 255); + const s32 border = 2; + + driver->draw2DRectangle(white, + core::rect(rect.UpperLeftCorner.X, rect.UpperLeftCorner.Y, + rect.LowerRightCorner.X, rect.UpperLeftCorner.Y + border), nullptr); + driver->draw2DRectangle(white, + core::rect(rect.UpperLeftCorner.X, rect.LowerRightCorner.Y - border, + rect.LowerRightCorner.X, rect.LowerRightCorner.Y), nullptr); + driver->draw2DRectangle(white, + core::rect(rect.UpperLeftCorner.X, rect.UpperLeftCorner.Y, + rect.UpperLeftCorner.X + border, rect.LowerRightCorner.Y), nullptr); + driver->draw2DRectangle(white, + core::rect(rect.LowerRightCorner.X - border, rect.UpperLeftCorner.Y, + rect.LowerRightCorner.X, rect.LowerRightCorner.Y), nullptr); + } + m_tooltip_element->draw(); /* @@ -3684,6 +3708,89 @@ void GUIFormSpecMenu::showTooltip(const std::wstring &text, bringToFront(m_tooltip_element); } +void GUIFormSpecMenu::autoScroll() +{ + gui::IGUIElement *focus = Environment->getFocus(); + if (!m_show_focus || !focus) + return; + + // Only process if focus changed or this is the first focus + if (focus == m_last_focused && m_last_focused != nullptr) + return; + + bool first_focus = (m_last_focused == nullptr); + m_last_focused = focus; + + // Find the scroll container that contains the focused element + for (const auto &cont : m_scroll_containers) { + if (!cont.second->isMyChild(focus)) + continue; + + gui::IGUIElement *clipper = cont.second->getParent(); + if (!clipper) + break; + + // Find scrollbars for this container + GUIScrollBar *scrollbar_v = nullptr; + GUIScrollBar *scrollbar_h = nullptr; + for (const auto &sb : m_scrollbars) { + if (sb.first.fname == cont.first) { + if (sb.second->isHorizontal()) + scrollbar_h = sb.second; + else + scrollbar_v = sb.second; + } + } + + core::rect clip = clipper->getAbsoluteClippingRect(); + core::rect elem = focus->getAbsolutePosition(); + f32 scrollfactor = cont.second->getScrollFactor(); + + if (scrollfactor == 0) + break; + + // Handle vertical scrolling + if (scrollbar_v) { + bool visible = elem.UpperLeftCorner.Y >= clip.UpperLeftCorner.Y && + elem.LowerRightCorner.Y <= clip.LowerRightCorner.Y; + if (first_focus || !visible) { + s32 target_y = elem.UpperLeftCorner.Y; + if (elem.getHeight() < clip.getHeight()) + target_y = clip.UpperLeftCorner.Y + (clip.getHeight() - elem.getHeight()) / 2; + + s32 new_pos = scrollbar_v->getPos() + (s32)std::round((target_y - elem.UpperLeftCorner.Y) / scrollfactor); + new_pos = rangelim(new_pos, scrollbar_v->getMin(), scrollbar_v->getMax()); + + if (new_pos != scrollbar_v->getPos()) { + scrollbar_v->setPos(new_pos); + cont.second->updateScrolling(); + } + } + } + + // Handle horizontal scrolling + if (scrollbar_h) { + bool visible = elem.UpperLeftCorner.X >= clip.UpperLeftCorner.X && + elem.LowerRightCorner.X <= clip.LowerRightCorner.X; + if (first_focus || !visible) { + s32 target_x = elem.UpperLeftCorner.X; + if (elem.getWidth() < clip.getWidth()) + target_x = clip.UpperLeftCorner.X + (clip.getWidth() - elem.getWidth()) / 2; + + s32 new_pos = scrollbar_h->getPos() + (s32)std::round((target_x - elem.UpperLeftCorner.X) / scrollfactor); + new_pos = rangelim(new_pos, scrollbar_h->getMin(), scrollbar_h->getMax()); + + if (new_pos != scrollbar_h->getPos()) { + scrollbar_h->setPos(new_pos); + cont.second->updateScrolling(); + } + } + } + + break; + } +} + void GUIFormSpecMenu::updateSelectedItem() { // Don't update when dragging an item @@ -3948,6 +4055,37 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event) if (GUIModalMenu::preprocessEvent(event)) return true; + // Handle keyboard and touch input to show/hide focus outline + switch (event.EventType) { + case EET_KEY_INPUT_EVENT: + if (event.KeyInput.PressedDown && event.KeyInput.Key == KEY_TAB && + !event.KeyInput.Control) { + m_show_focus = true; + m_last_focused = nullptr; + } + break; + case EET_MOUSE_INPUT_EVENT: + switch (event.MouseInput.Event) { + case EMIE_LMOUSE_PRESSED_DOWN: + case EMIE_RMOUSE_PRESSED_DOWN: + case EMIE_MMOUSE_PRESSED_DOWN: + m_show_focus = false; + m_last_focused = nullptr; + break; + default: + break; + } + break; + case EET_TOUCH_INPUT_EVENT: + if (event.TouchInput.Event == ETIE_PRESSED_DOWN) { + m_show_focus = false; + m_last_focused = nullptr; + } + break; + default: + break; + } + // The IGUITabControl renders visually using the skin's selected // font, which we override for the duration of form drawing, // but computes tab hotspots based on how it would have rendered diff --git a/src/gui/guiFormSpecMenu.h b/src/gui/guiFormSpecMenu.h index 5cdf2f6672..6680266ee6 100644 --- a/src/gui/guiFormSpecMenu.h +++ b/src/gui/guiFormSpecMenu.h @@ -385,6 +385,8 @@ private: std::optional m_focused_element = std::nullopt; JoystickController *m_joystick; bool m_show_debug = false; + bool m_show_focus = false; + gui::IGUIElement *m_last_focused = nullptr; struct parserData { bool explicit_size; @@ -496,6 +498,12 @@ private: void showTooltip(const std::wstring &text, const video::SColor &color, const video::SColor &bgcolor); + /** + * Auto-scrolls a scroll container to center the focused element. + * Handles both vertical and horizontal scrolling. + */ + void autoScroll(); + /** * In formspec version < 2 the elements were not ordered properly. Some element * types were drawn before others. diff --git a/src/gui/guiScrollContainer.h b/src/gui/guiScrollContainer.h index d1632f7653..61859d07d3 100644 --- a/src/gui/guiScrollContainer.h +++ b/src/gui/guiScrollContainer.h @@ -31,6 +31,11 @@ public: void setScrollBar(GUIScrollBar *scrollbar); void updateScrolling(); + inline f32 getScrollFactor() const + { + return m_scrollfactor; + } + private: enum OrientationEnum { diff --git a/src/gui/guiTable.cpp b/src/gui/guiTable.cpp index 7491c6828d..aa11df316b 100644 --- a/src/gui/guiTable.cpp +++ b/src/gui/guiTable.cpp @@ -828,8 +828,8 @@ bool GUITable::OnEvent(const SEvent &event) } else if (event.KeyInput.Key == KEY_ESCAPE || event.KeyInput.Key == KEY_SPACE || - (event.KeyInput.Key == KEY_TAB && event.KeyInput.Control)) { - // pass to parent + event.KeyInput.Key == KEY_TAB) { + // pass to parent for focus cycling (both plain Tab and Ctrl+Tab) return IGUIElement::OnEvent(event); } else if (event.KeyInput.PressedDown && event.KeyInput.Char) {