Skip to content

Commit 028ff1e

Browse files
committed
Implement Window::SetHasShadow on Linux
The shadow of a window with client-side decorations - every toplevel on Wayland, windows with a header bar on X11 - is GTK's own, drawn from the CSS of the window's decoration node. The window gets a style class and one screen-wide rule removes the shadow and its hairline border from windows carrying it. It is only ever applied to a mapped window: the shadow is also a margin of the surface, and GTK does not survive that margin changing between realizing and mapping a window (it asks for a size with the new margin and allocates with the old one). Asked earlier, it takes effect on the first map. A window the window manager decorates keeps its shadow, and HasShadow() keeps saying so. GetContentSize and friends measured a window without a child - one this library created - by its whole surface on Wayland, shadow margin and title bar included (532 x 409 for 480 x 320). GTK's own idea of the window's size tells the content's. window_shadow_example checks the state and that the content size survives: macOS, Linux Wayland and X11. Seen on GNOME 46 / Wayland through a screen capture, on a plain window and on the Flutter floating toolbar, whose faint frame it removes.
1 parent 1827d6e commit 028ff1e

5 files changed

Lines changed: 283 additions & 5 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ add_subdirectory(examples/window_example)
3434
add_subdirectory(examples/window_drag_session_example)
3535
add_subdirectory(examples/window_lifecycle_example)
3636
add_subdirectory(examples/window_parent_example)
37+
add_subdirectory(examples/window_shadow_example)
3738
add_subdirectory(examples/drag_drop_example)
3839
add_subdirectory(examples/desktop_features_example)
3940

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
cmake_minimum_required(VERSION 3.10)
2+
3+
project(window_shadow_example VERSION 0.0.1 LANGUAGES CXX)
4+
5+
# Set C++ standard
6+
set(CMAKE_CXX_STANDARD 17)
7+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
8+
9+
# Add example program
10+
add_executable(window_shadow_example
11+
"main.cpp"
12+
)
13+
14+
# Link main library
15+
target_link_libraries(window_shadow_example PRIVATE nativeapi)
16+
17+
# Set example program properties
18+
set_target_properties(window_shadow_example PROPERTIES
19+
OUTPUT_NAME "window_shadow_example"
20+
)
21+
22+
# Set example program compile options (macOS only)
23+
if(APPLE)
24+
set_source_files_properties("main.cpp"
25+
PROPERTIES
26+
COMPILE_FLAGS "-x objective-c++"
27+
)
28+
endif()
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#include <chrono>
2+
#include <cmath>
3+
#include <cstdlib>
4+
#include <functional>
5+
#include <iostream>
6+
#include <string>
7+
#include <thread>
8+
#include <vector>
9+
10+
#include "nativeapi.h"
11+
12+
using nativeapi::Size;
13+
using nativeapi::Window;
14+
15+
// Shows what Window::SetHasShadow() does to a window's state and - the part that went
16+
// wrong on Linux - to its content size: the exit code is the number of expectations that
17+
// did not hold. Whether a shadow is really drawn has to be looked at; "INFO" lines say
18+
// what the platform reports.
19+
//
20+
// It drives the windows itself and needs no input.
21+
int main() {
22+
nativeapi::SetMainThread();
23+
24+
int failures = 0;
25+
auto expect = [&](const std::string& what, bool ok, const std::string& detail = "") {
26+
std::cout << (ok ? "PASS " : "FAIL ") << what;
27+
if (!detail.empty()) {
28+
std::cout << " - " << detail;
29+
}
30+
std::cout << std::endl;
31+
if (!ok) {
32+
failures++;
33+
}
34+
};
35+
auto describe = [](const Size& s) {
36+
return std::to_string(std::lround(s.width)) + " x " + std::to_string(std::lround(s.height));
37+
};
38+
auto same = [](const Size& a, const Size& b) {
39+
return std::lround(a.width) == std::lround(b.width) &&
40+
std::lround(a.height) == std::lround(b.height);
41+
};
42+
43+
std::shared_ptr<Window> shown;
44+
std::shared_ptr<Window> early;
45+
Size before = {0, 0};
46+
// Platforms where the shadow is not the application's to remove report it unchanged.
47+
bool can_remove = true;
48+
49+
std::vector<std::function<void()>> steps = {
50+
[&] {
51+
std::cout << "show a window" << std::endl;
52+
shown = std::make_shared<Window>();
53+
shown->SetTitle("Shadow - toggled while shown");
54+
shown->SetContentSize({480, 320});
55+
shown->SetPosition({200, 200});
56+
shown->Show();
57+
},
58+
[&] {
59+
before = shown->GetContentSize();
60+
expect("a window has a shadow to begin with", shown->HasShadow());
61+
std::cout << "remove the shadow" << std::endl;
62+
shown->SetHasShadow(false);
63+
},
64+
[&] {
65+
can_remove = !shown->HasShadow();
66+
std::cout << "INFO the platform " << (can_remove ? "removed" : "kept") << " the shadow"
67+
<< std::endl;
68+
expect("the content keeps its size without the shadow",
69+
same(shown->GetContentSize(), before),
70+
describe(before) + " -> " + describe(shown->GetContentSize()));
71+
std::cout << "bring the shadow back" << std::endl;
72+
shown->SetHasShadow(true);
73+
},
74+
[&] {
75+
expect("the shadow is reported again", shown->HasShadow());
76+
expect("the content keeps its size with the shadow back",
77+
same(shown->GetContentSize(), before),
78+
describe(before) + " -> " + describe(shown->GetContentSize()));
79+
std::cout << "a second window: no shadow and a content size, both before it is shown"
80+
<< std::endl;
81+
early = std::make_shared<Window>();
82+
early->SetTitle("Shadow - removed before shown");
83+
early->SetHasShadow(false);
84+
early->SetContentSize({300, 200});
85+
early->SetPosition({760, 200});
86+
if (can_remove) {
87+
expect("the state is reported before the window is shown", !early->HasShadow());
88+
}
89+
early->Show();
90+
},
91+
[&] {
92+
if (can_remove) {
93+
expect("the state holds once the window is shown", !early->HasShadow());
94+
}
95+
expect("the content has the size it was given", same(early->GetContentSize(), {300, 200}),
96+
describe(early->GetContentSize()));
97+
std::cout << (failures == 0 ? "ALL PASS" : "FAILED") << std::endl;
98+
// Not Quit(): how a platform's loop ends must not decide the exit code.
99+
std::cout.flush();
100+
std::_Exit(failures);
101+
},
102+
};
103+
104+
std::thread driver([&] {
105+
for (auto& step : steps) {
106+
std::this_thread::sleep_for(std::chrono::milliseconds(1500));
107+
nativeapi::RunOnMainThread(step);
108+
}
109+
});
110+
driver.detach();
111+
112+
// On Windows destroying any window of the library ends the loop (its window
113+
// procedure posts WM_QUIT), so enter it again: the last step ends the process.
114+
while (true) {
115+
nativeapi::Application::GetInstance().Run();
116+
}
117+
}

src/platform/linux/window_linux.cpp

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ struct Layout {
4949
// GTK's own window API already speaks in content sizes and shadowless positions
5050
// (gtk_window_resize, gtk_window_move); this makes the measured side agree with it,
5151
// from where GTK allocated the window's child and its title bar.
52+
static GtkWidget* FindHeaderBar(GtkWidget* widget);
53+
5254
static Layout GetLayout(GtkWidget* widget, GdkWindow* gdk_window) {
5355
Layout layout;
5456
// An unmapped window has no frame yet, and GDK answers with an estimate that is not
@@ -70,7 +72,41 @@ static Layout GetLayout(GtkWidget* widget, GdkWindow* gdk_window) {
7072
GtkAllocation child_allocation = {};
7173
gint child_x = 0;
7274
gint child_y = 0;
73-
if (!child || !gtk_widget_get_mapped(child) ||
75+
if (!child) {
76+
// A window nobody put content into - one this library created - has no child to
77+
// measure. GTK's own idea of the window's size leaves client-side decorations out,
78+
// which tells the content's size; where it sits inside the surface is not public,
79+
// so the shadow is taken to be as wide above as below.
80+
gint width = 0;
81+
gint height = 0;
82+
gtk_window_get_size(GTK_WINDOW(widget), &width, &height);
83+
if (width <= 1 || height <= 1 ||
84+
(width >= layout.content.width && height >= layout.content.height)) {
85+
return layout;
86+
}
87+
gint title_height = 0;
88+
GtkWidget* header_bar = FindHeaderBar(widget);
89+
if (header_bar && gtk_widget_get_mapped(header_bar)) {
90+
title_height = gtk_widget_get_allocated_height(header_bar);
91+
}
92+
const gint side = (layout.content.width - width) / 2;
93+
const gint shadow_top = (layout.content.height - height - title_height) / 2;
94+
if (side < 0 || shadow_top < 0) {
95+
return layout;
96+
}
97+
layout.frame = {origin_x + side, origin_y + shadow_top, width, height + title_height};
98+
layout.content = {origin_x + side, origin_y + shadow_top + title_height, width, height};
99+
#ifdef GDK_WINDOWING_WAYLAND
100+
if (GDK_IS_WAYLAND_DISPLAY(gdk_window_get_display(gdk_window))) {
101+
layout.content.x -= layout.frame.x;
102+
layout.content.y -= layout.frame.y;
103+
layout.frame.x = 0;
104+
layout.frame.y = 0;
105+
}
106+
#endif
107+
return layout;
108+
}
109+
if (!gtk_widget_get_mapped(child) ||
74110
!gtk_widget_translate_coordinates(child, widget, 0, 0, &child_x, &child_y)) {
75111
return layout;
76112
}
@@ -845,13 +881,106 @@ TitleBarStyle Window::GetTitleBarStyle() const {
845881
return pimpl_->title_bar_style_;
846882
}
847883

884+
// The shadow of a window with client-side decorations - every toplevel on Wayland, and
885+
// windows with a header bar on X11 - is drawn by GTK itself, from the CSS of the window's
886+
// "decoration" node. That node cannot be styled through the window's own style context,
887+
// so the window gets a style class, and one rule for the whole screen takes the shadow
888+
// (and the hairline border that is part of it) away from windows carrying it. The state
889+
// lives on the widget, so every wrapper of the window agrees. A window the window
890+
// manager decorates has no such node: its shadow is not the application's to remove.
891+
static const char* kNoShadowStyleClass = "nativeapi-no-shadow";
892+
static const char* kPendingNoShadowKey = "nativeapi-pending-no-shadow";
893+
894+
static void EnsureNoShadowRule(GtkWidget* widget) {
895+
static bool installed = false;
896+
if (installed) {
897+
return;
898+
}
899+
installed = true;
900+
GtkCssProvider* provider = gtk_css_provider_new();
901+
gtk_css_provider_load_from_data(provider,
902+
"window.nativeapi-no-shadow decoration,"
903+
"window.nativeapi-no-shadow decoration:backdrop {"
904+
" box-shadow: none; border: none; }",
905+
-1, nullptr);
906+
gtk_style_context_add_provider_for_screen(gtk_widget_get_screen(widget),
907+
GTK_STYLE_PROVIDER(provider),
908+
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
909+
g_object_unref(provider);
910+
}
911+
912+
// Only ever on a mapped window. The shadow is also a margin of the surface, and GTK
913+
// does not survive that margin changing between realizing a window and mapping it: it
914+
// asks for a size with the new margin and allocates with the old one, which leaves the
915+
// content too small for good. On a mapped window the change is taken in - the content
916+
// size is asked for again, so that it is the surface that shrinks or grows.
917+
// GTK marks the windows it decorates itself with the "csd" style class.
918+
static bool DrawsOwnShadow(GtkWidget* widget) {
919+
return gtk_style_context_has_class(gtk_widget_get_style_context(widget), "csd");
920+
}
921+
922+
static void ApplyShadowClass(GtkWidget* widget, bool has_shadow) {
923+
GtkStyleContext* context = gtk_widget_get_style_context(widget);
924+
if (!has_shadow && !DrawsOwnShadow(widget)) {
925+
return; // the window manager's shadow: HasShadow() goes on saying true
926+
}
927+
if (has_shadow == !gtk_style_context_has_class(context, kNoShadowStyleClass)) {
928+
return;
929+
}
930+
if (has_shadow) {
931+
gtk_style_context_remove_class(context, kNoShadowStyleClass);
932+
} else {
933+
EnsureNoShadowRule(widget);
934+
gtk_style_context_add_class(context, kNoShadowStyleClass);
935+
}
936+
}
937+
938+
// What SetHasShadow() asked for while the window was not mapped: 1 for no shadow, 2 for
939+
// a shadow. Also covers a window that is hidden at the moment, which is in the same
940+
// state as one that was never shown.
941+
static gboolean OnMappedApplyShadow(GtkWidget* widget, GdkEvent* event, gpointer data) {
942+
(void)event;
943+
(void)data;
944+
const gint pending = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), kPendingNoShadowKey));
945+
g_object_set_data(G_OBJECT(widget), kPendingNoShadowKey, nullptr);
946+
if (pending != 0) {
947+
ApplyShadowClass(widget, pending == 2);
948+
}
949+
g_signal_handlers_disconnect_matched(widget, G_SIGNAL_MATCH_FUNC, 0, 0, nullptr,
950+
reinterpret_cast<gpointer>(OnMappedApplyShadow), nullptr);
951+
return FALSE;
952+
}
953+
848954
void Window::SetHasShadow(bool has_shadow) {
849-
// Window shadows are typically managed by the window manager
850-
// Provide stub implementation
955+
GtkWidget* widget = pimpl_->widget_;
956+
if (!widget || !GTK_IS_WINDOW(widget)) {
957+
return;
958+
}
959+
GObject* object = G_OBJECT(widget);
960+
if (gtk_widget_get_mapped(widget)) {
961+
g_object_set_data(object, kPendingNoShadowKey, nullptr);
962+
ApplyShadowClass(widget, has_shadow);
963+
return;
964+
}
965+
if (!has_shadow && gtk_widget_get_realized(widget) && !DrawsOwnShadow(widget)) {
966+
return; // see ApplyShadowClass()
967+
}
968+
if (!g_object_get_data(object, kPendingNoShadowKey)) {
969+
g_signal_connect(widget, "map-event", G_CALLBACK(OnMappedApplyShadow), nullptr);
970+
}
971+
g_object_set_data(object, kPendingNoShadowKey, GINT_TO_POINTER(has_shadow ? 2 : 1));
851972
}
852973

853974
bool Window::HasShadow() const {
854-
return true; // Default assumption
975+
GtkWidget* widget = pimpl_->widget_;
976+
if (!widget || !GTK_IS_WINDOW(widget)) {
977+
return true;
978+
}
979+
const gint pending = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), kPendingNoShadowKey));
980+
if (pending != 0) {
981+
return pending == 2;
982+
}
983+
return !gtk_style_context_has_class(gtk_widget_get_style_context(widget), kNoShadowStyleClass);
855984
}
856985

857986
void Window::SetOpacity(float opacity) {

src/window.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -789,7 +789,10 @@ class Window : public NativeObjectProvider, public std::enable_shared_from_this<
789789
* - Windows: ⚠️ Frameless windows only - The desktop compositor always draws the
790790
* shadow of a window that has a title bar; with TitleBarStyle::Hidden the shadow
791791
* follows this flag
792-
* - Linux: ⚠️ Recorded only - The window manager owns the shadow
792+
* - Linux: ⚠️ Client-side decorations only - Removed from the windows GTK decorates
793+
* itself: every window on Wayland, windows with a header bar on X11. On a window
794+
* that is not shown yet it takes effect when the window is shown. A window the
795+
* window manager decorates keeps its shadow, and HasShadow() keeps saying so.
793796
* - Android: ❌ Not applicable - Always ignored
794797
* - iOS: ❌ Not applicable - Always ignored
795798
* - OpenHarmony: ❌ Not applicable - Always ignored

0 commit comments

Comments
 (0)