@@ -26,7 +26,12 @@ inline int ReservedMargin() {
2626// States in which the window manager fits the whole surface to the space it gives the
2727// window - a tiling window manager's tiles, maximized, full screen. The gutter is then
2828// 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.
29+ // while the window is in one of them - when the window manager really sized the
30+ // surface. Hyprland reports every toplevel as maximized and tiled, floating ones too,
31+ // so that it draws no client-side shadow, and leaves the size to the client: nothing
32+ // is painted in the gutter then, but the gutter stays, because GDK takes no resize
33+ // while those states stand and taking the border away would hand the content the
34+ // whole surface. The declared window geometry excludes the gutter either way.
3035constexpr int kFittedStates =
3136 GDK_WINDOW_STATE_MAXIMIZED | GDK_WINDOW_STATE_FULLSCREEN | GDK_WINDOW_STATE_TILED |
3237 GDK_WINDOW_STATE_TOP_TILED | GDK_WINDOW_STATE_RIGHT_TILED |
@@ -39,16 +44,31 @@ struct State {
3944 int margin = ReservedMargin();
4045 std::shared_ptr<WindowShadow> custom;
4146 bool enabled = true ;
47+ // The window manager's states say fitted: nothing is painted in the gutter.
4248 bool fitted = false ;
49+ // The surface carries the gutter: the container border is the margin.
50+ bool gutter = false ;
51+ // The surface size last allocated or asked for, to tell a surface the window manager
52+ // sized for a fitted state from one its states merely relabelled.
53+ int surface_width = 0 , surface_height = 0 ;
54+ // Whether to carry a gutter at all is decided once the compositor's states are
55+ // known (see Decide()); until then neither border nor size is touched.
56+ bool decided = false ;
57+ gulong decide_on_map = 0 ;
58+ guint decide_timeout = 0 ;
4359 bool dirty = true ;
4460 int width = 0 , height = 0 ;
4561 WindowShape polygon;
4662 cairo_surface_t * image = nullptr ;
4763 ~State () {
64+ if (decide_timeout)
65+ g_source_remove (decide_timeout);
4866 if (image)
4967 cairo_surface_destroy (image);
5068 }
5169};
70+ inline void Decide (GtkWidget* widget, State* state);
71+ inline void Trace (const char * where, GtkWidget* widget, State* state);
5272inline State* Get (GtkWidget* widget) {
5373 return widget ? static_cast <State*>(g_object_get_data (G_OBJECT (widget), kState )) : nullptr ;
5474}
@@ -57,7 +77,28 @@ inline int Gutter(GtkWidget* widget) {
5777 auto * state = Get (widget);
5878 if (!state)
5979 return 0 ;
60- return state->fitted ? static_cast <int >(state->original_border ) : state->margin ;
80+ return state->gutter ? state->margin : static_cast <int >(state->original_border );
81+ }
82+ // What the surface is about to be sized to (SetContentSize), so that a configure
83+ // echoing it is not taken for the window manager's own size.
84+ inline void NoteRequestedSize (GtkWidget* widget, int width, int height) {
85+ if (auto * state = Get (widget)) {
86+ state->surface_width = width;
87+ state->surface_height = height;
88+ }
89+ }
90+ inline bool SurfaceSizedByWindowManager (GtkWidget* widget, State* state) {
91+ int width = 0 , height = 0 ;
92+ gtk_window_get_size (GTK_WINDOW (widget), &width, &height);
93+ return state->surface_width > 0 &&
94+ (width != state->surface_width || height != state->surface_height );
95+ }
96+ inline void SetGutter (GtkWidget* widget, State* state, bool gutter) {
97+ if (state->gutter == gutter)
98+ return ;
99+ state->gutter = gutter;
100+ gtk_container_set_border_width (GTK_CONTAINER (widget),
101+ gutter ? state->margin : state->original_border );
61102}
62103inline bool IsFitted (GtkWidget* widget) {
63104 auto * window = gtk_widget_get_window (widget);
@@ -159,7 +200,9 @@ inline void DeclareGutter(GtkWidget* widget) {
159200 return ;
160201 gdk_window_set_shadow_width (window, x, right, y, bottom);
161202}
162- inline void Allocated (GtkWidget* widget, GtkAllocation*, gpointer) {
203+ inline void Allocated (GtkWidget* widget, GtkAllocation*, gpointer data) {
204+ auto * state = static_cast <State*>(data);
205+ gtk_window_get_size (GTK_WINDOW (widget), &state->surface_width , &state->surface_height );
163206 RefreshShadowInput (widget);
164207 DeclareGutter (widget);
165208}
@@ -191,17 +234,32 @@ inline void ClearGutterBackground(GtkWidget* widget) {
191234 }
192235 gtk_style_context_add_class (gtk_widget_get_style_context (widget), kGutterStyleClass );
193236}
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.
237+ // Into a fitted state the window manager has already sized the surface, and changing
238+ // the border only moves the content edge to the surface edge; out of one it restores
239+ // the surface it had, gutter included, and the border moves the edge back in. A fitted
240+ // state that came with no size of the window manager's (Hyprland, see kFittedStates)
241+ // keeps the gutter: the surface is what the client asked for, and GDK would not
242+ // shrink it now anyway.
196243inline gboolean StateChanged (GtkWidget* widget, GdkEventWindowState* event, gpointer data) {
197244 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);
245+ Trace (" state-event" , widget, state);
246+ if (!state->decided ) {
247+ // The compositor's first configure brings the states: a fitted one, or none, in
248+ // which case the focus GDK assumed on show goes away. GDK's own show-time event
249+ // (WITHDRAWN cleared, FOCUSED set) is not an answer.
250+ const bool fitted_changed = (event->changed_mask & kFittedStates ) != 0 ;
251+ const bool focus_withdrawn = (event->changed_mask & GDK_WINDOW_STATE_FOCUSED ) &&
252+ !(event->new_window_state & GDK_WINDOW_STATE_FOCUSED );
253+ if (fitted_changed || focus_withdrawn)
254+ Decide (widget, state);
255+ return FALSE ;
204256 }
257+ const bool fitted = (event->new_window_state & kFittedStates ) != 0 ;
258+ if (fitted == state->fitted )
259+ return FALSE ;
260+ state->fitted = fitted;
261+ SetGutter (widget, state, !fitted || !SurfaceSizedByWindowManager (widget, state));
262+ gtk_widget_queue_draw (widget);
205263 return FALSE ;
206264}
207265inline void Remove (GtkWidget* widget) {
@@ -229,6 +287,85 @@ inline void Remove(GtkWidget* widget) {
229287 g_object_set_data (G_OBJECT (widget), kState , nullptr );
230288 gtk_widget_queue_draw (widget);
231289}
290+ // Grows the surface by the margin on every side, around the content as it is, and
291+ // moves it back so the content stays put. Only once the window manager's states are
292+ // known: on a compositor that reports every window as tiled (see kFittedStates) GDK
293+ // takes no resize afterwards, and a gutter added before would hand the content the
294+ // whole surface for good.
295+ inline void AddGutter (GtkWidget* widget, State* state) {
296+ int width = 0 , height = 0 ;
297+ int x = 0 , y = 0 ;
298+ gtk_window_get_position (GTK_WINDOW (widget), &x, &y);
299+ gtk_window_get_size (GTK_WINDOW (widget), &width, &height);
300+ SetGutter (widget, state, true );
301+ const int extra = state->margin - state->original_border ;
302+ NoteRequestedSize (widget, width + extra * 2 , height + extra * 2 );
303+ gtk_window_resize (GTK_WINDOW (widget), width + extra * 2 , height + extra * 2 );
304+ gtk_window_move (GTK_WINDOW (widget), x - extra, y - extra);
305+ }
306+ inline void Trace (const char * where, GtkWidget* widget, State* state) {
307+ static const bool on = g_getenv (" NATIVEAPI_SHADOW_TRACE" ) != nullptr ;
308+ if (!on) return ;
309+ auto * window = gtk_widget_get_window (widget);
310+ int w = 0 , h = 0 ;
311+ gtk_window_get_size (GTK_WINDOW (widget), &w, &h);
312+ g_printerr (" [shadow %ld] %s: state 0x%x mapped %d decided %d fitted %d gutter %d size %dx%d\n " ,
313+ (long )(g_get_monotonic_time () / 1000 ), where,
314+ window ? (unsigned )gdk_window_get_state (window) : 0u , gtk_widget_get_mapped (widget),
315+ state->decided , state->fitted , state->gutter , w, h);
316+ }
317+ inline void Decide (GtkWidget* widget, State* state) {
318+ if (state->decided )
319+ return ;
320+ Trace (" decide" , widget, state);
321+ state->decided = true ;
322+ if (state->decide_on_map ) {
323+ g_signal_handler_disconnect (widget, state->decide_on_map );
324+ state->decide_on_map = 0 ;
325+ }
326+ if (state->decide_timeout ) {
327+ g_source_remove (state->decide_timeout );
328+ state->decide_timeout = 0 ;
329+ }
330+ state->fitted = IsFitted (widget);
331+ if (!state->fitted )
332+ AddGutter (widget, state);
333+ gtk_widget_queue_draw (widget);
334+ }
335+ inline gboolean DecideOnTimeout (gpointer data) {
336+ auto * state = static_cast <State*>(data);
337+ state->decide_timeout = 0 ;
338+ Trace (" timeout" , state->target , state);
339+ Decide (state->target , state);
340+ return G_SOURCE_REMOVE ;
341+ }
342+ // A window that is on screen but reports no state (floating, unfocused) gets no
343+ // configure of its own accord: decide for it after a moment.
344+ constexpr guint kDecideTimeoutMs = 300 ;
345+ inline void ArmDecideTimeout (State* state) {
346+ if (!state->decided && !state->decide_timeout )
347+ state->decide_timeout = g_timeout_add (kDecideTimeoutMs , DecideOnTimeout, state);
348+ }
349+ inline gboolean ArmDecideOnMap (GtkWidget*, GdkEvent*, gpointer data) {
350+ ArmDecideTimeout (static_cast <State*>(data));
351+ return FALSE ;
352+ }
353+ // Decide now when the window already reports a fitted state; otherwise at the
354+ // window-state event that brings the compositor's answer (StateChanged), or shortly
355+ // after the window is mapped. Not at the first configure-event: GTK emits one for its
356+ // own initial size before the compositor has answered. Not on any state event either:
357+ // GDK marks a toplevel focused as it shows it, before the compositor has said so.
358+ inline void DecideWhenKnown (GtkWidget* widget, State* state) {
359+ if (IsFitted (widget)) {
360+ Decide (widget, state);
361+ return ;
362+ }
363+ if (gtk_widget_get_mapped (widget))
364+ ArmDecideTimeout (state);
365+ else
366+ state->decide_on_map = g_signal_connect (widget, " map-event" , G_CALLBACK (ArmDecideOnMap), state);
367+ }
368+
232369inline State* Ensure (GtkWidget* widget) {
233370 if (auto * state = Get (widget))
234371 return state;
@@ -266,26 +403,18 @@ inline State* Ensure(GtkWidget* widget) {
266403 state->custom = std::make_shared<WindowShadow>(*config);
267404 }
268405 state->original_border = gtk_container_get_border_width (GTK_CONTAINER (widget));
269- state->fitted = IsFitted (widget);
406+ state->fitted = IsFitted (widget); // final once Decide() has run
270407 g_object_set_data_full (G_OBJECT (widget), kState , state,
271408 [](gpointer p) { delete static_cast <State*>(p); });
272409 g_signal_connect_after (widget, " draw" , G_CALLBACK (Draw), state);
273410 g_signal_connect_after (widget, " size-allocate" , G_CALLBACK (Allocated), state);
274411 g_signal_connect (widget, " window-state-event" , G_CALLBACK (StateChanged), state);
275412 if (CanBeTranslucent (widget))
276413 ClearGutterBackground (widget);
277- if (state->fitted )
278- return state;
279414 // The gutter belongs to GTK, not the application's render tree. Keeping it
280415 // reserved while disabled avoids geometry changes when toggling the shadow.
281- int width = 0 , height = 0 ;
282- int x = 0 , y = 0 ;
283- gtk_window_get_position (GTK_WINDOW (widget), &x, &y);
284- gtk_window_get_size (GTK_WINDOW (widget), &width, &height);
285- gtk_container_set_border_width (GTK_CONTAINER (widget), state->margin );
286- const int extra = state->margin - state->original_border ;
287- gtk_window_resize (GTK_WINDOW (widget), width + extra * 2 , height + extra * 2 );
288- gtk_window_move (GTK_WINDOW (widget), x - extra, y - extra);
416+ Trace (" ensure" , widget, state);
417+ DecideWhenKnown (widget, state);
289418 return state;
290419}
291420inline void Configure (GtkWidget* widget, std::shared_ptr<WindowShadow> custom) {
0 commit comments