imgui icon indicating copy to clipboard operation
imgui copied to clipboard

Range Slider

Open davivid opened this issue 10 years ago • 39 comments

I would find a range slider very handy. Any plans to implement one?

davivid avatar Nov 10 '14 19:11 davivid

These already exist:

  • SliderFloat
  • SliderFloat2
  • SliderFloat3
  • SliderFloat4
  • SliderAngle
  • SliderInt

UUSim avatar Nov 10 '14 19:11 UUSim

I think he means a slider with two handles, used to specify a range of values?

JarrettBillingsley avatar Nov 10 '14 20:11 JarrettBillingsley

Could you specifi exactly what you need and how it would work?

ocornut avatar Nov 10 '14 21:11 ocornut

Sorry that wasn't clear. I mean a slider with two handles that allows you to set a range of values, a left handle sets the min and a right handle sets the max. For example you might have a random int generator, but you want the values to be in the range 5 - 25 this function could use such a slider control.

davivid avatar Nov 10 '14 21:11 davivid

Yup – I could definitely use that too, and in fact I would like both integer and floating point ranges. :) Thanks!

A=


Andrea Pessino (not female, just Italian)

Cofounder/Chief Technology Officer Ready At Dawn Studios

From: davivid [mailto:[email protected]] Sent: Monday, November 10, 2014 1:47 PM To: ocornut/imgui Subject: Re: [imgui] Range Slider (#76)

Sorry that wasn't clear. I mean a slider with two handles that allows you to set a range of values, a left handle sets the min and a right handle sets the max. For example you might have a random int generator, but you want the values to be in the range 5 - 25 this function could use such a slider control.

— Reply to this email directly or view it on GitHubhttps://github.com/ocornut/imgui/issues/76#issuecomment-62461229.

apessino avatar Nov 10 '14 22:11 apessino

OK.

Sliders currently always sets their value given the position of the click, so they'd have to be changed in this case to require clicking on the slider grab and handling mouse movement in a relative manner. This is already a desired feature for sliders when clicking close to the current value. Currently when clicking close to the current value you are pretty much guarantee to modify the value, but it should goes "relative movement mode" and wait for horizontal dragging. So this has to be supported first then we can rather easily add a "range / double slider" over it.

NB: Right now using SliderFloat2() + code that clamp values manually would be a reasonable workaround.

ocornut avatar Nov 12 '14 08:11 ocornut

Sorry this is still unresolved, such an old request!

For now I have added helpers using DragFloat / DragInt which don't have the same issues to solve as SliderFloat / SliderInt. So you don't visualize two boxes into the same frame but they are separate frame. Limits are enforced when you drag either sides (but as always you can always bypass limits using ctrl+click to input values).

DragFloatRange2()
DragIntRange2()

static float begin = 10, end = 90;
ImGui::DragFloatRange2("range", &begin, &end, 0.25f, 0.0f, 100.0f, "Min: %.1f %%", "Max: %.1f %%");

static int begin_i = 100, end_i = 1000;
ImGui::DragIntRange2("range int (no bounds)", &begin_i, &end_i, 5, 0, 0, "Min: %.0f units", "Max: %.0f units");

drag range

It's not "hard" do it for slider but right now it would require duplicating a bunch of code (100+ lines) which I'm not very happy with doing especially as the slider code is still in flux. In order to handle large int I first need to re-organize sliders and drags to that integer doesn't pass through a path that does int > float > int casting during edition.

ocornut avatar Jul 04 '15 18:07 ocornut

I've started to do a custom range but it is far from being finished. What would be the best way to handle the logic/rendering for a couple of sliders/grabbers together in a same group? I am right now creating a group with a couple of dummies, a scrollbar, and rendering two sliders on top of it. I am wondering if this is the best way to achieve it.

edit: preview

[Omar:Added attachment so that the data doesn't disappear in the future] ranges.zip

r-lyeh-archived avatar Jan 23 '16 17:01 r-lyeh-archived

Perhaps post the code. You'd probably want to create new code based on a combination of what Slider_, Drag_ and Scrollbar* do.

ocornut avatar Jan 24 '16 10:01 ocornut

I've almost finished range slider. Main work is concentrated on selecting nearest grip. Now I'm moving only first, but it works.

slider

wasikuss avatar Dec 07 '16 09:12 wasikuss

Almost done ;) It looks better ( no overlapping shapes ) and allows to drag closest grab to mouse position. There is bug in selecting closest grab - it should lock to currently dragged and do not change to other while moving. Additionally same rule could change color for only one of them.

You can look into https://github.com/wasikuss/imgui/tree/feature/range-slider or test it in https://github.com/wasikuss/imgui (soon emscripten playground will be added).

slider2

wasikuss avatar Dec 08 '16 12:12 wasikuss

@wasikuss I extracted your range slider into separate .inl/.h files and using ImGui's user includes for building it together with ImGui. This way it's easier to get integrated to other code before it's officially accepted.

Code is here: https://github.com/bkaradzic/bgfx/blob/master/3rdparty/ocornut-imgui/widgets/range_slider.inl

One improvement could be that moving middle bar moves min/max values together.

bkaradzic avatar Dec 26 '16 05:12 bkaradzic

Something similar https://github.com/nem0/LumixEngine/blob/timeline_gui/external/imgui/imgui_user.inl#L814 Lumix Engine

nem0 avatar Dec 26 '16 20:12 nem0

Another improvement that allows to shift a grabber while dragging the other one:

// Put this section while dragging is active. Notes:
// - assumes left is range[0] and right is range[1]
// - assumes left <= right always
if(right < left) right = left;
if(left > right) left = right;

r-lyeh-archived avatar Dec 27 '16 11:12 r-lyeh-archived

hey @nem0, i just integrated your timeline with a few minor modifs :) awesome work! thanks! :)

http://i.imgur.com/Va31qU4.gifv

r-lyeh-archived avatar Mar 18 '17 21:03 r-lyeh-archived

@r-lyeh: Are the few minor modifications publicly available ?

[Edit:] These are the ones I've just made (but they're not very user-friendly as far as I can see): [UPDATED:] Added label tracking (through column API) and support for scrolling (but no item culling). [UPDATED 2:] Added optional item culling (opt_exact_num_rows in BeginTimeline(...)), fixed scrolling hovered lines (@meshula), added keep_range_constant optional argument in TimelineEvent(...), added optional current_time in EndTimeline(...)

namespace ImGui {
// Definitions (header file)
// Timeline (from: https://github.com/nem0/LumixEngine/blob/timeline_gui/external/imgui/imgui_user.h)=
/* Possible enhancements:
 * Add some kind of "snap to grid" epsilon
 * Add zooming with CTRL+MouseWheel, and a horizontal scrollbar
 * Add different types of TimelineEvent (e.g. multiple ranges in a single line, dot-like markers, etc.)
*/
IMGUI_API bool BeginTimeline(const char* str_id, float max_value=0.f, int num_visible_rows=5,int opt_exact_num_rows=0); // last arg, when !=0, enables item culling
IMGUI_API bool TimelineEvent(const char* str_id, float* values, bool keep_range_constant=false);
IMGUI_API void EndTimeline(int num_vertical_grid_lines=5.f,float current_time=0.f,ImU32 timeline_running_color=IM_COL32(0,128,0,200));
} // namespace ImGui

namespace ImGui {
// Timeline implementation (cpp file) from: https://github.com/nem0/LumixEngine/blob/timeline_gui/external/imgui/imgui_user.inl
static float s_max_timeline_value=0.f;
static int s_timeline_num_rows = 0;
static int s_timeline_display_start = 0;
static int s_timeline_display_end = 0;
static int s_timeline_display_index = 0;

bool BeginTimeline(const char* str_id, float max_value, int num_visible_rows,int opt_exact_num_rows)
{
    // reset global variables
    s_max_timeline_value=0.f;
    s_timeline_num_rows = s_timeline_display_start = s_timeline_display_end = 0;
    s_timeline_display_index = -1;

    if (num_visible_rows<=0) num_visible_rows=5;
    const float row_height = ImGui::GetTextLineHeightWithSpacing();
    const bool rv = BeginChild(str_id,ImVec2(0,num_visible_rows>=0 ? (row_height*num_visible_rows) : -1.f),false);
    ImGui::PushStyleColor(ImGuiCol_Column,GImGui->Style.Colors[ImGuiCol_Border]);
    ImGui::Columns(2,str_id);
    const float contentRegionWidth = ImGui::GetWindowContentRegionWidth();
    if (ImGui::GetColumnOffset(1)>=contentRegionWidth*0.48f)ImGui::SetColumnOffset(1,contentRegionWidth*0.15f);
    s_max_timeline_value = max_value>=0 ? max_value : (contentRegionWidth*0.85f);
    if (opt_exact_num_rows>0) {
	// Item culling
	s_timeline_num_rows = opt_exact_num_rows;
	ImGui::CalcListClipping(s_timeline_num_rows, row_height, &s_timeline_display_start, &s_timeline_display_end);
	ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (s_timeline_display_start * row_height));
    }
    return rv;
}
static const float TIMELINE_RADIUS = 6;
bool TimelineEvent(const char* str_id, float* values,bool keep_range_constant)
{
    ++s_timeline_display_index;
    if (s_timeline_num_rows>0 &&
	    (s_timeline_display_index<s_timeline_display_start || s_timeline_display_index>=s_timeline_display_end)) return false;   // item culling

    ImGuiWindow* win = GetCurrentWindow();
    const ImU32 inactive_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Button]);
    const ImU32 active_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ButtonHovered]);
    const ImU32 line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ColumnActive]);
    bool changed = false;
    bool hovered = false;
    bool active = false;

    ImGui::Text("%s",str_id);
    ImGui::NextColumn();

    const float columnOffset = ImGui::GetColumnOffset(1);
    const float columnWidth = ImGui::GetColumnWidth(1)-GImGui->Style.ScrollbarSize;
    ImVec2 cursor_pos(GetWindowContentRegionMin().x + win->Pos.x+columnOffset-TIMELINE_RADIUS,win->DC.CursorPos.y);
    bool mustMoveBothEnds=false;
    const bool isMouseDraggingZero = IsMouseDragging(0);

    for (int i = 0; i < 2; ++i)
    {
        ImVec2 pos = cursor_pos;
        pos.x += columnWidth * values[i] / s_max_timeline_value + TIMELINE_RADIUS;
        pos.y += TIMELINE_RADIUS;

        SetCursorScreenPos(pos - ImVec2(TIMELINE_RADIUS, TIMELINE_RADIUS));
        PushID(i);
        InvisibleButton(str_id, ImVec2(2 * TIMELINE_RADIUS, 2 * TIMELINE_RADIUS));
        active = IsItemActive();
        if (active || IsItemHovered())
        {
            ImGui::SetTooltip("%f", values[i]);
	    if (!keep_range_constant)	{
		// @meshula:The item hovered line needs to be compensated for vertical scrolling. Thx!
		ImVec2 a(pos.x, GetWindowContentRegionMin().y + win->Pos.y + win->Scroll.y);
		ImVec2 b(pos.x, GetWindowContentRegionMax().y + win->Pos.y + win->Scroll.y);
		// possible aternative:
		//ImVec2 a(pos.x, win->Pos.y);
		//ImVec2 b(pos.x, win->Pos.y+win->Size.y);
		win->DrawList->AddLine(a, b, line_color);
	    }
	    hovered = true;
        }
	if (active && isMouseDraggingZero)
        {
	    if (!keep_range_constant) values[i] += GetIO().MouseDelta.x / columnWidth * s_max_timeline_value;
	    else mustMoveBothEnds = true;
	    changed = hovered = true;
        }
        PopID();
        win->DrawList->AddCircleFilled(
            pos, TIMELINE_RADIUS, IsItemActive() || IsItemHovered() ? active_color : inactive_color);
    }

    ImVec2 start = cursor_pos;
    start.x += columnWidth  * values[0] / s_max_timeline_value + 2 * TIMELINE_RADIUS;
    start.y += TIMELINE_RADIUS * 0.5f;
    ImVec2 end = start + ImVec2(columnWidth * (values[1] - values[0]) / s_max_timeline_value - 2 * TIMELINE_RADIUS,
                             TIMELINE_RADIUS);

    PushID(-1);
    SetCursorScreenPos(start);
    InvisibleButton(str_id, end - start);
    if ((IsItemActive() && isMouseDraggingZero) || mustMoveBothEnds)
    {
	const float deltaX = GetIO().MouseDelta.x / columnWidth * s_max_timeline_value;
	values[0] += deltaX;
	values[1] += deltaX;
        changed = hovered = true;
    }
    else if (IsItemHovered()) hovered = true;
    PopID();

    SetCursorScreenPos(cursor_pos + ImVec2(0, GetTextLineHeightWithSpacing()));

    win->DrawList->AddRectFilled(start, end, IsItemActive() || IsItemHovered() ? active_color : inactive_color);

    if (values[0]>values[1]) {float tmp=values[0];values[0]=values[1];values[1]=tmp;}
    if (values[1]>s_max_timeline_value) {values[0]-=values[1]-s_max_timeline_value;values[1]=s_max_timeline_value;}
    if (values[0]<0) {values[1]-=values[0];values[0]=0;}

    if (hovered) ImGui::SetMouseCursor(ImGuiMouseCursor_Move);

    ImGui::NextColumn();
    return changed;
}
void EndTimeline(int num_vertical_grid_lines,float current_time,ImU32 timeline_running_color)    {
    const float row_height = ImGui::GetTextLineHeightWithSpacing();
    if (s_timeline_num_rows>0) ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ((s_timeline_num_rows - s_timeline_display_end) * row_height));

    ImGui::NextColumn();

    ImGuiWindow* win = GetCurrentWindow();

    const float columnOffset = ImGui::GetColumnOffset(1);
    const float columnWidth = ImGui::GetColumnWidth(1)-GImGui->Style.ScrollbarSize;
    const float horizontal_interval = columnWidth / num_vertical_grid_lines;

    ImU32 color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Button]);
    ImU32 line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Border]);
    ImU32 text_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Text]);
    ImU32 moving_line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ColumnActive]);
    const float rounding = GImGui->Style.ScrollbarRounding;
    const float startY = ImGui::GetWindowHeight() + win->Pos.y;

    // Draw black vertical lines (inside scrolling area)
    for (int i = 1; i <= num_vertical_grid_lines; ++i)
    {
	ImVec2 a = GetWindowContentRegionMin() + win->Pos;
        a.x += i * horizontal_interval + columnOffset;
        win->DrawList->AddLine(a, ImVec2(a.x,startY), line_color);
    }

    // Draw moving vertical line
    if (current_time>0.f && current_time<s_max_timeline_value)	{
	ImVec2 a = GetWindowContentRegionMin() + win->Pos;
	a.x += columnWidth*(current_time/s_max_timeline_value) + columnOffset;
	win->DrawList->AddLine(a, ImVec2(a.x,startY), moving_line_color);
    }

    ImGui::Columns(1);
    ImGui::PopStyleColor();

    EndChild();

    // Draw bottom axis ribbon (outside scrolling region)
    win = GetCurrentWindow();
    ImVec2 start(ImGui::GetCursorScreenPos().x+columnOffset,ImGui::GetCursorScreenPos().y);
    ImVec2 end(start.x+columnWidth,start.y+row_height);
    if (current_time<=0)			win->DrawList->AddRectFilled(start, end, color, rounding);
    else if (current_time>s_max_timeline_value) win->DrawList->AddRectFilled(start, end, timeline_running_color, rounding);
    else {
	ImVec2 median(start.x+columnWidth*(current_time/s_max_timeline_value),end.y);
	win->DrawList->AddRectFilled(start, median, timeline_running_color, rounding,1|8);
	median.y=start.y;
	win->DrawList->AddRectFilled(median, end, color, rounding,2|4);
	win->DrawList->AddLine(median, ImVec2(median.x,end.y), moving_line_color);
    }

    char tmp[256]="";
    for (int i = 0; i < num_vertical_grid_lines; ++i)
    {
        ImVec2 a = start;
        a.x += i * horizontal_interval;
        a.y = start.y;

        ImFormatString(tmp, sizeof(tmp), "%.2f", i * s_max_timeline_value / num_vertical_grid_lines);
        win->DrawList->AddText(a, text_color, tmp);

    }
    ImGui::SetCursorPosY(ImGui::GetCursorPosY()+row_height);
}
// End Timeline
} // namespace ImGui

And I can use it this way:

        if (ImGui::BeginTimeline("MyTimeline",50.f,4,6))  // label, max_value, num_visible_rows, opt_exact_num_rows (for item culling)
        {
            static float events[12]={10.f,20.f,0.5f,30.f,40.f,50.f,20.f,40.f,15.f,22.5f,35.f,45.f};
            if (ImGui::TimelineEvent("Event1",&events[0])) {/*events[0] and/or events[1] modified*/}
            ImGui::TimelineEvent("Event2",&events[2]);
            ImGui::TimelineEvent("Event3",&events[4],true);    // Event3 can only be shifted
            ImGui::TimelineEvent("Event4",&events[6]);
            ImGui::TimelineEvent("Event5",&events[8]);
            ImGui::TimelineEvent("Event6",&events[10]);
        }
        const float elapsedTime = (float)(((unsigned)(ImGui::GetTime()*1000))%50000)/1000.f;    // So that it's always in [0,50]
        ImGui::EndTimeline(5,elapsedTime);  // num_vertical_grid_lines, current_time (optional), timeline_running_color (optional)

selection_050 The code works quite well (thanks @mem0 :smile:!) . The only problem is that I have to manually repeat the names of the events... [FIXED]

Flix01 avatar Mar 20 '17 11:03 Flix01

@Flix01 sure

edit: I havent played with columns here but I guess this is the way to go for the track labels. image


// https://github.com/ocornut/imgui/issues/76

// h

namespace ImGui {

    bool BeginTimeline(const char* str_id, float max_time);
    bool TimelineEvent(const char* str_id, float times[2]);
    void EndTimeline(float current_time = -1);

}

// cpp

// https://github.com/ocornut/imgui/issues/76

namespace ImGui {

static float s_max_timeline_value;


bool BeginTimeline(const char* str_id, float max_time)
{
	s_max_timeline_value = max_time;
	return BeginChild(str_id);
}


static const float TIMELINE_RADIUS = 6;


bool TimelineEvent(const char* str_id, float values[2])
{
	ImGuiWindow* win = GetCurrentWindow();
	const ImU32 inactive_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Button]);
	const ImU32 active_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ButtonHovered]);
	const ImU32 line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ColumnActive]);
	bool changed = false;
	ImVec2 cursor_pos = win->DC.CursorPos;

	// @r-lyeh {
	Button(str_id, ImVec2(120,0)); // @todo: enable/disable track channel here
	SameLine();
	cursor_pos += ImVec2(0, GetTextLineHeightWithSpacing() / 3);
	// }

	for (int i = 0; i < 2; ++i)
	{
		ImVec2 pos = cursor_pos;
		pos.x += win->Size.x * values[i] / s_max_timeline_value + TIMELINE_RADIUS;
		pos.y += TIMELINE_RADIUS;

		SetCursorScreenPos(pos - ImVec2(TIMELINE_RADIUS, TIMELINE_RADIUS));
		PushID(i);
		InvisibleButton(str_id, ImVec2(2 * TIMELINE_RADIUS, 2 * TIMELINE_RADIUS));
		if (IsItemActive() || IsItemHovered())
		{
			ImGui::SetTooltip("%f", values[i]);
			ImVec2 a(pos.x, GetWindowContentRegionMin().y + win->Pos.y + win->Scroll.y);
			ImVec2 b(pos.x, GetWindowContentRegionMax().y + win->Pos.y + win->Scroll.y);
			win->DrawList->AddLine(a, b, line_color);
		}
		if (IsItemActive() && IsMouseDragging(0))
		{
			values[i] += GetIO().MouseDelta.x / win->Size.x * s_max_timeline_value;
			changed = true;
		}
		PopID();
		win->DrawList->AddCircleFilled(
			pos, TIMELINE_RADIUS, IsItemActive() || IsItemHovered() ? active_color : inactive_color);
	}

	ImVec2 start = cursor_pos;
	start.x += win->Size.x * values[0] / s_max_timeline_value + 2 * TIMELINE_RADIUS;
	start.y += TIMELINE_RADIUS * 0.5f;
	ImVec2 end = start + ImVec2(win->Size.x * (values[1] - values[0]) / s_max_timeline_value - 2 * TIMELINE_RADIUS,
							 TIMELINE_RADIUS);

	PushID(-1);
	SetCursorScreenPos(start);
	InvisibleButton(str_id, end - start);
	if (IsItemActive() && IsMouseDragging(0))
	{
		values[0] += GetIO().MouseDelta.x / win->Size.x * s_max_timeline_value;
		values[1] += GetIO().MouseDelta.x / win->Size.x * s_max_timeline_value;
		changed = true;
	}
	PopID();

	SetCursorScreenPos(cursor_pos + ImVec2(0, GetTextLineHeightWithSpacing()));

	win->DrawList->AddRectFilled(start, end, IsItemActive() || IsItemHovered() ? active_color : inactive_color);

	if (values[0] > values[1])
	{
		float tmp = values[0];
		values[0] = values[1];
		values[1] = tmp;
	}
	if (values[1] > s_max_timeline_value) values[1] = s_max_timeline_value;
	if (values[0] < 0) values[0] = 0;
	return changed;
}


void EndTimeline( float t )
{
	ImGuiWindow* win = GetCurrentWindow();

	// @r-lyeh {
	if( t >= 0 ) {
		if( t > s_max_timeline_value ) t = s_max_timeline_value; t /= s_max_timeline_value;
		const ImU32 line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ColumnActive]);
		ImVec2 a(win->Pos.x + GetWindowContentRegionMin().x + t * GetWindowContentRegionWidth(), GetWindowContentRegionMin().y + win->Pos.y + win->Scroll.y);
		ImVec2 b(win->Pos.x + GetWindowContentRegionMin().x + t * GetWindowContentRegionWidth(), GetWindowContentRegionMax().y + win->Pos.y + win->Scroll.y);
		win->DrawList->AddLine(a, b, line_color);
	}
	// }

	ImU32 color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Button]);
	ImU32 line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Border]);
	ImU32 text_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Text]);
	float rounding = GImGui->Style.ScrollbarRounding;
	ImVec2 start(GetWindowContentRegionMin().x + win->Pos.x,
		GetWindowContentRegionMax().y - GetTextLineHeightWithSpacing() + win->Pos.y + win->Scroll.y);
	ImVec2 end = GetWindowContentRegionMax() + win->Pos + ImVec2(0,win->Scroll.y);

	win->DrawList->AddRectFilled(start, end, color, rounding);

	const int LINE_COUNT = 5;
	const ImVec2 text_offset(0, GetTextLineHeightWithSpacing());
	for (int i = 0; i <= LINE_COUNT; ++i)
	{
		ImVec2 a = GetWindowContentRegionMin() + win->Pos; // @r-lyeh: - ImVec2(TIMELINE_RADIUS, 0);
		a.x += i * (GetWindowContentRegionWidth() - 1) / LINE_COUNT; // @r-lyeh: -1
		ImVec2 b = a;
		b.y = start.y;
		win->DrawList->AddLine(a, b, line_color);
		char tmp[256];
		ImFormatString(tmp, sizeof(tmp), "%.2f", i * s_max_timeline_value / LINE_COUNT);
		win->DrawList->AddText(b, text_color, tmp);
	}

	EndChild();
}

}

r-lyeh-archived avatar Mar 20 '17 14:03 r-lyeh-archived

Thanks @r-lyeh !

edit: I havent played with columns here but I guess this is the way to go for the track labels.

Yes, that's what I was thinking. However the current code relies heavily on the cursor position, that is completely messed up when we use columns! So I guess it's not as easy as it seems...

Flix01 avatar Mar 20 '17 15:03 Flix01

I'm making some progress with the column API.

I'll update the code when it's ready (I'm currently trying to make scrolling work: and it takes time, especially if I'll try to add line culling...)

Flix01 avatar Mar 20 '17 16:03 Flix01

I've updated my modifications in the post above. Still no item culling; but I'm not sure if I can add it, since the number of items is not known...

Flix01 avatar Mar 20 '17 17:03 Flix01

Yes, that's what I was thinking. However the current code relies heavily on the cursor position, that is completely messed up when we use columns! So I guess it's not as easy as it seems...

Could you elaborate?

ocornut avatar Mar 20 '17 17:03 ocornut

@Flix01 The item hovered line needs to be compensated for vertical scrolling,

ImVec2 a(pos.x, GetWindowContentRegionMin().y + win->Pos.y + win->Scroll.y);
ImVec2 b(pos.x, GetWindowContentRegionMax().y + win->Pos.y + win->Scroll.y);

meshula avatar Mar 20 '17 23:03 meshula

@meshula: thanks! @ocornut: In the end I just had to pay attention to the column offset and the column width. I've just updated the code above and it seems to work.

Flix01 avatar Mar 21 '17 13:03 Flix01

Now we need a "set first" flag for column offset so that if the user drags the column separator, it "sticks" instead of snapping back :) There's a number of open issues on columns, maybe it's premature to try to fix this?

meshula avatar Mar 21 '17 18:03 meshula

@meshula: yes, although in my code it snaps back only when the column is pulled to the half right side of the window.

const float contentRegionWidth = ImGui::GetWindowContentRegionWidth();
if (ImGui::GetColumnOffset(1)>=contentRegionWidth*0.48f) ImGui::SetColumnOffset(1,contentRegionWidth*0.15f);
// The if branch is executed the first time and when the user drags
// the column to the right more than contentRegionWidth*0.48f    

Flix01 avatar Mar 21 '17 19:03 Flix01

Ah, that's clever. Not quite the behavior I would like, but I can cook up "something" with that solution in mind.

meshula avatar Mar 21 '17 21:03 meshula

@nem0 @Flix01 @r-lyeh

Here's a version that allows you to pan and zoom the timeline, via a little stretchable bar that appears under the number line. It's use of static variables is super clumsy because it restricts to only one active timeline, but it's fine for a proof of concept.

 #define IMGUI_DEFINE_MATH_OPERATORS
 #include <imgui_internal.h>
 
 #include <functional>
 
 // original source of timeline code is
 // https://github.com/nem0/LumixEngine
 //
 // modified further according to imgui issue 76
 // and adding a panzoomer
 // https://github.com/ocornut/imgui/issues/76
 //
 
 static float s_max_timeline_value = 100.f;
 static float s_pixel_offset = 0.f;
 
 static double s_time_in = 0.f;
 static double s_time_out = 1.f;
 
 static double s_time_offset = 0;
 static double s_time_scale = 1;
 
 namespace ImGui {

	bool BeginTimeline(const char* str_id, float pixel_offset, float max_value, int num_visible_rows)
	{
		s_time_scale = 1.0 / (s_time_out - s_time_in);
		s_time_offset = s_time_in * s_time_scale;


		if (num_visible_rows <= 0)
			num_visible_rows = 5;

		ImGuiWindow * win = GetCurrentWindow();

		float height = win->ContentsRegionRect.Max.y - win->ContentsRegionRect.Min.y
			- ImGui::GetTextLineHeightWithSpacing()   // space for the time bar
			- ImGui::GetTextLineHeightWithSpacing();  // space for horizontal scroller


		bool rv = BeginChild(str_id, ImVec2(0, height), false);

		ImGui::PushStyleColor(ImGuiCol_Column, GImGui->Style.Colors[ImGuiCol_Border]);
		ImGui::Columns(2, str_id); 

		static float _pixel_offset = 0;
		if (pixel_offset != _pixel_offset) {
			_pixel_offset = pixel_offset;
			ImGui::SetColumnOffset(1, pixel_offset);
		}
		s_max_timeline_value = max_value >= 0 ? max_value : (ImGui::GetWindowContentRegionWidth() * 0.85f);
		return rv;
	}


	static const float TIMELINE_RADIUS = 12;


	bool TimelineEvent(const char* str_id, double & val1, double & val2)
	{
		double values[2] = { val1, val2 };
		ImGuiWindow* win = GetCurrentWindow();
		const ImU32 inactive_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Button]);
		const ImU32 active_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ButtonHovered]);
		const ImU32 line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ColumnActive]);
		bool changed = false;
		bool hovered = false;
		bool active = false;

		ImGui::Text("%s", str_id);
		ImGui::NextColumn();

		const float columnOffset = ImGui::GetColumnOffset(1);
		const float columnWidth = ImGui::GetColumnWidth(1) - GImGui->Style.ScrollbarSize;

		ImVec2 cursor_pos(GetWindowContentRegionMin().x + win->Pos.x + columnOffset - TIMELINE_RADIUS, win->DC.CursorPos.y);

		float posx[2] = { 0,0 };

		for (int i = 0; i < 2; ++i)
		{
			ImVec2 pos = cursor_pos;
			pos.x += s_time_scale * columnWidth * float(values[i]) / s_max_timeline_value - columnWidth * s_time_offset + TIMELINE_RADIUS;
			pos.y += TIMELINE_RADIUS;
			posx[i] = pos.x;

			SetCursorScreenPos(pos - ImVec2(TIMELINE_RADIUS, TIMELINE_RADIUS));
			PushID(i);
			InvisibleButton(str_id, ImVec2(2 * TIMELINE_RADIUS, 2 * TIMELINE_RADIUS));
			active = IsItemActive();
			if (active || IsItemHovered())
			{
				ImGui::SetTooltip("%f", float(values[i]));
				ImVec2 a(pos.x, GetWindowContentRegionMin().y + win->Pos.y + win->Scroll.y);

				ImGuiWindow * parent_win = win->ParentWindow;
				float endy = parent_win->ContentsRegionRect.Max.y + win->Pos.y; // draw all the way to the bottom of the parent window

				ImVec2 b(pos.x, endy);
				win->DrawList->AddLine(a, b, line_color);
				hovered = true;
			}
			if (IsItemActive() && IsMouseDragging(0))
			{
				values[i] += GetIO().MouseDelta.x / win->Size.x * s_max_timeline_value;
				changed = hovered = true;
			}
			PopID();
			win->DrawList->AddCircleFilled(
				pos, TIMELINE_RADIUS, IsItemActive() || IsItemHovered() ? active_color : inactive_color);
		}

		ImVec2 start = cursor_pos;
		start.x = posx[0];
		start.y += TIMELINE_RADIUS * 0.5f;
		ImVec2 end = start;
		end.x = posx[1];
		end.y += TIMELINE_RADIUS;

		PushID(-1);
		SetCursorScreenPos(start);
		InvisibleButton(str_id, end - start);
		if (IsItemActive() && IsMouseDragging(0))
		{
			values[0] += GetIO().MouseDelta.x / win->Size.x * s_max_timeline_value;
			values[1] += GetIO().MouseDelta.x / win->Size.x * s_max_timeline_value;
			changed = hovered = true;
		}
		PopID();

		SetCursorScreenPos(cursor_pos + ImVec2(0, GetTextLineHeightWithSpacing()));

		win->DrawList->AddRectFilled(start, end, IsItemActive() || IsItemHovered() ? active_color : inactive_color);

		if (values[0] > values[1])
		{
			std::swap(values[0], values[1]);
		}
		if (values[1] > s_max_timeline_value) values[1] = s_max_timeline_value;
		if (values[0] < 0) values[0] = 0;

		if (hovered)
			ImGui::SetMouseCursor(ImGuiMouseCursor_Move);

		ImGui::NextColumn();

		val1 = values[0];
		val2 = values[1];

		return changed;
	}


	void EndTimeline(int num_vertical_grid_lines, double & time_in, double & time_out)
	{
		ImGui::NextColumn();

		ImGuiWindow* win = GetCurrentWindow();

		const float columnOffset = ImGui::GetColumnOffset(1);
		const float columnWidth = ImGui::GetColumnWidth(1) - GImGui->Style.ScrollbarSize;
		const float horizontal_interval = columnWidth / num_vertical_grid_lines;

		const ImU32 pz_inactive_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Button]);
		const ImU32 pz_active_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ButtonHovered]);
		const ImU32 pz_line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_ColumnActive]);

		const ImU32 color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Button]);
		const ImU32 line_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Border]);
		const ImU32 text_color = ColorConvertFloat4ToU32(GImGui->Style.Colors[ImGuiCol_Text]);
		const float rounding = GImGui->Style.ScrollbarRounding;
		const float startY = ImGui::GetWindowHeight() + win->Pos.y;

		// vertical lines
		for (int i = 0; i <= num_vertical_grid_lines; ++i)
		{
			ImVec2 a = GetWindowContentRegionMin() + win->Pos;
			a.x += s_time_scale * i * horizontal_interval + columnOffset - columnWidth * s_time_offset;
			win->DrawList->AddLine(a, ImVec2(a.x, startY), line_color);
		}

		ImGui::Columns(1);
		ImGui::PopStyleColor();

		EndChild();

		// draw bottom axis ribbon outside scrolling region
		win = GetCurrentWindow();

		float startx = ImGui::GetCursorScreenPos().x + columnOffset;
		float endy = GetWindowContentRegionMax().y + win->Pos.y;
		ImVec2 start(startx, endy - 2 * ImGui::GetTextLineHeightWithSpacing());
		ImVec2 end(startx + columnWidth, endy - ImGui::GetTextLineHeightWithSpacing());
		win->DrawList->AddRectFilled(start, end, color, rounding);

		char tmp[256] = "";
		for (int i = 0; i < num_vertical_grid_lines; ++i)
		{
			ImVec2 a = start;
			a.x = start.x + s_time_scale * i * horizontal_interval - columnWidth * s_time_offset;

			if (a.x < startx)
				continue;

			a.y = start.y;
			ImFormatString(tmp, sizeof(tmp), "%.2f", i * s_max_timeline_value / num_vertical_grid_lines);
			win->DrawList->AddText(a, text_color, tmp);
		}

		// draw time panzoomer

		float posx[2] = { 0,0 };
		double values[2] = { time_in, time_out };

		bool active = false;
		bool hovered = false;
		bool changed = false;
		ImVec2 cursor_pos = { start.x, end.y };

		for (int i = 0; i < 2; ++i)
		{
			ImVec2 pos = cursor_pos;
			pos.x += columnWidth * float(values[i]) + TIMELINE_RADIUS;
			pos.y += TIMELINE_RADIUS;
			posx[i] = pos.x;

			SetCursorScreenPos(pos - ImVec2(TIMELINE_RADIUS, TIMELINE_RADIUS));
			PushID(i);
			InvisibleButton("zoompanner", ImVec2(2 * TIMELINE_RADIUS, 2 * TIMELINE_RADIUS));
			active = IsItemActive();
			if (active || IsItemHovered())
			{
				hovered = true;
			}
			if (IsItemActive() && IsMouseDragging(0))
			{
				values[i] += GetIO().MouseDelta.x / columnWidth;
				changed = hovered = true;
			}
			PopID();

			win->DrawList->AddCircleFilled(
				pos, TIMELINE_RADIUS, IsItemActive() || IsItemHovered() ? pz_active_color : pz_inactive_color);
		}

		if (values[0] > values[1])
			std::swap(values[0], values[1]);

		start.x = posx[0];
		start.y += TIMELINE_RADIUS * 0.5f + ImGui::GetTextLineHeightWithSpacing();
		end.x = posx[1];
		end.y = start.y + TIMELINE_RADIUS;

		PushID(-1);
		SetCursorScreenPos(start);
		InvisibleButton("zoompanner", end - start);
		if (IsItemActive() && IsMouseDragging(0))
		{
			values[0] += GetIO().MouseDelta.x / columnWidth;
			values[1] += GetIO().MouseDelta.x / columnWidth;
			changed = hovered = true;
		}
		PopID();

		win->DrawList->AddRectFilled(start, end, IsItemActive() || IsItemHovered() ? pz_active_color : pz_inactive_color);

		for (int i = 0; i < 2; ++i)
		{
			if (values[i] < 0)
				values[i] = 0;
			if (values[i] > 1)
				values[i] = 1;
		}

		time_in = values[0];
		time_out = values[1];

		s_time_in = time_in;
		s_time_out = time_out;

		ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 2 * ImGui::GetTextLineHeightWithSpacing());
	}
 
 
 } // ImGui

meshula avatar Mar 22 '17 05:03 meshula

@meshula: Looks interesting and useful! Thanks for posting!

Flix01 avatar Mar 22 '17 11:03 Flix01

@meshula: cool! took me a while to figure out the input args but it finally worked :) mouse wheel now! :D

r-lyeh-archived avatar Mar 22 '17 12:03 r-lyeh-archived

One improvement could be that moving middle bar moves min/max values together.

@bkaradzic Did you added this feature? Any help about how to do it?

There's also some strange bounding box problems... Layout broke when using SameLine(

moebiussurfing avatar Jul 12 '21 22:07 moebiussurfing

@wasikuss @bkaradzic Do you have working version of this slider in 1.85?

caxapexac avatar Dec 31 '21 19:12 caxapexac

@caxapexac It's here: https://github.com/bkaradzic/bgfx/blob/master/3rdparty/dear-imgui/widgets/range_slider.inl

bkaradzic avatar Dec 31 '21 20:12 bkaradzic

@bkaradzic How to get RoundScalarWithFormatFloat and SliderCalcRatioFromValueFloat?

caxapexac avatar Jan 02 '22 16:01 caxapexac

@bkaradzic Oh its RoundScalarWithFormatT from new version. Whats the type? SliderCalcRatioFromValueFloat is not exist in this version at all

caxapexac avatar Jan 02 '22 16:01 caxapexac

Solved taken code from imgui_widgets.cpp from your repo

    // extern float RoundScalarWithFormatFloat(const char* format, ImGuiDataType data_type, float v);
    float RoundScalarWithFormatFloat(const char* format, ImGuiDataType data_type, float v)
    {
        return ImGui::RoundScalarWithFormatT<float, float>(format, data_type, v);
    }

    // extern float SliderCalcRatioFromValueFloat(ImGuiDataType data_type, float v, float v_min, float v_max, float power, float linear_zero_pos);
    float SliderCalcRatioFromValueFloat(ImGuiDataType data_type, float v, float v_min, float v_max, float power, float linear_zero_pos)
    {
        return ImGui::ScaleRatioFromValueT<float, float, float>(data_type, v, v_min, v_max, false, power, linear_zero_pos);
    }

caxapexac avatar Jan 02 '22 16:01 caxapexac

@ocornut, is there any news about implementation of this feature in ImGui by default? For now I have found no way to add @bkaradzic implementation without problems with linking. Why is it still not implemented, while the sollution exists for 5 years?

RUSLoker avatar Mar 25 '22 15:03 RUSLoker

@caxapexac It's here: https://github.com/bkaradzic/bgfx/blob/master/3rdparty/dear-imgui/widgets/range_slider.inl

I get an error "no operator "+" matches these operands" on line 170 & 171

Zejzz avatar Jul 27 '22 19:07 Zejzz

@Zejzz what version of imgui are you using? Write the full stacktrace of an error

caxapexac avatar Jul 27 '22 22:07 caxapexac

You need to use

#define IMGUI_DEFINE_MATH_OPERATORS #include “imgui_internal.h”

in order to access operators, unless your own math class was setup in imconfig.h

ocornut avatar Jul 28 '22 07:07 ocornut