Texture displacement: tooltip timing on a real clock, and a hover delay

The animations crawled when left alone and raced while the mouse moved.
They were timed by ImGui::GetTime(), and nothing in OrcaSlicer ever sets
io.DeltaTime, so it keeps imgui's default of 1/60 per frame: imgui's clock
counts frames, not seconds. On a canvas that renders only when something
asks it to, that is not a clock. Playback now reads a steady_clock, so a
frame is due when it is due whatever the canvas is doing.

An animation also restarts when its tooltip opens again, not only when a
different one opens: a gap in the frames it was drawn on is what that
looks like from here.

Tooltips now wait for the pointer to rest on a control for 0.45 s. The
panel is dense and a tooltip lands on the model beside it, so with no wait
a sweep from the top of the panel down to Bake flashed a dozen large panels
over the scene. After one has just been up the wait drops to 0.1 s for 0.7
s, so reading several settings in a row stays quick - but not to zero, or
that same sweep would flash again once one tooltip had been seen. A pointer
resting on a control produces no events, so the wait asks for the frame it
will appear on itself.
This commit is contained in:
ExPikaPaka
2026-10-13 13:58:52 +02:00
parent d2ba88c93d
commit db0e788d35
3 changed files with 71 additions and 6 deletions
+18 -2
View File
@@ -42,6 +42,17 @@ A file that is missing or will not decode is skipped, and a tooltip whose pictur
exactly the text tooltip it was before. So the art for a tooltip can land after the tooltip does, and
a half-finished set never breaks the panel.
## When a tooltip appears
A tooltip waits for the pointer to rest on its control for 0.45 s. The panel is dense and the tooltip
lands on the model beside it, so with no wait a sweep from the top of the panel down to Bake flashes a
dozen large panels over the scene. Once one has just been up, the wait drops to 0.1 s for 0.7 s
afterwards, so reading several settings in a row stays quick - but not to zero, or that same sweep
would flash again the moment one tooltip had been seen.
A pointer resting on a control produces no events, and the canvas only renders when something asks it
to, so the wait itself has to keep asking; see panel_tooltip().
## Decoding and playback
`slic3r/GUI/AnimatedTexture.hpp` decodes a file once and uploads one GL texture per frame. Stills go
@@ -57,14 +68,19 @@ The animation restarts when its tooltip opens, rather than running off a free-ru
short loops of one setting going from one value to another, and joining one halfway through shows the
end of a change the user never saw the start of.
Driving the frames takes two calls, not one. `GLCanvas3D::schedule_extra_frame()` only guarantees an
idle event at the moment the next frame is due; `GLCanvas3D::on_idle()` returns without rendering
Two things drive the frames, and both are needed. `GLCanvas3D::schedule_extra_frame()` only guarantees
an idle event at the moment the next frame is due; `GLCanvas3D::on_idle()` returns without rendering
anything unless something has marked the canvas dirty by then, so an animation left to the timer alone
advances one frame per mouse movement and stands still whenever the mouse does. `set_dirty()` on the
gizmo is what on_idle reads, through `GLGizmoBase::update_items_state()`, to find out there are frames
still to draw. The pair of them is what makes an animation play at its own speed and stop costing
anything the moment its tooltip closes.
Which frame is due is measured on a real clock, not `ImGui::GetTime()`. Nothing in OrcaSlicer ever sets
`io.DeltaTime`, so it keeps imgui's default of 1/60 per frame and imgui's clock counts frames rather
than seconds. On a canvas that renders only when asked, that is not a clock at all: an animation driven
by it crawls while the canvas is idle and races while a moving mouse is rendering it flat out.
## Making one
`scripts/gen_texture_displacement_tips.py` draws the lot, in four languages, picked by what the
@@ -3701,6 +3701,28 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
m_panel_icon_map[names[i]] = icons[i];
}
// Seconds on a real clock, for the tooltips' own timing.
//
// Not ImGui::GetTime(): nothing in OrcaSlicer ever sets io.DeltaTime, so it keeps imgui's default of
// 1/60 per frame and its clock counts frames rather than seconds. On a canvas that renders only when
// something asks it to, that is not a clock at all - an animation driven by it crawled while the canvas
// was idle and raced while a moving mouse was rendering it flat out.
static double tip_now()
{
using clock = std::chrono::steady_clock;
static const clock::time_point base = clock::now();
return std::chrono::duration<double>(clock::now() - base).count();
}
// How long the pointer has to rest on a control before its tooltip appears. The panel is dense and the
// tooltip lands on the model beside it, so with no wait at all a sweep from the top of the panel down to
// Bake flashes a dozen large panels over the scene. Once one has just been up the wait drops to almost
// nothing, so reading several settings in a row stays quick - but not to nothing, or that same sweep
// would flash again the moment one tooltip had been seen.
static constexpr double TIP_DELAY = 0.45;
static constexpr double TIP_DELAY_AGAIN = 0.10;
static constexpr double TIP_STICKY = 0.70;
void GLGizmoTextureDisplacement::panel_tooltip(const wxString &text, const std::vector<TipMedia> &media)
{
panel_tooltip(into_u8(text), media);
@@ -3708,6 +3730,23 @@ void GLGizmoTextureDisplacement::panel_tooltip(const wxString &text, const std::
void GLGizmoTextureDisplacement::panel_tooltip(const std::string &text, const std::vector<TipMedia> &media)
{
// The item's position stands in for its identity: moving to another control restarts the wait, and
// staying on this one carries it on.
const double now = tip_now();
const ImVec2 item = ImGui::GetItemRectMin();
if (item.x != m_tip_item.x || item.y != m_tip_item.y) {
m_tip_item = item;
m_tip_hover_since = now;
}
if (now - m_tip_hover_since < (now - m_tip_last_shown < TIP_STICKY ? TIP_DELAY_AGAIN : TIP_DELAY)) {
// A pointer resting on a control produces no events, so nothing would redraw the canvas and the
// tooltip would never get its chance to appear. Ask for the frame it appears on.
set_dirty();
m_parent.schedule_extra_frame(40);
return;
}
m_tip_last_shown = now;
const ImGuiStyle &style = ImGui::GetStyle();
const float gap = m_imgui->scaled(0.5f);
const float wrap = m_imgui->scaled(20.f);
@@ -3757,11 +3796,15 @@ void GLGizmoTextureDisplacement::render_tip_media(const std::vector<TipMedia> &m
// Each animation plays from its first frame when its tooltip opens: these are short loops showing
// one setting going from one value to another, and joining one halfway through shows the user the
// end of a change they never saw the start of.
if (key != m_tip_media_key) {
// Restart on a different tooltip, and on the same one opening again - which is what a gap in the
// frames it was drawn on means, since it is drawn on every frame for as long as it is on screen.
const int this_frame = ImGui::GetFrameCount();
if (key != m_tip_media_key || this_frame != m_tip_media_frame + 1) {
m_tip_media_key = key;
m_tip_media_start = ImGui::GetTime();
m_tip_media_start = tip_now();
}
const double time = ImGui::GetTime() - m_tip_media_start;
m_tip_media_frame = this_frame;
const double time = tip_now() - m_tip_media_start;
const ImGuiStyle &style = ImGui::GetStyle();
const float spacing = style.ItemSpacing.x;
@@ -1059,7 +1059,13 @@ private:
// from its first frame each time its tooltip opens, rather than from wherever a free-running clock
// happened to be.
std::string m_tip_media_key;
double m_tip_media_start = 0.;
double m_tip_media_start = 0.;
int m_tip_media_frame = -2; // the frame it was last drawn on, to spot the tooltip reopening
// How long the pointer has rested on the control a tooltip belongs to, and when one was last on
// screen, both on the real clock tip_now() keeps (see the .cpp for why ImGui's own will not do).
ImVec2 m_tip_item{-1.f, -1.f};
double m_tip_hover_since = 0.;
double m_tip_last_shown = -1e9;
};
} // namespace Slic3r::GUI