diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index b10db08adc..38d99db5c9 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -135,6 +135,7 @@ enum AvnWindowState Normal, Minimized, Maximized, + FullScreen, }; enum AvnStandardCursorType @@ -246,7 +247,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase { virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT SetCanResize(bool value) = 0; - virtual HRESULT SetHasDecorations(SystemDecorations value) = 0; + virtual HRESULT SetDecorations(SystemDecorations value) = 0; virtual HRESULT SetTitle (void* utf8Title) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 8092db3663..ec8fe9e6ee 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -35,6 +35,10 @@ struct INSWindowHolder struct IWindowStateChanged { virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; + virtual SystemDecorations Decorations () = 0; + virtual AvnWindowState WindowState () = 0; }; #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 68899df9f0..091219fc72 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -391,7 +391,7 @@ protected: void UpdateStyle() { - [Window setStyleMask:GetStyle()]; + [Window setStyleMask: GetStyle()]; } public: @@ -404,10 +404,13 @@ public: class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged { private: - bool _canResize = true; - SystemDecorations _hasDecorations = SystemDecorationsFull; - CGRect _lastUndecoratedFrame; + bool _canResize; + bool _fullScreenActive; + SystemDecorations _decorations; AvnWindowState _lastWindowState; + bool _inSetWindowState; + NSRect _preZoomSize; + bool _transitioningWindowState; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -421,6 +424,11 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _fullScreenActive = false; + _canResize = true; + _decorations = SystemDecorationsFull; + _transitioningWindowState = false; + _inSetWindowState = false; _lastWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; @@ -428,6 +436,20 @@ private: [Window setTabbingMode:NSWindowTabbingModeDisallowed]; } + void HideOrShowTrafficLights () + { + for (id subview in Window.contentView.superview.subviews) { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { + NSView *titlebarView = [subview subviews][0]; + for (id button in titlebarView.subviews) { + if ([button isKindOfClass:[NSButton class]]) { + [button setHidden: (_decorations != SystemDecorationsFull)]; + } + } + } + } + } + virtual HRESULT Show () override { @autoreleasepool @@ -439,6 +461,8 @@ private: WindowBaseImpl::Show(); + HideOrShowTrafficLights(); + return SetWindowState(_lastWindowState); } } @@ -459,41 +483,69 @@ private: [cparent->Window addChildWindow:Window ordered:NSWindowAbove]; WindowBaseImpl::Show(); + HideOrShowTrafficLights(); + return S_OK; } } + void StartStateTransition () override + { + _transitioningWindowState = true; + } + + void EndStateTransition () override + { + _transitioningWindowState = false; + } + + SystemDecorations Decorations () override + { + return _decorations; + } + + AvnWindowState WindowState () override + { + return _lastWindowState; + } + void WindowStateChanged () override { - AvnWindowState state; - GetWindowState(&state); - WindowEvents->WindowStateChanged(state); + if(!_inSetWindowState && !_transitioningWindowState) + { + AvnWindowState state; + GetWindowState(&state); + + if(_lastWindowState != state) + { + _lastWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } } bool UndecoratedIsMaximized () { - return CGRectEqualToRect([Window frame], [Window screen].visibleFrame); + auto windowSize = [Window frame]; + auto available = [Window screen].visibleFrame; + return CGRectEqualToRect(windowSize, available); } bool IsZoomed () { - return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); + return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized(); } void DoZoom() { - switch (_hasDecorations) + switch (_decorations) { case SystemDecorationsNone: - if (!UndecoratedIsMaximized()) - { - _lastUndecoratedFrame = [Window frame]; - } - - [Window zoom:Window]; + case SystemDecorationsBorderOnly: + [Window setFrame:[Window screen].visibleFrame display:true]; break; - case SystemDecorationsBorderOnly: + case SystemDecorationsFull: [Window performZoom:Window]; break; @@ -510,25 +562,52 @@ private: } } - virtual HRESULT SetHasDecorations(SystemDecorations value) override + virtual HRESULT SetDecorations(SystemDecorations value) override { @autoreleasepool { - _hasDecorations = value; + auto currentWindowState = _lastWindowState; + _decorations = value; + + if(_fullScreenActive) + { + return S_OK; + } + + auto currentFrame = [Window frame]; + UpdateStyle(); + + HideOrShowTrafficLights(); - switch (_hasDecorations) + switch (_decorations) { case SystemDecorationsNone: [Window setHasShadow:NO]; [Window setTitleVisibility:NSWindowTitleHidden]; [Window setTitlebarAppearsTransparent:YES]; + + if(currentWindowState == Maximized) + { + if(!UndecoratedIsMaximized()) + { + DoZoom(); + } + } break; case SystemDecorationsBorderOnly: [Window setHasShadow:YES]; [Window setTitleVisibility:NSWindowTitleHidden]; [Window setTitlebarAppearsTransparent:YES]; + + if(currentWindowState == Maximized) + { + if(!UndecoratedIsMaximized()) + { + DoZoom(); + } + } break; case SystemDecorationsFull: @@ -536,6 +615,13 @@ private: [Window setTitleVisibility:NSWindowTitleVisible]; [Window setTitlebarAppearsTransparent:NO]; [Window setTitle:_lastTitle]; + + if(currentWindowState == Maximized) + { + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } break; } @@ -592,13 +678,19 @@ private: return E_POINTER; } + if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen) + { + *ret = FullScreen; + return S_OK; + } + if([Window isMiniaturized]) { *ret = Minimized; return S_OK; } - if([Window isZoomed]) + if(IsZoomed()) { *ret = Maximized; return S_OK; @@ -610,16 +702,57 @@ private: } } + void EnterFullScreenMode () + { + _fullScreenActive = true; + + [Window setHasShadow:YES]; + [Window setTitleVisibility:NSWindowTitleVisible]; + [Window setTitlebarAppearsTransparent:NO]; + [Window setTitle:_lastTitle]; + + [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable]; + + [Window toggleFullScreen:nullptr]; + } + + void ExitFullScreenMode () + { + [Window toggleFullScreen:nullptr]; + + _fullScreenActive = false; + + SetDecorations(_decorations); + } + virtual HRESULT SetWindowState (AvnWindowState state) override { @autoreleasepool { + if(_lastWindowState == state) + { + return S_OK; + } + + _inSetWindowState = true; + + auto currentState = _lastWindowState; _lastWindowState = state; + if(currentState == Normal) + { + _preZoomSize = [Window frame]; + } + if(_shown) { switch (state) { case Maximized: + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + lastPositionSet.X = 0; lastPositionSet.Y = 0; @@ -635,40 +768,66 @@ private: break; case Minimized: - [Window miniaturize:Window]; + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + else + { + [Window miniaturize:Window]; + } + break; + + case FullScreen: + if([Window isMiniaturized]) + { + [Window deminiaturize:Window]; + } + + EnterFullScreenMode(); break; - default: + case Normal: if([Window isMiniaturized]) { [Window deminiaturize:Window]; } + if(currentState == FullScreen) + { + ExitFullScreenMode(); + } + if(IsZoomed()) { - DoZoom(); + if(_decorations == SystemDecorationsFull) + { + DoZoom(); + } + else + { + [Window setFrame:_preZoomSize display:true]; + auto newFrame = [Window contentRectForFrameRect:[Window frame]].size; + + [View setFrameSize:newFrame]; + } + } break; } } + _inSetWindowState = false; + return S_OK; } } virtual void OnResized () override { - if(_shown) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { - auto windowState = [Window isMiniaturized] ? Minimized - : (IsZoomed() ? Maximized : Normal); - - if (windowState != _lastWindowState) - { - _lastWindowState = windowState; - - WindowEvents->WindowStateChanged(windowState); - } + WindowStateChanged(); } } @@ -677,22 +836,23 @@ protected: { unsigned long s = NSWindowStyleMaskBorderless; - switch (_hasDecorations) + switch (_decorations) { case SystemDecorationsNone: + s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; break; case SystemDecorationsBorderOnly: - s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; + s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable; break; case SystemDecorationsFull: s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; + if(_canResize) { s = s | NSWindowStyleMaskResizable; } - break; } @@ -1171,6 +1331,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } +- (void)performClose:(id)sender +{ + if([[self delegate] respondsToSelector:@selector(windowShouldClose:)]) + { + if(![[self delegate] windowShouldClose:self]) return; + } + else if([self respondsToSelector:@selector(windowShouldClose:)]) + { + if(![self windowShouldClose:self]) return; + } + + [self close]; +} + - (void)pollModalSession:(nonnull NSModalSession)session { auto response = [NSApp runModalSession:session]; @@ -1399,7 +1573,66 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidResize:(NSNotification *)notification { - _parent->OnResized(); + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} + +- (void)windowWillExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + + if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized) + { + NSRect screenRect = [[self screen] visibleFrame]; + [self setFrame:screenRect display:YES]; + } + + if(parent->WindowState() == Minimized) + { + [self miniaturize:nullptr]; + } + + parent->WindowStateChanged(); + } +} + +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); + } } - (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index f3f70719e3..e02308b5c6 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -59,8 +59,8 @@ - - + + No Decorations Border Only Full Decorations @@ -69,6 +69,7 @@ Light Dark + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index bea751ad4c..935db20757 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow"> + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index b6aa3e92cd..0257b4ce66 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -1,4 +1,5 @@ using System.Reactive; +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Dialogs; @@ -11,6 +12,8 @@ namespace ControlCatalog.ViewModels private IManagedNotificationManager _notificationManager; private bool _isMenuItemChecked = true; + private WindowState _windowState; + private WindowState[] _windowStates; public MainWindowViewModel(IManagedNotificationManager notificationManager) { @@ -45,10 +48,32 @@ namespace ControlCatalog.ViewModels (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); }); - ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() => + ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() => { IsMenuItemChecked = !IsMenuItemChecked; }); + + WindowState = WindowState.Normal; + + WindowStates = new WindowState[] + { + WindowState.Minimized, + WindowState.Normal, + WindowState.Maximized, + WindowState.FullScreen, + }; + } + + public WindowState WindowState + { + get { return _windowState; } + set { this.RaiseAndSetIfChanged(ref _windowState, value); } + } + + public WindowState[] WindowStates + { + get { return _windowStates; } + set { this.RaiseAndSetIfChanged(ref _windowStates, value); } } public IManagedNotificationManager NotificationManager diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 905a14cfee..9cbde72f7f 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -74,16 +74,15 @@ namespace Avalonia.Controls.Presenters static TextPresenter() { - AffectsRender(PasswordCharProperty, - SelectionBrushProperty, SelectionForegroundBrushProperty, - SelectionStartProperty, SelectionEndProperty); + AffectsRender(SelectionBrushProperty); - Observable.Merge( - TextProperty.Changed, - SelectionStartProperty.Changed, - SelectionEndProperty.Changed, - PasswordCharProperty.Changed - ).AddClassHandler((x,_) => x.InvalidateFormattedText()); + Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed, + TextAlignmentProperty.Changed, TextWrappingProperty.Changed, + TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, + TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed, + SelectionStartProperty.Changed, SelectionEndProperty.Changed, + SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed + ).AddClassHandler((x, _) => x.InvalidateFormattedText()); CaretIndexProperty.Changed.AddClassHandler((x, e) => x.CaretIndexChanged((int)e.NewValue)); } @@ -184,7 +183,7 @@ namespace Avalonia.Controls.Presenters { get { - return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text)); + return _formattedText ?? (_formattedText = CreateFormattedText()); } } @@ -219,7 +218,7 @@ namespace Avalonia.Controls.Presenters get => GetValue(SelectionForegroundBrushProperty); set => SetValue(SelectionForegroundBrushProperty, value); } - + public IBrush CaretBrush { get => GetValue(CaretBrushProperty); @@ -284,13 +283,9 @@ namespace Avalonia.Controls.Presenters /// protected void InvalidateFormattedText() { - if (_formattedText != null) - { - _constraint = _formattedText.Constraint; - _formattedText = null; - } + _formattedText = null; - InvalidateVisual(); + InvalidateMeasure(); } /// @@ -307,6 +302,7 @@ namespace Avalonia.Controls.Presenters } FormattedText.Constraint = Bounds.Size; + context.DrawText(Foreground, new Point(), FormattedText); } @@ -424,20 +420,20 @@ namespace Avalonia.Controls.Presenters /// /// Creates the used to render the text. /// - /// The constraint of the text. - /// The text to generated the for. /// A object. - protected virtual FormattedText CreateFormattedText(Size constraint, string text) + protected virtual FormattedText CreateFormattedText() { FormattedText result = null; + var text = Text; + if (PasswordChar != default(char)) { - result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0)); + result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0)); } else { - result = CreateFormattedTextInternal(constraint, text); + result = CreateFormattedTextInternal(_constraint, text); } var selectionStart = SelectionStart; @@ -467,13 +463,15 @@ namespace Avalonia.Controls.Presenters { if (TextWrapping == TextWrapping.Wrap) { - FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity); + _constraint = new Size(availableSize.Width, double.PositiveInfinity); } else { - FormattedText.Constraint = Size.Infinity; + _constraint = Size.Infinity; } + _formattedText = null; + return FormattedText.Bounds.Size; } diff --git a/src/Avalonia.Controls/WindowState.cs b/src/Avalonia.Controls/WindowState.cs index 4ed30e726e..777b52dc11 100644 --- a/src/Avalonia.Controls/WindowState.cs +++ b/src/Avalonia.Controls/WindowState.cs @@ -19,5 +19,10 @@ namespace Avalonia.Controls /// The window is maximized. /// Maximized, + + /// + /// The window is fullscreen. + /// + FullScreen, } } diff --git a/src/Avalonia.Native/WindowImpl.cs b/src/Avalonia.Native/WindowImpl.cs index 6d6b2c5296..ec010815f4 100644 --- a/src/Avalonia.Native/WindowImpl.cs +++ b/src/Avalonia.Native/WindowImpl.cs @@ -67,7 +67,7 @@ namespace Avalonia.Native public void SetSystemDecorations(Controls.SystemDecorations enabled) { - _native.HasDecorations = (Interop.SystemDecorations)enabled; + _native.Decorations = (Interop.SystemDecorations)enabled; } public void SetTitleBarColor (Avalonia.Media.Color color) diff --git a/src/Avalonia.Visuals/Media/FormattedText.cs b/src/Avalonia.Visuals/Media/FormattedText.cs index 69806b22f2..53231ee1dd 100644 --- a/src/Avalonia.Visuals/Media/FormattedText.cs +++ b/src/Avalonia.Visuals/Media/FormattedText.cs @@ -200,7 +200,13 @@ namespace Avalonia.Media private void Set(ref T field, T value) { + if (field != null && field.Equals(value)) + { + return; + } + field = value; + _platformImpl = null; } } diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index db74a32b99..523b65c115 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -156,6 +156,7 @@ namespace Avalonia.X11 public readonly IntPtr _NET_SYSTEM_TRAY_OPCODE; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_HORZ; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_VERT; + public readonly IntPtr _NET_WM_STATE_FULLSCREEN; public readonly IntPtr _XEMBED; public readonly IntPtr _XEMBED_INFO; public readonly IntPtr _MOTIF_WM_HINTS; diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 3a919e2bc4..026a1457a3 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -220,16 +220,11 @@ namespace Avalonia.X11 var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; - if (_popup || _systemDecorations == SystemDecorations.None) - { + if (_popup + || _systemDecorations == SystemDecorations.None) decorations = 0; - } - else if (_systemDecorations == SystemDecorations.BorderOnly) - { - decorations = MotifDecorations.Border; - } - if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) + if (!_canResize) { functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); @@ -252,7 +247,7 @@ namespace Avalonia.X11 var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; - if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) + if (!_canResize) max = min = _realSize; if (preResize.HasValue) @@ -552,12 +547,21 @@ namespace Avalonia.X11 else if (value == WindowState.Maximized) { ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN); ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); } + else if (value == WindowState.FullScreen) + { + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_FULLSCREEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, + _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); + } else { ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); + ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN); ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT, _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); } @@ -585,6 +589,12 @@ namespace Avalonia.X11 break; } + if(pitems[c] == _x11.Atoms._NET_WM_STATE_FULLSCREEN) + { + state = WindowState.FullScreen; + break; + } + if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ || pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT) { @@ -810,7 +820,7 @@ namespace Avalonia.X11 public void SetSystemDecorations(SystemDecorations enabled) { - _systemDecorations = enabled; + _systemDecorations = enabled == SystemDecorations.Full ? SystemDecorations.Full : SystemDecorations.None; UpdateMotifHints(); UpdateSizeHints(null); } @@ -1052,7 +1062,7 @@ namespace Avalonia.X11 void ChangeWMAtoms(bool enable, params IntPtr[] atoms) { - if (atoms.Length < 1 || atoms.Length > 4) + if (atoms.Length != 1 && atoms.Length != 2) throw new ArgumentException(); if (!_mapped) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 6022e7a552..5f876464e2 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -149,7 +149,17 @@ namespace Avalonia.Skia if (index >= Text.Length || index < 0) { var r = rects.LastOrDefault(); - return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); + + var c = Text[Text.Length - 1]; + + switch (c) + { + case '\n': + case '\r': + return new Rect(r.X, r.Y, 0, _lineHeight); + default: + return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); + } } return rects[index]; } diff --git a/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs new file mode 100644 index 0000000000..1b01ebbe7f --- /dev/null +++ b/src/Windows/Avalonia.Win32/Interop/TaskBarList.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32.Interop +{ + internal class TaskBarList + { + private static IntPtr s_taskBarList; + private static HrInit s_hrInitDelegate; + private static MarkFullscreenWindow s_markFullscreenWindowDelegate; + + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// Fullscreen state. + public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen) + { + if (s_taskBarList == IntPtr.Zero) + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList); + + if (s_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + + if (s_hrInitDelegate is null) + { + s_hrInitDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); + } + + if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK) + { + s_taskBarList = IntPtr.Zero; + } + } + } + + if (s_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer(); + + if (s_markFullscreenWindowDelegate is null) + { + s_markFullscreenWindowDelegate = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + } + + s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen); + } + } + } +} diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index cbfa1abfb7..5601ccbafe 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop WS_SIZEFRAME = 0x40000, WS_SYSMENU = 0x80000, WS_TABSTOP = 0x10000, + WS_THICKFRAME = 0x40000, WS_VISIBLE = 0x10000000, WS_VSCROLL = 0x200000, WS_EX_DLGMODALFRAME = 0x00000001, @@ -1146,7 +1147,10 @@ namespace Avalonia.Win32.Interop internal static extern int CoCreateInstance(ref Guid clsid, IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); - + [DllImport("ole32.dll", PreserveSig = true)] + internal static extern int CoCreateInstance(ref Guid clsid, + IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter); + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); @@ -1642,6 +1646,8 @@ namespace Avalonia.Win32.Interop public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + public static readonly Guid TaskBarList = Guid.Parse("56FDF344-FD6D-11D0-958A-006097C9A090"); + public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"); } [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] @@ -1874,6 +1880,22 @@ namespace Avalonia.Win32.Interop [MarshalAs(UnmanagedType.LPWStr)] public string pszSpec; } + + public delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen); + public delegate HRESULT HrInit(IntPtr This); + + public struct ITaskBarList2VTable + { + public IntPtr IUnknown1; + public IntPtr IUnknown2; + public IntPtr IUnknown3; + public IntPtr HrInit; + public IntPtr AddTab; + public IntPtr DeleteTab; + public IntPtr ActivateTab; + public IntPtr SetActiveAlt; + public IntPtr MarkFullscreenWindow; + } } [Flags] diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index 963042b249..442794f0f0 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Win32 { public class ScreenImpl : IScreenImpl { - public int ScreenCount + public int ScreenCount { get => GetSystemMetrics(SystemMetric.SM_CMONITORS); } @@ -33,7 +33,7 @@ namespace Avalonia.Win32 var shcore = LoadLibrary("shcore.dll"); var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); if (method != IntPtr.Zero) - { + { GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); dpi = (double)x; } @@ -51,11 +51,8 @@ namespace Avalonia.Win32 RECT bounds = monitorInfo.rcMonitor; RECT workingArea = monitorInfo.rcWork; - PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left, - bounds.bottom - bounds.top); - PixelRect avaloniaWorkArea = - new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, - workingArea.bottom - workingArea.top); + PixelRect avaloniaBounds = bounds.ToPixelRect(); + PixelRect avaloniaWorkArea = workingArea.ToPixelRect(); screens[index] = new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 8bdd4b7bfa..c6164e0868 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Win32 Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; var openDialog = dialog as OpenFileDialog; @@ -105,9 +105,9 @@ namespace Avalonia.Win32 var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; - Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); diff --git a/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs new file mode 100644 index 0000000000..8193611f6d --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs @@ -0,0 +1,13 @@ +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + internal static class Win32TypeExtensions + { + public static PixelRect ToPixelRect(this RECT rect) + { + return new PixelRect(rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top); + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e193c72ef7..d82e9f64f1 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -37,10 +37,14 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; + private SavedWindowInfo _savedWindowInfo; + private bool _isFullScreenActive; + #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; #endif + private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE); private readonly List _disabledBy; private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; @@ -82,7 +86,9 @@ namespace Avalonia.Win32 _windowProperties = new WindowProperties { - ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full + ShowInTaskbar = false, + IsResizable = true, + Decorations = SystemDecorations.Full }; _rendererLock = new ManagedDeferredRendererLock(); @@ -538,27 +544,98 @@ namespace Avalonia.Win32 } } + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// Method must only be called from inside UpdateWindowProperties. + /// + /// + private void SetFullScreen(bool fullscreen) + { + if (fullscreen) + { + GetWindowRect(_hwnd, out var windowRect); + _savedWindowInfo.WindowRect = windowRect; + + var current = GetStyle(); + var currentEx = GetExtendedStyle(); + + _savedWindowInfo.Style = current; + _savedWindowInfo.ExStyle = currentEx; + + // Set new window style and size. + SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false); + SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false); + + // On expand, if we're given a window_rect, grow to it, otherwise do + // not resize. + MONITORINFO monitor_info = MONITORINFO.Create(); + GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); + + var window_rect = monitor_info.rcMonitor.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + + _isFullScreenActive = true; + } + else + { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + _isFullScreenActive = false; + + var windowStates = GetWindowStateStyles(); + SetStyle((_savedWindowInfo.Style & ~WindowStateMask) | windowStates, false); + SetExtendedStyle(_savedWindowInfo.ExStyle, false); + + // On restore, resize to the previous saved rect size. + var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + + UpdateWindowProperties(_windowProperties, true); + } + + TaskBarList.MarkFullscreen(_hwnd, fullscreen); + } + private void ShowWindow(WindowState state) { ShowWindowCommand command; + var newWindowProperties = _windowProperties; + switch (state) { case WindowState.Minimized: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Maximize; break; case WindowState.Normal: + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Restore; break; + case WindowState.FullScreen: + newWindowProperties.IsFullScreen = true; + UpdateWindowProperties(newWindowProperties); + return; + default: throw new ArgumentException("Invalid WindowState."); } + UpdateWindowProperties(newWindowProperties); + UnmanagedMethods.ShowWindow(_hwnd, command); if (state == WindowState.Maximized) @@ -590,22 +667,69 @@ namespace Avalonia.Win32 SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); } } + } + + private WindowStyles GetWindowStateStyles () + { + return GetStyle() & WindowStateMask; } - private WindowStyles GetStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + private WindowStyles GetStyle() + { + if (_isFullScreenActive) + { + return _savedWindowInfo.Style; + } + else + { + return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + } + } - private WindowStyles GetExtendedStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + private WindowStyles GetExtendedStyle() + { + if (_isFullScreenActive) + { + return _savedWindowInfo.ExStyle; + } + else + { + return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + } + } - private void SetStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style); + private void SetStyle(WindowStyles style, bool save = true) + { + if (save) + { + _savedWindowInfo.Style = style; + } - private void SetExtendedStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style); + if (!_isFullScreenActive) + { + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style); + } + } + + private void SetExtendedStyle(WindowStyles style, bool save = true) + { + if (save) + { + _savedWindowInfo.ExStyle = style; + } + + if (!_isFullScreenActive) + { + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style); + } + } private void UpdateEnabled() { EnableWindow(_hwnd, _disabledBy.Count == 0); } - private void UpdateWindowProperties(WindowProperties newProperties) + private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false) { var oldProperties = _windowProperties; @@ -613,7 +737,7 @@ namespace Avalonia.Win32 // according to the new values already. _windowProperties = newProperties; - if (oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) + if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges) { var exStyle = GetExtendedStyle(); @@ -632,7 +756,7 @@ namespace Avalonia.Win32 // Otherwise it will still show in the taskbar. } - if (oldProperties.IsResizable != newProperties.IsResizable) + if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges) { var style = GetStyle(); @@ -648,7 +772,12 @@ namespace Avalonia.Win32 SetStyle(style); } - if (oldProperties.Decorations != newProperties.Decorations) + if (oldProperties.IsFullScreen != newProperties.IsFullScreen) + { + SetFullScreen(newProperties.IsFullScreen); + } + + if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) { var style = GetStyle(); @@ -663,30 +792,33 @@ namespace Avalonia.Win32 style &= ~fullDecorationFlags; } - var margins = new MARGINS + SetStyle(style); + + if (!_isFullScreenActive) { - cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0 - }; + var margins = new MARGINS + { + cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0 + }; - DwmExtendFrameIntoClientArea(_hwnd, ref margins); + DwmExtendFrameIntoClientArea(_hwnd, ref margins); - GetClientRect(_hwnd, out var oldClientRect); - var oldClientRectOrigin = new POINT(); - ClientToScreen(_hwnd, ref oldClientRectOrigin); - oldClientRect.Offset(oldClientRectOrigin); + GetClientRect(_hwnd, out var oldClientRect); + var oldClientRectOrigin = new POINT(); + ClientToScreen(_hwnd, ref oldClientRectOrigin); + oldClientRect.Offset(oldClientRectOrigin); - SetStyle(style); + var newRect = oldClientRect; - var newRect = oldClientRect; + if (newProperties.Decorations == SystemDecorations.Full) + { + AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); + } - if (newProperties.Decorations == SystemDecorations.Full) - { - AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); + SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | + SetWindowPosFlags.SWP_FRAMECHANGED); } - - SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | - SetWindowPosFlags.SWP_FRAMECHANGED); } } @@ -713,11 +845,19 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + private struct SavedWindowInfo + { + public WindowStyles Style { get; set; } + public WindowStyles ExStyle { get; set; } + public RECT WindowRect { get; set; } + }; + private struct WindowProperties { public bool ShowInTaskbar; public bool IsResizable; public SystemDecorations Decorations; + public bool IsFullScreen; } } }