Checkbox widget replacement with better look #3351

Open
opened 2020-07-13 10:51:06 +00:00 by mrduda · 6 comments
mrduda commented 2020-07-13 10:51:06 +00:00 (Migrated from github.com)

Version/Branch of Dear ImGui:

Version: 1.78 WIP
Branch: master

Back-end/Renderer/Compiler/OS

Back-ends: imgui_impl_dx11.cpp
Compiler: MSVC 2019
Operating System: Win10

My Issue/Question:
This is my replacement for built-in checkbox widget. Tri-state is supported. Color is taken from ImGuiCol_Text, with border alpha dimmed down when not hovered.

Screenshots/Video
checkboxes

Standalone, minimal, complete and verifiable example:

#include <imgui/imgui_internal.h>

//-- replacement for ImGui::Checkbox
bool CheckboxEx(const char* label, bool* value)
{
	assert(value);
	
	bool result = false;
	auto* drawList = ImGui::GetWindowDrawList();

	const bool isMixedState = ImGui::GetCurrentWindow()->DC.ItemFlags & ImGuiItemFlags_MixedValue;

	const auto pos = ImGui::GetCursorScreenPos();
	const auto mousePos = ImGui::GetMousePos();

	const auto itemSpacing = ImGui::GetStyle().ItemSpacing;
	const float lineHeight = ImGui::GetTextLineHeight();
	const float boxSize = std::floor(lineHeight * 0.9f);
	const float boxOffsetHorz = std::ceil(itemSpacing.x * 1.3f);
	const float boxOffsetVert = itemSpacing.y + std::floor(0.5f * (lineHeight - boxSize));
	const float clearance = boxSize * 0.2f;
	const auto corner = pos + ImVec2(boxOffsetHorz, boxOffsetVert);

	char buf[1024];
	strcpy(buf, label);
	for (int i = 0; i < sizeof(buf); ++i)
	{
		if (buf[i] == '#')
		{
			buf[i] = '\0';
			break;
		}
	}
	const float labelWidth = ImGui::CalcTextSize(buf).x;

	bool isHovered = ImRect(pos, pos + ImVec2(lineHeight + labelWidth + 2.0f * itemSpacing.x, lineHeight)).Contains(mousePos);

	ImVec4 color = ImGui::GetStyleColorVec4(ImGuiCol_Text);
	ImVec4 colorMark = color;
	color.w *= isHovered ? 1.0f : 0.25f;
	drawList->AddRect(corner, corner + ImVec2(boxSize, boxSize), ImColor(color), 0.0f, 0, 1.0f);

	if (isHovered && ImGui::IsWindowHovered())
	{
		if (ImGui::IsMouseClicked(0))
		{
			if (isMixedState)
			{
				*value = false;
			}
			else
			{
				*value = !*value;
			}
			result = true;
		}
	}

	if (isMixedState)
	{
		drawList->AddRectFilled(corner + ImVec2(clearance, clearance), corner + ImVec2(boxSize - clearance, boxSize - clearance), ImColor(colorMark));
	}
	else if (*value)
	{
		ImVec2 checkMarkPts[3] = {
			corner + ImVec2(clearance, clearance + boxSize * 0.3f),
			corner + ImVec2(boxSize * 0.5f, boxSize - clearance),
			corner + ImVec2(boxSize - clearance, clearance),
		};
		drawList->AddPolyline(checkMarkPts, 3, ImColor(colorMark), false, 2.5f);
	}

	ImGui::Dummy(ImVec2(lineHeight + itemSpacing.x, lineHeight));

	if (strlen(buf) > 0)
	{
		ImGui::SameLine();
		ImGui::AlignTextToFramePadding();
		ImGui::Text(buf);
	}

	ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, pos.y + ImGui::GetTextLineHeightWithSpacing() + itemSpacing.y));

	return result;
}

// Please do not forget this!
ImGui::Begin("Example Bug");
bool b = false;
ImGui::CheckboxEx("This is a test", &b);
ImGui::End();
**Version/Branch of Dear ImGui:** Version: 1.78 WIP Branch: master **Back-end/Renderer/Compiler/OS** Back-ends: imgui_impl_dx11.cpp Compiler: MSVC 2019 Operating System: Win10 **My Issue/Question:** This is my replacement for built-in checkbox widget. Tri-state is supported. Color is taken from ImGuiCol_Text, with border alpha dimmed down when not hovered. **Screenshots/Video** ![checkboxes](https://user-images.githubusercontent.com/38907606/87296545-98629400-c50f-11ea-8079-7ddc2467df2d.png) **Standalone, minimal, complete and verifiable example:** ``` #include <imgui/imgui_internal.h> //-- replacement for ImGui::Checkbox bool CheckboxEx(const char* label, bool* value) { assert(value); bool result = false; auto* drawList = ImGui::GetWindowDrawList(); const bool isMixedState = ImGui::GetCurrentWindow()->DC.ItemFlags & ImGuiItemFlags_MixedValue; const auto pos = ImGui::GetCursorScreenPos(); const auto mousePos = ImGui::GetMousePos(); const auto itemSpacing = ImGui::GetStyle().ItemSpacing; const float lineHeight = ImGui::GetTextLineHeight(); const float boxSize = std::floor(lineHeight * 0.9f); const float boxOffsetHorz = std::ceil(itemSpacing.x * 1.3f); const float boxOffsetVert = itemSpacing.y + std::floor(0.5f * (lineHeight - boxSize)); const float clearance = boxSize * 0.2f; const auto corner = pos + ImVec2(boxOffsetHorz, boxOffsetVert); char buf[1024]; strcpy(buf, label); for (int i = 0; i < sizeof(buf); ++i) { if (buf[i] == '#') { buf[i] = '\0'; break; } } const float labelWidth = ImGui::CalcTextSize(buf).x; bool isHovered = ImRect(pos, pos + ImVec2(lineHeight + labelWidth + 2.0f * itemSpacing.x, lineHeight)).Contains(mousePos); ImVec4 color = ImGui::GetStyleColorVec4(ImGuiCol_Text); ImVec4 colorMark = color; color.w *= isHovered ? 1.0f : 0.25f; drawList->AddRect(corner, corner + ImVec2(boxSize, boxSize), ImColor(color), 0.0f, 0, 1.0f); if (isHovered && ImGui::IsWindowHovered()) { if (ImGui::IsMouseClicked(0)) { if (isMixedState) { *value = false; } else { *value = !*value; } result = true; } } if (isMixedState) { drawList->AddRectFilled(corner + ImVec2(clearance, clearance), corner + ImVec2(boxSize - clearance, boxSize - clearance), ImColor(colorMark)); } else if (*value) { ImVec2 checkMarkPts[3] = { corner + ImVec2(clearance, clearance + boxSize * 0.3f), corner + ImVec2(boxSize * 0.5f, boxSize - clearance), corner + ImVec2(boxSize - clearance, clearance), }; drawList->AddPolyline(checkMarkPts, 3, ImColor(colorMark), false, 2.5f); } ImGui::Dummy(ImVec2(lineHeight + itemSpacing.x, lineHeight)); if (strlen(buf) > 0) { ImGui::SameLine(); ImGui::AlignTextToFramePadding(); ImGui::Text(buf); } ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x, pos.y + ImGui::GetTextLineHeightWithSpacing() + itemSpacing.y)); return result; } // Please do not forget this! ImGui::Begin("Example Bug"); bool b = false; ImGui::CheckboxEx("This is a test", &b); ImGui::End(); ```
ocornut commented 2020-07-13 11:25:06 +00:00 (Migrated from github.com)

Hello @mrduda,

Thanks for submitting this.
You should be able to achieve a similar look by altering border color, frame color, checkmark colors:

image

(the checkmark shape is a little different, and sorry for blurry upscaled capture)

The aim of styling v2 will be to provide finer control per-widget style so this would be easier to alter the look of this while keeping a different look for other widgets.

Suggestions of things in your code in your widgets can could improved:

  • Your hovering check should be replaced with ItemHoverable or ButtonBehavior in order to honor all hovering rules (e.g. inhibited by popup, keyboard navigation).
  • Your copy and search for # is incorrect and unnecessary. You can call ImGui::RenderText() which will call FindRenderedTextEnd() to do the same in a more correct way.
  • Multiple items means IsItemXXX() function won't be honored on your custom Checkbox.

Modelling your code after Checkbox() would be preferable and will fix all those issues.

Hello @mrduda, Thanks for submitting this. You should be able to achieve a similar look by altering border color, frame color, checkmark colors: ![image](https://user-images.githubusercontent.com/8225057/87298980-7830d600-c50b-11ea-952a-7ada776a8991.png) (the checkmark shape is a little different, and sorry for blurry upscaled capture) The aim of styling v2 will be to provide finer control per-widget style so this would be easier to alter the look of this while keeping a different look for other widgets. Suggestions of things in your code in your widgets can could improved: - Your hovering check should be replaced with ItemHoverable or ButtonBehavior in order to honor all hovering rules (e.g. inhibited by popup, keyboard navigation). - Your copy and search for `#` is incorrect and unnecessary. You can call ImGui::RenderText() which will call `FindRenderedTextEnd()` to do the same in a more correct way. - Multiple items means `IsItemXXX()` function won't be honored on your custom Checkbox. Modelling your code after `Checkbox()` would be preferable and will fix all those issues.
mrduda commented 2020-07-13 15:40:23 +00:00 (Migrated from github.com)

Thank you for feedback!

The main reason behind this custom checkbox implementation was the size of original checkbox, which can't be decreased in any known way. The rest (color and border) is just a minor issue.

Thank you for feedback! The main reason behind this custom checkbox implementation was the size of original checkbox, which can't be decreased in any known way. The rest (color and border) is just a minor issue.
rokups commented 2020-07-13 15:52:22 +00:00 (Migrated from github.com)

You can modify checkbox size by adjusting style (ItemInnerSpacing, FramePadding) and font size. See ocornut/imgui@ab4ef822f0/imgui_widgets.cpp (L1044)

You can modify checkbox size by adjusting style (`ItemInnerSpacing`, `FramePadding`) and font size. See https://github.com/ocornut/imgui/blob/ab4ef822f0e85a6dd65723db9e0e632a1e1a45d8/imgui_widgets.cpp#L1044
mrduda commented 2020-07-14 06:24:42 +00:00 (Migrated from github.com)

Here's another approach, this time I took existing Checkbox() code and modified it only in size, style/color and checkmark parts:

bool CheckboxEx(const char* label, bool* v)
{
	ImGuiWindow* window = GetCurrentWindow();
	if (window->SkipItems)
		return false;

	ImGuiContext& g = *GImGui;
	const ImGuiStyle& style = g.Style;
	const ImGuiID id = window->GetID(label);
	const ImVec2 label_size = CalcTextSize(label, NULL, true);

	const float square_sz = GetFrameHeight();
	const ImVec2 pos = window->DC.CursorPos;
	const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 1.5f));
	ItemSize(total_bb, style.FramePadding.y);
	if (!ItemAdd(total_bb, id))
		return false;

	bool hovered, held;
	bool pressed = ButtonBehavior(total_bb, id, &hovered, &held);
	if (pressed)
	{
		*v = !(*v);
		MarkItemEdited(id);
	}

	const float boxSize = IM_FLOOR(square_sz * 0.65f);
	const float boxOffsetHorz = IM_FLOOR(style.ItemSpacing.x * 1.2f);
	const float boxOffsetVert = IM_FLOOR(std::floor(0.5f * (square_sz - boxSize)));

	ImRect check_bb(pos + ImVec2(boxOffsetHorz, boxOffsetVert), pos + ImVec2(boxOffsetHorz + boxSize, boxOffsetVert + boxSize));
	ImU32 check_col = GetColorU32((window->DC.ItemFlags & ImGuiItemFlags_Disabled) ? ImGuiCol_FrameBg : ImGuiCol_Text);
	RenderNavHighlight(total_bb, id);

	ImVec4 border_col = style.Colors[hovered ? ImGuiCol_Text : ImGuiCol_CheckMark];
	border_col.w *= 0.5f;

	ImGui::PushStyleColor(ImGuiCol_Border, border_col);
	ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
	RenderFrame(check_bb.Min, check_bb.Max, ImColor(0, 0, 0, 0), true, 0.0f);
	ImGui::PopStyleVar();
	ImGui::PopStyleColor();

	if (window->DC.ItemFlags & ImGuiItemFlags_MixedValue)
	{
		// Undocumented tristate/mixed/indeterminate checkbox (#2644)
		ImVec2 pad(ImMax(1.0f, IM_FLOOR(square_sz / 7.0f)), ImMax(1.0f, IM_FLOOR(square_sz / 7.0f)));
		window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, 0.0f);
	}
	else if (*v)
	{
		const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f));
		ImVec2 checkMarkPts[3] = {
			check_bb.Min + ImVec2(pad, pad + boxSize * 0.3f),
			check_bb.Min + ImVec2(boxSize * 0.45f, boxSize - pad),
			check_bb.Min + ImVec2(boxSize - pad, pad),
		};
		window->DrawList->AddPolyline(checkMarkPts, 3, ImColor(check_col), false, 2.5f);
	}

	check_bb = ImRect(pos, pos + ImVec2(square_sz, square_sz));

	if (g.LogEnabled)
		LogRenderedText(&total_bb.Min, *v ? "[x]" : "[ ]");
	if (label_size.x > 0.0f)
		RenderText(ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y), label);

	IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0));
	return pressed;
}

As for ItemInnerSpacing, FramePadding and font size suggestion: have you tried this and it worked, or is it only a guess? I'm not sure it will work as expected because changing font size may result in the incorrect text size calculation for checkbox label.

Here's another approach, this time I took existing Checkbox() code and modified it only in size, style/color and checkmark parts: ``` bool CheckboxEx(const char* label, bool* v) { ImGuiWindow* window = GetCurrentWindow(); if (window->SkipItems) return false; ImGuiContext& g = *GImGui; const ImGuiStyle& style = g.Style; const ImGuiID id = window->GetID(label); const ImVec2 label_size = CalcTextSize(label, NULL, true); const float square_sz = GetFrameHeight(); const ImVec2 pos = window->DC.CursorPos; const ImRect total_bb(pos, pos + ImVec2(square_sz + (label_size.x > 0.0f ? style.ItemInnerSpacing.x + label_size.x : 0.0f), label_size.y + style.FramePadding.y * 1.5f)); ItemSize(total_bb, style.FramePadding.y); if (!ItemAdd(total_bb, id)) return false; bool hovered, held; bool pressed = ButtonBehavior(total_bb, id, &hovered, &held); if (pressed) { *v = !(*v); MarkItemEdited(id); } const float boxSize = IM_FLOOR(square_sz * 0.65f); const float boxOffsetHorz = IM_FLOOR(style.ItemSpacing.x * 1.2f); const float boxOffsetVert = IM_FLOOR(std::floor(0.5f * (square_sz - boxSize))); ImRect check_bb(pos + ImVec2(boxOffsetHorz, boxOffsetVert), pos + ImVec2(boxOffsetHorz + boxSize, boxOffsetVert + boxSize)); ImU32 check_col = GetColorU32((window->DC.ItemFlags & ImGuiItemFlags_Disabled) ? ImGuiCol_FrameBg : ImGuiCol_Text); RenderNavHighlight(total_bb, id); ImVec4 border_col = style.Colors[hovered ? ImGuiCol_Text : ImGuiCol_CheckMark]; border_col.w *= 0.5f; ImGui::PushStyleColor(ImGuiCol_Border, border_col); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); RenderFrame(check_bb.Min, check_bb.Max, ImColor(0, 0, 0, 0), true, 0.0f); ImGui::PopStyleVar(); ImGui::PopStyleColor(); if (window->DC.ItemFlags & ImGuiItemFlags_MixedValue) { // Undocumented tristate/mixed/indeterminate checkbox (#2644) ImVec2 pad(ImMax(1.0f, IM_FLOOR(square_sz / 7.0f)), ImMax(1.0f, IM_FLOOR(square_sz / 7.0f))); window->DrawList->AddRectFilled(check_bb.Min + pad, check_bb.Max - pad, check_col, 0.0f); } else if (*v) { const float pad = ImMax(1.0f, IM_FLOOR(square_sz / 6.0f)); ImVec2 checkMarkPts[3] = { check_bb.Min + ImVec2(pad, pad + boxSize * 0.3f), check_bb.Min + ImVec2(boxSize * 0.45f, boxSize - pad), check_bb.Min + ImVec2(boxSize - pad, pad), }; window->DrawList->AddPolyline(checkMarkPts, 3, ImColor(check_col), false, 2.5f); } check_bb = ImRect(pos, pos + ImVec2(square_sz, square_sz)); if (g.LogEnabled) LogRenderedText(&total_bb.Min, *v ? "[x]" : "[ ]"); if (label_size.x > 0.0f) RenderText(ImVec2(check_bb.Max.x + style.ItemInnerSpacing.x, check_bb.Min.y + style.FramePadding.y), label); IMGUI_TEST_ENGINE_ITEM_INFO(id, label, window->DC.ItemFlags | ImGuiItemStatusFlags_Checkable | (*v ? ImGuiItemStatusFlags_Checked : 0)); return pressed; } ``` As for ItemInnerSpacing, FramePadding and font size suggestion: have you tried this and it worked, or is it only a guess? I'm not sure it will work as expected because changing font size may result in the incorrect text size calculation for checkbox label.
rokups commented 2020-07-14 08:41:37 +00:00 (Migrated from github.com)

You should be using PushStyleVar()/PopStyleVar() or PushStyleColor()/PopStyleColor() instead of copying entire widget code and modifying those values.

You should be using `PushStyleVar()`/`PopStyleVar()` or `PushStyleColor()`/`PopStyleColor()` instead of copying entire widget code and modifying those values.
mrduda commented 2020-07-15 06:23:22 +00:00 (Migrated from github.com)

You should be using PushStyleVar()/PopStyleVar() or PushStyleColor()/PopStyleColor() instead of copying entire widget code and modifying those values.

As I said before, wrapping ImGui::Checkbox() into Push/PopStyleVar/Color doesn't help to achieve desired checkbox size. The method you have shown (by changing font size and other) does not work. Also, checkmark is custom and the color for checkbox border when mouse is over cannot be changed by Push/Pop calls. Anyway, thanks for attention.

> You should be using `PushStyleVar()`/`PopStyleVar()` or `PushStyleColor()`/`PopStyleColor()` instead of copying entire widget code and modifying those values. As I said before, wrapping ImGui::Checkbox() into Push/PopStyleVar/Color doesn't help to achieve desired checkbox size. The method you have shown (by changing font size and other) does not work. Also, checkmark is custom and the color for checkbox border when mouse is over cannot be changed by Push/Pop calls. Anyway, thanks for attention.
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ocornut/imgui#3351
No description provided.