The repository had no licence at all, so every source was all-rights-reserved by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino Ferrari) and a two-line SPDX header to each of the 113 first-party sources -- 48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the grant travels with each file rather than relying on the root document alone. Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely vendored code is untouched: third_party/stb_truetype.h carries no header of ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py emit() now writes the two lines into its header comment and the file was regenerated -- gen_atlas.py --check passes and the regeneration adds only those comment lines to the previously committed file. README's provenance section now states MIT and links LICENSE. No code, test or public API change.
495 lines
21 KiB
C++
495 lines
21 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
// The Dear ImGui adapter's own logic, pinned against real ImGui frames on the
|
|
// tests' surfaceless EGL context -- no window, no display server.
|
|
//
|
|
// What is tested here is what this project owns, not the toolkit's: ImGui reports
|
|
// input as *levels* where `plot::input` wants edges, so the widget synthesises
|
|
// them, gates delivery on hover, captures a drag across a region exit, and takes
|
|
// the wheel away from window scrolling; and because it draws through GL rather
|
|
// than ImGui's draw list, it has to keep its painting inside the child's *visible*
|
|
// rectangle itself. The suite is otherwise ImGui-free, so this file and its link
|
|
// are added only when the adapter is built (tests/CMakeLists.txt).
|
|
|
|
#include "harness.hpp"
|
|
#include "egl_headless.hpp"
|
|
|
|
#include <imgui/imgui_plot.hpp>
|
|
|
|
#include <imgui.h>
|
|
|
|
#include <plot/data/stream_series.hpp>
|
|
#include <plot/gl/api.hpp>
|
|
#include <plot/render/renderer.hpp>
|
|
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <limits>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
/// The off-screen target and the ImGui display are the same size: the widget's
|
|
/// region is a sub-rectangle of the framebuffer, which is the point of the test.
|
|
constexpr u32 kFb = 512;
|
|
constexpr float kDisplay = 512.0f;
|
|
constexpr float kChild = 400.0f;
|
|
|
|
/// A plot the widget drives, and the host frame around it.
|
|
struct Fixture {
|
|
plottest::HeadlessContext ctx;
|
|
plot::Renderer renderer;
|
|
plot::data::StreamSeries series{4096, 0.0, 0.01};
|
|
plot::Plot plot;
|
|
plot::imgui::PlotWidget widget;
|
|
u32 ax = 0;
|
|
u32 ay = 0;
|
|
/// A second plot, so a host that draws two through one widget can be tested.
|
|
plot::Plot other;
|
|
u32 ax_other = 0;
|
|
u32 ay_other = 0;
|
|
|
|
float win_w = kDisplay;
|
|
float win_h = kDisplay;
|
|
float child_w = kChild;
|
|
float child_h = kChild;
|
|
/// Content past the window, so the hosting window has a scroll range: the wheel
|
|
/// containment test needs one to be able to observe a scroll at all.
|
|
bool tall_parent = false;
|
|
bool prev_down = false;
|
|
|
|
ImVec2 child_origin{0.0f, 0.0f}; ///< the child's top-left, read in-frame (points)
|
|
float scroll_y = 0.0f; ///< the hosting window's scroll, read in-frame
|
|
|
|
/// The pixels the widget painted: the bounding box of everything that is not the
|
|
/// host's clear colour, in top-left framebuffer coordinates.
|
|
u32 paint_min_x = kFb;
|
|
u32 paint_min_y = kFb;
|
|
u32 paint_max_x = 0;
|
|
u32 paint_max_y = 0;
|
|
|
|
/// The host's own frame. Green on purpose: no colour the plot draws, nor any
|
|
/// blend of them, is (0, 192, 0), so "the host's pixel survived" is decidable.
|
|
plot::Color host_color = plot::Color::hex(0x00C000);
|
|
std::uint8_t host_rgb_[3] = {0, 0, 0};
|
|
|
|
Fixture() : ctx(kFb, kFb) {}
|
|
~Fixture() { ImGui::DestroyContext(); }
|
|
|
|
Fixture(const Fixture&) = delete;
|
|
Fixture& operator=(const Fixture&) = delete;
|
|
|
|
bool init() {
|
|
plot::Status st;
|
|
if (!ctx.create(&st)) {
|
|
std::printf("[FAIL] headless context: %s\n", st.message);
|
|
return false;
|
|
}
|
|
if (!renderer.init(&st)) {
|
|
std::printf("[FAIL] renderer init: %s\n", st.message);
|
|
return false;
|
|
}
|
|
|
|
ImGui::CreateContext();
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.IniFilename = nullptr;
|
|
io.DisplaySize = ImVec2(kDisplay, kDisplay);
|
|
io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
|
|
io.Fonts->AddFontDefault();
|
|
std::uint8_t* pixels = nullptr;
|
|
int tw = 0;
|
|
int th = 0;
|
|
// The widget needs a built atlas exactly as a platform backend would leave it;
|
|
// no renderer backend is involved, so the texture id is a placeholder.
|
|
io.Fonts->GetTexDataAsRGBA32(&pixels, &tw, &th);
|
|
io.Fonts->SetTexID(static_cast<ImTextureID>(1));
|
|
|
|
for (int i = 0; i < 4096; ++i) series.push(static_cast<f32>(std::sin(0.05 * i)));
|
|
series.acquire();
|
|
ax = plot.add_x_axis({"time", {0.0, 20.0}, false, plot::AxisPlace::Bottom, false});
|
|
ay = plot.add_y_axis({"signal", {-1.2, 1.2}, false, plot::AxisPlace::Left, false});
|
|
plot.add_trace({&series, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
|
|
plot.set_x_range(ax, {0.0, 20.0}); // explicit: clears auto scale, so a pan shows
|
|
plot.set_y_range(ay, {-1.2, 1.2});
|
|
|
|
other.set_viewport(plot.viewport());
|
|
ax_other = other.add_x_axis({"time", {0.0, 50.0}, false, plot::AxisPlace::Bottom, false});
|
|
ay_other = other.add_y_axis({"signal", {-1.0, 1.0}, false, plot::AxisPlace::Left, false});
|
|
other.add_trace({&series, plot::Color::hex(0xFFB020), 1.5f, ax_other, ay_other});
|
|
other.set_x_range(ax_other, {0.0, 50.0});
|
|
other.set_y_range(ay_other, {-1.0, 1.0});
|
|
|
|
// The host colour as the pipeline actually produced it: the pixel tests compare
|
|
// bytes, not a rounding of the clear value.
|
|
clear_host();
|
|
std::uint8_t probe[4] = {0, 0, 0, 0};
|
|
pixel(0, 0, probe);
|
|
host_rgb_[0] = probe[0];
|
|
host_rgb_[1] = probe[1];
|
|
host_rgb_[2] = probe[2];
|
|
return true;
|
|
}
|
|
|
|
void clear_host() {
|
|
plot::gl::glViewport(0, 0, static_cast<plot::gl::GLsizei>(ctx.width),
|
|
static_cast<plot::gl::GLsizei>(ctx.height));
|
|
plot::gl::glClearColor(host_color.r, host_color.g, host_color.b, host_color.a);
|
|
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
|
|
}
|
|
|
|
/// One widget frame with the host's input queued exactly as a platform backend
|
|
/// would queue it. `pos` is in ImGui points; `-FLT_MAX` is ImGui's "the pointer is
|
|
/// not on this window" sentinel.
|
|
void frame(const ImVec2& pos, bool down, float wheel = 0.0f, plot::Plot* target = nullptr) {
|
|
plot::Plot& shown = target != nullptr ? *target : plot;
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.AddMousePosEvent(pos.x, pos.y);
|
|
if (down != prev_down) {
|
|
io.AddMouseButtonEvent(0, down);
|
|
prev_down = down;
|
|
}
|
|
if (wheel != 0.0f) io.AddMouseWheelEvent(0.0f, wheel);
|
|
|
|
// A bare window: no background to paint over the plot, and no title bar or
|
|
// scrollbar to add pixels the scan below would mistake for the widget's.
|
|
const ImGuiWindowFlags host_flags = ImGuiWindowFlags_NoBackground |
|
|
ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoScrollbar;
|
|
ImGui::NewFrame();
|
|
ImGui::SetNextWindowPos(ImVec2(0.0f, 0.0f), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(win_w, win_h), ImGuiCond_Always);
|
|
ImGui::Begin("host", nullptr, host_flags);
|
|
child_origin = ImGui::GetCursorScreenPos(); // where the widget's child goes
|
|
widget.draw("plot", shown, plot::Vec2{child_w, child_h}, renderer);
|
|
if (tall_parent) ImGui::Dummy(ImVec2(10.0f, 900.0f)); // content past the window
|
|
scroll_y = ImGui::GetScrollY();
|
|
ImGui::End();
|
|
ImGui::Render();
|
|
}
|
|
|
|
/// The framebuffer pixel at a *top-left* coordinate. `read_pixel` speaks GL's
|
|
/// bottom-left convention, as the render smoke test notes.
|
|
void pixel(u32 x, u32 y, std::uint8_t out[4]) {
|
|
if (!ctx.read_pixel(x, ctx.height - 1u - y, out)) PLOT_CHECK(false);
|
|
}
|
|
|
|
bool is_host(const std::uint8_t px[4]) const {
|
|
return px[0] == host_rgb_[0] && px[1] == host_rgb_[1] && px[2] == host_rgb_[2];
|
|
}
|
|
|
|
/// Records the bounding box of the pixels that are not the host's clear colour.
|
|
/// Reads the whole framebuffer once, so a draw that escaped the widget's region
|
|
/// shows up even if it is nowhere near the pixels a point check samples.
|
|
void scan_painted() {
|
|
std::uint8_t probe[4] = {0, 0, 0, 0};
|
|
pixel(0, 0, probe); // read_pixel binds the target the frame was drawn into
|
|
std::vector<std::uint8_t> rgba(static_cast<plot::usize>(kFb) * kFb * 4u, 0u);
|
|
plot::gl::glPixelStorei(plot::gl::GL_PACK_ALIGNMENT, 4);
|
|
plot::gl::glReadPixels(0, 0, static_cast<plot::gl::GLsizei>(ctx.width),
|
|
static_cast<plot::gl::GLsizei>(ctx.height), plot::gl::GL_RGBA,
|
|
plot::gl::GL_UNSIGNED_BYTE, rgba.data());
|
|
|
|
paint_min_x = kFb;
|
|
paint_min_y = kFb;
|
|
paint_max_x = 0;
|
|
paint_max_y = 0;
|
|
for (u32 y = 0; y < kFb; ++y) {
|
|
// glReadPixels hands rows back bottom-up; this scan speaks top-left, the
|
|
// convention every Rect here uses.
|
|
const std::uint8_t* row =
|
|
rgba.data() + static_cast<plot::usize>(kFb - 1u - y) * kFb * 4u;
|
|
for (u32 x = 0; x < kFb; ++x) {
|
|
if (is_host(row + static_cast<plot::usize>(x) * 4u)) continue;
|
|
if (x < paint_min_x) paint_min_x = x;
|
|
if (x > paint_max_x) paint_max_x = x;
|
|
if (y < paint_min_y) paint_min_y = y;
|
|
if (y > paint_max_y) paint_max_y = y;
|
|
}
|
|
}
|
|
}
|
|
|
|
plot::Range x_range() const { return plot.x_range(ax); }
|
|
bool panning() const {
|
|
const plot::input::Controller* c = widget.controller();
|
|
return c != nullptr && c->drag_mode() == plot::input::DragMode::Pan;
|
|
}
|
|
};
|
|
|
|
constexpr float kMaxFloat = std::numeric_limits<float>::max();
|
|
|
|
} // namespace
|
|
|
|
PLOT_TEST(imgui_widget_pans_only_while_hovered_and_dragging) {
|
|
Fixture f;
|
|
if (!f.init()) PLOT_CHECK(false);
|
|
|
|
// A hovered pointer with no button moves nothing: the pointer must be *over* the
|
|
// plot and a drag must be in progress.
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
const plot::Range hovered = f.x_range();
|
|
f.frame(ImVec2(260.0f, 200.0f), false);
|
|
PLOT_CHECK_NEAR(f.x_range().min, hovered.min, 1e-9);
|
|
PLOT_CHECK_NEAR(f.x_range().max, hovered.max, 1e-9);
|
|
|
|
// A press begins a pan, but a press alone moves nothing either.
|
|
f.frame(ImVec2(200.0f, 200.0f), true);
|
|
PLOT_CHECK(f.panning());
|
|
PLOT_CHECK_NEAR(f.x_range().min, hovered.min, 1e-9);
|
|
|
|
// The drag pans (grab-and-drag: the data follows the cursor) and keeps the span.
|
|
const plot::Range pressed = f.x_range();
|
|
f.frame(ImVec2(260.0f, 200.0f), true);
|
|
PLOT_CHECK(f.x_range().min < pressed.min - 1e-6);
|
|
PLOT_CHECK_NEAR(f.x_range().span(), pressed.span(), 1e-9);
|
|
|
|
// The release ends it, and the next hovered move must not pan again.
|
|
f.frame(ImVec2(260.0f, 200.0f), false);
|
|
PLOT_CHECK(!f.panning());
|
|
const plot::Range released = f.x_range();
|
|
f.frame(ImVec2(300.0f, 200.0f), false);
|
|
PLOT_CHECK_NEAR(f.x_range().min, released.min, 1e-9);
|
|
PLOT_CHECK_NEAR(f.x_range().max, released.max, 1e-9);
|
|
|
|
// A press and drag *outside* the child -- the slider-in-the-same-window case --
|
|
// must not touch the plot at all.
|
|
f.frame(ImVec2(470.0f, 470.0f), false);
|
|
f.frame(ImVec2(470.0f, 470.0f), true);
|
|
f.frame(ImVec2(440.0f, 440.0f), true);
|
|
PLOT_CHECK_NEAR(f.x_range().min, released.min, 1e-9);
|
|
f.frame(ImVec2(440.0f, 440.0f), false);
|
|
}
|
|
|
|
PLOT_TEST(imgui_widget_drag_capture_survives_leaving_the_child) {
|
|
Fixture f;
|
|
if (!f.init()) PLOT_CHECK(false);
|
|
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
f.frame(ImVec2(200.0f, 200.0f), true);
|
|
const plot::Range pressed = f.x_range();
|
|
|
|
// Leftwards inside the child: the range minimum rises.
|
|
f.frame(ImVec2(160.0f, 200.0f), true);
|
|
PLOT_CHECK(f.x_range().min > pressed.min + 1e-6);
|
|
const plot::Range inside = f.x_range();
|
|
|
|
// Rightwards, with the pointer now outside the child: a captured drag keeps
|
|
// panning, or the gesture would silently stop following the cursor.
|
|
f.frame(ImVec2(470.0f, 470.0f), true);
|
|
PLOT_CHECK(f.x_range().min < inside.min - 1e-6);
|
|
|
|
// ... and the release arriving out there still ends it.
|
|
const plot::Range outside = f.x_range();
|
|
f.frame(ImVec2(470.0f, 470.0f), false);
|
|
PLOT_CHECK(!f.panning());
|
|
f.frame(ImVec2(430.0f, 430.0f), false);
|
|
PLOT_CHECK_NEAR(f.x_range().min, outside.min, 1e-9);
|
|
}
|
|
|
|
PLOT_TEST(imgui_widget_wheel_zooms_when_hovered_and_does_not_scroll_the_host) {
|
|
Fixture f;
|
|
f.tall_parent = true; // the hosting window can scroll, so containment is testable
|
|
if (!f.init()) PLOT_CHECK(false);
|
|
|
|
// Control: the wheel scrolls the hosting window when it is over the window's own
|
|
// area. Downwards, because ImGui clamps a scroll at the top and would prove
|
|
// nothing. Two frames of quiet first: ImGui sizes a window's content (and so its
|
|
// scroll range) from the previous frame, and a wheel arriving before that range
|
|
// is known is not scrolled anywhere.
|
|
f.frame(ImVec2(470.0f, 470.0f), false);
|
|
f.frame(ImVec2(470.0f, 470.0f), false);
|
|
const float scroll_before = f.scroll_y;
|
|
f.frame(ImVec2(470.0f, 470.0f), false, -1.0f);
|
|
PLOT_CHECK(f.scroll_y > scroll_before + 1.0f);
|
|
|
|
// The same wheel over the parent must not zoom the plot: it is not hovered.
|
|
const plot::Range over_parent = f.x_range();
|
|
f.frame(ImVec2(470.0f, 470.0f), false, -1.0f);
|
|
PLOT_CHECK_NEAR(f.x_range().span(), over_parent.span(), 1e-9);
|
|
|
|
// Over the plot, the wheel zooms it (positive scrolls up, which zooms in) and
|
|
// the hosting window does not scroll.
|
|
f.frame(ImVec2(200.0f, 200.0f), false); // hover: claims the wheel for the plot
|
|
const float scroll_before_plot = f.scroll_y;
|
|
const double span_before = f.x_range().span();
|
|
f.frame(ImVec2(200.0f, 200.0f), false, -1.0f);
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
PLOT_CHECK(f.x_range().span() > span_before);
|
|
PLOT_CHECK_NEAR(static_cast<double>(f.scroll_y), static_cast<double>(scroll_before_plot), 1e-3);
|
|
|
|
const double span_before_in = f.x_range().span();
|
|
f.frame(ImVec2(200.0f, 200.0f), false, 1.0f);
|
|
PLOT_CHECK(f.x_range().span() < span_before_in);
|
|
}
|
|
|
|
PLOT_TEST(imgui_widget_invalid_position_does_not_move_the_range) {
|
|
Fixture f;
|
|
if (!f.init()) PLOT_CHECK(false);
|
|
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
f.frame(ImVec2(200.0f, 200.0f), true); // press over the plot: capture
|
|
const plot::Range pressed = f.x_range();
|
|
|
|
// The platform backends leave (-FLT_MAX, -FLT_MAX) behind when the pointer leaves
|
|
// the window (imgui_impl_glfw's cursor-enter callback) or the window loses focus
|
|
// (ClearInputMouse). Scaled by the framebuffer scale and differenced against the
|
|
// last real position, that is a ~1e35 movement -- and with a press captured, a
|
|
// ~1e35 pan, which `set_x_range` would take (and, clearing auto scale, would not
|
|
// give back).
|
|
f.frame(ImVec2(-kMaxFloat, -kMaxFloat), true);
|
|
PLOT_CHECK(std::isfinite(f.x_range().min) && std::isfinite(f.x_range().max));
|
|
PLOT_CHECK_NEAR(f.x_range().min, pressed.min, 1e-9);
|
|
PLOT_CHECK_NEAR(f.x_range().max, pressed.max, 1e-9);
|
|
PLOT_CHECK(f.panning()); // still captured: only the position was unusable
|
|
|
|
// The release still ends the drag even with no usable position in the event.
|
|
f.frame(ImVec2(-kMaxFloat, -kMaxFloat), false);
|
|
PLOT_CHECK(!f.panning());
|
|
|
|
// A drag held across a sentinel, then a real position again. The movement must be
|
|
// measured from the last *real* position -- 100 px from (200, 200) to (300, 300) --
|
|
// and not from the sentinel. This is asserted with the drag alive on purpose:
|
|
// nothing can move a range once the button is up, so the same check after the
|
|
// release could not fail and would prove nothing.
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
f.frame(ImVec2(200.0f, 200.0f), true);
|
|
const plot::Range second_press = f.x_range();
|
|
f.frame(ImVec2(-kMaxFloat, -kMaxFloat), true);
|
|
PLOT_CHECK(f.panning());
|
|
const double axes_w = static_cast<double>(std::as_const(f.plot).layout().axes.w);
|
|
const double expected_shift = 100.0 / axes_w * second_press.span();
|
|
f.frame(ImVec2(300.0f, 300.0f), true);
|
|
PLOT_CHECK(f.panning());
|
|
PLOT_CHECK(std::isfinite(f.x_range().min));
|
|
PLOT_CHECK_NEAR(f.x_range().min, second_press.min - expected_shift, 0.05 * expected_shift);
|
|
f.frame(ImVec2(300.0f, 300.0f), false);
|
|
PLOT_CHECK(!f.panning());
|
|
}
|
|
|
|
PLOT_TEST(imgui_widget_paints_the_whole_child_when_nothing_clips_it) {
|
|
Fixture f;
|
|
if (!f.init()) PLOT_CHECK(false);
|
|
f.clear_host();
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
f.scan_painted();
|
|
|
|
// The requested child rectangle, in top-left coordinates.
|
|
const u32 x0 = static_cast<u32>(f.child_origin.x);
|
|
const u32 y0 = static_cast<u32>(f.child_origin.y);
|
|
const u32 x1 = x0 + static_cast<u32>(f.child_w);
|
|
const u32 y1 = y0 + static_cast<u32>(f.child_h);
|
|
|
|
// The clamp against the clip rect must be a no-op when nothing clips the child:
|
|
// the painted rectangle is exactly the child's, corner to corner.
|
|
PLOT_CHECK(f.paint_min_x <= x0 + 1u && f.paint_min_y <= y0 + 1u);
|
|
PLOT_CHECK(f.paint_max_x >= x1 - 2u && f.paint_max_y >= y1 - 2u);
|
|
|
|
// ... and nothing of it escapes: the pixels past the child's edge are still the
|
|
// host's.
|
|
std::uint8_t out[4] = {0, 0, 0, 0};
|
|
f.pixel(x1 + 6u, y1 + 6u, out);
|
|
PLOT_CHECK(f.is_host(out));
|
|
f.pixel(x1 + 6u, y0 + 6u, out);
|
|
PLOT_CHECK(f.is_host(out));
|
|
}
|
|
|
|
PLOT_TEST(imgui_widget_rebinding_a_plot_does_not_fabricate_a_press) {
|
|
Fixture f;
|
|
if (!f.init()) PLOT_CHECK(false);
|
|
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
f.frame(ImVec2(200.0f, 200.0f), true); // press over the first plot
|
|
PLOT_CHECK(f.panning());
|
|
|
|
// The host switches to its second plot with the button still held. ImGui reports
|
|
// a *level*, so there is no press edge in this frame: synthesising one out of the
|
|
// clear state a re-bind starts from would begin a drag the user never made.
|
|
f.frame(ImVec2(210.0f, 200.0f), true, 0.0f, &f.other);
|
|
PLOT_CHECK(f.widget.controller() != nullptr);
|
|
PLOT_CHECK(!f.panning());
|
|
const plot::Range other_before = f.other.x_range(f.ax_other);
|
|
f.frame(ImVec2(260.0f, 200.0f), true, 0.0f, &f.other);
|
|
PLOT_CHECK(!f.panning());
|
|
PLOT_CHECK_NEAR(f.other.x_range(f.ax_other).min, other_before.min, 1e-9);
|
|
f.frame(ImVec2(260.0f, 200.0f), false, 0.0f, &f.other); // release
|
|
PLOT_CHECK(!f.panning());
|
|
|
|
// Back on the first plot, a press that *is* a press still starts a drag: the
|
|
// button state was kept across the re-bind, not frozen.
|
|
f.frame(ImVec2(200.0f, 200.0f), false);
|
|
f.frame(ImVec2(200.0f, 200.0f), true);
|
|
PLOT_CHECK(f.panning());
|
|
}
|
|
|
|
PLOT_TEST(imgui_widget_paints_only_the_visible_part_of_a_clipped_child) {
|
|
Fixture f;
|
|
f.win_w = 200.0f; // the child (400x400) cannot fit, so ImGui clips it
|
|
f.win_h = 200.0f;
|
|
if (!f.init()) PLOT_CHECK(false);
|
|
f.clear_host();
|
|
// The pointer is outside the window entirely: this is about painting, not input.
|
|
f.frame(ImVec2(300.0f, 300.0f), false);
|
|
f.scan_painted();
|
|
|
|
const u32 x0 = static_cast<u32>(f.child_origin.x);
|
|
const u32 y0 = static_cast<u32>(f.child_origin.y);
|
|
const u32 x1 = x0 + static_cast<u32>(f.child_w); // the *requested* rectangle
|
|
const u32 y1 = y0 + static_cast<u32>(f.child_h);
|
|
|
|
// GL draws do not go through ImGui's clipping, so a child the window cuts off
|
|
// would otherwise clear and paint over the host's pixels beside it. The painted
|
|
// rectangle must stop well short of the requested one ...
|
|
PLOT_CHECK(f.paint_max_x + 20u < x1);
|
|
PLOT_CHECK(f.paint_max_y + 20u < y1);
|
|
// ... while still covering the part that is visible: this is a clamp, not a
|
|
// refusal to draw.
|
|
PLOT_CHECK(f.paint_max_x - f.paint_min_x > 100u);
|
|
PLOT_CHECK(f.paint_max_y - f.paint_min_y > 100u);
|
|
|
|
// The pixel deep inside the requested rectangle but outside the visible part kept
|
|
// the host's colour, and the one near the visible corner was painted.
|
|
std::uint8_t out[4] = {0, 0, 0, 0};
|
|
f.pixel(x1 - 5u, y1 - 5u, out);
|
|
PLOT_CHECK(f.is_host(out));
|
|
f.pixel(f.paint_min_x + 40u, f.paint_min_y + 40u, out);
|
|
PLOT_CHECK(!f.is_host(out));
|
|
}
|
|
|
|
/// The widget states the display's scale to the renderer, which is what makes the
|
|
/// chrome one physical size on any DPI: `io.DisplayFramebufferScale` is the same
|
|
/// factor the widget already maps points to framebuffer pixels by, so a host on a 2x
|
|
/// surface gets a 2x *device* font and a 2x device strip -- the same logical layout
|
|
/// as on a 1x one.
|
|
PLOT_TEST(imgui_widget_states_the_display_scale_to_the_renderer) {
|
|
Fixture f;
|
|
PLOT_CHECK(f.init());
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
|
|
f.frame(ImVec2(10.0f, 10.0f), false);
|
|
PLOT_CHECK_NEAR(f.renderer.style().ui_scale, 1.0f, 1e-6);
|
|
const plot::f32 cell_1x = f.renderer.axis_font_px();
|
|
const plot::f32 strip_1x = f.plot.layout().y_axis_width;
|
|
PLOT_CHECK(cell_1x > 1.0f);
|
|
PLOT_CHECK(strip_1x > 1.0f);
|
|
|
|
// The same widget and the same logical size, on a 2x surface.
|
|
io.DisplayFramebufferScale = ImVec2(2.0f, 2.0f);
|
|
f.frame(ImVec2(10.0f, 10.0f), false);
|
|
PLOT_CHECK_NEAR(f.renderer.style().ui_scale, 2.0f, 1e-6);
|
|
// The renderer hands it to the plot before laying out, so the *device* chrome
|
|
// doubles while the logical figure (device / scale) is what the 1x run measured.
|
|
PLOT_CHECK_NEAR(f.plot.ui_scale(), 2.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(f.renderer.axis_font_px(), 2.0f * cell_1x, 1e-3);
|
|
PLOT_CHECK_NEAR(f.plot.layout().y_axis_width, 2.0f * strip_1x, 0.05f);
|
|
|
|
// Back to 1x: the scale is re-read every frame rather than latched at the first
|
|
// one, so a window moved to another display follows.
|
|
io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f);
|
|
f.frame(ImVec2(10.0f, 10.0f), false);
|
|
PLOT_CHECK_NEAR(f.renderer.style().ui_scale, 1.0f, 1e-6);
|
|
PLOT_CHECK_NEAR(f.renderer.axis_font_px(), cell_1x, 1e-3);
|
|
PLOT_CHECK_NEAR(f.plot.layout().y_axis_width, strip_1x, 1e-3);
|
|
}
|