diff --git a/demo/Ursa.Demo/Pages/NotificationDemo.axaml b/demo/Ursa.Demo/Pages/NotificationDemo.axaml
new file mode 100644
index 0000000..846ff53
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NotificationDemo.axaml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Pages/NotificationDemo.axaml.cs b/demo/Ursa.Demo/Pages/NotificationDemo.axaml.cs
new file mode 100644
index 0000000..31d0e78
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NotificationDemo.axaml.cs
@@ -0,0 +1,32 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Ursa.Controls;
+using Ursa.Demo.ViewModels;
+
+namespace Ursa.Demo.Pages;
+
+public partial class NotificationDemo : UserControl
+{
+ private NotificationDemoViewModel _viewModel;
+
+ public NotificationDemo()
+ {
+ InitializeComponent();
+ _viewModel = new NotificationDemoViewModel();
+ DataContext = _viewModel;
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ var topLevel = TopLevel.GetTopLevel(this);
+ _viewModel.NotificationManager = new WindowNotificationManager(topLevel) { MaxItems = 3 };
+ }
+
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromLogicalTree(e);
+ _viewModel.NotificationManager?.Uninstall();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Pages/ToastDemo.axaml b/demo/Ursa.Demo/Pages/ToastDemo.axaml
new file mode 100644
index 0000000..72f2145
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ToastDemo.axaml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Pages/ToastDemo.axaml.cs b/demo/Ursa.Demo/Pages/ToastDemo.axaml.cs
new file mode 100644
index 0000000..714eeb9
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/ToastDemo.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia;
+using Avalonia.Controls;
+using Ursa.Controls;
+using Ursa.Demo.ViewModels;
+
+namespace Ursa.Demo.Pages;
+
+public partial class ToastDemo : UserControl
+{
+ private ToastDemoViewModel _viewModel;
+
+ public ToastDemo()
+ {
+ InitializeComponent();
+ _viewModel = new ToastDemoViewModel();
+ DataContext = _viewModel;
+ }
+
+ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToVisualTree(e);
+ var topLevel = TopLevel.GetTopLevel(this);
+ _viewModel.ToastManager = new WindowToastManager(topLevel) { MaxItems = 3 };
+ }
+
+ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromVisualTree(e);
+ _viewModel.ToastManager?.Uninstall();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index d4af1cb..763ca99 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -53,6 +53,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyMessageBox => new MessageBoxDemoViewModel(),
MenuKeys.MenuKeyMultiComboBox => new MultiComboBoxDemoViewModel(),
MenuKeys.MenuKeyNavMenu => new NavMenuDemoViewModel(),
+ MenuKeys.MenuKeyNotification => new NotificationDemoViewModel(),
MenuKeys.MenuKeyNumberDisplayer => new NumberDisplayerDemoViewModel(),
MenuKeys.MenuKeyNumericUpDown => new NumericUpDownDemoViewModel(),
MenuKeys.MenuKeyNumPad => new NumPadDemoViewModel(),
@@ -68,6 +69,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyTreeComboBox => new TreeComboBoxDemoViewModel(),
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
MenuKeys.MenuKeyThemeToggler => new ThemeTogglerDemoViewModel(),
+ MenuKeys.MenuKeyToast => new ToastDemoViewModel(),
MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(),
MenuKeys.MenuKeyTimeBox => new TimeBoxDemoViewModel(),
MenuKeys.MenuKeyPinCode => new PinCodeDemoViewModel(),
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index 742989c..267aaf7 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -38,6 +38,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Message Box", Key = MenuKeys.MenuKeyMessageBox },
new() { MenuHeader = "MultiComboBox", Key = MenuKeys.MenuKeyMultiComboBox, Status = "Updated" },
new() { MenuHeader = "Nav Menu", Key = MenuKeys.MenuKeyNavMenu },
+ new() { MenuHeader = "Notification", Key = MenuKeys.MenuKeyNotification, Status = "New"},
new() { MenuHeader = "Number Displayer", Key = MenuKeys.MenuKeyNumberDisplayer, Status = "New" },
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad },
@@ -54,6 +55,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
new() { MenuHeader = "TreeComboBox", Key = MenuKeys.MenuKeyTreeComboBox },
new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon},
+ new() { MenuHeader = "Toast", Key = MenuKeys.MenuKeyToast, Status = "New"},
new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar },
new() { MenuHeader = "Time Box", Key = MenuKeys.MenuKeyTimeBox },
};
@@ -89,6 +91,7 @@ public static class MenuKeys
public const string MenuKeyMessageBox = "MessageBox";
public const string MenuKeyMultiComboBox = "MultiComboBox";
public const string MenuKeyNavMenu = "NavMenu";
+ public const string MenuKeyNotification = "Notification";
public const string MenuKeyNumberDisplayer = "NumberDisplayer";
public const string MenuKeyNumericUpDown = "NumericUpDown";
public const string MenuKeyNumPad = "NumPad";
@@ -104,6 +107,7 @@ public static class MenuKeys
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
public const string MenuKeyThemeToggler = "ThemeToggler";
public const string MenuKeyTreeComboBox = "TreeComboBox";
+ public const string MenuKeyToast = "Toast";
public const string MenuKeyToolBar = "ToolBar";
public const string MenuKeyPinCode = "PinCode";
public const string MenuKeyTimeBox = "TimeBox";
diff --git a/demo/Ursa.Demo/ViewModels/NotificationDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NotificationDemoViewModel.cs
new file mode 100644
index 0000000..6fafc95
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/NotificationDemoViewModel.cs
@@ -0,0 +1,51 @@
+using System;
+using Avalonia.Controls.Notifications;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Notification = Ursa.Controls.Notification;
+using WindowNotificationManager = Ursa.Controls.WindowNotificationManager;
+
+namespace Ursa.Demo.ViewModels;
+
+public partial class NotificationDemoViewModel : ObservableObject
+{
+ public WindowNotificationManager? NotificationManager { get; set; }
+
+ [ObservableProperty] private bool _showIcon = true;
+ [ObservableProperty] private bool _showClose = true;
+
+ [RelayCommand]
+ public void ChangePosition(object obj)
+ {
+ if (obj is string s && NotificationManager is not null)
+ {
+ Enum.TryParse(s, out var notificationPosition);
+ NotificationManager.Position = notificationPosition;
+ }
+ }
+
+ [RelayCommand]
+ public void ShowNormal(object obj)
+ {
+ if (obj is not string s) return;
+ Enum.TryParse(s, out var notificationType);
+ NotificationManager?.Show(
+ new Notification("Welcome", "This is message"),
+ showIcon: ShowIcon,
+ showClose: ShowClose,
+ type: notificationType);
+ }
+
+ [RelayCommand]
+ public void ShowLight(object obj)
+ {
+ if (obj is not string s) return;
+ Enum.TryParse(s, out var notificationType);
+ NotificationManager?.Show(
+ new Notification("Welcome", "This is message"),
+ showIcon: ShowIcon,
+ showClose: ShowClose,
+ type: notificationType,
+ classes: ["Light"]);
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/ToastDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/ToastDemoViewModel.cs
new file mode 100644
index 0000000..fa0c87a
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/ToastDemoViewModel.cs
@@ -0,0 +1,64 @@
+using System;
+using Avalonia.Controls.Notifications;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Ursa.Controls;
+
+namespace Ursa.Demo.ViewModels;
+
+public partial class ToastDemoViewModel : ObservableObject
+{
+ public WindowToastManager? ToastManager { get; set; }
+
+ [ObservableProperty] private bool _showIcon = true;
+ [ObservableProperty] private bool _showClose = true;
+
+ [RelayCommand]
+ public void ShowNormal(object obj)
+ {
+ if (obj is string s)
+ {
+ Enum.TryParse(s, out var notificationType);
+ ToastManager?.Show(
+ new Toast("This is message"),
+ showIcon: ShowIcon,
+ showClose: ShowClose,
+ type: notificationType);
+ }
+
+ // ToastManager?.Show(new ToastDemoViewModel
+ // {
+ // Content = "This is message",
+ // ToastManager = ToastManager
+ // });
+ }
+
+ [RelayCommand]
+ public void ShowLight(object obj)
+ {
+ if (obj is string s)
+ {
+ Enum.TryParse(s, out var notificationType);
+ ToastManager?.Show(
+ new Toast("This is message"),
+ showIcon: ShowIcon,
+ showClose: ShowClose,
+ type: notificationType,
+ classes: ["Light"]);
+ }
+ }
+
+ public string? Content { get; set; }
+
+ [RelayCommand]
+ public void YesCommand()
+ {
+ ToastManager?.Show(new Toast("Yes!"));
+ }
+
+ [RelayCommand]
+ public void NoCommand()
+ {
+ ToastManager?.Show(new Toast("No!"));
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/Notification.axaml b/src/Ursa.Themes.Semi/Controls/Notification.axaml
new file mode 100644
index 0000000..11d6b80
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/Notification.axaml
@@ -0,0 +1,463 @@
+
+
+
+
+
+
+
+ Hello, Ursa!
+
+
+
+
+
+
+
+
+
+ Hello, Ursa!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/Toast.axaml b/src/Ursa.Themes.Semi/Controls/Toast.axaml
new file mode 100644
index 0000000..316dc55
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/Toast.axaml
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
+ Hello, Ursa!
+
+
+
+ Hello, Ursa!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index de5d6f3..f8947d3 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -30,6 +30,7 @@
+
@@ -47,6 +48,7 @@
+
diff --git a/src/Ursa.Themes.Semi/Themes/Dark/NotificationShared.axaml b/src/Ursa.Themes.Semi/Themes/Dark/NotificationShared.axaml
new file mode 100644
index 0000000..efa4307
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Dark/NotificationShared.axaml
@@ -0,0 +1,15 @@
+
+ inset 0 0 0 1 #1AFFFFFF, 0 4 14 0 #40000000
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Ursa.Themes.Semi/Themes/Dark/Toast.axaml b/src/Ursa.Themes.Semi/Themes/Dark/Toast.axaml
new file mode 100644
index 0000000..ef28a3c
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Dark/Toast.axaml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
index 91ca159..91a84b5 100644
--- a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
@@ -12,10 +12,12 @@
+
+
diff --git a/src/Ursa.Themes.Semi/Themes/Light/NotificationShared.axaml b/src/Ursa.Themes.Semi/Themes/Light/NotificationShared.axaml
new file mode 100644
index 0000000..26f70c6
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Light/NotificationShared.axaml
@@ -0,0 +1,15 @@
+
+ 0 0 1 0 #4A000000, 0 4 14 0 #1A000000
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Themes/Light/Toast.axaml b/src/Ursa.Themes.Semi/Themes/Light/Toast.axaml
new file mode 100644
index 0000000..1be95d0
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Light/Toast.axaml
@@ -0,0 +1,5 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
index 91ca159..91a84b5 100644
--- a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
@@ -12,10 +12,12 @@
+
+
diff --git a/src/Ursa.Themes.Semi/Themes/Shared/Notification.axaml b/src/Ursa.Themes.Semi/Themes/Shared/Notification.axaml
new file mode 100644
index 0000000..b850601
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Shared/Notification.axaml
@@ -0,0 +1,3 @@
+
+ 20 16 12 16
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Themes/Shared/Toast.axaml b/src/Ursa.Themes.Semi/Themes/Shared/Toast.axaml
new file mode 100644
index 0000000..197939b
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Shared/Toast.axaml
@@ -0,0 +1,17 @@
+
+ 40
+ 1
+ 6
+
+ 12
+ 12 8
+
+ 18
+ 18
+ 0 2 0 0
+
+ 600
+ 12 0
+ 450
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
index 5b18e6f..638a5bf 100644
--- a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
@@ -15,12 +15,14 @@
+
+
diff --git a/src/Ursa/Controls/Notification/INotification.cs b/src/Ursa/Controls/Notification/INotification.cs
new file mode 100644
index 0000000..7563629
--- /dev/null
+++ b/src/Ursa/Controls/Notification/INotification.cs
@@ -0,0 +1,17 @@
+namespace Ursa.Controls;
+
+///
+/// Represents a notification that can be shown in a window or by the host operating system.
+///
+public interface INotification : IMessage
+{
+ ///
+ /// Gets the Title of the notification.
+ ///
+ string? Title { get; }
+
+ ///
+ /// Gets the Content of the notification.
+ ///
+ string? Content { get; }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Notification/INotificationManager.cs b/src/Ursa/Controls/Notification/INotificationManager.cs
new file mode 100644
index 0000000..7bfe40a
--- /dev/null
+++ b/src/Ursa/Controls/Notification/INotificationManager.cs
@@ -0,0 +1,14 @@
+namespace Ursa.Controls;
+
+///
+/// Represents a notification manager that can be used to show notifications in a window or using
+/// the host operating system.
+///
+public interface INotificationManager
+{
+ ///
+ /// Show a notification.
+ ///
+ /// The notification to be displayed.
+ void Show(INotification notification);
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Notification/Notification.cs b/src/Ursa/Controls/Notification/Notification.cs
new file mode 100644
index 0000000..e3bb00c
--- /dev/null
+++ b/src/Ursa/Controls/Notification/Notification.cs
@@ -0,0 +1,108 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls.Notifications;
+using Avalonia.Metadata;
+
+namespace Ursa.Controls;
+
+///
+/// A notification that can be shown in a window or by the host operating system.
+///
+///
+/// This class represents a notification that can be displayed either in a window using
+/// or by the host operating system (to be implemented).
+///
+public class Notification : INotification, INotifyPropertyChanged
+{
+ private string? _title, _content;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The title of the notification.
+ /// The content to be displayed in the notification.
+ /// The of the notification.
+ /// The expiry time at which the notification will close.
+ /// Use for notifications that will remain open.
+ /// A value indicating whether the notification should show a close button.
+ /// An Action to call when the notification is clicked.
+ /// An Action to call when the notification is closed.
+ public Notification(
+ string? title,
+ string? content,
+ NotificationType type = NotificationType.Information,
+ TimeSpan? expiration = null,
+ bool showClose = true,
+ Action? onClick = null,
+ Action? onClose = null)
+ {
+ Title = title;
+ Content = content;
+ Type = type;
+ Expiration = expiration ?? TimeSpan.FromSeconds(3);
+ ShowClose = showClose;
+ OnClick = onClick;
+ OnClose = onClose;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public Notification() : this(null, null)
+ {
+ }
+
+ ///
+ public string? Title
+ {
+ get => _title;
+ set
+ {
+ if (_title != value)
+ {
+ _title = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ [Content]
+ public string? Content
+ {
+ get => _content;
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ public NotificationType Type { get; set; }
+
+ ///
+ public TimeSpan Expiration { get; set; }
+
+ ///
+ public bool ShowIcon { get; set; }
+
+ ///
+ public bool ShowClose { get; }
+
+ ///
+ public Action? OnClick { get; set; }
+
+ ///
+ public Action? OnClose { get; set; }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ {
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Notification/NotificationCard.cs b/src/Ursa/Controls/Notification/NotificationCard.cs
new file mode 100644
index 0000000..89687e8
--- /dev/null
+++ b/src/Ursa/Controls/Notification/NotificationCard.cs
@@ -0,0 +1,49 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Notifications;
+using Avalonia.LogicalTree;
+
+namespace Ursa.Controls;
+
+///
+/// Control that represents and displays a notification.
+///
+[PseudoClasses(
+ WindowNotificationManager.PC_TopLeft,
+ WindowNotificationManager.PC_TopRight,
+ WindowNotificationManager.PC_BottomLeft,
+ WindowNotificationManager.PC_BottomRight,
+ WindowNotificationManager.PC_TopCenter,
+ WindowNotificationManager.PC_BottomCenter
+)]
+public class NotificationCard : MessageCard
+{
+ private NotificationPosition _position;
+
+ public NotificationPosition Position
+ {
+ get => _position;
+ set => SetAndRaise(PositionProperty, ref _position, value);
+ }
+
+ public static readonly DirectProperty PositionProperty =
+ AvaloniaProperty.RegisterDirect(nameof(Position),
+ o => o.Position, (o, v) => o.Position = v);
+
+ protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnAttachedToLogicalTree(e);
+ UpdatePseudoClasses(Position);
+ }
+
+ private void UpdatePseudoClasses(NotificationPosition position)
+ {
+ PseudoClasses.Set(WindowNotificationManager.PC_TopLeft, position == NotificationPosition.TopLeft);
+ PseudoClasses.Set(WindowNotificationManager.PC_TopRight, position == NotificationPosition.TopRight);
+ PseudoClasses.Set(WindowNotificationManager.PC_BottomLeft, position == NotificationPosition.BottomLeft);
+ PseudoClasses.Set(WindowNotificationManager.PC_BottomRight, position == NotificationPosition.BottomRight);
+ PseudoClasses.Set(WindowNotificationManager.PC_TopCenter, position == NotificationPosition.TopCenter);
+ PseudoClasses.Set(WindowNotificationManager.PC_BottomCenter, position == NotificationPosition.BottomCenter);
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/Notification/WindowNotificationManager.cs b/src/Ursa/Controls/Notification/WindowNotificationManager.cs
new file mode 100644
index 0000000..f6b517e
--- /dev/null
+++ b/src/Ursa/Controls/Notification/WindowNotificationManager.cs
@@ -0,0 +1,178 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Notifications;
+using Avalonia.Layout;
+using Avalonia.Threading;
+
+namespace Ursa.Controls;
+
+///
+/// An that displays notifications in a .
+///
+[PseudoClasses(PC_TopLeft, PC_TopRight, PC_BottomLeft, PC_BottomRight, PC_TopCenter, PC_BottomCenter)]
+public class WindowNotificationManager : WindowMessageManager, INotificationManager
+{
+ public const string PC_TopLeft = ":topleft";
+ public const string PC_TopRight = ":topright";
+ public const string PC_BottomLeft = ":bottomleft";
+ public const string PC_BottomRight = ":bottomright";
+ public const string PC_TopCenter = ":topcenter";
+ public const string PC_BottomCenter = ":bottomcenter";
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PositionProperty =
+ AvaloniaProperty.Register(nameof(Position),
+ NotificationPosition.TopRight);
+
+ ///
+ /// Defines which corner of the screen notifications can be displayed in.
+ ///
+ ///
+ public NotificationPosition Position
+ {
+ get => GetValue(PositionProperty);
+ set => SetValue(PositionProperty, value);
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The TopLevel that will host the control.
+ public WindowNotificationManager(TopLevel? host) : this()
+ {
+ if (host is not null)
+ {
+ InstallFromTopLevel(host);
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public WindowNotificationManager()
+ {
+ UpdatePseudoClasses(Position);
+ }
+
+ static WindowNotificationManager()
+ {
+ HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Stretch);
+ VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Stretch);
+ }
+
+ ///
+ public void Show(INotification content)
+ {
+ Show(content, content.Type, content.Expiration,
+ content.ShowIcon, content.ShowClose,
+ content.OnClick, content.OnClose);
+ }
+
+ ///
+ public override void Show(object content)
+ {
+ if (content is INotification notification)
+ {
+ Show(notification, notification.Type, notification.Expiration,
+ notification.ShowIcon, notification.ShowClose,
+ notification.OnClick, notification.OnClose);
+ }
+ else
+ {
+ Show(content, NotificationType.Information);
+ }
+ }
+
+ ///
+ /// Shows a Notification
+ ///
+ /// the content of the notification
+ /// the type of the notification
+ /// the expiration time of the notification after which it will automatically close. If the value is Zero then the notification will remain open until the user closes it
+ /// whether to show the icon
+ /// whether to show the close button
+ /// an Action to be run when the notification is clicked
+ /// an Action to be run when the notification is closed
+ /// style classes to apply
+ public async void Show(
+ object content,
+ NotificationType type,
+ TimeSpan? expiration = null,
+ bool showIcon = true,
+ bool showClose = true,
+ Action? onClick = null,
+ Action? onClose = null,
+ string[]? classes = null)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+
+ var notificationControl = new NotificationCard
+ {
+ Content = content,
+ NotificationType = type,
+ ShowIcon = showIcon,
+ ShowClose = showClose,
+ [!NotificationCard.PositionProperty] = this[!PositionProperty]
+ };
+
+ // Add style classes if any
+ if (classes is not null)
+ {
+ foreach (var @class in classes)
+ {
+ notificationControl.Classes.Add(@class);
+ }
+ }
+
+ notificationControl.MessageClosed += (sender, _) =>
+ {
+ onClose?.Invoke();
+
+ _items?.Remove(sender);
+ };
+
+ notificationControl.PointerPressed += (_, _) => { onClick?.Invoke(); };
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ _items?.Add(notificationControl);
+
+ if (_items?.OfType().Count(i => !i.IsClosing) > MaxItems)
+ {
+ _items.OfType().First(i => !i.IsClosing).Close();
+ }
+ });
+
+ if (expiration == TimeSpan.Zero)
+ {
+ return;
+ }
+
+ await Task.Delay(expiration ?? TimeSpan.FromSeconds(3));
+
+ notificationControl.Close();
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == PositionProperty)
+ {
+ UpdatePseudoClasses(change.GetNewValue());
+ }
+ }
+
+ private void UpdatePseudoClasses(NotificationPosition position)
+ {
+ PseudoClasses.Set(PC_TopLeft, position == NotificationPosition.TopLeft);
+ PseudoClasses.Set(PC_TopRight, position == NotificationPosition.TopRight);
+ PseudoClasses.Set(PC_BottomLeft, position == NotificationPosition.BottomLeft);
+ PseudoClasses.Set(PC_BottomRight, position == NotificationPosition.BottomRight);
+ PseudoClasses.Set(PC_TopCenter, position == NotificationPosition.TopCenter);
+ PseudoClasses.Set(PC_BottomCenter, position == NotificationPosition.BottomCenter);
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/NotificationShared/IMessage.cs b/src/Ursa/Controls/NotificationShared/IMessage.cs
new file mode 100644
index 0000000..53fdbe9
--- /dev/null
+++ b/src/Ursa/Controls/NotificationShared/IMessage.cs
@@ -0,0 +1,40 @@
+using Avalonia.Controls.Notifications;
+
+namespace Ursa.Controls;
+
+///
+/// Represents a message that can be shown in a window or by the host operating system.
+///
+public interface IMessage
+{
+ ///
+ /// Gets the of the message.
+ ///
+ NotificationType Type { get; }
+
+ ///
+ /// Gets a value indicating whether the message should show an icon.
+ ///
+ bool ShowIcon { get; }
+
+ ///
+ /// Gets a value indicating whether the message should show a close button.
+ ///
+ bool ShowClose { get; }
+
+ ///
+ /// Gets the expiration time of the message after which it will automatically close.
+ /// If the value is then the message will remain open until the user closes it.
+ ///
+ TimeSpan Expiration { get; }
+
+ ///
+ /// Gets an Action to be run when the message is clicked.
+ ///
+ Action? OnClick { get; }
+
+ ///
+ /// Gets an Action to be run when the message is closed.
+ ///
+ Action? OnClose { get; }
+}
\ No newline at end of file
diff --git a/src/Ursa/Controls/NotificationShared/MessageCard.cs b/src/Ursa/Controls/NotificationShared/MessageCard.cs
new file mode 100644
index 0000000..3c0bd60
--- /dev/null
+++ b/src/Ursa/Controls/NotificationShared/MessageCard.cs
@@ -0,0 +1,216 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Notifications;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+
+namespace Ursa.Controls;
+
+///
+/// Control that represents and displays a message.
+///
+[PseudoClasses(PC_Information, PC_Success, PC_Warning, PC_Error)]
+public abstract class MessageCard : ContentControl
+{
+ public const string PC_Information = ":information";
+ public const string PC_Success = ":success";
+ public const string PC_Warning = ":warning";
+ public const string PC_Error = ":error";
+
+ private bool _isClosing;
+
+ static MessageCard()
+ {
+ CloseOnClickProperty.Changed.AddClassHandler