#include "windows.h" #include "gdi.h" #include "shapes.h" #include #include #include #include #include #include // Visual styles: the uxtheme interface the common controls draw through, answered with the look of Windows 11 (rounded controls, // light fills, thin borders and the accent color) drawn with antialiasing into the caller's device context. The classic look stays // available: RAVEN_THEME=classic turns the styles off, as switching off the theme service does on Windows, and every control then // draws itself the classic way. namespace raven { namespace { using shapes::mix; // A device context as a surface for the shapes: its clip and visible region decide which pixels change. struct PainterSurface { Painter &painter; void blend(int64_t x, int64_t y, uint32_t color, double alpha) { if (!painter.visible(x, y)) return; painter.plot(x, y, alpha >= 0.998 ? color : mix(painter.read(x, y), color, alpha)); } }; using Shapes = shapes::Shapes; constexpr uint32_t s_ok = 0, e_fail = 0x80004005U, e_invalidarg = 0x80070057U, e_handle = 0x80070006U, e_notimpl = 0x80004001U, e_prop = 0x80070490U; constexpr uint32_t rgb(uint32_t r, uint32_t g, uint32_t b) { return r | g << 8 | b << 16; } // The colors controls are drawn with: the Windows 11 light palette, and the dark one the controls of a window take when the program // gives it a "DarkMode_" theme name, as programs do to follow the system's dark mode. struct Palette { uint32_t accent, accent_hot, accent_pressed, accent_dark; uint32_t text, text_secondary, text_disabled, text_on_accent; uint32_t control, control_hot, control_pressed, control_disabled, white; uint32_t stroke, stroke_bottom, stroke_strong, stroke_disabled, divider; uint32_t surface, subtle_hot, subtle_pressed, glyph, thumb, thumb_hot, thumb_pressed, selection, selection_hot; uint32_t box, box_hot, box_pressed; }; constexpr Palette light{ rgb(0x00, 0x5f, 0xb8), rgb(0x19, 0x6e, 0xbf), rgb(0x31, 0x7e, 0xc6), rgb(0x00, 0x3e, 0x92), rgb(0x1a, 0x1a, 0x1a), rgb(0x60, 0x60, 0x60), rgb(0xa0, 0xa0, 0xa0), rgb(0xff, 0xff, 0xff), rgb(0xfb, 0xfb, 0xfb), rgb(0xf6, 0xf6, 0xf6), rgb(0xf3, 0xf3, 0xf3), rgb(0xf5, 0xf5, 0xf5), rgb(0xff, 0xff, 0xff), rgb(0xe5, 0xe5, 0xe5), rgb(0xc4, 0xc4, 0xc4), rgb(0x8a, 0x8a, 0x8a), rgb(0xc5, 0xc5, 0xc5), rgb(0xe0, 0xe0, 0xe0), rgb(0xf3, 0xf3, 0xf3), rgb(0xea, 0xea, 0xea), rgb(0xe0, 0xe0, 0xe0), rgb(0x5c, 0x5c, 0x5c), rgb(0x8a, 0x8a, 0x8a), rgb(0x6e, 0x6e, 0x6e), rgb(0x58, 0x58, 0x58), rgb(0xcc, 0xe8, 0xff), rgb(0xe5, 0xf3, 0xff), rgb(0xf6, 0xf6, 0xf6), rgb(0xed, 0xed, 0xed), rgb(0xe5, 0xe5, 0xe5)}; constexpr Palette dark{ rgb(0x4c, 0xc2, 0xff), rgb(0x47, 0xb1, 0xe8), rgb(0x42, 0xa1, 0xd2), rgb(0x3a, 0x96, 0xc7), rgb(0xff, 0xff, 0xff), rgb(0xc5, 0xc5, 0xc5), rgb(0x6e, 0x6e, 0x6e), rgb(0x00, 0x00, 0x00), rgb(0x2d, 0x2d, 0x2d), rgb(0x32, 0x32, 0x32), rgb(0x27, 0x27, 0x27), rgb(0x2a, 0x2a, 0x2a), rgb(0x1f, 0x1f, 0x1f), rgb(0x3a, 0x3a, 0x3a), rgb(0x4a, 0x4a, 0x4a), rgb(0x9a, 0x9a, 0x9a), rgb(0x4a, 0x4a, 0x4a), rgb(0x3a, 0x3a, 0x3a), rgb(0x20, 0x20, 0x20), rgb(0x2d, 0x2d, 0x2d), rgb(0x29, 0x29, 0x29), rgb(0xc5, 0xc5, 0xc5), rgb(0x9f, 0x9f, 0x9f), rgb(0xb0, 0xb0, 0xb0), rgb(0x8a, 0x8a, 0x8a), rgb(0x26, 0x4f, 0x78), rgb(0x2d, 0x2d, 0x2d), rgb(0x27, 0x27, 0x27), rgb(0x2f, 0x2f, 0x2f), rgb(0x35, 0x35, 0x35)}; struct Box { double left, top, right, bottom; double width() const { return right - left; } double height() const { return bottom - top; } double cx() const { return (left + right) / 2; } double cy() const { return (top + bottom) / 2; } }; // A control surface: rounded, filled, with a light border whose bottom edge is a shade darker, as Windows 11 buttons and fields. void control_surface(Shapes &shapes, const Box &box, uint32_t fill, uint32_t border, std::optional bottom, double radius = 4) { shapes.rounded(box.left, box.top, box.right, box.bottom, radius, fill, border); if (bottom) shapes.segment(box.left + radius, box.bottom - 0.5, box.right - radius, box.bottom - 0.5, 1, *bottom); } void draw_button(Shapes &shapes, const Palette &tone, int part, int state, const Box &box) { switch (part) { case 1: { // BP_PUSHBUTTON: normal, hot, pressed, disabled, defaulted, defaulted animating if (state == 5 || state == 6) { control_surface(shapes, box, tone.accent, tone.accent, tone.accent_dark); return; } const uint32_t fill = state == 2 ? tone.control_hot : state == 3 ? tone.control_pressed : state == 4 ? tone.control_disabled : tone.control; control_surface(shapes, box, fill, tone.stroke, state == 3 || state == 4 ? std::nullopt : std::optional(tone.stroke_bottom)); return; } case 2: { // BP_RADIOBUTTON: unchecked and checked, each normal, hot, pressed, disabled const double size = std::min(box.width(), box.height()), radius = size / 2, cx = box.left + radius, cy = box.cy(); const int phase = (state - 1) % 4; if (state <= 4) { shapes.circle(cx, cy, radius, phase == 1 ? tone.box_hot : phase == 2 ? tone.box_pressed : tone.box, phase == 3 ? tone.stroke_disabled : tone.stroke_strong); } else { shapes.circle(cx, cy, radius, phase == 3 ? tone.stroke_disabled : phase == 1 ? tone.accent_hot : phase == 2 ? tone.accent_pressed : tone.accent, std::nullopt); shapes.circle(cx, cy, radius * (phase == 1 ? 0.5 : phase == 2 ? 0.36 : 0.43), tone.text_on_accent, std::nullopt); } return; } case 3: { // BP_CHECKBOX: unchecked, checked, mixed, implicit and excluded, each normal, hot, pressed, disabled const double size = std::min(box.width(), box.height()); const Box square{box.left, box.cy() - size / 2, box.left + size, box.cy() + size / 2}; const int kind = (state - 1) / 4, phase = (state - 1) % 4; if (kind == 0 || kind > 2) { shapes.rounded(square.left, square.top, square.right, square.bottom, 3, phase == 1 ? tone.box_hot : phase == 2 ? tone.box_pressed : tone.box, phase == 3 ? tone.stroke_disabled : tone.stroke_strong); return; } shapes.rounded(square.left, square.top, square.right, square.bottom, 3, phase == 3 ? tone.stroke_disabled : phase == 1 ? tone.accent_hot : phase == 2 ? tone.accent_pressed : tone.accent, std::nullopt); const double s = size; if (kind == 1) { shapes.segment(square.left + s * 0.24, square.top + s * 0.52, square.left + s * 0.42, square.top + s * 0.70, s * 0.11, tone.text_on_accent); shapes.segment(square.left + s * 0.42, square.top + s * 0.70, square.left + s * 0.78, square.top + s * 0.32, s * 0.11, tone.text_on_accent); } else { shapes.segment(square.left + s * 0.28, square.cy(), square.right - s * 0.28, square.cy(), s * 0.11, tone.text_on_accent); } return; } case 4: // BP_GROUPBOX shapes.rounded(box.left, box.top, box.right, box.bottom, 4, std::nullopt, tone.stroke); return; default: control_surface(shapes, box, state == 2 ? tone.control_hot : tone.control, tone.stroke, tone.stroke_bottom); } } // A text field: white when it has the keyboard, with a strong bottom line that turns into the accent. void draw_field(Shapes &shapes, const Palette &tone, int state, const Box &box, bool focused) { const bool disabled = state == 4, hot = state == 2; shapes.rounded(box.left, box.top, box.right, box.bottom, 4, disabled ? tone.control_disabled : focused ? tone.white : hot ? tone.control_hot : tone.control, tone.stroke); if (focused) shapes.rounded(box.left + 1, box.bottom - 2, box.right - 1, box.bottom, 1, tone.accent, std::nullopt); else if (!disabled) shapes.segment(box.left + 3, box.bottom - 0.5, box.right - 3, box.bottom - 0.5, 1, tone.stroke_strong); } void draw_scrollbar(Shapes &shapes, const Palette &tone, int part, int state, const Box &box) { switch (part) { case 1: { // SBP_ARROWBTN: up, down, left, right; each normal, hot, pressed, disabled, then the hover states const int direction = state <= 16 ? (state - 1) / 4 : (state - 17) % 4; const int phase = state <= 16 ? (state - 1) % 4 : 1; const uint32_t color = phase == 3 ? tone.stroke_disabled : phase == 2 ? tone.thumb_pressed : phase == 1 ? tone.thumb_hot : tone.thumb; shapes.fill(box.left, box.top, box.right, box.bottom, tone.surface); shapes.triangle(box.cx(), box.cy(), std::min(box.width(), box.height()) * 0.42, "udlr"[direction], color); return; } case 2: case 3: { // SBP_THUMBBTNHORZ, SBP_THUMBBTNVERT: a pill narrower than the bar const bool vertical = part == 3; const uint32_t color = state == 3 ? tone.thumb_pressed : state == 2 ? tone.thumb_hot : tone.thumb; shapes.fill(box.left, box.top, box.right, box.bottom, tone.surface); const double thickness = std::max(3.0, (vertical ? box.width() : box.height()) * 0.4); if (vertical) shapes.rounded(box.cx() - thickness / 2, box.top + 2, box.cx() + thickness / 2, box.bottom - 2, thickness / 2, color, std::nullopt); else shapes.rounded(box.left + 2, box.cy() - thickness / 2, box.right - 2, box.cy() + thickness / 2, thickness / 2, color, std::nullopt); return; } case 4: case 5: case 6: case 7: // the tracks above, below and beside the thumb shapes.fill(box.left, box.top, box.right, box.bottom, tone.surface); return; case 10: // SBP_SIZEBOX: three dots in a corner shapes.fill(box.left, box.top, box.right, box.bottom, tone.surface); for (int dot = 0; dot < 3; ++dot) shapes.circle(box.right - 4 - dot * 4, box.bottom - 4, 1, tone.thumb, std::nullopt), shapes.circle(box.right - 4, box.bottom - 4 - dot * 4, 1, tone.thumb, std::nullopt); return; default: return; } } void draw_header(Shapes &shapes, const Palette &tone, int part, int state, const Box &box) { if (part == 4) { // HP_HEADERSORTARROW: sorted up, sorted down shapes.chevron(box.cx(), box.cy(), std::min(box.width(), 8.0), state == 2 ? 'd' : 'u', tone.text_secondary); return; } const uint32_t fill = state == 3 || state == 6 || state == 9 ? tone.subtle_pressed : state == 2 || state == 5 || state == 8 ? rgb(0xf5, 0xf5, 0xf5) : tone.white; shapes.fill(box.left, box.top, box.right, box.bottom, fill); if (part != 0) shapes.fill(box.right - 1, box.top + 4, box.right, box.bottom - 4, tone.divider); shapes.fill(box.left, box.bottom - 1, box.right, box.bottom, tone.divider); } void draw_tab(Shapes &shapes, const Palette &tone, int part, int state, const Box &box) { if (part == 9 || part == 10) { // TABP_PANE, TABP_BODY shapes.rounded(box.left, box.top, box.right, box.bottom, 4, tone.white, tone.stroke); return; } if (state == 3 || state == 5) // selected or focused: joined to the pane shapes.rounded(box.left, box.top, box.right, box.bottom + 4, 4, tone.white, tone.stroke); else if (state == 2) shapes.rounded(box.left + 1, box.top + 2, box.right - 1, box.bottom - 1, 4, tone.subtle_hot, std::nullopt); } void draw_toolbar(Shapes &shapes, const Palette &tone, int part, int state, const Box &box) { if (part == 5 || part == 6) { // TP_SEPARATOR, TP_SEPARATORVERT if (part == 5) shapes.fill(std::floor(box.cx()), box.top + 3, std::floor(box.cx()) + 1, box.bottom - 3, tone.divider); else shapes.fill(box.left + 3, std::floor(box.cy()), box.right - 3, std::floor(box.cy()) + 1, tone.divider); return; } const bool checked = state == 5 || state == 6; if (state == 2 || state == 3 || checked) shapes.rounded(box.left, box.top, box.right, box.bottom, 4, state == 3 ? tone.subtle_pressed : checked && state != 6 ? rgb(0xe5, 0xe5, 0xe5) : tone.subtle_hot, checked ? std::optional(rgb(0xd0, 0xd0, 0xd0)) : std::nullopt); if (part == 2 || part == 4) // the dropdown arrow shapes.chevron(part == 4 ? box.cx() : box.right - 6, box.cy(), 6, 'd', state == 4 ? tone.text_disabled : tone.glyph); } void draw_part(Shapes &shapes, const Palette &tone, const std::string &klass, int part, int state, const Box &box, bool focused) { if (klass == "button") draw_button(shapes, tone, part, state, box); else if (klass == "edit") draw_field(shapes, tone, state, box, focused || state == 5); else if (klass == "combobox") { if (part == 1 || part == 6) { // CP_DROPDOWNBUTTON, CP_DROPDOWNBUTTONRIGHT if (state == 2 || state == 3) shapes.rounded(box.left + 2, box.top + 2, box.right - 2, box.bottom - 2, 3, state == 3 ? tone.subtle_pressed : tone.subtle_hot, std::nullopt); shapes.chevron(box.cx(), box.cy(), 8, 'd', state == 4 ? tone.text_disabled : tone.glyph); } else { draw_field(shapes, tone, state, box, focused); } } else if (klass == "listbox") shapes.rounded(box.left, box.top, box.right, box.bottom, 4, std::nullopt, tone.stroke); else if (klass == "scrollbar") draw_scrollbar(shapes, tone, part, state, box); else if (klass == "header") draw_header(shapes, tone, part, state, box); else if (klass == "treeview") { if (part == 2) // TVP_GLYPH: closed, opened shapes.chevron(box.cx(), box.cy(), std::min(box.width(), box.height()) * 0.5, state == 2 ? 'd' : 'r', tone.glyph); else if (part == 1 && state >= 2 && state != 4) shapes.rounded(box.left, box.top, box.right, box.bottom, 4, state == 2 ? tone.selection_hot : tone.selection, std::nullopt); } else if (klass == "listview") { if (part == 1 && state >= 2 && state != 4) shapes.rounded(box.left, box.top, box.right, box.bottom, 4, state == 2 ? tone.selection_hot : tone.selection, std::nullopt); } else if (klass == "tab") draw_tab(shapes, tone, part, state, box); else if (klass == "toolbar") draw_toolbar(shapes, tone, part, state, box); else if (klass == "rebar") { if (part == 4) // RP_CHEVRON shapes.chevron(box.cx(), box.cy(), 6, 'r', tone.glyph); else if (part == 0 || part == 3) shapes.fill(box.left, box.top, box.right, box.bottom, tone.surface); } else if (klass == "status") { if (part == 0) shapes.fill(box.left, box.top, box.right, box.bottom, tone.surface); else if (part == 1 || part == 2) // SP_PANE, SP_GRIPPERPANE: a divider at the right shapes.fill(box.right - 1, box.top + 3, box.right, box.bottom - 3, tone.divider); else if (part == 3) for (int row = 0; row < 3; ++row) for (int column = 0; column <= row; ++column) shapes.circle(box.right - 3 - column * 4, box.bottom - 3 - (2 - row) * 4, 1, tone.thumb, std::nullopt); } else if (klass == "progress") { if (part == 1 || part == 2) // the track: a thin rounded groove shapes.rounded(box.left, box.cy() - 2, box.right, box.cy() + 2, 2, rgb(0xd6, 0xd6, 0xd6), std::nullopt); else if (part == 3 || part == 4 || part == 5 || part == 6) shapes.fill(box.left, std::max(box.top, box.cy() - 2), box.right, std::min(box.bottom, box.cy() + 2), tone.accent); } else if (klass == "trackbar") { if (part == 1) shapes.rounded(box.left, box.cy() - 2, box.right, box.cy() + 2, 2, tone.thumb, std::nullopt); else if (part == 2) shapes.rounded(box.cx() - 2, box.top, box.cx() + 2, box.bottom, 2, tone.thumb, std::nullopt); else if (part >= 3 && part <= 8) { const double radius = std::min(box.width(), box.height()) / 2; shapes.circle(box.cx(), box.cy(), radius, tone.white, tone.stroke); shapes.circle(box.cx(), box.cy(), radius * (state == 2 ? 0.6 : state == 3 ? 0.4 : 0.5), state == 5 ? tone.stroke_disabled : tone.accent, std::nullopt); } } else if (klass == "spin") { if (state == 2 || state == 3) shapes.rounded(box.left, box.top, box.right, box.bottom, 3, state == 3 ? tone.subtle_pressed : tone.subtle_hot, std::nullopt); const char direction = part == 1 ? 'u' : part == 2 ? 'd' : part == 3 ? 'r' : 'l'; shapes.chevron(box.cx(), box.cy(), std::min(box.width(), box.height()) * 0.45, direction, state == 4 ? tone.text_disabled : tone.glyph); } } // The color of text on a part, which the controls ask for and which DrawThemeText uses. uint32_t text_color(const Palette &tone, const std::string &klass, int part, int state) { if (klass == "button") { if (part == 1 && (state == 5 || state == 6)) return tone.text_on_accent; if ((part == 1 && state == 4) || ((part == 2 || part == 3) && (state - 1) % 4 == 3)) return tone.text_disabled; return tone.text; } if ((klass == "tab" || klass == "toolbar" || klass == "edit" || klass == "combobox") && state == 4) return tone.text_disabled; return tone.text; } // A clip region narrowed to a rectangle. std::optional clipped(const std::optional &clip, const Box &box) { DesktopRegion region; region.add({int32_t(box.left), int32_t(box.top), int32_t(box.right), int32_t(box.bottom)}); if (!clip) return region; DesktopRegion both; both.combine(*clip, region, 1); return both; } bool rounded_part(const std::string &klass, int part) { return klass == "button" || (klass == "tab" && part != 9 && part != 10) || klass == "combobox" || klass == "edit" || klass == "spin"; } } bool NativeWindows::themed() const { static const bool classic = [] { const char *chosen = std::getenv("RAVEN_THEME"); return chosen && std::string_view(chosen) == "classic"; }(); return !classic; } void NativeWindows::register_theme_api(ApiRegistry ®istry) { const auto palette = [this](uint64_t theme) -> const Palette & { return dark_themes.contains(theme) ? dark : light; }; // A window a program allowed dark mode takes the dark variant of the application names it opens, as the system does while the app // mode forces dark; the system itself is light, so allowing dark alone changes nothing. const auto dark_allowed = [this](uint64_t window) { return app_mode == 2 && dark_allowed_windows.contains(window); }; const auto open = [this, dark_allowed](ApiCall &call, uint64_t window, uint64_t list) { if (!themed() || (window && unthemed_windows.contains(window)) || !list) { call.fail(1168, 0); return; } // The first class of the list this engine draws; a prefix such as "Explorer::" names a variant of the same class, and a // "DarkMode_" one, or that application name given to the window, its dark variant. static const std::set known{"button", "edit", "combobox", "listbox", "scrollbar", "header", "listview", "treeview", "tab", "toolbar", "rebar", "status", "progress", "trackbar", "spin", "window", "menu", "tooltip", "explorerbar", "datepicker", "taskdialog", "textstyle"}; const auto text = call.wide(list, 512); size_t start = 0; while (start <= text.size()) { auto end = text.find(u';', start); if (end == text.npos) end = text.size(); std::string name; for (size_t at = start; at < end; ++at) name.push_back(char(text[at] < 128 ? std::tolower(int(text[at])) : '?')); bool dark = window && dark_windows.contains(window); if (const auto colon = name.rfind("::"); colon != name.npos) { dark = dark || name.find("darkmode_") != name.npos || (window && dark_allowed(window)); name = name.substr(colon + 2); } dark = dark || (window && named_windows.contains(window) && dark_allowed(window)); while (!name.empty() && name.front() == ' ') name.erase(name.begin()); if (known.contains(name)) { const uint64_t handle = next_theme++; themes[handle] = name; if (dark) dark_themes.insert(handle); if (window) window_themes[window] = handle; call.ret(handle); return; } start = end + 1; } call.fail(1168, 0); }; registry.add("OpenThemeData", 2, [open](ApiCall &call) { open(call, call[0], call[1]); }); registry.add("OpenThemeDataEx", 3, [open](ApiCall &call) { open(call, call[0], call[1]); }); registry.add("OpenThemeDataForDpi", 3, [open](ApiCall &call) { open(call, call[0], call[1]); }); registry.add("CloseThemeData", 1, [this](ApiCall &call) { dark_themes.erase(call[0]); if (!themes.erase(call[0])) { call.ret(call[0] ? e_handle : s_ok); return; } std::erase_if(window_themes, [&](const auto &entry) { return entry.second == call[0]; }); call.ret(s_ok); }); registry.add("GetWindowTheme", 1, [this](ApiCall &call) { const auto found = window_themes.find(call[0]); call.ret(found != window_themes.end() && themes.contains(found->second) ? found->second : 0); }); // SetWindowTheme with empty names takes a window out of visual styles, as programs do for controls they draw the classic way; a // "DarkMode_" application name gives its controls the dark palette. registry.add("SetWindowTheme", 3, [this](ApiCall &call) { const bool off = call[1] && call[2] && call.wide(call[1], 4).empty() && call.wide(call[2], 4).empty(); if (off) unthemed_windows.insert(call[0]); else unthemed_windows.erase(call[0]); std::u16string application = call[1] ? call.wide(call[1], 64) : std::u16string(); for (auto &character : application) if (character < 128) character = char16_t(std::tolower(int(character))); if (application.starts_with(u"darkmode_")) dark_windows.insert(call[0]); else dark_windows.erase(call[0]); if (!application.empty()) named_windows.insert(call[0]); else named_windows.erase(call[0]); // The window hears that its theme changed, as controls reopen their theme data then. if (objects.window(call[0])) send(call[0], 0x31a, 0, 0); call.ret(s_ok); }); // The dark mode functions programs take by number: the window and app permissions the theme data follows, and the system's own // preference, which is light. registry.add("AllowDarkModeForWindow", 2, [this](ApiCall &call) { if (!objects.window(call[0])) { call.ret(0); return; } if (call.u32(1) & 0xff) dark_allowed_windows.insert(call[0]); else dark_allowed_windows.erase(call[0]); call.ret(1); }); registry.add("IsDarkModeAllowedForWindow", 1, [this](ApiCall &call) { call.ret(dark_allowed_windows.contains(call[0])); }); registry.add("SetPreferredAppMode", 1, [this](ApiCall &call) { const uint32_t previous = app_mode; if (call.u32(0) <= 3) app_mode = call.u32(0); call.ret(previous); }); registry.add("IsDarkModeAllowedForApp", 0, [this](ApiCall &call) { call.ret(app_mode == 1 || app_mode == 2); }); registry.add("ShouldAppsUseDarkMode", 0, [](ApiCall &call) { call.ret(0); }); registry.add("ShouldSystemUseDarkMode", 0, [](ApiCall &call) { call.ret(0); }); registry.add("GetIsImmersiveColorUsingHighContrast", 1, [](ApiCall &call) { call.ret(0); }); registry.add("RefreshImmersiveColorPolicyState", 0, [](ApiCall &call) { call.ret(0); }); registry.add("FlushMenuThemes", 0, [](ApiCall &call) { call.ret(0); }); registry.add("IsThemeActive", 0, [this](ApiCall &call) { call.ret(themed()); }); registry.add("IsAppThemed", 0, [this](ApiCall &call) { call.ret(themed()); }); registry.add("IsCompositionActive", 0, [this](ApiCall &call) { call.ret(themed()); }); registry.add("GetThemeAppProperties", 0, [this](ApiCall &call) { call.ret(themed() ? 7 : 0); }); registry.add("SetThemeAppProperties", 1, [](ApiCall &call) { call.ret(0); }); registry.add("EnableThemeDialogTexture", 2, [](ApiCall &call) { call.ret(s_ok); }); registry.add("IsThemeDialogTextureEnabled", 1, [](ApiCall &call) { call.ret(0); }); registry.add("IsThemePartDefined", 3, [this](ApiCall &call) { call.ret(themes.contains(call[0])); }); registry.add("IsThemeBackgroundPartiallyTransparent", 3, [this](ApiCall &call) { const auto found = themes.find(call[0]); call.ret(found != themes.end() && rounded_part(found->second, call.i32(1))); }); const auto read_box = [](ApiCall &call, uint64_t address) { const auto bytes = call.read(address, 16); int32_t values[4]; std::memcpy(values, bytes.data(), 16); return Box{double(values[0]), double(values[1]), double(values[2]), double(values[3])}; }; // DrawThemeBackground(theme, dc, part, state, rect, clip rect) and the Ex form, whose options may carry the clip. const auto background = [this, read_box, palette](ApiCall &call, bool extended) { const auto found = themes.find(call[0]); auto *dc = objects.dc(call[1]); if (found == themes.end() || !dc || !call[4]) { call.ret(found == themes.end() ? e_handle : e_invalidarg); return; } auto box = read_box(call, call[4]); std::optional clip; if (!extended && call[5]) clip = read_box(call, call[5]); else if (extended && call[5]) { // DTBGOPTS: size, flags, then the clip rectangle when DTBG_CLIPRECT (1) is set. const auto options = call.read(call[5], 24); uint32_t flags; std::memcpy(&flags, options.data() + 4, 4); if (flags & 1) clip = read_box(call, call[5] + 8); } const auto saved_clip = dc->application_clip; if (clip) dc->application_clip = clipped(dc->application_clip, *clip); try { Painter painter(objects, *dc); PainterSurface surface{painter}; Shapes shapes{surface, double(dc->origin_x), double(dc->origin_y)}; draw_part(shapes, palette(call[0]), found->second, call.i32(2), call.i32(3), box, dc->window && dc->window == focus); dc->application_clip = saved_clip; if (painter.changed()) if (dc->window && dc->leased) drawing(dc->window); } catch (uint32_t) { dc->application_clip = saved_clip; } call.ret(s_ok); }; registry.add("DrawThemeBackground", 6, [background](ApiCall &call) { background(call, false); }); registry.add("DrawThemeBackgroundEx", 6, [background](ApiCall &call) { background(call, true); }); // The parent paints itself behind a control with rounded corners: its background and client drawing go to the control's context, // shifted so that its pixels land where they show through. registry.add("DrawThemeParentBackground", 3, [this, read_box](ApiCall &call) { auto *dc = objects.dc(call[1]); const auto *window = objects.window(call[0]); const auto *parent = window && window->parent ? objects.window(window->parent) : nullptr; if (!dc || !parent) { call.ret(e_fail); return; } const int32_t dx = window->x - parent->x, dy = window->y - parent->y; const uint64_t parent_handle = window->parent; const auto saved_x = dc->origin_x, saved_y = dc->origin_y; const auto saved_clip = dc->application_clip; if (call[2]) dc->application_clip = clipped(dc->application_clip, read_box(call, call[2])); // The clip is kept in the control's coordinates, so it moves with the origin. if (dc->application_clip) dc->application_clip->offset(dx, dy); dc->origin_x -= dx; dc->origin_y -= dy; send(parent_handle, 0x14, call[1], 0); send(parent_handle, 0x318, call[1], 4); if (auto *again = objects.dc(call[1])) { again->origin_x = saved_x; again->origin_y = saved_y; again->application_clip = saved_clip; } call.ret(s_ok); }); // The content of a part sits inside its border and padding; the extent is the part around a given content. const auto padding = [](const std::string &klass, int part) { if (klass == "button" && part == 1) return 4; if (klass == "edit" || klass == "combobox" || klass == "listbox") return 2; if (klass == "tab" || klass == "header") return 3; return 0; }; registry.add("GetThemeBackgroundContentRect", 6, [this, read_box, padding](ApiCall &call) { const auto found = themes.find(call[0]); if (found == themes.end() || !call[4] || !call[5]) { call.ret(found == themes.end() ? e_handle : e_invalidarg); return; } const auto box = read_box(call, call[4]); const int inset = padding(found->second, call.i32(2)); const int32_t values[4]{int32_t(box.left) + inset, int32_t(box.top) + inset, int32_t(box.right) - inset, int32_t(box.bottom) - inset}; call.write(call[5], std::span(reinterpret_cast(values), 16)); call.ret(s_ok); }); registry.add("GetThemeBackgroundExtent", 6, [this, read_box, padding](ApiCall &call) { const auto found = themes.find(call[0]); if (found == themes.end() || !call[4] || !call[5]) { call.ret(found == themes.end() ? e_handle : e_invalidarg); return; } const auto box = read_box(call, call[4]); const int inset = padding(found->second, call.i32(2)); const int32_t values[4]{int32_t(box.left) - inset, int32_t(box.top) - inset, int32_t(box.right) + inset, int32_t(box.bottom) + inset}; call.write(call[5], std::span(reinterpret_cast(values), 16)); call.ret(s_ok); }); // GetThemePartSize(theme, dc, part, state, rect, size kind, SIZE): the glyphs the controls lay out around. registry.add("GetThemePartSize", 7, [this](ApiCall &call) { const auto found = themes.find(call[0]); if (found == themes.end() || !call[6]) { call.ret(found == themes.end() ? e_handle : e_invalidarg); return; } const auto &klass = found->second; const int part = call.i32(2); int32_t size[2]{0, 0}; if (klass == "button" && (part == 2 || part == 3)) size[0] = size[1] = 13; else if (klass == "treeview" && part == 2) size[0] = size[1] = 12; else if (klass == "header" && part == 4) size[0] = 9, size[1] = 5; else if (klass == "trackbar" && part >= 3 && part <= 8) size[0] = size[1] = 18; else if (klass == "toolbar" && (part == 2 || part == 4)) size[0] = 13, size[1] = 13; else if (klass == "rebar" && part == 4) size[0] = size[1] = 16; else if (klass == "scrollbar") size[0] = size[1] = 17; else { call.ret(e_notimpl); return; } call.write(call[6], std::span(reinterpret_cast(size), 8)); call.ret(s_ok); }); registry.add("GetThemeMargins", 7, [](ApiCall &call) { if (call[6]) { const int32_t zero[4]{0, 0, 0, 0}; call.write(call[6], std::span(reinterpret_cast(zero), 16)); } call.ret(s_ok); }); // GetThemeColor(theme, part, state, property, COLORREF): text, fill and border colors. registry.add("GetThemeColor", 5, [this, palette](ApiCall &call) { const auto found = themes.find(call[0]); if (found == themes.end() || !call[4]) { call.ret(found == themes.end() ? e_handle : e_invalidarg); return; } const auto &tone = palette(call[0]); uint32_t color; switch (call.u32(3)) { case 3803: // TMT_TEXTCOLOR color = text_color(palette(call[0]), found->second, call.i32(1), call.i32(2)); break; case 3802: // TMT_FILLCOLOR case 3821: // TMT_FILLCOLORHINT color = found->second == "status" || found->second == "rebar" || found->second == "toolbar" ? tone.surface : tone.white; break; case 3801: // TMT_BORDERCOLOR case 3822: // TMT_BORDERCOLORHINT color = tone.stroke; break; case 3810: // TMT_EDGEHIGHLIGHTCOLOR case 3808: // TMT_EDGELIGHTCOLOR color = tone.white; break; case 3811: // TMT_EDGESHADOWCOLOR case 3812: // TMT_EDGEDKSHADOWCOLOR color = tone.divider; break; case 3823: // TMT_ACCENTCOLORHINT color = tone.accent; break; default: call.ret(e_prop); return; } call.store(call[4], color); call.ret(s_ok); }); // Progress bars fill smoothly: one-pixel chunks with no space between them. registry.add("GetThemeInt", 5, [this](ApiCall &call) { const auto found = themes.find(call[0]); if (found == themes.end() || !call[4]) { call.ret(found == themes.end() ? e_handle : e_invalidarg); return; } const uint32_t property = call.u32(3); if (found->second == "progress" && (property == 2411 || property == 2412)) { call.store(call[4], property == 2411 ? 1 : 0); call.ret(s_ok); return; } call.ret(e_prop); }); for (const char *name : {"GetThemeFont", "GetThemeBool", "GetThemeEnumValue", "GetThemeRect", "GetThemePosition", "GetThemeString", "GetThemeFilename", "GetThemeMetric", "GetThemeIntList", "GetThemePropertyOrigin", "GetThemeStream"}) registry.add(name, std::string_view(name) == "GetThemeFont" || std::string_view(name) == "GetThemeMetric" ? 6 : 5, [](ApiCall &call) { call.ret(e_prop); }); registry.add("GetThemeTransitionDuration", 6, [](ApiCall &call) { call.ret(e_prop); }); registry.add("GetThemeSysColor", 2, [this](ApiCall &call) { call.ret(objects.system_color(call.i32(1))); }); registry.add("GetThemeSysSize", 2, [](ApiCall &call) { call.ret(0); }); registry.add("GetThemeSysBool", 2, [](ApiCall &call) { call.ret(0); }); registry.add("GetThemeSysFont", 3, [](ApiCall &call) { call.ret(e_prop); }); registry.add("GetThemeSysInt", 3, [](ApiCall &call) { call.ret(e_prop); }); registry.add("GetThemeSysString", 4, [](ApiCall &call) { call.ret(e_prop); }); registry.add("GetThemeDocumentationProperty", 4, [](ApiCall &call) { call.ret(e_prop); }); registry.add("GetCurrentThemeName", 6, [this](ApiCall &call) { if (!themed()) { call.ret(e_fail); return; } if (call[0] && call.u32(1)) call.put_wide(call[0], u"C:\\Windows\\resources\\Themes\\Aero\\Aero.msstyles", call.u32(1)); if (call[2] && call.u32(3)) call.put_wide(call[2], u"NormalColor", call.u32(3)); if (call[4] && call.u32(5)) call.put_wide(call[4], u"NormalSize", call.u32(5)); call.ret(s_ok); }); // HitTestThemeBackground(theme, dc, part, state, options, rect, region, POINT, code): the point arrives as one 64-bit argument. registry.add("HitTestThemeBackground", 9, [read_box](ApiCall &call) { if (!call[5] || !call[8]) { call.ret(e_invalidarg); return; } // HTCLIENT inside the rectangle, HTNOWHERE outside: the parts have no transparent regions to test. const auto box = read_box(call, call[5]); const int32_t x = int32_t(call[7] & 0xffffffff), y = int32_t(call[7] >> 32); call.store(call[8], x >= box.left && x < box.right && y >= box.top && y < box.bottom ? 1 : 0); call.ret(s_ok); }); // DrawThemeEdge draws the light Windows 11 border on the sides asked for, and answers the content inside it. registry.add("DrawThemeEdge", 8, [this, read_box, palette](ApiCall &call) { auto *dc = objects.dc(call[1]); if (!themes.contains(call[0]) || !dc || !call[4]) { call.ret(e_invalidarg); return; } const auto box = read_box(call, call[4]); const uint32_t sides = call.u32(6); const auto &tone = palette(call[0]); try { Painter painter(objects, *dc); PainterSurface surface{painter}; Shapes shapes{surface, double(dc->origin_x), double(dc->origin_y)}; if (sides & 1) shapes.fill(box.left, box.top, box.left + 1, box.bottom, tone.stroke); if (sides & 2) shapes.fill(box.left, box.top, box.right, box.top + 1, tone.stroke); if (sides & 4) shapes.fill(box.right - 1, box.top, box.right, box.bottom, tone.stroke); if (sides & 8) shapes.fill(box.left, box.bottom - 1, box.right, box.bottom, tone.stroke); if (painter.changed()) if (dc->window && dc->leased) drawing(dc->window); } catch (uint32_t) { } if (call[7]) { const int32_t values[4]{int32_t(box.left) + (sides & 1 ? 1 : 0), int32_t(box.top) + (sides & 2 ? 1 : 0), int32_t(box.right) - (sides & 4 ? 1 : 0), int32_t(box.bottom) - (sides & 8 ? 1 : 0)}; call.write(call[7], std::span(reinterpret_cast(values), 16)); } call.ret(s_ok); }); // DrawThemeText(theme, dc, part, state, text, count, flags, flags2, rect) and the Ex form: the text in the part's color, placed as // DrawText places it, through the device context's own text output. const auto draw_text = [this, ®istry, read_box, palette](ApiCall &call, bool extended) { const auto found = themes.find(call[0]); auto *dc = objects.dc(call[1]); if (found == themes.end() || !dc || !call[4] || !call[8]) { call.ret(found == themes.end() ? e_handle : e_invalidarg); return; } const int32_t count = call.i32(5); auto text = count < 0 ? call.wide(call[4], 4096) : call.wide(call[4], size_t(std::min(count, 4096))); if (count >= 0 && text.size() > size_t(count)) text.resize(size_t(count)); const uint32_t flags = call.u32(6); const auto box = read_box(call, call[8]); uint32_t color = text_color(palette(call[0]), found->second, call.i32(2), call.i32(3)); if (extended && call[9]) { // DTTOPTS: size, flags, then the text color when DTT_TEXTCOLOR (1) is set. const auto options = call.read(call[9], 12); uint32_t options_flags; std::memcpy(&options_flags, options.data() + 4, 4); if (options_flags & 1) std::memcpy(&color, options.data() + 8, 4); } // Prefix characters mark mnemonics: the ampersand goes, a doubled one stays as one. if (!(flags & 0x800)) { std::u16string plain; for (size_t at = 0; at < text.size(); ++at) { if (text[at] == u'&' && at + 1 < text.size()) ++at; else if (text[at] == u'&') continue; plain.push_back(text[at]); } text = std::move(plain); } std::vector lines; if (flags & 0x20) lines.push_back(text); else for (size_t start = 0;;) { const auto end = text.find(u'\n', start); auto line = text.substr(start, end == text.npos ? text.npos : end - start); if (!line.empty() && line.back() == u'\r') line.pop_back(); lines.push_back(line); if (end == text.npos) break; start = end + 1; } const auto extent = registry.find("GetTextExtentPoint32W"); const auto output = registry.find("ExtTextOutW"); if (!extent || !output) { call.ret(e_fail); return; } const auto scratch = process.map_memory(65536, PROT_READ | PROT_WRITE); try { const auto measure = [&](const std::u16string &line) { call.put_wide(scratch, line, line.size() + 1); ApiCall inner(call.process, call.environment, {call[1], scratch, line.size(), scratch + 32768}); extent->second(inner); const auto bytes = call.read(scratch + 32768, 8); int32_t size[2]; std::memcpy(size, bytes.data(), 8); return std::pair{size[0], size[1]}; }; int32_t line_height = measure(u"Ag").second; const int32_t total = line_height * int32_t(lines.size()); double y = flags & 0x8 ? box.bottom - total : flags & 0x4 ? box.top + (box.height() - total) / 2 : box.top; const auto saved_color = dc->text_color; const auto saved_mode = dc->background_mode; dc->text_color = color; dc->background_mode = 1; for (auto line : lines) { auto width = measure(line).first; // DT_END_ELLIPSIS shortens a line that does not fit and ends it with three dots. if ((flags & 0x8000) && width > box.width()) { while (!line.empty() && measure(line + u"...").first > box.width()) line.pop_back(); line += u"..."; width = measure(line).first; } const double x = flags & 0x2 ? box.right - width : flags & 0x1 ? box.left + (box.width() - width) / 2 : box.left; const int32_t clip[4]{int32_t(box.left), int32_t(box.top), int32_t(box.right), int32_t(box.bottom)}; call.write(scratch + 32768, std::span(reinterpret_cast(clip), 16)); call.put_wide(scratch, line, line.size() + 1); ApiCall inner(call.process, call.environment, {call[1], uint64_t(int64_t(std::lround(x))), uint64_t(int64_t(std::lround(y))), uint64_t((flags & 0x100) ? 0 : 4), scratch + 32768, scratch, line.size(), 0}); output->second(inner); y += line_height; } if (auto *again = objects.dc(call[1])) { again->text_color = saved_color; again->background_mode = saved_mode; } } catch (...) { process.unmap_memory(scratch); throw; } process.unmap_memory(scratch); call.ret(s_ok); }; registry.add("DrawThemeText", 9, [draw_text](ApiCall &call) { draw_text(call, false); }); registry.add("DrawThemeTextEx", 10, [draw_text](ApiCall &call) { draw_text(call, true); }); // GetThemeTextExtent(theme, dc, part, state, text, count, flags, bounding rect, extent rect): the size the text takes in the DC. registry.add("GetThemeTextExtent", 9, [this, ®istry](ApiCall &call) { if (!themes.contains(call[0]) || !call[4] || !call[8]) { call.ret(e_invalidarg); return; } const auto extent = registry.find("GetTextExtentPoint32W"); const int32_t count = call.i32(5); auto text = count < 0 ? call.wide(call[4], 4096) : call.wide(call[4], size_t(std::min(count, 4096))); if (count >= 0 && text.size() > size_t(count)) text.resize(size_t(count)); const auto scratch = process.map_memory(65536, PROT_READ | PROT_WRITE); int32_t size[2]{0, 0}; try { call.put_wide(scratch, text, text.size() + 1); ApiCall inner(call.process, call.environment, {call[1], scratch, text.size(), scratch + 32768}); if (extent) extent->second(inner); const auto bytes = call.read(scratch + 32768, 8); std::memcpy(size, bytes.data(), 8); } catch (...) { process.unmap_memory(scratch); throw; } process.unmap_memory(scratch); int32_t left = 0, top = 0; if (call[7]) { const auto bytes = call.read(call[7], 8); std::memcpy(&left, bytes.data(), 4); std::memcpy(&top, bytes.data() + 4, 4); } const int32_t values[4]{left, top, left + size[0], top + size[1]}; call.write(call[8], std::span(reinterpret_cast(values), 16)); call.ret(s_ok); }); registry.add("DrawThemeIcon", 7, [](ApiCall &call) { call.ret(e_notimpl); }); registry.add("GetThemeTextMetrics", 5, [](ApiCall &call) { call.ret(e_notimpl); }); registry.add("GetThemeBackgroundRegion", 5, [](ApiCall &call) { call.ret(e_notimpl); }); registry.add("BufferedPaintInit", 0, [](ApiCall &call) { call.ret(s_ok); }); registry.add("BufferedPaintUnInit", 0, [](ApiCall &call) { call.ret(s_ok); }); } }