Dark UI pack (title bar, menu bar, toolbar, status, dialogs) + PerMonitorV2, vector icons, touch hardening - #85
Closed
purrfecto114-lgtm wants to merge 100 commits into
Closed
Conversation
Touch: - pinch to zoom (anchored at finger center, same zoom steps as the mouse wheel) - two finger pan with inertia - two finger tap to reset the zoom - double tap on touch screens to toggle 1:1 / best fit - single finger touch keeps mouse semantics (click actions unaffected) - enlarged toolbar buttons and icons on touch devices Zoom UI: - zoom in / zoom out buttons on the toolbar - floating zoom controls (zoom out, zoom in, 1:1, best fit), available in fullscreen - View - Zoom Controls menu setting (show_zoom_controls ini, default on for touch) Other: - English and Simplified Chinese localization for the new UI - GitHub Actions release workflow (MSVC build, pre-release packaging) - version bumped to 1.1.0-beta.1 (pre-release)
…nder sources Upstream master references 28 dialog control ids in viv.c that are not defined in res/resource.h yet (renamed in code, resource.h not regenerated) and lists 5 render_*.c/h files in the vs2019/vs2026 projects that do not exist in the repository. - add the 28 missing IDC_* defines to res/resource.h (17 alias to the existing control ids, 11 new ids for statics that are localized at runtime, assigned 1055-1066) - assign the new ids to the corresponding controls in voidImageViewer.rc - remove the render_*.c/h references from vs2019 and vs2026 projects - bump _APS_NEXT_CONTROL_VALUE to 1067 This unblocks the Windows (MSVC) build used by the release workflow.
The localization sources were added upstream without updating the vs2019 project file, so localization_get_string/localization_init were unresolved at link time. Also verified vs2026 already includes the localization sources. (crt.c remains excluded: it is unreferenced min-CRT code.)
viv.c requires one of VERSION_X64/VERSION_X86/VERSION_ARM/VERSION_ARM64 to select the version target machine suffix. The Win32 configurations only defined WIN32 (the platform) and none of the VERSION_X* macros, so compiling for x86 hit #error unknown target machine. Added VERSION_X86 to all Win32 ItemDefinitionGroups in the vs2019 and vs2026 projects.
MSBuild resolves relative OutDir against the project file directory (vs2019\), not the workspace root, so the verify step could not find voidImageViewer.exe. Use %GITHUB_WORKSPACE% absolute paths instead.
…beta.1 The zoomin.ico/zoomout.ico icon files were generated and IDI_ZOOMOUT/ IDI_ZOOMIN were defined, but the ICON resource entries and the VS_VERSION_INFO bump were missing from voidImageViewer.rc, so builds embedded no zoom icons and reported version 1.0.0.15. - add IDI_ZOOMOUT / IDI_ZOOMIN ICON entries - FILEVERSION/PRODUCTVERSION 1,1,0,1 and version strings 1.1.0-beta.1
…ing one Deleting and re-pushing a tag converts its GitHub release to a draft. gh release edit --draft=false republishes it.
…d Chinese translation - unified NSIS installer: language selection dialog (English / SimpChinese) on the first page, remembered in HKCU and reused by the uninstaller - per language license data, option pages, messages; language is forwarded to the application via the new /language install option so it starts in the setup language - new Language setting in Options - General (auto / english / chinese) applied immediately: menu, toolbar and zoom control tooltips refresh live - new language ini setting (auto|english|chinese) and /language command line option - rewrote the Simplified Chinese localization in plain wording, translated the file type descriptions (were English) - fixed NSIS Chinese option pages encoding (UTF-16LE BOM, they garbled on non-Chinese systems via CP_ACP conversion) - force /utf-8 in the vs2019/vs2026 projects so Chinese literals no longer rely on ansi passthrough - setup now offers to run the app on the finish page and installs Changes.txt - release workflow builds and publishes both installers
makensis resolves the OutFile path relative to the .nsi script location, so the setup executables land in nsis\ and not in the repository root.
…ar, cleaner menus - zoom presets are now a dense table: each step grows the image by 1% (1.01x) instead of the old coarse jumps (mouse wheel, toolbar, menu and pinch) - wheel deltas step proportionally: fast flicks zoom further in one event - pinch zoom follows the fingers at the same 1% granularity - the status bar shows the real zoom percent of the image and updates live - fixed tearing while dragging: the exposed strip is repainted immediately (UpdateWindow after ScrollWindowEx), stale pixels can no longer be scrolled back into the image - all painting now goes through a cached backbuffer and is presented in a single blit (rtl layouts keep direct painting) - scrolling is limited to the image viewport, it no longer smears the status bar and toolbar - the floating zoom controls gained a close (X) button (re-enable from View - Zoom Controls) - menu cleanup: one View - Zoom submenu with all zoom commands (in, out, 1:1, best fit, reset), best fit is visible again, right click menu gained a zoom section
… hover - File > Save As (Ctrl+S): re-encode the current image as PNG, JPEG (quality 90) or BMP with the built-in GDI+ encoders. zero new dependencies or resources, in-memory rotations are included. - default navigation sort is now filename natural order ascending (was: date modified, newest first), matching windows explorer so next/previous is intuitive. saved settings are not overridden. - the floating zoom controls bar highlights the hovered button, repainting only that button. no bitmap icons were added. - localization_id_t widened to unsigned short (261 strings now), new strings added to both languages.
…jump to limits - the zoom ladder is now truly geometric: each step multiplies the best fit size by 1.01x. (max_zoom was based on the native image size when fit < native, so every "1%" step actually grew windowed-downscaled photos by 3-4%+ and pinch zoom over-shot the finger movement: a 2x finger spread rendered ~4.5x, fast pinches exploded the zoom level) - pinch zoom hardening: gesture distances are sanity checked (some windows builds report them with a multi monitor offset or negative wrapped around), per message ratios are clamped to [0.5,2.0], and all accumulated steps are applied in one pass per message (a fast pinch used to run up to 278 full repaints per gesture message) - fixed the zoom buttons: _viv_zoom_in() still passed the pre-beta.3 MAKEWPARAM-encoded delta (120<<16) into the new proportional stepping, so every toolbar / floating bar click jumped straight to the maximum or minimum zoom instead of stepping - a wheel notch / zoom button click now steps ~10% (10 x 1% steps) and the steps also apply on top of the 1:1 transition so gestures keep tracking the fingers from 1:1 mode - the floating zoom bar is now just zoom out + zoom in buttons drawn with the shared toolbar icons; the 1:1 / best fit / close buttons are gone (the commands stay in the View - Zoom menu and the right click menu, the bar can be hidden from View - Zoom Controls) - fixed the JPEG save encoder quality GUID: quality 90 was silently ignored and windows saved at its default quality - two finger tap now refreshes the status bar zoom display - status bar: guard zero zoom factors for extreme aspect ratio images - localization: LOCALIZATION_ID_INVALID now tracks LOCALIZATION_ID_COUNT instead of a stale literal (261) after removing three unused zoom bar tooltips
- the aspect ratio can no longer change while zooming: the pan && scan feature that stretched x and y independently (19 confusing menu entries, 18 numpad shortcuts) is removed. the zoom ladder always scales both axes together. - fixed the ~0.5s zoom lag at deep zoom: magnified StretchBlt ignores the clipping region and stretched the entire (up to 16x fit = ~276 megapixel) destination on every paint. the whole-destination path is now only taken when the destination is fully on screen, otherwise the clip limited stretch bounds the work by the client area. - the status bar shows a single zoom percent instead of two x/y percents that read as if the aspect could drift. - regrouped the View menu: ui toggles and layout presets moved into a new View - Layout submenu, fullscreen/slideshow/refresh sit together, the pan && scan submenu is gone (13 -> 11 top level entries, 19 rows less). - refreshed misleading chinese translations (allow shrinking, fill window, interpolation filters, slideshow status) and simplified the status format. - added a regression test suite under tests/: zoom math (aspect invariant across geometries and levels, geometric 1.01x ladder, pinch step roundtrip, paint work bound, single percent status) and menu structure (pan&scan removal, menu table shape, localization alignment). - verified: mingw compile diagnostics identical to the beta.5 baseline (350 entries, zero new), windres resource differs only in the 4 version bytes, localization arrays align positionally (242 = 242 = 242). - note: custom hotkeys for commands moved into View - Layout reset once (hotkeys are stored by menu path); defaults are unaffected.
- status bar zoom percent fixed: beta.6 passed five varargs (a double pan position first) to the one-%d format, so va_arg(int) read the double's bits and the bar showed garbage like -755914244%. the call now passes exactly one int: render/native pixels, 1:1 reads 100%. - zoom range: the cap is 16x the LARGER of best fit and native (upstream semantics), reached through a longer 1024-entry 1.01x ladder. small images now max out at exactly 1600% (was a confusing 1590%) and photos larger than the window keep deep zoom (a 4000px photo in a 1600px window topped out at 477% before). the live top position is measured (_viv_zoom_pos_max) so the wheel never spins in a dead zone, and the ladder stays clamped on window resizes. - dead pan&&scan zoom state fully removed. - new dark mode following the windows theme (win10 1809+, graceful on older): dark title bar, menus, status bar, zoom controls and canvas default; automatic/light/dark in Options - General; live switching via WM_SETTINGCHANGE; high-contrast always keeps the light ui. - regression tests: status vararg safety, ladder cap/dead-zone model, dark mode wiring, version consistency. new scheduled 'tests' workflow runs them on every push and daily.
- the reachable top of the zoom ladder is cached behind an O(1) signature (its only inputs: the image, the viewport and the layout settings). a wheel tick or a window resize no longer re-walks the 1024 entry ladder and no longer saves/restores the zoom globals on every event. - leaving 1:1 mode with the wheel now finds the re-entry zoom level with a binary search: ~10 render measurements instead of up to 1024 full render-size computations (the render size is monotonic in the ladder position, so the result is provably identical). - the canvas background brush is cached across paints and rebuilt only when its color changes (config or dark theme switch): a paint no longer allocates and frees a GDI brush each frame. - regression tests: binary search == linear scan equivalence across every tested geometry (with a measurement count bound), the ladder-top cache signature model, and source wiring checks for the new code paths. - verified: mingw compile diagnostics identical to the beta.7 baseline (297 normalized entries, zero new), windres resource build with the 1.1.0-beta.8 version string, both python test suites pass.
- read the system theme from the documented AppsUseLightTheme registry value (the uxtheme ordinal 132 probe stays as a fallback: it returns wrong values on some windows 10 1903+ builds) - follow the theme live in more situations: any WM_SETTINGCHANGE and WM_THEMECHANGED re-read it, high contrast flips update the chrome, and an elevated viewer now receives the broadcasts (uipi filter) - cache the detection (probed once, dropped on setting changes): the paint path used to probe on every status bar custom draw - repaint on a theme flip is gated on the dark state actually changing - tooltips now follow the dark palette (zoom controls + toolbar hints), and the tint survives the toolbar recreation on a language switch
- the dialogs now follow the dark mode: options (and its pages), about, rename, edit key, custom rate and the everything search used to pop fully light dialogs out of a dark window - every dialog gets a dark title bar, the dark explorer control style, dark text and backgrounds for its statics, edits and lists, and a dark background fill (shared dispatcher, zero cost when light) - the options navigation follows: dark tree item colors, dark tab controls, and the light tab dialog texture is skipped while dark - the about dialog paints its upper area with the dark palette - the dark brush is created once and reused; deleted at kill
- View > Backdrop selects what shows under the transparent pixels of PNG/GIF/WebP images: follow the window background (the default and the previous behavior, dark ui aware), black, white, a custom color or a checkerboard. remembered in the ini. - the backdrop is painted on the load thread for every frame that has alpha, so it is built for that path: the brushes are cached (nothing allocated per frame) and the checkerboard is a pattern bitmap that gdi tiles in a single FillRect call - this also replaces the old per-frame CreateSolidBrush/FillRect/ DeleteObject chain under alpha frames: one cached-brush FillRect now - the installer always shows the language selection dialog now (preselecting the remembered language): an upgrade install used to silently reuse the remembered language with no visible way to switch
- fixed the zoom stall when zooming in from around half the image size: crossing the half-size mipmap boundary switched from the cheap half mipmap to the full image running the slow shrink filter (HALFTONE default) over every pixel on every paint. the half mipmap is now magnified up to the full size instead: the full image is only used when it is actually magnified (4x cheaper, looks the same) - big images with an embedded exif thumbnail (cameras, phones) now appear instantly as a low resolution first frame posted before the full decode starts, and sharpen when the full frame arrives. images without a thumbnail take the normal path with zero extra cost - the preview never enters the last-image slot (the previous image stays intact for switching back)
- fix fatal startup bug from beta.12: the progressive preview loaded gdiplus by a nonexistent export name (GdipGetImageThumbnailImage vs the real GdipGetImageThumbnail) and treated the miss as fatal, so the exe died at os_init and the /install self-install step failed, leaving nothing in the installed programs list. the load is now optional (plain GetProcAddress) with the real name and parameter order. - regroup the image right click menu: zoom commands in one zoom submenu, slim slideshow rate ladder (full list stays in the menu bar), paste joins the copy group. - complete the paste groundwork: ctrl+v now displays an image copied to the clipboard (CF_DIB primary, CF_BITMAP fallback, private dib copy, graceful no-filename state). - complete the EN localization table comments for the nine association description slots.
…log fix zoom in/out buttons (toolbar, keyboard, configured mouse zoom action) now step whole 10 percents: a zoom that is not a multiple of 10 first snaps to the nearest multiple of 10 (the literal spec, even when the snap moves against the click direction), then every click changes 10 percent. the mouse wheel and pinch gestures keep the smooth proportional ladder stepping. the target percent is found with a binary search over the monotonic ladder (~10 measurements, same proof as the beta.8 1:1 exit); an exact 100% enters the pixel perfect 1:1 mode with the _viv_view_1to1() semantics; a zoom out click at the ladder floor is a no-op instead of snapping up; and when the ladder physically cannot display the target (values are skipped past ~120%, ~14 apart past 1400%) the click jumps to the next multiple of 10 in its direction so repeated clicks never oscillate - verified by a mirrored python model over five geometries in zoom_math_test.py. the zoom percent is now always visible as the leftmost status bar pane (the old three second temporary text is gone). clicking it (hand cursor via WM_SETCURSOR + SB_GETRECT) opens a small Set Zoom dialog to type an exact percent 1..1600; the frame counter pane (its index shifted) is now found dynamically; the pane is measured and dark-painted like the other parts. two new strings are translated in both languages (254/254/254). fixed a silent beta.10 bug: the shared dark dialog color handler was placed inside switch(msg) before the first case label - unreachable in c (gcc kept warning 'statement will never be executed'), so dialogs never actually painted backgrounds and text dark; only the title bar and control themes did. the dispatcher now runs first on every message in all 11 dialog procs, and the jump to dialog is wired too (it was missed in the beta.10 audit). machine compatibility: the new code only uses win2000-era apis (SB_GETRECT, IDC_HAND, PtInRect, DialogBox); the vcxproj stays byte-identical to upstream's compatibility settings; the fatal dynamic load list still only contains upstream-inherited ancient gdiplus exports.
the release title and the notes header hardcoded '(BETA)' even for release candidates. the phase suffix is now derived in the tag/version step (-rc. -> ' (RC)', -beta. -> ' (BETA)', final -> ''), the pre-release blurb no longer hardcodes 'beta', and the notes gain a 'recent changes' section summarizing beta.11..rc.1 above the detailed history (the current rc.1 release was patched via the api to match).
…, arp an external code review reported 27 findings; each was verified against the source and the win32 api documentation before fixing. the real ones: - touch gestures never worked: the os_SetGestureConfig wrapper had its parameter types shifted (DWORD id / UINT count where winuser.h wants UINT cIDs / UINT cbSize) so the call configured zero gestures with a garbage size, and the gesture ids were wrong (2 is GID_END). now passes 3 configs with sizeof(GESTURECONFIG) and the real ids (GID_ZOOM 3, GID_PAN 4, GID_TWOFINGERTAP 6; pan wants inertia and blocks single finger pan and gutters). - the app never appeared in the installed programs list: nothing wrote the add/remove programs key (the setup delegates to the exe /install, and the exe only wrote file associations). /install now writes the uninstall key (hklm when admin, hkcu otherwise) and /uninstall removes it from both hives; the nsis .onInit strips the now quoted uninstall string before path parsing. - security: all 10 fd.cFileName copies bounded to MAX_PATH (the review found one, the sweep found nine more), string_get_word takes a buffer size (all 15 callers updated), everything ipc WM_COPYDATA replies are validated field by field (_viv_copydata_read + _viv_everything_item_to_fd) before trusting sender offsets, and the pasted CF_DIB checks GlobalSize before reading its bits. - webp transparency now honors the backdrop: the frame is composited over the backdrop into a 24bpp dib section, exactly like the gdi+ path, instead of being pre-flattened onto the window background. - webp zero duration frames can no longer stall the frame skip loop; the mipmap stop condition compares mip_high (wide images at 50..99% were one level too blurry); status pos/rgb panes test their content instead of the pointer; save-as refuses the progressive preview thumbnail; stale preload events can not write past the frame array; GetLayout loads from gdi32 (was user32, never resolved). debunked findings (left unfixed on purpose): exif orientations 5-8 do swap width/height (two sites), the shipping release configs have /utf-8 and default /GS on. tests: menu_structure gains t_review_fixes (28 new guards), the mipmap boundary assertion now mirrors the fixed axis; tests.yml gains a windows compile job so master pushes build both shipping configurations before any tag.
the transplanted build commands lost one level of backslash escaping (a trailing \" instead of \\" in the /p:OutDir argument), so cmd passed an unterminated quote into msbuild and MSB4184 rejected the path. the lines are now byte-identical to the working release.yml build steps.
…unds fixes every re-review claim verified against evidence before touching code: - rejected: GID_TWOFINGERTAP is 6 in winuser.h (5 is GID_ROTATE, 7 is GID_PRESSANDTAP) - verified against the mingw-w64 12.0 header and microsoft learn; the rc.2 gesture ids were already correct. a comment now marks the value so the claim does not resurface. - rejected: the last_stretch_mode 4701 suppression is harmless (both reads are guarded by did_set_stretch_blt_mode / set-before-read). - fixed: webp transposed exif orientation (5-8) now reports swapped dimensions in the first frame reply and swaps the per frame mipmap dims, mirroring the gdi+ path (status bar and mipmap selection used the un-rotated canvas size). - fixed: the quoted uninstall command copy is bounded to the remaining buffer space (a max length install path could write one wchar past). - fixed: the jump-to modal pump re-posts a consumed WM_QUIT so the main loop terminates instead of blocking in WaitMessage forever. - fixed: a zero file drop no longer clears the current playlist. - hardened: the first frame reply clamps a zero frame count to one. tests: t_review_fixes_round2 (8 guards) + t_version rc.3; both suites pass. local mingw diagnostics identical to the rc.2 baseline (zero new warnings), windres and makensis smoke pass.
…todetect, rc mojibake, repo hygiene
windows-latest is mid-migration to the Windows Server 2025 + VS2026 image, and that image has no v143 toolset (MSB8020) and no NSIS. Pin the shipping compile to windows-2022 (v143, vs2019 project) and add windows-2025 (v145, vs2026 project) as the forward-compatibility and image-drift leg. The daily schedule now runs only the compile matrix (the drift detector): the python suites are already covered by every push, PR and tag. Tag pushes now run tests.yml as well. Actions are pinned to exact commit SHAs (checkout v7.0.1, setup-python v7.0.0, setup-msbuild v3).
…obber
Release pipeline hardening (user-approved D5/D7):
- validate: effective tag (input or pushed ref) passes a whitelist
regex ^v\d+\.\d+(\.\d+){1,2}(-(beta|rc)\.\d+)?$ (3 or 4 numeric
segments), checks out the tag, verifies HEAD is the tag commit and
that the tag matches src/version.h; all user input reaches shells
via env only, after validation
- tests: both python suites must pass on the tag's commit before
anything is built (release now depends on tests by construction)
- build: windows-2022 pinned, contents:read only; NSIS choco fallback
pinned to 3.12.0; SHA-256 of every asset exported as job outputs
- publish: contents:write only here; artifact re-downloaded and every
hash re-verified against the build job outputs (transport
integrity); gh release upload --clobber removed and replaced with a
refusal to overwrite an existing release; prerelease flag derived
from the version phase; notes generated from Changes.txt at the tag
(the embedded beta.1..rc.3 history in the workflow is gone);
sha256.txt published as a release asset
- workflow-level permissions narrowed to contents: read
…l crt.c Second review round, user-verified leftovers (plus one mirror the audit found): - the two frame array allocations now multiply through safe_size_mul (the wrench was exported by safe_size.h since rc.2 and used zero times): a 32 bit sizeof(_viv_frame_t)*count could wrap before reaching the allocator, allocating a small buffer that the frame writes then overflow - the everything FILE_SIZE-indexed branch requested SIZE *and* DATE_ MODIFIED, duplicating the DATE_MODIFIED branch right below it; now it requests SIZE only. the audit found the same typo in BOTH search senders (the second one at _viv_send_everything_search was missed by the report) - src/crt.c is deleted: it is GPL v2 (David Carpenter 2017) in an MIT repository, uncompiled by all three projects but including viv.h, so a single accidental project re-add would import the GPL into the shipping binary. the fulfilled upstream TODO comment is removed too - .gitignore gains __pycache__/ and *.pyc (the one uncovered class)
Upstream 1.3.2 -> 1.6.0 (tarball sha256 93a852c2..., provenance recorded in
libwebp/VERSION.imported) merged with the directory pruning in ONE change
so the file-list audit is paid once.
Correction to the approved plan, found while executing it: the plan
assumed the old 89-file project list compiled no encoder code (no file
under src/enc/). In fact it compiled 24 encoder-side files living in
src/dsp and src/utils (cost*, enc*, lossless_enc*, ssim*,
bit_writer_utils, huffman_encode_utils, quant_levels_utils) which also
#include src/enc headers - so pruning src/enc with the old list would
have broken the build, and encoder code was shipping inside the exe.
This import switches the projects to upstream's own decode-only file
set (66 files: dec 10 + demux 2 + dsp decode 44 + utils decode 10, as
classified by libwebp 1.6.0's Makefile.am/CMakeLists): the encoder is
now genuinely absent from both the tree and the binary. The only new
file is src/dsp/lossless_avx2.c (AVX2 lossless prediction, active on
MSVC via the WEBP_MSC_AVX2 path in cpu.h).
Pruned from the tree: webp_js/ examples/ imageio/ swig/ man/ gradle/
(+ gradle top-level files) infra/ extras/ sharpyuv/ src/enc/ src/mux/
and the 24 encoder-side files. Kept: tests/ doc/ cmake/ and all
src/{dec,demux,dsp,utils,webp} headers. Include-closure scan proves
the kept set references nothing pruned.
Verification:
- the 66-file set was compiled natively on linux and decodes a
lossless still byte-exact, a lossy still, and a two-frame animation
byte-exact through the WebPAnimDecoder API (the exact viv surface)
- src/webp.c and src/viv.c compile clean against the new headers
(mingw diagnostics identical to the pre-import baseline)
- both vcxproj lists updated (66+14), XML valid, every referenced
file exists
the 1.1.11 font round recorded the design (the about title derives its larger face from the message font) but the height stayed the literal 32 (a 96 dpi design point: the 8/3 proportion collapsed to 2/3 at 150% and 1/2 at 200%), the font site was the only one left on the unsuffixed forms (the wide pipeline borrowed from the project unicode define), and the handle was created once and cached for the process lifetime (a dpi change between two about opens kept the stale face). the fix derives the title end to end from the live dialog font: wm_getfont -> getobjectw into a logfontw -> the height scaled by 8/3 -> createfontindirectw, rebuilt at every open, the old handle dying only after the control took the new face, a creation failure keeping the previous face drawing. gates: three suites green locally (zoom_math / menu_structure / simulation, 775+ checks incl. the new round51 guards); gcc syntax check not runnable in this sandbox (no cross compiler - ci owns the compile gate); no push (red line: push only after all four gates, tag/release user-gated).
the dark mode field report on the about dialog: the band chrome (the two separator lines and the button strip face) was painted at runtime by a wm_paint case - three fillrect passes whose band edges were pixel literals (48 and 46 at the 96 dpi design point, scaled by the system dpi fraction), while the dialog template positions the buttons in dialog units: two coordinate systems that only met at 96 dpi. at any other scale (and after the b41 round made the dialog font per-dpi) the lines drifted off the button strip and the band boundaries crossed the controls - the rendering the report called temporary drawing that never reached the resource template. the fix moves the band chrome into the resource template: - the geometry: two 1-du line controls (idc_aboutline1, idc_aboutline2) and one 24-du strip control (idc_aboutband) declared before the buttons in the idd_about template, so they sit under them in the z order. the template grid owns the band layout now: the lines ride the button strip at every dpi, in both themes. - the colors: the wm_paint case is gone. the control color replies carry the same palettes the passes used (the dark 0x45/0x70 separator lines and the 0x25 strip face, the light 0xec lines and the white strip), the banner keeps its 1.1.11 face, and the dialog face itself answers through the WM_CTLCOLORDLG pipeline. - the about color replies run before the shared dark dialog handler (its flat 0x20 static reply would flatten the line and strip faces). guards: t_about_band_round64 (11 checks, red first); the version, the changelog-top, the about-paint and about-band guards from the earlier rounds moved to the new shapes; the simulation glyph-budget guard now skips the blank label rows (the band controls carry geometry and color, no glyphs). three suites green.
…ments only). viv.c measured at 21,129 lines / 536 functions / 159 file-scope statics with the window procedure at 2,208 lines; the dialectic resolves to a viv_state.h extern layer first, then eleven domain slices in measured-closure order (recent -> everything -> playlist -> render -> anim -> fullscreen -> dialogs -> load -> chrome -> view -> residual), pure physical moves only - no function body rewritten, renamed or reordered. the guard strategy: the three suites will read viv.c plus every viv_*.c spliced in dictionary order so the existing 140+ shape guards keep covering the moved code without edits. the r69 guards pin the documents, the discipline, the monolith baseline (no growth allowed from here) and the not-yet-existing state layer. no C code changes in this round
…2-line core plus eleven domain modules (recent, playlist, load, anim, render, chrome, dark, dialogs, view, install, menu) and a viv_state.h shared-context layer, landed in one pass per the recalibrated direction. pure physical moves only - every function body is byte-identical; the only text edits are static-prefix strips on the 163 cross-module exports and the 110 state externs, while 52 single-domain variables moved with their consumers and keep static. the guard suites now read viv.c spliced with every viv_*.c (plus viv_state.h for the macro guards) so the 140+ shape guards cover the moved code with only optional-static recalibrations; the r69 monolith-baseline guards are recalibrated to the split landing and new r70 guards pin the residual under 5,000 lines, every domain under 3,000, the pure-move window (21,130 to 21,7xx spliced lines, declarations only) and the props registration. performance and size are unchanged by construction: the projects already compile with /GL whole-program optimization so cross-unit inlining survives the split, and exported symbols never enter the release binary. also restores the Changes.txt CRLF discipline that the r69 edit silently dropped (caught by this suite). the compile gate runs on this commit before any rc.3 version bump.
…s tore the preprocessor conditionals: the region builder worked from col-0 directive items only, so the indented nested defines inside the #ifdef VERSION_TARGET_MACHINE chain were absorbed into their outer directive and the chain split mid-way - viv.c kept an orphan #else (C1019) while viv_state.h carried an unbalanced head - and the #ifdef VERSION_X86 block wrapping the unicows path lost its opener to the state layer with the wrapped code left behind. regions are now maximal runs of blank-or-directive lines (any indent) that only join a region when their conditional stack balances inside it: a conditional that wraps real code can never be torn from it. the full machine chain and the association-count define moved complete, the unicows block stays whole in viv.c, and the generator now reads the pre-split viv.c from git (dca43ef) instead of the working tree - the tree is the split itself, and a regenerate over it had already produced garbage stage output caught before any deployment. pure-move conservation re-verified from the git baseline: zero errors across byte conservation, brace balance, reference resolution and linkage checks.
…er carried an include guard - upstream survived because every translation unit included it exactly once. the R70 layering changed that: each domain .c includes viv.h directly and again through viv_state.h and the domain headers, so the unguarded command-id enum redefined itself out of every compile (C2365/C2086 across all units). the guard is the standard idiom; no other text in viv.h moves.
…d-context header printed its types before its macros, but the key-list type sizes its two arrays with _VIV_COMMAND_COUNT - a macro that measures the command table (which stays in viv.c as a state definition) - so the type parsed against an undeclared identifier (C2065, then C2229 zero-sized array). the macros now print first: they are measurement definitions the types consume, and macro bodies only expand lazily at use sites that already see the full header. the verifier gained an ordering check: every shared macro used inside a struct or enum body must be defined earlier in the header, so this class of mistake can not reach CI again.
…move left four cross-unit symbols unresolved: the command table extern printed after the key-list type that sizes its arrays with the measurement macro (the identifier parsed undeclared, then the arrays sized zero), and three shared states - the status pane text store, the cursor visibility flag and the options page id table - kept their static prefixes in the core while chrome and dialogs referenced them. the extern now precedes the type that consumes its measurement, the three states dropped static and joined the shared-state section (113 externs now), and the guard suite pins all four: the macro-define-before-use and extern-before-use orderings, and the three exports without static. the pane text store guard followed its subject out of static. the libwebp guards follow the housekeeping that already landed (the pruned build systems, docs, fuzzers and lint configs; configure.ac and version.imported stay as the pins the suite reads), and a new tools/link_scan.py audits the link stage the compile cannot see: every extern owns exactly one definition and no static leaks across units - 113/113 clean, zero leaks.
…s sizeof on an unsized extern to zero - warning c4034, and error c2229 when the zero sizes a struct member - so the five table-count macros the state layer carried could not measure their tables from any other translation unit: the key-list struct sized its pointer arrays to zero (the compile error), and the animation-rate clamp in the anim domain, the slideshow preset scan in view and the association walks in playlist, dialogs and install all silently looped zero times (the warnings the compile error masked). the macros now carry their literals - 154 commands, 21 animation rates, 17 slideshow presets, 3 options pages, 11 association extensions - and each defining translation unit pins its literal to the real table with a negative-subscript typedef assert (unique names, no platform macro dependency), so a table edit that forgets the count fails the build instead of shipping a silent zero. also repairs a newline join the split generator fumbled: os.h lost the line breaks between the combobox theme declaration and its successor, so the light window theme declaration ended inside a line comment (c4013, an implicit-int call site in chrome); every bare-cr join in os.h and os.c is back to crlf. the guards pin the whole class: the five macros are sizeof-free, the five typedef asserts are present, the light theme declaration stands on its own line, and no bare-cr join remains.
…ociations. version 1.1.12-rc.3, build 45. the changelog and the readme tell the full split story - the one-shot landing, the pure-move discipline, the five ci catches that shaped it (the exports, the counts, the fumbled line join), the libwebp housekeeping and the link-stage audit tool - with the counts as measured: 4,715 residual core lines, 113 shared-state externs, 62 pruned libwebp files. the setup association page grows the emf and wmf checkboxes in both languages (second column, thirteen fields): the metafile pair joined the exe-side association table in 1.1.12 but the page never offered them, so a setup install silently skipped the two; the exe-side switches were already generic (/emf /noemf /wmf /nowmf) and the guards pin the page fields, the column geometry and the switch forwarding in both languages.
…ite band returned with two new triggers - opening the options dialog and switching the theme - and the field screenshot showed the mixed chrome: a light caption over a dark dialog body. the root is a race, not a repaint miss: the system side repaints the frame and the comctl classes in light asynchronously (the app mode policy flush, the theme broadcast sweep) while the app level dark answer never flipped, so every re-apply was gated on was_dark != is_dark and never ran. the delayed recheck timer and the wm_themechanged handler now re-apply the chrome unconditionally (the sweep is idempotent), the options dark mode combo schedules the assert after its app mode flush, the options dialog re-runs its own full dark pass on a one shot timer so a creation moment that read an unsettled state heals itself, and the main window erases with the dark chrome face in the dark ui so a bypassing paint shows dark instead of the light class brush. the options navigation tree kept the light explorer class label gray on the dark face (a themed tree ignores the text color message) - the dark dialog children walk pins the tree face and label colors per pass now, the same values the initdialog applies, handed back to the system on the light flip. the rc.3 core line count corrected to 4,718 as measured. the guards pin the five new invariants and the recalibrated counts (dialog dark passes 13, explorer class applications 3, recheck timers 3, growth window 21,900).
…ne, every guard anchor kept. the whats-new section stopped duplicating changes.txt - it now carries the current rc in two tight bullets (the theme race root fix and the options tree contrast), one line per recent version (rc.3 the split with the 4,718 measured core, rc.2 the about template move, 1.1.11 the type system), and a pointer to the changelog for everything older; the 1.0.x era sections are gone (four-release-old news belongs to changes.txt). the still-current feature points moved to their owning sections instead of living as changelog prose: the below-fit pinch ladder joins the touch table, the dark-dialog completeness joins the canvas section, and the theme-flip line now states the rc.4 unconditional re-assert. the three suites pass on the compressed readme (the five pinned strings - the rc heading pair, the v143 adjudication pair and the runner matrix pair - all verified present), link scan stays 113/113. this round also audited every file deletion in the fork's history against the user's impact question: the 357 paxheaders files were tar-extraction junk, the 167-file libwebp shrink was the user-approved decode-only vendoring, the 62-file housekeeping is guard-pinned with copying, patents and authors kept, the five carpet-review files were zero-reference stragglers, and the vs2005/vs/minicrt/res-icon deletions are superseded build paths - zero current references anywhere, nothing to restore.
…re debts the rc series carried are paid. first, the touch gesture cluster is home in the view domain: viv_chrome.c carried the gesture engine (_viv_on_gesture), the touch click probe (_viv_is_touch_click) and the double click handler (_viv_touch_double_click) since the one-shot split, but chrome is window dressing and gestures are zoom and pan input - the spec's slice 10 said so all along. the five engine statics moved with the functions (byte-identical blocks, the only chrome symbol the cluster needed was the status temp-zoom refresh, which view already calls), and chrome no longer references the cluster at all. second, the window procedure is a domain module: _viv_proc was a 2,208-line switch; the case bodies are now 42 static per-message handlers in the new viv_wndproc.c (the 44 case labels - 41 WM_ messages, two internal _VIV_WM_ messages and the raw tablet query - collapse to 42 handlers because two label pairs share a body), the dispatch itself is 95 lines. every case body moved byte-identical: only the 34 case-exit break statements became explicit DefWindowProc returns (verified per line: 21 unbraced direct exits, 12 braced in-block exits, 1 post-block exit, zero inner breaks touched - the brace-depth walk is the discriminator). the symbols the handlers share with the core - the background brush (lazily created in WM_PAINT, released in _viv_kill) and the command line processor (WM_COPYDATA forwards to it, init consumes it) - are externed through viv_state.h, 113 -> 114 externs; the move-window drag reference, the animation-paint flag, the brush color and the drop-files helper moved as file statics into the domain. the core is 2,431 lines now, the new domain 2,530, both under their caps, the props register both new files exactly once (both vs2019 and vs2026 import the shared list), and link_scan stays clean at 114/114 with zero static leaks. third, the splice guard grew the file manifest: the test suite reads viv.c spliced with every src/viv_*.c (a glob), so a new domain module changed the measured total silently - the growth window was pinned against an invisible list. the manifest is pinned now (the core, the twelve domain modules, the state layer - 13 files), so adding or renaming a module without registering it fails the suite; this round's own props registration was caught exactly that way (the files.props uses lf endings, the first insert used crlf and silently missed). the segment guards that anchored on "case WM_" labels were re-anchored on the handler functions - the old anchors now hit the dialog domain's own WM_DPICHANGED/WM_THEMECHANGED copies first and one of them was passing against the wrong segment by accident. the growth window recalibrates to 22,300 (measured 22,056, +238 for the split overhead), the view domain cap to 3,300 for the gesture homecoming (3,166 measured, spec slice 10 includes the gestures), and the version pins in all three suites move to rc.5 / build 47 with the changelog and readme entries following the compressed whats-new discipline.
…he python suites and the link scan could not see. first, the touch double-tap path kept a bare break inside an if-block - the extraction generator classified break statements by brace depth, but break binds to the nearest loop or switch and an if-block catches nothing, so that one exit stayed a break in a function with no switch left (c2043, reported exactly once at wm_lbuttondblclk). the fixer now walks the statement context - the pending control keyword persists across lines because c89 puts the brace on the next one, and a semicolon inside the for-header parens does not reset it - and rewrites exactly the breaks with no loop or switch above them: one, matching the compiler's ground truth. second, the 0x2c4 handler name carried a trailing colon (the label parser stripped the colon before splitting the comment, so the tablet-query name never replaced the hex); both the definition and the dispatch line are _viv_on_wm_tablet_querysystemgesturestatus now. third, the identifier audit that only covered function calls missed everything that is not a call: the wm_gesturenotify and wm_gesture sdk fallback defines and the context-menu items table with its sizeof count macro lived in the viv.c core with no header declaring them, visible only inside the core's translation unit. all three moved into the wndproc domain (the only remaining consumer), and the audit now walks every identifier - macros, arrays, types - against the headers, reporting zero core-local dependencies left. the changelog and the guard pin the corrected exit count (35 rewrites plus the 18 unconditional tails = 53 defwindowproc returns, measured).
…nu is gone for good. five rounds of field reports lived on the seam where the system measured and painted the menu bar and the app patched around it - the white strip right of the last item (the system kept its light tail, the nc fill chased it, the theme races re-opened it), the pad capture that measured slot-minus-text and compounded through the two measurement systems (the field screenshots measured a 17px label gap in the light ui and a 94px gap in the dark ui, the same labels), and the flip sweeps that re-asserted the owner draw state. the remake owns the strip end to end: the window is created without a menu, the top bar is a client side child window (the new viv_menubar domain, 588 lines) that registers its own class and paints every pixel itself, the layout is one fixed air (4px at 96dpi, rescaled) plus the label extent at the menu font for both themes - which reproduces the system light layout within a pixel - and the popups are the same hmenu tree opened with trackpopupmenuex at the item rects. the keyboard entry points survive: alt + mnemonic (wm_syschar routes to the bar's mnemonic scan), f10 opens the first menu, the underline policy (hidden until alt is held) repaints on the alt transitions (wm_syskeydown and wm_syskeyup watch vk_menu), and the popup state refresh that lived in wm_initmenu runs from both wm_initmenu and the new wm_initmenupopup route. the press-then-release open keeps the classic drag-across-items behavior reachable. the status bar owns its dark face now: the comctl status class paints its background from the light palette even under the dark theme class (the flat light slab under the dark canvas the field caught), so the subclass erases with the dark face, every pane is registered as owner drawn from the first update (the empty-bar case never sent a wm_drawitem and showed the bare light face - the pane store now tracks per-pane liveness), and the dark paint tail repaints the sunken top edge and the size grip with the chrome palette so the bottom band reads as one dark chrome. the toolbar button spacing is pinned: the comctl auto size adapts each button to its own icon width (the zoom pair sat 12px tighter than the prev/next pair) and the theme re-metrics drift the paddings again, so tb_setbuttoninfo pins one uniform button width and one separator width per index, re-pinned after the theme flip and the dpi change. the deleted machinery: the root item tagging, the draw and measure routes, the pad capture, the gap fill, the non client tail fill, the theme toggle and the re-measure walk - 502 lines of viv_chrome.c; the dispatch carries 44 handlers (wm_syschar, wm_syskeydown, wm_syskeyup and wm_initmenupopup joined; wm_measureitem and wm_ncpaint left with the owner draw they existed for, 58 defwindowproc returns measured), the splice manifest is the 13-domain list (viv_menubar registered in the props exactly once), the link scan stays clean at 111 externs, and the growth window still holds at 22,271 spliced lines.
…s one self drawn window now - the nested owner drawn buttons, the layered tray child, and the separate capsule comctl windows are gone; the tray, the stadium shaped capsules, the hit test, the capture based press tracking, and the hover state are all one painter with a hit test that returns HTCLIENT on capsule rects and HTTRANSPARENT everywhere else so a fading pill never eats clicks. the windowed pill stays two buttons in the bottom right and the fullscreen bar stays six centered at the bottom with the idle fade; both modes share one tooltip (rect based, re targeted on dpi and mode changes), the keyboard path is real (tab, arrows, space, enter), and the disabled state dims the glyphs through DrawState instead of disappearing.
…make, the everything-gets-the-theme round, the dpi correctness round) land as one import - the viv_toolbar, viv_settings, viv_msgbox and viv_theme domains, the owner drawn popup menus with the row pools, the remade settings window and message boxes, the remade zoom pill row, the status date pane and the zig cc build (build-zig/build.sh over files.txt), verbatim from the user's tree minus the build scratch that never belonged in the repo (the zig object tree and logs, the status proc scratch extract, the private run-matrix script and the generated manifest copy - the gitignore covers the regenerable ones). a carpet review round follows as the next commit.
…emake went through a seven-subagent adversarial carpet review (one agent per domain plus a win7 compatibility audit with online cross-validation and an independent rebuild); this round repairs what the review found. the two headline breaks: the menubar roots measured empty since the rc.9 owner draw flip (the popup rows store no string and the bar kept reading miim_string - every root collapsed to a blank sliver and the alt mnemonics died; the bar now re-derives the labels from the live rows through _viv_menu_row_item_text) and the status date pane rendered a lone zero (a wide format string through the narrow printf parser; the stamp now builds with the localized getdateformat/gettimeformat pair). the viewport geometry caught up with the top strips: the toolbar moved under the menubar in rc.8 but the image math still reserved its band above the status bar - one view-top helper (menubar plus toolbar) now rides every size computation, paint blit, mouse anchor, scroll clip and the zoom pill placement, so the forty-dip dead strip and the hidden image top are gone. the theme token cache flushes on every flip path (the system-initiated half-skinned ui), the toolbar play slot resolves from the live state instead of forcing fullscreen, the toolbar faces ride the tokens (no private palette duplicate, the light hover is the quiet 3dlight), the strip background drag moves the window again, the message boxes are owned/leak-free/close-correct/dpi-following, the settings shortcut editor keeps a no-op confirm a no-op and makes ctrl+return rebindable, win 7 closes the dpi ladder (the legacy dpiaware manifest tag plus the setprocessdpiaware runtime tier - the rc.10 win 7 claim was wrong, unaware not aware), the recent-files header is owner drawn (the type flag had landed in fstate), radio rows draw the dot, grayed rows never fire from the keyboard, the mru swap defers under an open popup, the idle pump treats the getmessage error return as an error, the pill percent reads the app-wide render percent, a failed glyph build never caches, the line endings renormalize to the baseline conventions, and the pixel probe tooltip claim is corrected (it has been dead code since before rc.7 - the rc.9 line was written into that dead code). the link scan now audits the full public api (prototype-style header declarations join the extern set: 111 to 298 symbols, still clean), the growth window recalibrates to 29600 (measured 29408), and 38 new carpet-repair guards pin every fix. verified: the full tree compiles under zig cc (98 units) and the three suites pass.
… pane draws with CtlType == ODT_MENU (a historical quirk), so the menu branch the remake put first in _viv_on_wm_drawitem cast the pane index - the itemData - into a menu row pointer and dereferenced address 0x1 on every slow-decoded image: the preload pane draws while the load thread still runs, and a fast gif/bmp decode never shows that pane, which is why the anomaly sweep's crash matrix looked png-specific (every crashing sample was an image whose decode was slow enough to draw the loading pane; the pass set simply never displayed it). the panes route by the control id first again - the baseline's proven discriminator - and the menu rows after. found by the ci smoke sweep on the rc.11 push, binary-searched to the user's import via a diagnostic branch, stack-captured under cdb with _NO_DEBUG_HEAP=1 (the debug heap masks the timing window), fault-resolved to the fixed offset 0x486dc via the release map file, and the exact frame chain landed: menu_draw_item <- drawitem dispatch <- comctl PaintStatusWnd <- status_set(SB_SETTEXTW part|SBT_OWNERDRAW) <- status_update <- start_first_frame <- the first-frame reply. the guard pins the branch order.
…port audit's three platform-level roots, fixed and pinned. the manifest's windows 8.1 supportedOS GUID was a hand-corrupted value (the first block matched the official guid and the rest did not) so a windows 8.1 host never matched its own entry and kept the 8.0 view; the manifest now carries the official {1f676c76-80e1-4239-95bb-83d0f6d0da78}, verified against the microsoft application-manifests documentation. unicows is dead weight for a win7+ target: the win32 link lines of both projects carried UnicoWS.lib straight from the original upstream import - a windows 9x compatibility stub the modern sdks are phasing out; nothing in the tree imports an mslu symbol (the x64 configurations never carried the lib and always linked), so the dependency is deleted from the vs2019 and vs2026 win32 configurations; the ci legs still link today (the hosted sdks still ship the stub), so this is the forward-compatibility clean, not a regression repair. wm_dpichanged dereferenced its lparam unconditionally: the system supplies the suggested rect, but a synthetic message or a compatibility layer may not; the resize is now optional - the dpi refresh that rides the same handler still runs, only the suggested-rect SetWindowPos is guarded. three regression guards pin the round, the version guards move to build 54, and the merge normalizes the audit package's line endings to the baseline conventions (crlf manifest/wndproc/vs2019 project, lf vs2026 project and the python suites).
…s 10 field report, three roots. the play face read the animation preference bit as the playing state - that bit defaults to 1 and every image switch resets it, so the pause bars sat on every static photo; the face now gates the clock on the frame count (the ontop rule's own gate) and the five clock-changing paths (the pause toggle, the frame step and prev, the frame home and end jumps) notify the toolbar and recompute the on-top window. zoom needed two clicks: the button snap to the nearest multiple of ten could move against the click direction (a fresh 31% best fit zoomed in to 30 - invisible - and the visible step only came on the second click) and a zoom out from the best fit hit the dead pos==0 gate written before the below-fit extension; the snap is direction strict now (out lands on the multiple strictly below, in on the one strictly above), the floor gate reads the true ladder floor, and a sparse-zone fallback steps one position when a ten-point target sits between two ladder positions. it looked bad on non-win11: the remade surfaces leaned on win11-only dwm attributes with no fallback - the settings window is a borderless popup that got no shadow on windows 10/7 (a bare floating panel), its dropdowns opened as native string menus with the system blue highlight and the native checkmark next to the accent pill sidebar, the light hover token sat ~3 points off the strip, the light sidebar equaled the content face, and the light toolbar carried a private gray between the menu and button faces. the settings class registers the drop shadow style off win11, the settings dropdowns ride the owner drawn menu rows (the same painter, radio dots, one pool per open, the shared popup theming extracted from the main wndproc), the light tokens recalibrate (hover contrast, the nav layer, the menu-tone strip), the message box buttons take the settings pill radius, and the zoom pill keeps its row still under an active press. the dwm chrome calls double as the platform probe now (the attribute 33 result latches the answer, os_is_win11 reads it) so every fallback decision is one honest check instead of a silent no-op. verified: the three suites pass (the zoom math model carries the direction-strict contract, the below-fit domain and the field-report regression), the link scan stays clean at 300 externs, and the growth window recalibrates to 29900 (measured 29615).
…r zoom percent field report, two roots, one design. the click never reached the pane: the pane doubles as the status bar drag anchor inherited from upstream, its subclass ate every left button down on it and handed it to the move loop, which swallowed the button up too - and the open command rides the up (the comctl status bar fires its click notify on the up, with no down-pairing check), so the editor only opened from an inactive window's orphan up, which reads to the user as two clicks. the anchor is retired: the pane is pure input now, the drag lives on the toolbar, the menu gaps and the title bar. the select box was the editor itself: a 1998 centered modal dialog with a sunken number field, native buttons and a select-all blue block that never met the remade chrome. the editor is in place now - the pane itself becomes a borderless number field in the strip's own font and colors (the status bar subclass answers ctlcoloredit for it), enter commits, escape cancels, focus lost commits, typing replaces the selected digits; the commit contract carries over unchanged (the empty or zero input is ignored, the 1600 clamp holds, the anchor stays the view center). the dialog template, its two controls and its two localization strings are gone; the entry function keeps its name so the click site is untouched. the audit round added one fix: a canvas click now takes the keyboard home - the editor held the focus while it lived, so a click on the image left the arrow and page keys routed into the number field (a dead navigation zone until enter or escape); the click's focus move is itself what commits an open editor, and the focus return only happens while the editor still owns the keyboard (no focus stealing from other apps). the pane drag helper is deleted from the os layer with its only consumer. verified: the three suites pass with the recalibrated guards (the dialog counts move from eleven to ten, the font statement count likewise, the zoom dialog pins replaced by the editor contract pins and the new rc.79 guard section), the link scan stays clean at 300 externs, and the growth window holds.
…s and one hardening, all field-measured first. the recent files list reordered itself on rotate: the rotate chain never touches the list (the only writer is the recent push called from the single-file open, and the shell verb runs the system's lossless rotation) - what reordered it was the silent re-open, any path handing the viewer the file already on screen (the single-instance forward, a re-drop, a re-click of the current entry, the rotate-then-double-click recheck) feeding the list as a brand-new open, so the file jumped to the top with no visible open anywhere. the push site now asks one question - is this the file already on screen? a reload is not a recent open - and the honest trade is recorded: clicking the recent entry of the displayed file no longer reshuffles it. the list also kept dead rows: a delete left its entry until some later click failed, a rename left the old name forever; both operations maintain their own entry now, the rename swapping in place so nothing jumps. the text proportions at 4k 225%: the settings window was the one surface on its own font basis (13 dip rows over the message font face) while the menu bar, toolbar, message boxes and classic dialogs all sit on the system font - a fixed +8 percent ratio, one invisible pixel at 96 dpi and two to three visible pixels per glyph at 216, the field's own measurements (menu 25px, settings 28px). the rows move to 12 dip and the descriptions to 11: the settings text now coincides with the message font basis at every dpi, and the dropdowns stop changing size between the closed value and the open list. the status bar never had a font of its own - it ran on whatever the common control defaulted to, which scales but is not the strip's basis; the bar now carries the menu font explicitly and re-pins it on a dpi change, and the in-place zoom editor inherits it through the same wm_getfont it always used. the garbled line under the shortcuts buttons: the capture hint copied its localization string through the wide-string copier, but localization returns utf-8 - each byte pair became one mojibake character (the glyph soup under the add and edit buttons, pinned by computing the exact expected bytes for the edit-caption string). the copy crosses the utf-8 bridge now and a tree-wide guard scans for the pattern so no other site can grow it. the win7+ hardening: the menu theming finds the system menu window by the #32768 class name, shared by every menu in every process on the desktop - the find could return another process's open menu and theme that one, leaving our own popup plain and writing into a stranger; both find sites now validate the owning process. verified: the three suites pass (the round-42 mru deferring count recalibrates 4 to 5 for the rename helper, the r80 guard section carries 28 new checks), the link scan stays clean at 302 externs, and the growth window holds.
…on the recent-list fix, one design change. the last round's guard asked the right question at the wrong place: 'is this the file already on screen' was answered for every open - including the opens where the user had just said so, a click on a recent entry, a re-open through the dialog, a re-drop of the same file - so the honest trade of rc.15 (clicking the recent entry of the displayed file no longer re-shuffled it to the top) was that cost. the intent is now declared where it is knowable instead of guessed where it is not: the open-by-name carries a recent-list policy from its caller. VIV_OPEN_RECENT comes from the three user commands (the open dialog, the drag-drop, the recent click) and feeds the recent list unconditionally - the standard mru contract, a re-open of the displayed file re-tops it. VIV_OPEN_FORWARDED comes from the one path that cannot know its own intent, the single-instance forward of a second launch - the re-entry the rotate verb's refresh and the rotate-then-recheck double-click both ride - and keeps the one honest question: a same-file forward is a reload, not a recent open, and does not reorder the list, while a forwarded new file still enters as a brand-new open and the startup command line is vacuously fine (no file is on screen yet, so the first open still enters). the surgery is one parameter wide: the signature gains recent_policy, the push condition asks the policy first and the file identity second, the rotate chain still touches no recent code, and the mru hygiene (delete drops, rename swaps in place) rides unchanged. verified: the three suites pass (the r80 call-site pins recalibrate to the declared policies, the new round-81 section pins the intent contract - two declarations plus four call sites tree-wide, the old one-argument signature gone - and the version guards move to build 58), the link scan stays clean at 302 externs, and the 71-file bracket balance holds.
…nal carpet review (v2, two independent cross-validation passes) read the rc.16 tree line by line; this round absorbs its confirmed findings and records its refutations. the refutations matter as much as the fixes: the gesture leak it asked to self-check is not there (every unhandled wm_gesture path - gid_begin, gid_end, the default, the failed decode - returns 0 and the caller forwards it to defwindowproc, which owns the info handle; only the handled paths close it themselves; setgestureconfig's cbsize is the single-struct size the api wants), and the strip height audit found every view geometry in the tree riding the one _viv_get_view_top() helper. the confirmed fixes: the status pane index now reads itemID (the official carrier comctl fills) with itemData as the compatibility fallback, and an unresolvable pane paints the strip face instead of returning silently unhandled - the rc.11 bottom white bar's exact failure mode, closed; the crash-guard comment now calls the status bar's ctltype what it is (uninitialized garbage, not a historical quirk) so nobody simplifies the control-id routing into the 0x1 dereference again; the status strip joins the token system - the erase, the pane fill and text, the grip fill and dots, and the zoom editor field colors all rode hardcoded values while the menubar and toolbar ride the chrome token, all sites route through the theme now and the three strips read as one chrome; the settings window re-measures its frame at its own dpi after the create (the show path owed the wm_dpichanged branch's correction) and grows from the left, right and bottom edges with the design size as the minimum track, so a page whose content outgrows the panel can never trap its footer; and the startup path syncs the window dpi after the main window exists, before the first strip is built from the globals (GetDC(0) reads the primary monitor). the deferrals are recorded on purpose (the review's own advice): the retired options dialog stays frozen until list browsing returns, the per-window dpi refactor and the resource.h id splits stay on the debt list, and the dpi probes stay (compiled to nothing in release). verified: the three suites pass (the round-82 guard section pins the itemID routing with the fallback paint, the token strip with the hardcoded palette gone, the settings corrections, the startup sync and the rewritten comment; the pane paint and zoom editor guards recalibrate to the token pair), the changelog carries the round, and the growth window holds.
…dded border metric is gated to winver 0x0600 in the sdk headers and this tree builds at _WIN32_WINNT 0x0501, so both ci compile legs failed with C2065 the moment the tag was pushed (the python suites cannot see an sdk gate - the local truth sources are all text-level). the fix is the house pattern for exactly this gap class: the #ifndef fallback define beside its consumer, the same shape the wndproc domain uses for the gesture messages. the growth window recalibrates to 30200 (the round's additions measured the spliced total at 29907, seven lines over the rc.13 ceiling) and the guard pins the fallback. verified: the three suites pass. the tag moves to this commit - its release never published, the rc.11 precedent.
…romoted to stable - the round itself carries no code, only the version identity, the changelog and the readme: every fix it rides already shipped as 1.1.12-rc.17 and passed the full gate on real windows (the three suites, both compile legs, the anomaly sweep smoke test, and the release build itself). the stable mark is the verdict on the whole arc: the gui remake (the token system, the owner drawn menus, the remade settings window and message boxes, the client side top bar, the self drawn zoom row), the structure split, the crash the remake planted and the smoke sweep caught, the platform guardrails, the field arcs, and the review absorption. the promotion guard section pins the identity (version 1.1.12 build 60, type empty), the stable changelog head, the no-code statement, and the readme current-stable line with the rc.17 entry demoted to the previous candidate; every version guard in the tree moves to build 60. verified: the three suites pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A zero-dependency dark mode + modern chrome pack for voidImageViewer, built entirely on public or dynamically-resolved Windows APIs. Nothing is forced: light mode stays byte-identical, every dark path activates only when the system asks for dark, and every modern API resolves at runtime, silently skipped on older builds.
55 commits; all CI legs green (Python regression suites + windows-2022/v143 + windows-2025/v145 compile matrix). Shipped as the fork's 1.0.03 release (x64/x86 installer + zip).
The headline fix: the menu bar - the last white strip
Even with immersive dark mode, a win32 menu bar keeps the system light color on Windows 11 and pre-1903 builds. Top-level items are now owner-drawn while dark is active: the system keeps layout, click tracking, keyboard navigation and dropdowns; each item paints the dark palette (face 0x202020, hover 0x454545, label 0xE8E8E8). The strip around the items fills from a union of the drawn item rects (GetMenuBarInfo rects can be stale after a theme switch). Light mode hands the items back to the system; labels measure with the DPI-aware menu font.
Dark layers
Hardening
Pinch-zoom distance floor (collapsed fingers re-baseline), 1600% zoom ceiling (never below the fit floor), Options under File with the Layout submenu, resolution pinned bottom-right, and the manifest supportedOS GUID list (without the Win10 GUID the immersive dark menus refuse to theme).
Robustness (three user-audited rounds)
Three audit rounds rescanned all 30 source files and the playlist, rotation, mipmap, gesture, save-as, IPC paths: no new high-severity issue; 5 earlier claims retracted after re-verification. Landed:
DPI
PerMonitorV2 in the manifest (+ PerMonitor fallback); WM_DPICHANGED re-reads the DPI, accepts the suggested rect and rebuilds every scaled resource - mixed-DPI dragging stays sharp.
Compatibility
Test plan
CI on the code-round head (98a7e9a): Python regression suites + v143 + v145 compile matrix, all green.