@@ -40,6 +40,8 @@ public enum PageType
4040
4141public partial class MainWindow : Window
4242{
43+ private const string FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE = "UNIGETUI_FORCE_NATIVE_LINUX_DECORATIONS" ;
44+
4345 // Workaround for Avalonia 12 issue #21160 / #21212: BorderOnly + ExtendClientArea
4446 // strips WS_CAPTION / WS_THICKFRAME, which makes DWM disable Aero Snap drag-to-top,
4547 // Win+Up, and the maximize/minimize/restore animations. Re-add those bits on every
@@ -242,14 +244,15 @@ private void SetupTitleBar()
242244 }
243245 else if ( OperatingSystem . IsLinux ( ) )
244246 {
245- // WSLg can report incorrect maximize/input bounds with frameless windows.
246- // Keep native decorations there and use the in-app toolbar only.
247- bool isWsl = IsRunningUnderWsl ( ) ;
248- WindowDecorations = isWsl ? WindowDecorations . Full : WindowDecorations . None ;
247+ // WSLg and SSH-forwarded X11 can report incorrect maximize/input bounds
248+ // with frameless windows. Keep native decorations for those environments.
249+ bool useNativeDecorations = ShouldUseNativeLinuxWindowDecorations ( out string decorationReason ) ;
250+ Logger . Info ( $ "Linux window decorations: { ( useNativeDecorations ? "native" : "custom" ) } ({ decorationReason } )") ;
251+ WindowDecorations = useNativeDecorations ? WindowDecorations . Full : WindowDecorations . None ;
249252 TitleBarGrid . ClearValue ( HeightProperty ) ;
250253 TitleBarGrid . Height = 44 ;
251254 HamburgerPanel . Margin = new Thickness ( 10 , 0 , 8 , 0 ) ;
252- LinuxWindowButtons . IsVisible = ! isWsl ;
255+ LinuxWindowButtons . IsVisible = ! useNativeDecorations ;
253256 MainContentGrid . Margin = new Thickness ( 0 , 44 , 0 , 0 ) ;
254257 // Keep maximize icon in sync with window state
255258 this . GetObservable ( WindowStateProperty ) . Subscribe ( state =>
@@ -259,21 +262,111 @@ private void SetupTitleBar()
259262
260263 // Avalonia's X11 backend treats BorderOnly as None (no decorations at all).
261264 // Add invisible resize grips so the user can still resize by dragging edges.
262- if ( ! isWsl )
265+ if ( ! useNativeDecorations )
263266 {
264267 CreateResizeGrips ( ) ;
265268 }
266269
267270 }
268271 }
269272
273+ private static bool ShouldUseNativeLinuxWindowDecorations ( out string reason )
274+ {
275+ if ( TryGetNativeLinuxDecorationsOverride ( out bool forceNativeDecorations ) )
276+ {
277+ reason = $ "{ FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE } ={ ( forceNativeDecorations ? "true" : "false" ) } ";
278+ return forceNativeDecorations ;
279+ }
280+
281+ if ( IsRunningUnderWsl ( ) )
282+ {
283+ reason = "WSL environment" ;
284+ return true ;
285+ }
286+
287+ if ( IsRunningUnderSshX11Forwarding ( ) )
288+ {
289+ reason = "SSH X11 forwarding" ;
290+ return true ;
291+ }
292+
293+ reason = "default Linux desktop" ;
294+ return false ;
295+ }
296+
297+ private static bool TryGetNativeLinuxDecorationsOverride ( out bool forceNativeDecorations )
298+ {
299+ forceNativeDecorations = false ;
300+
301+ string ? overrideValue = Environment . GetEnvironmentVariable ( FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE ) ;
302+ if ( string . IsNullOrWhiteSpace ( overrideValue ) )
303+ {
304+ return false ;
305+ }
306+
307+ switch ( overrideValue . Trim ( ) . ToLowerInvariant ( ) )
308+ {
309+ case "1" :
310+ case "true" :
311+ case "on" :
312+ case "yes" :
313+ case "enabled" :
314+ forceNativeDecorations = true ;
315+ return true ;
316+
317+ case "0" :
318+ case "false" :
319+ case "off" :
320+ case "no" :
321+ case "disabled" :
322+ forceNativeDecorations = false ;
323+ return true ;
324+
325+ default :
326+ Logger . Warn ( $ "Ignoring invalid { FORCE_NATIVE_LINUX_DECORATIONS_ENVIRONMENT_VARIABLE } value '{ overrideValue } '. Use true/false.") ;
327+ return false ;
328+ }
329+ }
330+
270331 private static bool IsRunningUnderWsl ( )
271332 {
272333 string ? wslDistro = Environment . GetEnvironmentVariable ( "WSL_DISTRO_NAME" ) ;
273334 string ? wslInterop = Environment . GetEnvironmentVariable ( "WSL_INTEROP" ) ;
274335 return ! string . IsNullOrWhiteSpace ( wslDistro ) || ! string . IsNullOrWhiteSpace ( wslInterop ) ;
275336 }
276337
338+ private static bool IsRunningUnderSshX11Forwarding ( )
339+ {
340+ if ( ! OperatingSystem . IsLinux ( ) )
341+ {
342+ return false ;
343+ }
344+
345+ string ? display = Environment . GetEnvironmentVariable ( "DISPLAY" ) ;
346+ if ( string . IsNullOrWhiteSpace ( display ) )
347+ {
348+ return false ;
349+ }
350+
351+ bool hasSshSession =
352+ ! string . IsNullOrWhiteSpace ( Environment . GetEnvironmentVariable ( "SSH_CONNECTION" ) ) ||
353+ ! string . IsNullOrWhiteSpace ( Environment . GetEnvironmentVariable ( "SSH_CLIENT" ) ) ;
354+ if ( ! hasSshSession )
355+ {
356+ return false ;
357+ }
358+
359+ string normalizedDisplay = display . Trim ( ) ;
360+ if ( normalizedDisplay . StartsWith ( ":" , StringComparison . Ordinal ) ||
361+ normalizedDisplay . StartsWith ( "unix/" , StringComparison . OrdinalIgnoreCase ) ||
362+ normalizedDisplay . StartsWith ( "unix:" , StringComparison . OrdinalIgnoreCase ) )
363+ {
364+ return false ;
365+ }
366+
367+ return normalizedDisplay . Contains ( ':' ) ;
368+ }
369+
277370 /// <summary>
278371 /// Creates invisible resize-grip borders at the edges and corners of the window,
279372 /// enabling mouse-driven resize on platforms where native decorations are absent
0 commit comments