/* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "stdafx.h" #include "editor/editorIcons.hpp" #include "rendering/renderer.h" #include "utilities/Globals.h" #include "stb/stb_image.h" #include #include #include #include #include namespace editor_icons { namespace { // editor_icons.png, written out by CMake unsigned char const png[] = { #include "editor_icons_png.inc" }; int const cell{64}; // pixels of an icon in the png int const columns{8}; int const rows{(static_cast(icon::count) + columns - 1) / columns}; // the coverage of the icons, a byte a pixel, as they're in the png. empty if it couldn't be read std::vector const &coverage() { static std::vector const pixels = []() { std::vector result; // read in a thread of its own: the texture loader turns the images upside down for the threads which don't say otherwise, // and what a thread says stays with it std::thread reader([&result]() { stbi_set_flip_vertically_on_load_thread(0); int width{0}, height{0}, components{0}; auto *rgba{stbi_load_from_memory(png, static_cast(sizeof(png)), &width, &height, &components, 4)}; if (rgba == nullptr) return; if (width == columns * cell && height >= rows * cell) { result.resize(static_cast(width) * height); for (std::size_t index = 0; index < result.size(); ++index) result[index] = rgba[index * 4 + 3]; } stbi_image_free(rgba); }); reader.join(); return result; }(); return pixels; } // the icons scaled down to the size they're drawn at, each with a pixel of empty space around it, so that the filtering // doesn't pick up a neighbour struct atlas { std::uint64_t texture{0}; int size{0}; // pixels of an icon bool failed{false}; }; atlas g_atlas; int stride(int const Size) { return Size + 2; } // part of the source pixel Pixel the target pixel Target of the size Scale covers float overlap(int const Target, float const Scale, int const Pixel) { auto const start{std::max(Target * Scale, static_cast(Pixel))}; auto const end{std::min((Target + 1) * Scale, static_cast(Pixel + 1))}; return std::max(0.0f, end - start); } std::vector scaled(int const Size) { auto const &source{coverage()}; auto const width{columns * stride(Size)}; auto const height{rows * stride(Size)}; std::vector rgba(static_cast(width) * height * 4, 255); for (std::size_t index = 3; index < rgba.size(); index += 4) rgba[index] = 0; // each target pixel the average of the source pixels under it, the ones under its edges by the part they're under it auto const scale{static_cast(cell) / Size}; std::vector across(static_cast(Size) * cell); for (int index = 0; index < static_cast(icon::count); ++index) { auto const sourcex{(index % columns) * cell}; auto const sourcey{(index / columns) * cell}; for (int y = 0; y < cell; ++y) { for (int x = 0; x < Size; ++x) { float sum{0.0f}; for (int pixel = static_cast(x * scale); pixel < std::min(cell, static_cast(std::ceil((x + 1) * scale))); ++pixel) sum += overlap(x, scale, pixel) * source[static_cast(sourcey + y) * columns * cell + sourcex + pixel]; across[static_cast(y) * Size + x] = sum / scale; } } auto const targetx{(index % columns) * stride(Size) + 1}; auto const targety{(index / columns) * stride(Size) + 1}; for (int y = 0; y < Size; ++y) { for (int x = 0; x < Size; ++x) { float sum{0.0f}; for (int pixel = static_cast(y * scale); pixel < std::min(cell, static_cast(std::ceil((y + 1) * scale))); ++pixel) sum += overlap(y, scale, pixel) * across[static_cast(pixel) * Size + x]; rgba[(static_cast(targety + y) * width + targetx + x) * 4 + 3] = static_cast(std::clamp(sum / scale + 0.5f, 0.0f, 255.0f)); } } } return rgba; } // the texture of the icons at the size, made again when the size changes bool prepare(int const Size) { if (g_atlas.failed) return false; if (g_atlas.texture != 0 && g_atlas.size == Size) return true; auto *renderer{GfxRenderer ? GfxRenderer->GetImguiRenderer() : nullptr}; if (renderer == nullptr || coverage().empty()) { g_atlas.failed = true; return false; } if (g_atlas.texture != 0) renderer->Release_Image(g_atlas.texture); auto const rgba{scaled(Size)}; g_atlas.texture = renderer->Create_Image(rgba.data(), columns * stride(Size), rows * stride(Size)); g_atlas.size = Size; // a renderer which can't show them gets no more requests g_atlas.failed = (g_atlas.texture == 0); return false == g_atlas.failed; } } // namespace float size() { return std::round(20.0f * std::max(1.0f, Global.ui_scale)); } bool draw(ImDrawList *List, icon const Icon, ImVec2 const &Center, ImU32 const Color) { // all at the one size, the texture is made for it auto const pixels{static_cast(size())}; if (List == nullptr || Icon >= icon::count || false == prepare(pixels)) return false; auto const index{static_cast(Icon)}; auto const width{static_cast(columns * stride(pixels))}; auto const height{static_cast(rows * stride(pixels))}; ImVec2 const uv0{((index % columns) * stride(pixels) + 1) / width, ((index / columns) * stride(pixels) + 1) / height}; ImVec2 const uv1{uv0.x + pixels / width, uv0.y + pixels / height}; // on whole pixels, the icon was scaled for them ImVec2 const min{std::round(Center.x - pixels * 0.5f), std::round(Center.y - pixels * 0.5f)}; List->AddImage(static_cast(g_atlas.texture), min, ImVec2(min.x + pixels, min.y + pixels), uv0, uv1, Color); return true; } } // namespace editor_icons