Skip to content

Commit a3caaa4

Browse files
committed
Linux: a hidden title bar's shadow gutter no longer leaves an empty band around the content
The core shadow reserves a gutter for the largest supported shadow, about 132 logical pixels a side, and it showed as an empty band: - A tiling window manager, maximizing or full screen fits the whole surface, gutter included, to the space it gives the window. The gutter is now taken away while the window is in a maximized, full-screen or tiled state, and the size and position setters use the gutter the window has right now. - GtkWindow painted its background under the gutter, an opaque band hiding the shadow. A window that can be translucent now has that background cleared. - The window manager took the gutter for part of the window. It is now declared as shadow (gdk_window_set_shadow_width), so the content is what gets placed and tiled. The shadow also moved a realized content widget into a GtkOverlay, which unrealizes and realizes it again. A Flutter FlView never rendered after that: black on Hyprland, and on GNOME its window was never shown. Realized content now stays where it is; the overlay only painted the part of a polygon shape's shadow inside the content. Verified with a Flutter app on Hyprland 0.56 (Omarchy) and GNOME 46 on Wayland.
1 parent 151f891 commit a3caaa4

2 files changed

Lines changed: 101 additions & 7 deletions

File tree

‎src/platform/linux/window_linux.cpp‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -837,10 +837,8 @@ void Window::SetContentSize(Size size) {
837837
pimpl_->requested_content_size_ = size;
838838
}
839839
if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) {
840-
if (linux_shadow::Get(pimpl_->widget_)) {
841-
size.width += linux_shadow::Get(pimpl_->widget_)->margin * 2;
842-
size.height += linux_shadow::Get(pimpl_->widget_)->margin * 2;
843-
}
840+
size.width += linux_shadow::Gutter(pimpl_->widget_) * 2;
841+
size.height += linux_shadow::Gutter(pimpl_->widget_) * 2;
844842
GtkWindow* gtk_window = GTK_WINDOW(pimpl_->widget_);
845843
if (!gtk_window_get_resizable(gtk_window)) {
846844
// GTK derives a non-resizable window's fixed geometry from its default
@@ -1141,7 +1139,7 @@ void Window::SetPosition(Point point) {
11411139
if (pimpl_->widget_ && GTK_IS_WINDOW(pimpl_->widget_)) {
11421140
// gtk_window_move() positions the frame, and remembers the position for a window
11431141
// that is not mapped yet — which gdk_window_move() does not.
1144-
const int margin = linux_shadow::Get(pimpl_->widget_) ? linux_shadow::Get(pimpl_->widget_)->margin : 0;
1142+
const int margin = linux_shadow::Gutter(pimpl_->widget_);
11451143
gtk_window_move(GTK_WINDOW(pimpl_->widget_), (gint)point.x - margin, (gint)point.y - margin);
11461144
} else if (pimpl_->gdk_window_) {
11471145
gdk_window_move(pimpl_->gdk_window_, (gint)point.x, (gint)point.y);

‎src/platform/linux/window_shadow_linux.h‎

Lines changed: 98 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,23 @@ inline int ReservedMargin() {
2323
return window_shadow::Margin(maximum);
2424
}
2525

26+
// States in which the window manager fits the whole surface to the space it gives the
27+
// window - a tiling window manager's tiles, maximized, full screen. The gutter is then
28+
// no shadow, only a band of empty window around the content, so it is taken away
29+
// while the window is in one of them.
30+
constexpr int kFittedStates =
31+
GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN | GDK_WINDOW_STATE_TILED |
32+
GDK_WINDOW_STATE_TOP_TILED | GDK_WINDOW_STATE_RIGHT_TILED |
33+
GDK_WINDOW_STATE_BOTTOM_TILED | GDK_WINDOW_STATE_LEFT_TILED;
34+
2635
struct State {
2736
GtkWidget* target = nullptr;
2837
GtkWidget* overlay = nullptr;
2938
guint original_border = 0;
3039
int margin = ReservedMargin();
3140
std::shared_ptr<WindowShadow> custom;
3241
bool enabled = true;
42+
bool fitted = false;
3343
bool dirty = true;
3444
int width = 0, height = 0;
3545
WindowShape polygon;
@@ -42,6 +52,17 @@ struct State {
4252
inline State* Get(GtkWidget* widget) {
4353
return widget ? static_cast<State*>(g_object_get_data(G_OBJECT(widget), kState)) : nullptr;
4454
}
55+
// The gutter the surface has around the content right now.
56+
inline int Gutter(GtkWidget* widget) {
57+
auto* state = Get(widget);
58+
if (!state)
59+
return 0;
60+
return state->fitted ? static_cast<int>(state->original_border) : state->margin;
61+
}
62+
inline bool IsFitted(GtkWidget* widget) {
63+
auto* window = gtk_widget_get_window(widget);
64+
return window && (gdk_window_get_state(window) & kFittedStates);
65+
}
4566
inline void Path(cairo_t* cr, State* state, int width, int height) {
4667
if (!state->polygon.GetPointCount()) {
4768
cairo_rectangle(cr, 0, 0, width, height);
@@ -59,7 +80,7 @@ inline void Path(cairo_t* cr, State* state, int width, int height) {
5980
}
6081
inline gboolean Paint(GtkWidget* widget, cairo_t* cr, gpointer data, bool inside) {
6182
auto* state = static_cast<State*>(data);
62-
if (!state->enabled)
83+
if (!state->enabled || state->fitted)
6384
return FALSE;
6485
GtkWidget* child = gtk_bin_get_child(GTK_BIN(widget));
6586
if (!child)
@@ -118,8 +139,70 @@ inline gboolean DrawOverlay(GtkWidget*, cairo_t* cr, gpointer data) {
118139
auto* state = static_cast<State*>(data);
119140
return Paint(state->target, cr, data, true);
120141
}
142+
// The gutter is shadow, not window. Tell the window manager, so that it places,
143+
// snaps and tiles the content rather than the whole surface (_GTK_FRAME_EXTENTS on
144+
// X11, the xdg_surface window geometry on Wayland). GTK declares its own
145+
// decoration's extents while allocating the window; this runs after it and
146+
// declares everything around the content, GTK's part included.
147+
inline void DeclareGutter(GtkWidget* widget) {
148+
auto* window = gtk_widget_get_window(widget);
149+
auto* child = gtk_bin_get_child(GTK_BIN(widget));
150+
int x = 0, y = 0;
151+
if (!window || !child || !gtk_widget_get_visible(child) ||
152+
!gtk_widget_translate_coordinates(child, widget, 0, 0, &x, &y))
153+
return;
154+
const int right = gtk_widget_get_allocated_width(widget) - x -
155+
gtk_widget_get_allocated_width(child);
156+
const int bottom = gtk_widget_get_allocated_height(widget) - y -
157+
gtk_widget_get_allocated_height(child);
158+
if (x < 0 || y < 0 || right < 0 || bottom < 0)
159+
return;
160+
gdk_window_set_shadow_width(window, x, right, y, bottom);
161+
}
121162
inline void Allocated(GtkWidget* widget, GtkAllocation*, gpointer) {
122163
RefreshShadowInput(widget);
164+
DeclareGutter(widget);
165+
}
166+
167+
// GtkWindow paints its background under the whole allocation, gutter included,
168+
// which would hide the shadow behind an opaque band. A window that can be
169+
// translucent gets this class, and one rule for the screen clears its background;
170+
// the content paints its own.
171+
constexpr char kGutterStyleClass[] = "nativeapi-shadow-gutter";
172+
inline bool CanBeTranslucent(GtkWidget* widget) {
173+
auto* screen = gtk_widget_get_screen(widget);
174+
return gdk_screen_is_composited(screen) &&
175+
gtk_widget_get_visual(widget) == gdk_screen_get_rgba_visual(screen);
176+
}
177+
inline void ClearGutterBackground(GtkWidget* widget) {
178+
static bool installed = false;
179+
if (!installed) {
180+
installed = true;
181+
GtkCssProvider* provider = gtk_css_provider_new();
182+
gtk_css_provider_load_from_data(provider,
183+
"window.nativeapi-shadow-gutter,"
184+
"window.nativeapi-shadow-gutter:backdrop {"
185+
" background: none; }",
186+
-1, nullptr);
187+
gtk_style_context_add_provider_for_screen(gtk_widget_get_screen(widget),
188+
GTK_STYLE_PROVIDER(provider),
189+
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
190+
g_object_unref(provider);
191+
}
192+
gtk_style_context_add_class(gtk_widget_get_style_context(widget), kGutterStyleClass);
193+
}
194+
// The window manager has already sized the surface for the new state; changing the
195+
// border only moves the content edge to the surface edge, or back in.
196+
inline gboolean StateChanged(GtkWidget* widget, GdkEventWindowState* event, gpointer data) {
197+
auto* state = static_cast<State*>(data);
198+
const bool fitted = (event->new_window_state & kFittedStates) != 0;
199+
if (fitted != state->fitted) {
200+
state->fitted = fitted;
201+
gtk_container_set_border_width(GTK_CONTAINER(widget),
202+
fitted ? state->original_border : state->margin);
203+
gtk_widget_queue_draw(widget);
204+
}
205+
return FALSE;
123206
}
124207
inline void Remove(GtkWidget* widget) {
125208
auto* state = Get(widget);
@@ -130,6 +213,7 @@ inline void Remove(GtkWidget* widget) {
130213
const int width = child ? gtk_widget_get_allocated_width(child) : 0;
131214
const int height = child ? gtk_widget_get_allocated_height(child) : 0;
132215
g_signal_handlers_disconnect_by_data(widget, state);
216+
gtk_style_context_remove_class(gtk_widget_get_style_context(widget), kGutterStyleClass);
133217
if (state->overlay && child == state->overlay) {
134218
auto* content = gtk_bin_get_child(GTK_BIN(state->overlay));
135219
g_object_ref(content);
@@ -155,7 +239,13 @@ inline State* Ensure(GtkWidget* widget) {
155239
// Parent-window drawing is occluded by native GL child windows. A pass-through
156240
// GTK overlay paints the part of the core shadow inside the content rectangle;
157241
// the parent paints only the outer gutter, without double compositing.
158-
if (auto* content = gtk_bin_get_child(GTK_BIN(widget))) {
242+
// That part exists only for a polygon shape, and moving the content into the
243+
// overlay unrealizes and realizes it again, which a realized host widget may
244+
// not survive: a Flutter FlView then never renders again - black on Hyprland,
245+
// and on GNOME its window is never shown at all. Such content stays where it
246+
// is; the parent then paints the whole shadow outside the content.
247+
auto* content = gtk_bin_get_child(GTK_BIN(widget));
248+
if (content && !gtk_widget_get_realized(content)) {
159249
g_object_ref(content);
160250
gtk_container_remove(GTK_CONTAINER(widget), content);
161251
state->overlay = gtk_overlay_new();
@@ -176,10 +266,16 @@ inline State* Ensure(GtkWidget* widget) {
176266
state->custom = std::make_shared<WindowShadow>(*config);
177267
}
178268
state->original_border = gtk_container_get_border_width(GTK_CONTAINER(widget));
269+
state->fitted = IsFitted(widget);
179270
g_object_set_data_full(G_OBJECT(widget), kState, state,
180271
[](gpointer p) { delete static_cast<State*>(p); });
181272
g_signal_connect_after(widget, "draw", G_CALLBACK(Draw), state);
182273
g_signal_connect_after(widget, "size-allocate", G_CALLBACK(Allocated), state);
274+
g_signal_connect(widget, "window-state-event", G_CALLBACK(StateChanged), state);
275+
if (CanBeTranslucent(widget))
276+
ClearGutterBackground(widget);
277+
if (state->fitted)
278+
return state;
183279
// The gutter belongs to GTK, not the application's render tree. Keeping it
184280
// reserved while disabled avoids geometry changes when toggling the shadow.
185281
int width = 0, height = 0;

0 commit comments

Comments
 (0)