diff --git a/imgui.cpp b/imgui.cpp index 2bb3ad3d9..9a878b6fd 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1341,6 +1341,7 @@ static void UpdateTexturesNewFrame(); static void UpdateTexturesEndFrame(); static void UpdateSettings(); static int UpdateWindowManualResize(ImGuiWindow* window, int* border_hovered, int* border_held, int resize_grip_count, ImU32 resize_grip_col[4], const ImRect& visibility_rect); +static void RenderWindowShadow(ImGuiWindow* window); static void RenderWindowOuterBorders(ImGuiWindow* window); static void RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar_rect, bool title_bar_is_highlight, bool handle_borders_and_resize_grips, int resize_grip_count, const ImU32 resize_grip_col[4], float resize_grip_draw_size); static void RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open); @@ -1466,6 +1467,9 @@ ImGuiStyle::ImGuiStyle() AntiAliasedFill = true; // Enable anti-aliased filled shapes (rounded rectangles, circles, etc.). CurveTessellationTol = 1.25f; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. CircleTessellationMaxError = 0.30f; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + WindowShadowSize = 100.0f; // Size (in pixels) of window shadows. + WindowShadowOffsetDist = 0.0f; // Offset distance (in pixels) of window shadows from casting window. + WindowShadowOffsetAngle = IM_PI * 0.25f; // Offset angle of window shadows from casting window (0.0f = left, 0.5f*PI = bottom, 1.0f*PI = right, 1.5f*PI = top). // Behaviors HoverStationaryDelay = 0.15f; // Delay for IsItemHovered(ImGuiHoveredFlags_Stationary). Time required to consider mouse stationary. @@ -3727,6 +3731,7 @@ const char* ImGui::GetStyleColorName(ImGuiCol idx) case ImGuiCol_NavWindowingHighlight: return "NavWindowingHighlight"; case ImGuiCol_NavWindowingDimBg: return "NavWindowingDimBg"; case ImGuiCol_ModalWindowDimBg: return "ModalWindowDimBg"; + case ImGuiCol_WindowShadow: return "WindowShadow"; } IM_ASSERT(0); return "Unknown"; @@ -7084,6 +7089,11 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar window->DrawList->AddRectFilled(window->Pos + ImVec2(0, window->TitleBarHeight), window->Pos + window->Size, bg_col, window_rounding, (flags & ImGuiWindowFlags_NoTitleBar) ? 0 : ImDrawFlags_RoundCornersBottom); } + // Draw window shadow + if (style.WindowShadowSize > 0.0f && (!(flags & ImGuiWindowFlags_ChildWindow) || (flags & ImGuiWindowFlags_Popup))) + if (style.Colors[ImGuiCol_WindowShadow].w > 0.0f) + RenderWindowShadow(window); + // Title bar if (!(flags & ImGuiWindowFlags_NoTitleBar)) { @@ -7132,6 +7142,16 @@ void ImGui::RenderWindowDecorations(ImGuiWindow* window, const ImRect& title_bar window->DC.NavLayerCurrent = ImGuiNavLayer_Main; } +void ImGui::RenderWindowShadow(ImGuiWindow* window) +{ + ImGuiContext& g = *GImGui; + ImGuiStyle& style = g.Style; + float shadow_size = style.WindowShadowSize; + ImU32 shadow_col = GetColorU32(ImGuiCol_WindowShadow); + ImVec2 shadow_offset = ImVec2(ImCos(style.WindowShadowOffsetAngle), ImSin(style.WindowShadowOffsetAngle)) * style.WindowShadowOffsetDist; + window->DrawList->AddShadowRect(window->Pos, window->Pos + window->Size, shadow_col, shadow_size, shadow_offset, ImDrawFlags_ShadowCutOutShapeBackground, window->WindowRounding); +} + // Render title text, collapse button, close button void ImGui::RenderWindowTitleBarContents(ImGuiWindow* window, const ImRect& title_bar_rect, const char* name, bool* p_open) { @@ -9004,6 +9024,12 @@ void ImGui::UpdateCurrentFontSize(float restore_font_size_after_scaling) g.FontBakedScale = (g.Font != NULL && window != NULL) ? (g.FontSize / g.FontBaked->Size) : 0.0f; g.DrawListSharedData.FontSize = g.FontSize; g.DrawListSharedData.FontScale = g.FontBakedScale; + if (g.Font) + { + ImFontAtlas* atlas = g.Font->OwnerAtlas; + g.DrawListSharedData.ShadowRectIds = &atlas->ShadowRectIds[0]; + g.DrawListSharedData.ShadowRectUvs = &atlas->ShadowRectUvs[0]; + } } // Exposed in case user may want to override setting density. diff --git a/imgui.h b/imgui.h index be945c611..42615dc5a 100644 --- a/imgui.h +++ b/imgui.h @@ -1785,6 +1785,7 @@ enum ImGuiCol_ ImGuiCol_NavWindowingHighlight, // Highlight window when using Ctrl+Tab ImGuiCol_NavWindowingDimBg, // Darken/colorize entire screen behind the Ctrl+Tab window list, when active ImGuiCol_ModalWindowDimBg, // Darken/colorize entire screen behind a modal window, when one is active + ImGuiCol_WindowShadow, // Window shadows ImGuiCol_COUNT, #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS @@ -2329,6 +2330,10 @@ struct ImGuiStyle float CurveTessellationTol; // Tessellation tolerance when using PathBezierCurveTo() without a specific number of segments. Decrease for highly tessellated curves (higher quality, more polygons), increase to reduce quality. float CircleTessellationMaxError; // Maximum error (in pixels) allowed when using AddCircle()/AddCircleFilled() or drawing rounded corner rectangles with no explicit segment count specified. Decrease for higher quality but more geometry. + float WindowShadowSize; // Size (in pixels) of window shadows. Set this to zero to disable shadows. + float WindowShadowOffsetDist; // Offset distance (in pixels) of window shadows from casting window. + float WindowShadowOffsetAngle; // Offset angle of window shadows from casting window (0.0f = left, 0.5f*PI = bottom, 1.0f*PI = right, 1.5f*PI = top). + // Colors ImVec4 Colors[ImGuiCol_COUNT]; @@ -3223,6 +3228,7 @@ enum ImDrawFlags_ ImDrawFlags_RoundCornersAll = ImDrawFlags_RoundCornersTopLeft | ImDrawFlags_RoundCornersTopRight | ImDrawFlags_RoundCornersBottomLeft | ImDrawFlags_RoundCornersBottomRight, ImDrawFlags_RoundCornersDefault_ = ImDrawFlags_RoundCornersAll, // Default to ALL corners if none of the _RoundCornersXX flags are specified. ImDrawFlags_RoundCornersMask_ = ImDrawFlags_RoundCornersAll | ImDrawFlags_RoundCornersNone, + ImDrawFlags_ShadowCutOutShapeBackground = 1 << 9, // Do not render the shadow shape under the objects to be shadowed to save on fill-rate or facilitate blending. Slower on CPU. }; // Flags for ImDrawList instance. Those are set automatically by ImGui:: functions from ImGuiIO settings, and generally not manipulated directly. @@ -3321,6 +3327,23 @@ struct ImDrawList IMGUI_API void AddImageQuad(ImTextureRef tex_ref, const ImVec2& p1, const ImVec2& p2, const ImVec2& p3, const ImVec2& p4, const ImVec2& uv1 = ImVec2(0, 0), const ImVec2& uv2 = ImVec2(1, 0), const ImVec2& uv3 = ImVec2(1, 1), const ImVec2& uv4 = ImVec2(0, 1), ImU32 col = IM_COL32_WHITE); IMGUI_API void AddImageRounded(ImTextureRef tex_ref, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min, const ImVec2& uv_max, ImU32 col, float rounding, ImDrawFlags flags = 0); + // Shadows primitives + // [BETA] API + // - Add shadow for a object, with min/max or center/radius describing the object extents, and offset shifting the shadow. + // - Rounding parameters refer to the object itself, not the shadow! + // - By default, the area under the object is filled, because this is simpler to process. + // Using the ImDrawFlags_ShadowCutOutShapeBackground flag makes the function not render this area and leave a hole under the object. + // - Shadows w/ fill under the object: a bit faster for CPU, more pixels rendered, visible/darkening if used behind a transparent shape. + // Typically used by: small, frequent objects, opaque objects, transparent objects if shadow darkening isn't an issue. + // - Shadows w/ hole under the object: a bit slower for CPU, less pixels rendered, no difference if used behind a transparent shape. + // Typically used by: large, infrequent objects, transparent objects if exact blending/color matter. + // - FIXME-SHADOWS: 'offset' + ImDrawFlags_ShadowCutOutShapeBackground are not currently supported together with AddShadowCircle(), AddShadowConvexPoly(), AddShadowNGon(). + #define IMGUI_HAS_SHADOWS 1 + IMGUI_API void AddShadowRect(const ImVec2& obj_min, const ImVec2& obj_max, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags = 0, float obj_rounding = 0.0f); + IMGUI_API void AddShadowCircle(const ImVec2& obj_center, float obj_radius, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags = 0, int obj_num_segments = 12); + IMGUI_API void AddShadowConvexPoly(const ImVec2* points, int points_count, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags = 0); + IMGUI_API void AddShadowNGon(const ImVec2& obj_center, float obj_radius, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags, int obj_num_segments); + // Stateful path API, add points then finish with PathFillConvex() or PathStroke() // - Important: filled shapes must always use clockwise winding order! The anti-aliasing fringe depends on it. Counter-clockwise shapes will have "inward" anti-aliasing. // so e.g. 'PathArcTo(center, radius, PI * -0.5f, PI)' is ok, whereas 'PathArcTo(center, radius, PI, PI * -0.5f)' won't have correct anti-aliasing when followed by PathFillConvex(). @@ -3511,6 +3534,24 @@ struct ImTextureData // [SECTION] Font API (ImFontConfig, ImFontGlyph, ImFontAtlasFlags, ImFontAtlas, ImFontGlyphRangesBuilder, ImFont) //----------------------------------------------------------------------------- +// [Internal] Shadow texture baking config +struct ImFontAtlasShadowTexConfig +{ + int TexCornerSize; // Size of the corner areas. + int TexEdgeSize; // Size of the edge areas (and by extension the center). Changing this is normally unnecessary. + float TexFalloffPower; // The power factor for the shadow falloff curve. + float TexDistanceFieldOffset; // How much to offset the distance field by (allows over/under-shadowing, potentially useful for accommodating rounded corners on the "casting" shape). + bool TexBlur; // Do we want to Gaussian blur the shadow texture? + + inline ImFontAtlasShadowTexConfig() { memset(this, 0, sizeof(*this)); } + IMGUI_API void SetupDefaults(); + int GetRectTexPadding() const { return 2; } // Number of pixels of padding to add to the rectangular texture to avoid sampling artifacts at the edges. + int CalcRectTexSize() const { return TexCornerSize + TexEdgeSize + GetRectTexPadding(); } // The size of the texture area required for the actual 2x2 rectangle shadow texture (after the redundant corners have been removed). Padding is required here to avoid sampling artifacts at the edge adjoining the removed corners. int CalcConvexTexWidth() const; // The width of the texture area required for the convex shape shadow texture. + int GetConvexTexPadding() const { return 8; } // Number of pixels of padding to add to the convex shape texture to avoid sampling artifacts at the edges. This also acts as padding for the expanded corner triangles. + int CalcConvexTexWidth() const; // The width of the texture area required for the convex shape shadow texture. + int CalcConvexTexHeight() const; // The height of the texture area required for the convex shape shadow texture. +}; + // A font input/source (we may rename this to ImFontSource in the future) struct ImFontConfig { @@ -3757,6 +3798,11 @@ struct ImFontAtlas int RefCount; // Number of contexts using this atlas ImGuiContext* OwnerContext; // Context which own the atlas will be in charge of updating and destroying it. + // [Internal] Shadow data + ImFontAtlasRectId ShadowRectIds[2]; // IDs of rect for shadow textures + ImVec4 ShadowRectUvs[10]; // UV coordinates for shadow textures, 9 for the rectangle shadows and the final entry for the convex shape shadows + ImFontAtlasShadowTexConfig ShadowTexConfig; // Shadow texture baking config + // [Obsolete] #ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS // Legacy: You can request your rectangles to be mapped as font glyph (given a font + Unicode point), so you can render e.g. custom colorful icons and use them as regular glyphs. --> Prefer using a custom ImFontLoader. diff --git a/imgui_demo.cpp b/imgui_demo.cpp index a3996d961..b2a38af4f 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -8626,6 +8626,22 @@ void ImGui::ShowStyleEditor(ImGuiStyle* ref) EndTabItem(); } + if (BeginTabItem("Shadows")) + { + Text("Window shadows:"); + ColorEdit4("Color", (float*)&style.Colors[ImGuiCol_WindowShadow], ImGuiColorEditFlags_AlphaBar); + SameLine(); + HelpMarker("Same as 'WindowShadow' in Colors tab."); + + SliderFloat("Size", &style.WindowShadowSize, 0.0f, 128.0f, "%.1f"); + SameLine(); + HelpMarker("Set shadow size to zero to disable shadows."); + SliderFloat("Offset distance", &style.WindowShadowOffsetDist, 0.0f, 64.0f, "%.0f"); + SliderAngle("Offset angle", &style.WindowShadowOffsetAngle); + + EndTabItem(); + } + EndTabBar(); } PopItemWidth(); @@ -10096,6 +10112,168 @@ static void ShowExampleAppCustomRendering(bool* p_open) ImGui::EndTabItem(); } + if (ImGui::BeginTabItem("Shadows")) + { + static float shadow_thickness = 40.0f; + static ImVec4 shadow_color = ImVec4(0.0f, 0.0f, 0.0f, 1.0f); + static bool shadow_filled = false; + static ImVec4 shape_color = ImVec4(0.9f, 0.6f, 0.3f, 1.0f); + static float shape_rounding = 0.0f; + static ImVec2 shadow_offset(0.0f, 0.0f); + static ImVec4 background_color = ImVec4(0.5f, 0.5f, 0.7f, 1.0f); + static bool wireframe = false; + static bool aa = true; + static int poly_shape_index = 0; + ImGui::Checkbox("Shadow filled", &shadow_filled); + ImGui::SameLine(); + HelpMarker("This will fill the section behind the shape to shadow. It's often unnecessary and wasteful but provided for consistency."); + ImGui::Checkbox("Wireframe shapes", &wireframe); + ImGui::SameLine(); + HelpMarker("This draws the shapes in wireframe so you can see the shadow underneath."); + ImGui::Checkbox("Anti-aliasing", &aa); + + ImGui::DragFloat("Shadow Thickness", &shadow_thickness, 1.0f, 0.0f, 100.0f, "%.02f"); + ImGui::SliderFloat2("Offset", (float*)&shadow_offset, -32.0f, 32.0f); + ImGui::SameLine(); + HelpMarker("Note that currently circles/convex shapes do not support non-zero offsets for unfilled shadows."); + ImGui::ColorEdit4("Background Color", &background_color.x); + ImGui::ColorEdit4("Shadow Color", &shadow_color.x); + ImGui::ColorEdit4("Shape Color", &shape_color.x); + ImGui::DragFloat("Shape Rounding", &shape_rounding, 1.0f, 0.0f, 20.0f, "%.02f"); + ImGui::Combo("Convex shape", &poly_shape_index, "Shape 1\0Shape 2\0Shape 3\0Shape 4\0Shape 4 (winding reversed)"); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImDrawListFlags old_flags = draw_list->Flags; + + if (aa) + draw_list->Flags |= ~ImDrawListFlags_AntiAliasedFill; + else + draw_list->Flags &= ~ImDrawListFlags_AntiAliasedFill; + + // Fill a strip of background + { + ImVec2 p = ImGui::GetCursorScreenPos(); + draw_list->AddRectFilled(p, ImVec2(p.x + ImGui::GetContentRegionAvail().x, p.y + 200.0f), ImGui::GetColorU32(background_color)); + } + + // Rectangle + { + ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::Dummy(ImVec2(200.0f, 200.0f)); + + ImVec2 r1(p.x + 50.0f, p.y + 50.0f); + ImVec2 r2(p.x + 150.0f, p.y + 150.0f); + ImDrawFlags draw_flags = shadow_filled ? ImDrawFlags_None : ImDrawFlags_ShadowCutOutShapeBackground; + draw_list->AddShadowRect(r1, r2, ImGui::GetColorU32(shadow_color), shadow_thickness, shadow_offset, draw_flags, shape_rounding); + + if (wireframe) + draw_list->AddRect(r1, r2, ImGui::GetColorU32(shape_color), shape_rounding); + else + draw_list->AddRectFilled(r1, r2, ImGui::GetColorU32(shape_color), shape_rounding); + } + + ImGui::SameLine(); + + // Circle + { + ImVec2 p = ImGui::GetCursorScreenPos(); + ImGui::Dummy(ImVec2(200.0f, 200.0f)); + + // FIXME-SHADOWS: Offset forced to zero when shadow is not filled because it isn't supported + float off = 10.0f; + ImVec2 r1(p.x + 50.0f + off, p.y + 50.0f + off); + ImVec2 r2(p.x + 150.0f - off, p.y + 150.0f - off); + ImVec2 center(p.x + 100.0f, p.y + 100.0f); + ImDrawFlags draw_flags = shadow_filled ? ImDrawFlags_None : ImDrawFlags_ShadowCutOutShapeBackground; + draw_list->AddShadowCircle(center, 50.0f, ImGui::GetColorU32(shadow_color), shadow_thickness, shadow_filled ? shadow_offset : ImVec2(0.0f, 0.0f), draw_flags, 0); + + if (wireframe) + draw_list->AddCircle(center, 50.0f, ImGui::GetColorU32(shape_color), 0); + else + draw_list->AddCircleFilled(center, 50.0f, ImGui::GetColorU32(shape_color), 0); + } + + ImGui::SameLine(); + + // Convex shape + { + ImVec2 pos = ImGui::GetCursorScreenPos(); + ImGui::Dummy(ImVec2(200.0f, 200.0f)); + + const ImVec2 poly_centre(pos.x + 50.0f, pos.y + 100.0f); + ImVec2 poly_points[8]; + int poly_points_count = 0; + + switch (poly_shape_index) + { + default: + case 0: + { + poly_points[0] = ImVec2(poly_centre.x - 32.0f, poly_centre.y); + poly_points[1] = ImVec2(poly_centre.x - 24.0f, poly_centre.y + 24.0f); + poly_points[2] = ImVec2(poly_centre.x, poly_centre.y + 32.0f); + poly_points[3] = ImVec2(poly_centre.x + 24.0f, poly_centre.y + 24.0f); + poly_points[4] = ImVec2(poly_centre.x + 32.0f, poly_centre.y); + poly_points[5] = ImVec2(poly_centre.x + 24.0f, poly_centre.y - 24.0f); + poly_points[6] = ImVec2(poly_centre.x, poly_centre.y - 32.0f); + poly_points[7] = ImVec2(poly_centre.x - 32.0f, poly_centre.y - 32.0f); + poly_points_count = 8; + break; + } + case 1: + { + poly_points[0] = ImVec2(poly_centre.x + 40.0f, poly_centre.y - 20.0f); + poly_points[1] = ImVec2(poly_centre.x, poly_centre.y + 32.0f); + poly_points[2] = ImVec2(poly_centre.x - 24.0f, poly_centre.y - 32.0f); + poly_points_count = 3; + break; + } + case 2: + { + poly_points[0] = ImVec2(poly_centre.x - 32.0f, poly_centre.y); + poly_points[1] = ImVec2(poly_centre.x, poly_centre.y + 32.0f); + poly_points[2] = ImVec2(poly_centre.x + 32.0f, poly_centre.y); + poly_points[3] = ImVec2(poly_centre.x, poly_centre.y - 32.0f); + poly_points_count = 4; + break; + } + case 3: + { + poly_points[0] = ImVec2(poly_centre.x - 4.0f, poly_centre.y - 20.0f); + poly_points[1] = ImVec2(poly_centre.x + 12.0f, poly_centre.y + 2.0f); + poly_points[2] = ImVec2(poly_centre.x + 8.0f, poly_centre.y + 16.0f); + poly_points[3] = ImVec2(poly_centre.x, poly_centre.y + 32.0f); + poly_points[4] = ImVec2(poly_centre.x - 16.0f, poly_centre.y - 32.0f); + poly_points_count = 5; + break; + } + case 4: // Same as test case 3 but with reversed winding + { + poly_points[0] = ImVec2(poly_centre.x - 16.0f, poly_centre.y - 32.0f); + poly_points[1] = ImVec2(poly_centre.x, poly_centre.y + 32.0f); + poly_points[2] = ImVec2(poly_centre.x + 8.0f, poly_centre.y + 16.0f); + poly_points[3] = ImVec2(poly_centre.x + 12.0f, poly_centre.y + 2.0f); + poly_points[4] = ImVec2(poly_centre.x - 4.0f, poly_centre.y - 20.0f); + poly_points_count = 5; + break; + } + } + + // FIXME-SHADOWS: Offset forced to zero when shadow is not filled because it isn't supported + ImDrawFlags draw_flags = shadow_filled ? ImDrawFlags_None : ImDrawFlags_ShadowCutOutShapeBackground; + draw_list->AddShadowConvexPoly(poly_points, poly_points_count, ImGui::GetColorU32(shadow_color), shadow_thickness, shadow_filled ? shadow_offset : ImVec2(0.0f, 0.0f), draw_flags); + + if (wireframe) + draw_list->AddPolyline(poly_points, poly_points_count, ImGui::GetColorU32(shape_color), true, 1.0f); + else + draw_list->AddConvexPolyFilled(poly_points, poly_points_count, ImGui::GetColorU32(shape_color)); + } + + draw_list->Flags = old_flags; + + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("BG/FG draw lists")) { static bool draw_bg = true; diff --git a/imgui_draw.cpp b/imgui_draw.cpp index de6462295..c2f7bf497 100644 --- a/imgui_draw.cpp +++ b/imgui_draw.cpp @@ -9,9 +9,11 @@ Index of this file: // [SECTION] Style functions // [SECTION] ImDrawList // [SECTION] ImTriangulator, ImDrawList concave polygon fill +// [SECTION] ImDrawList Shadow Primitives // [SECTION] ImDrawListSplitter // [SECTION] ImDrawData // [SECTION] Helpers ShadeVertsXXX functions +// [SECTION] ImFontAtlasShadowTexConfig // [SECTION] ImFontConfig // [SECTION] ImFontAtlas, ImFontAtlasBuilder // [SECTION] ImFontAtlas: backend for stb_truetype @@ -246,6 +248,7 @@ void ImGui::StyleColorsDark(ImGuiStyle* dst) colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.35f); + colors[ImGuiCol_WindowShadow] = ImVec4(0.08f, 0.08f, 0.08f, 0.35f); } void ImGui::StyleColorsClassic(ImGuiStyle* dst) @@ -313,6 +316,7 @@ void ImGui::StyleColorsClassic(ImGuiStyle* dst) colors[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00f, 1.00f, 1.00f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80f, 0.80f, 0.80f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); + colors[ImGuiCol_WindowShadow] = ImVec4(0.08f, 0.08f, 0.08f, 0.35f); } // Those light colors are better suited with a thicker font than the default one + FrameBorder @@ -381,6 +385,31 @@ void ImGui::StyleColorsLight(ImGuiStyle* dst) colors[ImGuiCol_NavWindowingHighlight] = ImVec4(0.70f, 0.70f, 0.70f, 0.70f); colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.20f); colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.20f, 0.20f, 0.20f, 0.35f); + colors[ImGuiCol_WindowShadow] = ImVec4(0.08f, 0.08f, 0.08f, 0.35f); +} + +//----------------------------------------------------------------------------- +// [SECTION] ImFontAtlasShadowTexConfig +//----------------------------------------------------------------------------- + +void ImFontAtlasShadowTexConfig::SetupDefaults() +{ + TexCornerSize = 16; + TexEdgeSize = 1; + TexFalloffPower = 4.8f; + TexDistanceFieldOffset = 3.8f; + TexBlur = true; +} + +int ImFontAtlasShadowTexConfig::CalcConvexTexWidth() const +{ + // We have to pad the texture enough that we don't go off the edges when we expand the corner triangles + return (int)((TexCornerSize / ImCos(IM_PI * 0.25f)) + (GetConvexTexPadding() * 2)); +} + +int ImFontAtlasShadowTexConfig::CalcConvexTexHeight() const +{ + return CalcConvexTexWidth(); // Same value } //----------------------------------------------------------------------------- @@ -2102,6 +2131,724 @@ void ImDrawList::AddConcavePolyFilled(const ImVec2* points, const int points_cou } } +//----------------------------------------------------------------------------- +// [SECTION] ImDrawList Shadow Primitives +//----------------------------------------------------------------------------- +// - AddSubtractedRect() [Internal] +// - ClipPolygonShape() [Internal] +// - AddSubtractedRect() [Internal] +// - AddRectShadow() +//----------------------------------------------------------------------------- + +// Adds a rectangle (A) with another rectangle (B) subtracted from it (i.e. the portion of A covered by B is not drawn). Does not handle rounded corners (use the version that takes a convex polygon for that). +static void AddSubtractedRect(ImDrawList* draw_list, const ImVec2& a_min, const ImVec2& a_max, const ImVec2& a_min_uv, const ImVec2& a_max_uv, ImVec2 b_min, ImVec2 b_max, ImU32 col) +{ + // Early out without drawing anything if A is zero-size + if (a_min.x >= a_max.x || a_min.y >= a_max.y) + return; + + // Early out without drawing anything if B covers A entirely + if (a_min.x >= b_min.x && a_max.x <= b_max.x && a_min.y >= b_min.y && a_max.y <= b_max.y) + return; + + // First clip the extents of B to A + b_min = ImMax(b_min, a_min); + b_max = ImMin(b_max, a_max); + if (b_min.x >= b_max.x || b_min.y >= b_max.y) + { + // B is entirely outside A, so just draw A as-is + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(a_min, a_max, a_min_uv, a_max_uv, col); + return; + } + + // Otherwise we need to emit (up to) four quads to cover the visible area... + // Our layout looks like this (numbers are vertex indices, letters are quads): + // + // 0---8------9-----1 + // | | B | | + // + 4------5 + + // | A |xxxxxx| C | + // | |xxxxxx| | + // + 7------6 + + // | | D | | + // 3---11-----10----2 + + const int max_verts = 12; + const int max_indices = 6 * 4; // At most four quads + draw_list->PrimReserve(max_indices, max_verts); + + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx idx = (ImDrawIdx)draw_list->_VtxCurrentIdx; + + // Write vertices + vtx_write[0].pos = ImVec2(a_min.x, a_min.y); vtx_write[0].uv = ImVec2(a_min_uv.x, a_min_uv.y); vtx_write[0].col = col; + vtx_write[1].pos = ImVec2(a_max.x, a_min.y); vtx_write[1].uv = ImVec2(a_max_uv.x, a_min_uv.y); vtx_write[1].col = col; + vtx_write[2].pos = ImVec2(a_max.x, a_max.y); vtx_write[2].uv = ImVec2(a_max_uv.x, a_max_uv.y); vtx_write[2].col = col; + vtx_write[3].pos = ImVec2(a_min.x, a_max.y); vtx_write[3].uv = ImVec2(a_min_uv.x, a_max_uv.y); vtx_write[3].col = col; + + const ImVec2 pos_to_uv_scale = (a_max_uv - a_min_uv) / (a_max - a_min); // Guaranteed never to be a /0 because we check for zero-size A above + const ImVec2 pos_to_uv_offset = (a_min_uv / pos_to_uv_scale) - a_min; + + // Helper that generates an interpolated UV based on position +#define LERP_UV(x_pos, y_pos) (ImVec2(((x_pos) + pos_to_uv_offset.x) * pos_to_uv_scale.x, ((y_pos) + pos_to_uv_offset.y) * pos_to_uv_scale.y)) + vtx_write[4].pos = ImVec2(b_min.x, b_min.y); vtx_write[4].uv = LERP_UV(b_min.x, b_min.y); vtx_write[4].col = col; + vtx_write[5].pos = ImVec2(b_max.x, b_min.y); vtx_write[5].uv = LERP_UV(b_max.x, b_min.y); vtx_write[5].col = col; + vtx_write[6].pos = ImVec2(b_max.x, b_max.y); vtx_write[6].uv = LERP_UV(b_max.x, b_max.y); vtx_write[6].col = col; + vtx_write[7].pos = ImVec2(b_min.x, b_max.y); vtx_write[7].uv = LERP_UV(b_min.x, b_max.y); vtx_write[7].col = col; + vtx_write[8].pos = ImVec2(b_min.x, a_min.y); vtx_write[8].uv = LERP_UV(b_min.x, a_min.y); vtx_write[8].col = col; + vtx_write[9].pos = ImVec2(b_max.x, a_min.y); vtx_write[9].uv = LERP_UV(b_max.x, a_min.y); vtx_write[9].col = col; + vtx_write[10].pos = ImVec2(b_max.x, a_max.y); vtx_write[10].uv = LERP_UV(b_max.x, a_max.y); vtx_write[10].col = col; + vtx_write[11].pos = ImVec2(b_min.x, a_max.y); vtx_write[11].uv = LERP_UV(b_min.x, a_max.y); vtx_write[11].col = col; +#undef LERP_UV + draw_list->_VtxWritePtr += 12; + draw_list->_VtxCurrentIdx += 12; + + // Write indices for each quad (if it is visible) + if (b_min.x > a_min.x) // A + { + idx_write[0] = (ImDrawIdx)(idx + 0); idx_write[1] = (ImDrawIdx)(idx + 8); idx_write[2] = (ImDrawIdx)(idx + 11); + idx_write[3] = (ImDrawIdx)(idx + 0); idx_write[4] = (ImDrawIdx)(idx + 11); idx_write[5] = (ImDrawIdx)(idx + 3); + idx_write += 6; + } + if (b_min.y > a_min.y) // B + { + idx_write[0] = (ImDrawIdx)(idx + 8); idx_write[1] = (ImDrawIdx)(idx + 9); idx_write[2] = (ImDrawIdx)(idx + 5); + idx_write[3] = (ImDrawIdx)(idx + 8); idx_write[4] = (ImDrawIdx)(idx + 5); idx_write[5] = (ImDrawIdx)(idx + 4); + idx_write += 6; + } + if (a_max.x > b_max.x) // C + { + idx_write[0] = (ImDrawIdx)(idx + 9); idx_write[1] = (ImDrawIdx)(idx + 1); idx_write[2] = (ImDrawIdx)(idx + 2); + idx_write[3] = (ImDrawIdx)(idx + 9); idx_write[4] = (ImDrawIdx)(idx + 2); idx_write[5] = (ImDrawIdx)(idx + 10); + idx_write += 6; + } + if (a_max.y > b_max.y) // D + { + idx_write[0] = (ImDrawIdx)(idx + 7); idx_write[1] = (ImDrawIdx)(idx + 6); idx_write[2] = (ImDrawIdx)(idx + 10); + idx_write[3] = (ImDrawIdx)(idx + 7); idx_write[4] = (ImDrawIdx)(idx + 10); idx_write[5] = (ImDrawIdx)(idx + 11); + idx_write += 6; + } + + const int used_indices = (int)(idx_write - draw_list->_IdxWritePtr); + draw_list->_IdxWritePtr = idx_write; + draw_list->PrimUnreserve(max_indices - used_indices, 0); +} + +// Clip a polygonal shape to a rectangle, writing the results into dest_points. The number of points emitted is returned (may be zero if the polygon was entirely outside the rectangle, or the source polygon was not valid). dest_points may still be written to even if zero was returned. +// allocated_dest_points should contain the number of allocated points in dest_points - in general this should be the number of source points + 4 to accommodate the worst case. If this is exceeded data will be truncated and -1 returned. Stack space work area is allocated based on this value so it shouldn't be too large. +static int ClipPolygonShape(ImVec2* src_points, int num_src_points, ImVec2* dest_points, int allocated_dest_points, ImVec2 clip_rect_min, ImVec2 clip_rect_max) +{ + // Early-out with an empty result if clipping region is zero-sized + if (clip_rect_max.x <= clip_rect_min.x || clip_rect_max.y <= clip_rect_min.y) + return 0; + + // Early-out if there is no source geometry + if (num_src_points < 3) + return 0; + + // The four clip planes here are indexed as: + // 0 = X-, 1 = X+, 2 = Y-, 3 = Y+ + ImU8* outflags[2]; // Double-buffered flags for each vertex indicating which of the four clip planes it is outside of + outflags[0] = (ImU8*)alloca(2 * allocated_dest_points * sizeof(ImU8)); + outflags[1] = outflags[0] + allocated_dest_points; + + // Calculate initial outflags + ImU8 outflags_anded = 0xFF; + ImU8 outflags_ored = 0; + for (int point_idx = 0; point_idx < num_src_points; point_idx++) + { + const ImVec2 pos = src_points[point_idx]; + const ImU8 point_outflags = (pos.x < clip_rect_min.x ? 1 : 0) | (pos.x > clip_rect_max.x ? 2 : 0) | (pos.y < clip_rect_min.y ? 4 : 0) | (pos.y > clip_rect_max.y ? 8 : 0); + outflags[0][point_idx] = point_outflags; // Writing to buffer 0 + outflags_anded &= point_outflags; + outflags_ored |= point_outflags; + } + if (outflags_anded != 0) // Entirely clipped by any one plane, so nothing remains + return 0; + + if (outflags_ored == 0) // Entirely within bounds, so trivial accept + { + if (allocated_dest_points < num_src_points) + return -1; // Not sure what the caller was thinking if this happens, but we should handle it gracefully + + memcpy(dest_points, src_points, num_src_points * sizeof(ImVec2)); + return num_src_points; + } + + // Shape needs clipping + ImVec2* clip_buf[2]; // Double-buffered work area + clip_buf[0] = (ImVec2*)alloca(2 * allocated_dest_points * sizeof(ImVec2)); //-V630 + clip_buf[1] = clip_buf[0] + allocated_dest_points; + + memcpy(clip_buf[0], src_points, num_src_points * sizeof(ImVec2)); + int clip_buf_size = num_src_points; // Number of vertices currently in the clip buffer + + int read_buffer_idx = 0; // The index of the clip buffer/out-flags we are reading (0 or 1) + + for (int clip_plane = 0; clip_plane < 4; clip_plane++) // 0 = X-, 1 = X+, 2 = Y-, 3 = Y+ + { + const int clip_plane_bit = 1 << clip_plane; // Bit mask for our current plane in out-flags + if ((outflags_ored & clip_plane_bit) == 0) + continue; // All vertices are inside this plane, so no need to clip + + ImVec2* read_vert = &clip_buf[read_buffer_idx][0]; // Clip buffer vertex we are currently reading + ImVec2* write_vert = &clip_buf[1 - read_buffer_idx][0]; // Clip buffer vertex we are currently writing + ImVec2* write_vert_end = write_vert + allocated_dest_points; // End of the write buffer + ImU8* read_outflags = &outflags[read_buffer_idx][0]; // Out-flag we are currently reading + ImU8* write_outflags = &outflags[1 - read_buffer_idx][0]; // Out-flag we are currently writing + + // Keep track of the last vertex visited, initially the last in the list + ImVec2* last_vert = &read_vert[clip_buf_size - 1]; + ImU8 last_outflags = read_outflags[clip_buf_size - 1]; + + for (int vert = 0; vert < clip_buf_size; vert++) + { + ImU8 current_outflags = *(read_outflags++); + bool out = (current_outflags & clip_plane_bit) != 0; + if (((current_outflags ^ last_outflags) & clip_plane_bit) == 0) // We haven't crossed the clip plane + { + if (!out) + { + // Emit vertex as-is + if (write_vert >= write_vert_end) + return -1; // Ran out of buffer space, so abort + *(write_vert++) = *read_vert; + *(write_outflags++) = current_outflags; + } + } + else + { + // Emit a vertex at the intersection point + float t = 0.0f; + ImVec2 pos0 = *last_vert; + ImVec2 pos1 = *read_vert; + ImVec2 intersect_pos; + switch (clip_plane) + { + case 0: t = (clip_rect_min.x - pos0.x) / (pos1.x - pos0.x); intersect_pos = ImVec2(clip_rect_min.x, pos0.y + ((pos1.y - pos0.y) * t)); break; // X- + case 1: t = (clip_rect_max.x - pos0.x) / (pos1.x - pos0.x); intersect_pos = ImVec2(clip_rect_max.x, pos0.y + ((pos1.y - pos0.y) * t)); break; // X+ + case 2: t = (clip_rect_min.y - pos0.y) / (pos1.y - pos0.y); intersect_pos = ImVec2(pos0.x + ((pos1.x - pos0.x) * t), clip_rect_min.y); break; // Y- + case 3: t = (clip_rect_max.y - pos0.y) / (pos1.y - pos0.y); intersect_pos = ImVec2(pos0.x + ((pos1.x - pos0.x) * t), clip_rect_max.y); break; // Y+ + } + + if (write_vert >= write_vert_end) + return -1; // Ran out of buffer space, so abort + + // Write new out-flags for the vertex we just emitted + *(write_vert++) = intersect_pos; + *(write_outflags++) = (intersect_pos.x < clip_rect_min.x ? 1 : 0) | (intersect_pos.x > clip_rect_max.x ? 2 : 0) | (intersect_pos.y < clip_rect_min.y ? 4 : 0) | (intersect_pos.y > clip_rect_max.y ? 8 : 0); + + if (!out) + { + // When coming back in, also emit the actual vertex + if (write_vert >= write_vert_end) + return -1; // Ran out of buffer space, so abort + *(write_vert++) = *read_vert; + *(write_outflags++) = current_outflags; + } + + last_outflags = current_outflags; + } + + last_vert = read_vert; + read_vert++; // Advance to next vertex + } + + clip_buf_size = (int)(write_vert - &clip_buf[1 - read_buffer_idx][0]); // Update buffer size + read_buffer_idx = 1 - read_buffer_idx; // Swap buffers + } + + if (clip_buf_size < 3) + return 0; // Nothing to return + + // Copy results to output buffer, removing any redundant vertices + int num_out_verts = 0; + ImVec2 last_vert = clip_buf[read_buffer_idx][clip_buf_size - 1]; + for (int i = 0; i < clip_buf_size; i++) + { + ImVec2 vert = clip_buf[read_buffer_idx][i]; + if (ImLengthSqr(vert - last_vert) <= 0.00001f) + continue; + dest_points[num_out_verts++] = vert; + last_vert = vert; + } + + // Return size (IF this is still a valid shape) + return (num_out_verts > 2) ? num_out_verts : 0; +} + +// Adds a rectangle (A) with a convex shape (B) subtracted from it (i.e. the portion of A covered by B is not drawn). +static void AddSubtractedRect(ImDrawList* draw_list, const ImVec2& a_min, const ImVec2& a_max, const ImVec2& a_min_uv, const ImVec2& a_max_uv, ImVec2* b_points, int num_b_points, ImU32 col) +{ + // Early out without drawing anything if A is zero-size + if (a_min.x >= a_max.x || a_min.y >= a_max.y) + return; + + // First clip B to A + const int max_clipped_points = num_b_points + 4; + ImVec2* clipped_b_points = (ImVec2*)alloca(max_clipped_points * sizeof(ImVec2)); //-V630 + const int num_clipped_points = ClipPolygonShape(b_points, num_b_points, clipped_b_points, max_clipped_points, a_min, a_max); + IM_ASSERT(num_clipped_points >= 0); // -1 would indicate max_clipped_points was too small, which shouldn't happen + + b_points = clipped_b_points; + num_b_points = num_clipped_points; + + if (num_clipped_points == 0) + { + // B is entirely outside A, so just draw A as-is + draw_list->PrimReserve(6, 4); + draw_list->PrimRectUV(a_min, a_max, a_min_uv, a_max_uv, col); + } + else + { + // We need to generate clipped geometry + // To do this we walk the inner polygon and connect each edge to one of the four corners of our rectangle based on the quadrant their normal points at + const int max_verts = num_b_points + 4; // Inner points plus the four corners + const int max_indices = (num_b_points * 3) + (4 * 3); // Worst case is one triangle per inner edge and then four filler triangles + draw_list->PrimReserve(max_indices, max_verts); + + ImDrawIdx* idx_write = draw_list->_IdxWritePtr; + ImDrawVert* vtx_write = draw_list->_VtxWritePtr; + ImDrawIdx inner_idx = (ImDrawIdx)draw_list->_VtxCurrentIdx; // Starting index for inner vertices + + // Write inner vertices + const ImVec2 pos_to_uv_scale = (a_max_uv - a_min_uv) / (a_max - a_min); // Guaranteed never to be a /0 because we check for zero-size A above + const ImVec2 pos_to_uv_offset = (a_min_uv / pos_to_uv_scale) - a_min; + + // Helper that generates an interpolated UV based on position +#define LERP_UV(x_pos, y_pos) (ImVec2(((x_pos) + pos_to_uv_offset.x) * pos_to_uv_scale.x, ((y_pos) + pos_to_uv_offset.y) * pos_to_uv_scale.y)) + for (int i = 0; i < num_b_points; i++) + { + vtx_write[i].pos = b_points[i]; + vtx_write[i].uv = LERP_UV(b_points[i].x, b_points[i].y); + vtx_write[i].col = col; + } +#undef LERP_UV + + vtx_write += num_b_points; + + // Write outer vertices + ImDrawIdx outer_idx = (ImDrawIdx)(inner_idx + num_b_points); // Starting index for outer vertices + + ImVec2 outer_verts[4]; + outer_verts[0] = ImVec2(a_min.x, a_min.y); // X- Y- (quadrant 0, top left) + outer_verts[1] = ImVec2(a_max.x, a_min.y); // X+ Y- (quadrant 1, top right) + outer_verts[2] = ImVec2(a_max.x, a_max.y); // X+ Y+ (quadrant 2, bottom right) + outer_verts[3] = ImVec2(a_min.x, a_max.y); // X- Y+ (quadrant 3, bottom left) + + vtx_write[0].pos = outer_verts[0]; vtx_write[0].uv = ImVec2(a_min_uv.x, a_min_uv.y); vtx_write[0].col = col; + vtx_write[1].pos = outer_verts[1]; vtx_write[1].uv = ImVec2(a_max_uv.x, a_min_uv.y); vtx_write[1].col = col; + vtx_write[2].pos = outer_verts[2]; vtx_write[2].uv = ImVec2(a_max_uv.x, a_max_uv.y); vtx_write[2].col = col; + vtx_write[3].pos = outer_verts[3]; vtx_write[3].uv = ImVec2(a_min_uv.x, a_max_uv.y); vtx_write[3].col = col; + + draw_list->_VtxCurrentIdx += num_b_points + 4; + draw_list->_VtxWritePtr += num_b_points + 4; + + // Now walk the inner vertices in order + ImVec2 last_inner_vert = b_points[num_b_points - 1]; + int last_inner_vert_idx = num_b_points - 1; + int last_outer_vert_idx = -1; + int first_outer_vert_idx = -1; + + // Triangle-area based check for degenerate triangles + // Min area (0.1f) is doubled (* 2.0f) because we're calculating (area * 2) here +#define IS_DEGENERATE(a, b, c) (ImFabs((((a).x * ((b).y - (c).y)) + ((b).x * ((c).y - (a).y)) + ((c).x * ((a).y - (b).y)))) < (0.1f * 2.0f)) + + // Check the winding order of the inner vertices using the sign of the triangle area, and set the outer vertex winding to match + int outer_vertex_winding = (((b_points[0].x * (b_points[1].y - b_points[2].y)) + (b_points[1].x * (b_points[2].y - b_points[0].y)) + (b_points[2].x * (b_points[0].y - b_points[1].y))) < 0.0f) ? -1 : 1; + for (int inner_vert_idx = 0; inner_vert_idx < num_b_points; inner_vert_idx++) + { + ImVec2 current_inner_vert = b_points[inner_vert_idx]; + + // Calculate normal (not actually normalized, as for our purposes here it doesn't need to be) + ImVec2 normal(current_inner_vert.y - last_inner_vert.y, -(current_inner_vert.x - last_inner_vert.x)); + + // Calculate the outer vertex index based on the quadrant the normal points at (0=top left, 1=top right, 2=bottom right, 3=bottom left) + int outer_vert_idx = (ImFabs(normal.x) > ImFabs(normal.y)) ? ((normal.x >= 0.0f) ? ((normal.y > 0.0f) ? 2 : 1) : ((normal.y > 0.0f) ? 3 : 0)) : ((normal.y >= 0.0f) ? ((normal.x > 0.0f) ? 2 : 3) : ((normal.x > 0.0f) ? 1 : 0)); + ImVec2 outer_vert = outer_verts[outer_vert_idx]; + + // Write the main triangle (connecting the inner edge to the corner) + if (!IS_DEGENERATE(last_inner_vert, current_inner_vert, outer_vert)) + { + idx_write[0] = (ImDrawIdx)(inner_idx + last_inner_vert_idx); + idx_write[1] = (ImDrawIdx)(inner_idx + inner_vert_idx); + idx_write[2] = (ImDrawIdx)(outer_idx + outer_vert_idx); + idx_write += 3; + } + + // We don't initially know which outer vertex we are going to start from, so set that here when processing the first inner vertex + if (first_outer_vert_idx == -1) + { + first_outer_vert_idx = outer_vert_idx; + last_outer_vert_idx = outer_vert_idx; + } + + // Now walk the outer edge and write any filler triangles needed (connecting outer edges to the inner vertex) + while (outer_vert_idx != last_outer_vert_idx) + { + int next_outer_vert_idx = (last_outer_vert_idx + outer_vertex_winding) & 3; + if (!IS_DEGENERATE(outer_verts[last_outer_vert_idx], outer_verts[next_outer_vert_idx], last_inner_vert)) + { + idx_write[0] = (ImDrawIdx)(outer_idx + last_outer_vert_idx); + idx_write[1] = (ImDrawIdx)(outer_idx + next_outer_vert_idx); + idx_write[2] = (ImDrawIdx)(inner_idx + last_inner_vert_idx); + idx_write += 3; + } + last_outer_vert_idx = next_outer_vert_idx; + } + + last_inner_vert = current_inner_vert; + last_inner_vert_idx = inner_vert_idx; + } + + // Write remaining filler triangles for any un-traversed outer edges + if (first_outer_vert_idx != -1) + { + while (first_outer_vert_idx != last_outer_vert_idx) + { + int next_outer_vert_idx = (last_outer_vert_idx + outer_vertex_winding) & 3; + if (!IS_DEGENERATE(outer_verts[last_outer_vert_idx], outer_verts[next_outer_vert_idx], last_inner_vert)) + { + idx_write[0] = (ImDrawIdx)(outer_idx + last_outer_vert_idx); + idx_write[1] = (ImDrawIdx)(outer_idx + next_outer_vert_idx); + idx_write[2] = (ImDrawIdx)(inner_idx + last_inner_vert_idx); + idx_write += 3; + } + last_outer_vert_idx = next_outer_vert_idx; + } + } +#undef IS_DEGENERATE + + int used_indices = (int)(idx_write - draw_list->_IdxWritePtr); + draw_list->_IdxWritePtr = idx_write; + draw_list->PrimUnreserve(max_indices - used_indices, 0); + } +} + +void ImDrawList::AddShadowRect(const ImVec2& obj_min, const ImVec2& obj_max, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags, float obj_rounding) +{ + if ((shadow_col & IM_COL32_A_MASK) == 0) + return; + + ImVec2* inner_rect_points = NULL; // Points that make up the shape of the inner rectangle (used when it has rounded corners) + int inner_rect_points_count = 0; + + // Generate a path describing the inner rectangle and copy it to our buffer + const bool is_filled = (flags & ImDrawFlags_ShadowCutOutShapeBackground) == 0; + const bool is_rounded = (obj_rounding > 0.0f) && ((flags & ImDrawFlags_RoundCornersMask_) != ImDrawFlags_RoundCornersNone); // Do we have rounded corners? + if (is_rounded && !is_filled) + { + IM_ASSERT(_Path.Size == 0); + PathRect(obj_min, obj_max, obj_rounding, flags); + inner_rect_points_count = _Path.Size; + inner_rect_points = (ImVec2*)alloca(inner_rect_points_count * sizeof(ImVec2)); //-V630 + memcpy(inner_rect_points, _Path.Data, inner_rect_points_count * sizeof(ImVec2)); + _Path.Size = 0; + } + + if (is_filled) + PrimReserve(6 * 9, 4 * 9); // Reserve space for adding unclipped chunks + + // Draw the relevant chunks of the texture (the texture is split into a 3x3 grid) + // FIXME-OPT: Might make sense to optimize/unroll for the fast paths (filled or not rounded) + for (int x = 0; x < 3; x++) + { + for (int y = 0; y < 3; y++) + { + const int uv_index = x + (y + y + y); // y*3 formatted so as to ensure the compiler avoids an actual multiply + const ImVec4 uvs = _Data->ShadowRectUvs[uv_index]; + + ImVec2 draw_min, draw_max; + switch (x) + { + case 0: draw_min.x = obj_min.x - shadow_thickness; draw_max.x = obj_min.x; break; + case 1: draw_min.x = obj_min.x; draw_max.x = obj_max.x; break; + case 2: draw_min.x = obj_max.x; draw_max.x = obj_max.x + shadow_thickness; break; + } + switch (y) + { + case 0: draw_min.y = obj_min.y - shadow_thickness; draw_max.y = obj_min.y; break; + case 1: draw_min.y = obj_min.y; draw_max.y = obj_max.y; break; + case 2: draw_min.y = obj_max.y; draw_max.y = obj_max.y + shadow_thickness; break; + } + + ImVec2 uv_min(uvs.x, uvs.y); + ImVec2 uv_max(uvs.z, uvs.w); + if (is_filled) + PrimRectUV(draw_min + shadow_offset, draw_max + shadow_offset, uv_min, uv_max, shadow_col); // No clipping path (draw entire shadow) + else if (is_rounded) + AddSubtractedRect(this, draw_min + shadow_offset, draw_max + shadow_offset, uv_min, uv_max, inner_rect_points, inner_rect_points_count, shadow_col); // Complex path for rounded rectangles + else + AddSubtractedRect(this, draw_min + shadow_offset, draw_max + shadow_offset, uv_min, uv_max, obj_min, obj_max, shadow_col); // Simple fast path for non-rounded rectangles + } + } +} + +// Add a shadow for a convex shape described by points and num_points +void ImDrawList::AddShadowConvexPoly(const ImVec2* points, int points_count, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags) +{ + const bool is_filled = (flags & ImDrawFlags_ShadowCutOutShapeBackground) == 0; + IM_ASSERT((is_filled || (ImLengthSqr(shadow_offset) < 0.00001f)) && "Drawing circle/convex shape shadows with no center fill and an offset is not currently supported"); + IM_ASSERT(points_count >= 3); + + // Calculate poly vertex order + const int vertex_winding = (((points[0].x * (points[1].y - points[2].y)) + (points[1].x * (points[2].y - points[0].y)) + (points[2].x * (points[0].y - points[1].y))) < 0.0f) ? -1 : 1; + + // If we're using anti-aliasing, then inset the shadow by 0.5 pixels to avoid unpleasant fringing artifacts + const bool use_inset_distance = (Flags & ImDrawListFlags_AntiAliasedFill) && (!is_filled); + const float inset_distance = 0.5f; + + const ImVec4 uvs = _Data->ShadowRectUvs[9]; + + int tex_width = _Data->Font->OwnerAtlas->TexData->Width; + int tex_height = _Data->Font->OwnerAtlas->TexData->Height; + float inv_tex_width = 1.0f / (float)tex_width; + float inv_tex_height = 1.0f / (float)tex_height; + + ImVec2 solid_uv = ImVec2(uvs.z, uvs.w); // UV at the inside of an edge + ImVec2 edge_uv = ImVec2(uvs.x, uvs.w); // UV at the outside of an edge + + ImVec2 solid_to_edge_delta_texels = edge_uv - solid_uv; // Delta between the solid/edge points in texel-space (we need this in pixels - or, to be more precise, to be at a 1:1 aspect ratio - for the rotation to work) + solid_to_edge_delta_texels.x *= (float)tex_width; + solid_to_edge_delta_texels.y *= (float)tex_height; + + // Our basic algorithm here is that we generate a straight section along each edge, and then either one or two curved corner triangles at the corners, + // which use an appropriate chunk of the texture to generate a smooth curve. + const int num_edges = points_count; + + // Normalize a vector +#define NORMALIZE(vec) ((vec) / ImLength((vec), 0.001f)) + + const int required_stack_mem = (num_edges * sizeof(ImVec2)) + (num_edges * sizeof(float)); + ImU8* base_mem_for_normals_and_edges = (ImU8*)alloca(required_stack_mem); + ImU8* mem_for_normals_and_edges = (ImU8*)base_mem_for_normals_and_edges; + + // Calculate edge normals + ImVec2* edge_normals = (ImVec2*)(void*)mem_for_normals_and_edges; + mem_for_normals_and_edges += num_edges * sizeof(ImVec2); + + for (int edge_index = 0; edge_index < num_edges; edge_index++) + { + ImVec2 edge_start = points[edge_index]; // No need to apply offset here because the normal is unaffected + ImVec2 edge_end = points[(edge_index + 1) % num_edges]; + ImVec2 edge_normal = NORMALIZE(ImVec2(edge_end.y - edge_start.y, -(edge_end.x - edge_start.x))); + edge_normals[edge_index] = edge_normal * (float)vertex_winding; // Flip normals for reverse winding + } + + // Pre-calculate edge scales + // We need to do this because we need the edge strips to have widths that match up with the corner sections, otherwise pixel cracking can occur along the boundaries + float* edge_size_scales = (float*)(void*)mem_for_normals_and_edges; + mem_for_normals_and_edges += num_edges * sizeof(float); + IM_ASSERT_PARANOID(mem_for_normals_and_edges == (base_mem_for_normals_and_edges + required_stack_mem)); // Check we used exactly what we allocated + + { + ImVec2 prev_edge_normal = edge_normals[num_edges - 1]; + for (int edge_index = 0; edge_index < num_edges; edge_index++) + { + ImVec2 edge_normal = edge_normals[edge_index]; + float cos_angle_coverage = ImDot(edge_normal, prev_edge_normal); + + if (cos_angle_coverage < 0.999999f) + { + // If we are covering more than 90 degrees we need an intermediate vertex to stop the required expansion tending towards infinity. + // And thus the effective angle will be halved (matches the similar code in loop below) + float angle_coverage = ImAcos(cos_angle_coverage); + if (cos_angle_coverage <= 0.0f) // -V1051 + angle_coverage *= 0.5f; + edge_size_scales[edge_index] = 1.0f / ImCos(angle_coverage * 0.5f); // How much we need to expand our size by to avoid clipping the corner of the texture off + } + else + { + edge_size_scales[edge_index] = 1.0f; // No corner, thus default scale + } + + prev_edge_normal = edge_normal; + } + } + + const int max_vertices = (4 + (3 * 2) + (is_filled ? 1 : 0)) * num_edges; // 4 vertices per edge plus 3*2 for potentially two corner triangles, plus one per vertex for fill + const int max_indices = ((6 + (3 * 2)) * num_edges) + (is_filled ? ((num_edges - 2) * 3) : 0); // 2 tris per edge plus up to two corner triangles, plus fill triangles + PrimReserve(max_indices, max_vertices); + ImDrawIdx* idx_write = _IdxWritePtr; + ImDrawVert* vtx_write = _VtxWritePtr; + ImDrawIdx current_idx = (ImDrawIdx)_VtxCurrentIdx; + + //ImVec2 previous_edge_start = points[0] + offset; + ImVec2 prev_edge_normal = edge_normals[num_edges - 1]; + ImVec2 edge_start = points[0] + shadow_offset; + + if (use_inset_distance) + edge_start -= NORMALIZE(edge_normals[0] + prev_edge_normal) * inset_distance; + + for (int edge_index = 0; edge_index < num_edges; edge_index++) + { + ImVec2 edge_end = points[(edge_index + 1) % num_edges] + shadow_offset; + ImVec2 edge_normal = edge_normals[edge_index]; + const float size_scale_start = edge_size_scales[edge_index]; + const float size_scale_end = edge_size_scales[(edge_index + 1) % num_edges]; + + if (use_inset_distance) + edge_end -= NORMALIZE(edge_normals[(edge_index + 1) % num_edges] + edge_normal) * inset_distance; + + // Add corner section + float cos_angle_coverage = ImDot(edge_normal, prev_edge_normal); + if (cos_angle_coverage < 0.999999f) // Don't fill if the corner is actually straight + { + // If we are covering more than 90 degrees we need an intermediate vertex to stop the required expansion tending towards infinity. + // And thus the effective angle has been halved (matches the similar code in loop above) + int num_steps = (cos_angle_coverage <= 0.0f) ? 2 : 1; + + for (int step = 0; step < num_steps; step++) + { + if (num_steps > 1) + { + if (step == 0) + edge_normal = NORMALIZE(edge_normal + prev_edge_normal); // Use half-way normal for first step + else + edge_normal = edge_normals[edge_index]; // Then use the "real" next edge normal for the second + + cos_angle_coverage = ImDot(edge_normal, prev_edge_normal); // Recalculate angle + } + + // Calculate UV for the section of the curved texture + + const float angle_coverage = ImAcos(cos_angle_coverage); + const float sin_angle_coverage = ImSin(angle_coverage); + + ImVec2 edge_delta = solid_to_edge_delta_texels; + edge_delta *= size_scale_start; + + ImVec2 rotated_edge_delta = ImVec2((edge_delta.x * cos_angle_coverage) + (edge_delta.y * sin_angle_coverage), (edge_delta.x * sin_angle_coverage) + (edge_delta.y * cos_angle_coverage)); + + // Convert from texels back into UV space + edge_delta.x *= inv_tex_width; + edge_delta.y *= inv_tex_height; + rotated_edge_delta.x *= inv_tex_width; + rotated_edge_delta.y *= inv_tex_height; + + ImVec2 expanded_edge_uv = solid_uv + edge_delta; + ImVec2 other_edge_uv = solid_uv + rotated_edge_delta; // Rotated UV to encompass the necessary section of the curve + + float expanded_thickness = shadow_thickness * size_scale_start; + + // Add a triangle to fill the corner + ImVec2 outer_edge_start = edge_start + (prev_edge_normal * expanded_thickness); + ImVec2 outer_edge_end = edge_start + (edge_normal * expanded_thickness); + + vtx_write->pos = edge_start; vtx_write->col = shadow_col; vtx_write->uv = solid_uv; vtx_write++; + vtx_write->pos = outer_edge_end; vtx_write->col = shadow_col; vtx_write->uv = expanded_edge_uv; vtx_write++; + vtx_write->pos = outer_edge_start; vtx_write->col = shadow_col; vtx_write->uv = other_edge_uv; vtx_write++; + + *(idx_write++) = current_idx; + *(idx_write++) = current_idx + 1; + *(idx_write++) = current_idx + 2; + current_idx += 3; + + prev_edge_normal = edge_normal; + } + } + + // Add section along edge + const float edge_length = ImLength(edge_end - edge_start, 0.0f); + if (edge_length > 0.00001f) // Don't try and process degenerate edges + { + ImVec2 outer_edge_start = edge_start + (edge_normal * shadow_thickness * size_scale_start); + ImVec2 outer_edge_end = edge_end + (edge_normal * shadow_thickness * size_scale_end); + ImVec2 scaled_edge_uv_start = solid_uv + ((edge_uv - solid_uv) * size_scale_start); + ImVec2 scaled_edge_uv_end = solid_uv + ((edge_uv - solid_uv) * size_scale_end); + + // Write vertices, inner first, then outer + vtx_write->pos = edge_start; vtx_write->col = shadow_col; vtx_write->uv = solid_uv; vtx_write++; + vtx_write->pos = edge_end; vtx_write->col = shadow_col; vtx_write->uv = solid_uv; vtx_write++; + vtx_write->pos = outer_edge_end; vtx_write->col = shadow_col; vtx_write->uv = scaled_edge_uv_end; vtx_write++; + vtx_write->pos = outer_edge_start; vtx_write->col = shadow_col; vtx_write->uv = scaled_edge_uv_start; vtx_write++; + + *(idx_write++) = current_idx; + *(idx_write++) = current_idx + 1; + *(idx_write++) = current_idx + 2; + *(idx_write++) = current_idx; + *(idx_write++) = current_idx + 2; + *(idx_write++) = current_idx + 3; + current_idx += 4; + } + + edge_start = edge_end; + } + + // Fill if requested + if (is_filled) + { + // Add vertices + for (int edge_index = 0; edge_index < num_edges; edge_index++) + { + vtx_write->pos = points[edge_index] + shadow_offset; + vtx_write->col = shadow_col; + vtx_write->uv = solid_uv; + vtx_write++; + } + + // Add triangles + for (int edge_index = 2; edge_index < num_edges; edge_index++) + { + *(idx_write++) = current_idx; + *(idx_write++) = (ImDrawIdx)(current_idx + edge_index - 1); + *(idx_write++) = (ImDrawIdx)(current_idx + edge_index); + } + + current_idx += (ImDrawIdx)num_edges; + } + + // Release any unused vertices/indices + int used_indices = (int)(idx_write - _IdxWritePtr); + int used_vertices = (int)(vtx_write - _VtxWritePtr); + _IdxWritePtr = idx_write; + _VtxWritePtr = vtx_write; + _VtxCurrentIdx = current_idx; + PrimUnreserve(max_indices - used_indices, max_vertices - used_vertices); +#undef NORMALIZE +} + +// Draw a shadow for a circular object +// Uses the draw path and so wipes any existing data there +void ImDrawList::AddShadowCircle(const ImVec2& obj_center, float obj_radius, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags, int num_segments) +{ + // Obtain segment count + if (num_segments <= 0) + { + // Automatic segment count + const int radius_idx = (int)obj_radius - 1; + if (radius_idx < IM_ARRAYSIZE(_Data->CircleSegmentCounts)) + num_segments = _Data->CircleSegmentCounts[radius_idx]; // Use cached value + else + num_segments = IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_CALC(obj_radius, _Data->CircleSegmentMaxError); + } + else + { + // Explicit segment count (still clamp to avoid drawing insanely tessellated shapes) + num_segments = ImClamp(num_segments, 3, IM_DRAWLIST_CIRCLE_AUTO_SEGMENT_MAX); + } + + // Generate a path describing the inner circle and copy it to our buffer + IM_ASSERT(_Path.Size == 0); + const float a_max = (IM_PI * 2.0f) * ((float)num_segments - 1.0f) / (float)num_segments; + if (num_segments == 12) + PathArcToFast(obj_center, obj_radius, 0, 12 - 1); + else + PathArcTo(obj_center, obj_radius, 0.0f, a_max, num_segments - 1); + + // Draw the shadow using the convex shape code + AddShadowConvexPoly(_Path.Data, _Path.Size, shadow_col, shadow_thickness, shadow_offset, flags); + _Path.Size = 0; +} + +void ImDrawList::AddShadowNGon(const ImVec2& obj_center, float obj_radius, ImU32 shadow_col, float shadow_thickness, const ImVec2& shadow_offset, ImDrawFlags flags, int num_segments) +{ + IM_ASSERT(num_segments != 0); + AddShadowCircle(obj_center, obj_radius, shadow_col, shadow_thickness, shadow_offset, flags, num_segments); +} + //----------------------------------------------------------------------------- // [SECTION] ImDrawListSplitter //----------------------------------------------------------------------------- @@ -2644,6 +3391,10 @@ ImFontAtlas::ImFontAtlas() TexNextUniqueID = 1; FontNextUniqueID = 1; Builder = NULL; + + // FIXME-SHADOWS: move elsewhere? + ShadowRectIds[0] = ShadowRectIds[1] = -1; + ShadowTexConfig.SetupDefaults(); } ImFontAtlas::~ImFontAtlas() @@ -2691,6 +3442,9 @@ void ImFontAtlas::ClearInputData() font->Flags |= ImFontFlags_NoLoadGlyphs; } Sources.clear(); + + // FIXME-SHADOWS: Move elsewhere? + ShadowRectIds[0] = ShadowRectIds[1] = -1; } // Clear CPU-side copy of the texture data. @@ -3585,6 +4339,8 @@ static void ImFontAtlasBuildUpdateLinesTexData(ImFontAtlas* atlas) } } +static void ImFontAtlasBuildUpdateShadowTexData(ImFontAtlas* atlas); // FIXME-SHADOWS: Move impl here. + //----------------------------------------------------------------------------------------------------------------------------- // Was tempted to lazily init FontSrc but wouldn't save much + makes it more complicated to detect invalid data at AddFont() @@ -4080,6 +4836,7 @@ void ImFontAtlasTextureRepack(ImFontAtlas* atlas, int w, int h) // Update other cached UV ImFontAtlasBuildUpdateLinesTexData(atlas); ImFontAtlasBuildUpdateBasicTexData(atlas); + ImFontAtlasBuildUpdateShadowTexData(atlas); builder->LockDisableResize = false; ImFontAtlasUpdateDrawListsSharedData(atlas); @@ -4196,6 +4953,259 @@ void ImFontAtlasTextureCompact(ImFontAtlas* atlas) ImFontAtlasTextureRepack(atlas, new_tex_size.x, new_tex_size.y); } +// Calculates the signed distance from sample_pos to the nearest point on the rectangle defined by rect_min->rect_max +static float DistanceFromRectangle(const ImVec2& sample_pos, const ImVec2& rect_min, const ImVec2& rect_max) +{ + ImVec2 rect_centre = (rect_min + rect_max) * 0.5f; + ImVec2 rect_half_size = (rect_max - rect_min) * 0.5f; + ImVec2 local_sample_pos = sample_pos - rect_centre; + ImVec2 axis_dist = ImVec2(ImFabs(local_sample_pos.x), ImFabs(local_sample_pos.y)) - rect_half_size; + float out_dist = ImLength(ImVec2(ImMax(axis_dist.x, 0.0f), ImMax(axis_dist.y, 0.0f)), 0.00001f); + float in_dist = ImMin(ImMax(axis_dist.x, axis_dist.y), 0.0f); + return out_dist + in_dist; +} + +// Calculates the signed distance from sample_pos to the point given +static float DistanceFromPoint(const ImVec2& sample_pos, const ImVec2& point) +{ + return ImLength(sample_pos - point, 0.0f); +} + +// Perform a single Gaussian blur pass with a fixed kernel size and sigma +static void GaussianBlurPass(float* src, float* dest, int size, bool horizontal) +{ + // See http://dev.theomader.com/gaussian-kernel-calculator/ + const float coefficients[] = { 0.0f, 0.0f, 0.000003f, 0.000229f, 0.005977f, 0.060598f, 0.24173f, 0.382925f, 0.24173f, 0.060598f, 0.005977f, 0.000229f, 0.000003f, 0.0f, 0.0f }; + const int kernel_size = IM_ARRAYSIZE(coefficients); + const int sample_step = horizontal ? 1 : size; + + float* read_ptr = src; + float* write_ptr = dest; + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + { + float result = 0.0f; + int current_offset = (horizontal ? x : y) - ((kernel_size - 1) >> 1); + float* sample_ptr = read_ptr - (((kernel_size - 1) >> 1) * sample_step); + for (int j = 0; j < kernel_size; j++) + { + if (current_offset >= 0 && current_offset < size) + result += (*sample_ptr) * coefficients[j]; + current_offset++; + sample_ptr += sample_step; + } + read_ptr++; + *(write_ptr++) = result; + } +} + +// Perform an in-place Gaussian blur of a square array of floats with a fixed kernel size and sigma +// Uses a stack allocation for the temporary data so potentially dangerous with large size values +static void GaussianBlur(float* data, int size) +{ + // Do two passes, one from data into temp and then the second back to data again + float* temp = (float*)alloca(size * size * sizeof(float)); + GaussianBlurPass(data, temp, size, true); + GaussianBlurPass(temp, data, size, false); +} + +// Generate the actual pixel data for rounded corners in the atlas +static void ImFontAtlasBuildUpdateShadowTexData(ImFontAtlas* atlas) +{ + // The actual size we want to reserve, including padding + const ImFontAtlasShadowTexConfig* shadow_cfg = &atlas->ShadowTexConfig; + const unsigned int effective_size = shadow_cfg->CalcRectTexSize() + shadow_cfg->GetRectTexPadding(); + const int corner_size = shadow_cfg->TexCornerSize; + const int edge_size = shadow_cfg->TexEdgeSize; + + // Because of the blur, we have to generate the full 3x3 texture here, and then we chop that down to just the 2x2 section we need later. + // 'size' correspond to the our 3x3 size, whereas 'shadow_tex_size' correspond to our 2x2 version where duplicate mirrored corners are not stored. + + // The rectangular shadow texture + ImFontAtlasRect r; + bool add_and_draw = (atlas->GetCustomRect(atlas->ShadowRectIds[0], &r) == false); + if (add_and_draw) + { + atlas->ShadowRectIds[0] = atlas->AddCustomRect(effective_size, effective_size, &r); + IM_ASSERT(atlas->ShadowRectIds[0] != ImFontAtlasRectId_Invalid); + + const int size = shadow_cfg->TexCornerSize + shadow_cfg->TexEdgeSize + shadow_cfg->TexCornerSize; + + // The bounds of the rectangle we are generating the shadow from + const ImVec2 shadow_rect_min((float)corner_size, (float)corner_size); + const ImVec2 shadow_rect_max((float)(corner_size + edge_size), (float)(corner_size + edge_size)); + + // Remove the padding we added + const int padding = shadow_cfg->GetRectTexPadding(); + r.x += (unsigned short)padding; + r.y += (unsigned short)padding; + r.w -= (unsigned short)padding * 2; + r.h -= (unsigned short)padding * 2; + + // Generate distance field + // We draw the actual texture content by evaluating the distance field for the inner rectangle + float* tex_data = (float*)alloca(size * size * sizeof(float)); + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + { + float dist = DistanceFromRectangle(ImVec2((float)x, (float)y), shadow_rect_min, shadow_rect_max); + float alpha = 1.0f - ImMin(ImMax(dist + shadow_cfg->TexDistanceFieldOffset, 0.0f) / ImMax(shadow_cfg->TexCornerSize + shadow_cfg->TexDistanceFieldOffset, 0.001f), 1.0f); + alpha = ImPow(alpha, shadow_cfg->TexFalloffPower); // Apply power curve to give a nicer falloff + tex_data[x + (y * size)] = alpha; + } + + // Blur + if (shadow_cfg->TexBlur) + GaussianBlur(tex_data, size); + + // Copy to texture, truncating to the actual required texture size (the bottom/right of the source data is chopped off, as we don't need it - see below). The truncated size is essentially the top 2x2 of our data, plus a little bit of padding for sampling. + ImTextureData* tex = atlas->TexData; + const int shadow_tex_size = shadow_cfg->CalcRectTexSize(); + for (int y = 0; y < shadow_tex_size; y++) + for (int x = 0; x < shadow_tex_size; x++) + { + const float alpha_f = tex_data[x + (y * size)]; + const unsigned char alpha_8 = (unsigned char)(0xFF * alpha_f); + + // FIXME-SHADOWS: May be optimized. + switch (atlas->TexData->Format) + { + case ImTextureFormat_Alpha8: + { + ImU8* out_p = (ImU8*)(void*)tex->GetPixelsAt(r.x + x, r.y + y); + *out_p = alpha_8; + break; + } + case ImTextureFormat_RGBA32: + { + ImU32* out_p = (ImU32*)(void*)tex->GetPixelsAt(r.x + x, r.y + y); + *out_p = IM_COL32(255, 255, 255, alpha_8);; + break; + } + } + } + } + + // Refresh UV coordinates + // Generate UVs for each of the nine sections, which are arranged in a 3x3 grid starting from 0 in the top-left and going across then down + if (atlas->GetCustomRect(atlas->ShadowRectIds[0], &r)) + { + // Remove the padding we added + const int padding = shadow_cfg->GetRectTexPadding(); + r.x += (unsigned short)padding; + r.y += (unsigned short)padding; + r.w -= (unsigned short)padding * 2; + r.h -= (unsigned short)padding * 2; + + for (int i = 0; i < 9; i++) + { + // The third row/column of the 3x3 grid are generated by flipping the appropriate chunks of the upper 2x2 grid. + bool flip_h = false; // Do we need to flip the UVs horizontally? + bool flip_v = false; // Do we need to flip the UVs vertically? + + ImFontAtlasRect sub_rect = r; + switch (i % 3) + { + case 0: sub_rect.w = (unsigned short)corner_size; break; + case 1: sub_rect.x += (unsigned short)corner_size; sub_rect.w = (unsigned short)edge_size; break; + case 2: sub_rect.w = (unsigned short)corner_size; flip_h = true; break; + } + + switch (i / 3) + { + case 0: sub_rect.h = (unsigned short)corner_size; break; + case 1: sub_rect.y += (unsigned short)corner_size; sub_rect.h = (unsigned short)edge_size; break; + case 2: sub_rect.h = (unsigned short)corner_size; flip_v = true; break; + } + + // FIXME-SHADOWS: caching UV !! + ImVec2 uv0 = ImVec2((float)(sub_rect.x), (float)(sub_rect.y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(sub_rect.x + sub_rect.w), (float)(sub_rect.y + sub_rect.h)) * atlas->TexUvScale; + atlas->ShadowRectUvs[i] = ImVec4(flip_h ? uv1.x : uv0.x, flip_v ? uv1.y : uv0.y, flip_h ? uv0.x : uv1.x, flip_v ? uv0.y : uv1.y); + } + } + + // The convex shape shadow texture + add_and_draw = (atlas->GetCustomRect(atlas->ShadowRectIds[1], &r) == false); + if (add_and_draw) + { + atlas->ShadowRectIds[1] = atlas->AddCustomRect(shadow_cfg->CalcConvexTexWidth() + shadow_cfg->GetConvexTexPadding(), shadow_cfg->CalcConvexTexHeight() + shadow_cfg->GetConvexTexPadding(), &r); + IM_ASSERT(atlas->ShadowRectIds[1] != ImFontAtlasRectId_Invalid); + + const int size = shadow_cfg->TexCornerSize * 2; + const int padding = shadow_cfg->GetConvexTexPadding(); + + // Generate distance field + // We draw the actual texture content by evaluating the distance field for the distance from a center point + ImVec2 center_point(size * 0.5f, size * 0.5f); + float* tex_data = (float*)alloca(size * size * sizeof(float)); + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + { + float dist = DistanceFromPoint(ImVec2((float)x, (float)y), center_point); + float alpha = 1.0f - ImMin(ImMax((float)dist + shadow_cfg->TexDistanceFieldOffset, 0.0f) / ImMax((float)shadow_cfg->TexCornerSize + shadow_cfg->TexDistanceFieldOffset, 0.001f), 1.0f); + alpha = ImPow(alpha, shadow_cfg->TexFalloffPower); // Apply power curve to give a nicer falloff + tex_data[x + (y * size)] = alpha; + } + + // Blur + if (shadow_cfg->TexBlur) + GaussianBlur(tex_data, size); + + // Copy to texture, truncating to the actual required texture size (the bottom/right of the source data is chopped off, as we don't need it - see below) + // We push the data down and right by the amount we padded the top of the texture (see CalcConvexTexWidth/CalcConvexTexHeight) for details + const int padded_size = (int)(shadow_cfg->TexCornerSize / ImCos(IM_PI * 0.25f)); + const int src_x_offset = padding + (padded_size - shadow_cfg->TexCornerSize); + const int src_y_offset = padding + (padded_size - shadow_cfg->TexCornerSize); + + const int tex_width = shadow_cfg->CalcConvexTexWidth(); + const int tex_height = shadow_cfg->CalcConvexTexHeight(); + ImTextureData* tex = atlas->TexData; + for (int y = 0; y < tex_height; y++) + for (int x = 0; x < tex_width; x++) + { + const int src_x = ImClamp(x - src_x_offset, 0, size - 1); + const int src_y = ImClamp(y - src_y_offset, 0, size - 1); + const float alpha_f = tex_data[src_x + (src_y * size)]; + const unsigned char alpha_8 = (unsigned char)(0xFF * alpha_f); + + // FIXME-SHADOWS: May be optimized. + switch (atlas->TexData->Format) + { + case ImTextureFormat_Alpha8: + { + ImU8* out_p = (ImU8*)(void*)tex->GetPixelsAt(r.x + x, r.y + y); + *out_p = alpha_8; + break; + } + case ImTextureFormat_RGBA32: + { + ImU32* out_p = (ImU32*)(void*)tex->GetPixelsAt(r.x + x, r.y + y); + *out_p = IM_COL32(255, 255, 255, alpha_8);; + break; + } + } + } + } + + // Refresh UV coordinates + if (atlas->GetCustomRect(atlas->ShadowRectIds[1], &r)) + { + // Remove the padding we added + const int padding = shadow_cfg->GetConvexTexPadding(); + const int tex_width = shadow_cfg->CalcConvexTexWidth(); + const int tex_height = shadow_cfg->CalcConvexTexHeight(); + r.x += (unsigned short)padding; + r.y += (unsigned short)padding; + r.w = (unsigned short)(tex_width - (padding * 2)); + r.h = (unsigned short)(tex_height - (padding * 2)); + + ImVec2 uv0 = ImVec2((float)(r.x), (float)(r.y)) * atlas->TexUvScale; + ImVec2 uv1 = ImVec2((float)(r.x + r.w), (float)(r.y + r.h)) * atlas->TexUvScale; + atlas->ShadowRectUvs[9] = ImVec4(uv0.x, uv0.y, uv1.x, uv1.y); + } +} + // Start packing over current empty texture void ImFontAtlasBuildInit(ImFontAtlas* atlas) { @@ -4230,6 +5240,7 @@ void ImFontAtlasBuildInit(ImFontAtlas* atlas) // Add required texture data ImFontAtlasBuildUpdateLinesTexData(atlas); ImFontAtlasBuildUpdateBasicTexData(atlas); + ImFontAtlasBuildUpdateShadowTexData(atlas); // Register fonts ImFontAtlasBuildUpdatePointers(atlas); diff --git a/imgui_internal.h b/imgui_internal.h index f577c170b..72c27c91c 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -170,6 +170,7 @@ struct ImGuiOldColumns; // Storage data for a columns set for legacy struct ImGuiPopupData; // Storage for current popup stack struct ImGuiSettingsHandler; // Storage for one type registered in the .ini file struct ImGuiStyleMod; // Stacked style modifier, backup of modified data so we can restore it +struct ImGuiStyleShadowTexConfig; // Shadow Texture baking config struct ImGuiStyleVarInfo; // Style variable information (e.g. to access style variables from an enum) struct ImGuiTabBar; // Storage for a tab bar struct ImGuiTabItem; // Storage for a tab item (within a tab bar) @@ -512,6 +513,7 @@ inline ImVec4 ImLerp(const ImVec4& a, const ImVec4& b, float t) { return inline float ImSaturate(float f) { return (f < 0.0f) ? 0.0f : (f > 1.0f) ? 1.0f : f; } inline float ImLengthSqr(const ImVec2& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y); } inline float ImLengthSqr(const ImVec4& lhs) { return (lhs.x * lhs.x) + (lhs.y * lhs.y) + (lhs.z * lhs.z) + (lhs.w * lhs.w); } +inline float ImLength(const ImVec2& lhs, float fail_value) { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImSqrt(d); return fail_value; } inline float ImInvLength(const ImVec2& lhs, float fail_value) { float d = (lhs.x * lhs.x) + (lhs.y * lhs.y); if (d > 0.0f) return ImRsqrt(d); return fail_value; } inline float ImTrunc(float f) { return (float)(int)(f); } inline ImVec2 ImTrunc(const ImVec2& v) { return ImVec2((float)(int)(v.x), (float)(int)(v.y)); } @@ -872,6 +874,9 @@ struct IMGUI_API ImDrawListSharedData float ArcFastRadiusCutoff; // Cutoff radius after which arc drawing will fallback to slower PathArcTo() ImU8 CircleSegmentCounts[64]; // Precomputed segment count for given radius before we calculate it dynamically (to avoid calculation overhead) + int* ShadowRectIds; // IDs of rects for shadow texture (2 entries) + const ImVec4* ShadowRectUvs; // UV coordinates for shadow texture (10 entries) + ImDrawListSharedData(); ~ImDrawListSharedData(); void SetCircleTessellationMaxError(float max_error); diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index e487b3f96..b846f39a5 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -9070,10 +9070,14 @@ bool ImGui::BeginViewportSideBar(const char* name, ImGuiViewport* viewport_p, Im } window_flags |= ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; + + // Create window + PushStyleColor(ImGuiCol_WindowShadow, ImVec4(0, 0, 0, 0)); PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0)); // Lift normal size constraint bool is_open = Begin(name, NULL, window_flags); PopStyleVar(2); + PopStyleColor(); return is_open; }