diff --git a/src/gui/guiEditBox.cpp b/src/gui/guiEditBox.cpp index ac715e74a..34689fc13 100644 --- a/src/gui/guiEditBox.cpp +++ b/src/gui/guiEditBox.cpp @@ -774,9 +774,9 @@ bool GUIEditBox::processMouse(const SEvent &event) } case EMIE_MOUSE_WHEEL: if (m_vscrollbar && m_vscrollbar->isVisible()) { - s32 pos = m_vscrollbar->getPos(); + s32 pos = m_vscrollbar->getTargetPos(); s32 step = m_vscrollbar->getSmallStep(); - m_vscrollbar->setPos(pos - event.MouseInput.Wheel * step); + m_vscrollbar->setPosInterpolated(pos - event.MouseInput.Wheel * step); return true; } break; diff --git a/src/gui/guiHyperText.cpp b/src/gui/guiHyperText.cpp index 76bc98a71..3db6f0071 100644 --- a/src/gui/guiHyperText.cpp +++ b/src/gui/guiHyperText.cpp @@ -1084,7 +1084,7 @@ bool GUIHyperText::OnEvent(const SEvent &event) checkHover(event.MouseInput.X, event.MouseInput.Y); if (event.MouseInput.Event == EMIE_MOUSE_WHEEL && m_vscrollbar->isVisible()) { - m_vscrollbar->setPos(m_vscrollbar->getPos() - + m_vscrollbar->setPosInterpolated(m_vscrollbar->getTargetPos() - event.MouseInput.Wheel * m_vscrollbar->getSmallStep()); m_text_scrollpos.Y = -m_vscrollbar->getPos(); m_drawer.draw(m_display_text_rect, m_text_scrollpos); diff --git a/src/gui/guiScrollBar.cpp b/src/gui/guiScrollBar.cpp index 60e9b05f0..88f101a2b 100644 --- a/src/gui/guiScrollBar.cpp +++ b/src/gui/guiScrollBar.cpp @@ -12,6 +12,7 @@ the arrow buttons where there is insufficient space. #include "guiScrollBar.h" #include "guiButton.h" +#include "porting.h" #include GUIScrollBar::GUIScrollBar(IGUIEnvironment *environment, IGUIElement *parent, s32 id, @@ -38,40 +39,32 @@ bool GUIScrollBar::OnEvent(const SEvent &event) switch (event.EventType) { case EET_KEY_INPUT_EVENT: if (event.KeyInput.PressedDown) { - const s32 old_pos = scroll_pos; + const s32 old_pos = getTargetPos(); bool absorb = true; switch (event.KeyInput.Key) { case KEY_LEFT: case KEY_UP: - setPos(scroll_pos - small_step); + setPosInterpolated(old_pos - small_step); break; case KEY_RIGHT: case KEY_DOWN: - setPos(scroll_pos + small_step); + setPosInterpolated(old_pos + small_step); break; case KEY_HOME: - setPos(min_pos); + setPosInterpolated(min_pos); break; case KEY_PRIOR: - setPos(scroll_pos - large_step); + setPosInterpolated(old_pos - large_step); break; case KEY_END: - setPos(max_pos); + setPosInterpolated(max_pos); break; case KEY_NEXT: - setPos(scroll_pos + large_step); + setPosInterpolated(old_pos + large_step); break; default: absorb = false; } - if (scroll_pos != old_pos) { - SEvent e; - e.EventType = EET_GUI_EVENT; - e.GUIEvent.Caller = this; - e.GUIEvent.Element = nullptr; - e.GUIEvent.EventType = EGET_SCROLL_BAR_CHANGED; - Parent->OnEvent(e); - } if (absorb) return true; } @@ -79,16 +72,9 @@ bool GUIScrollBar::OnEvent(const SEvent &event) case EET_GUI_EVENT: if (event.GUIEvent.EventType == EGET_BUTTON_CLICKED) { if (event.GUIEvent.Caller == up_button) - setPos(scroll_pos - small_step); + setPosInterpolated(getTargetPos() - small_step); else if (event.GUIEvent.Caller == down_button) - setPos(scroll_pos + small_step); - - SEvent e; - e.EventType = EET_GUI_EVENT; - e.GUIEvent.Caller = this; - e.GUIEvent.Element = nullptr; - e.GUIEvent.EventType = EGET_SCROLL_BAR_CHANGED; - Parent->OnEvent(e); + setPosInterpolated(getTargetPos() + small_step); return true; } else if (event.GUIEvent.EventType == EGET_ELEMENT_FOCUS_LOST) if (event.GUIEvent.Caller == this) @@ -102,14 +88,7 @@ bool GUIScrollBar::OnEvent(const SEvent &event) if (Environment->hasFocus(this)) { s8 d = event.MouseInput.Wheel < 0 ? -1 : 1; s8 h = is_horizontal ? 1 : -1; - setPos(getPos() + (d * small_step * h)); - - SEvent e; - e.EventType = EET_GUI_EVENT; - e.GUIEvent.Caller = this; - e.GUIEvent.Element = nullptr; - e.GUIEvent.EventType = EGET_SCROLL_BAR_CHANGED; - Parent->OnEvent(e); + setPosInterpolated(getTargetPos() + (d * small_step * h)); return true; } break; @@ -228,11 +207,45 @@ void GUIScrollBar::draw() IGUIElement::draw(); } +static inline s32 interpolate_scroll(s32 from, s32 to, f32 amount) +{ + s32 step = core::round32((to - from) * core::clamp(amount, 0.001f, 1.0f)); + if (step == 0) + return to; + return from + step; +} + +void GUIScrollBar::interpolatePos() +{ + if (target_pos.has_value()) { + // Adjust to match 60 FPS. This also means that interpolation is + // effectively disabled at <= 30 FPS. + f32 amount = 0.5f * (last_delta_ms / 16.667f); + setPosRaw(interpolate_scroll(scroll_pos, *target_pos, amount)); + if (scroll_pos == target_pos) + target_pos = std::nullopt; + + SEvent e; + e.EventType = EET_GUI_EVENT; + e.GUIEvent.Caller = this; + e.GUIEvent.Element = nullptr; + e.GUIEvent.EventType = EGET_SCROLL_BAR_CHANGED; + Parent->OnEvent(e); + } +} + +void GUIScrollBar::OnPostRender(u32 time_ms) +{ + last_delta_ms = porting::getDeltaMs(last_time_ms, time_ms); + last_time_ms = time_ms; + interpolatePos(); +} + void GUIScrollBar::updateAbsolutePosition() { IGUIElement::updateAbsolutePosition(); refreshControls(); - setPos(scroll_pos); + updatePos(); } s32 GUIScrollBar::getPosFromMousePos(const core::position2di &pos) const @@ -250,7 +263,12 @@ s32 GUIScrollBar::getPosFromMousePos(const core::position2di &pos) const return core::isnotzero(range()) ? s32(f32(p) / f32(w) * range() + 0.5f) + min_pos : 0; } -void GUIScrollBar::setPos(const s32 &pos) +void GUIScrollBar::updatePos() +{ + setPosRaw(scroll_pos); +} + +void GUIScrollBar::setPosRaw(const s32 &pos) { s32 thumb_area = 0; s32 thumb_min = 0; @@ -276,6 +294,23 @@ void GUIScrollBar::setPos(const s32 &pos) border_size; } +void GUIScrollBar::setPos(const s32 &pos) +{ + setPosRaw(pos); + target_pos = std::nullopt; +} + +void GUIScrollBar::setPosInterpolated(const s32 &pos) +{ + s32 clamped = core::s32_clamp(pos, min_pos, max_pos); + if (scroll_pos != clamped) { + target_pos = clamped; + interpolatePos(); + } else { + target_pos = std::nullopt; + } +} + void GUIScrollBar::setSmallStep(const s32 &step) { small_step = step > 0 ? step : 10; @@ -295,7 +330,7 @@ void GUIScrollBar::setMax(const s32 &max) bool enable = core::isnotzero(range()); up_button->setEnabled(enable); down_button->setEnabled(enable); - setPos(scroll_pos); + updatePos(); } void GUIScrollBar::setMin(const s32 &min) @@ -307,13 +342,13 @@ void GUIScrollBar::setMin(const s32 &min) bool enable = core::isnotzero(range()); up_button->setEnabled(enable); down_button->setEnabled(enable); - setPos(scroll_pos); + updatePos(); } void GUIScrollBar::setPageSize(const s32 &size) { page_size = size; - setPos(scroll_pos); + updatePos(); } void GUIScrollBar::setArrowsVisible(ArrowVisibility visible) @@ -327,6 +362,15 @@ s32 GUIScrollBar::getPos() const return scroll_pos; } +s32 GUIScrollBar::getTargetPos() const +{ + if (target_pos.has_value()) { + s32 clamped = core::s32_clamp(*target_pos, min_pos, max_pos); + return clamped; + } + return scroll_pos; +} + void GUIScrollBar::refreshControls() { IGUISkin *skin = Environment->getSkin(); diff --git a/src/gui/guiScrollBar.h b/src/gui/guiScrollBar.h index 3ff3bba35..a976d1a59 100644 --- a/src/gui/guiScrollBar.h +++ b/src/gui/guiScrollBar.h @@ -13,6 +13,7 @@ the arrow buttons where there is insufficient space. #pragma once #include "irrlichttypes_extrabloated.h" +#include class ISimpleTextureSource; @@ -33,21 +34,30 @@ public: DEFAULT }; - virtual void draw(); - virtual void updateAbsolutePosition(); - virtual bool OnEvent(const SEvent &event); + virtual void draw() override; + virtual void updateAbsolutePosition() override; + virtual bool OnEvent(const SEvent &event) override; + virtual void OnPostRender(u32 time_ms) override; s32 getMax() const { return max_pos; } s32 getMin() const { return min_pos; } s32 getLargeStep() const { return large_step; } s32 getSmallStep() const { return small_step; } s32 getPos() const; + s32 getTargetPos() const; void setMax(const s32 &max); void setMin(const s32 &min); void setSmallStep(const s32 &step); void setLargeStep(const s32 &step); + //! Sets a position immediately, aborting any ongoing interpolation. + // setPos does not send EGET_SCROLL_BAR_CHANGED events for you. void setPos(const s32 &pos); + //! Sets a target position for interpolation. + // If you want to do an interpolated addition, use + // setPosInterpolated(getTargetPos() + x). + // setPosInterpolated takes care of sending EGET_SCROLL_BAR_CHANGED events. + void setPosInterpolated(const s32 &pos); void setPageSize(const s32 &size); void setArrowsVisible(ArrowVisibility visible); @@ -79,4 +89,11 @@ private: video::SColor current_icon_color; ISimpleTextureSource *m_tsrc; + + void setPosRaw(const s32 &pos); + void updatePos(); + std::optional target_pos; + u32 last_time_ms = 0; + u32 last_delta_ms = 17; // assume 60 FPS + void interpolatePos(); }; diff --git a/src/gui/guiTable.cpp b/src/gui/guiTable.cpp index 81c38ffd8..530580124 100644 --- a/src/gui/guiTable.cpp +++ b/src/gui/guiTable.cpp @@ -869,7 +869,7 @@ bool GUITable::OnEvent(const SEvent &event) core::position2d p(event.MouseInput.X, event.MouseInput.Y); if (event.MouseInput.Event == EMIE_MOUSE_WHEEL) { - m_scrollbar->setPos(m_scrollbar->getPos() + + m_scrollbar->setPosInterpolated(m_scrollbar->getTargetPos() + (event.MouseInput.Wheel < 0 ? -3 : 3) * - (s32) m_rowheight / 2); return true;