|
| 1 | +// License: Apache 2.0. See LICENSE file in root directory. |
| 2 | +// Copyright(c) 2026 RealSense, Inc. All Rights Reserved. |
| 3 | + |
| 4 | +#pragma once |
| 5 | + |
| 6 | +#include <librealsense2/rs.hpp> |
| 7 | +#include <imgui.h> |
| 8 | + |
| 9 | +#include <algorithm> |
| 10 | +#include <functional> |
| 11 | +#include <limits> |
| 12 | +#include <memory> |
| 13 | +#include <string> |
| 14 | + |
| 15 | +namespace rs2 |
| 16 | +{ |
| 17 | + // Shared "under change" color ramp - gold (just touched) fading to the panel's own |
| 18 | + // ImGuiCol_FrameBgHovered blue (about to commit) - used by the framed group's own fill in |
| 19 | + // end_frame_and_maybe_commit() below, and by anything else (e.g. the row header's enable |
| 20 | + // toggle) that wants to visually echo the same pending-auto-commit state. `progress` is |
| 21 | + // 0 = just touched/still editing, 1 = about to fire. |
| 22 | + inline ImVec4 composite_control_dirty_blend( float progress ) |
| 23 | + { |
| 24 | + ImVec4 start_color( 255.f / 255.f, 210.f / 255.f, 40.f / 255.f, 90.f / 255.f ); // bright gold/yellow |
| 25 | + constexpr float initial_brightness = 1.2f; // "under change" color, 20% brighter |
| 26 | + start_color.x = std::min( start_color.x * initial_brightness, 1.0f ); |
| 27 | + start_color.y = std::min( start_color.y * initial_brightness, 1.0f ); |
| 28 | + start_color.z = std::min( start_color.z * initial_brightness, 1.0f ); |
| 29 | + const ImVec4 target_blue = ImGui::GetStyle().Colors[ImGuiCol_FrameBgHovered]; |
| 30 | + return ImVec4( |
| 31 | + start_color.x + ( target_blue.x - start_color.x ) * progress, |
| 32 | + start_color.y + ( target_blue.y - start_color.y ) * progress, |
| 33 | + start_color.z + ( target_blue.z - start_color.z ) * progress, |
| 34 | + start_color.w + ( target_blue.w - start_color.w ) * progress ); |
| 35 | + } |
| 36 | + |
| 37 | + // PROTOTYPE / DEMO: reusable debounced-auto-commit editor for a composite option's local |
| 38 | + // struct T. Extracted from the original HKR MinZ Control implementation so any other |
| 39 | + // multi-param composite option can get the same behavior - touch -> fade in -> reset on |
| 40 | + // retouch -> hard commit on lapse - by holding one of these instead of re-implementing the |
| 41 | + // mechanism. See embedded_filter_model::draw_minz_control_editor() for the reference usage. |
| 42 | + // |
| 43 | + // The whole framed group is treated as ONE editing unit, matching T being one atomic |
| 44 | + // multi-field struct sent in a single UVC transaction - there is no per-field Send. |
| 45 | + // |
| 46 | + // Per-frame usage: |
| 47 | + // 1. ensure_initialized(filter, id, error_message) - seeds `value` from a GET, once. |
| 48 | + // 2. Draw the control-specific widgets against `value`. For each one: |
| 49 | + // - call touch() on every change while the edit is in progress (e.g. every tick of a |
| 50 | + // slider drag) - this flags the group dirty and defers any pending commit; |
| 51 | + // - call finalize() once that field's edit is done (slider release via |
| 52 | + // ImGui::IsItemDeactivatedAfterEdit(), or immediately after a checkbox/radio click, |
| 53 | + // which has no drag phase so touch+finalize happen together) - this (re)schedules |
| 54 | + // the real auto-commit `commit_delay` seconds out. |
| 55 | + // 3. end_frame_and_maybe_commit(filter, id, error_message, frame_min, frame_max, |
| 56 | + // any_field_active_this_frame, before_commit) - draws the fading fill + shrinking border |
| 57 | + // around the widgets' bounding box (continuously, for the full commit_delay window - no |
| 58 | + // early freeze), shows the hover tooltip, and sends `value` to the device in one atomic |
| 59 | + // write once the debounce timer has lapsed with no further touch() calls. |
| 60 | + // `any_field_active_this_frame` is true if any of THIS control's own widgets is the one |
| 61 | + // currently active (mid-drag/mid-click) - pass false once focus has genuinely left the |
| 62 | + // group (some OTHER widget elsewhere is now active) to finish the countdown immediately |
| 63 | + // instead of making the user wait it out. `before_commit`, if set, is called with `value` |
| 64 | + // right before it's actually sent - this control's own code (which knows T's fields, |
| 65 | + // unlike this generic class) is the right place to enforce cross-field invariants, e.g. |
| 66 | + // "editing any field implies the control should be enabled too". |
| 67 | + template< typename T > |
| 68 | + class composite_control_editor |
| 69 | + { |
| 70 | + public: |
| 71 | + T value{}; |
| 72 | + bool initialized = false; |
| 73 | + |
| 74 | + // Seeds `value` from a real GET the first time this is called; a no-op afterward. Returns |
| 75 | + // whether `value` is safe to use (false only if the initial GET failed). `on_read`, if |
| 76 | + // set, is called with the freshly-read `value` right after this GET succeeds - the |
| 77 | + // caller (which knows T's fields, unlike this generic class) is the right place to log |
| 78 | + // what actually came back from the device. |
| 79 | + bool ensure_initialized( const std::shared_ptr< rs2::embedded_filter > & filter, |
| 80 | + rs2_composite_option_id id, |
| 81 | + std::string & error_message, |
| 82 | + const std::function< void( const T & ) > & on_read = nullptr ) |
| 83 | + { |
| 84 | + if( initialized ) |
| 85 | + return true; |
| 86 | + try |
| 87 | + { |
| 88 | + value = filter->get_composite_option_as< T >( id ); |
| 89 | + initialized = true; |
| 90 | + if( on_read ) |
| 91 | + on_read( value ); |
| 92 | + } |
| 93 | + catch( const std::exception & e ) |
| 94 | + { |
| 95 | + error_message = e.what(); |
| 96 | + } |
| 97 | + return initialized; |
| 98 | + } |
| 99 | + |
| 100 | + // Call while a field is actively being changed (every tick of a slider drag, or a |
| 101 | + // checkbox/radio click). Flags the group dirty and parks the deadline at +infinity so |
| 102 | + // nothing commits mid-edit. |
| 103 | + void touch() |
| 104 | + { |
| 105 | + _dirty = true; |
| 106 | + _commit_deadline = std::numeric_limits< double >::max(); |
| 107 | + } |
| 108 | + |
| 109 | + // Call once a field's edit is finalized (slider released, or immediately after a |
| 110 | + // checkbox/radio click). Schedules the real auto-commit commit_delay seconds out. |
| 111 | + void finalize() { _commit_deadline = ImGui::GetTime() + commit_delay; } |
| 112 | + |
| 113 | + // Side-effect-free readout of the same progress end_frame_and_maybe_commit() animates |
| 114 | + // with: false if nothing is pending, true with `progress` in [0,1] (0 = just |
| 115 | + // touched/still mid-edit, 1 = about to commit) otherwise. Lets other UI elements for the |
| 116 | + // same control (e.g. an enable toggle drawn elsewhere) mirror the pending-commit state |
| 117 | + // without duplicating the deadline math. |
| 118 | + bool try_get_progress( float & progress ) const |
| 119 | + { |
| 120 | + if( ! _dirty ) |
| 121 | + return false; |
| 122 | + double remaining = _commit_deadline - ImGui::GetTime(); |
| 123 | + double frac_remaining = std::min( std::max( remaining / commit_delay, 0.0 ), 1.0 ); |
| 124 | + progress = static_cast< float >( 1.0 - frac_remaining ); |
| 125 | + return true; |
| 126 | + } |
| 127 | + |
| 128 | + // Draws the dirty-state fill/border for [frame_min, frame_max] - call after drawing this |
| 129 | + // control's own fields, once their bounding box is known - and sends `value` to the |
| 130 | + // device in one atomic write if the debounce timer has lapsed since the last touch(). |
| 131 | + void end_frame_and_maybe_commit( const std::shared_ptr< rs2::embedded_filter > & filter, |
| 132 | + rs2_composite_option_id id, |
| 133 | + std::string & error_message, |
| 134 | + const ImVec2 & frame_min, |
| 135 | + const ImVec2 & frame_max, |
| 136 | + bool any_field_active_this_frame, |
| 137 | + const std::function< void( T & ) > & before_commit = nullptr ) |
| 138 | + { |
| 139 | + constexpr float base_border_thickness = 1.0f; |
| 140 | + float border_thickness = base_border_thickness; |
| 141 | + |
| 142 | + // While actively being edited (deadline parked at +infinity by touch()) the fill and |
| 143 | + // border sit at their starting intensity: 0% faded (bright gold), 400% border width. |
| 144 | + // Once a countdown is actually running (finalize() set a real deadline), both fade |
| 145 | + // smoothly over the ENTIRE commit_delay window, right up until the instant the commit |
| 146 | + // fires below - no early cap/freeze partway through, or the animation visibly stalls |
| 147 | + // for the remainder of the wait, which reads as a bug rather than a fade. The fill |
| 148 | + // cools from gold toward the border's own blue (ImGuiCol_FrameBgHovered - the same |
| 149 | + // color the border is drawn in, so the fill settles into harmony with it rather than |
| 150 | + // into the panel background); the border shrinks from 400% down to 250% of normal. |
| 151 | + // The commit itself then snaps both back to their plain idle look (no fill, 100% |
| 152 | + // border) in one abrupt jump rather than continuing the gentle fade, so the moment |
| 153 | + // something was actually sent reads as a distinct, more pronounced change than the |
| 154 | + // animation leading up to it. |
| 155 | + float progress = 0.0f; |
| 156 | + if( try_get_progress( progress ) ) |
| 157 | + { |
| 158 | + ImVec4 blended = composite_control_dirty_blend( progress ); |
| 159 | + |
| 160 | + ImGui::GetWindowDrawList()->AddRectFilled( frame_min, frame_max, ImGui::ColorConvertFloat4ToU32( blended ), 3.0f ); |
| 161 | + border_thickness = base_border_thickness * ( border_start_scale + ( border_end_scale - border_start_scale ) * progress ); |
| 162 | + } |
| 163 | + |
| 164 | + ImGui::GetWindowDrawList()->AddRect( |
| 165 | + frame_min, frame_max, |
| 166 | + ImGui::GetColorU32( ImGuiCol_FrameBgHovered ), |
| 167 | + 3.0f, // rounding |
| 168 | + 0, // flags |
| 169 | + border_thickness ); |
| 170 | + |
| 171 | + if( ImGui::IsMouseHoveringRect( frame_min, frame_max ) ) |
| 172 | + { |
| 173 | + try |
| 174 | + { |
| 175 | + ImGui::SetTooltip( "%s", filter->get_composite_option_description( id ) ); |
| 176 | + } |
| 177 | + catch( const std::exception & ) |
| 178 | + { |
| 179 | + // Best-effort tooltip only - a failure here shouldn't disrupt the editor. |
| 180 | + } |
| 181 | + } |
| 182 | + |
| 183 | + // Shortcut: focus genuinely left the group - some OTHER widget elsewhere is active |
| 184 | + // this frame, not just "nothing is active right now" (which is also true during the |
| 185 | + // normal quiet gap between finishing one field and touching the next one in THIS |
| 186 | + // group, and must NOT cut the wait short). When that happens, don't make the user |
| 187 | + // wait out the rest of the countdown - finish it now, same as if it had lapsed |
| 188 | + // naturally. |
| 189 | + if( _dirty && ! any_field_active_this_frame && ImGui::IsAnyItemActive() ) |
| 190 | + _commit_deadline = ImGui::GetTime(); |
| 191 | + |
| 192 | + // Fires once the countdown elapses quietly - checked every frame, so any fresh |
| 193 | + // touch() (which re-parks the deadline at +infinity) naturally defers this for as |
| 194 | + // long as the user keeps adjusting fields. |
| 195 | + if( _dirty && ImGui::GetTime() >= _commit_deadline ) |
| 196 | + { |
| 197 | + try |
| 198 | + { |
| 199 | + if( before_commit ) |
| 200 | + before_commit( value ); |
| 201 | + filter->set_composite_option_from( id, value ); |
| 202 | + } |
| 203 | + catch( const std::exception & e ) |
| 204 | + { |
| 205 | + error_message = e.what(); |
| 206 | + } |
| 207 | + _dirty = false; |
| 208 | + _commit_deadline = std::numeric_limits< double >::max(); |
| 209 | + } |
| 210 | + } |
| 211 | + |
| 212 | + private: |
| 213 | + bool _dirty = false; |
| 214 | + double _commit_deadline = std::numeric_limits< double >::max(); |
| 215 | + |
| 216 | + static constexpr double commit_delay = 1.7; // seconds of quiet before auto-sending |
| 217 | + static constexpr float border_start_scale = 4.0f; // 400% of normal width, right after an edit |
| 218 | + static constexpr float border_end_scale = 2.5f; // 250% of normal width, right before commit |
| 219 | + }; |
| 220 | +} |
0 commit comments