mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-31 10:53:20 +08:00
[ui] add horizontal privacy screen branding with bottom alignment
This commit is contained in:
@@ -47,6 +47,22 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
|||||||
return std::ceil([text boundingRectWithSize:NSMakeSize(width, CGFLOAT_MAX)
|
return std::ceil([text boundingRectWithSize:NSMakeSize(width, CGFLOAT_MAX)
|
||||||
options:kTextOptions].size.height);
|
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
|
@interface CrossDeskPrivacyCoverView : NSView
|
||||||
@@ -65,19 +81,24 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
|||||||
if (width <= 0 || height <= 0) return;
|
if (width <= 0 || height <= 0) return;
|
||||||
// Match the Windows cover's sizes, colors and bottom-left alignment.
|
// Match the Windows cover's sizes, colors and bottom-left alignment.
|
||||||
// AppKit coordinates are points; Retina backing scale is applied by AppKit.
|
// 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;
|
const CGFloat margin = 32 * scale, text_width = 398 * scale;
|
||||||
NSColor* foreground = [NSColor colorWithSRGBRed:245.0 / 255 green:247.0 / 255
|
NSColor* foreground = [NSColor colorWithSRGBRed:245.0 / 255 green:247.0 / 255
|
||||||
blue:250.0 / 255 alpha:1];
|
blue:250.0 / 255 alpha:1];
|
||||||
NSAttributedString* brand = Text(@"CrossDesk", 14 * scale, NSFontWeightSemibold, foreground);
|
// Match the horizontal, two-tone wordmark in docs/images/crossdesk-banner.svg.
|
||||||
const CGFloat logo_size = std::ceil(brand.size.width);
|
NSAttributedString* brand = BrandText(scale);
|
||||||
const CGFloat brand_height = TextHeight(brand, logo_size);
|
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 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)
|
[self.logo drawInRect:NSMakeRect(margin, logo_top, logo_size, logo_size)
|
||||||
fromRect:NSZeroRect operation:NSCompositingOperationSourceOver
|
fromRect:NSZeroRect operation:NSCompositingOperationSourceOver
|
||||||
fraction:1 respectFlipped:YES hints:@{NSImageHintInterpolation: @(NSImageInterpolationHigh)}];
|
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];
|
options:kTextOptions];
|
||||||
if (!self.allowLocalUnlock) return;
|
if (!self.allowLocalUnlock) return;
|
||||||
NSColor* muted = [NSColor colorWithSRGBRed:181.0 / 255 green:190.0 / 255
|
NSColor* muted = [NSColor colorWithSRGBRed:181.0 / 255 green:190.0 / 255
|
||||||
@@ -87,7 +108,7 @@ CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
|||||||
NSFontWeightSemibold, foreground);
|
NSFontWeightSemibold, foreground);
|
||||||
const CGFloat hint_height = TextHeight(hint, text_width);
|
const CGFloat hint_height = TextHeight(hint, text_width);
|
||||||
const CGFloat shortcut_height = TextHeight(shortcut, 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,
|
[hint drawWithRect:NSMakeRect(text_left, bottom - shortcut_height - 12 * scale - hint_height,
|
||||||
text_width, hint_height) options:kTextOptions];
|
text_width, hint_height) options:kTextOptions];
|
||||||
[shortcut drawWithRect:NSMakeRect(text_left, bottom - shortcut_height,
|
[shortcut drawWithRect:NSMakeRect(text_left, bottom - shortcut_height,
|
||||||
|
|||||||
@@ -13,13 +13,15 @@ namespace crossdesk {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr size_t kInstructionLines = 2;
|
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 {
|
struct PaintResources {
|
||||||
HDC dc;
|
HDC dc;
|
||||||
int saved;
|
int saved;
|
||||||
HICON logo = nullptr;
|
HICON logo = nullptr;
|
||||||
std::array<HFONT, kInstructionLines + 1> fonts{};
|
std::array<HFONT, kInstructionLines + kBrandSegments> fonts{};
|
||||||
~PaintResources() {
|
~PaintResources() {
|
||||||
// Deselect every owned GDI object before releasing it.
|
// Deselect every owned GDI object before releasing it.
|
||||||
if (saved) RestoreDC(dc, saved);
|
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;
|
if (width <= 0 || height <= 0) return false;
|
||||||
// Scale with the monitor DPI, shrinking the whole group on small displays.
|
// Scale with the monitor DPI, shrinking the whole group on small displays.
|
||||||
// All coordinates remain in the owning window's physical-pixel context.
|
// 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) {
|
const auto px = [scale](int value) {
|
||||||
return (std::max)(1, static_cast<int>(value * scale + 0.5));
|
return (std::max)(1, static_cast<int>(value * scale + 0.5));
|
||||||
};
|
};
|
||||||
@@ -61,55 +63,69 @@ bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
|
|||||||
COLORREF color;
|
COLORREF color;
|
||||||
RECT bounds{};
|
RECT bounds{};
|
||||||
};
|
};
|
||||||
std::array<Line, kInstructionLines + 1> lines{{
|
std::array<Line, kInstructionLines + kBrandSegments> lines{{
|
||||||
{unlock_hint, 16, FW_NORMAL, RGB(181, 190, 204)},
|
{unlock_hint, 16, FW_NORMAL, RGB(181, 190, 204)},
|
||||||
{L"Ctrl + Alt + Shift + F12", 24, FW_SEMIBOLD, RGB(245, 247, 250)},
|
{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 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) {
|
for (size_t i = 0; i < lines.size(); ++i) {
|
||||||
if (i < kInstructionLines && !allow_local_unlock) continue;
|
if (i < kInstructionLines && !allow_local_unlock) continue;
|
||||||
auto& line = lines[i];
|
auto& line = lines[i];
|
||||||
|
const bool is_brand = i >= kInstructionLines;
|
||||||
resources.fonts[i] =
|
resources.fonts[i] =
|
||||||
CreateFontW(-px(line.size), 0, 0, 0, line.weight, FALSE, FALSE, FALSE,
|
CreateFontW(-px(line.size), 0, 0, 0, line.weight, FALSE, FALSE, FALSE,
|
||||||
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
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;
|
if (!resources.fonts[i] || !Select(dc, resources.fonts[i])) return false;
|
||||||
line.bounds = {0, 0, text_width, 0};
|
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)
|
line.bounds.right > text_width)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const int content_height =
|
const int content_height =
|
||||||
static_cast<int>(lines[0].bounds.bottom + lines[1].bounds.bottom) +
|
static_cast<int>(lines[0].bounds.bottom + lines[1].bounds.bottom) +
|
||||||
(allow_local_unlock ? px(12) : 0);
|
(allow_local_unlock ? px(12) : 0);
|
||||||
// Match the icon width to the name; each side keeps its own height.
|
// Match the horizontal, two-tone wordmark in docs/images/crossdesk-banner.svg.
|
||||||
const int brand_gap = px(6);
|
const int logo_size = px(48);
|
||||||
const int brand_width = static_cast<int>(lines[kBrandLine].bounds.right);
|
const int brand_gap = px(8);
|
||||||
const int logo_size = brand_width;
|
const int segment_gap = px(1);
|
||||||
const int brand_height =
|
const int brand_width = logo_size + brand_gap + segment_gap +
|
||||||
logo_size + brand_gap + static_cast<int>(lines[kBrandLine].bounds.bottom);
|
static_cast<int>(lines[kCrossLine].bounds.right + lines[kDeskLine].bounds.right);
|
||||||
|
const int brand_height = (std::max)({logo_size,
|
||||||
|
static_cast<int>(lines[kCrossLine].bounds.bottom),
|
||||||
|
static_cast<int>(lines[kDeskLine].bounds.bottom)});
|
||||||
if (logo_size <= 0 ||
|
if (logo_size <= 0 ||
|
||||||
(std::max)(brand_height, content_height) + 2 * margin > height ||
|
(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;
|
return false;
|
||||||
resources.logo = static_cast<HICON>(LoadImageW(
|
resources.logo = static_cast<HICON>(LoadImageW(
|
||||||
module, L"IDI_CROSSDESK_LOGO", IMAGE_ICON, logo_size, logo_size, 0));
|
module, L"IDI_CROSSDESK_LOGO", IMAGE_ICON, logo_size, logo_size, 0));
|
||||||
if (!resources.logo) return false;
|
if (!resources.logo) return false;
|
||||||
const int brand_left = rect.left + margin;
|
const int brand_left = rect.left + margin;
|
||||||
const int left = brand_left + brand_width + px(24);
|
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;
|
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))
|
logo_size, 0, nullptr, DI_NORMAL))
|
||||||
return false;
|
return false;
|
||||||
if (!Select(dc, resources.fonts[kBrandLine])) return false;
|
int brand_text_left = brand_left + logo_size + brand_gap;
|
||||||
RECT brand_bounds{brand_left, brand_top + logo_size + brand_gap,
|
for (size_t i = kCrossLine; i <= kDeskLine; ++i) {
|
||||||
brand_left + brand_width, rect.bottom - margin};
|
const auto& line = lines[i];
|
||||||
SetTextColor(dc, lines[kBrandLine].color);
|
if (!Select(dc, resources.fonts[i])) return false;
|
||||||
if (!DrawTextW(dc, lines[kBrandLine].text, -1, &brand_bounds,
|
const int brand_text_top = brand_bottom - line.bounds.bottom;
|
||||||
DT_CENTER | DT_SINGLELINE | DT_NOPREFIX))
|
RECT bounds{brand_text_left, brand_text_top,
|
||||||
return false;
|
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;
|
if (!allow_local_unlock) return true;
|
||||||
for (size_t i = 0; i < kInstructionLines; ++i) {
|
for (size_t i = 0; i < kInstructionLines; ++i) {
|
||||||
const auto& line = lines[i];
|
const auto& line = lines[i];
|
||||||
|
|||||||
Reference in New Issue
Block a user