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

Checkbox widget replacement with better look #3351

Open
mrduda opened this issue Jul 13, 2020 · 6 comments
Open

Checkbox widget replacement with better look #3351

mrduda opened this issue Jul 13, 2020 · 6 comments
Labels

Comments

@mrduda
Copy link

mrduda commented Jul 13, 2020

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();
@ocornut ocornut added the style label Jul 13, 2020
@ocornut
Copy link
Owner

ocornut commented Jul 13, 2020

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.

@mrduda
Copy link
Author

mrduda commented Jul 13, 2020

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

rokups commented Jul 13, 2020

You can modify checkbox size by adjusting style (ItemInnerSpacing, FramePadding) and font size. See

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 * 2.0f));

@mrduda
Copy link
Author

mrduda commented Jul 14, 2020

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

rokups commented Jul 14, 2020

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

@mrduda
Copy link
Author

mrduda commented Jul 15, 2020

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 up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants