diff --git a/docs/HLSD/tooltip-illustrations.md b/docs/HLSD/tooltip-illustrations.md index 69f7cc717a..7f03de512f 100644 --- a/docs/HLSD/tooltip-illustrations.md +++ b/docs/HLSD/tooltip-illustrations.md @@ -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 diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index f580abfeb3..1686275a45 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -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(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 &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 &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 &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; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp index dc01a36295..7762b95e3e 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp @@ -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