Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Range Slider #76

Open
davivid opened this issue Nov 10, 2014 · 39 comments
Open

Range Slider #76

davivid opened this issue Nov 10, 2014 · 39 comments

Comments

@davivid
Copy link

davivid commented Nov 10, 2014

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

@UUSim
Copy link

UUSim commented Nov 10, 2014

These already exist:

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

@JarrettBillingsley
Copy link
Contributor

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

@ocornut
Copy link
Owner

ocornut commented Nov 10, 2014

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

@davivid
Copy link
Author

davivid commented Nov 10, 2014

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.

@apessino
Copy link

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//issues/76#issuecomment-62461229.

@ocornut
Copy link
Owner

ocornut commented Nov 12, 2014

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
Copy link
Owner

ocornut commented Jul 4, 2015

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.

@r-lyeh-archived
Copy link

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

@ocornut
Copy link
Owner

ocornut commented Jan 24, 2016

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

@wasikuss
Copy link

wasikuss commented Dec 7, 2016

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
Copy link

wasikuss commented Dec 8, 2016

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

@bkaradzic
Copy link
Contributor

bkaradzic commented Dec 26, 2016

@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.

@nem0
Copy link
Contributor

nem0 commented Dec 26, 2016

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

@r-lyeh-archived
Copy link

r-lyeh-archived commented Dec 27, 2016

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;

glennsky pushed a commit to glennsky/ofxImGui that referenced this issue Mar 5, 2017
ocornut/imgui#76
Taken from: wasikuss/imgui@a50515a

needed SliderBehaviorCalcRatioFromValue() added to imgui.cpp to compile

added timeline and curveeditor widgets from imgui_user files from here:
https://github.com/nem0/LumixEngine/tree/master/external/imgui
@r-lyeh-archived
Copy link

r-lyeh-archived commented Mar 18, 2017

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

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

@Flix01
Copy link

Flix01 commented Mar 20, 2017

@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 😄!) .
The only problem is that I have to manually repeat the names of the events... [FIXED]

@r-lyeh-archived
Copy link

r-lyeh-archived commented Mar 20, 2017

@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();
}

}

@Flix01
Copy link

Flix01 commented Mar 20, 2017

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
Copy link

Flix01 commented Mar 20, 2017

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
Copy link

Flix01 commented Mar 20, 2017

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...

@ocornut
Copy link
Owner

ocornut commented Mar 20, 2017

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?

@meshula
Copy link

meshula commented Mar 20, 2017

@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);

@Flix01
Copy link

Flix01 commented Mar 21, 2017

@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.

@meshula
Copy link

meshula commented Mar 21, 2017

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?

@Flix01
Copy link

Flix01 commented Mar 21, 2017

@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    

@meshula
Copy link

meshula commented Mar 21, 2017

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

@meshula
Copy link

meshula commented Mar 22, 2017

@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

@Flix01
Copy link

Flix01 commented Mar 22, 2017

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

@r-lyeh-archived
Copy link

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

@moebiussurfing
Copy link

moebiussurfing commented Jul 12, 2021

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(

@caxapexac
Copy link

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

@bkaradzic
Copy link
Contributor

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

@caxapexac
Copy link

@bkaradzic
How to get RoundScalarWithFormatFloat and SliderCalcRatioFromValueFloat?

@caxapexac
Copy link

caxapexac commented Jan 2, 2022

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

@caxapexac
Copy link

caxapexac commented Jan 2, 2022

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);
    }

@RUSLoker
Copy link

RUSLoker commented Mar 25, 2022

@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?

@Zejzz
Copy link

Zejzz commented Jul 27, 2022

@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

@caxapexac
Copy link

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

@ocornut
Copy link
Owner

ocornut commented Jul 28, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests