From 5cfd91a3093713782eeb1a5234a18803ee826d8d Mon Sep 17 00:00:00 2001 From: ExPikaPaka Date: Tue, 13 Oct 2026 14:21:55 +0200 Subject: [PATCH] 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. --- docs/HLSD/tooltip-illustrations.md | 24 ++++++---- src/slic3r/GUI/AnimatedTexture.cpp | 14 ++---- src/slic3r/GUI/AnimatedTexture.hpp | 7 +-- .../GUI/Gizmos/GLGizmoTextureDisplacement.cpp | 46 +++++++++++-------- 4 files changed, 47 insertions(+), 44 deletions(-) diff --git a/docs/HLSD/tooltip-illustrations.md b/docs/HLSD/tooltip-illustrations.md index 7f03de512f..1189c89c3f 100644 --- a/docs/HLSD/tooltip-illustrations.md +++ b/docs/HLSD/tooltip-illustrations.md @@ -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. diff --git a/src/slic3r/GUI/AnimatedTexture.cpp b/src/slic3r/GUI/AnimatedTexture.cpp index cb0adf58a1..fee8688ba5 100644 --- a/src/slic3r/GUI/AnimatedTexture.cpp +++ b/src/slic3r/GUI/AnimatedTexture.cpp @@ -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(); } diff --git a/src/slic3r/GUI/AnimatedTexture.hpp b/src/slic3r/GUI/AnimatedTexture.hpp index ab05aaecf8..be6a0c8b7f 100644 --- a/src/slic3r/GUI/AnimatedTexture.hpp +++ b/src/slic3r/GUI/AnimatedTexture.hpp @@ -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 diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 1686275a45..2b04e0eef7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -3730,6 +3730,17 @@ void GLGizmoTextureDisplacement::panel_tooltip(const wxString &text, const std:: void GLGizmoTextureDisplacement::panel_tooltip(const std::string &text, const std::vector &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 &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.