Texture displacement: no tooltip while a control is being used, and a wait

that finishes on its own

The hover delay only finished while the mouse was moving: a pointer resting
on a control produces no events, so nothing redrew the canvas and the wait
never reached its end - the tooltip appeared on the next pixel of movement
instead. It asked for its frames with schedule_extra_frame(), which only
arranges a timer whose event an idle event is assumed to follow, and
on_idle renders nothing unless the canvas was already dirty when it ran.
The wait and the animations now both use request_extra_frame(), which ends
on_idle with evt.RequestMore() and so obliges wx to deliver the next idle;
it is what 3DBed's fade uses. Asking for frames as fast as they come is
safe now that which frame is drawn comes from the clock rather than from
the frame count - a redraw that arrives early draws the same frame again.

Nothing explains a control while the control is being used, so no tooltip
appears while a slider is being dragged, a value typed into one, a combo
open or a layer card dragged. The pointer sits on the very thing being
edited, so a tooltip there covered the model exactly while the user was
watching it change. The full wait comes back afterwards rather than the
short one: having just let go of a slider is not a reason to be shown a
picture of it.

AnimatedTexture::texture_id() loses its time-to-next-frame out-parameter,
which nothing asks for any more.
This commit is contained in:
ExPikaPaka
2026-10-13 14:21:55 +02:00
parent db0e788d35
commit 5cfd91a309
4 changed files with 47 additions and 44 deletions
+14 -10
View File
@@ -50,8 +50,20 @@ dozen large panels over the scene. Once one has just been up, the wait drops to
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.
No tooltip appears at all while a control is being used - a slider dragged, a value typed into one, a
combo open, a layer card being dragged. The pointer sits on the very thing being edited, so a tooltip
there covers the model exactly while the user is watching it change. The full wait comes back
afterwards rather than the short one: having just let go of a slider is not a reason to be shown a
picture of it.
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().
to, so the wait has to ask for its own frames, and so does an animation once one is on screen. Both use
`GLCanvas3D::request_extra_frame()`, which ends `on_idle()` with `evt.RequestMore()` and so obliges wx
to deliver the next idle. `schedule_extra_frame()` is not enough on its own: it only arranges a timer
whose event an idle event is assumed to follow, and on_idle renders nothing unless the canvas was
already dirty when it ran. Asking for frames as fast as they come is safe here because which frame is
drawn comes from the clock, not from the frame count, so a redraw that arrives early simply draws the
same frame again.
## Decoding and playback
@@ -68,15 +80,7 @@ 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.
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
Which frame is drawn 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.
+3 -11
View File
@@ -207,26 +207,18 @@ void AnimatedTexture::reset()
m_duration = 0.;
}
unsigned int AnimatedTexture::texture_id(double time, double *time_to_next) const
unsigned int AnimatedTexture::texture_id(double time) const
{
if (m_frames.empty()) {
if (time_to_next != nullptr)
*time_to_next = 0.;
if (m_frames.empty())
return 0;
}
if (m_frames.size() == 1 || m_duration <= 0.) {
if (time_to_next != nullptr)
*time_to_next = 0.;
if (m_frames.size() == 1 || m_duration <= 0.)
return m_frames.front().texture->get_id();
}
const double t = time - m_duration * std::floor(time / m_duration);
// Frames are few; a linear walk beats keeping an index around and getting it stale.
size_t i = 0;
while (i + 1 < m_frames.size() && t >= m_frames[i].start + m_frames[i].delay)
++i;
if (time_to_next != nullptr)
*time_to_next = std::max(0., m_frames[i].start + m_frames[i].delay - t);
return m_frames[i].texture->get_id();
}
+2 -5
View File
@@ -39,11 +39,8 @@ public:
// Length of one loop, in seconds; 0 for a still image.
double duration() const { return m_duration; }
// The texture to draw `time` seconds into the loop (which wraps), and - when `time_to_next` is
// given - how much longer that frame stays up. A canvas that only redraws on demand uses the
// latter to ask for the next redraw at the right moment instead of spinning at full speed.
// 0, and a time_to_next of 0, when nothing is loaded.
unsigned int texture_id(double time, double *time_to_next = nullptr) const;
// The texture to draw `time` seconds into the loop, which wraps. 0 when nothing is loaded.
unsigned int texture_id(double time) const;
private:
struct Frame
@@ -3730,6 +3730,17 @@ void GLGizmoTextureDisplacement::panel_tooltip(const wxString &text, const std::
void GLGizmoTextureDisplacement::panel_tooltip(const std::string &text, const std::vector<TipMedia> &media)
{
// Nothing explains a control while the control is being used: dragging a slider, typing a value
// into one, picking from a combo, dragging a layer card. The pointer sits on the very thing being
// edited, so without this the tooltip covers the model exactly while the user is watching it change.
// The full wait comes back afterwards rather than the short one - having just let go of a slider is
// not a reason to be shown a picture of it.
if (ImGui::IsAnyItemActive() || ImGui::IsAnyMouseDown()) {
m_tip_item = ImVec2(-1.f, -1.f);
m_tip_last_shown = -1e9;
return;
}
// 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();
@@ -3739,10 +3750,16 @@ void GLGizmoTextureDisplacement::panel_tooltip(const std::string &text, const st
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);
// A pointer resting on a control produces no events at all, so unless the wait asks for frames
// itself nothing redraws the canvas and the wait never finishes - the tooltip then appears only
// on the next pixel of mouse movement.
//
// request_extra_frame() and not schedule_extra_frame(): the latter only arranges a timer, whose
// event an idle event is assumed to follow, and GLCanvas3D::on_idle() renders nothing unless the
// canvas was already dirty when it ran. request_extra_frame() ends on_idle with
// evt.RequestMore(), which obliges wx to deliver the next one. It is what 3DBed's fade uses.
// Costing a few hundred milliseconds of frames is the cheaper half of that trade.
m_parent.request_extra_frame();
return;
}
m_tip_last_shown = now;
@@ -3829,23 +3846,16 @@ void GLGizmoTextureDisplacement::render_tip_media(const std::vector<TipMedia> &m
}
ImGui::BeginGroup();
double next = 0.;
const unsigned int id = texture->texture_id(time, &next);
const unsigned int id = texture->texture_id(time);
ImGui::SetCursorPosX(cell_x + std::round(0.5f * (cell_w - w)));
ImGui::Image((ImTextureID) (intptr_t) id, ImVec2(w, h));
ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), frame, style.FrameRounding);
// The canvas only redraws on demand, so an animation has to ask for its own frames - and for
// exactly when the next one is due, rather than for a redraw as fast as the machine will go.
//
// Both halves are needed. schedule_extra_frame() alone only guarantees an idle event at that
// moment; GLCanvas3D::on_idle() returns without rendering anything unless something has marked
// the canvas dirty by then, which is why an animation left to that alone advanced one frame per
// mouse movement and stood still whenever the mouse did. set_dirty() is what on_idle reads
// (through GLGizmoBase::update_items_state()) to find out this gizmo still has frames to draw.
if (texture->is_animated()) {
set_dirty();
m_parent.schedule_extra_frame(int(next * 1000.0));
}
// The canvas only redraws on demand, so an animation has to ask for its own frames. It asks for
// them as fast as they come rather than pacing itself with a timer: which frame is drawn is
// decided by the clock above, so a redraw that arrives early simply draws the same frame again,
// and a guaranteed frame is worth more here than a frugal one that may never arrive.
if (texture->is_animated())
m_parent.request_extra_frame();
if (!entry.caption.empty()) {
// Centred under its own illustration, and never wider than its column.