Skip to content

Commit 1256017

Browse files
Shadetailclaude
andcommitted
Per-monitor DPI scaling for the whole UI (v1.0.2)
The window must be per-monitor DPI aware for Desktop Duplication, so the UI scales itself: fonts and style rebuild from the window DPI (crisp bitmap font at 1x, Consolas at scaled sizes), WM_DPICHANGED rescales live between frames, and all fixed pixel sizes - widths, buttons, strip, readout, overlay label offsets, scope margins, drag hit targets - follow UiScale(). Readout dodges the button strip and panel combos when they would collide; the controls popup re-snaps on scale changes and clamps to the window height. HDRSCOPES_UISCALE env overrides for testing. Reviewed by Codex (gpt-5.6-sol): its fixes (sub-1x font scaling, readout plate scaling, histogram/waveform overlay offsets, ref-line drag tolerance) are included, plus fixes for its findings (rescale ordering, popup re-snap, left-side collision, top reservation). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4c1ef05 commit 1256017

8 files changed

Lines changed: 161 additions & 63 deletions

File tree

‎CMakeLists.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
cmake_minimum_required(VERSION 3.20)
2-
project(HDRScopes VERSION 1.0.1 LANGUAGES CXX)
2+
project(HDRScopes VERSION 1.0.2 LANGUAGES CXX)
33
if(MSVC)
44
enable_language(RC)
55
endif()

‎src/main.cpp‎

Lines changed: 121 additions & 36 deletions
Large diffs are not rendered by default.

‎src/scope/CIEScope.cpp‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,9 @@ void CIEScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s) {
8484
}
8585

8686
void CIEScope::DrawControls(Settings& s) {
87+
const float u = UiScale();
8788
const char* diags[] = { "xy (1931)", "u'v' (1976)" };
88-
ImGui::SetNextItemWidth(150); ImGui::Combo("Diagram", &s.cieDiagram, diags, 2);
89+
ImGui::SetNextItemWidth(150 * u); ImGui::Combo("Diagram", &s.cieDiagram, diags, 2);
8990
UiReset(s.cieDiagram, UiDefaults().cieDiagram);
9091
UiTip("xy (1931) is the classic horseshoe diagram; u'v' (1976) spaces colors "
9192
"more perceptually evenly.");
@@ -106,7 +107,7 @@ void CIEScope::DrawControls(Settings& s) {
106107
UiTip("Outline the outermost chromaticity excursions - the source's actual "
107108
"gamut footprint - even where the trace is too faint to see.");
108109
if (s.cieExtents) {
109-
ImGui::SameLine(); ImGui::SetNextItemWidth(110);
110+
ImGui::SameLine(); ImGui::SetNextItemWidth(110 * u);
110111
ImGui::SliderFloat("##exop", &s.cieExtentsOpacity, 0.0f, 1.0f, "opacity %.2f");
111112
UiReset(s.cieExtentsOpacity, UiDefaults().cieExtentsOpacity);
112113
}

‎src/scope/HistogramScope.cpp‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ double HistogramScope::ScreenXToNits(float x, const ScopeFrame& f, const Setting
124124

125125
void HistogramScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s) {
126126
const float top = f.graphP0.y, bot = f.graphP1.y, left = f.graphP0.x, right = f.graphP1.x;
127+
const float u = UiScale();
127128
const ImVec4 gc(s.graticuleColor.x, s.graticuleColor.y, s.graticuleColor.z, s.graticuleOpacity);
128129
const ImU32 colMajor = ImGui::GetColorU32(gc);
129130
const ImU32 colText = ImGui::GetColorU32(ImVec4(0.82f, 0.82f, 0.82f, std::max(0.5f, s.graticuleOpacity)));
@@ -137,7 +138,7 @@ void HistogramScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings&
137138
if (dec < 10000.0) for (int m = 2; m <= 9; ++m) {
138139
double n = dec * m; if (n > 10000.0) break;
139140
float mx = NitsToScreenX(n, f, s);
140-
if (inX(mx)) dl->AddLine(ImVec2(mx, bot - 12.0f), ImVec2(mx, bot), colMajor, 1.0f);
141+
if (inX(mx)) dl->AddLine(ImVec2(mx, bot - 12.0f * u), ImVec2(mx, bot), colMajor, 1.0f);
141142
}
142143
}
143144
dl->PopClipRect();
@@ -146,7 +147,7 @@ void HistogramScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings&
146147
char lab[16];
147148
if (dec >= 1000.0) snprintf(lab, sizeof(lab), "%gk", dec / 1000.0); else snprintf(lab, sizeof(lab), "%g", dec);
148149
ImVec2 ts = ImGui::CalcTextSize(lab);
149-
dl->AddText(ImVec2(x - ts.x * 0.5f, bot + 4.0f), colText, lab);
150+
dl->AddText(ImVec2(x - ts.x * 0.5f, bot + 4.0f * u), colText, lab);
150151
}
151152

152153
// Nit value of the axis position under the cursor, attached to the cursor.
@@ -155,8 +156,8 @@ void HistogramScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings&
155156
char lab[32];
156157
FormatNits(std::clamp(ScreenXToNits(io.MousePos.x, f, s), 0.0, 10000.0), lab, sizeof(lab));
157158
ImVec2 ts = ImGui::CalcTextSize(lab);
158-
ImVec2 tp(io.MousePos.x + 14.0f, io.MousePos.y - ts.y * 0.5f);
159-
if (tp.x + ts.x > right) tp.x = io.MousePos.x - ts.x - 10.0f;
159+
ImVec2 tp(io.MousePos.x + 14.0f * u, io.MousePos.y - ts.y * 0.5f);
160+
if (tp.x + ts.x > right) tp.x = io.MousePos.x - ts.x - 10.0f * u;
160161
tp.y = std::clamp(tp.y, top, bot - ts.y);
161162
dl->AddText(ImVec2(tp.x + 1, tp.y + 1), IM_COL32(0, 0, 0, 200), lab);
162163
dl->AddText(tp, IM_COL32(235, 235, 235, 230), lab);
@@ -181,8 +182,9 @@ void HistogramScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings&
181182
}
182183

183184
void HistogramScope::DrawControls(Settings& s) {
185+
const float u = UiScale();
184186
const char* modes[] = { "LRGB rows", "Overlay RGB", "Luma" };
185-
ImGui::SetNextItemWidth(160); ImGui::Combo("Mode", &s.histoMode, modes, 3);
187+
ImGui::SetNextItemWidth(160 * u); ImGui::Combo("Mode", &s.histoMode, modes, 3);
186188
UiReset(s.histoMode, UiDefaults().histoMode);
187189
UiTip("LRGB rows: four stacked bands (luma, R, G, B). Overlay RGB: the channels "
188190
"drawn over each other in one band. Luma: a single luminance band.");
@@ -193,7 +195,7 @@ void HistogramScope::DrawControls(Settings& s) {
193195
ImGui::PushID(i + 100);
194196
ImVec4 c = s.histoChannelEnabled[i] ? onCol[i] : ImVec4(0.2f, 0.2f, 0.2f, 1);
195197
ImGui::PushStyleColor(ImGuiCol_Button, c); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, c); ImGui::PushStyleColor(ImGuiCol_ButtonActive, c);
196-
if (ImGui::Button(names[i], ImVec2(26, 26))) s.histoChannelEnabled[i] = !s.histoChannelEnabled[i];
198+
if (ImGui::Button(names[i], ImVec2(26 * u, 26 * u))) s.histoChannelEnabled[i] = !s.histoChannelEnabled[i];
197199
UiReset(s.histoChannelEnabled[i], true);
198200
ImGui::PopStyleColor(3); ImGui::PopID(); if (i < 2) ImGui::SameLine();
199201
}
@@ -209,7 +211,7 @@ void HistogramScope::DrawControls(Settings& s) {
209211
float lg = std::log10(gmin), hg = std::log10(gmax);
210212
float t = (std::log10(std::clamp(s.histoGain, gmin, gmax)) - lg) / (hg - lg);
211213
int bri = (int)(t * 100.0f + 0.5f);
212-
ImGui::SetNextItemWidth(180);
214+
ImGui::SetNextItemWidth(180 * u);
213215
if (ImGui::SliderInt("Brightness", &bri, 0, 100))
214216
s.histoGain = std::pow(10.0f, lg + (bri / 100.0f) * (hg - lg));
215217
UiReset(s.histoGain, UiDefaults().histoGain);

‎src/scope/ScopePanel.cpp‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include "scope/ScopePanel.h"
22
#include "scope/ScopeFactory.h"
3+
#include "util/UiReset.h"
34
#include <algorithm>
45
#include <cmath>
56

@@ -19,10 +20,12 @@ void ScopePanel::Draw(int index, const ImVec2& panelP0, const ImVec2& panelP1,
1920
// Compute the histogram for this frame.
2021
scope_->Compute(input, s);
2122

22-
// Lay out the graph rect inside the panel (reserve margins).
23+
// Lay out the graph rect inside the panel (reserve margins). Scopes return
24+
// margins in 1x px; scale here so label room tracks the UI scale.
2325
Margins m = scope_->GetMargins(s);
24-
ImVec2 g0(panelP0.x + m.l, panelP0.y + m.t);
25-
ImVec2 g1(panelP1.x - m.r, panelP1.y - m.b);
26+
const float mu = UiScale();
27+
ImVec2 g0(panelP0.x + m.l * mu, panelP0.y + m.t * mu);
28+
ImVec2 g1(panelP1.x - m.r * mu, panelP1.y - m.b * mu);
2629
float gw = g1.x - g0.x, gh = g1.y - g0.y;
2730
float aspect = scope_->AspectRatio();
2831
if (aspect > 0.0f && gw > 0 && gh > 0) {

‎src/scope/VectorScope.cpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ void VectorScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s)
9696
}
9797

9898
void VectorScope::DrawControls(Settings& s) {
99+
const float u = UiScale();
99100
int enc = s.vectorPQ ? 1 : 0;
100101
if (ImGui::Combo("Encoding", &enc, "Rec.709 (SDR)\0PQ (HDR)\0")) s.vectorPQ = (enc == 1);
101102
UiReset(s.vectorPQ, UiDefaults().vectorPQ);
@@ -130,7 +131,7 @@ void VectorScope::DrawControls(Settings& s) {
130131
UiTip("Outline the outermost chroma excursions - the source's actual color "
131132
"footprint - even where the trace is too faint to see.");
132133
if (s.vectorExtents) {
133-
ImGui::SameLine(); ImGui::SetNextItemWidth(110);
134+
ImGui::SameLine(); ImGui::SetNextItemWidth(110 * u);
134135
ImGui::SliderFloat("##exop", &s.vectorExtentsOpacity, 0.0f, 1.0f, "opacity %.2f");
135136
UiReset(s.vectorExtentsOpacity, UiDefaults().vectorExtentsOpacity);
136137
}

‎src/scope/WaveformScope.cpp‎

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ void WaveformScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s
262262
const float top = f.graphP0.y, bot = f.graphP1.y;
263263
const float left = f.graphP0.x, right = f.graphP1.x;
264264
const float graphW = right - left, graphH = bot - top;
265+
const float u = UiScale();
265266
auto inRange = [&](float y) { return y >= top - 0.5f && y <= bot + 0.5f; };
266267

267268
const ImVec4 gc = ImVec4(s.graticuleColor.x, s.graticuleColor.y, s.graticuleColor.z, s.graticuleOpacity);
@@ -284,7 +285,7 @@ void WaveformScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s
284285
for (int m = 2; m <= 9; ++m) {
285286
double n = dec * m; if (n > 10000.0) break;
286287
float my = NitsToScreenY(n, f, s);
287-
if (inRange(my)) dl->AddLine(ImVec2(left - 20.0f, my), ImVec2(left, my), colMinor, 1.0f);
288+
if (inRange(my)) dl->AddLine(ImVec2(left - 20.0f * u, my), ImVec2(left, my), colMinor, 1.0f);
288289
}
289290
}
290291

@@ -298,7 +299,7 @@ void WaveformScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s
298299
else snprintf(lab, sizeof(lab), "%g", dec);
299300
ImVec2 ts = ImGui::CalcTextSize(lab);
300301
float ly = std::max(y - ts.y * 0.5f, top);
301-
dl->AddText(ImVec2(left - 4.0f - ts.x, ly), colText, lab);
302+
dl->AddText(ImVec2(left - 4.0f * u - ts.x, ly), colText, lab);
302303
}
303304

304305
// Extents white envelope line (style 1).
@@ -347,9 +348,9 @@ void WaveformScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s
347348
// Label sits inside the plot at the left edge, matching the graticule
348349
// labels' side (those live outside; keeping this inside avoids overlap).
349350
char lab[32]; snprintf(lab, sizeof(lab), "%.0f", s.refLines[i].nits);
350-
dl->AddText(ImVec2(left + 6.0f, y - 14.0f), col, lab);
351+
dl->AddText(ImVec2(left + 6.0f * u, y - ImGui::GetFontSize() - 1.0f * u), col, lab);
351352

352-
if (hovered && io.MouseClicked[0] && fabsf(io.MousePos.y - y) < 5.0f) draggingRef_ = (int)i;
353+
if (hovered && io.MouseClicked[0] && fabsf(io.MousePos.y - y) < 5.0f * u) draggingRef_ = (int)i;
353354
}
354355
if (draggingRef_ >= 0) {
355356
if (io.MouseDown[0] && draggingRef_ < (int)s.refLines.size())
@@ -362,8 +363,8 @@ void WaveformScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s
362363
char lab[32];
363364
FormatNits(std::clamp(ScreenYToNits(io.MousePos.y, f, s), 0.0, 10000.0), lab, sizeof(lab));
364365
ImVec2 ts = ImGui::CalcTextSize(lab);
365-
ImVec2 tp(io.MousePos.x + 14.0f, io.MousePos.y - ts.y * 0.5f);
366-
if (tp.x + ts.x > right) tp.x = io.MousePos.x - ts.x - 10.0f;
366+
ImVec2 tp(io.MousePos.x + 14.0f * u, io.MousePos.y - ts.y * 0.5f);
367+
if (tp.x + ts.x > right) tp.x = io.MousePos.x - ts.x - 10.0f * u;
367368
tp.y = std::clamp(tp.y, top, bot - ts.y);
368369
dl->AddText(ImVec2(tp.x + 1, tp.y + 1), IM_COL32(0, 0, 0, 200), lab);
369370
dl->AddText(tp, IM_COL32(235, 235, 235, 230), lab);
@@ -393,6 +394,7 @@ void WaveformScope::DrawOverlay(ImDrawList* dl, const ScopeFrame& f, Settings& s
393394
}
394395

395396
void WaveformScope::DrawControls(Settings& s) {
397+
const float u = UiScale();
396398
ImGui::RadioButton("Luminance", &s.waveMode, 0);
397399
UiReset(s.waveMode, UiDefaults().waveMode);
398400
UiTip("One trace of overall brightness (luminance) per column."); ImGui::SameLine();
@@ -411,7 +413,7 @@ void WaveformScope::DrawControls(Settings& s) {
411413
ImGui::PushStyleColor(ImGuiCol_Button, c);
412414
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, c);
413415
ImGui::PushStyleColor(ImGuiCol_ButtonActive, c);
414-
if (ImGui::Button(names[i], ImVec2(26, 26))) s.channelEnabled[i] = !s.channelEnabled[i];
416+
if (ImGui::Button(names[i], ImVec2(26 * u, 26 * u))) s.channelEnabled[i] = !s.channelEnabled[i];
415417
UiReset(s.channelEnabled[i], true);
416418
ImGui::PopStyleColor(3); ImGui::PopID();
417419
if (i < 2) ImGui::SameLine();
@@ -428,7 +430,7 @@ void WaveformScope::DrawControls(Settings& s) {
428430
"trace is too faint to see - as colored points or a thin envelope line.");
429431
if (s.waveExtents) {
430432
ImGui::SameLine();
431-
ImGui::SetNextItemWidth(150);
433+
ImGui::SetNextItemWidth(150 * u);
432434
const char* styles[] = { "Colored points", "White envelope line" };
433435
ImGui::Combo("##extstyle", &s.waveExtentsStyle, styles, 2);
434436
UiReset(s.waveExtentsStyle, UiDefaults().waveExtentsStyle);
@@ -440,7 +442,7 @@ void WaveformScope::DrawControls(Settings& s) {
440442
UiTip("Read every source pixel for the extents even when Quality samples "
441443
"a coarser grid, so single-pixel peaks can't slip through.");
442444
}
443-
ImGui::SetNextItemWidth(150);
445+
ImGui::SetNextItemWidth(150 * u);
444446
ImGui::SliderFloat("Extents opacity", &s.waveExtentsOpacity, 0.0f, 1.0f, "%.2f");
445447
UiReset(s.waveExtentsOpacity, UiDefaults().waveExtentsOpacity);
446448
}
@@ -457,17 +459,17 @@ void WaveformScope::DrawControls(Settings& s) {
457459
UiTip("Smooth the trace across neighboring columns (Resolve-style low pass): "
458460
"exposure bands become easier to read, single-column spikes fade.");
459461
if (s.lowPass) {
460-
ImGui::SameLine(); ImGui::SetNextItemWidth(120);
462+
ImGui::SameLine(); ImGui::SetNextItemWidth(120 * u);
461463
ImGui::SliderFloat("Strength##lpa", &s.lowPassAmount, 0.0f, 1.0f, "%.2f");
462464
UiReset(s.lowPassAmount, UiDefaults().lowPassAmount);
463465
}
464466

465467
// Reference lines: custom count, log drag (shift = 10x finer), thickness.
466468
ImGui::SeparatorText("Reference lines");
467-
ImGui::SetNextItemWidth(120);
469+
ImGui::SetNextItemWidth(120 * u);
468470
ImGui::SliderFloat("Thickness", &s.refLineThickness, 1.0f, 6.0f, "%.1f px");
469471
UiReset(s.refLineThickness, UiDefaults().refLineThickness);
470-
ImGui::SetNextItemWidth(120);
472+
ImGui::SetNextItemWidth(120 * u);
471473
ImGui::SliderFloat("Opacity##ref", &s.refLineOpacity, 0.0f, 1.0f, "%.2f");
472474
UiReset(s.refLineOpacity, UiDefaults().refLineOpacity);
473475
ImGuiIO& io = ImGui::GetIO();
@@ -481,7 +483,7 @@ void WaveformScope::DrawControls(Settings& s) {
481483
ImGui::SameLine();
482484
float v = (float)s.refLines[i].nits;
483485
float speed = io.KeyShift ? 0.02f : 0.5f; // hold Shift for 10x-finer control
484-
ImGui::SetNextItemWidth(150);
486+
ImGui::SetNextItemWidth(150 * u);
485487
if (ImGui::DragFloat("nits", &v, speed, 0.0f, 10000.0f, "%.2f", ImGuiSliderFlags_Logarithmic))
486488
s.refLines[i].nits = std::clamp((double)v, 0.0, 10000.0);
487489
UiReset(s.refLines[i].nits, defaultRef.nits);

‎src/util/UiReset.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
inline const Settings& UiDefaults() { static const Settings d; return d; }
1212

13+
// Current UI scale (window DPI / 96). Fixed pixel sizes in UI code multiply by
14+
// this; fonts and style paddings are rebuilt in main.cpp's ApplyUiScale.
15+
inline float& UiScale() { static float s = 1.0f; return s; }
16+
1317
inline bool UiResetClicked() {
1418
return ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right);
1519
}

0 commit comments

Comments
 (0)