Skip to content

Commit ea529b9

Browse files
Request animation frames when handling events, not when receiving them
Move RequestAnimationFrame() from the two event producers (the TerminalInputParser callback and App::PostEvent) to the Event branch of App::Internal::HandleTask, below the internal-reply early-returns. A single place now decides which events grant an animation frame, next to the code discriminating event kinds. As a result, every internal terminal reply (cursor shape, terminal capabilities, name/version, emulator) stops arming animation frames: previously only cursor position reports were exempted, and only on the parser path. This also moves the animation_requested_ write onto the main loop thread, removing an unsynchronized write when PostEvent is called from another thread. Add a test covering the event -> animation tick pipeline. See #1302.
1 parent eb52ba1 commit ea529b9

3 files changed

Lines changed: 65 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ Next
88
- Bugfix: Fix high CPU usage (app and terminal emulator, e.g. tmux) caused by a
99
cursor position request/reply feedback loop redrawing the screen at ~60fps in
1010
the non-alternate-screen modes. See #1302.
11+
- Request the animation frame when an event is handled instead of when it is
12+
received. All internal terminal replies (not only cursor position reports)
13+
stop triggering animation frames.
1114

1215
7.0.1 (2026-07-14)
1316
------------------

src/ftxui/component/animation_test.cpp

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,18 @@
33
// the LICENSE file.
44

55
#include <gtest/gtest.h>
6+
#include <chrono> // for milliseconds
67
#include <functional> // for function
8+
#include <thread> // for sleep_for
79
#include <vector> // for allocator, vector
810

911
#include "ftxui/component/animation.hpp" // for Function, BackIn, BackInOut, BackOut, BounceIn, BounceInOut, BounceOut, CircularIn, CircularInOut, CircularOut, CubicIn, CubicInOut, CubicOut, ElasticIn, ElasticInOut, ElasticOut, ExponentialIn, ExponentialInOut, ExponentialOut, Linear, QuadraticIn, QuadraticInOut, QuadraticOut, QuarticIn, QuarticInOut, QuarticOut, QuinticIn, QuinticInOut, QuinticOut, SineIn, SineInOut, SineOut
12+
#include "ftxui/component/app.hpp" // for App
13+
#include "ftxui/component/component.hpp" // for Make
14+
#include "ftxui/component/component_base.hpp" // for ComponentBase
15+
#include "ftxui/component/event.hpp" // for Event
16+
#include "ftxui/component/loop.hpp" // for Loop
17+
#include "ftxui/dom/elements.hpp" // for text
1018

1119
namespace ftxui {
1220

@@ -35,4 +43,51 @@ TEST(AnimationTest, StartAndEnd) {
3543
}
3644
}
3745

46+
namespace {
47+
48+
class AnimationCounter : public ComponentBase {
49+
public:
50+
int count = 0;
51+
52+
private:
53+
Element OnRender() override { return text(""); }
54+
void OnAnimation(animation::Params& /*params*/) override { count++; }
55+
};
56+
57+
// Give the recurring AnimationTask (reposted every 15ms) time to become due,
58+
// then run the loop to execute it.
59+
void RunAnimationTask(Loop& loop) {
60+
std::this_thread::sleep_for(std::chrono::milliseconds(20));
61+
loop.RunOnce();
62+
}
63+
64+
} // namespace
65+
66+
// OnAnimation must tick once per handled event, and not tick for the
67+
// terminal's replies to FTXUI's own internal requests. See #1302.
68+
TEST(AnimationTest, EventRequestsAnimationFrame) {
69+
auto component = Make<AnimationCounter>();
70+
auto screen = App::FixedSize(10, 1);
71+
Loop loop(&screen, component);
72+
loop.RunOnce();
73+
74+
// No event: the recurring AnimationTask is a no-op.
75+
RunAnimationTask(loop);
76+
EXPECT_EQ(component->count, 0);
77+
78+
// A handled event grants exactly one animation tick.
79+
screen.PostEvent(Event::Custom);
80+
loop.RunOnce();
81+
RunAnimationTask(loop);
82+
EXPECT_EQ(component->count, 1);
83+
RunAnimationTask(loop);
84+
EXPECT_EQ(component->count, 1);
85+
86+
// An internal terminal reply must not grant one.
87+
screen.PostEvent(Event::CursorPosition("", 1, 1));
88+
loop.RunOnce();
89+
RunAnimationTask(loop);
90+
EXPECT_EQ(component->count, 1);
91+
}
92+
3893
} // namespace ftxui

src/ftxui/component/app.cpp

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -556,15 +556,7 @@ App::Internal::Internal(App* app,
556556
dimension_(dimension),
557557
use_alternative_screen_(use_alternative_screen),
558558
terminal_input_parser([&](Event event) {
559-
// Cursor position reports are terminal replies to our own \x1b[6n
560-
// requests, consumed internally. Requesting an animation frame for
561-
// them creates a redraw -> request -> reply -> redraw feedback loop.
562-
// See https://github.com/ArthurSonzogni/FTXUI/issues/1302.
563-
const bool internal_reply = event.is_cursor_position();
564559
event_buffer.Push(std::move(event));
565-
if (!internal_reply) {
566-
public_->RequestAnimationFrame();
567-
}
568560
}),
569561
cursor_position_request(this, [this] {
570562
TerminalSend(DeviceStatusReport(DSRMode::kCursor));
@@ -926,6 +918,13 @@ void App::Internal::HandleTask(Component component, Task& task) {
926918
#endif
927919

928920
frame_valid_ = false;
921+
922+
// Give animated components one tick in response to the event. This must
923+
// stay below the early-returns above: requesting a frame for the
924+
// terminal's replies to our own requests creates a redraw -> request ->
925+
// reply -> redraw feedback loop.
926+
// See https://github.com/ArthurSonzogni/FTXUI/issues/1302.
927+
public_->RequestAnimationFrame();
929928
return;
930929
}
931930

@@ -1541,7 +1540,6 @@ void App::Post(Task task) {
15411540

15421541
void App::PostEvent(Event event) {
15431542
internal_->event_buffer.Push(std::move(event));
1544-
RequestAnimationFrame();
15451543
}
15461544

15471545
// static

0 commit comments

Comments
 (0)