[0.0.0] Capture window: a zoom slider, and fitting to the window, its width or its height

This commit is contained in:
Musa Mahmood 2026-10-07 12:03:54 +01:00
parent 4323831801
commit 94d56f25b1

View File

@ -9,6 +9,8 @@ VIEWER_MAX_TEXTURES :: 1200; // previews on the GPU at once, about 200 KB
VIEWER_PREFETCH_ROWS :: 8; // most rows decoded ahead of the view, above and below
VIEWER_UPLOAD_BUDGET_MS :: 4.0; // time per frame spent uploading previews
VIEWER_FULL_WINDOW :: "Capture";
VIEWER_MIN_ZOOM :: 5.0; // percent, for the full-size window
VIEWER_MAX_ZOOM :: 800.0;
GPU_TEXTURE_MAX_SIDE :: 16384; // what D3D11 guarantees; larger images are scaled down to fit
@ -36,6 +38,14 @@ Viewer_Capture :: struct {
in_flight: bool; // under viewer.mutex: queued for a worker, or being decoded
}
// How the full-size window sizes its image.
Viewer_Fit :: enum u8 {
OFF; // at the zoom the slider sets
WINDOW; // the whole image in the window
WIDTH; // the window's width, scrolling down
HEIGHT; // the window's height, scrolling across
}
// Pixels a worker decoded, waiting for the main thread to upload them.
Decoded :: struct {
index: s32;
@ -76,7 +86,8 @@ viewer: struct {
full_source_width, full_source_height: s32;
full_loading: bool;
full_failed: bool;
full_actual_size: bool;
full_fit := Viewer_Fit.WINDOW;
full_zoom: float = 100; // percent of the size it was captured at; set by the fit, unless OFF
}
// ---------------------------------------------------------------- startup and shutdown
@ -498,11 +509,24 @@ viewer_full_window :: () { using ImGui;
TextColored(app_theme.palette.dim, "% x %", viewer.full_source_width, viewer.full_source_height);
}
if viewer.full_loading { SameLine(); TextColored(app_theme.palette.dim, "Loading..."); }
SameLine();
Checkbox("Actual size", *viewer.full_actual_size);
SameLine();
TextColored(app_theme.palette.dim, "Left / Right: previous / next");
// Zoom is a percentage of the size the capture was taken at. A fit sets it from the window, larger
// or smaller, and the slider shows what it works out to; moving the slider turns the fit off,
// and the zoom then stays for the next capture.
fit := viewer.full_fit;
Text("Fit:");
SameLine(); if RadioButton("Window", fit == .WINDOW) fit = .WINDOW;
SameLine(); if RadioButton("Width", fit == .WIDTH) fit = .WIDTH;
SameLine(); if RadioButton("Height", fit == .HEIGHT) fit = .HEIGHT;
SameLine(); if RadioButton("Off", fit == .OFF) fit = .OFF;
viewer.full_fit = fit;
SameLine();
SetNextItemWidth(260);
if SliderFloat("Zoom", *viewer.full_zoom, VIEWER_MIN_ZOOM, VIEWER_MAX_ZOOM, "%.0f%%", .Logarithmic) viewer.full_fit = .OFF;
SameLine();
if SmallButton("100%") { viewer.full_fit = .OFF; viewer.full_zoom = 100; }
// Left and Right step through the captures in grid order while this window has focus.
if IsWindowFocused(.RootAndChildWindows) {
if IsKeyPressed(.LeftArrow) && viewer.full_index > 0 viewer_request_full(viewer.full_index - 1);
@ -512,16 +536,28 @@ viewer_full_window :: () { using ImGui;
if viewer.full_failed {
TextColored(app_theme.palette.warning, "This capture cannot be shown; see the Event Log.");
} else if viewer.full_texture.handle {
w, h := cast(float) viewer.full_texture.width, cast(float) viewer.full_texture.height;
BeginChild("image", .{0, 0}, .None, .HorizontalScrollbar);
if viewer.full_actual_size {
// At the size it was captured; an image scaled down to fit a texture is drawn back up.
Image(gpu_texture_ref(viewer.full_texture), .{cast(float) viewer.full_source_width, cast(float) viewer.full_source_height});
} else {
available := GetContentRegionAvail();
scale := min(min(available.x / w, available.y / h), 1.0); // fitted, never enlarged
Image(gpu_texture_ref(viewer.full_texture), .{w * scale, h * scale});
// Each fit keeps the same scrollbars from frame to frame. Otherwise fitting the width of a tall
// image would bring up a vertical scrollbar, narrowing the area, changing the fit, and so on.
flags := WindowFlags.HorizontalScrollbar;
if viewer.full_fit == {
case .WINDOW; flags = .NoScrollbar;
case .WIDTH; flags |= .AlwaysVerticalScrollbar;
case .HEIGHT; flags |= .AlwaysHorizontalScrollbar;
}
BeginChild("image", .{0, 0}, .None, flags);
area := GetContentRegionAvail();
// Sized from the captured size, so an image scaled down to fit a texture is drawn back up.
source_w, source_h := cast(float) viewer.full_source_width, cast(float) viewer.full_source_height;
if source_w > 0 && source_h > 0 && area.x > 0 && area.y > 0 {
if viewer.full_fit == {
case .WINDOW; viewer.full_zoom = 100 * min(area.x / source_w, area.y / source_h);
case .WIDTH; viewer.full_zoom = 100 * area.x / source_w;
case .HEIGHT; viewer.full_zoom = 100 * area.y / source_h;
}
}
zoom := clamp(viewer.full_zoom, VIEWER_MIN_ZOOM, VIEWER_MAX_ZOOM) / 100;
Image(gpu_texture_ref(viewer.full_texture), .{source_w * zoom, source_h * zoom});
EndChild();
// The previous image stays up while the next one loads, so stepping through does not flicker.
}