diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 55f7a1d5..721ce6a7 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -1233,6 +1233,236 @@ static std::string FormatTimelineTime(double time_in_s) { return buf; } +void App::TimelineScrubberGui() { + // Timeline scrubber: spine + sliding knob widget. + const double max_time = tmp_.sim_head_time_; + const int hist_size = sim_history_.Size(); + const int hist_min = 1 - hist_size; // most negative index (oldest) + const int hist_max = 0; // index 0 = most recent + int current_index = sim_history_.GetIndex(); + const bool locked = (hist_size <= 1); + + // Compute current timestamp for LH label. + double curr_time = + (has_data() && current_index == sim_history_.GetIndex()) + ? data()->time + : (max_time + + current_index * + (has_model() ? model()->opt.timestep : 0.002)); + if (curr_time < 0.0) curr_time = 0.0; + + const int curr_step = (has_model() && model()->opt.timestep > 0) + ? static_cast(std::round( + curr_time / model()->opt.timestep)) + : 0; + const int max_step = + (has_model() && model()->opt.timestep > 0) + ? static_cast(std::round(max_time / model()->opt.timestep)) + : 0; + + // Both LH and RH labels aim to be at 3 digits, switching units + // independently. + std::string lh_top_str = FormatTimelineTime(curr_time); + std::string rh_top_str = FormatTimelineTime(max_time); + std::string lh_bot_str = "(" + std::to_string(curr_step) + ")"; + std::string rh_bot_str = "(" + std::to_string(max_step) + ")"; + const char* lh_top_label = lh_top_str.c_str(); + const char* rh_top_label = rh_top_str.c_str(); + const char* lh_bot_label = lh_bot_str.c_str(); + const char* rh_bot_label = rh_bot_str.c_str(); + + // Use slightly smaller text for timeline labels (e.g. 90% scale). + ImGui::SetWindowFontScale(0.9f); + + const ImVec2 cursor = ImGui::GetCursorScreenPos(); + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float base_label_w = + std::max(ImGui::CalcTextSize("88.8 ms").x, + ImGui::CalcTextSize("88.8 \xC2\xB5s").x); + const float curr_lh_w = + std::max({base_label_w, ImGui::CalcTextSize(lh_top_label).x, + ImGui::CalcTextSize(lh_bot_label).x}); + if (curr_lh_w > tmp_.timeline_lh_width) { + tmp_.timeline_lh_width = curr_lh_w; + } + const float curr_rh_w = + std::max({base_label_w, ImGui::CalcTextSize(rh_top_label).x, + ImGui::CalcTextSize(rh_bot_label).x}); + if (curr_rh_w > tmp_.timeline_rh_width) { + tmp_.timeline_rh_width = curr_rh_w; + } + const float lh_box_w = tmp_.timeline_lh_width; + const float rh_box_w = tmp_.timeline_rh_width; + const float track_w = + std::max(10.0f, ImGui::GetContentRegionAvail().x - lh_box_w - + rh_box_w - spacing * 2.0f); + + const float spine_h = 3.0f; + const float knob_w = 12.0f; + const float knob_h = 27.0f; // 1.5x taller than 18.0f + const float text_line_h = ImGui::GetTextLineHeight(); + const float custom_line_spacing = + 1.0f; // Decreased spacing between the two lines + const float text_2lines_h = text_line_h * 2.0f + custom_line_spacing; + const float total_h = std::max(knob_h, text_2lines_h); + + // Vertical center of the track row. + const float row_center_y = cursor.y + total_h * 0.5f; + const float track_x0 = cursor.x + lh_box_w + spacing; + const float spine_y0 = row_center_y - spine_h * 0.5f; + const float spine_y1 = row_center_y + spine_h * 0.5f; + const float spine_x0 = track_x0; + const float spine_x1 = track_x0 + track_w; + + // Compute knob position [0,1] along the spine. + float t = 1.0f; // Default: pinned to right end. + if (!locked) { + t = static_cast(current_index - hist_min) / + static_cast(hist_max - hist_min); + } + const float knob_cx = spine_x0 + t * (track_w - knob_w) + knob_w * 0.5f; + + // Invisible interaction button over the full track area. + ImGui::SetCursorScreenPos( + ImVec2(track_x0, row_center_y - total_h * 0.5f)); + ImGui::InvisibleButton("##Scrubber", ImVec2(track_w, total_h)); + const bool hovered = ImGui::IsItemHovered(); + const bool active = ImGui::IsItemActive(); + + // Handle dragging. + if (!locked && (active || (hovered && ImGui::IsMouseDown( + ImGuiMouseButton_Left)))) { + const float mouse_x = ImGui::GetIO().MousePos.x; + if (!tmp_.scrubber_active) { + tmp_.scrubber_active = true; + if (std::abs(mouse_x - knob_cx) <= knob_w) { + tmp_.scrubber_grab_offset = mouse_x - knob_cx; + } else { + tmp_.scrubber_grab_offset = 0.0f; + } + } + + const float effective_mouse_x = mouse_x - tmp_.scrubber_grab_offset; + const float raw_t = (effective_mouse_x - spine_x0 - knob_w * 0.5f) / + std::max(1.0f, track_w - knob_w); + const float clamped_t = std::clamp(raw_t, 0.0f, 1.0f); + + int new_index = current_index; + const float snap_eps = std::max( + 0.02f, std::min(0.05f, 8.0f / std::max(1.0f, track_w - knob_w))); + if (clamped_t <= snap_eps || + effective_mouse_x <= spine_x0 + knob_w * 0.5f + 6.0f) { + new_index = hist_min; + } else if (clamped_t >= 1.0f - snap_eps || + effective_mouse_x >= spine_x1 - knob_w * 0.5f - 6.0f) { + new_index = hist_max; + } else { + const float inner_t = + (clamped_t - snap_eps) / (1.0f - 2.0f * snap_eps); + new_index = + hist_min + + static_cast(std::round(inner_t * (hist_max - hist_min))); + } + + if (new_index != current_index) { + LoadHistory(new_index); + current_index = new_index; + t = static_cast(current_index - hist_min) / + static_cast(hist_max - hist_min); + if (has_data() && current_index == sim_history_.GetIndex()) { + curr_time = data()->time; + if (curr_time < 0.0) curr_time = 0.0; + lh_top_str = FormatTimelineTime(curr_time); + lh_top_label = lh_top_str.c_str(); + const int updated_curr_step = + (has_model() && model()->opt.timestep > 0) + ? static_cast( + std::round(curr_time / model()->opt.timestep)) + : 0; + lh_bot_str = "(" + std::to_string(updated_curr_step) + ")"; + lh_bot_label = lh_bot_str.c_str(); + const float updated_lh_w = + std::max({base_label_w, ImGui::CalcTextSize(lh_top_label).x, + ImGui::CalcTextSize(lh_bot_label).x}); + if (updated_lh_w > tmp_.timeline_lh_width) { + tmp_.timeline_lh_width = updated_lh_w; + } + } + } + } else { + tmp_.scrubber_active = false; + } + + // Draw LH labels (two lines, both right-aligned). + const float text_y0 = row_center_y - text_2lines_h * 0.5f; + const float text_y1 = text_y0 + text_line_h + custom_line_spacing; + + const float lh_top_x = + cursor.x + lh_box_w - ImGui::CalcTextSize(lh_top_label).x; + ImGui::SetCursorScreenPos(ImVec2(lh_top_x, text_y0)); + ImGui::TextUnformatted(lh_top_label); + + const float lh_bot_x = + cursor.x + lh_box_w - ImGui::CalcTextSize(lh_bot_label).x; + ImGui::SetCursorScreenPos(ImVec2(lh_bot_x, text_y1)); + ImGui::TextUnformatted(lh_bot_label); + + // Draw RH labels (two lines, both left-aligned). + const float rh_x0 = track_x0 + track_w + spacing; + ImGui::SetCursorScreenPos(ImVec2(rh_x0, text_y0)); + ImGui::TextUnformatted(rh_top_label); + + ImGui::SetCursorScreenPos(ImVec2(rh_x0, text_y1)); + ImGui::TextUnformatted(rh_bot_label); + + // Restore window font scale. + ImGui::SetWindowFontScale(1.0f); + + // Draw spine and knob on the draw list. + ImDrawList* dl = ImGui::GetWindowDrawList(); + const ImGuiStyle& style = ImGui::GetStyle(); + + // Spine. + const ImVec4 scrollbar_bg = + ImGui::GetStyle().Colors[ImGuiCol_ScrollbarBg]; + ImGuiCol bg_col_idx; + if (active) { + bg_col_idx = ImGuiCol_FrameBgActive; + } else if (hovered) { + bg_col_idx = ImGuiCol_FrameBgHovered; + } else { + bg_col_idx = (scrollbar_bg.w > 0.01f) ? ImGuiCol_ScrollbarBg + : ImGuiCol_FrameBg; + } + const ImU32 spine_col = ImGui::GetColorU32(bg_col_idx); + dl->AddRectFilled(ImVec2(spine_x0, spine_y0), + ImVec2(spine_x1, spine_y1), spine_col, + spine_h * 0.5f); + + // Knob rectangle. + const float knob_x0 = knob_cx - knob_w * 0.5f; + const float knob_x1 = knob_cx + knob_w * 0.5f; + const float knob_y0 = row_center_y - knob_h * 0.5f; + const float knob_y1 = row_center_y + knob_h * 0.5f; + + ImU32 knob_col; + if (active) { + knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrabActive); + } else if (hovered) { + knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrab); + } else { + knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrab); + } + dl->AddRectFilled(ImVec2(knob_x0, knob_y0), ImVec2(knob_x1, knob_y1), + knob_col, style.GrabRounding); + // Knob border. + dl->AddRect(ImVec2(knob_x0, knob_y0), ImVec2(knob_x1, knob_y1), + ImGui::GetColorU32(ImGuiCol_Border), style.GrabRounding); + + // Advance layout cursor past this row. + ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + total_h)); +} + void App::ModelOptionsGui() { const float min_width = platform::GetExpectedLabelWidth(); const ImGuiChildFlags child_flags = @@ -1378,235 +1608,8 @@ void App::ModelOptionsGui() { } ImGui::SetItemTooltip("%s", "Load next frame from history / Single step"); - // Timeline scrubber: spine + sliding knob widget. - { - const double max_time = tmp_.sim_head_time_; - const int hist_size = sim_history_.Size(); - const int hist_min = 1 - hist_size; // most negative index (oldest) - const int hist_max = 0; // index 0 = most recent - int current_index = sim_history_.GetIndex(); - const bool locked = (hist_size <= 1); - - // Compute current timestamp for LH label. - double curr_time = - (has_data() && current_index == sim_history_.GetIndex()) - ? data()->time - : (max_time + - current_index * - (has_model() ? model()->opt.timestep : 0.002)); - if (curr_time < 0.0) curr_time = 0.0; - - const int curr_step = (has_model() && model()->opt.timestep > 0) - ? static_cast(std::round( - curr_time / model()->opt.timestep)) - : 0; - const int max_step = - (has_model() && model()->opt.timestep > 0) - ? static_cast(std::round(max_time / model()->opt.timestep)) - : 0; - - // Both LH and RH labels aim to be at 3 digits, switching units - // independently. - std::string lh_top_str = FormatTimelineTime(curr_time); - std::string rh_top_str = FormatTimelineTime(max_time); - std::string lh_bot_str = "(" + std::to_string(curr_step) + ")"; - std::string rh_bot_str = "(" + std::to_string(max_step) + ")"; - const char* lh_top_label = lh_top_str.c_str(); - const char* rh_top_label = rh_top_str.c_str(); - const char* lh_bot_label = lh_bot_str.c_str(); - const char* rh_bot_label = rh_bot_str.c_str(); - - // Use slightly smaller text for timeline labels (e.g. 90% scale). - ImGui::SetWindowFontScale(0.9f); - - const ImVec2 cursor = ImGui::GetCursorScreenPos(); - const float spacing = ImGui::GetStyle().ItemSpacing.x; - const float base_label_w = - std::max(ImGui::CalcTextSize("88.8 ms").x, - ImGui::CalcTextSize("88.8 \xC2\xB5s").x); - const float curr_lh_w = - std::max({base_label_w, ImGui::CalcTextSize(lh_top_label).x, - ImGui::CalcTextSize(lh_bot_label).x}); - if (curr_lh_w > tmp_.timeline_lh_width) { - tmp_.timeline_lh_width = curr_lh_w; - } - const float curr_rh_w = - std::max({base_label_w, ImGui::CalcTextSize(rh_top_label).x, - ImGui::CalcTextSize(rh_bot_label).x}); - if (curr_rh_w > tmp_.timeline_rh_width) { - tmp_.timeline_rh_width = curr_rh_w; - } - const float lh_box_w = tmp_.timeline_lh_width; - const float rh_box_w = tmp_.timeline_rh_width; - const float track_w = - std::max(10.0f, ImGui::GetContentRegionAvail().x - lh_box_w - - rh_box_w - spacing * 2.0f); - - const float spine_h = 3.0f; - const float knob_w = 12.0f; - const float knob_h = 27.0f; // 1.5x taller than 18.0f - const float text_line_h = ImGui::GetTextLineHeight(); - const float custom_line_spacing = - 1.0f; // Decreased spacing between the two lines - const float text_2lines_h = text_line_h * 2.0f + custom_line_spacing; - const float total_h = std::max(knob_h, text_2lines_h); - - // Vertical center of the track row. - const float row_center_y = cursor.y + total_h * 0.5f; - const float track_x0 = cursor.x + lh_box_w + spacing; - const float spine_y0 = row_center_y - spine_h * 0.5f; - const float spine_y1 = row_center_y + spine_h * 0.5f; - const float spine_x0 = track_x0; - const float spine_x1 = track_x0 + track_w; - - // Compute knob position [0,1] along the spine. - float t = 1.0f; // Default: pinned to right end. - if (!locked) { - t = static_cast(current_index - hist_min) / - static_cast(hist_max - hist_min); - } - const float knob_cx = spine_x0 + t * (track_w - knob_w) + knob_w * 0.5f; - - // Invisible interaction button over the full track area. - ImGui::SetCursorScreenPos( - ImVec2(track_x0, row_center_y - total_h * 0.5f)); - ImGui::InvisibleButton("##TimelineScrubber", ImVec2(track_w, total_h)); - const bool hovered = ImGui::IsItemHovered(); - const bool active = ImGui::IsItemActive(); - - // Handle dragging. - if (!locked && (active || (hovered && ImGui::IsMouseDown( - ImGuiMouseButton_Left)))) { - const float mouse_x = ImGui::GetIO().MousePos.x; - if (!tmp_.scrubber_active) { - tmp_.scrubber_active = true; - if (std::abs(mouse_x - knob_cx) <= knob_w) { - tmp_.scrubber_grab_offset = mouse_x - knob_cx; - } else { - tmp_.scrubber_grab_offset = 0.0f; - } - } - - const float effective_mouse_x = mouse_x - tmp_.scrubber_grab_offset; - const float raw_t = (effective_mouse_x - spine_x0 - knob_w * 0.5f) / - std::max(1.0f, track_w - knob_w); - const float clamped_t = std::clamp(raw_t, 0.0f, 1.0f); - - int new_index = current_index; - const float snap_eps = std::max( - 0.02f, std::min(0.05f, 8.0f / std::max(1.0f, track_w - knob_w))); - if (clamped_t <= snap_eps || - effective_mouse_x <= spine_x0 + knob_w * 0.5f + 6.0f) { - new_index = hist_min; - } else if (clamped_t >= 1.0f - snap_eps || - effective_mouse_x >= spine_x1 - knob_w * 0.5f - 6.0f) { - new_index = hist_max; - } else { - const float inner_t = - (clamped_t - snap_eps) / (1.0f - 2.0f * snap_eps); - new_index = - hist_min + - static_cast(std::round(inner_t * (hist_max - hist_min))); - } - - if (new_index != current_index) { - LoadHistory(new_index); - current_index = new_index; - t = static_cast(current_index - hist_min) / - static_cast(hist_max - hist_min); - if (has_data() && current_index == sim_history_.GetIndex()) { - curr_time = data()->time; - if (curr_time < 0.0) curr_time = 0.0; - lh_top_str = FormatTimelineTime(curr_time); - lh_top_label = lh_top_str.c_str(); - const int updated_curr_step = - (has_model() && model()->opt.timestep > 0) - ? static_cast( - std::round(curr_time / model()->opt.timestep)) - : 0; - lh_bot_str = "(" + std::to_string(updated_curr_step) + ")"; - lh_bot_label = lh_bot_str.c_str(); - const float updated_lh_w = - std::max({base_label_w, ImGui::CalcTextSize(lh_top_label).x, - ImGui::CalcTextSize(lh_bot_label).x}); - if (updated_lh_w > tmp_.timeline_lh_width) { - tmp_.timeline_lh_width = updated_lh_w; - } - } - } - } else { - tmp_.scrubber_active = false; - } - - // Draw LH labels (two lines, both right-aligned). - const float text_y0 = row_center_y - text_2lines_h * 0.5f; - const float text_y1 = text_y0 + text_line_h + custom_line_spacing; - - const float lh_top_x = - cursor.x + lh_box_w - ImGui::CalcTextSize(lh_top_label).x; - ImGui::SetCursorScreenPos(ImVec2(lh_top_x, text_y0)); - ImGui::TextUnformatted(lh_top_label); - - const float lh_bot_x = - cursor.x + lh_box_w - ImGui::CalcTextSize(lh_bot_label).x; - ImGui::SetCursorScreenPos(ImVec2(lh_bot_x, text_y1)); - ImGui::TextUnformatted(lh_bot_label); - - // Draw RH labels (two lines, both left-aligned). - const float rh_x0 = track_x0 + track_w + spacing; - ImGui::SetCursorScreenPos(ImVec2(rh_x0, text_y0)); - ImGui::TextUnformatted(rh_top_label); - - ImGui::SetCursorScreenPos(ImVec2(rh_x0, text_y1)); - ImGui::TextUnformatted(rh_bot_label); - - // Restore window font scale. - ImGui::SetWindowFontScale(1.0f); - - // Draw spine and knob on the draw list. - ImDrawList* dl = ImGui::GetWindowDrawList(); - const ImGuiStyle& style = ImGui::GetStyle(); - - // Spine. - const ImVec4 scrollbar_bg = - ImGui::GetStyle().Colors[ImGuiCol_ScrollbarBg]; - ImGuiCol bg_col_idx; - if (active) { - bg_col_idx = ImGuiCol_FrameBgActive; - } else if (hovered) { - bg_col_idx = ImGuiCol_FrameBgHovered; - } else { - bg_col_idx = (scrollbar_bg.w > 0.01f) ? ImGuiCol_ScrollbarBg - : ImGuiCol_FrameBg; - } - const ImU32 spine_col = ImGui::GetColorU32(bg_col_idx); - dl->AddRectFilled(ImVec2(spine_x0, spine_y0), - ImVec2(spine_x1, spine_y1), spine_col, - spine_h * 0.5f); - - // Knob rectangle. - const float knob_x0 = knob_cx - knob_w * 0.5f; - const float knob_x1 = knob_cx + knob_w * 0.5f; - const float knob_y0 = row_center_y - knob_h * 0.5f; - const float knob_y1 = row_center_y + knob_h * 0.5f; - - ImU32 knob_col; - if (active) { - knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrabActive); - } else if (hovered) { - knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrab); - } else { - knob_col = ImGui::GetColorU32(ImGuiCol_SliderGrab); - } - dl->AddRectFilled(ImVec2(knob_x0, knob_y0), ImVec2(knob_x1, knob_y1), - knob_col, style.GrabRounding); - // Knob border. - dl->AddRect(ImVec2(knob_x0, knob_y0), ImVec2(knob_x1, knob_y1), - ImGui::GetColorU32(ImGuiCol_Border), style.GrabRounding); - - // Advance layout cursor past this row. - ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + total_h)); - } + // Timeline scrubber. + TimelineScrubberGui(); } // Keyframe controls. @@ -2202,13 +2205,7 @@ void App::ToolBarGui() { platform::StepControlGui(&step_control_, tmp_.speed_index); ImGui::SameLine(0, separator_width); - ImGui::SetNextItemWidth(120); - ImGui::BeginDisabled(std::thread::hardware_concurrency() <= 1); - if (ImGui::SliderInt("##NumThreads", &ui_.nthread, 0, 8, "%d threads")) { - tmp_.update_threadpool = true; - } - ImGui::EndDisabled(); - ImGui::SetItemTooltip("%s", "Number of threads in threadpool"); + TimelineScrubberGui(); ImGui::TableNextColumn(); diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index a66d5e82..266076cd 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -232,6 +232,7 @@ class App { void MainMenuGui(); void ToolBarGui(); + void TimelineScrubberGui(); void StatusBarGui(); void HelpGui(); void FileDialogGui();