Skip to content

Add JonaOS widgets - #5270

Open
Stunning-dev wants to merge 1 commit into
ramensoftware:mainfrom
Stunning-dev:patch-9
Open

Add JonaOS widgets#5270
Stunning-dev wants to merge 1 commit into
ramensoftware:mainfrom
Stunning-dev:patch-9

Conversation

@Stunning-dev

Copy link
Copy Markdown

Mod authorship

If this pull request introduces a new mod, please complete the section below.

This mod was created by:

    • The submitter, without AI assistance
    • The submitter, with AI assistance
    • Claude
    • ChatGPT
    • Gemini
    • Another AI (please specify):
    • Other (please specify):

Please select the options that best apply. Your selection does not affect the acceptance criteria, but it helps reviewers understand the context of the code and provide relevant feedback.

@windhawk-reviewer windhawk-reviewer Bot added the waiting-for-author The author's turn: request an AI review, or respond to one that was posted. label Aug 29, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Thanks for the pull request! This repository uses a two-stage review: an AI review that you run yourself, followed by a human review.

To get started, comment /ai-review. Once you're happy with the result, comment /ready-for-reviewer to hand it over to a human reviewer.

See the pull request review process for the full details.

@Stunning-dev

Copy link
Copy Markdown
Author

/ai-review

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-ai-review An AI review was requested and is being prepared. and removed waiting-for-author The author's turn: request an AI review, or respond to one that was posted. labels Aug 29, 2026
@windhawk-reviewer

Copy link
Copy Markdown

Submission review

Note: This review was done by Claude. Due to the amount of submissions, doing a fully manual review for each pull request is no longer feasible. Thank you for understanding.

Remember: The AI reviewer can be wrong - it may misread code, flag correct code as broken, or suggest changes that make things worse. Treat its findings as suggestions to verify, not instructions to follow blindly. You're responsible for the code you submit, so if a finding doesn't hold up, say so instead of changing working code to satisfy it.

Please address the following issues. The items in the collapsed sections are optional, so it's your call whether to address them.


Compared to the previous submission the file changed in exactly three places: a README line, the small-button/round-shortcut sizes and font sizes, and a horizontalHeight parameter on RectForOrientation. Everything else is byte-identical, so the findings below are largely the same ones as last time — including the one the maintainer asked about directly.

1. Display scaling is still ignored, so the widgets are the wrong size on any scaled monitor.

This was item 1 of the previous review and the maintainer's explicit follow-up question ("Any reason not to add DPI support according to the suggestion in review note 1?"). UpdateScale() is unchanged:

void UpdateScale() {
    int sw = GetSystemMetrics(SM_CXSCREEN);
    int sh = GetSystemMetrics(SM_CYSCREEN);
    if (sw >= 1920 && sh >= 1080) {
        g_scale = 1.0f;

Nothing reads the monitor DPI, and UiThreadProc still doesn't pin a DPI awareness context, so the UI thread inherits whatever windhawk.exe happens to be. On a 3840×2160 panel at 150% this yields g_scale = 1.0RenderScale() = 0.75, so the 390×620 design lands in ~293×465 physical pixels — about a third of the intended size, text included. Every offset in the drawing code is a raw pixel literal, so nothing compensates. Note also that WM_DPICHANGED is only delivered to per-monitor-aware windows, so that handler is dead as things stand.

The closest existing mod — also a windhawk.exe tool mod drawing a layered GDI+ desktop panel — pins awareness on the UI thread and scales by the real DPI: quick-launch-media-panel.wh.cpp#L2027 with its DpiForMonitor helper at #L651; dynamic-island-for-windows.wh.cpp#L586 does the same. The change is small:

DWORD WINAPI UiThreadProc(LPVOID) {
    SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
    ...
void UpdateScale() {
    ...
    UINT dpi = g_hwnd ? GetDpiForWindow(g_hwnd) : 96;   // or GetDpiForMonitor on the primary monitor
    g_scale *= (dpi ? dpi : 96) / 96.0f;
}

If there's a reason you'd rather not do this, that's worth saying explicitly in the PR rather than leaving it open a second time.

2. The text-readability fix doesn't reach the actual cause — a global 0.75× shrink is applied to every font.

The font bumps in this revision (round shortcuts 8.4/10.0kRoundShortcutFontSize = 12, small buttons 13kSmallButtonFontSize = 15) go in the right direction, but everything is drawn inside g.ScaleTransform(renderScale, renderScale) where renderScale = g_scale * kWidgetSizeScale and kWidgetSizeScale = 0.75f. So on a plain 1920×1080 display at 100% the sizes that actually reach the screen are:

element design px device px
round shortcut label (WiFi, Accessibility) 12 9
clock numerals 13 9.75
slider label (Volume, Battery) 13 9.75
calendar weekday header 14 10.5
small button / calendar day numbers 15 11.25

The Windows shell UI font is 9 pt Segoe UI = 12 device px at 100% scaling, so every string in the mod is still below standard UI text size — which is what the screenshot in the previous PR was showing. Either drop kWidgetSizeScale to 1.0f (and shrink the design rects if the panel then feels too big), or raise the font constants so that size * 0.75 lands at 12+ px. Bumping the design-space numbers while the 0.75 multiplier stays in place will keep coming up short.

3. The new README line advertises a feature the mod doesn't implement.

# View Calendar, Time, Battery, Open your Music easily without need of navigating to File Explorer, and Control Volume with just a scroll.

There is no WM_MOUSEWHEEL case in WidgetWndProc — volume is only changed by pressing and dragging the slider, so "Control Volume with just a scroll" doesn't describe what happens. Either add the handler (the window returns HTCLIENT over widgets, so with "scroll inactive windows on hover" — the Windows 10/11 default — wheel messages will reach it):

case WM_MOUSEWHEEL: {
    POINT screenPt = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)};
    if (HitTestWidget(LogicalPointFromScreen(screenPt)) == WidgetVolume) {
        SetSystemVolume(g_volumeLevel + GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA * 0.02f);
        Render();
        return 0;
    }
    break;
}

or reword the sentence to describe the drag.

Two smaller things on the same line: it starts with #, so the whole paragraph renders as an H1 heading on the mod page rather than as body text — drop the #. And the mod is titled JonaOS Draggable Desktop Widgets in @name but JonaOS Draggable Transparent Widgets in the README heading.

4. The volume-slider hit test still runs before the widget hit test, so a widget dragged over the slider changes the volume instead.

Unchanged from the previous review:

case WM_LBUTTONDOWN: {
    PointF pt = LogicalPointFromLParam(lParam);
    RectF slider = VolumeSliderRect();
    if (PtInRectF(slider, pt)) {
        g_draggingSlider = true;
        SetCapture(hwnd);
        SetSystemVolume(...);
        return 0;
    }
    int hit = HitTestWidget(pt);

VolumeSliderRect() is derived from g_widgets[WidgetVolume].rc unconditionally, with no check that Volume is the topmost widget under the cursor. Widgets are freely draggable and overlap easily, and HitTestWidget iterates back-to-front precisely to respect draw order — this branch bypasses it. Drag any of the WiFi/Hotspot/Bluetooth/Accessibility circles (or Battery/Search/Personalization, all drawn above Volume) onto the volume slider, and clicking that widget sets the system volume instead of dragging it. Resolve the hit test first:

int hit = HitTestWidget(pt);
if (hit == WidgetVolume && PtInRectF(VolumeSliderRect(), pt)) {
    g_draggingSlider = true;
    ...
}

5. Two of the three Show-Desktop guards (FIX C) still can't fire.

Also unchanged. Guard 1 (WM_WINDOWPOSCHANGING stripping SWP_HIDEWINDOW) is the one that does the work and is a legitimate design choice — no change requested there. The other two are dead:

  • Guard 2 tests wParam == FALSE && lParam == SW_PARENTCLOSING. SW_PARENTCLOSING means "the window's owner window is being minimized"; the window is created with hWndParent = nullptr so it has no owner, and when a hide comes from ShowWindow the docs state lParam is zero. The branch never runs, so kRestoreWindowMessage is never posted. (The comment's other claim — that skipping DefWindowProc for WM_SHOWWINDOW prevents the hide — isn't how that message works either; it's a notification, and DefWindowProc's handling of it only concerns owned windows.)
  • Guard 3 (WM_SIZE / SIZE_MINIMIZED) is unreachable from the same path: Show Desktop hides this window, it doesn't minimize it.

Add a Wh_Log to each of the three, do the three-finger swipe / Win+D, keep whichever actually fires and delete the other two along with kRestoreWindowMessage. Dead defense-in-depth reads as coverage that isn't there.

Optional improvements

Minor polish — none of this affects users, so it's your call. These all carry over unchanged from the previous review.

  • Dead code. ScaledDesignWidth, ScaledDesignHeight and ScaledMargin (and therefore kDesignH) are never called, and line 455 has an unused global: AppearanceTheme _appearanceTheme = AppearanceTheme::LiquidGlass; (shadowed in intent by g_appearanceTheme; leading-underscore names at namespace scope are also reserved). #include <shlobj.h> is unused. DwmExtendFrameIntoClientArea(hwnd, &margins) on a WS_POPUP window whose entire content comes from UpdateLayeredWindow is a no-op — dropping it lets you remove #include <dwmapi.h> and -ldwmapi.

  • Comments describe an edit history that doesn't exist in this repo. FIX A, FIX B, FIX C, // FIX B: was missing — caused White Transparent to silently stay as LiquidGlass, // ── Audio / battery COM helpers (unchanged from original) ──. There is no "previously" and no "original" here — a reader has only this file. Trim these to describe what the code does now.

  • std::atomic<float> g_volumeLevel. VolumeEndpointCallback::OnNotify runs on an MMDevice worker thread and writes g_volumeLevel, while the UI thread reads it in Render/DrawSlider. It's a formal data race; std::atomic<float> (relaxed) costs nothing here. Same for volatile bool g_unloadingstd::atomic<bool> (written from the Windhawk thread in WhTool_ModUninit, read by the UI thread's loop; volatile isn't a synchronization primitive).

  • Settings-change race in the pre-window window. WhTool_ModSettingsChanged posts to the UI thread when g_hwnd exists but calls LoadSettings() directly from the Windhawk thread when it doesn't, which can overlap with the UI thread's startup LoadWidgetPositions/Render. Narrow (only between CreateThread and CreateWindowExW) and only on a settings change, but posting to g_uiThreadId instead keeps all settings reads on one thread.

  • WindhawkUtils::StringSetting instead of manual get/free. ReadVerticalOrientationSetting and LoadSettings do Wh_GetStringSetting + Wh_FreeStringSetting by hand with if (value) guards. Wh_GetStringSetting never returns NULL (it returns L""), so the guards are dead, and the RAII wrapper in windhawk_utils.h removes the free calls entirely.

  • Unchecked return values. GdiplusStartup, RegisterClassExW and CreateWindowExW are all unchecked; if any fails, the UI thread spins an empty message loop forever with no indication of what went wrong. A Wh_Log + early return would make that debuggable.

  • Bounded thread wait. WhTool_ModUninit does WaitForSingleObject(g_uiThread, 3000) and continues on timeout. Harmless here because Wh_ModUninit calls ExitProcess(0) right after, so the image is never unmapped while the thread runs — but on timeout the WM_DESTROY handler never runs, so that session's widget positions are lost. An unbounded wait (or at least a Wh_Log on WAIT_TIMEOUT) is the safer shape.

  • The tool-mod boilerplate is reformatted rather than pasted verbatim. It's semantically identical to the wiki snippet, but the wiki asks for a verbatim copy so it can be diffed and updated mechanically — see explorer-folder-hover-menu.wh.cpp for the unmodified form, and https://github.com/ramensoftware/windhawk/wiki/Mods-as-tools:-Running-mods-in-a-dedicated-process for the source.

  • Typo in a user-visible string. CrimsomBlush appears in the settings options, in the AppearanceTheme enum, in g_palettes and in the README caption — it should be Crimson.

  • musicFolderPath is passed straight to ShellExecuteW as the file to open. TryOpenUri(g_musicFolderPath) will happily launch whatever the string resolves to, including an .exe or a URL, even though the description says "Paste a folder path here". ShellExecuteW(nullptr, L"open", L"explorer.exe", g_musicFolderPath, nullptr, SW_SHOWNORMAL) guarantees it opens a folder view.

  • Two small documentation inaccuracies. The README caption reads eg(E:Music), which isn't a valid path (missing backslash — the settings description correctly says D:\Music). And the materialCustomColor description advertises #AARRGGBB, but ParseMaterialAccentColor skips the alpha pair and discards it — either say so or use it.

  • DesktopWorkArea() runs a SystemParametersInfoW call per WM_NCHITTEST. The window covers the whole work area at HWND_BOTTOM, so every mouse move over empty desktop reaches WM_NCHITTESTLogicalPointFromScreenSPI_GETWORKAREA, plus an 11-widget hit test. Caching the work area (refreshed on WM_DISPLAYCHANGE / WM_SETTINGCHANGE) removes a per-mouse-move syscall.

  • g_palettes can be const. It's never mutated, and CurrentTheme() already returns a const ThemePalette*.

Functionality notes

Non-critical observations and ideas about the feature behavior itself. These also carry over from the previous revision, plus one new one at the top.

  • Vertical orientation truncates the button labels, and the larger font made it worse. In vertical mode WidgetSearch/WidgetPersonalization are kSearchVerticalWidth = 73.33 wide, and DrawSmallButton gives the label rc.Width - 8 ≈ 65 px. "Windows Search" and "Personalization" at 15 px Segoe UI need roughly 95–100 px, so StringTrimmingEllipsisCharacter cuts them off. Either use a shorter label in vertical mode ("Search" / "Theme"), wrap to two lines (drop StringFormatFlagsNoWrap for this case), or widen the vertical form.

  • Z-order after an Explorer restart. The window is placed at HWND_BOTTOM once at creation and re-asserted only when the work area changes. When Explorer restarts, the desktop windows (Progman/WorkerW) are recreated and can end up above the widget layer, making the widgets disappear until something else moves the work area. Handling RegisterWindowMessage(L"TaskbarCreated") (broadcast to all top-level windows, so your window will get it) and re-issuing the HWND_BOTTOM SetWindowPos would cover that.

  • Work-area changes are picked up late. Render() re-reads SPI_GETWORKAREA and repositions, but it only runs on a battery change or a minute rollover — so enabling taskbar auto-hide, or moving/resizing the taskbar, leaves the surface misaligned for up to a minute. WM_SETTINGCHANGE with wParam == SPI_SETWORKAREA is broadcast to top-level windows and would make it immediate.

  • g_onBatteryCapable is set but never read. RefreshBattery carefully detects "no battery in this system" and then sets g_batteryLevel = 1.0f, so on a desktop PC the battery widget shows a permanently full battery instead of hiding itself. Presumably the flag was meant to skip drawing (and hit-testing) that widget.

  • The battery widget looks interactive but isn't. DrawSlider draws the same track + fill + round thumb for Battery as for Volume, so it reads as draggable; only the Volume slider is hit-tested. A plain bar with no thumb would set the right expectation.

  • A permanent work-area-sized render surface. EnsureRenderSurface caches a 32bpp DIB the size of the whole work area — ~33 MB on a 4K display, held for as long as the mod runs — and every Render() pushes that entire surface through UpdateLayeredWindow, including on every WM_MOUSEMOVE during a drag. Caching it was the right call; if you want to go further, UpdateLayeredWindowIndirect accepts a prcDirty so only the changed region is composited, or the window could be sized to the bounding box of the widgets rather than the whole desktop.

  • Primary monitor only. DesktopWorkArea() uses SPI_GETWORKAREA, which returns the primary monitor's work area, so the surface and all drag-clamping are confined to that monitor. (Now documented in the README — good.)

  • The calendar is hard-coded to English and to a Sunday-first week. months[] and days[] = {L"S", L"M", L"T", L"W", L"T", L"F", L"S"} are literals, and cell = firstDow + day - 1 assumes the row starts at wDayOfWeek == 0. English defaults are correct for Windhawk, but GetLocaleInfoEx (LOCALE_SMONTHNAME1…, LOCALE_SABBREVDAYNAME1…, LOCALE_IFIRSTDAYOFWEEK) would match the user's locale for free, including the Monday-first layout most of Europe uses.

  • UpdateCalendarHeight() mutates only the calendar's Height on every render. If the user drags the calendar to the bottom edge in a 5-row month and the next month has 6 rows, the widget grows past the desktop bounds with no re-clamp. A ClampWidgetToDesktop(g_widgets[WidgetCalendar]) after the height change would cover it.

  • Synthesizing Win+S can misfire. OpenWindowsSearch injects VK_LWIN + S via SendInput without neutralizing physically-held modifiers — if the user happens to be holding Shift, the injected combination becomes Win+Shift+S and opens the Snipping Tool overlay instead. It also does nothing if the search hotkey is disabled by policy.

  • Audio initialization is never retried. If CoCreateInstance(MMDeviceEnumerator) fails at startup (or there's no render endpoint yet), g_audioEnumerator stays null, the notification client is never registered, and nothing ever tries again — the volume widget is stuck at its initial value for the rest of the session. A retry from the one-second WM_TIMER when g_endpointVolume is null would be enough.

  • ClearType on a per-pixel-alpha surface. g.SetTextRenderingHint(TextRenderingHintClearTypeGridFit) is the one path in the render that goes through GDI, which doesn't maintain the alpha channel that UpdateLayeredWindow(..., ULW_ALPHA) expects. It looks fine over the opaque widget bodies in your screenshots, but the Liquid Glass theme fills with Color(1, 255, 255, 255) (alpha 1/255), where this is most likely to bite. TextRenderingHintAntiAliasGridFit is GDI+-native and alpha-correct — and given item 2 above, it's worth checking whether ClearType is also part of why the small text reads poorly.

  • The Acrylic grain is recomputed every frame. The speckle pattern is deterministic per pixel coordinate, so it's identical on every render — rendering it once into a cached tile and blitting it would remove ~1,500 antialiased 1×1 FillRectangle calls per frame. Note also that with SmoothingModeAntiAlias and a 0.75 scale transform active, each 1×1 rect is antialiased across neighbouring pixels, softening the grain more than intended.

  • The Music widget shows playback UI but opens a folder. The equalizer bars plus a prominent play button suggest media transport controls, while clicking just runs ShellExecuteW on the configured folder. Either soften the artwork or, for real controls, the GlobalSystemMediaTransportControlsSession WinRT API is what island-media-controls.wh.cpp uses.

  • 20 hard-coded palettes is a lot of surface to maintain. Most differ only in three colors. A handful of curated presets plus a "Custom" option with headerColor/bodyColor/accentColor/text-color string settings would give users more and cost you less code (and 20 README screenshots).

  • Closest existing mods, for context. desktop-live-overlay draws live content on the desktop behind icons, and quick-launch-media-panel is a windhawk.exe tool mod with a desktop panel. Neither is draggable per-widget or covers the clock/calendar/volume/battery/Quick-Settings set, so I don't think this duplicates them — but it's worth a line in the README saying how it differs, since users will compare.


Next steps:

  • /ai-review - after pushing fixes, to get a review of the updated code. You can repeat this as many times as you need, but each review is thorough and usually there's no need for more than 2-3 iterations.
  • /ready-for-reviewer - once you're satisfied with the state of the pull request, to hand it over to a human reviewer. If some findings above are left unaddressed, add a short note explaining why.

See the review process for details.

@windhawk-reviewer windhawk-reviewer Bot added waiting-for-author The author's turn: request an AI review, or respond to one that was posted. and removed waiting-for-ai-review An AI review was requested and is being prepared. labels Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-for-author The author's turn: request an AI review, or respond to one that was posted.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant