diff --git a/apps/desktop/src/platform/macos/privacy/privacy_cover_view.mm b/apps/desktop/src/platform/macos/privacy/privacy_cover_view.mm index 7b42c941..8e680260 100644 --- a/apps/desktop/src/platform/macos/privacy/privacy_cover_view.mm +++ b/apps/desktop/src/platform/macos/privacy/privacy_cover_view.mm @@ -47,6 +47,22 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) { return std::ceil([text boundingRectWithSize:NSMakeSize(width, CGFLOAT_MAX) options:kTextOptions].size.height); } + +NSAttributedString* BrandText(CGFloat scale) { + NSFont* font = [NSFont fontWithName:@"Arial-BoldMT" size:29 * scale]; + if (!font) font = [NSFont boldSystemFontOfSize:29 * scale]; + auto* brand = [[NSMutableAttributedString alloc] initWithString:@"CrossDesk" + attributes:@{NSFontAttributeName: font, + NSForegroundColorAttributeName: + [NSColor colorWithSRGBRed:69.0 / 255 green:135.0 / 255 + blue:219.0 / 255 alpha:1]}]; + [brand addAttribute:NSForegroundColorAttributeName + value:[NSColor colorWithSRGBRed:37.0 / 255 green:83.0 / 255 + blue:151.0 / 255 alpha:1] + range:NSMakeRange(5, 4)]; + [brand addAttribute:NSKernAttributeName value:@(scale) range:NSMakeRange(4, 1)]; + return brand; +} } @interface CrossDeskPrivacyCoverView : NSView @@ -65,19 +81,24 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) { if (width <= 0 || height <= 0) return; // Match the Windows cover's sizes, colors and bottom-left alignment. // AppKit coordinates are points; Retina backing scale is applied by AppKit. - const CGFloat scale = std::min({CGFloat(1), width / 640, height / 200}); + const CGFloat scale = std::min({CGFloat(1), width / 768, height / 200}); const CGFloat margin = 32 * scale, text_width = 398 * scale; NSColor* foreground = [NSColor colorWithSRGBRed:245.0 / 255 green:247.0 / 255 blue:250.0 / 255 alpha:1]; - NSAttributedString* brand = Text(@"CrossDesk", 14 * scale, NSFontWeightSemibold, foreground); - const CGFloat logo_size = std::ceil(brand.size.width); - const CGFloat brand_height = TextHeight(brand, logo_size); + // Match the horizontal, two-tone wordmark in docs/images/crossdesk-banner.svg. + NSAttributedString* brand = BrandText(scale); + const CGFloat logo_size = 48 * scale; + const CGFloat brand_text_width = std::ceil(brand.size.width); + const CGFloat brand_text_height = std::ceil(brand.size.height); + const CGFloat brand_width = logo_size + 8 * scale + brand_text_width; const CGFloat bottom = height - margin; - const CGFloat logo_top = bottom - brand_height - 6 * scale - logo_size; + const CGFloat logo_top = bottom - logo_size; [self.logo drawInRect:NSMakeRect(margin, logo_top, logo_size, logo_size) fromRect:NSZeroRect operation:NSCompositingOperationSourceOver fraction:1 respectFlipped:YES hints:@{NSImageHintInterpolation: @(NSImageInterpolationHigh)}]; - [brand drawWithRect:NSMakeRect(margin, bottom - brand_height, logo_size, brand_height) + [brand drawWithRect:NSMakeRect(margin + logo_size + 8 * scale, + bottom - brand_text_height, + brand_text_width, brand_text_height) options:kTextOptions]; if (!self.allowLocalUnlock) return; NSColor* muted = [NSColor colorWithSRGBRed:181.0 / 255 green:190.0 / 255 @@ -87,7 +108,7 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) { NSFontWeightSemibold, foreground); const CGFloat hint_height = TextHeight(hint, text_width); const CGFloat shortcut_height = TextHeight(shortcut, text_width); - const CGFloat text_left = margin + logo_size + 24 * scale; + const CGFloat text_left = margin + brand_width + 24 * scale; [hint drawWithRect:NSMakeRect(text_left, bottom - shortcut_height - 12 * scale - hint_height, text_width, hint_height) options:kTextOptions]; [shortcut drawWithRect:NSMakeRect(text_left, bottom - shortcut_height, diff --git a/apps/desktop/src/platform/windows/privacy/band_module/privacy_cover_renderer.cpp b/apps/desktop/src/platform/windows/privacy/band_module/privacy_cover_renderer.cpp index d52355e0..a1cc21d7 100644 --- a/apps/desktop/src/platform/windows/privacy/band_module/privacy_cover_renderer.cpp +++ b/apps/desktop/src/platform/windows/privacy/band_module/privacy_cover_renderer.cpp @@ -13,13 +13,15 @@ namespace crossdesk { namespace { constexpr size_t kInstructionLines = 2; -constexpr size_t kBrandLine = kInstructionLines; +constexpr size_t kBrandSegments = 2; +constexpr size_t kCrossLine = kInstructionLines; +constexpr size_t kDeskLine = kCrossLine + 1; struct PaintResources { HDC dc; int saved; HICON logo = nullptr; - std::array fonts{}; + std::array fonts{}; ~PaintResources() { // Deselect every owned GDI object before releasing it. if (saved) RestoreDC(dc, saved); @@ -45,7 +47,7 @@ bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module, if (width <= 0 || height <= 0) return false; // Scale with the monitor DPI, shrinking the whole group on small displays. // All coordinates remain in the owning window's physical-pixel context. - const double scale = (std::min)({dpi / 96.0, width / 640.0, height / 200.0}); + const double scale = (std::min)({dpi / 96.0, width / 768.0, height / 200.0}); const auto px = [scale](int value) { return (std::max)(1, static_cast(value * scale + 0.5)); }; @@ -61,55 +63,69 @@ bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module, COLORREF color; RECT bounds{}; }; - std::array lines{{ + std::array lines{{ {unlock_hint, 16, FW_NORMAL, RGB(181, 190, 204)}, {L"Ctrl + Alt + Shift + F12", 24, FW_SEMIBOLD, RGB(245, 247, 250)}, - {L"CrossDesk", 14, FW_SEMIBOLD, RGB(245, 247, 250)}, + {L"Cross", 29, FW_BOLD, RGB(69, 135, 219)}, + {L"Desk", 29, FW_BOLD, RGB(37, 83, 151)}, }}; constexpr UINT flags = DT_LEFT | DT_WORDBREAK | DT_NOPREFIX; + constexpr UINT brand_flags = DT_LEFT | DT_SINGLELINE | DT_NOPREFIX; for (size_t i = 0; i < lines.size(); ++i) { if (i < kInstructionLines && !allow_local_unlock) continue; auto& line = lines[i]; + const bool is_brand = i >= kInstructionLines; resources.fonts[i] = CreateFontW(-px(line.size), 0, 0, 0, line.weight, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, - ANTIALIASED_QUALITY, DEFAULT_PITCH, L"Microsoft YaHei UI"); + ANTIALIASED_QUALITY, DEFAULT_PITCH, + is_brand ? L"Arial" : L"Microsoft YaHei UI"); if (!resources.fonts[i] || !Select(dc, resources.fonts[i])) return false; line.bounds = {0, 0, text_width, 0}; - if (!DrawTextW(dc, line.text, -1, &line.bounds, flags | DT_CALCRECT) || + if (!DrawTextW(dc, line.text, -1, &line.bounds, + (is_brand ? brand_flags : flags) | DT_CALCRECT) || line.bounds.right > text_width) return false; } const int content_height = static_cast(lines[0].bounds.bottom + lines[1].bounds.bottom) + (allow_local_unlock ? px(12) : 0); - // Match the icon width to the name; each side keeps its own height. - const int brand_gap = px(6); - const int brand_width = static_cast(lines[kBrandLine].bounds.right); - const int logo_size = brand_width; - const int brand_height = - logo_size + brand_gap + static_cast(lines[kBrandLine].bounds.bottom); + // Match the horizontal, two-tone wordmark in docs/images/crossdesk-banner.svg. + const int logo_size = px(48); + const int brand_gap = px(8); + const int segment_gap = px(1); + const int brand_width = logo_size + brand_gap + segment_gap + + static_cast(lines[kCrossLine].bounds.right + lines[kDeskLine].bounds.right); + const int brand_height = (std::max)({logo_size, + static_cast(lines[kCrossLine].bounds.bottom), + static_cast(lines[kDeskLine].bounds.bottom)}); if (logo_size <= 0 || (std::max)(brand_height, content_height) + 2 * margin > height || - brand_width + px(24) + text_width + 2 * margin > width) + brand_width + (allow_local_unlock ? px(24) + text_width : 0) + 2 * margin > width) return false; resources.logo = static_cast(LoadImageW( module, L"IDI_CROSSDESK_LOGO", IMAGE_ICON, logo_size, logo_size, 0)); if (!resources.logo) return false; const int brand_left = rect.left + margin; const int left = brand_left + brand_width + px(24); - const int brand_top = rect.bottom - margin - brand_height; + const int brand_bottom = rect.bottom - margin; int top = rect.bottom - margin - content_height; - if (!DrawIconEx(dc, brand_left, brand_top, resources.logo, logo_size, + if (!DrawIconEx(dc, brand_left, brand_bottom - logo_size, + resources.logo, logo_size, logo_size, 0, nullptr, DI_NORMAL)) return false; - if (!Select(dc, resources.fonts[kBrandLine])) return false; - RECT brand_bounds{brand_left, brand_top + logo_size + brand_gap, - brand_left + brand_width, rect.bottom - margin}; - SetTextColor(dc, lines[kBrandLine].color); - if (!DrawTextW(dc, lines[kBrandLine].text, -1, &brand_bounds, - DT_CENTER | DT_SINGLELINE | DT_NOPREFIX)) - return false; + int brand_text_left = brand_left + logo_size + brand_gap; + for (size_t i = kCrossLine; i <= kDeskLine; ++i) { + const auto& line = lines[i]; + if (!Select(dc, resources.fonts[i])) return false; + const int brand_text_top = brand_bottom - line.bounds.bottom; + RECT bounds{brand_text_left, brand_text_top, + brand_text_left + line.bounds.right, + brand_text_top + line.bounds.bottom}; + SetTextColor(dc, line.color); + if (!DrawTextW(dc, line.text, -1, &bounds, brand_flags)) return false; + brand_text_left += line.bounds.right + segment_gap; + } if (!allow_local_unlock) return true; for (size_t i = 0; i < kInstructionLines; ++i) { const auto& line = lines[i];