[Scummvm-git-logs] scummvm master -> 1be746527bf3025857e5e9f1a2b21fd5fc3ae4bd
sev-
noreply at scummvm.org
Tue Apr 14 22:09:15 UTC 2026
This automated email contains information about 2 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
b0e2f6c0b4 GUI: Add FluidScroller and VelocityTracker class for physics-based scrolling
1be746527b GUI: Implement fluid scrolling in About dialog
Commit: b0e2f6c0b4ddf60083f11ad43c00cc75bfe45f46
https://github.com/scummvm/scummvm/commit/b0e2f6c0b4ddf60083f11ad43c00cc75bfe45f46
Author: Mohit Bankar (mohitbankar1212 at gmail.com)
Date: 2026-04-15T00:09:10+02:00
Commit Message:
GUI: Add FluidScroller and VelocityTracker class for physics-based scrolling
These utilities provide momentum, physics-based spring-back, and rubber-banding logic for fluid, iOS-style scrolling interactions.
Changed paths:
A gui/animation/FluidScroll.cpp
A gui/animation/FluidScroll.h
gui/module.mk
diff --git a/gui/animation/FluidScroll.cpp b/gui/animation/FluidScroll.cpp
new file mode 100644
index 00000000000..19220415dd9
--- /dev/null
+++ b/gui/animation/FluidScroll.cpp
@@ -0,0 +1,244 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/*
+ * Based on the implementation by fluid-scroll repository
+ * at https://github.com/ktiays/fluid-scroll
+ */
+
+#include "common/system.h"
+#include "common/util.h"
+#include "gui/animation/FluidScroll.h"
+
+namespace GUI {
+
+const float FluidScroller::kVelocityThreshold = 0.01f;
+const float FluidScroller::kValueThreshold = 0.1f;
+const float FluidScroller::kDefaultSpringResponse = 0.575f;
+const float FluidScroller::kRubberBandCoefficient = 0.55f;
+const float FluidScroller::kRubberBandStretchFraction = 0.25f;
+const float FluidScroller::kDecelerationRate = 0.998f;
+
+FluidScroller::VelocityTracker::VelocityTracker() {
+ reset();
+}
+
+void FluidScroller::VelocityTracker::reset() {
+ index = 0;
+ count = 0;
+ memset(samples, 0, sizeof(samples));
+}
+
+void FluidScroller::VelocityTracker::addPoint(uint32 time, float position) {
+ samples[index].time = time;
+ samples[index].position = position;
+ index = (index + 1) % kHistorySize;
+ if (count < kHistorySize)
+ count++;
+}
+
+float FluidScroller::VelocityTracker::calculateVelocity() const {
+ if (count < 2)
+ return 0.0f;
+
+ // We look at the last few samples (up to 4) to determine the average velocity
+ float velocities[4];
+ int validVelocities = 0;
+
+ for (int i = 0; i < 4 && i < count - 1; ++i) {
+ int i1 = (index + kHistorySize - 1 - i) % kHistorySize; // current point
+ int i2 = (index + kHistorySize - 2 - i) % kHistorySize; // previous point
+
+ uint32 dt = samples[i1].time - samples[i2].time;
+
+ if (dt > 0)
+ velocities[validVelocities++] = (samples[i1].position - samples[i2].position) / (float)dt;
+ else
+ break;
+ }
+
+ if (validVelocities == 0)
+ return 0.0f;
+
+ // Weighted average of historical velocities
+ float totalVelocity = 0.0f;
+ float totalWeight = 0.0f;
+ float weight = 1.0f;
+ for (int i = 0; i < validVelocities; ++i) {
+ totalVelocity += velocities[i] * weight;
+ totalWeight += weight;
+ weight *= 0.6f;
+ }
+ return totalVelocity / totalWeight;
+}
+
+FluidScroller::FluidScroller() :
+ _mode(kModeNone),
+ _startTime(0),
+ _scrollPosRaw(0.0f),
+ _animationOffset(0.0f),
+ _maxScroll(0.0f),
+ _viewportHeight(0),
+ _initialVelocity(0.0f),
+ _lambda(0.0f),
+ _stretchDistance(0.0f),
+ _impactVelocity(0.0f) {
+}
+
+void FluidScroller::setBounds(float maxScroll, int viewportHeight) {
+ _maxScroll = maxScroll;
+ _viewportHeight = viewportHeight;
+}
+
+void FluidScroller::reset() {
+ _mode = kModeNone;
+ _startTime = 0;
+ _initialVelocity = 0.0f;
+ _scrollPosRaw = 0.0f;
+ _velocityTracker.reset();
+}
+
+void FluidScroller::stopAnimation() {
+ _mode = kModeNone;
+ _velocityTracker.reset();
+}
+
+void FluidScroller::feedDrag(uint32 time, int deltaY) {
+ _scrollPosRaw += (float)deltaY;
+ _velocityTracker.addPoint(time, _scrollPosRaw);
+}
+
+float FluidScroller::setPosition(float pos, bool checkBound) {
+ _scrollPosRaw = pos;
+ if (checkBound)
+ checkBoundaries();
+ return getVisualPosition();
+}
+
+void FluidScroller::startFling() {
+ float velocity = _velocityTracker.calculateVelocity();
+
+ if (fabsf(velocity) < 0.1f) {
+ checkBoundaries();
+ return;
+ }
+
+ _mode = kModeFling;
+ _startTime = g_system->getMillis();
+ _initialVelocity = velocity;
+ _animationOffset = _scrollPosRaw;
+}
+
+void FluidScroller::absorb(float velocity, float distance) {
+ _mode = kModeSpringBack;
+ _startTime = g_system->getMillis();
+
+ _lambda = 2.0f * (float)M_PI / kDefaultSpringResponse;
+ _stretchDistance = distance;
+
+ // Convert velocity from pixels/ms to pixels/s for the spring formula
+ _impactVelocity = velocity * 1000.0f + _lambda * distance;
+}
+
+bool FluidScroller::update(uint32 time, float &outVisualPos) {
+ if (_mode == kModeNone) {
+ outVisualPos = getVisualPosition();
+ return false;
+ }
+
+ float elapsed = (float)(time - _startTime);
+
+ if (_mode == kModeFling) {
+ float coefficient = powf(kDecelerationRate, elapsed);
+ float velocity = _initialVelocity * coefficient;
+ float offset = _initialVelocity * (1.0f / logf(kDecelerationRate)) * (coefficient - 1.0f);
+
+ if (fabsf(velocity) < kVelocityThreshold) {
+ _mode = kModeNone;
+ checkBoundaries();
+ outVisualPos = getVisualPosition();
+ return _mode != kModeNone;
+ }
+
+ _scrollPosRaw = _animationOffset + offset;
+
+ // Boundaries during fling
+ if (_scrollPosRaw < 0) {
+ absorb(velocity, _scrollPosRaw);
+ _animationOffset = 0;
+ } else if (_scrollPosRaw > _maxScroll) {
+ absorb(velocity, _scrollPosRaw - _maxScroll);
+ _animationOffset = _maxScroll;
+ }
+
+ } else if (_mode == kModeSpringBack) {
+ float t = elapsed / 1000.0f;
+ float offset = (_stretchDistance + _impactVelocity * t) * expf(-_lambda * t);
+ float velocity = getVelocityAt(t);
+
+ if (fabsf(offset) < kValueThreshold && fabsf(velocity) / 1000.0f < kVelocityThreshold) {
+ _mode = kModeNone;
+ _scrollPosRaw = _animationOffset;
+ outVisualPos = getVisualPosition();
+ return false;
+ }
+
+ _scrollPosRaw = _animationOffset + offset;
+ }
+
+ outVisualPos = getVisualPosition();
+ return true;
+}
+
+float FluidScroller::getVisualPosition() const {
+ float rubberBandRange = (float)_viewportHeight * kRubberBandStretchFraction;
+
+ if (_scrollPosRaw < 0)
+ return -calculateRubberBandOffset(-_scrollPosRaw, rubberBandRange);
+ else if (_scrollPosRaw > _maxScroll)
+ return _maxScroll + calculateRubberBandOffset(_scrollPosRaw - _maxScroll, rubberBandRange);
+
+ return _scrollPosRaw;
+}
+
+void FluidScroller::checkBoundaries() {
+ if (_scrollPosRaw < 0) {
+ absorb(0, _scrollPosRaw);
+ _animationOffset = 0;
+ } else if (_scrollPosRaw > _maxScroll) {
+ absorb(0, _scrollPosRaw - _maxScroll);
+ _animationOffset = _maxScroll;
+ }
+}
+
+float FluidScroller::getVelocityAt(float timeInSeconds) const {
+ if (_mode != kModeSpringBack)
+ return 0.0f;
+ return (_impactVelocity - _lambda * (_stretchDistance + _impactVelocity * timeInSeconds)) * expf(-_lambda * timeInSeconds);
+}
+
+float FluidScroller::calculateRubberBandOffset(float offset, float range) {
+ if (range <= 0)
+ return 0;
+ return (1.0f - (1.0f / ((offset * kRubberBandCoefficient / range) + 1.0f))) * range;
+}
+
+} // End of namespace GUI
diff --git a/gui/animation/FluidScroll.h b/gui/animation/FluidScroll.h
new file mode 100644
index 00000000000..80f1fbb0ec7
--- /dev/null
+++ b/gui/animation/FluidScroll.h
@@ -0,0 +1,142 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+/*
+ * Based on the implementation by fluid-scroll repository
+ * at https://github.com/ktiays/fluid-scroll
+ */
+
+#ifndef GUI_ANIMATION_FLUID_SCROLL_H
+#define GUI_ANIMATION_FLUID_SCROLL_H
+
+#include "common/scummsys.h"
+
+namespace GUI {
+
+class FluidScroller {
+public:
+ FluidScroller();
+ ~FluidScroller() {}
+
+ /**
+ * Configure the constraints for the content
+ * @param maxScroll The maximum scrollable distance (total height - viewport height)
+ * @param viewportHeight The height of the scrolling area, used for rubber-band range
+ */
+ void setBounds(float maxScroll, int viewportHeight);
+
+ void reset();
+
+ // Reset the animation state (fling/spring-back), keeping current position
+ void stopAnimation();
+
+ /**
+ * Record a pointer movement and update the raw position
+ * @param time Current system time in ms
+ * @param deltaY The movement since the last frame
+ */
+ void feedDrag(uint32 time, int deltaY);
+
+ // Start a fling using the recorded velocity
+ void startFling();
+
+ // Check if there is an active animation (fling or spring-back)
+ bool isAnimating() const { return _mode != kModeNone; }
+
+ /**
+ * Update the internal animation state
+ * @param time Current system time in ms
+ * @param outVisualPos The resulting visual scroll position (including rubber-banding)
+ * @return True if an animation is active and updated
+ */
+ bool update(uint32 time, float &outVisualPos);
+
+ float setPosition(float pos, bool checkBound = false);
+
+ // Get the current visual scroll position
+ float getVisualPosition() const;
+
+ // Trigger an elastic spring-back if the current position is out of bounds
+ void checkBoundaries();
+
+private:
+ enum Mode {
+ kModeNone,
+ kModeFling,
+ kModeSpringBack
+ };
+
+ // Velocity tracking
+ struct VelocityTracker {
+ struct Point {
+ uint32 time;
+ float position;
+ };
+ static const int kHistorySize = 20;
+ Point samples[kHistorySize];
+ int index;
+ int count;
+
+ VelocityTracker();
+ void reset();
+ void addPoint(uint32 time, float position);
+ float calculateVelocity() const;
+ };
+
+ VelocityTracker _velocityTracker;
+
+ Mode _mode;
+ uint32 _startTime;
+
+ // Scroll status
+ float _scrollPosRaw; // Physical position (can go out of bounds)
+ float _animationOffset; // Anchor position used as the starting point for animation offsets
+ float _maxScroll;
+ int _viewportHeight;
+
+ // Fling parameter
+ float _initialVelocity;
+
+ // Spring parameters
+ float _lambda; // Spring stiffness factor
+ float _stretchDistance; // Initial distance beyond the edge when spring-back begins
+ float _impactVelocity; // Velocity when hitting the boundary
+
+
+ float getVelocityAt(float timeInSeconds) const;
+
+ // Transition from movement to spring-back animation when hitting an edge
+ void absorb(float velocity, float distance);
+
+ // Returns the visual offset to apply when scrolled past an edge
+ static float calculateRubberBandOffset(float offset, float range);
+
+ static const float kRubberBandStretchFraction; // Maximum stretch limit as a fraction of viewport height
+ static const float kDecelerationRate; // Rate at which fling velocity slows down
+ static const float kVelocityThreshold; // Minimum velocity to keep animation running
+ static const float kValueThreshold; // Minimum value difference to keep spring active
+ static const float kDefaultSpringResponse; // Natural response time of the spring
+ static const float kRubberBandCoefficient; // Coefficient for rubber-band stiffness
+};
+
+} // End of namespace GUI
+
+#endif
diff --git a/gui/module.mk b/gui/module.mk
index c5e07660cb3..04df343de0e 100644
--- a/gui/module.mk
+++ b/gui/module.mk
@@ -36,6 +36,7 @@ MODULE_OBJS := \
unknown-game-dialog.o \
widget.o \
animation/Animation.o \
+ animation/FluidScroll.o \
animation/RepeatAnimationWrapper.o \
animation/SequenceAnimationComposite.o \
widgets/editable.o \
Commit: 1be746527bf3025857e5e9f1a2b21fd5fc3ae4bd
https://github.com/scummvm/scummvm/commit/1be746527bf3025857e5e9f1a2b21fd5fc3ae4bd
Author: Mohit Bankar (mohitbankar1212 at gmail.com)
Date: 2026-04-15T00:09:10+02:00
Commit Message:
GUI: Implement fluid scrolling in About dialog
Changed paths:
gui/about.cpp
gui/about.h
diff --git a/gui/about.cpp b/gui/about.cpp
index dcee678d062..64634d7e58e 100644
--- a/gui/about.cpp
+++ b/gui/about.cpp
@@ -32,6 +32,7 @@
#include "gui/gui-manager.h"
#include "gui/ThemeEval.h"
#include "gui/widgets/scrollbar.h"
+#include "gui/animation/FluidScroll.h"
#include "gui/widget.h"
namespace GUI {
@@ -88,14 +89,19 @@ static const char *const gpl_text[] = {
AboutDialog::AboutDialog(bool inGame)
: Dialog(10, 20, 300, 174),
- _scrollPos(0), _scrollTime(0), _willClose(false), _autoScroll(true), _inGame(inGame),
+ _scrollPos(0.0f), _scrollTime(0), _willClose(false), _autoScroll(true), _inGame(inGame),
_isDragging(false), _dragLastY(0) {
+ _fluidScroller = new FluidScroller();
_scrollbar = nullptr;
_closeButton = nullptr;
reflowLayout();
}
+AboutDialog::~AboutDialog() {
+ delete _fluidScroller;
+}
+
void AboutDialog::buildLines() {
_lines.clear();
@@ -251,9 +257,10 @@ void AboutDialog::addLine(const Common::U32String &str) {
void AboutDialog::open() {
_scrollTime = g_system->getMillis() + kScrollStartDelay;
- _scrollPos = 0;
+ _scrollPos = 0.0f;
_willClose = false;
+ _fluidScroller->reset();
Dialog::open();
}
@@ -275,9 +282,14 @@ void AboutDialog::drawDialog(DrawLayer layerToDraw) {
// TODO: Maybe prerender all of the text into another surface,
// and then simply compose that over the screen surface
// in the right way. Should be even faster...
- const int firstLine = _scrollPos / _lineHeight;
- const int lastLine = MIN((_scrollPos + (_textRect.height())) / _lineHeight + 1, (uint32)_lines.size());
- int y = _y + _textRect.top - (_scrollPos % _lineHeight);
+ float visualScrollPos = _fluidScroller->getVisualPosition();
+ int firstLine = (int)floorf(visualScrollPos / (float)_lineHeight);
+ int lastLine = (int)floorf((visualScrollPos + (float)_textRect.height()) / (float)_lineHeight) + 1;
+ firstLine = CLIP(firstLine, 0, (int)_lines.size());
+ lastLine = CLIP(lastLine, 0, (int)_lines.size());
+
+ float yOffset = visualScrollPos - (float)firstLine * (float)_lineHeight;
+ int y = _y + _textRect.top - (int)yOffset;
for (int line = firstLine; line < lastLine; line++) {
Common::U32String str = _lines[line];
@@ -341,6 +353,18 @@ void AboutDialog::drawDialog(DrawLayer layerToDraw) {
void AboutDialog::handleTickle() {
const uint32 t = g_system->getMillis();
+
+ if (_fluidScroller->update(t, _scrollPos)) {
+ if (_scrollbar) {
+ _scrollbar->_currentPos = (int)_scrollPos;
+ _scrollbar->recalc();
+ }
+ drawDialog(kDrawLayerForeground);
+ // Update scrollTime to prevent jump (if auto-scroll resumes)
+ _scrollTime = t;
+ return;
+ }
+
int scrollOffset = ((int)t - (int)_scrollTime) / kScrollMillisPerPixel;
if (_autoScroll && scrollOffset > 0) {
int modifiers = g_system->getEventManager()->getModifierState();
@@ -355,13 +379,16 @@ void AboutDialog::handleTickle() {
_scrollTime = t;
if (_scrollPos < 0) {
- _scrollPos = 0;
- } else if ((uint32)_scrollPos > _lines.size() * _lineHeight) {
- _scrollPos = 0;
+ _scrollPos = 0.0f;
+ } else if (_scrollPos > (float)_lines.size() * (float)_lineHeight) {
+ _scrollPos = 0.0f;
_scrollTime += kScrollStartDelay;
}
+
+ _fluidScroller->setPosition(_scrollPos);
+
if (_scrollbar) {
- _scrollbar->_currentPos = _scrollPos;
+ _scrollbar->_currentPos = (int)_scrollPos;
_scrollbar->recalc();
}
drawDialog(kDrawLayerForeground);
@@ -369,7 +396,10 @@ void AboutDialog::handleTickle() {
}
void AboutDialog::handleMouseUp(int x, int y, int button, int clickCount) {
- _isDragging = false;
+ if (_isDragging) {
+ _isDragging = false;
+ _fluidScroller->startFling();
+ }
Dialog::handleMouseUp(x, y, button, clickCount);
}
@@ -377,6 +407,8 @@ void AboutDialog::handleMouseDown(int x, int y, int button, int clickCount) {
if (button == 1 && !findWidget(x, y)) {
_isDragging = true;
_dragLastY = y;
+ _autoScroll = false;
+ _fluidScroller->stopAnimation();
}
Dialog::handleMouseDown(x, y, button, clickCount);
}
@@ -388,27 +420,19 @@ void AboutDialog::handleMouseMoved(int x, int y, int button) {
if (deltaY != 0) {
_autoScroll = false;
- int buttonHeight = g_gui.xmlEval()->getVar("Globals.Button.Height", 24);
- int visibleHeight = _scrollbar ? _scrollbar->_entriesPerPage : (_h - buttonHeight - 20 - _yOff);
- int maxScroll = MAX(0, (int)(_lines.size() * _lineHeight) - visibleHeight);
-
- _scrollPos += deltaY;
-
- if (_scrollPos < 0)
- _scrollPos = 0;
- else if (_scrollPos > maxScroll)
- _scrollPos = maxScroll;
+ _fluidScroller->feedDrag(g_system->getMillis(), deltaY);
+ _scrollPos = _fluidScroller->getVisualPosition();
if (_scrollbar) {
- _scrollbar->_currentPos = _scrollPos;
+ _scrollbar->_currentPos = (int)_scrollPos;
_scrollbar->recalc();
}
drawDialog(kDrawLayerForeground);
}
+ } else {
+ Dialog::handleMouseMoved(x, y, button);
}
-
- Dialog::handleMouseMoved(x, y, button);
}
void AboutDialog::handleMouseWheel(int x, int y, int direction) {
@@ -418,19 +442,13 @@ void AboutDialog::handleMouseWheel(int x, int y, int direction) {
return;
_autoScroll = false;
-
- int buttonHeight = g_gui.xmlEval()->getVar("Globals.Button.Height", 24);
- int visibleHeight = _scrollbar ? _scrollbar->_entriesPerPage : (_h - buttonHeight - 20 - _yOff);
- int maxScroll = MAX(0, (int)(_lines.size() * _lineHeight) - visibleHeight);
+ _fluidScroller->stopAnimation();
_scrollPos += stepping;
- if (_scrollPos < 0)
- _scrollPos = 0;
- else if (_scrollPos > maxScroll)
- _scrollPos = maxScroll;
+ _scrollPos = _fluidScroller->setPosition(_scrollPos, true);
if (_scrollbar) {
- _scrollbar->_currentPos = _scrollPos;
+ _scrollbar->_currentPos = (int)_scrollPos;
_scrollbar->recalc();
}
@@ -439,8 +457,10 @@ void AboutDialog::handleMouseWheel(int x, int y, int direction) {
void AboutDialog::handleCommand(CommandSender *sender, uint32 cmd, uint32 data) {
if (cmd == kSetPositionCmd) {
- _scrollPos = data;
+ _scrollPos = (float)data;
_autoScroll = false;
+ _fluidScroller->stopAnimation();
+ _scrollPos = _fluidScroller->setPosition(_scrollPos, false);
drawDialog(kDrawLayerForeground);
} else if (cmd == kCloseCmd) {
close();
@@ -522,6 +542,9 @@ void AboutDialog::reflowLayout() {
screenArea.constrain(_x, _y, _w, _h);
buildLines();
+
+ int maxScroll = MAX(0, (int)(_lines.size() * _lineHeight) - _textRect.height());
+ _fluidScroller->setBounds((float)maxScroll, _textRect.height());
}
diff --git a/gui/about.h b/gui/about.h
index 253a9982b12..693c6ec2516 100644
--- a/gui/about.h
+++ b/gui/about.h
@@ -33,10 +33,11 @@ namespace GUI {
class EEHandler;
class ScrollBarWidget;
class ButtonWidget;
+class FluidScroller;
class AboutDialog : public Dialog {
protected:
- int _scrollPos;
+ float _scrollPos;
uint32 _scrollTime;
Common::U32StringArray _lines;
uint32 _lineHeight;
@@ -48,6 +49,7 @@ protected:
bool _isDragging;
int _dragLastY;
+ FluidScroller *_fluidScroller;
ScrollBarWidget *_scrollbar;
ButtonWidget *_closeButton;
Common::Rect _textRect;
@@ -59,6 +61,7 @@ protected:
public:
AboutDialog(bool inGame = false);
+ ~AboutDialog() override;
void open() override;
void close() override;
More information about the Scummvm-git-logs
mailing list