ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
60.95k stars 10.29k forks source link

Checkbox widget replacement with better look #3351

Open mrduda opened 4 years ago

mrduda commented 4 years ago

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 commented 4 years ago

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:

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

mrduda commented 4 years ago

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 4 years ago

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 4 years ago

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 4 years ago

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

mrduda commented 4 years ago

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.