From 94d56f25b1150cf3e1765f83ecc88a3868ad02c4 Mon Sep 17 00:00:00 2001 From: Musa Mahmood Date: Wed, 7 Oct 2026 12:03:54 +0100 Subject: [PATCH] [0.0.0] Capture window: a zoom slider, and fitting to the window, its width or its height --- src/app/viewer.jai | 62 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/src/app/viewer.jai b/src/app/viewer.jai index df3230a..00863a0 100644 --- a/src/app/viewer.jai +++ b/src/app/viewer.jai @@ -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. }