1
0
mirror of https://github.com/luanti-org/luanti.git synced 2026-01-13 12:45:29 +01:00

Outline focused element when Tab is pressed (#16798)

This commit is contained in:
siliconsniffer
2026-01-12 20:40:16 +01:00
committed by GitHub
parent e047c436a8
commit fbd3707646
5 changed files with 186 additions and 3 deletions

View File

@@ -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

View File

@@ -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<s32> rect = focused->getAbsoluteClippingRect();
const video::SColor white(255, 255, 255, 255);
const s32 border = 2;
driver->draw2DRectangle(white,
core::rect<s32>(rect.UpperLeftCorner.X, rect.UpperLeftCorner.Y,
rect.LowerRightCorner.X, rect.UpperLeftCorner.Y + border), nullptr);
driver->draw2DRectangle(white,
core::rect<s32>(rect.UpperLeftCorner.X, rect.LowerRightCorner.Y - border,
rect.LowerRightCorner.X, rect.LowerRightCorner.Y), nullptr);
driver->draw2DRectangle(white,
core::rect<s32>(rect.UpperLeftCorner.X, rect.UpperLeftCorner.Y,
rect.UpperLeftCorner.X + border, rect.LowerRightCorner.Y), nullptr);
driver->draw2DRectangle(white,
core::rect<s32>(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<s32> clip = clipper->getAbsoluteClippingRect();
core::rect<s32> 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

View File

@@ -385,6 +385,8 @@ private:
std::optional<std::string> 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.

View File

@@ -31,6 +31,11 @@ public:
void setScrollBar(GUIScrollBar *scrollbar);
void updateScrolling();
inline f32 getScrollFactor() const
{
return m_scrollfactor;
}
private:
enum OrientationEnum
{

View File

@@ -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) {