Skip to content

Commit c77ba36

Browse files
Evgeni Raikhelclaude
andcommitted
Extract reusable composite_control_editor<T>, fix fade freeze, add focus-loss shortcut
New common/composite-control-editor.h: composite_control_editor<T> encapsulates the touch/fade/reset-on-retouch/hard-commit-on-lapse state machine previously hardcoded for MinZ, so any other multi-param composite option can reuse it by holding its own composite_control_editor<its-struct-type> instead of re-implementing the mechanism. embedded_filter_model now holds one _minz_editor member of this type instead of four separate fields; draw_minz_control_editor()/embedded_filter_enable_disable()/ populate_options() updated accordingly. Fixes a real bug found by testing: the fill-color fade was capped at 25% progress and then froze there for the rest of the 1.7s window before jumping to idle - looked like a stall, not an animation. Fade now tracks the full window uncapped, same as the border width gradient (400% -> 250%). Fill color is now gold/yellow fading toward the border's own blue (ImGuiCol_FrameBgHovered) rather than toward the panel background. Adds a focus-loss shortcut: if some OTHER widget elsewhere becomes active while this group is dirty (not just "nothing is active", which is also true during the normal gap between finishing one field and touching the next one in this group), finish the countdown immediately instead of making the user wait out the rest of it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 10fb6b4 commit c77ba36

4 files changed

Lines changed: 232 additions & 112 deletions

File tree

common/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ set(COMMON_SRC
7575
"${CMAKE_CURRENT_LIST_DIR}/hdr-model.h"
7676
"${CMAKE_CURRENT_LIST_DIR}/hdr-model.cpp"
7777
"${CMAKE_CURRENT_LIST_DIR}/embedded-filter-model.h"
78+
"${CMAKE_CURRENT_LIST_DIR}/composite-control-editor.h"
7879
"${CMAKE_CURRENT_LIST_DIR}/embedded-filter-model.cpp"
7980
"${CMAKE_CURRENT_LIST_DIR}/textual-icons.h"
8081
)

common/composite-control-editor.h

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

0 commit comments

Comments
 (0)