diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index e0806f145..a27c45b32 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -166,6 +166,9 @@ Other Changes: - Misc: - Minor optimization: reduce redudant label scanning in common widgets. - Added missing Test Engine hooks for PlotXXX(), VSliderXXX(), TableHeader(). +- Demo: + - Added simple demo for a scrollable/zoomable image viewer with a grid. + Available in `Examples->Image Viewer` and `Widgets->Images`. - Backends: - Added support for new standardized draw callbacks in most backends: (#9378) - Allegro5: Reset n/a n/a diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 4adb48fb0..d43d3495f 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -73,6 +73,7 @@ Index of this file: // [SECTION] Demo Window / ShowDemoWindow() // [SECTION] DemoWindowMenuBar() // [SECTION] Helpers: ExampleTreeNode, ExampleMemberInfo (for use by Property Editor & Multi-Select demos) +// [SECTION] Helpers: ExampleImageViewer // [SECTION] DemoWindowWidgetsBasic() // [SECTION] DemoWindowWidgetsBullets() // [SECTION] DemoWindowWidgetsCollapsingHeaders() @@ -108,6 +109,7 @@ Index of this file: // [SECTION] User Guide / ShowUserGuide() // [SECTION] Example App: Main Menu Bar / ShowExampleAppMainMenuBar() // [SECTION] Example App: Debug Console / ShowExampleAppConsole() +// [SECTION] Example App: Image Viewer / ShowExampleAppImageViewer() // [SECTION] Example App: Debug Log / ShowExampleAppLog() // [SECTION] Example App: Simple Layout / ShowExampleAppLayout() // [SECTION] Example App: Property Editor / ShowExampleAppPropertyEditor() @@ -238,6 +240,7 @@ static void ShowExampleAppAssetsBrowser(bool* p_open); static void ShowExampleAppConsole(bool* p_open); static void ShowExampleAppCustomRendering(bool* p_open); static void ShowExampleAppDocuments(bool* p_open); +static void ShowExampleAppImageViewer(bool* p_open); static void ShowExampleAppLog(bool* p_open); static void ShowExampleAppLayout(bool* p_open); static void ShowExampleAppPropertyEditor(bool* p_open, ImGuiDemoWindowData* demo_data); @@ -308,6 +311,7 @@ struct ImGuiDemoWindowData bool ShowAppConsole = false; bool ShowAppCustomRendering = false; bool ShowAppDocuments = false; + bool ShowAppImageViewer = false; bool ShowAppLog = false; bool ShowAppLayout = false; bool ShowAppPropertyEditor = false; @@ -353,6 +357,7 @@ void ImGui::ShowDemoWindow(bool* p_open) if (demo_data.ShowAppAssetsBrowser) { ShowExampleAppAssetsBrowser(&demo_data.ShowAppAssetsBrowser); } if (demo_data.ShowAppConsole) { ShowExampleAppConsole(&demo_data.ShowAppConsole); } if (demo_data.ShowAppCustomRendering) { ShowExampleAppCustomRendering(&demo_data.ShowAppCustomRendering); } + if (demo_data.ShowAppImageViewer) { ShowExampleAppImageViewer(&demo_data.ShowAppImageViewer); } if (demo_data.ShowAppLog) { ShowExampleAppLog(&demo_data.ShowAppLog); } if (demo_data.ShowAppLayout) { ShowExampleAppLayout(&demo_data.ShowAppLayout); } if (demo_data.ShowAppPropertyEditor) { ShowExampleAppPropertyEditor(&demo_data.ShowAppPropertyEditor, &demo_data); } @@ -677,6 +682,7 @@ static void DemoWindowMenuBar(ImGuiDemoWindowData* demo_data) ImGui::MenuItem("Console", NULL, &demo_data->ShowAppConsole); ImGui::MenuItem("Custom rendering", NULL, &demo_data->ShowAppCustomRendering); ImGui::MenuItem("Documents", NULL, &demo_data->ShowAppDocuments); + ImGui::MenuItem("Image Viewer", NULL, &demo_data->ShowAppImageViewer); ImGui::MenuItem("Log", NULL, &demo_data->ShowAppLog); ImGui::MenuItem("Property editor", NULL, &demo_data->ShowAppPropertyEditor); ImGui::MenuItem("Simple layout", NULL, &demo_data->ShowAppLayout); @@ -825,6 +831,87 @@ static ExampleTreeNode* ExampleTree_CreateDemoTree() return node_L0; } +//----------------------------------------------------------------------------- +// [SECTION] Helpers: ExampleImageViewer +//----------------------------------------------------------------------------- + +struct ExampleImageViewerData +{ + ImU32 ImageBgColor = IM_COL32(100, 100, 100, 255); + ImU32 GridColor = IM_COL32(255, 255, 255, 100); + bool GridEnabled = true; + bool ViewReset = true; + ImVec2 ViewOffset; // in image space + float Zoom = 10.0f; + float ZoomMin = 1.0f; + float ZoomMax = 10000.0f; +}; + +static void ExampleImageViewer_DrawOptions(ExampleImageViewerData* data) +{ + ImGui::SetNextItemShortcut(ImGuiKey_G, ImGuiInputFlags_Tooltip); // | ImGuiInputFlags_RouteGlobal + ImGui::Checkbox("Grid", &data->GridEnabled); + ImGui::SameLine(); + ImGui::SetNextItemWidth(ImGui::GetFontSize() * 10.0f); + float zoom_100 = data->Zoom * 100.0f; + if (ImGui::DragFloat("##Zoom", &zoom_100, 5.0f, data->ZoomMin * 100.0f, data->ZoomMax * 100.0f, "%.0f%%", ImGuiSliderFlags_AlwaysClamp)) + data->Zoom = zoom_100 / 100.0f; +} + +static void ExampleImageViewer_DrawCanvas(ExampleImageViewerData* data, ImVec2 canvas_size, ImTextureRef image_tex_ref, int image_w, int image_h) +{ + ImGuiIO& io = ImGui::GetIO(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + IM_ASSERT(canvas_size.x >= 0.0f && canvas_size.y >= 0.0f); + + // Layout canvas + ImGui::InvisibleButton("##Canvas", canvas_size); + ImVec2 canvas_min = ImGui::GetItemRectMin(); + ImVec2 canvas_max = ImGui::GetItemRectMax(); + + if (data->ViewReset) + data->ViewOffset = ImVec2((canvas_size.x * 0.5f / data->Zoom) - 0.5f, (canvas_size.y * 0.5f / data->Zoom) - 0.5f); // Add half a pixel padding + data->ViewReset = false; + + // Handle inputs + ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY); // FIXME: Not while scrolling? + if (ImGui::IsItemHovered() && io.MouseWheel != 0.0f) + data->Zoom = IM_CLAMP(data->Zoom * (1.0f + io.MouseWheel * 0.10f), data->ZoomMin, data->ZoomMax); + float zoom = data->Zoom; // (float)(int)ViewZoom; + if (ImGui::IsItemActive() && ImGui::IsMouseDragging(0)) + { + data->ViewOffset.x -= io.MouseDelta.x / zoom; + data->ViewOffset.y -= io.MouseDelta.y / zoom; + } + + // Display image + ImVec2 image_min, image_max; + image_min.x = (float)(int)((canvas_min.x - (data->ViewOffset.x * zoom)) + (canvas_size.x * 0.5f)); + image_min.y = (float)(int)((canvas_min.y - (data->ViewOffset.y * zoom)) + (canvas_size.y * 0.5f)); + image_max.x = (float)(int)(image_min.x + image_w * zoom); + image_max.y = (float)(int)(image_min.y + image_h * zoom); + draw_list->AddRect(ImVec2(canvas_min.x - 1.0f, canvas_min.y - 1.0f), ImVec2(canvas_max.x + 1.0f, canvas_max.y + 1.0f), IM_COL32(255, 255, 255, 255)); + draw_list->PushClipRect(canvas_min, canvas_max, true); + draw_list->AddRectFilled(image_min, image_max, data->ImageBgColor); + if (platform_io.DrawCallback_SetSamplerNearest != NULL) + draw_list->AddCallback(platform_io.DrawCallback_SetSamplerNearest); + draw_list->AddImage(image_tex_ref, image_min, image_max); + if (platform_io.DrawCallback_SetSamplerLinear != NULL) + draw_list->AddCallback(ImGui::GetPlatformIO().DrawCallback_SetSamplerLinear); + + // Display grid lines for visible pixels + if (data->GridEnabled && zoom > 6.0f) + { + const float step = (float)zoom; + for (int px = (int)((canvas_min.x - image_min.x) / step); px <= (int)((canvas_max.x - image_min.x) / step); px++) + draw_list->AddLineV(image_min.x + px * step, canvas_min.y, canvas_max.y, data->GridColor, 1.0f); + for (int py = (int)((canvas_min.y - image_min.y) / step); py <= (int)((canvas_max.y - image_min.y) / step); py++) + draw_list->AddLineH(canvas_min.x, canvas_max.x, image_min.y + py * step, data->GridColor, 1.0f); + } + draw_list->PopClipRect(); +} + //----------------------------------------------------------------------------- // [SECTION] DemoWindowWidgetsBasic() //----------------------------------------------------------------------------- @@ -1802,40 +1889,29 @@ static void DemoWindowWidgetsImages() // - Read https://github.com/ocornut/imgui/wiki/Image-Loading-and-Displaying-Examples // Grab the current texture identifier used by the font atlas. - ImTextureRef my_tex_id = io.Fonts->TexRef; + ImFontAtlas* atlas = io.Fonts; + ImTextureRef my_tex_id = atlas->TexRef; + float my_tex_w = (float)atlas->TexData->Width; // Regular user code should never have to care about TexData-> fields, but since we want to display the entire texture here, we pull Width/Height from it. + float my_tex_h = (float)atlas->TexData->Height; + ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h); - // Regular user code should never have to care about TexData-> fields, but since we want to display the entire texture here, we pull Width/Height from it. - float my_tex_w = (float)io.Fonts->TexData->Width; - float my_tex_h = (float)io.Fonts->TexData->Height; + // Basic drawing + ImGui::SeparatorText("Image()/ImageWithBg() function"); + ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left + ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right + ImGui::PushStyleVar(ImGuiStyleVar_ImageBorderSize, IM_MAX(1.0f, ImGui::GetStyle().ImageBorderSize)); + ImGui::ImageWithBg(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); + ImGui::PopStyleVar(); - { - ImGui::Text("%.0fx%.0f", my_tex_w, my_tex_h); - ImVec2 pos = ImGui::GetCursorScreenPos(); - ImVec2 uv_min = ImVec2(0.0f, 0.0f); // Top-left - ImVec2 uv_max = ImVec2(1.0f, 1.0f); // Lower-right - ImGui::PushStyleVar(ImGuiStyleVar_ImageBorderSize, IM_MAX(1.0f, ImGui::GetStyle().ImageBorderSize)); - ImGui::ImageWithBg(my_tex_id, ImVec2(my_tex_w, my_tex_h), uv_min, uv_max, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); - if (ImGui::BeginItemTooltip()) - { - float region_sz = 32.0f; - float region_x = io.MousePos.x - pos.x - region_sz * 0.5f; - float region_y = io.MousePos.y - pos.y - region_sz * 0.5f; - float zoom = 4.0f; - if (region_x < 0.0f) { region_x = 0.0f; } - else if (region_x > my_tex_w - region_sz) { region_x = my_tex_w - region_sz; } - if (region_y < 0.0f) { region_y = 0.0f; } - else if (region_y > my_tex_h - region_sz) { region_y = my_tex_h - region_sz; } - ImGui::Text("Min: (%.2f, %.2f)", region_x, region_y); - ImGui::Text("Max: (%.2f, %.2f)", region_x + region_sz, region_y + region_sz); - ImVec2 uv0 = ImVec2((region_x) / my_tex_w, (region_y) / my_tex_h); - ImVec2 uv1 = ImVec2((region_x + region_sz) / my_tex_w, (region_y + region_sz) / my_tex_h); - ImGui::ImageWithBg(my_tex_id, ImVec2(region_sz * zoom, region_sz * zoom), uv0, uv1, ImVec4(0.0f, 0.0f, 0.0f, 1.0f)); - ImGui::EndTooltip(); - } - ImGui::PopStyleVar(); - } + // Fancy widget + ImGui::SeparatorText("Interactive Image Viewer"); + static ExampleImageViewerData image_viewer; + ImVec2 canvas_size(ImGui::GetContentRegionAvail().x, my_tex_h * 2.0f); + ExampleImageViewer_DrawOptions(&image_viewer); + ExampleImageViewer_DrawCanvas(&image_viewer, canvas_size, my_tex_id, (int)my_tex_w, (int)my_tex_h); IMGUI_DEMO_MARKER("Widgets/Images/Textured buttons"); + ImGui::SeparatorText("Textured Buttons"); ImGui::TextWrapped("And now some textured buttons.."); static int pressed_count = 0; for (int i = 0; i < 8; i++) @@ -9239,6 +9315,28 @@ static void ShowExampleAppConsole(bool* p_open) console.Draw("Example: Console", p_open); } +//----------------------------------------------------------------------------- +// [SECTION] Example App: Image Viewer / ShowExampleAppImageViewer() +//----------------------------------------------------------------------------- + +static void ShowExampleAppImageViewer(bool* p_open) +{ + ImFontAtlas* atlas = ImGui::GetIO().Fonts; + ImTextureRef tex_ref = atlas->TexRef; // We don't have access to other textures in this demo! + int tex_w = atlas->TexData->Width; + int tex_h = atlas->TexData->Height; + if (ImGui::Begin("Example: Image Viewer", p_open)) + { + static ExampleImageViewerData image_viewer; + ExampleImageViewer_DrawOptions(&image_viewer); + ImVec2 canvas_size = ImGui::GetContentRegionAvail(); + ImVec2 canvas_min_size = ImGui::IsWindowAppearing() ? ImVec2(3.0f * tex_w, 4.0f * tex_h) : ImVec2(1.0f, 1.0f); + canvas_size = ImVec2(IM_MAX(canvas_size.x, canvas_min_size.x), IM_MAX(canvas_size.y, canvas_min_size.y)); + ExampleImageViewer_DrawCanvas(&image_viewer, canvas_size, tex_ref, tex_w, tex_h); + } + ImGui::End(); +} + //----------------------------------------------------------------------------- // [SECTION] Example App: Debug Log / ShowExampleAppLog() //-----------------------------------------------------------------------------