// Main window message handling, layout, and painting.
#include "main_window.hpp"
#include <commctrl.h>
#include <dwmapi.h>
#include <uxtheme.h>
#include <cstdio>
#include <string>
#ifdef _MSC_VER
#pragma comment(lib, "comctl32.lib")
#pragma comment(lib, "uxtheme.lib")
#endif
namespace {
using ::theme_manager::ApplyTitleBarTheme;
using ::theme_manager::BuildPalette;
using ::theme_manager::Palette;
constexpr int kHeaderH = 56;
constexpr int kFooterH = 52;
constexpr int kSliderH = 40;
constexpr int kFooterGap = 4; // gap between slider bottom and footer top
constexpr int kSliderMarginH = 8; // horizontal margin each side of slider
constexpr int kBadgeW = 120; // dB badge area reserved at header right
constexpr const wchar_t* kTitle = L"Bass Booster";
// Upper bound of the slider range; the lower bound is always zero.
constexpr int kSliderMax = 1000;
// Pixel dimensions of the window client area.
struct ClientSize {
int width;
int height;
};
// State snapshot for one paint pass; assembled once so helper paint routines
// stay stateless and avoid reaching into `MainWindow`.
struct PaintContext {
Palette palette;
LayoutRegions layout;
const AudioPipelineInterface* audio; // non-owning; may be null
};
void PaintHeaderBadge(HDC hdc, const PaintContext& ctx) {
// `swprintf` avoids `std::format` locale overhead for a fixed-size label.
const double gain_db = ctx.audio != nullptr ? ctx.audio->gain_db() : 0.0;
constexpr int kDbBufSize = 32;
wchar_t db_str[kDbBufSize];
swprintf(db_str, kDbBufSize, L"+%.1f dB", gain_db);
constexpr int kBadgeFontH = 20;
constexpr int kBadgePad = 10; // top offset and right inset of the dB badge
constexpr int kBadgeBottom = 46;
HFONT badge_font = CreateFontW(
/*cHeight=*/kBadgeFontH, /*cWidth=*/0,
/*cEscapement=*/0, /*cOrientation=*/0,
/*cWeight=*/FW_BOLD, /*bItalic=*/FALSE,
/*bUnderline=*/FALSE, /*bStrikeOut=*/FALSE,
/*iCharSet=*/DEFAULT_CHARSET, /*iOutPrecision=*/OUT_DEFAULT_PRECIS,
/*iClipPrecision=*/CLIP_DEFAULT_PRECIS, /*iQuality=*/CLEARTYPE_QUALITY,
/*iPitchAndFamily=*/DEFAULT_PITCH | FF_SWISS,
/*pszFaceName=*/L"Segoe UI");
HFONT old_f = static_cast<HFONT>(SelectObject(hdc, badge_font));
SetTextColor(hdc, ctx.palette.accent);
RECT badge_rc = {.left = ctx.layout.header.right - kBadgeW,
.top = kBadgePad,
.right = ctx.layout.header.right - kBadgePad,
.bottom = kBadgeBottom};
DrawTextW(hdc, db_str, /*cchText=*/-1, &badge_rc,
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, old_f);
DeleteObject(badge_font);
}
void PaintHeader(HDC hdc, const PaintContext& ctx) {
HBRUSH surf = CreateSolidBrush(ctx.palette.surface);
FillRect(hdc, &ctx.layout.header, surf);
DeleteObject(surf);
HPEN sep = CreatePen(/*iStyle=*/PS_SOLID, /*cWidth=*/1, ctx.palette.border);
HPEN old = static_cast<HPEN>(SelectObject(hdc, sep));
MoveToEx(hdc, ctx.layout.header.left, ctx.layout.header.bottom - 1,
/*lppt=*/nullptr);
LineTo(hdc, ctx.layout.header.right, ctx.layout.header.bottom - 1);
SelectObject(hdc, old);
DeleteObject(sep);
constexpr int kTitleFontH = 26;
constexpr int kSubFontH = 13;
constexpr int kTextPadL = 16;
constexpr int kTitleTop = 8;
constexpr int kTitleBottom = 38;
constexpr int kSubTop = 34;
constexpr int kSubBottomPad = 4; // padding below subtitle to header bottom
HFONT title_font = CreateFontW(
/*cHeight=*/kTitleFontH, /*cWidth=*/0,
/*cEscapement=*/0, /*cOrientation=*/0,
/*cWeight=*/FW_SEMIBOLD, /*bItalic=*/FALSE,
/*bUnderline=*/FALSE, /*bStrikeOut=*/FALSE,
/*iCharSet=*/DEFAULT_CHARSET, /*iOutPrecision=*/OUT_DEFAULT_PRECIS,
/*iClipPrecision=*/CLIP_DEFAULT_PRECIS, /*iQuality=*/CLEARTYPE_QUALITY,
/*iPitchAndFamily=*/DEFAULT_PITCH | FF_SWISS,
/*pszFaceName=*/L"Segoe UI");
HFONT sub_font = CreateFontW(
/*cHeight=*/kSubFontH, /*cWidth=*/0,
/*cEscapement=*/0, /*cOrientation=*/0,
/*cWeight=*/FW_NORMAL, /*bItalic=*/FALSE,
/*bUnderline=*/FALSE, /*bStrikeOut=*/FALSE,
/*iCharSet=*/DEFAULT_CHARSET, /*iOutPrecision=*/OUT_DEFAULT_PRECIS,
/*iClipPrecision=*/CLIP_DEFAULT_PRECIS, /*iQuality=*/CLEARTYPE_QUALITY,
/*iPitchAndFamily=*/DEFAULT_PITCH | FF_SWISS,
/*pszFaceName=*/L"Segoe UI");
SetBkMode(hdc, TRANSPARENT);
HFONT old_font = static_cast<HFONT>(SelectObject(hdc, title_font));
SetTextColor(hdc, ctx.palette.text);
RECT title_rc = {.left = kTextPadL,
.top = kTitleTop,
.right = ctx.layout.header.right - kBadgeW,
.bottom = kTitleBottom};
DrawTextW(hdc, kTitle, /*cchText=*/-1, &title_rc,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
PaintHeaderBadge(hdc, ctx);
SelectObject(hdc, sub_font);
SetTextColor(hdc, ctx.palette.text_muted);
static const std::wstring kNoDevice = L"No device";
const std::wstring& dev =
ctx.audio != nullptr ? ctx.audio->endpoint_name() : kNoDevice;
RECT sub_rc = {.left = kTextPadL,
.top = kSubTop,
.right = ctx.layout.header.right - kBadgeW,
.bottom = kHeaderH - kSubBottomPad};
DrawTextW(hdc, dev.c_str(), /*cchText=*/-1, &sub_rc,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, old_font);
DeleteObject(title_font);
DeleteObject(sub_font);
}
void PaintSliderLabel(HDC hdc, const PaintContext& ctx) {
HBRUSH bg_brush = CreateSolidBrush(ctx.palette.background);
FillRect(hdc, &ctx.layout.slider_label, bg_brush);
DeleteObject(bg_brush);
constexpr int kLabelFontH = 12;
constexpr int kLabelColW = 80; // left/right label column width
HFONT font = CreateFontW(
/*cHeight=*/kLabelFontH, /*cWidth=*/0,
/*cEscapement=*/0, /*cOrientation=*/0,
/*cWeight=*/FW_NORMAL, /*bItalic=*/FALSE,
/*bUnderline=*/FALSE, /*bStrikeOut=*/FALSE,
/*iCharSet=*/DEFAULT_CHARSET, /*iOutPrecision=*/OUT_DEFAULT_PRECIS,
/*iClipPrecision=*/CLIP_DEFAULT_PRECIS, /*iQuality=*/CLEARTYPE_QUALITY,
/*iPitchAndFamily=*/DEFAULT_PITCH | FF_SWISS,
/*pszFaceName=*/L"Segoe UI");
HFONT old_f = static_cast<HFONT>(SelectObject(hdc, font));
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, ctx.palette.text_muted);
RECT left_rc = ctx.layout.slider_label;
left_rc.left += kSliderMarginH;
left_rc.right = left_rc.left + kLabelColW;
RECT right_rc = ctx.layout.slider_label;
right_rc.right -= kSliderMarginH;
right_rc.left = right_rc.right - kLabelColW;
RECT cent_rc = ctx.layout.slider_label;
cent_rc.left += kLabelColW;
cent_rc.right -= kLabelColW;
DrawTextW(hdc, L"FLAT", /*cchText=*/-1, &left_rc,
DT_LEFT | DT_VCENTER | DT_SINGLELINE);
DrawTextW(hdc, L"BASS BOOST", /*cchText=*/-1, ¢_rc,
DT_CENTER | DT_VCENTER | DT_SINGLELINE);
DrawTextW(hdc, L"MORE BASS", /*cchText=*/-1, &right_rc,
DT_RIGHT | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, old_f);
DeleteObject(font);
}
void PaintFooter(HDC hdc, const PaintContext& ctx) {
HBRUSH surf = CreateSolidBrush(ctx.palette.surface);
FillRect(hdc, &ctx.layout.footer, surf);
DeleteObject(surf);
HPEN sep = CreatePen(/*iStyle=*/PS_SOLID, /*cWidth=*/1, ctx.palette.border);
HPEN old = static_cast<HPEN>(SelectObject(hdc, sep));
MoveToEx(hdc, ctx.layout.footer.left, ctx.layout.footer.top,
/*lppt=*/nullptr);
LineTo(hdc, ctx.layout.footer.right, ctx.layout.footer.top);
SelectObject(hdc, old);
DeleteObject(sep);
constexpr int kFooterFontH = 11;
constexpr int kFooterPad = 12; // horizontal text padding in footer
HFONT font = CreateFontW(
/*cHeight=*/kFooterFontH, /*cWidth=*/0,
/*cEscapement=*/0, /*cOrientation=*/0,
/*cWeight=*/FW_NORMAL, /*bItalic=*/FALSE,
/*bUnderline=*/FALSE, /*bStrikeOut=*/FALSE,
/*iCharSet=*/DEFAULT_CHARSET, /*iOutPrecision=*/OUT_DEFAULT_PRECIS,
/*iClipPrecision=*/CLIP_DEFAULT_PRECIS, /*iQuality=*/CLEARTYPE_QUALITY,
/*iPitchAndFamily=*/DEFAULT_PITCH | FF_SWISS,
/*pszFaceName=*/L"Segoe UI");
HFONT old_f = static_cast<HFONT>(SelectObject(hdc, font));
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, ctx.palette.text_muted);
RECT text_rc = ctx.layout.footer;
text_rc.left += kFooterPad;
text_rc.right -= kFooterPad;
DrawTextW(hdc, L"Shelf: 100 Hz | WASAPI Loopback Capture",
/*cchText=*/-1, &text_rc, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, old_f);
DeleteObject(font);
}
LRESULT OnCtlColor(HDC hdc, const PaintContext& ctx) {
SetBkColor(hdc, ctx.palette.surface);
SetTextColor(hdc, ctx.palette.text);
// Static brush: recreated on theme change; harmless leak of one `HBRUSH`.
static HBRUSH s_bg_brush = nullptr;
if (s_bg_brush != nullptr) {
DeleteObject(s_bg_brush);
}
s_bg_brush = CreateSolidBrush(ctx.palette.surface);
return reinterpret_cast<LRESULT>(s_bg_brush);
}
bool RegisterMainWindowClass(HINSTANCE instance, const wchar_t* class_name,
WNDPROC window_proc) {
INITCOMMONCONTROLSEX ice = {.dwSize = sizeof(ice), .dwICC = ICC_BAR_CLASSES};
InitCommonControlsEx(&ice);
WNDCLASSEXW wnd_class = {};
wnd_class.cbSize = sizeof(wnd_class);
wnd_class.style = CS_HREDRAW | CS_VREDRAW;
wnd_class.lpfnWndProc = window_proc;
wnd_class.cbWndExtra = sizeof(void*);
wnd_class.hInstance = instance;
wnd_class.hIcon = LoadIcon(/*hInstance=*/nullptr, IDI_APPLICATION);
wnd_class.hIconSm = LoadIcon(/*hInstance=*/nullptr, IDI_APPLICATION);
wnd_class.hCursor = LoadCursor(/*hInstance=*/nullptr, IDC_ARROW);
wnd_class.hbrBackground = nullptr;
wnd_class.lpszClassName = class_name;
return RegisterClassExW(&wnd_class) != 0 ||
GetLastError() == ERROR_CLASS_ALREADY_EXISTS;
}
// Creates the horizontal trackbar slider as a child of `parent`. Returns the
// slider's window handle, or nullptr if creation failed.
HWND CreateSliderControl(HWND parent, HINSTANCE instance, ClientSize client) {
constexpr UINT kSliderControlId = 100;
// Page step = 5% of the slider range, so one click moves the thumb
// noticeably.
constexpr int kPageDivisor = 20;
constexpr int kSliderThumbLen = 16; // narrow thumb so it reaches both edges
const int slider_top = client.height - kFooterH - kFooterGap - kSliderH;
HWND slider = CreateWindowExW(
/*dwExStyle=*/0, /*lpClassName=*/TRACKBAR_CLASSW,
/*lpWindowName=*/nullptr,
/*dwStyle=*/WS_CHILD | WS_VISIBLE | TBS_HORZ | TBS_NOTICKS |
TBS_FIXEDLENGTH,
/*x=*/kSliderMarginH, /*y=*/slider_top,
/*nWidth=*/client.width - (kSliderMarginH * 2), /*nHeight=*/kSliderH,
/*hWndParent=*/parent,
/*hMenu=*/reinterpret_cast<HMENU>(static_cast<INT_PTR>(kSliderControlId)),
/*hInstance=*/instance, /*lpParam=*/nullptr);
if (slider == nullptr) {
return nullptr;
}
SendMessageW(slider, TBM_SETRANGEMIN, /*fRedraw=*/FALSE, /*minimum=*/0);
SendMessageW(slider, TBM_SETRANGEMAX, /*fRedraw=*/FALSE,
/*maximum=*/kSliderMax);
SendMessageW(slider, TBM_SETPOS, /*fRedraw=*/TRUE, /*position=*/0);
SendMessageW(slider, TBM_SETPAGESIZE, /*wParam=*/0,
/*pageSize=*/kSliderMax / kPageDivisor);
SendMessageW(slider, TBM_SETTHUMBLENGTH, /*length=*/kSliderThumbLen,
/*lParam=*/0);
// Empty theme name strips the visual style so `WM_CTLCOLORSCROLLBAR`
// messages reach the parent, enabling custom track and thumb colors.
SetWindowTheme(slider, /*pszSubAppName=*/L"", /*pszSubIdList=*/nullptr);
return slider;
}
// Returns recomputed layout rectangles for the current client size and
// repositions the slider child window to match.
LayoutRegions ComputeLayout(HWND slider_hwnd, ClientSize client) {
constexpr int kLabelH = 24; // slider label row height
const int footer_top = client.height - kFooterH;
const int slider_top = footer_top - kFooterGap - kSliderH;
const int label_top = slider_top - kLabelH;
if (slider_hwnd != nullptr) {
SetWindowPos(slider_hwnd, /*hWndInsertAfter=*/nullptr,
/*x=*/kSliderMarginH, /*y=*/slider_top,
/*cx=*/client.width - (kSliderMarginH * 2), /*cy=*/kSliderH,
/*uFlags=*/SWP_NOZORDER | SWP_NOACTIVATE);
}
return {.header = {.left = 0,
.top = 0,
.right = client.width,
.bottom = kHeaderH},
.slider_label = {.left = 0,
.top = label_top,
.right = client.width,
.bottom = slider_top},
.footer = {.left = 0,
.top = footer_top,
.right = client.width,
.bottom = client.height}};
}
// Draws the entire client area into a double-buffered back surface to
// eliminate flicker.
void PaintWindow(HWND hwnd, const PaintContext& ctx) {
PAINTSTRUCT paint_struct;
HDC hdc = BeginPaint(hwnd, &paint_struct);
RECT client_rect;
GetClientRect(hwnd, &client_rect);
const int width = client_rect.right;
const int height = client_rect.bottom;
// Double-buffer: draw into an off-screen bitmap, then `BitBlt` it to the
// screen in one shot to eliminate flicker.
HDC mem_dc = CreateCompatibleDC(hdc);
HBITMAP mem_bmp = CreateCompatibleBitmap(hdc, width, height);
HGDIOBJ old_bmp = SelectObject(mem_dc, mem_bmp);
HBRUSH bg_brush = CreateSolidBrush(ctx.palette.background);
FillRect(mem_dc, &client_rect, bg_brush);
DeleteObject(bg_brush);
PaintHeader(mem_dc, ctx);
PaintSliderLabel(mem_dc, ctx);
PaintFooter(mem_dc, ctx);
BitBlt(hdc, /*x=*/0, /*y=*/0, /*cx=*/width, /*cy=*/height,
/*hdcSrc=*/mem_dc, /*x1=*/0, /*y1=*/0, SRCCOPY);
SelectObject(mem_dc, old_bmp);
DeleteObject(mem_bmp);
DeleteDC(mem_dc);
EndPaint(hwnd, &paint_struct);
}
// Associates the `MainWindow` instance pointer with the window on creation,
// then delegates all messages to the instance's `HandleMessage`.
LRESULT CALLBACK DispatchWindowMessage(HWND hwnd, UINT msg, WPARAM wparam,
LPARAM lparam) {
if (msg == WM_NCCREATE) {
const auto* create_struct = reinterpret_cast<CREATESTRUCTW*>(lparam);
SetWindowLongPtrW(
hwnd, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(create_struct->lpCreateParams));
}
auto* self =
reinterpret_cast<MainWindow*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
if (self != nullptr) {
return self->HandleMessage(hwnd, msg, wparam, lparam);
}
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
} // namespace
MainWindow::MainWindow(AudioPipelineInterface* audio) : audio_(audio) {}
bool MainWindow::Create(HINSTANCE instance, int cmd_show) {
constexpr const wchar_t* kClassName = L"BassBoosterMain";
constexpr int kInitialWidth = 620;
constexpr int kInitialHeight = 240;
if (!RegisterMainWindowClass(instance, kClassName, DispatchWindowMessage)) {
return false;
}
instance_ = instance;
palette_ = BuildPalette();
hwnd_ = CreateWindowExW(
/*dwExStyle=*/0, /*lpClassName=*/kClassName, /*lpWindowName=*/kTitle,
/*dwStyle=*/WS_OVERLAPPEDWINDOW,
/*x=*/CW_USEDEFAULT, /*y=*/CW_USEDEFAULT,
/*nWidth=*/kInitialWidth, /*nHeight=*/kInitialHeight,
/*hWndParent=*/nullptr, /*hMenu=*/nullptr,
/*hInstance=*/instance, /*lpParam=*/this);
if (hwnd_ == nullptr) {
return false;
}
ApplyTitleBarTheme(hwnd_);
ShowWindow(hwnd_, cmd_show);
UpdateWindow(hwnd_);
return true;
}
LRESULT MainWindow::HandleMessage(HWND hwnd, UINT msg, WPARAM wparam,
LPARAM lparam) {
switch (msg) {
case WM_NCCREATE:
hwnd_ = hwnd;
return DefWindowProcW(hwnd, msg, wparam, lparam);
case WM_CREATE: {
RECT client_rect;
GetClientRect(hwnd, &client_rect);
const ClientSize client = {.width = client_rect.right,
.height = client_rect.bottom};
slider_hwnd_ = CreateSliderControl(hwnd, instance_, client);
layout_ = ComputeLayout(slider_hwnd_, client);
InvalidateRect(hwnd, /*lpRect=*/nullptr, /*bErase=*/FALSE);
return 0;
}
case WM_SIZE: {
const ClientSize client = {.width = LOWORD(lparam),
.height = HIWORD(lparam)};
layout_ = ComputeLayout(slider_hwnd_, client);
InvalidateRect(hwnd, /*lpRect=*/nullptr, /*bErase=*/FALSE);
return 0;
}
case WM_GETMINMAXINFO: {
constexpr int kMinWidth = 480;
constexpr int kMinHeight = 200;
auto* info = reinterpret_cast<MINMAXINFO*>(lparam);
info->ptMinTrackSize = {.x = kMinWidth, .y = kMinHeight};
return 0;
}
case WM_PAINT:
PaintWindow(hwnd,
PaintContext{
.palette = palette_, .layout = layout_, .audio = audio_});
return 0;
case WM_ERASEBKGND:
// Return 1 to tell Windows we handled the erase; the actual background
// is painted in `WM_PAINT` via a back-buffer, so erasing here would
// cause flicker.
return 1;
case WM_HSCROLL: {
if (reinterpret_cast<HWND>(lparam) != slider_hwnd_) {
return 0;
}
const LRESULT pos =
SendMessageW(slider_hwnd_, TBM_GETPOS, /*wParam=*/0, /*lParam=*/0);
if (audio_ != nullptr) {
audio_->SetBoostLevel(static_cast<double>(pos) /
static_cast<double>(kSliderMax));
}
InvalidateRect(hwnd, &layout_.header, /*bErase=*/FALSE);
return 0;
}
case WM_SETTINGCHANGE:
case WM_THEMECHANGED:
palette_ = BuildPalette();
ApplyTitleBarTheme(hwnd);
InvalidateRect(hwnd, /*lpRect=*/nullptr, /*bErase=*/TRUE);
return 0;
case WM_CTLCOLORSCROLLBAR:
case WM_CTLCOLORSTATIC:
case WM_CTLCOLORBTN:
return OnCtlColor(
reinterpret_cast<HDC>(wparam),
PaintContext{
.palette = palette_, .layout = layout_, .audio = audio_});
case WM_DESTROY:
if (audio_ != nullptr) {
audio_->Stop();
}
PostQuitMessage(/*nExitCode=*/0);
return 0;
default:
return DefWindowProcW(hwnd, msg, wparam, lparam);
}
}