492 lines
20 KiB
C++
492 lines
20 KiB
C++
// 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);
|
|
}
|