Browse Source

Merge branch 'master' into feature/3744-onapplytemplate

pull/3858/head
Dariusz Komosiński 7 years ago
committed by GitHub
parent
commit
081b7b0daa
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      build/SkiaSharp.props
  2. 3
      native/Avalonia.Native/inc/avalonia-native.h
  3. 4
      native/Avalonia.Native/src/OSX/app.mm
  4. 6
      native/Avalonia.Native/src/OSX/window.h
  5. 330
      native/Avalonia.Native/src/OSX/window.mm
  6. 5
      samples/ControlCatalog/MainView.xaml
  7. 2
      samples/ControlCatalog/MainWindow.xaml
  8. 27
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  9. 2
      src/Avalonia.Animation/Animation.cs
  10. 9
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  11. 3
      src/Avalonia.Animation/Animators/Animator`1.cs
  12. 20
      src/Avalonia.Animation/KeyFrame.cs
  13. 349
      src/Avalonia.Animation/KeySpline.cs
  14. 22
      src/Avalonia.Controls/ComboBox.cs
  15. 2
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  16. 46
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  17. 13
      src/Avalonia.Controls/Window.cs
  18. 5
      src/Avalonia.Controls/WindowState.cs
  19. 2
      src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
  20. 2
      src/Avalonia.Native/WindowImpl.cs
  21. 148
      src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
  22. 26
      src/Avalonia.Visuals/Media/FontManager.cs
  23. 3
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  24. 16
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  25. 6
      src/Avalonia.Visuals/Media/FormattedText.cs
  26. 2
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
  27. 2
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  28. 44
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs
  29. 26
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  30. 9
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  31. 14
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  32. 1
      src/Avalonia.X11/X11Atoms.cs
  33. 32
      src/Avalonia.X11/X11Window.cs
  34. 67
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  35. 46
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  36. 15
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  37. 9
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  38. 126
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  39. 4
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  40. 2
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  41. 9
      src/Skia/Avalonia.Skia/SkiaOptions.cs
  42. 2
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  43. 2
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  44. 55
      src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
  45. 24
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  46. 11
      src/Windows/Avalonia.Win32/ScreenImpl.cs
  47. 6
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  48. 13
      src/Windows/Avalonia.Win32/Win32TypeExtensions.cs
  49. 192
      src/Windows/Avalonia.Win32/WindowImpl.cs
  50. 145
      tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
  51. 313
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  52. 2
      tests/Avalonia.RenderTests/TestBase.cs
  53. 48
      tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
  54. 13
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  55. 24
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  56. 11
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  57. 12
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

4
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup> <ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.1" /> <PackageReference Include="SkiaSharp" Version="1.68.2" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" /> <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

3
native/Avalonia.Native/inc/avalonia-native.h

@ -135,6 +135,7 @@ enum AvnWindowState
Normal, Normal,
Minimized, Minimized,
Maximized, Maximized,
FullScreen,
}; };
enum AvnStandardCursorType enum AvnStandardCursorType
@ -246,7 +247,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{ {
virtual HRESULT ShowDialog (IAvnWindow* parent) = 0; virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 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 SetTitle (void* utf8Title) = 0;
virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0;

4
native/Avalonia.Native/src/OSX/app.mm

@ -15,6 +15,10 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
} }
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
[[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]];
} }
} }

6
native/Avalonia.Native/src/OSX/window.h

@ -19,6 +19,8 @@ class WindowBaseImpl;
-(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) pollModalSession: (NSModalSession _Nonnull) session;
-(void) restoreParentWindow; -(void) restoreParentWindow;
-(bool) shouldTryToHandleEvents; -(bool) shouldTryToHandleEvents;
-(bool) isModal;
-(void) setModal: (bool) isModal;
-(void) showAppMenuOnly; -(void) showAppMenuOnly;
-(void) showWindowMenuWithAppMenu; -(void) showWindowMenuWithAppMenu;
-(void) applyMenu:(NSMenu* _Nullable)menu; -(void) applyMenu:(NSMenu* _Nullable)menu;
@ -33,6 +35,10 @@ struct INSWindowHolder
struct IWindowStateChanged struct IWindowStateChanged
{ {
virtual void WindowStateChanged () = 0; virtual void WindowStateChanged () = 0;
virtual void StartStateTransition () = 0;
virtual void EndStateTransition () = 0;
virtual SystemDecorations Decorations () = 0;
virtual AvnWindowState WindowState () = 0;
}; };
#endif /* window_h */ #endif /* window_h */

330
native/Avalonia.Native/src/OSX/window.mm

@ -391,7 +391,7 @@ protected:
void UpdateStyle() void UpdateStyle()
{ {
[Window setStyleMask:GetStyle()]; [Window setStyleMask: GetStyle()];
} }
public: public:
@ -404,10 +404,13 @@ public:
class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
{ {
private: private:
bool _canResize = true; bool _canResize;
SystemDecorations _hasDecorations = SystemDecorationsFull; bool _fullScreenActive;
CGRect _lastUndecoratedFrame; SystemDecorations _decorations;
AvnWindowState _lastWindowState; AvnWindowState _lastWindowState;
bool _inSetWindowState;
NSRect _preZoomSize;
bool _transitioningWindowState;
FORWARD_IUNKNOWN() FORWARD_IUNKNOWN()
BEGIN_INTERFACE_MAP() BEGIN_INTERFACE_MAP()
@ -421,10 +424,30 @@ private:
ComPtr<IAvnWindowEvents> WindowEvents; ComPtr<IAvnWindowEvents> WindowEvents;
WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
{ {
_fullScreenActive = false;
_canResize = true;
_decorations = SystemDecorationsFull;
_transitioningWindowState = false;
_inSetWindowState = false;
_lastWindowState = Normal; _lastWindowState = Normal;
WindowEvents = events; WindowEvents = events;
[Window setCanBecomeKeyAndMain]; [Window setCanBecomeKeyAndMain];
[Window disableCursorRects]; [Window disableCursorRects];
[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 virtual HRESULT Show () override
@ -433,8 +456,13 @@ private:
{ {
if([Window parentWindow] != nil) if([Window parentWindow] != nil)
[[Window parentWindow] removeChildWindow:Window]; [[Window parentWindow] removeChildWindow:Window];
[Window setModal:FALSE];
WindowBaseImpl::Show(); WindowBaseImpl::Show();
HideOrShowTrafficLights();
return SetWindowState(_lastWindowState); return SetWindowState(_lastWindowState);
} }
} }
@ -450,44 +478,74 @@ private:
if(cparent == nullptr) if(cparent == nullptr)
return E_INVALIDARG; return E_INVALIDARG;
[Window setModal:TRUE];
[cparent->Window addChildWindow:Window ordered:NSWindowAbove]; [cparent->Window addChildWindow:Window ordered:NSWindowAbove];
WindowBaseImpl::Show(); WindowBaseImpl::Show();
HideOrShowTrafficLights();
return S_OK; 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 void WindowStateChanged () override
{ {
AvnWindowState state; if(!_inSetWindowState && !_transitioningWindowState)
GetWindowState(&state); {
WindowEvents->WindowStateChanged(state); AvnWindowState state;
GetWindowState(&state);
if(_lastWindowState != state)
{
_lastWindowState = state;
WindowEvents->WindowStateChanged(state);
}
}
} }
bool UndecoratedIsMaximized () bool UndecoratedIsMaximized ()
{ {
return CGRectEqualToRect([Window frame], [Window screen].visibleFrame); auto windowSize = [Window frame];
auto available = [Window screen].visibleFrame;
return CGRectEqualToRect(windowSize, available);
} }
bool IsZoomed () bool IsZoomed ()
{ {
return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized(); return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized();
} }
void DoZoom() void DoZoom()
{ {
switch (_hasDecorations) switch (_decorations)
{ {
case SystemDecorationsNone: case SystemDecorationsNone:
if (!UndecoratedIsMaximized()) case SystemDecorationsBorderOnly:
{ [Window setFrame:[Window screen].visibleFrame display:true];
_lastUndecoratedFrame = [Window frame];
}
[Window zoom:Window];
break; break;
case SystemDecorationsBorderOnly:
case SystemDecorationsFull: case SystemDecorationsFull:
[Window performZoom:Window]; [Window performZoom:Window];
break; break;
@ -504,25 +562,52 @@ private:
} }
} }
virtual HRESULT SetHasDecorations(SystemDecorations value) override virtual HRESULT SetDecorations(SystemDecorations value) override
{ {
@autoreleasepool @autoreleasepool
{ {
_hasDecorations = value; auto currentWindowState = _lastWindowState;
_decorations = value;
if(_fullScreenActive)
{
return S_OK;
}
auto currentFrame = [Window frame];
UpdateStyle(); UpdateStyle();
HideOrShowTrafficLights();
switch (_hasDecorations) switch (_decorations)
{ {
case SystemDecorationsNone: case SystemDecorationsNone:
[Window setHasShadow:NO]; [Window setHasShadow:NO];
[Window setTitleVisibility:NSWindowTitleHidden]; [Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES]; [Window setTitlebarAppearsTransparent:YES];
if(currentWindowState == Maximized)
{
if(!UndecoratedIsMaximized())
{
DoZoom();
}
}
break; break;
case SystemDecorationsBorderOnly: case SystemDecorationsBorderOnly:
[Window setHasShadow:YES]; [Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleHidden]; [Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES]; [Window setTitlebarAppearsTransparent:YES];
if(currentWindowState == Maximized)
{
if(!UndecoratedIsMaximized())
{
DoZoom();
}
}
break; break;
case SystemDecorationsFull: case SystemDecorationsFull:
@ -530,6 +615,13 @@ private:
[Window setTitleVisibility:NSWindowTitleVisible]; [Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO]; [Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle]; [Window setTitle:_lastTitle];
if(currentWindowState == Maximized)
{
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
[View setFrameSize:newFrame];
}
break; break;
} }
@ -586,13 +678,19 @@ private:
return E_POINTER; return E_POINTER;
} }
if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen)
{
*ret = FullScreen;
return S_OK;
}
if([Window isMiniaturized]) if([Window isMiniaturized])
{ {
*ret = Minimized; *ret = Minimized;
return S_OK; return S_OK;
} }
if([Window isZoomed]) if(IsZoomed())
{ {
*ret = Maximized; *ret = Maximized;
return S_OK; return S_OK;
@ -604,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 virtual HRESULT SetWindowState (AvnWindowState state) override
{ {
@autoreleasepool @autoreleasepool
{ {
if(_lastWindowState == state)
{
return S_OK;
}
_inSetWindowState = true;
auto currentState = _lastWindowState;
_lastWindowState = state; _lastWindowState = state;
if(currentState == Normal)
{
_preZoomSize = [Window frame];
}
if(_shown) if(_shown)
{ {
switch (state) { switch (state) {
case Maximized: case Maximized:
if(currentState == FullScreen)
{
ExitFullScreenMode();
}
lastPositionSet.X = 0; lastPositionSet.X = 0;
lastPositionSet.Y = 0; lastPositionSet.Y = 0;
@ -629,40 +768,66 @@ private:
break; break;
case Minimized: case Minimized:
[Window miniaturize:Window]; if(currentState == FullScreen)
{
ExitFullScreenMode();
}
else
{
[Window miniaturize:Window];
}
break;
case FullScreen:
if([Window isMiniaturized])
{
[Window deminiaturize:Window];
}
EnterFullScreenMode();
break; break;
default: case Normal:
if([Window isMiniaturized]) if([Window isMiniaturized])
{ {
[Window deminiaturize:Window]; [Window deminiaturize:Window];
} }
if(currentState == FullScreen)
{
ExitFullScreenMode();
}
if(IsZoomed()) if(IsZoomed())
{ {
DoZoom(); if(_decorations == SystemDecorationsFull)
{
DoZoom();
}
else
{
[Window setFrame:_preZoomSize display:true];
auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
[View setFrameSize:newFrame];
}
} }
break; break;
} }
} }
_inSetWindowState = false;
return S_OK; return S_OK;
} }
} }
virtual void OnResized () override virtual void OnResized () override
{ {
if(_shown) if(_shown && !_inSetWindowState && !_transitioningWindowState)
{ {
auto windowState = [Window isMiniaturized] ? Minimized WindowStateChanged();
: (IsZoomed() ? Maximized : Normal);
if (windowState != _lastWindowState)
{
_lastWindowState = windowState;
WindowEvents->WindowStateChanged(windowState);
}
} }
} }
@ -671,22 +836,23 @@ protected:
{ {
unsigned long s = NSWindowStyleMaskBorderless; unsigned long s = NSWindowStyleMaskBorderless;
switch (_hasDecorations) switch (_decorations)
{ {
case SystemDecorationsNone: case SystemDecorationsNone:
s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
break; break;
case SystemDecorationsBorderOnly: case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
break; break;
case SystemDecorationsFull: case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless; s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
if(_canResize) if(_canResize)
{ {
s = s | NSWindowStyleMaskResizable; s = s | NSWindowStyleMaskResizable;
} }
break; break;
} }
@ -1144,6 +1310,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
ComPtr<WindowBaseImpl> _parent; ComPtr<WindowBaseImpl> _parent;
bool _canBecomeKeyAndMain; bool _canBecomeKeyAndMain;
bool _closed; bool _closed;
bool _isModal;
AvnMenu* _menu; AvnMenu* _menu;
double _lastScaling; double _lastScaling;
} }
@ -1164,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 - (void)pollModalSession:(nonnull NSModalSession)session
{ {
auto response = [NSApp runModalSession:session]; auto response = [NSApp runModalSession:session];
@ -1322,11 +1503,25 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
auto ch = objc_cast<AvnWindow>(uch); auto ch = objc_cast<AvnWindow>(uch);
if(ch == nil) if(ch == nil)
continue; continue;
if(![ch isModal])
continue;
return FALSE; return FALSE;
} }
return TRUE; return TRUE;
} }
-(bool) isModal
{
return _isModal;
}
-(void) setModal: (bool) isModal
{
_isModal = isModal;
}
-(void)makeKeyWindow -(void)makeKeyWindow
{ {
if([self activateAppropriateChild: true]) if([self activateAppropriateChild: true])
@ -1378,7 +1573,66 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
- (void)windowDidResize:(NSNotification *)notification - (void)windowDidResize:(NSNotification *)notification
{ {
_parent->OnResized(); auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->WindowStateChanged();
}
}
- (void)windowWillExitFullScreen:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->StartStateTransition();
}
}
- (void)windowDidExitFullScreen:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_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<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->StartStateTransition();
}
}
- (void)windowDidEnterFullScreen:(NSNotification *)notification
{
auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
if(parent != nullptr)
{
parent->EndStateTransition();
parent->WindowStateChanged();
}
} }
- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame - (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame

5
samples/ControlCatalog/MainView.xaml

@ -59,8 +59,8 @@
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem> <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem> <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
<TabControl.Tag> <TabControl.Tag>
<StackPanel Width="115" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom"> <StackPanel Width="115" Spacing="4" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="8">
<ComboBox x:Name="Decorations" SelectedIndex="0" Margin="0,0,0,8"> <ComboBox x:Name="Decorations" SelectedIndex="0">
<ComboBoxItem>No Decorations</ComboBoxItem> <ComboBoxItem>No Decorations</ComboBoxItem>
<ComboBoxItem>Border Only</ComboBoxItem> <ComboBoxItem>Border Only</ComboBoxItem>
<ComboBoxItem>Full Decorations</ComboBoxItem> <ComboBoxItem>Full Decorations</ComboBoxItem>
@ -69,6 +69,7 @@
<ComboBoxItem>Light</ComboBoxItem> <ComboBoxItem>Light</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem> <ComboBoxItem>Dark</ComboBoxItem>
</ComboBox> </ComboBox>
<ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" />
</StackPanel> </StackPanel>
</TabControl.Tag> </TabControl.Tag>
</TabControl> </TabControl>

2
samples/ControlCatalog/MainWindow.xaml

@ -7,7 +7,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views" xmlns:v="clr-namespace:ControlCatalog.Views"
x:Class="ControlCatalog.MainWindow"> x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>

27
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@ -1,4 +1,5 @@
using System.Reactive; using System.Reactive;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Dialogs; using Avalonia.Dialogs;
@ -11,6 +12,8 @@ namespace ControlCatalog.ViewModels
private IManagedNotificationManager _notificationManager; private IManagedNotificationManager _notificationManager;
private bool _isMenuItemChecked = true; private bool _isMenuItemChecked = true;
private WindowState _windowState;
private WindowState[] _windowStates;
public MainWindowViewModel(IManagedNotificationManager notificationManager) public MainWindowViewModel(IManagedNotificationManager notificationManager)
{ {
@ -45,10 +48,32 @@ namespace ControlCatalog.ViewModels
(App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
}); });
ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() => ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
{ {
IsMenuItemChecked = !IsMenuItemChecked; 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 public IManagedNotificationManager NotificationManager

2
src/Avalonia.Animation/Animation.cs

@ -254,7 +254,7 @@ namespace Avalonia.Animation
cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds); cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds);
} }
var newKF = new AnimatorKeyFrame(handler, cue); var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline);
subscriptions.Add(newKF.BindSetter(setter, control)); subscriptions.Add(newKF.BindSetter(setter, control));

9
src/Avalonia.Animation/AnimatorKeyFrame.cs

@ -24,11 +24,20 @@ namespace Avalonia.Animation
{ {
AnimatorType = animatorType; AnimatorType = animatorType;
Cue = cue; Cue = cue;
KeySpline = null;
}
public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline)
{
AnimatorType = animatorType;
Cue = cue;
KeySpline = keySpline;
} }
internal bool isNeutral; internal bool isNeutral;
public Type AnimatorType { get; } public Type AnimatorType { get; }
public Cue Cue { get; } public Cue Cue { get; }
public KeySpline KeySpline { get; }
public AvaloniaProperty Property { get; private set; } public AvaloniaProperty Property { get; private set; }
private object _value; private object _value;

3
src/Avalonia.Animation/Animators/Animator`1.cs

@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators
else else
newValue = (T)lastKeyframe.Value; newValue = (T)lastKeyframe.Value;
if (lastKeyframe.KeySpline != null)
progress = lastKeyframe.KeySpline.GetSplineProgress(progress);
return Interpolate(progress, oldValue, newValue); return Interpolate(progress, oldValue, newValue);
} }

20
src/Avalonia.Animation/KeyFrame.cs

@ -19,6 +19,7 @@ namespace Avalonia.Animation
{ {
private TimeSpan _ktimeSpan; private TimeSpan _ktimeSpan;
private Cue _kCue; private Cue _kCue;
private KeySpline _kKeySpline;
public KeyFrame() public KeyFrame()
{ {
@ -74,6 +75,25 @@ namespace Avalonia.Animation
} }
} }
/// <summary>
/// Gets or sets the KeySpline of this <see cref="KeyFrame"/>.
/// </summary>
/// <value>The key spline.</value>
public KeySpline KeySpline
{
get
{
return _kKeySpline;
}
set
{
_kKeySpline = value;
if (value != null && !value.IsValid())
{
throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0.");
}
}
}
} }

349
src/Avalonia.Animation/KeySpline.cs

@ -0,0 +1,349 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Text;
using Avalonia;
using Avalonia.Utilities;
// Ported from WPF open-source code.
// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
namespace Avalonia.Animation
{
/// <summary>
/// Determines how an animation is used based on a cubic bezier curve.
/// X1 and X2 must be between 0.0 and 1.0, inclusive.
/// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline
/// </summary>
[TypeConverter(typeof(KeySplineTypeConverter))]
public class KeySpline : AvaloniaObject
{
// Control points
private double _controlPointX1;
private double _controlPointY1;
private double _controlPointX2;
private double _controlPointY2;
private bool _isSpecified;
private bool _isDirty;
// The parameter that corresponds to the most recent time
private double _parameter;
// Cached coefficients
private double _Bx; // 3*points[0].X
private double _Cx; // 3*points[1].X
private double _Cx_Bx; // 2*(Cx - Bx)
private double _three_Cx; // 3 - Cx
private double _By; // 3*points[0].Y
private double _Cy; // 3*points[1].Y
// constants
private const double _accuracy = .001; // 1/3 the desired accuracy in X
private const double _fuzz = .000001; // computational zero
/// <summary>
/// Create a <see cref="KeySpline"/> with X1 = Y1 = 0 and X2 = Y2 = 1.
/// </summary>
public KeySpline()
{
_controlPointX1 = 0.0;
_controlPointY1 = 0.0;
_controlPointX2 = 1.0;
_controlPointY2 = 1.0;
_isDirty = true;
}
/// <summary>
/// Create a <see cref="KeySpline"/> with the given parameters
/// </summary>
/// <param name="x1">X coordinate for the first control point</param>
/// <param name="y1">Y coordinate for the first control point</param>
/// <param name="x2">X coordinate for the second control point</param>
/// <param name="y2">Y coordinate for the second control point</param>
public KeySpline(double x1, double y1, double x2, double y2)
{
_controlPointX1 = x1;
_controlPointY1 = y1;
_controlPointX2 = x2;
_controlPointY2 = y2;
_isDirty = true;
}
/// <summary>
/// Parse a <see cref="KeySpline"/> from a string. The string
/// needs to contain 4 values in it for the 2 control points.
/// </summary>
/// <param name="value">string with 4 values in it</param>
/// <param name="culture">culture of the string</param>
/// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
/// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
public static KeySpline Parse(string value, CultureInfo culture)
{
using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
{
return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
}
}
/// <summary>
/// X coordinate of the first control point
/// </summary>
public double ControlPointX1
{
get => _controlPointX1;
set
{
if (IsValidXValue(value))
{
_controlPointX1 = value;
}
else
{
throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0.");
}
}
}
/// <summary>
/// Y coordinate of the first control point
/// </summary>
public double ControlPointY1
{
get => _controlPointY1;
set => _controlPointY1 = value;
}
/// <summary>
/// X coordinate of the second control point
/// </summary>
public double ControlPointX2
{
get => _controlPointX2;
set
{
if (IsValidXValue(value))
{
_controlPointX2 = value;
}
else
{
throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0.");
}
}
}
/// <summary>
/// Y coordinate of the second control point
/// </summary>
public double ControlPointY2
{
get => _controlPointY2;
set => _controlPointY2 = value;
}
/// <summary>
/// Calculates spline progress from a linear progress.
/// </summary>
/// <param name="linearProgress">the linear progress</param>
/// <returns>the spline progress</returns>
public double GetSplineProgress(double linearProgress)
{
if (_isDirty)
{
Build();
}
if (!_isSpecified)
{
return linearProgress;
}
else
{
SetParameterFromX(linearProgress);
return GetBezierValue(_By, _Cy, _parameter);
}
}
/// <summary>
/// Check to see whether the <see cref="KeySpline"/> is valid by looking
/// at its X values.
/// </summary>
/// <returns>true if the X values for this <see cref="KeySpline"/> fall in
/// acceptable range; false otherwise.</returns>
public bool IsValid()
{
return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2);
}
/// <summary>
///
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
private bool IsValidXValue(double value)
{
return value >= 0.0 && value <= 1.0;
}
/// <summary>
/// Compute cached coefficients.
/// </summary>
private void Build()
{
if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1)
{
// This KeySpline would have no effect on the progress.
_isSpecified = false;
}
else
{
_isSpecified = true;
_parameter = 0;
// X coefficients
_Bx = 3 * _controlPointX1;
_Cx = 3 * _controlPointX2;
_Cx_Bx = 2 * (_Cx - _Bx);
_three_Cx = 3 - _Cx;
// Y coefficients
_By = 3 * _controlPointY1;
_Cy = 3 * _controlPointY2;
}
_isDirty = false;
}
/// <summary>
/// Get an X or Y value with the Bezier formula.
/// </summary>
/// <param name="b">the second Bezier coefficient</param>
/// <param name="c">the third Bezier coefficient</param>
/// <param name="t">the parameter value to evaluate at</param>
/// <returns>the value of the Bezier function at the given parameter</returns>
static private double GetBezierValue(double b, double c, double t)
{
double s = 1.0 - t;
double t2 = t * t;
return b * t * s * s + c * t2 * s + t2 * t;
}
/// <summary>
/// Get X and dX/dt at a given parameter
/// </summary>
/// <param name="t">the parameter value to evaluate at</param>
/// <param name="x">the value of x there</param>
/// <param name="dx">the value of dx/dt there</param>
private void GetXAndDx(double t, out double x, out double dx)
{
double s = 1.0 - t;
double t2 = t * t;
double s2 = s * s;
x = _Bx * t * s2 + _Cx * t2 * s + t2 * t;
dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2;
}
/// <summary>
/// Compute the parameter value that corresponds to a given X value, using a modified
/// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make
/// use of some known properties of this particular function:
/// * We are only interested in solutions in the interval [0,1]
/// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use
/// that to clamp down the search interval with every probe.
/// * The derivative of X and Y are between 0 and 3.
/// </summary>
/// <param name="time">the time, scaled to fit in [0,1]</param>
private void SetParameterFromX(double time)
{
// Dynamic search interval to clamp with
double bottom = 0;
double top = 1;
if (time == 0)
{
_parameter = 0;
}
else if (time == 1)
{
_parameter = 1;
}
else
{
// Loop while improving the guess
while (top - bottom > _fuzz)
{
double x, dx, absdx;
// Get x and dx/dt at the current parameter
GetXAndDx(_parameter, out x, out dx);
absdx = Math.Abs(dx);
// Clamp down the search interval, relying on the monotonicity of X(t)
if (x > time)
{
top = _parameter; // because parameter > solution
}
else
{
bottom = _parameter; // because parameter < solution
}
// The desired accuracy is in ultimately in y, not in x, so the
// accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt).
// But dy/dt <=3, so we omit that
if (Math.Abs(x - time) < _accuracy * absdx)
{
break; // We're there
}
if (absdx > _fuzz)
{
// Nonzero derivative, use Newton-Raphson to obtain the next guess
double next = _parameter - (x - time) / dx;
// If next guess is out of the search interval then clamp it in
if (next >= top)
{
_parameter = (_parameter + top) / 2;
}
else if (next <= bottom)
{
_parameter = (_parameter + bottom) / 2;
}
else
{
// Next guess is inside the search interval, accept it
_parameter = next;
}
}
else // Zero derivative, halve the search interval
{
_parameter = (bottom + top) / 2;
}
}
}
}
}
/// <summary>
/// Converts string values to <see cref="KeySpline"/> values
/// </summary>
public class KeySplineTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return KeySpline.Parse((string)value, culture);
}
}
}

22
src/Avalonia.Controls/ComboBox.cs

@ -232,6 +232,23 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed; _popup.Closed += PopupClosed;
} }
/// <summary>
/// Called when the ComboBox popup is closed, with the <see cref="PopupClosedEventArgs"/>
/// that caused the popup to close.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method can be overridden to control whether the event that caused the popup to close
/// is swallowed or passed through.
/// </remarks>
protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
}
internal void ItemFocused(ComboBoxItem dropDownItem) internal void ItemFocused(ComboBoxItem dropDownItem)
{ {
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
@ -245,10 +262,7 @@ namespace Avalonia.Controls
_subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null; _subscriptionsOnOpen = null;
if (e.CloseEvent is PointerEventArgs pointerEvent) PopupClosedOverride(e);
{
pointerEvent.Handled = true;
}
if (CanFocus(this)) if (CanFocus(this))
{ {

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -392,7 +392,7 @@ namespace Avalonia.Controls.Platform
{ {
var control = e.Source as ILogical; var control = e.Source as ILogical;
if (!Menu.IsLogicalParentOf(control)) if (!Menu.IsLogicalAncestorOf(control))
{ {
Menu.Close(); Menu.Close();
} }

46
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -74,16 +74,15 @@ namespace Avalonia.Controls.Presenters
static TextPresenter() static TextPresenter()
{ {
AffectsRender<TextPresenter>(PasswordCharProperty, AffectsRender<TextPresenter>(SelectionBrushProperty);
SelectionBrushProperty, SelectionForegroundBrushProperty,
SelectionStartProperty, SelectionEndProperty);
Observable.Merge( Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
TextProperty.Changed, TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
SelectionStartProperty.Changed, TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed,
SelectionEndProperty.Changed, TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
PasswordCharProperty.Changed SelectionStartProperty.Changed, SelectionEndProperty.Changed,
).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText()); SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed
).AddClassHandler<TextPresenter>((x, _) => x.InvalidateFormattedText());
CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue)); CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
} }
@ -184,7 +183,7 @@ namespace Avalonia.Controls.Presenters
{ {
get get
{ {
return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text)); return _formattedText ?? (_formattedText = CreateFormattedText());
} }
} }
@ -219,7 +218,7 @@ namespace Avalonia.Controls.Presenters
get => GetValue(SelectionForegroundBrushProperty); get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value); set => SetValue(SelectionForegroundBrushProperty, value);
} }
public IBrush CaretBrush public IBrush CaretBrush
{ {
get => GetValue(CaretBrushProperty); get => GetValue(CaretBrushProperty);
@ -284,13 +283,9 @@ namespace Avalonia.Controls.Presenters
/// </summary> /// </summary>
protected void InvalidateFormattedText() protected void InvalidateFormattedText()
{ {
if (_formattedText != null) _formattedText = null;
{
_constraint = _formattedText.Constraint;
_formattedText = null;
}
InvalidateVisual(); InvalidateMeasure();
} }
/// <summary> /// <summary>
@ -307,6 +302,7 @@ namespace Avalonia.Controls.Presenters
} }
FormattedText.Constraint = Bounds.Size; FormattedText.Constraint = Bounds.Size;
context.DrawText(Foreground, new Point(), FormattedText); context.DrawText(Foreground, new Point(), FormattedText);
} }
@ -424,20 +420,20 @@ namespace Avalonia.Controls.Presenters
/// <summary> /// <summary>
/// Creates the <see cref="FormattedText"/> used to render the text. /// Creates the <see cref="FormattedText"/> used to render the text.
/// </summary> /// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to generated the <see cref="FormattedText"/> for.</param>
/// <returns>A <see cref="FormattedText"/> object.</returns> /// <returns>A <see cref="FormattedText"/> object.</returns>
protected virtual FormattedText CreateFormattedText(Size constraint, string text) protected virtual FormattedText CreateFormattedText()
{ {
FormattedText result = null; FormattedText result = null;
var text = Text;
if (PasswordChar != default(char)) if (PasswordChar != default(char))
{ {
result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0)); result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
} }
else else
{ {
result = CreateFormattedTextInternal(constraint, text); result = CreateFormattedTextInternal(_constraint, text);
} }
var selectionStart = SelectionStart; var selectionStart = SelectionStart;
@ -467,13 +463,15 @@ namespace Avalonia.Controls.Presenters
{ {
if (TextWrapping == TextWrapping.Wrap) if (TextWrapping == TextWrapping.Wrap)
{ {
FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity); _constraint = new Size(availableSize.Width, double.PositiveInfinity);
} }
else else
{ {
FormattedText.Constraint = Size.Infinity; _constraint = Size.Infinity;
} }
_formattedText = null;
return FormattedText.Bounds.Size; return FormattedText.Bounds.Size;
} }

13
src/Avalonia.Controls/Window.cs

@ -500,6 +500,19 @@ namespace Avalonia.Controls
EnsureInitialized(); EnsureInitialized();
IsVisible = true; IsVisible = true;
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
if (initialSize != ClientSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(initialSize);
}
}
LayoutManager.ExecuteInitialLayoutPass(this); LayoutManager.ExecuteInitialLayoutPass(this);
var result = new TaskCompletionSource<TResult>(); var result = new TaskCompletionSource<TResult>();

5
src/Avalonia.Controls/WindowState.cs

@ -19,5 +19,10 @@ namespace Avalonia.Controls
/// The window is maximized. /// The window is maximized.
/// </summary> /// </summary>
Maximized, Maximized,
/// <summary>
/// The window is fullscreen.
/// </summary>
FullScreen,
} }
} }

2
src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs

@ -34,7 +34,7 @@ namespace Avalonia.Dialogs
return; return;
} }
var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control); var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
if (e.ClickCount == 2 || isQuickLink) if (e.ClickCount == 2 || isQuickLink)
{ {
if (model.ItemType == ManagedFileChooserItemType.File) if (model.ItemType == ManagedFileChooserItemType.File)

2
src/Avalonia.Native/WindowImpl.cs

@ -67,7 +67,7 @@ namespace Avalonia.Native
public void SetSystemDecorations(Controls.SystemDecorations enabled) public void SetSystemDecorations(Controls.SystemDecorations enabled)
{ {
_native.HasDecorations = (Interop.SystemDecorations)enabled; _native.Decorations = (Interop.SystemDecorations)enabled;
} }
public void SetTitleBarColor (Avalonia.Media.Color color) public void SetTitleBarColor (Avalonia.Media.Color color)

148
src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs

@ -1,11 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Avalonia.LogicalTree namespace Avalonia.LogicalTree
{ {
/// <summary>
/// Provides extension methods for working with the logical tree.
/// </summary>
public static class LogicalExtensions public static class LogicalExtensions
{ {
/// <summary>
/// Enumerates the ancestors of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical's ancestors.</returns>
public static IEnumerable<ILogical> GetLogicalAncestors(this ILogical logical) public static IEnumerable<ILogical> GetLogicalAncestors(this ILogical logical)
{ {
Contract.Requires<ArgumentNullException>(logical != null); Contract.Requires<ArgumentNullException>(logical != null);
@ -19,6 +26,11 @@ namespace Avalonia.LogicalTree
} }
} }
/// <summary>
/// Enumerates an <see cref="ILogical"/> and its ancestors in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical and its ancestors.</returns>
public static IEnumerable<ILogical> GetSelfAndLogicalAncestors(this ILogical logical) public static IEnumerable<ILogical> GetSelfAndLogicalAncestors(this ILogical logical)
{ {
yield return logical; yield return logical;
@ -29,11 +41,50 @@ namespace Avalonia.LogicalTree
} }
} }
/// <summary>
/// Finds first ancestor of given type.
/// </summary>
/// <typeparam name="T">Ancestor type.</typeparam>
/// <param name="logical">The logical.</param>
/// <param name="includeSelf">If given logical should be included in search.</param>
/// <returns>First ancestor of given type.</returns>
public static T FindLogicalAncestorOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
{
if (logical is null)
{
return null;
}
ILogical parent = includeSelf ? logical : logical.LogicalParent;
while (parent != null)
{
if (parent is T result)
{
return result;
}
parent = parent.LogicalParent;
}
return null;
}
/// <summary>
/// Enumerates the children of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical children.</returns>
public static IEnumerable<ILogical> GetLogicalChildren(this ILogical logical) public static IEnumerable<ILogical> GetLogicalChildren(this ILogical logical)
{ {
return logical.LogicalChildren; return logical.LogicalChildren;
} }
/// <summary>
/// Enumerates the descendants of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical's ancestors.</returns>
public static IEnumerable<ILogical> GetLogicalDescendants(this ILogical logical) public static IEnumerable<ILogical> GetLogicalDescendants(this ILogical logical)
{ {
foreach (ILogical child in logical.LogicalChildren) foreach (ILogical child in logical.LogicalChildren)
@ -47,6 +98,11 @@ namespace Avalonia.LogicalTree
} }
} }
/// <summary>
/// Enumerates an <see cref="ILogical"/> and its descendants in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical and its ancestors.</returns>
public static IEnumerable<ILogical> GetSelfAndLogicalDescendants(this ILogical logical) public static IEnumerable<ILogical> GetSelfAndLogicalDescendants(this ILogical logical)
{ {
yield return logical; yield return logical;
@ -57,16 +113,56 @@ namespace Avalonia.LogicalTree
} }
} }
/// <summary>
/// Finds first descendant of given type.
/// </summary>
/// <typeparam name="T">Descendant type.</typeparam>
/// <param name="logical">The logical.</param>
/// <param name="includeSelf">If given logical should be included in search.</param>
/// <returns>First descendant of given type.</returns>
public static T FindLogicalDescendantOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
{
if (logical is null)
{
return null;
}
if (includeSelf && logical is T result)
{
return result;
}
return FindDescendantOfTypeCore<T>(logical);
}
/// <summary>
/// Gets the logical parent of an <see cref="ILogical"/>.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The parent, or null if the logical is unparented.</returns>
public static ILogical GetLogicalParent(this ILogical logical) public static ILogical GetLogicalParent(this ILogical logical)
{ {
return logical.LogicalParent; return logical.LogicalParent;
} }
/// <summary>
/// Gets the logical parent of an <see cref="ILogical"/>.
/// </summary>
/// <typeparam name="T">The type of the logical parent.</typeparam>
/// <param name="logical">The logical.</param>
/// <returns>
/// The parent, or null if the logical is unparented or its parent is not of type <typeparamref name="T"/>.
/// </returns>
public static T GetLogicalParent<T>(this ILogical logical) where T : class public static T GetLogicalParent<T>(this ILogical logical) where T : class
{ {
return logical.LogicalParent as T; return logical.LogicalParent as T;
} }
/// <summary>
/// Enumerates the siblings of an <see cref="ILogical"/> in the logical tree.
/// </summary>
/// <param name="logical">The logical.</param>
/// <returns>The logical siblings.</returns>
public static IEnumerable<ILogical> GetLogicalSiblings(this ILogical logical) public static IEnumerable<ILogical> GetLogicalSiblings(this ILogical logical)
{ {
ILogical parent = logical.LogicalParent; ILogical parent = logical.LogicalParent;
@ -80,9 +176,55 @@ namespace Avalonia.LogicalTree
} }
} }
public static bool IsLogicalParentOf(this ILogical logical, ILogical target) /// <summary>
/// Tests whether an <see cref="ILogical"/> is an ancestor of another logical.
/// </summary>
/// <param name="logical">The logical.</param>
/// <param name="target">The potential descendant.</param>
/// <returns>
/// True if <paramref name="logical"/> is an ancestor of <paramref name="target"/>;
/// otherwise false.
/// </returns>
public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target)
{ {
return target.GetLogicalAncestors().Any(x => x == logical); ILogical current = target?.LogicalParent;
while (current != null)
{
if (current == logical)
{
return true;
}
current = current.LogicalParent;
}
return false;
}
private static T FindDescendantOfTypeCore<T>(ILogical logical) where T : class
{
var logicalChildren = logical.LogicalChildren;
var logicalChildrenCount = logicalChildren.Count;
for (var i = 0; i < logicalChildrenCount; i++)
{
ILogical child = logicalChildren[i];
if (child is T result)
{
return result;
}
var childResult = FindDescendantOfTypeCore<T>(child);
if (!(childResult is null))
{
return childResult;
}
}
return null;
} }
} }
} }

26
src/Avalonia.Visuals/Media/FontManager.cs

@ -23,6 +23,11 @@ namespace Avalonia.Media
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
if (string.IsNullOrEmpty(DefaultFontFamilyName))
{
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName); _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
} }
@ -39,7 +44,8 @@ namespace Avalonia.Media
var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>(); var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered."); if (fontManagerImpl == null)
throw new InvalidOperationException("No font manager implementation was registered.");
current = new FontManager(fontManagerImpl); current = new FontManager(fontManagerImpl);
@ -87,7 +93,7 @@ namespace Avalonia.Media
fontFamily = _defaultFontFamily; fontFamily = _defaultFontFamily;
} }
var key = new FontKey(fontFamily, fontWeight, fontStyle); var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface)) if (_typefaceCache.TryGetValue(key, out var typeface))
{ {
@ -126,9 +132,21 @@ namespace Avalonia.Media
FontStyle fontStyle = FontStyle.Normal, FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null) FontFamily fontFamily = null, CultureInfo culture = null)
{ {
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? foreach (var cachedTypeface in _typefaceCache.Values)
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : {
// First try to find a cached typeface by style and weight to avoid redundant glyph index lookup.
if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight
&& cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0)
{
return cachedTypeface;
}
}
var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
_typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) :
null; null;
return matchedTypeface;
} }
} }
} }

3
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@ -1,7 +1,6 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using Avalonia.Utilities; using Avalonia.Utilities;
@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
throw new ArgumentNullException(nameof(familyNames)); throw new ArgumentNullException(nameof(familyNames));
} }
Names = familyNames.Split(',').Select(x => x.Trim()).ToArray(); Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
PrimaryFamilyName = Names[0]; PrimaryFamilyName = Names[0];

16
src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts
{ {
public readonly struct FontKey : IEquatable<FontKey> public readonly struct FontKey : IEquatable<FontKey>
{ {
public readonly FontFamily FontFamily; public FontKey(string familyName, FontWeight weight, FontStyle style)
public readonly FontStyle Style;
public readonly FontWeight Weight;
public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
{ {
FontFamily = fontFamily; FamilyName = familyName;
Style = style; Style = style;
Weight = weight; Weight = weight;
} }
public string FamilyName { get; }
public FontStyle Style { get; }
public FontWeight Weight { get; }
public override int GetHashCode() public override int GetHashCode()
{ {
var hash = FontFamily.GetHashCode(); var hash = FamilyName.GetHashCode();
hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Style;
hash = hash * 31 + (int)Weight; hash = hash * 31 + (int)Weight;
@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
public bool Equals(FontKey other) public bool Equals(FontKey other)
{ {
return FontFamily == other.FontFamily && return FamilyName == other.FamilyName &&
Style == other.Style && Style == other.Style &&
Weight == other.Weight; Weight == other.Weight;
} }

6
src/Avalonia.Visuals/Media/FormattedText.cs

@ -200,7 +200,13 @@ namespace Avalonia.Media
private void Set<T>(ref T field, T value) private void Set<T>(ref T field, T value)
{ {
if (field != null && field.Equals(value))
{
return;
}
field = value; field = value;
_platformImpl = null; _platformImpl = null;
} }
} }

2
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
//ToDo: Fix FontFamily fallback //ToDo: Fix FontFamily fallback
currentTypeface = currentTypeface =
FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style); FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily);
if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
{ {

2
src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@ -2,7 +2,7 @@
namespace Avalonia.Media.TextFormatting.Unicode namespace Avalonia.Media.TextFormatting.Unicode
{ {
internal ref struct CodepointEnumerator public ref struct CodepointEnumerator
{ {
private ReadOnlySlice<char> _text; private ReadOnlySlice<char> _text;

44
src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs

@ -1,44 +0,0 @@
namespace Avalonia.Media.TextFormatting.Unicode
{
public enum UnicodeGeneralCategory : byte
{
Other, //C# Cc | Cf | Cn | Co | Cs
Control, //Cc
Format, //Cf
Unassigned, //Cn
PrivateUse, //Co
Surrogate, //Cs
Letter, //L# Ll | Lm | Lo | Lt | Lu
CasedLetter, //LC# Ll | Lt | Lu
LowercaseLetter, //Ll
ModifierLetter, //Lm
OtherLetter, //Lo
TitlecaseLetter, //Lt
UppercaseLetter, //Lu
Mark, //M
SpacingMark, //Mc
EnclosingMark, //Me
NonspacingMark, //Mn
Number, //N# Nd | Nl | No
DecimalNumber, //Nd
LetterNumber, //Nl
OtherNumber, //No
Punctuation, //P
ConnectorPunctuation, //Pc
DashPunctuation, //Pd
ClosePunctuation, //Pe
FinalPunctuation, //Pf
InitialPunctuation, //Pi
OtherPunctuation, //Po
OpenPunctuation, //Ps
Symbol, //S# Sc | Sk | Sm | So
CurrencySymbol, //Sc
ModifierSymbol, //Sk
MathSymbol, //Sm
OtherSymbol, //So
Separator, //Z# Zl | Zp | Zs
LineSeparator, //Zl
ParagraphSeparator, //Zp
SpaceSeparator, //Zs
}
}

26
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -35,6 +35,7 @@ namespace Avalonia.Rendering
private IRef<IDrawOperation> _currentDraw; private IRef<IDrawOperation> _currentDraw;
private readonly IDeferredRendererLock _lock; private readonly IDeferredRendererLock _lock;
private readonly object _sceneLock = new object(); private readonly object _sceneLock = new object();
private readonly Action _updateSceneIfNeededDelegate;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DeferredRenderer"/> class. /// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
@ -49,7 +50,7 @@ namespace Avalonia.Rendering
IRenderLoop renderLoop, IRenderLoop renderLoop,
ISceneBuilder sceneBuilder = null, ISceneBuilder sceneBuilder = null,
IDispatcher dispatcher = null, IDispatcher dispatcher = null,
IDeferredRendererLock rendererLock = null) IDeferredRendererLock rendererLock = null) : base(true)
{ {
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
@ -59,6 +60,7 @@ namespace Avalonia.Rendering
Layers = new RenderLayers(); Layers = new RenderLayers();
_renderLoop = renderLoop; _renderLoop = renderLoop;
_lock = rendererLock ?? new ManagedDeferredRendererLock(); _lock = rendererLock ?? new ManagedDeferredRendererLock();
_updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
} }
/// <summary> /// <summary>
@ -73,7 +75,7 @@ namespace Avalonia.Rendering
public DeferredRenderer( public DeferredRenderer(
IVisual root, IVisual root,
IRenderTarget renderTarget, IRenderTarget renderTarget,
ISceneBuilder sceneBuilder = null) ISceneBuilder sceneBuilder = null) : base(true)
{ {
Contract.Requires<ArgumentNullException>(root != null); Contract.Requires<ArgumentNullException>(root != null);
Contract.Requires<ArgumentNullException>(renderTarget != null); Contract.Requires<ArgumentNullException>(renderTarget != null);
@ -83,6 +85,7 @@ namespace Avalonia.Rendering
_sceneBuilder = sceneBuilder ?? new SceneBuilder(); _sceneBuilder = sceneBuilder ?? new SceneBuilder();
Layers = new RenderLayers(); Layers = new RenderLayers();
_lock = new ManagedDeferredRendererLock(); _lock = new ManagedDeferredRendererLock();
_updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -261,7 +264,8 @@ namespace Avalonia.Rendering
try try
{ {
var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context); var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context);
if (updated)
FpsTick();
using (scene) using (scene)
{ {
if (scene?.Item != null) if (scene?.Item != null)
@ -318,17 +322,25 @@ namespace Avalonia.Rendering
_lastSceneId = scene.Generation; _lastSceneId = scene.Generation;
var isUiThread = Dispatcher.UIThread.CheckAccess();
// We have consumed the previously available scene, but there might be some dirty // We have consumed the previously available scene, but there might be some dirty
// rects since the last update. *If* we are on UI thread, we can force immediate scene // rects since the last update. *If* we are on UI thread, we can force immediate scene
// rebuild before rendering anything on-screen // rebuild before rendering anything on-screen
// We are calling the same method recursively here // We are calling the same method recursively here
if (!recursiveCall && Dispatcher.UIThread.CheckAccess() && NeedsUpdate) if (!recursiveCall && isUiThread && NeedsUpdate)
{ {
UpdateScene(); UpdateScene();
var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true); var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true);
return (rs, true); return (rs, true);
} }
// We are rendering a new scene version, so it's highly likely
// that there is already a pending update for animations
// So we are scheduling an update call so UI thread could prepare a scene before
// the next render timer tick
if (!recursiveCall && !isUiThread)
Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render);
// Indicate that we have updated the layers // Indicate that we have updated the layers
return (sceneRef.Clone(), true); return (sceneRef.Clone(), true);
} }
@ -534,6 +546,12 @@ namespace Avalonia.Rendering
context = RenderTarget.CreateDrawingContext(this); context = RenderTarget.CreateDrawingContext(this);
} }
private void UpdateSceneIfNeeded()
{
if(NeedsUpdate)
UpdateScene();
}
private void UpdateScene() private void UpdateScene()
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();

9
src/Avalonia.Visuals/Rendering/RendererBase.cs

@ -7,6 +7,7 @@ namespace Avalonia.Rendering
{ {
public class RendererBase public class RendererBase
{ {
private readonly bool _useManualFpsCounting;
private static int s_fontSize = 18; private static int s_fontSize = 18;
private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private int _framesThisSecond; private int _framesThisSecond;
@ -14,8 +15,9 @@ namespace Avalonia.Rendering
private FormattedText _fpsText; private FormattedText _fpsText;
private TimeSpan _lastFpsUpdate; private TimeSpan _lastFpsUpdate;
public RendererBase() public RendererBase(bool useManualFpsCounting = false)
{ {
_useManualFpsCounting = useManualFpsCounting;
_fpsText = new FormattedText _fpsText = new FormattedText
{ {
Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default), Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default),
@ -23,12 +25,15 @@ namespace Avalonia.Rendering
}; };
} }
protected void FpsTick() => _framesThisSecond++;
protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount) protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount)
{ {
var now = _stopwatch.Elapsed; var now = _stopwatch.Elapsed;
var elapsed = now - _lastFpsUpdate; var elapsed = now - _lastFpsUpdate;
++_framesThisSecond; if (!_useManualFpsCounting)
++_framesThisSecond;
if (elapsed.TotalSeconds > 1) if (elapsed.TotalSeconds > 1)
{ {

14
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -377,7 +377,19 @@ namespace Avalonia.VisualTree
/// </returns> /// </returns>
public static bool IsVisualAncestorOf(this IVisual visual, IVisual target) public static bool IsVisualAncestorOf(this IVisual visual, IVisual target)
{ {
return target.GetVisualAncestors().Any(x => x == visual); IVisual current = target?.VisualParent;
while (current != null)
{
if (current == visual)
{
return true;
}
current = current.VisualParent;
}
return false;
} }
public static IEnumerable<IVisual> SortByZIndex(this IEnumerable<IVisual> elements) public static IEnumerable<IVisual> SortByZIndex(this IEnumerable<IVisual> elements)

1
src/Avalonia.X11/X11Atoms.cs

@ -156,6 +156,7 @@ namespace Avalonia.X11
public readonly IntPtr _NET_SYSTEM_TRAY_OPCODE; public readonly IntPtr _NET_SYSTEM_TRAY_OPCODE;
public readonly IntPtr _NET_WM_STATE_MAXIMIZED_HORZ; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_HORZ;
public readonly IntPtr _NET_WM_STATE_MAXIMIZED_VERT; public readonly IntPtr _NET_WM_STATE_MAXIMIZED_VERT;
public readonly IntPtr _NET_WM_STATE_FULLSCREEN;
public readonly IntPtr _XEMBED; public readonly IntPtr _XEMBED;
public readonly IntPtr _XEMBED_INFO; public readonly IntPtr _XEMBED_INFO;
public readonly IntPtr _MOTIF_WM_HINTS; public readonly IntPtr _MOTIF_WM_HINTS;

32
src/Avalonia.X11/X11Window.cs

@ -220,16 +220,11 @@ namespace Avalonia.X11
var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border |
MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH; MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH;
if (_popup || _systemDecorations == SystemDecorations.None) if (_popup
{ || _systemDecorations == SystemDecorations.None)
decorations = 0; decorations = 0;
}
else if (_systemDecorations == SystemDecorations.BorderOnly)
{
decorations = MotifDecorations.Border;
}
if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) if (!_canResize)
{ {
functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize); functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH); decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
@ -252,7 +247,7 @@ namespace Avalonia.X11
var min = _minMaxSize.minSize; var min = _minMaxSize.minSize;
var max = _minMaxSize.maxSize; var max = _minMaxSize.maxSize;
if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly) if (!_canResize)
max = min = _realSize; max = min = _realSize;
if (preResize.HasValue) if (preResize.HasValue)
@ -552,12 +547,21 @@ namespace Avalonia.X11
else if (value == WindowState.Maximized) else if (value == WindowState.Maximized)
{ {
ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); 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, ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
_x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); _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 else
{ {
ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN); 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, ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
_x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ); _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
} }
@ -585,6 +589,12 @@ namespace Avalonia.X11
break; break;
} }
if(pitems[c] == _x11.Atoms._NET_WM_STATE_FULLSCREEN)
{
state = WindowState.FullScreen;
break;
}
if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ || if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ ||
pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT) pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
{ {
@ -810,7 +820,7 @@ namespace Avalonia.X11
public void SetSystemDecorations(SystemDecorations enabled) public void SetSystemDecorations(SystemDecorations enabled)
{ {
_systemDecorations = enabled; _systemDecorations = enabled == SystemDecorations.Full ? SystemDecorations.Full : SystemDecorations.None;
UpdateMotifHints(); UpdateMotifHints();
UpdateSizeHints(null); UpdateSizeHints(null);
} }
@ -1052,7 +1062,7 @@ namespace Avalonia.X11
void ChangeWMAtoms(bool enable, params IntPtr[] atoms) void ChangeWMAtoms(bool enable, params IntPtr[] atoms)
{ {
if (atoms.Length < 1 || atoms.Length > 4) if (atoms.Length != 1 && atoms.Length != 2)
throw new ArgumentException(); throw new ArgumentException();
if (!_mapped) if (!_mapped)

67
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -30,6 +30,10 @@ namespace Avalonia.Skia
private Matrix _currentTransform; private Matrix _currentTransform;
private GRContext _grContext; private GRContext _grContext;
private bool _disposed; private bool _disposed;
private readonly SKPaint _strokePaint = new SKPaint();
private readonly SKPaint _fillPaint = new SKPaint();
/// <summary> /// <summary>
/// Context create info. /// Context create info.
/// </summary> /// </summary>
@ -153,7 +157,7 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawLine(IPen pen, Point p1, Point p2) public void DrawLine(IPen pen, Point p1, Point p2)
{ {
using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{ {
Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
} }
@ -165,8 +169,8 @@ namespace Avalonia.Skia
var impl = (GeometryImpl) geometry; var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size; var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper)) using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
{ {
if (fill.Paint != null) if (fill.Paint != null)
{ {
@ -188,7 +192,7 @@ namespace Avalonia.Skia
if (brush != null) if (brush != null)
{ {
using (var paint = CreatePaint(brush, rect.Size)) using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
{ {
if (isRounded) if (isRounded)
{ {
@ -204,7 +208,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null) if (pen?.Brush != null)
{ {
using (var paint = CreatePaint(pen, rect.Size)) using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
{ {
if (isRounded) if (isRounded)
{ {
@ -222,7 +226,7 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{ {
using (var paint = CreatePaint(foreground, text.Bounds.Size)) using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
{ {
var textImpl = (FormattedTextImpl) text; var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@ -232,14 +236,14 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
{ {
using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size)) using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
{ {
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
paint.ApplyTo(glyphRunImpl.Paint); ConfigureTextRendering(paintWrapper);
Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
(float)baselineOrigin.Y, glyphRunImpl.Paint); (float)baselineOrigin.Y, paintWrapper.Paint);
} }
} }
@ -323,7 +327,7 @@ namespace Avalonia.Skia
var paint = new SKPaint(); var paint = new SKPaint();
Canvas.SaveLayer(paint); Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(mask, bounds.Size)); _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
} }
/// <inheritdoc /> /// <inheritdoc />
@ -364,6 +368,15 @@ namespace Avalonia.Skia
} }
} }
internal void ConfigureTextRendering(PaintWrapper wrapper)
{
var paint = wrapper.Paint;
paint.IsEmbeddedBitmapText = true;
paint.SubpixelText = true;
paint.LcdRenderText = _canTextUseLcdRendering;
}
/// <summary> /// <summary>
/// Configure paint wrapper for using gradient brush. /// Configure paint wrapper for using gradient brush.
/// </summary> /// </summary>
@ -514,17 +527,16 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Creates paint wrapper for given brush. /// Creates paint wrapper for given brush.
/// </summary> /// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param> /// <param name="brush">Source brush.</param>
/// <param name="targetSize">Target size.</param> /// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns>Paint wrapper for given brush.</returns> /// <returns>Paint wrapper for given brush.</returns>
internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
{ {
var paint = new SKPaint var paintWrapper = new PaintWrapper(paint, disposePaint);
{
IsAntialias = true
};
var paintWrapper = new PaintWrapper(paint); paint.IsAntialias = true;
double opacity = brush.Opacity * _currentOpacity; double opacity = brush.Opacity * _currentOpacity;
@ -572,10 +584,12 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Creates paint wrapper for given pen. /// Creates paint wrapper for given pen.
/// </summary> /// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param> /// <param name="pen">Source pen.</param>
/// <param name="targetSize">Target size.</param> /// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns></returns> /// <returns></returns>
private PaintWrapper CreatePaint(IPen pen, Size targetSize) private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
{ {
// In Skia 0 thickness means - use hairline rendering // In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered. // and for us it means - there is nothing rendered.
@ -584,8 +598,7 @@ namespace Avalonia.Skia
return default; return default;
} }
var rv = CreatePaint(pen.Brush, targetSize); var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
var paint = rv.Paint;
paint.IsStroke = true; paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness; paint.StrokeWidth = (float) pen.Thickness;
@ -668,7 +681,7 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Skia cached paint state. /// Skia cached paint state.
/// </summary> /// </summary>
private struct PaintState : IDisposable private readonly struct PaintState : IDisposable
{ {
private readonly SKColor _color; private readonly SKColor _color;
private readonly SKShader _shader; private readonly SKShader _shader;
@ -696,14 +709,16 @@ namespace Avalonia.Skia
{ {
//We are saving memory allocations there //We are saving memory allocations there
public readonly SKPaint Paint; public readonly SKPaint Paint;
private readonly bool _disposePaint;
private IDisposable _disposable1; private IDisposable _disposable1;
private IDisposable _disposable2; private IDisposable _disposable2;
private IDisposable _disposable3; private IDisposable _disposable3;
public PaintWrapper(SKPaint paint) public PaintWrapper(SKPaint paint, bool disposePaint)
{ {
Paint = paint; Paint = paint;
_disposePaint = disposePaint;
_disposable1 = null; _disposable1 = null;
_disposable2 = null; _disposable2 = null;
@ -751,7 +766,15 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{ {
Paint?.Dispose(); if (_disposePaint)
{
Paint?.Dispose();
}
else
{
Paint?.Reset();
}
_disposable1?.Dispose(); _disposable1?.Dispose();
_disposable2?.Dispose(); _disposable2?.Dispose();
_disposable3?.Dispose(); _disposable3?.Dispose();

46
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@ -32,6 +32,27 @@ namespace Avalonia.Skia
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
{ {
SKFontStyle skFontStyle;
switch (fontWeight)
{
case FontWeight.Normal when fontStyle == FontStyle.Normal:
skFontStyle = SKFontStyle.Normal;
break;
case FontWeight.Normal when fontStyle == FontStyle.Italic:
skFontStyle = SKFontStyle.Italic;
break;
case FontWeight.Bold when fontStyle == FontStyle.Normal:
skFontStyle = SKFontStyle.Bold;
break;
case FontWeight.Bold when fontStyle == FontStyle.Italic:
skFontStyle = SKFontStyle.BoldItalic;
break;
default:
skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle);
break;
}
if (culture == null) if (culture == null)
{ {
culture = CultureInfo.CurrentUICulture; culture = CultureInfo.CurrentUICulture;
@ -45,31 +66,32 @@ namespace Avalonia.Skia
t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
if (fontFamily != null) if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
{ {
foreach (var familyName in fontFamily.FamilyNames) var familyNames = fontFamily.FamilyNames;
for (var i = 1; i < familyNames.Count; i++)
{ {
var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, var skTypeface =
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
if (skTypeface == null) if (skTypeface == null)
{ {
continue; continue;
} }
fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true; return true;
} }
} }
else else
{ {
var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
if (skTypeface != null) if (skTypeface != null)
{ {
fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
return true; return true;
} }
@ -82,7 +104,7 @@ namespace Avalonia.Skia
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{ {
var skTypeface = SKTypeface.Default; SKTypeface skTypeface = null;
if (typeface.FontFamily.Key == null) if (typeface.FontFamily.Key == null)
{ {
@ -109,6 +131,12 @@ namespace Avalonia.Skia
skTypeface = fontCollection.Get(typeface); skTypeface = fontCollection.Get(typeface);
} }
if (skTypeface == null)
{
throw new InvalidOperationException(
$"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
}
return new GlyphTypefaceImpl(skTypeface); return new GlyphTypefaceImpl(skTypeface);
} }
} }

15
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -149,7 +149,17 @@ namespace Avalonia.Skia
if (index >= Text.Length || index < 0) if (index >= Text.Length || index < 0)
{ {
var r = rects.LastOrDefault(); 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]; return rects[index];
} }
@ -266,7 +276,8 @@ namespace Avalonia.Skia
if (fb != null) if (fb != null)
{ {
//TODO: figure out how to get the brush size //TODO: figure out how to get the brush size
currentWrapper = context.CreatePaint(fb, new Size()); currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
new Size());
} }
else else
{ {

9
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@ -7,17 +7,11 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public class GlyphRunImpl : IGlyphRunImpl public class GlyphRunImpl : IGlyphRunImpl
{ {
public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) public GlyphRunImpl(SKTextBlob textBlob)
{ {
Paint = paint;
TextBlob = textBlob; TextBlob = textBlob;
} }
/// <summary>
/// Gets the paint to draw with.
/// </summary>
public SKPaint Paint { get; }
/// <summary> /// <summary>
/// Gets the text blob to draw. /// Gets the text blob to draw.
/// </summary> /// </summary>
@ -26,7 +20,6 @@ namespace Avalonia.Skia
void IDisposable.Dispose() void IDisposable.Dispose()
{ {
TextBlob.Dispose(); TextBlob.Dispose();
Paint.Dispose();
} }
} }
} }

126
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -18,7 +18,7 @@ namespace Avalonia.Skia
private GRContext GrContext { get; } private GRContext GrContext { get; }
public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu) public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu, long maxResourceBytes = 100663296)
{ {
if (customSkiaGpu != null) if (customSkiaGpu != null)
{ {
@ -26,6 +26,10 @@ namespace Avalonia.Skia
GrContext = _customSkiaGpu.GrContext; GrContext = _customSkiaGpu.GrContext;
GrContext.GetResourceCacheLimits(out var maxResources, out _);
GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
return; return;
} }
@ -39,6 +43,10 @@ namespace Avalonia.Skia
: GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc))) : GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc)))
{ {
GrContext = GRContext.Create(GRBackend.OpenGL, iface); GrContext = GRContext.Create(GRBackend.OpenGL, iface);
GrContext.GetResourceCacheLimits(out var maxResources, out _);
GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
} }
display.ClearContext(); display.ClearContext();
} }
@ -149,6 +157,16 @@ namespace Avalonia.Skia
return new WriteableBitmapImpl(size, dpi, format); return new WriteableBitmapImpl(size, dpi, format);
} }
private static readonly SKPaint s_paint = new SKPaint
{
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder();
/// <inheritdoc /> /// <inheritdoc />
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{ {
@ -158,92 +176,84 @@ namespace Avalonia.Skia
var typeface = glyphTypeface.Typeface; var typeface = glyphTypeface.Typeface;
var paint = new SKPaint s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
{ s_paint.Typeface = typeface;
TextSize = (float)glyphRun.FontRenderingEmSize,
Typeface = typeface,
TextEncoding = SKTextEncoding.GlyphId,
IsAntialias = true,
IsStroke = false,
SubpixelText = true
};
using (var textBlobBuilder = new SKTextBlobBuilder())
{
SKTextBlob textBlob;
width = 0; SKTextBlob textBlob;
var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); width = 0;
if (glyphRun.GlyphOffsets.IsEmpty) var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
{
if (glyphTypeface.IsFixedPitch)
{
textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
textBlob = textBlobBuilder.Build();
width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
else
{
var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++) if (glyphRun.GlyphOffsets.IsEmpty)
{ {
positions[i] = (float)width; if (glyphTypeface.IsFixedPitch)
{
if (glyphRun.GlyphAdvances.IsEmpty) s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
{
width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
width += glyphRun.GlyphAdvances[i];
}
}
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); textBlob = s_textBlobBuilder.Build();
textBlob = textBlobBuilder.Build(); width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
}
} }
else else
{ {
var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0; var positions = buffer.GetPositionSpan();
for (var i = 0; i < count; i++) for (var i = 0; i < count; i++)
{ {
var glyphOffset = glyphRun.GlyphOffsets[i]; positions[i] = (float)width;
glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (glyphRun.GlyphAdvances.IsEmpty) if (glyphRun.GlyphAdvances.IsEmpty)
{ {
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
} }
else else
{ {
currentX += glyphRun.GlyphAdvances[i]; width += glyphRun.GlyphAdvances[i];
} }
} }
buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX; textBlob = s_textBlobBuilder.Build();
}
}
else
{
var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count);
var glyphPositions = buffer.GetPositionSpan();
var currentX = 0.0;
for (var i = 0; i < count; i++)
{
var glyphOffset = glyphRun.GlyphOffsets[i];
textBlob = textBlobBuilder.Build(); glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
if (glyphRun.GlyphAdvances.IsEmpty)
{
currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
}
else
{
currentX += glyphRun.GlyphAdvances[i];
}
} }
return new GlyphRunImpl(paint, textBlob); buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
width = currentX;
textBlob = s_textBlobBuilder.Build();
} }
return new GlyphRunImpl(textBlob);
} }
} }
} }

4
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -19,7 +19,7 @@ namespace Avalonia.Skia
public SKTypeface Get(Typeface typeface) public SKTypeface Get(Typeface typeface)
{ {
var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style); var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style);
return GetNearestMatch(_typefaces, key); return GetNearestMatch(_typefaces, key);
} }
@ -49,7 +49,7 @@ namespace Avalonia.Skia
if (keys.Length == 0) if (keys.Length == 0)
{ {
return SKTypeface.Default; return null;
} }
key = keys[0]; key = keys[0];

2
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -54,7 +54,7 @@ namespace Avalonia.Skia
continue; continue;
} }
var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
typeFaceCollection.AddTypeface(key, typeface); typeFaceCollection.AddTypeface(key, typeface);
} }

9
src/Skia/Avalonia.Skia/SkiaOptions.cs

@ -8,9 +8,18 @@ namespace Avalonia
/// </summary> /// </summary>
public class SkiaOptions public class SkiaOptions
{ {
public SkiaOptions()
{
MaxGpuResourceSizeBytes = 100663296; // Value taken from skia.
}
/// <summary> /// <summary>
/// Custom gpu factory to use. Can be used to customize behavior of Skia renderer. /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer.
/// </summary> /// </summary>
public Func<ICustomSkiaGpu> CustomGpuFactory { get; set; } public Func<ICustomSkiaGpu> CustomGpuFactory { get; set; }
/// <summary>
/// The maximum number of bytes for video memory to store textures and resources.
/// </summary>
public long MaxGpuResourceSizeBytes { get; set; }
} }
} }

2
src/Skia/Avalonia.Skia/SkiaPlatform.cs

@ -18,7 +18,7 @@ namespace Avalonia.Skia
public static void Initialize(SkiaOptions options) public static void Initialize(SkiaOptions options)
{ {
var customGpu = options.CustomGpuFactory?.Invoke(); var customGpu = options.CustomGpuFactory?.Invoke();
var renderInterface = new PlatformRenderInterface(customGpu); var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes);
AvaloniaLocator.CurrentMutable AvaloniaLocator.CurrentMutable
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface) .Bind<IPlatformRenderInterface>().ToConstant(renderInterface)

2
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media
var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle);
return true; return true;
} }

55
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;
/// <summary>
/// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
/// </summary>
/// <param name="fullscreen">Fullscreen state.</param>
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<HrInit>((*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<MarkFullscreenWindow>((*ptr)->MarkFullscreenWindow);
}
s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen);
}
}
}
}

24
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop
WS_SIZEFRAME = 0x40000, WS_SIZEFRAME = 0x40000,
WS_SYSMENU = 0x80000, WS_SYSMENU = 0x80000,
WS_TABSTOP = 0x10000, WS_TABSTOP = 0x10000,
WS_THICKFRAME = 0x40000,
WS_VISIBLE = 0x10000000, WS_VISIBLE = 0x10000000,
WS_VSCROLL = 0x200000, WS_VSCROLL = 0x200000,
WS_EX_DLGMODALFRAME = 0x00000001, WS_EX_DLGMODALFRAME = 0x00000001,
@ -1146,7 +1147,10 @@ namespace Avalonia.Win32.Interop
internal static extern int CoCreateInstance(ref Guid clsid, internal static extern int CoCreateInstance(ref Guid clsid,
IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); 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)] [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); 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 SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B");
public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); 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 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)] [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
@ -1874,6 +1880,22 @@ namespace Avalonia.Win32.Interop
[MarshalAs(UnmanagedType.LPWStr)] [MarshalAs(UnmanagedType.LPWStr)]
public string pszSpec; 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] [Flags]

11
src/Windows/Avalonia.Win32/ScreenImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Win32
{ {
public class ScreenImpl : IScreenImpl public class ScreenImpl : IScreenImpl
{ {
public int ScreenCount public int ScreenCount
{ {
get => GetSystemMetrics(SystemMetric.SM_CMONITORS); get => GetSystemMetrics(SystemMetric.SM_CMONITORS);
} }
@ -33,7 +33,7 @@ namespace Avalonia.Win32
var shcore = LoadLibrary("shcore.dll"); var shcore = LoadLibrary("shcore.dll");
var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); var method = GetProcAddress(shcore, nameof(GetDpiForMonitor));
if (method != IntPtr.Zero) if (method != IntPtr.Zero)
{ {
GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _);
dpi = (double)x; dpi = (double)x;
} }
@ -51,11 +51,8 @@ namespace Avalonia.Win32
RECT bounds = monitorInfo.rcMonitor; RECT bounds = monitorInfo.rcMonitor;
RECT workingArea = monitorInfo.rcWork; RECT workingArea = monitorInfo.rcWork;
PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left, PixelRect avaloniaBounds = bounds.ToPixelRect();
bounds.bottom - bounds.top); PixelRect avaloniaWorkArea = workingArea.ToPixelRect();
PixelRect avaloniaWorkArea =
new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left,
workingArea.bottom - workingArea.top);
screens[index] = screens[index] =
new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1,
monitor); monitor);

6
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 clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
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; var frm = (UnmanagedMethods.IFileDialog)unk;
var openDialog = dialog as OpenFileDialog; var openDialog = dialog as OpenFileDialog;
@ -105,9 +105,9 @@ namespace Avalonia.Win32
var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; 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; var frm = (UnmanagedMethods.IFileDialog)unk;
uint options; uint options;
frm.GetOptions(out options); frm.GetOptions(out options);

13
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);
}
}
}

192
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -37,10 +37,14 @@ namespace Avalonia.Win32
{ WindowEdge.West, HitTestValues.HTLEFT } { WindowEdge.West, HitTestValues.HTLEFT }
}; };
private SavedWindowInfo _savedWindowInfo;
private bool _isFullScreenActive;
#if USE_MANAGED_DRAG #if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag; private readonly ManagedWindowResizeDragHelper _managedDrag;
#endif #endif
private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE);
private readonly List<WindowImpl> _disabledBy; private readonly List<WindowImpl> _disabledBy;
private readonly TouchDevice _touchDevice; private readonly TouchDevice _touchDevice;
private readonly MouseDevice _mouseDevice; private readonly MouseDevice _mouseDevice;
@ -82,7 +86,9 @@ namespace Avalonia.Win32
_windowProperties = new WindowProperties _windowProperties = new WindowProperties
{ {
ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full ShowInTaskbar = false,
IsResizable = true,
Decorations = SystemDecorations.Full
}; };
_rendererLock = new ManagedDeferredRendererLock(); _rendererLock = new ManagedDeferredRendererLock();
@ -538,27 +544,98 @@ namespace Avalonia.Win32
} }
} }
/// <summary>
/// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
/// Method must only be called from inside UpdateWindowProperties.
/// </summary>
/// <param name="fullscreen"></param>
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) private void ShowWindow(WindowState state)
{ {
ShowWindowCommand command; ShowWindowCommand command;
var newWindowProperties = _windowProperties;
switch (state) switch (state)
{ {
case WindowState.Minimized: case WindowState.Minimized:
newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Minimize; command = ShowWindowCommand.Minimize;
break; break;
case WindowState.Maximized: case WindowState.Maximized:
newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Maximize; command = ShowWindowCommand.Maximize;
break; break;
case WindowState.Normal: case WindowState.Normal:
newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Restore; command = ShowWindowCommand.Restore;
break; break;
case WindowState.FullScreen:
newWindowProperties.IsFullScreen = true;
UpdateWindowProperties(newWindowProperties);
return;
default: default:
throw new ArgumentException("Invalid WindowState."); throw new ArgumentException("Invalid WindowState.");
} }
UpdateWindowProperties(newWindowProperties);
UnmanagedMethods.ShowWindow(_hwnd, command); UnmanagedMethods.ShowWindow(_hwnd, command);
if (state == WindowState.Maximized) if (state == WindowState.Maximized)
@ -590,22 +667,69 @@ namespace Avalonia.Win32
SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); 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() private void UpdateEnabled()
{ {
EnableWindow(_hwnd, _disabledBy.Count == 0); EnableWindow(_hwnd, _disabledBy.Count == 0);
} }
private void UpdateWindowProperties(WindowProperties newProperties) private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false)
{ {
var oldProperties = _windowProperties; var oldProperties = _windowProperties;
@ -613,7 +737,7 @@ namespace Avalonia.Win32
// according to the new values already. // according to the new values already.
_windowProperties = newProperties; _windowProperties = newProperties;
if (oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges)
{ {
var exStyle = GetExtendedStyle(); var exStyle = GetExtendedStyle();
@ -632,7 +756,7 @@ namespace Avalonia.Win32
// Otherwise it will still show in the taskbar. // Otherwise it will still show in the taskbar.
} }
if (oldProperties.IsResizable != newProperties.IsResizable) if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges)
{ {
var style = GetStyle(); var style = GetStyle();
@ -648,7 +772,12 @@ namespace Avalonia.Win32
SetStyle(style); SetStyle(style);
} }
if (oldProperties.Decorations != newProperties.Decorations) if (oldProperties.IsFullScreen != newProperties.IsFullScreen)
{
SetFullScreen(newProperties.IsFullScreen);
}
if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges)
{ {
var style = GetStyle(); var style = GetStyle();
@ -663,30 +792,33 @@ namespace Avalonia.Win32
style &= ~fullDecorationFlags; 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); GetClientRect(_hwnd, out var oldClientRect);
var oldClientRectOrigin = new POINT(); var oldClientRectOrigin = new POINT();
ClientToScreen(_hwnd, ref oldClientRectOrigin); ClientToScreen(_hwnd, ref oldClientRectOrigin);
oldClientRect.Offset(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) SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,
{ SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE |
AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); 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; 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 private struct WindowProperties
{ {
public bool ShowInTaskbar; public bool ShowInTaskbar;
public bool IsResizable; public bool IsResizable;
public SystemDecorations Decorations; public SystemDecorations Decorations;
public bool IsFullScreen;
} }
} }
} }

145
tests/Avalonia.Animation.UnitTests/KeySplineTests.cs

@ -0,0 +1,145 @@
using System;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Styling;
using Xunit;
namespace Avalonia.Animation.UnitTests
{
public class KeySplineTests
{
[Theory]
[InlineData("1,2 3,4")]
[InlineData("1 2 3 4")]
[InlineData("1 2,3 4")]
[InlineData("1,2,3,4")]
public void Can_Parse_KeySpline_Via_TypeConverter(string input)
{
var conv = new KeySplineTypeConverter();
var keySpline = (KeySpline)conv.ConvertFrom(input);
Assert.Equal(1, keySpline.ControlPointX1);
Assert.Equal(2, keySpline.ControlPointY1);
Assert.Equal(3, keySpline.ControlPointX2);
Assert.Equal(4, keySpline.ControlPointY2);
}
[Theory]
[InlineData(0.00)]
[InlineData(0.50)]
[InlineData(1.00)]
public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input)
{
var keySpline = new KeySpline();
keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown
keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown
}
[Theory]
[InlineData(-0.01)]
[InlineData(1.01)]
public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input)
{
var keySpline = new KeySpline();
Assert.Throws<ArgumentException>(() => keySpline.ControlPointX1 = input);
Assert.Throws<ArgumentException>(() => keySpline.ControlPointX2 = input);
}
/*
To get the test values for the KeySpline test, you can:
1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations
2) Add the following xaml somewhere:
<Button Content="Capture"
Click="Button_Click"/>
<ScrollViewer VerticalScrollBarVisibility="Visible">
<TextBlock Name="CaptureData"
Text="---"
TextWrapping="Wrap" />
</ScrollViewer>
3) Add the following code to the code behind:
private void Button_Click(object sender, RoutedEventArgs e)
{
CaptureData.Text += string.Format("\n{0} | {1}", myTranslateTransform3D.OffsetX, (TimeSpan)ExampleStoryboard.GetCurrentTime(this));
CaptureData.Text +=
"\nKeySpline=\"" + mySplineKeyFrame.KeySpline.ControlPoint1.X.ToString() + "," +
mySplineKeyFrame.KeySpline.ControlPoint1.Y.ToString() + " " +
mySplineKeyFrame.KeySpline.ControlPoint2.X.ToString() + "," +
mySplineKeyFrame.KeySpline.ControlPoint2.Y.ToString() + "\"";
CaptureData.Text += "\n-----";
}
4) Run the app, mess with the slider values, then click the button to capture output values
**/
[Fact]
public void Check_KeySpline_Handled_properly()
{
var keyframe1 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, -2.5d),
},
KeyTime = TimeSpan.FromSeconds(0)
};
var keyframe2 = new KeyFrame()
{
Setters =
{
new Setter(RotateTransform.AngleProperty, 2.5d),
},
KeyTime = TimeSpan.FromSeconds(5),
KeySpline = new KeySpline(0.1123555056179775,
0.657303370786517,
0.8370786516853934,
0.499999999999999999)
};
var animation = new Animation()
{
Duration = TimeSpan.FromSeconds(5),
Children =
{
keyframe1,
keyframe2
},
IterationCount = new IterationCount(5),
PlaybackDirection = PlaybackDirection.Alternate
};
var rotateTransform = new RotateTransform(-2.5);
var rect = new Rectangle()
{
RenderTransform = rotateTransform
};
var clock = new TestClock();
var animationRun = animation.RunAsync(rect, clock);
// position is what you'd expect at end and beginning
clock.Step(TimeSpan.Zero);
Assert.Equal(rotateTransform.Angle, -2.5);
clock.Step(TimeSpan.FromSeconds(5));
Assert.Equal(rotateTransform.Angle, 2.5);
// test some points in between end and beginning
var tolerance = 0.01;
clock.Step(TimeSpan.Parse("00:00:10.0153932"));
var expected = -2.4122350198982545;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:11.2655407"));
expected = -0.37153223002125113;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:12.6158773"));
expected = 0.3967885416786294;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
clock.Step(TimeSpan.Parse("00:00:14.6495256"));
expected = 1.8016358493761722;
Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
}
}
}

313
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -336,210 +336,227 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Fact] public class SizingTests
public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
{ {
using (UnitTestApplication.Start(TestServices.StyledWindow)) [Fact]
public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
{ {
var child = new ChildControl(); using (UnitTestApplication.Start(TestServices.StyledWindow))
var target = new Window {
{ var child = new ChildControl();
Width = 100, var target = new Window
Height = 50, {
SizeToContent = SizeToContent.Manual, Width = 100,
Content = child Height = 50,
}; SizeToContent = SizeToContent.Manual,
Content = child
};
target.Show(); Show(target);
Assert.Equal(1, child.MeasureSizes.Count); Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(100, 50), child.MeasureSizes[0]); Assert.Equal(new Size(100, 50), child.MeasureSizes[0]);
}
} }
}
[Fact] [Fact]
public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified() public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var windowImpl = MockWindowingPlatform.CreateWindowMock(); using (UnitTestApplication.Start(TestServices.StyledWindow))
windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
var child = new ChildControl();
var target = new Window(windowImpl.Object)
{ {
SizeToContent = SizeToContent.Manual, var windowImpl = MockWindowingPlatform.CreateWindowMock();
Content = child windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
};
target.Show(); var child = new ChildControl();
var target = new Window(windowImpl.Object)
{
SizeToContent = SizeToContent.Manual,
Content = child
};
Assert.Equal(1, child.MeasureSizes.Count); Show(target);
Assert.Equal(new Size(550, 450), child.MeasureSizes[0]);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(550, 450), child.MeasureSizes[0]);
}
} }
}
[Fact] [Fact]
public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var child = new ChildControl(); using (UnitTestApplication.Start(TestServices.StyledWindow))
var target = new Window
{ {
Width = 100, var child = new ChildControl();
Height = 50, var target = new Window
SizeToContent = SizeToContent.WidthAndHeight, {
Content = child Width = 100,
}; Height = 50,
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show(); Show(target);
Assert.Equal(1, child.MeasureSizes.Count); Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(Size.Infinity, child.MeasureSizes[0]); Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
}
} }
}
[Fact] [Fact]
public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
{
// Issue #3784.
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var windowImpl = MockWindowingPlatform.CreateWindowMock(); // Issue #3784.
var clientSize = new Size(200, 200); using (UnitTestApplication.Start(TestServices.StyledWindow))
var maxClientSize = new Size(480, 480);
windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
{ {
clientSize = size.Constrain(maxClientSize); var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Object.Resized?.Invoke(clientSize); var clientSize = new Size(200, 200);
}); var maxClientSize = new Size(480, 480);
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
{
clientSize = size.Constrain(maxClientSize);
windowImpl.Object.Resized?.Invoke(clientSize);
});
var child = new Canvas windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
{
Width = 400,
Height = 800,
};
var target = new Window(windowImpl.Object)
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show(); var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window(windowImpl.Object)
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
Show(target);
Assert.Equal(new Size(400, 480), target.Bounds.Size); Assert.Equal(new Size(400, 480), target.Bounds.Size);
// Issue #3784 causes this to be (0, 160) which makes no sense as Window has no // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
// parent control to be offset against. // parent control to be offset against.
Assert.Equal(new Point(0, 0), target.Bounds.Position); Assert.Equal(new Point(0, 0), target.Bounds.Position);
}
} }
}
[Fact] [Fact]
public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight() public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var child = new Canvas using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
Width = 400, var child = new Canvas
Height = 800, {
}; Width = 400,
Height = 800,
};
var target = new Window() var target = new Window()
{ {
SizeToContent = SizeToContent.WidthAndHeight, SizeToContent = SizeToContent.WidthAndHeight,
Content = child Content = child
}; };
target.Show(); Show(target);
Assert.Equal(400, target.Width); Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height); Assert.Equal(800, target.Height);
}
} }
}
[Fact] [Fact]
public void SizeToContent_Should_Not_Be_Lost_On_Show() public void SizeToContent_Should_Not_Be_Lost_On_Show()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var child = new Canvas using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
Width = 400, var child = new Canvas
Height = 800, {
}; Width = 400,
Height = 800,
};
var target = new Window() var target = new Window()
{ {
SizeToContent = SizeToContent.WidthAndHeight, SizeToContent = SizeToContent.WidthAndHeight,
Content = child Content = child
}; };
target.Show(); Show(target);
Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
}
} }
}
[Fact] [Fact]
public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var child = new Canvas using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
Width = 400, var child = new Canvas
Height = 800, {
}; Width = 400,
Height = 800,
};
var target = new Window() var target = new Window()
{ {
SizeToContent = SizeToContent.WidthAndHeight, SizeToContent = SizeToContent.WidthAndHeight,
Content = child Content = child
}; };
target.Show(); Show(target);
Assert.Equal(400, target.Width); Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height); Assert.Equal(800, target.Height);
child.Width = 410; child.Width = 410;
target.LayoutManager.ExecuteLayoutPass(); target.LayoutManager.ExecuteLayoutPass();
Assert.Equal(410, target.Width); Assert.Equal(410, target.Width);
Assert.Equal(800, target.Height); Assert.Equal(800, target.Height);
Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
}
} }
}
[Fact] [Fact]
public void Setting_Width_Should_Resize_WindowImpl() public void Setting_Width_Should_Resize_WindowImpl()
{
// Issue #3796
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var target = new Window() // Issue #3796
using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
Width = 400, var target = new Window()
Height = 800, {
}; Width = 400,
Height = 800,
};
target.Show(); Show(target);
Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height);
Assert.Equal(400, target.Width); target.Width = 410;
Assert.Equal(800, target.Height); target.LayoutManager.ExecuteLayoutPass();
target.Width = 410; var windowImpl = Mock.Get(target.PlatformImpl);
target.LayoutManager.ExecuteLayoutPass(); windowImpl.Verify(x => x.Resize(new Size(410, 800)));
Assert.Equal(410, target.Width);
}
}
var windowImpl = Mock.Get(target.PlatformImpl); protected virtual void Show(Window window)
windowImpl.Verify(x => x.Resize(new Size(410, 800))); {
Assert.Equal(410, target.Width); window.Show();
}
}
public class DialogSizingTests : SizingTests
{
protected override void Show(Window window)
{
var owner = new Window();
window.ShowDialog(owner);
} }
} }

2
tests/Avalonia.RenderTests/TestBase.cs

@ -184,7 +184,7 @@ namespace Avalonia.Direct2D1.RenderTests
public void Signal(DispatcherPriority prio) public void Signal(DispatcherPriority prio)
{ {
throw new NotImplementedException(); // No-op
} }
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)

48
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests
public class CustomFontManagerImpl : IFontManagerImpl public class CustomFontManagerImpl : IFontManagerImpl
{ {
private readonly Typeface[] _customTypefaces; private readonly Typeface[] _customTypefaces;
private readonly string _defaultFamilyName;
private readonly Typeface _defaultTypeface = private readonly Typeface _defaultTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
private readonly Typeface _italicTypeface = private readonly Typeface _italicTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans");
private readonly Typeface _emojiTypeface = private readonly Typeface _emojiTypeface =
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji");
@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests
public CustomFontManagerImpl() public CustomFontManagerImpl()
{ {
_customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
_defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
} }
public string GetDefaultFontFamilyName() public string GetDefaultFontFamilyName()
{ {
return _defaultTypeface.FontFamily.ToString(); return _defaultFamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests
return _customTypefaces.Select(x => x.FontFamily.Name); return _customTypefaces.Select(x => x.FontFamily.Name);
} }
private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
CultureInfo culture, out FontKey fontKey) CultureInfo culture, out FontKey fontKey)
{ {
foreach (var customTypeface in _customTypefaces) foreach (var customTypeface in _customTypefaces)
{ {
if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
{
continue; continue;
fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle); }
fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle);
return true; return true;
} }
var fallback = SKFontManager.Default.MatchCharacter(codepoint); var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle); fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle);
return true; return true;
} }
public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
{ {
SKTypeface skTypeface;
switch (typeface.FontFamily.Name) switch (typeface.FontFamily.Name)
{ {
case "Twitter Color Emoji": case "Twitter Color Emoji":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case "Noto Sans": case "Noto Sans":
{
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
skTypeface = typefaceCollection.Get(typeface);
break;
}
case "Noto Mono": case "Noto Mono":
var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); {
var skTypeface = typefaceCollection.Get(typeface); var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
return new GlyphTypefaceImpl(skTypeface); skTypeface = typefaceCollection.Get(typeface);
break;
}
default: default:
return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name, {
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style)); skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
(SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
break;
}
} }
return new GlyphTypefaceImpl(skTypeface);
} }
} }
} }

13
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("Noto Mono", skTypeface.FamilyName); Assert.Equal("Noto Mono", skTypeface.FamilyName);
} }
} }
[Fact]
public void Should_Throw_For_Invalid_Custom_Font()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var fontManager = new FontManagerImpl();
Assert.Throws<InvalidOperationException>(() =>
fontManager.CreateGlyphTypeface(
new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
}
}
} }
} }

24
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default, Typeface.Default,
12.0f, 12.0f,
Brushes.Black.ToImmutable(), Brushes.Black.ToImmutable(),
maxWidth : 200, maxWidth : 200,
maxHeight : 125, maxHeight : 125,
textStyleOverrides: spans); textStyleOverrides: spans);
@ -506,10 +506,30 @@ namespace Avalonia.Skia.UnitTests
} }
} }
private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
[Fact]
public void Should_Wrap()
{
using (Start())
{
for (var i = 0; i < 2000; i++)
{
var layout = new TextLayout(
Text,
Typeface.Default,
12,
Brushes.Black,
textWrapping: TextWrapping.Wrap,
maxWidth: 50);
}
}
}
public static IDisposable Start() public static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null), .With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(), textShaperImpl: new TextShaperImpl(),
fontManagerImpl : new CustomFontManagerImpl())); fontManagerImpl : new CustomFontManagerImpl()));

11
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@ -8,14 +8,21 @@ namespace Avalonia.UnitTests
{ {
public class MockFontManagerImpl : IFontManagerImpl public class MockFontManagerImpl : IFontManagerImpl
{ {
private readonly string _defaultFamilyName;
public MockFontManagerImpl(string defaultFamilyName = "Default")
{
_defaultFamilyName = defaultFamilyName;
}
public string GetDefaultFontFamilyName() public string GetDefaultFontFamilyName()
{ {
return "Default"; return _defaultFamilyName;
} }
public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false) public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
{ {
return new[] { "Default" }; return new[] { _defaultFamilyName };
} }
public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,

12
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@ -1,4 +1,5 @@
using Avalonia.Media; using System;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
} }
} }
[Fact]
public void Should_Throw_When_Default_FamilyName_Is_Null()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null))))
{
Assert.Throws<InvalidOperationException>(() => FontManager.Current);
}
}
} }
} }

Loading…
Cancel
Save