diff --git a/demo/Ursa.Demo/Pages/NotificationDemo.axaml b/demo/Ursa.Demo/Pages/NotificationDemo.axaml
new file mode 100644
index 0000000..b5cd1db
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NotificationDemo.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..c4c0458
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/NotificationDemo.axaml.cs
@@ -0,0 +1,25 @@
+using Avalonia;
+using Avalonia.Controls;
+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 };
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 6edd07a..0a965e8 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -52,6 +52,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(),
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index ffab9a6..a0ce383 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -37,6 +37,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 },
new() { MenuHeader = "Number Displayer", Key = MenuKeys.MenuKeyNumberDisplayer, Status = "New" },
new() { MenuHeader = "Numeric UpDown", Key = MenuKeys.MenuKeyNumericUpDown },
new() { MenuHeader = "NumPad", Key = MenuKeys.MenuKeyNumPad },
@@ -88,6 +89,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";
diff --git a/demo/Ursa.Demo/ViewModels/NotificationDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/NotificationDemoViewModel.cs
new file mode 100644
index 0000000..da1623c
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/NotificationDemoViewModel.cs
@@ -0,0 +1,42 @@
+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 _showClose = true;
+
+ [RelayCommand]
+ public void ShowNormal(object obj)
+ {
+ if (obj is string s)
+ {
+ Enum.TryParse(s, out var notificationType);
+ NotificationManager?.Show(
+ new Notification("Welcome", "This is message"),
+ showClose: ShowClose,
+ type: notificationType);
+ }
+ }
+
+ [RelayCommand]
+ public void ShowLight(object obj)
+ {
+ if (obj is string s)
+ {
+ Enum.TryParse(s, out var notificationType);
+ NotificationManager?.Show(
+ new Notification("Welcome", "This is message"),
+ showClose: ShowClose,
+ type: notificationType,
+ classes: ["Light"]);
+ }
+ }
+}
\ 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..a550bc3
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/Notification.axaml
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+ 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 7003457..de4f44c 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -29,6 +29,7 @@
+
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..d3e1230
--- /dev/null
+++ b/src/Ursa/Controls/Notification/Notification.cs
@@ -0,0 +1,103 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls.Notifications;
+
+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();
+ }
+ }
+ }
+
+ ///
+ public string? Content
+ {
+ get => _content;
+ set
+ {
+ if (_content != value)
+ {
+ _content = value;
+ OnPropertyChanged();
+ }
+ }
+ }
+
+ ///
+ public NotificationType Type { get; set; }
+
+ ///
+ public TimeSpan Expiration { 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..5956712
--- /dev/null
+++ b/src/Ursa/Controls/Notification/NotificationCard.cs
@@ -0,0 +1,6 @@
+namespace Ursa.Controls;
+
+///
+/// Control that represents and displays a notification.
+///
+public class NotificationCard : MessageCard;
\ 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..4d66012
--- /dev/null
+++ b/src/Ursa/Controls/Notification/WindowNotificationManager.cs
@@ -0,0 +1,171 @@
+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.ShowClose, content.OnClick, content.OnClose);
+ }
+
+ ///
+ public override void Show(object content)
+ {
+ if (content is INotification notification)
+ {
+ Show(notification, notification.Type, notification.Expiration, 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 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 showClose = true,
+ Action? onClick = null,
+ Action? onClose = null,
+ string[]? classes = null)
+ {
+ Dispatcher.UIThread.VerifyAccess();
+
+ var notificationControl = new NotificationCard
+ {
+ Content = content,
+ NotificationType = type,
+ ShowClose = showClose
+ };
+
+ // 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