macOS: Colored Titlebar and WM Window Decoration Styles API - GSoC 2024

As part of the GSoC 2024 project "Improvements to the Blender macOS User
Interface Experience" [1], this patch implements colored titlebar window
decorations on macOS, based on the current Blender theme colors.

Additionally, this patch introduces a new WM/GHOST API for implementing
and enabling custom decoration styles on any system/desktop environment
that support customizing window decorations.

[1]: https://devtalk.blender.org/t/gsoc-2024-proposal-improvements-to-the-blender-macos-user-interface-experience/34022)

Pull Request: https://projects.blender.org/blender/blender/pulls/123982
This commit is contained in:
Jonas Holzman
2025-02-04 16:18:19 +01:00
parent 2b5946da4e
commit ce42d92503
17 changed files with 322 additions and 7 deletions

View File

@@ -593,6 +593,31 @@ extern char *GHOST_GetTitle(GHOST_WindowHandle windowhandle);
*/
extern GHOST_TSuccess GHOST_SetPath(GHOST_WindowHandle windowhandle, const char *filepath);
/**
* Return the current window decoration style flags.
*/
extern GHOST_TWindowDecorationStyleFlags GHOST_GetWindowDecorationStyleFlags(
GHOST_WindowHandle windowhandle);
/**
* Set the window decoration style flags.
* \param styleFlags: Window decoration style flags.
*/
extern void GHOST_SetWindowDecorationStyleFlags(GHOST_WindowHandle windowhandle,
GHOST_TWindowDecorationStyleFlags styleFlags);
/**
* Set the window decoration style settings.
* \param decorationSettings: Window decoration style settings.
*/
extern void GHOST_SetWindowDecorationStyleSettings(
GHOST_WindowHandle windowhandle, GHOST_WindowDecorationStyleSettings decorationSettings);
/**
* Apply the window decoration style using the current flags and settings.
*/
extern GHOST_TSuccess GHOST_ApplyWindowDecorationStyle(GHOST_WindowHandle windowhandle);
/**
* Returns the window rectangle dimensions.
* These are screen coordinates.

View File

@@ -87,6 +87,29 @@ class GHOST_IWindow {
*/
virtual GHOST_TSuccess setPath(const char *filepath) = 0;
/**
* Return the current window decoration style flags.
*/
virtual GHOST_TWindowDecorationStyleFlags getWindowDecorationStyleFlags() = 0;
/**
* Set the window decoration style flags.
* \param styleFlags: Window decoration style flags.
*/
virtual void setWindowDecorationStyleFlags(GHOST_TWindowDecorationStyleFlags styleFlags) = 0;
/**
* Set the window decoration style settings.
* \param decorationSettings: Window decoration style settings.
*/
virtual void setWindowDecorationStyleSettings(
GHOST_WindowDecorationStyleSettings decorationSettings) = 0;
/**
* Apply the window decoration style using the current flags and settings.
*/
virtual GHOST_TSuccess applyWindowDecorationStyle() = 0;
/**
* Returns the window rectangle dimensions.
* These are screen coordinates.

View File

@@ -119,6 +119,10 @@ typedef enum {
* Support detecting the physical trackpad direction.
*/
GHOST_kCapabilityTrackpadPhysicalDirection = (1 << 7),
/**
* Support for window decoration styles.
*/
GHOST_kCapabilityWindowDecorationStyles = (1 << 8),
} GHOST_TCapabilityFlag;
/**
@@ -129,7 +133,8 @@ typedef enum {
(GHOST_kCapabilityCursorWarp | GHOST_kCapabilityWindowPosition | \
GHOST_kCapabilityPrimaryClipboard | GHOST_kCapabilityGPUReadFrontBuffer | \
GHOST_kCapabilityClipboardImages | GHOST_kCapabilityDesktopSample | \
GHOST_kCapabilityInputIME | GHOST_kCapabilityTrackpadPhysicalDirection)
GHOST_kCapabilityInputIME | GHOST_kCapabilityTrackpadPhysicalDirection | \
GHOST_kCapabilityWindowDecorationStyles)
/* Xtilt and Ytilt represent how much the pen is tilted away from
* vertically upright in either the X or Y direction, with X and Y the
@@ -694,6 +699,11 @@ typedef enum {
/* Can be extended as needed. */
} GHOST_TUserSpecialDirTypes;
typedef enum {
GHOST_kDecorationNone = 0,
GHOST_kDecorationColoredTitleBar = (1 << 0),
} GHOST_TWindowDecorationStyleFlags;
typedef struct {
/** Number of pixels on a line. */
uint32_t xPixels;
@@ -720,6 +730,11 @@ typedef struct {
GHOST_GPUDevice preferred_device;
} GHOST_GPUSettings;
typedef struct {
float colored_titlebar_bg_color[3];
float colored_titlebar_fg_color[3];
} GHOST_WindowDecorationStyleSettings;
#ifdef WITH_VULKAN_BACKEND
typedef struct {
/** Image handle to the image that will be presented to the user. */

View File

@@ -606,6 +606,33 @@ GHOST_TSuccess GHOST_SetPath(GHOST_WindowHandle windowhandle, const char *filepa
return window->setPath(filepath);
}
GHOST_TWindowDecorationStyleFlags GHOST_GetWindowDecorationStyleFlags(
GHOST_WindowHandle windowhandle)
{
GHOST_IWindow *window = (GHOST_IWindow *)windowhandle;
return window->getWindowDecorationStyleFlags();
}
void GHOST_SetWindowDecorationStyleFlags(GHOST_WindowHandle windowhandle,
GHOST_TWindowDecorationStyleFlags styleFlags)
{
GHOST_IWindow *window = (GHOST_IWindow *)windowhandle;
window->setWindowDecorationStyleFlags(styleFlags);
}
void GHOST_SetWindowDecorationStyleSettings(GHOST_WindowHandle windowhandle,
GHOST_WindowDecorationStyleSettings decorationSettings)
{
GHOST_IWindow *window = (GHOST_IWindow *)windowhandle;
window->setWindowDecorationStyleSettings(decorationSettings);
}
GHOST_TSuccess GHOST_ApplyWindowDecorationStyle(GHOST_WindowHandle windowhandle)
{
GHOST_IWindow *window = (GHOST_IWindow *)windowhandle;
return window->applyWindowDecorationStyle();
}
GHOST_RectangleHandle GHOST_GetWindowBounds(GHOST_WindowHandle windowhandle)
{
const GHOST_IWindow *window = (const GHOST_IWindow *)windowhandle;

View File

@@ -54,7 +54,8 @@ class GHOST_SystemHeadless : public GHOST_System {
~(GHOST_kCapabilityWindowPosition | GHOST_kCapabilityCursorWarp |
GHOST_kCapabilityPrimaryClipboard |
GHOST_kCapabilityDesktopSample |
GHOST_kCapabilityClipboardImages | GHOST_kCapabilityInputIME));
GHOST_kCapabilityClipboardImages | GHOST_kCapabilityInputIME |
GHOST_kCapabilityWindowDecorationStyles));
}
char *getClipboard(bool /*selection*/) const override
{

View File

@@ -791,7 +791,9 @@ GHOST_TCapabilityFlag GHOST_SystemSDL::getCapabilities() const
/* This SDL back-end has not yet implemented image copy/paste. */
GHOST_kCapabilityClipboardImages |
/* No support yet for IME input methods. */
GHOST_kCapabilityInputIME));
GHOST_kCapabilityInputIME |
/* No support for window decoration styles. */
GHOST_kCapabilityWindowDecorationStyles));
}
char *GHOST_SystemSDL::getClipboard(bool /*selection*/) const

View File

@@ -8730,6 +8730,10 @@ GHOST_TCapabilityFlag GHOST_SystemWayland::getCapabilities() const
GHOST_kCapabilityGPUReadFrontBuffer |
/* This WAYLAND back-end has not yet implemented desktop color sample. */
GHOST_kCapabilityDesktopSample |
/* This WAYLAND back-end doesn't have support for window decoration styles.
* In all likelihood, this back-end will eventually need to support client-side
* decorations, see #113795. */
GHOST_kCapabilityWindowDecorationStyles |
/* This flag will eventually be removed. */
((has_wl_trackpad_physical_direction == 1) ?
0 :

View File

@@ -586,7 +586,10 @@ GHOST_TCapabilityFlag GHOST_SystemWin32::getCapabilities() const
return GHOST_TCapabilityFlag(GHOST_CAPABILITY_FLAG_ALL &
~(
/* WIN32 has no support for a primary selection clipboard. */
GHOST_kCapabilityPrimaryClipboard));
GHOST_kCapabilityPrimaryClipboard |
/* This WIN32 backend has not yet implemented support for window
* decoration styles. */
GHOST_kCapabilityWindowDecorationStyles));
}
GHOST_TSuccess GHOST_SystemWin32::init()

View File

@@ -1817,7 +1817,9 @@ GHOST_TCapabilityFlag GHOST_SystemX11::getCapabilities() const
/* No support yet for image copy/paste. */
GHOST_kCapabilityClipboardImages |
/* No support yet for IME input methods. */
GHOST_kCapabilityInputIME));
GHOST_kCapabilityInputIME |
/* No support for window decoration styles. */
GHOST_kCapabilityWindowDecorationStyles));
}
void GHOST_SystemX11::addDirtyWindow(GHOST_WindowX11 *bad_wind)

View File

@@ -32,6 +32,8 @@ GHOST_Window::GHOST_Window(uint32_t width,
m_progressBarVisible(false),
m_canAcceptDragOperation(false),
m_isUnsavedChanges(false),
m_windowDecorationStyleFlags(GHOST_kDecorationNone),
m_windowDecorationStyleSettings(),
m_wantStereoVisual(wantStereoVisual),
m_nativePixelSize(1.0f),
m_context(new GHOST_ContextNone(false))
@@ -55,6 +57,22 @@ void *GHOST_Window::getOSWindow() const
return nullptr;
}
GHOST_TWindowDecorationStyleFlags GHOST_Window::getWindowDecorationStyleFlags()
{
return m_windowDecorationStyleFlags;
}
void GHOST_Window::setWindowDecorationStyleFlags(GHOST_TWindowDecorationStyleFlags styleFlags)
{
m_windowDecorationStyleFlags = styleFlags;
}
void GHOST_Window::setWindowDecorationStyleSettings(
GHOST_WindowDecorationStyleSettings decorationSettings)
{
m_windowDecorationStyleSettings = decorationSettings;
}
GHOST_TSuccess GHOST_Window::setDrawingContextType(GHOST_TDrawingContextType type)
{
if (type != m_drawingContextType) {

View File

@@ -89,6 +89,33 @@ class GHOST_Window : public GHOST_IWindow {
return GHOST_kFailure;
}
/**
* Return the current window decoration style flags.
*/
virtual GHOST_TWindowDecorationStyleFlags getWindowDecorationStyleFlags() override;
/**
* Set the window decoration style flags.
* \param styleFlags: Window decoration style flags.
*/
virtual void setWindowDecorationStyleFlags(
GHOST_TWindowDecorationStyleFlags styleFlags) override;
/**
* Set the window decoration style settings.
* \param decorationSettings: Window decoration style settings.
*/
virtual void setWindowDecorationStyleSettings(
GHOST_WindowDecorationStyleSettings decorationSettings) override;
/**
* Apply the window decoration style using the current flags and settings.
*/
virtual GHOST_TSuccess applyWindowDecorationStyle() override
{
return GHOST_kSuccess;
}
/**
* Returns the current cursor shape.
* \return The current cursor shape.
@@ -410,6 +437,10 @@ class GHOST_Window : public GHOST_IWindow {
/** Stores whether this is a full screen window. */
bool m_fullScreen;
/** Window Decoration Styles. */
GHOST_TWindowDecorationStyleFlags m_windowDecorationStyleFlags;
GHOST_WindowDecorationStyleSettings m_windowDecorationStyleSettings;
/** Whether to attempt to initialize a context with a stereo frame-buffer. */
bool m_wantStereoVisual;

View File

@@ -93,6 +93,11 @@ class GHOST_WindowCocoa : public GHOST_Window {
*/
GHOST_TSuccess setPath(const char *filepath) override;
/**
* Apply the window decoration style using the current flags and settings.
*/
GHOST_TSuccess applyWindowDecorationStyle() override;
/**
* Returns the window rectangle dimensions.
* The dimensions are given in screen coordinates that are

View File

@@ -565,6 +565,38 @@ GHOST_TSuccess GHOST_WindowCocoa::setPath(const char *filepath)
return GHOST_kSuccess;
}
GHOST_TSuccess GHOST_WindowCocoa::applyWindowDecorationStyle()
{
@autoreleasepool {
if (m_windowDecorationStyleFlags & GHOST_kDecorationColoredTitleBar) {
const float *background_color = m_windowDecorationStyleSettings.colored_titlebar_bg_color;
/* Titlebar background color. */
m_window.backgroundColor = [NSColor colorWithRed:background_color[0]
green:background_color[1]
blue:background_color[2]
alpha:1.0];
/* Titlebar foreground color.
* Use the value component of the titlebar background's HSV representation to determine
* whether we should use the macOS dark or light titlebar text appearance. With values below
* 0.5 considered as dark themes, and values above 0.5 considered as light themes.
*/
const float hsv_v = MAX(background_color[0], MAX(background_color[1], background_color[2]));
const NSAppearanceName win_appearance = hsv_v > 0.5 ? NSAppearanceNameVibrantLight :
NSAppearanceNameVibrantDark;
m_window.appearance = [NSAppearance appearanceNamed:win_appearance];
m_window.titlebarAppearsTransparent = YES;
}
else {
m_window.titlebarAppearsTransparent = NO;
}
}
return GHOST_kSuccess;
}
void GHOST_WindowCocoa::getWindowBounds(GHOST_Rect &bounds) const
{
GHOST_ASSERT(getValid(), "GHOST_WindowCocoa::getWindowBounds(): window invalid");

View File

@@ -2720,6 +2720,13 @@ void ED_area_newspace(bContext *C, ScrArea *area, int type, const bool skip_regi
WM_window_title(CTX_wm_manager(C), CTX_wm_window(C));
}
/* If window decoration styles are supported, send a notification to re-apply them. */
/* TODO: The `bl_animation_keyframing` test fails here if WM_capabilities_flags is called in
* background mode. Remove the !G.background check once the test has been fixed. */
if (!G.background && WM_capabilities_flag() & WM_CAPABILITY_WINDOW_DECORATION_STYLES) {
WM_event_add_notifier(C, NC_WINDOW, nullptr);
}
/* also redraw when re-used */
ED_area_tag_redraw(area);
}

View File

@@ -660,10 +660,16 @@ void ED_screen_do_listen(bContext *C, const wmNotifier *note)
}
break;
case NC_WINDOW:
if (WM_capabilities_flag() & WM_CAPABILITY_WINDOW_DECORATION_STYLES) {
WM_window_apply_decoration_style(win, screen);
}
screen->do_draw = true;
break;
case NC_SCREEN:
if (note->action == NA_EDITED) {
if (WM_capabilities_flag() & WM_CAPABILITY_WINDOW_DECORATION_STYLES) {
WM_window_apply_decoration_style(win, screen);
}
screen->do_draw = screen->do_refresh = true;
}
break;
@@ -1312,7 +1318,7 @@ void ED_screen_global_areas_refresh(wmWindow *win)
{
/* Don't create global area for child and temporary windows. */
bScreen *screen = BKE_workspace_active_screen_get(win->workspace_hook);
if ((win->parent != nullptr) || screen->temp) {
if (!WM_window_is_main_top_level(win)) {
if (win->global_areas.areabase.first) {
screen->do_refresh = true;
BKE_screen_area_map_free(&win->global_areas);

View File

@@ -185,10 +185,12 @@ enum eWM_CapabilitiesFlag {
WM_CAPABILITY_INPUT_IME = (1 << 6),
/** Trackpad physical scroll detection. */
WM_CAPABILITY_TRACKPAD_PHYSICAL_DIRECTION = (1 << 7),
/** Support for window decoration styles. */
WM_CAPABILITY_WINDOW_DECORATION_STYLES = (1 << 8),
/** The initial value, indicates the value needs to be set by inspecting GHOST. */
WM_CAPABILITY_INITIALIZED = (1u << 31),
};
ENUM_OPERATORS(eWM_CapabilitiesFlag, WM_CAPABILITY_TRACKPAD_PHYSICAL_DIRECTION)
ENUM_OPERATORS(eWM_CapabilitiesFlag, WM_CAPABILITY_WINDOW_DECORATION_STYLES)
eWM_CapabilitiesFlag WM_capabilities_flag();
@@ -284,6 +286,7 @@ void WM_window_rect_calc(const wmWindow *win, rcti *r_rect);
* \note Depends on #UI_SCALE_FAC. Should that be outdated, call #WM_window_set_dpi first.
*/
void WM_window_screen_rect_calc(const wmWindow *win, rcti *r_rect);
bool WM_window_is_main_top_level(const wmWindow *win);
bool WM_window_is_fullscreen(const wmWindow *win);
bool WM_window_is_maximized(const wmWindow *win);
@@ -374,6 +377,29 @@ void WM_window_title(wmWindowManager *wm, wmWindow *win, const char *title = nul
bool WM_stereo3d_enabled(wmWindow *win, bool skip_stereo3d_check);
/* Window Decoration Styles. */
/* Flags for #WM_window_decoration_set_style().
* NOTE: To be kept in sync with #GHOST_TWindowDecorationFlags. */
enum eWM_WindowDecorationStyleFlag {
/** No decoration styling. */
WM_WINDOW_DECORATION_STYLE_NONE = 0,
/** Colored Titlebar. */
WM_WINDOW_DECORATION_STYLE_COLORED_TITLEBAR = (1 << 0),
};
ENUM_OPERATORS(eWM_WindowDecorationStyleFlag, WM_WINDOW_DECORATION_STYLE_COLORED_TITLEBAR)
/* Get/set window decoration style flags. */
eWM_WindowDecorationStyleFlag WM_window_get_decoration_style_flags(const wmWindow *win);
void WM_window_set_decoration_style_flags(const wmWindow *win,
eWM_WindowDecorationStyleFlag style_flags);
/* Apply the window decoration style using the current style flags and by parsing style
* settings from the current Blender theme.
* The screen parameter is optional, and can be passed for enhanced theme parsing.
* NOTE: Avoid calling this function directly, prefer sending an NC_WINDOW WM notification instead.
*/
void WM_window_apply_decoration_style(const wmWindow *win, const bScreen *screen = nullptr);
/* `wm_files.cc`. */
void WM_file_autoexec_init(const char *filepath);

View File

@@ -598,6 +598,72 @@ void WM_window_set_dpi(const wmWindow *win)
U.widget_unit = int(roundf(18.0f * U.scale_factor)) + (2 * pixelsize);
}
eWM_WindowDecorationStyleFlag WM_window_get_decoration_style_flags(const wmWindow *win)
{
const GHOST_TWindowDecorationStyleFlags ghost_style_flags = GHOST_GetWindowDecorationStyleFlags(
static_cast<GHOST_WindowHandle>(win->ghostwin));
eWM_WindowDecorationStyleFlag wm_style_flags = WM_WINDOW_DECORATION_STYLE_NONE;
if (ghost_style_flags & GHOST_kDecorationColoredTitleBar) {
wm_style_flags |= WM_WINDOW_DECORATION_STYLE_COLORED_TITLEBAR;
}
return wm_style_flags;
}
void WM_window_set_decoration_style_flags(const wmWindow *win,
eWM_WindowDecorationStyleFlag style_flags)
{
unsigned int ghost_style_flags = GHOST_kDecorationNone;
if (style_flags & WM_WINDOW_DECORATION_STYLE_COLORED_TITLEBAR) {
ghost_style_flags |= GHOST_kDecorationColoredTitleBar;
}
GHOST_SetWindowDecorationStyleFlags(
static_cast<GHOST_WindowHandle>(win->ghostwin),
static_cast<GHOST_TWindowDecorationStyleFlags>(ghost_style_flags));
}
static void wm_window_decoration_style_set_from_theme(const wmWindow *win, const bScreen *screen)
{
/* Set the decoration style settings from the current theme colors.
* NOTE: screen may be null. In which case, only the window is used as a theme provider. */
GHOST_WindowDecorationStyleSettings decoration_settings = {};
/* Colored Titlebar Decoration. */
/* For main windows, use the topbar color. */
if (WM_window_is_main_top_level(win)) {
UI_SetTheme(SPACE_TOPBAR, RGN_TYPE_HEADER);
}
/* For single editor floating windows, use the editor header color. */
else if (screen && BLI_listbase_is_single(&screen->areabase)) {
const ScrArea *main_area = static_cast<ScrArea *>(screen->areabase.first);
UI_SetTheme(main_area->spacetype, RGN_TYPE_HEADER);
}
/* For floating window with multiple editors/areas, use the default space color. */
else {
UI_SetTheme(0, RGN_TYPE_WINDOW);
}
float titlebar_bg_color[3], titlebar_fg_color[3];
UI_GetThemeColor3fv(TH_BACK, titlebar_bg_color);
UI_GetThemeColor3fv(TH_BUTBACK_TEXT, titlebar_fg_color);
copy_v3_v3(decoration_settings.colored_titlebar_bg_color, titlebar_bg_color);
copy_v3_v3(decoration_settings.colored_titlebar_fg_color, titlebar_fg_color);
GHOST_SetWindowDecorationStyleSettings(static_cast<GHOST_WindowHandle>(win->ghostwin),
decoration_settings);
}
void WM_window_apply_decoration_style(const wmWindow *win, const bScreen *screen)
{
BLI_assert(WM_capabilities_flag() & WM_CAPABILITY_WINDOW_DECORATION_STYLES);
wm_window_decoration_style_set_from_theme(win, screen);
GHOST_ApplyWindowDecorationStyle(static_cast<GHOST_WindowHandle>(win->ghostwin));
}
/**
* When windows are activated, simulate modifier press/release to match the current state of
* held modifier keys, see #40317.
@@ -857,6 +923,12 @@ static void wm_window_ghostwindow_ensure(wmWindowManager *wm, wmWindow *win, boo
wm_window_ensure_eventstate(win);
WM_window_set_dpi(win);
if (WM_capabilities_flag() & WM_CAPABILITY_WINDOW_DECORATION_STYLES) {
/* Only decoration style we have for now. */
WM_window_set_decoration_style_flags(win, WM_WINDOW_DECORATION_STYLE_COLORED_TITLEBAR);
WM_window_apply_decoration_style(win);
}
}
/* Add key-map handlers (1 handler for all keys in map!). */
@@ -2174,6 +2246,9 @@ eWM_CapabilitiesFlag WM_capabilities_flag()
if (ghost_flag & GHOST_kCapabilityTrackpadPhysicalDirection) {
flag |= WM_CAPABILITY_TRACKPAD_PHYSICAL_DIRECTION;
}
if (ghost_flag & GHOST_kCapabilityWindowDecorationStyles) {
flag |= WM_CAPABILITY_WINDOW_DECORATION_STYLES;
}
return flag;
}
@@ -2808,6 +2883,19 @@ bool WM_window_is_maximized(const wmWindow *win)
return win->windowstate == GHOST_kWindowStateMaximized;
}
bool WM_window_is_main_top_level(const wmWindow *win)
{
/**
* Return whether the window is a main/top-level window. In which case it is expected to contain
* global areas (topbar/statusbar).
*/
const bScreen *screen = BKE_workspace_active_screen_get(win->workspace_hook);
if ((win->parent != nullptr) || screen->temp) {
return false;
}
return true;
}
/** \} */
/* -------------------------------------------------------------------- */