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 committed 2026-10-13 14:21:55 +02:00
1 parent db0e788d35
commit 5cfd91a309
4 files changed
+47 -44

No files matched your search

+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.