Skip to content

Commit

Permalink
[Primitives] Add notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
hyazinthh committed Mar 11, 2024
1 parent 6e99565 commit 9864912
Show file tree
Hide file tree
Showing 12 changed files with 695 additions and 0 deletions.
7 changes: 7 additions & 0 deletions src/Aardvark.Media.sln
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "29 - Garbage Collection", "
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "27 - GoldenLayout", "Examples (dotnetcore)\27 - GoldenLayout\27 - GoldenLayout.fsproj", "{3AFDD56A-F6AB-4BB0-AD88-8912071E998C}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "28 - Notifications", "Examples (dotnetcore)\28 - Notifications\28 - Notifications.fsproj", "{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -400,6 +402,10 @@ Global
{3AFDD56A-F6AB-4BB0-AD88-8912071E998C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3AFDD56A-F6AB-4BB0-AD88-8912071E998C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3AFDD56A-F6AB-4BB0-AD88-8912071E998C}.Release|Any CPU.Build.0 = Release|Any CPU
{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -466,6 +472,7 @@ Global
{E6E319E2-7A57-41B0-99B4-FF454EA81CF6} = {5DAFA99B-848D-4185-B4C1-287119815657}
{5F1B5AF1-73B4-44F4-89D7-84BBC929B770} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
{3AFDD56A-F6AB-4BB0-AD88-8912071E998C} = {DAC89FC7-17D3-467D-929D-781A88DA5324}
{B4F07EA9-16EA-4E80-93AD-4EF15EBAFCE8} = {DAC89FC7-17D3-467D-929D-781A88DA5324}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B7FCCF28-D562-4E8F-86A7-2310B38A1016}
Expand Down
3 changes: 3 additions & 0 deletions src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@
<Compile Include="Controllers\LegacyCameraController.fs" />
<Compile Include="Golden\GoldenLayoutModel.fs" />
<Compile Include="Golden\GoldenLayout.fs" />
<Compile Include="Notifications\NotificationsModel.fs" />
<Compile Include="Notifications\Notifications.fs" />
<Compile Include="Docking.fs" />
<Compile Include="OpenDialog.fs" />
<Compile Include="SaveDialog.fs" />
Expand All @@ -79,6 +81,7 @@
<EmbeddedResource Include="resources\spectrum.css" />
<EmbeddedResource Include="resources\spectrum.js" />
<EmbeddedResource Include="resources\spectrum-overrides.css" />
<EmbeddedResource Include="resources\notifications.js" />
<None Include="paket.references" />
</ItemGroup>
<ItemGroup>
Expand Down
238 changes: 238 additions & 0 deletions src/Aardvark.UI.Primitives/Notifications/Notifications.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
namespace Aardvark.UI.Primitives.Notifications

open Aardvark.UI
open FSharp.Data.Adaptive

[<AutoOpen>]
module NotificationsApp =

type NotificationBuilder() =
member inline x.Yield(()) =
{ Title = None
Message = "Message"
Icon = None
Progress = None
Theme = Theme.Default
Position = Position.TopRight
CenterContent = false
CloseIcon = false
Duration = Duration.Default }

[<CustomOperation("title")>]
member inline x.Title(n: Notification, title: string) =
{ n with Title = Some title }

[<CustomOperation("message")>]
member inline x.Message(n: Notification, message: string) =
{ n with Message = message }

[<CustomOperation("icon")>]
member inline x.Icon(n: Notification, icon: string) =
{ n with Icon = Some icon }

[<CustomOperation("progress")>]
member inline x.Progress(n: Notification, progress: Progress) =
{ n with Progress = Some progress }

[<CustomOperation("progress")>]
member inline x.Progress(n: Notification, show: bool) =
{ n with Progress = if show then Some Progress.Default else None }

[<CustomOperation("theme")>]
member inline x.Theme(n: Notification, theme: Theme) =
{ n with Theme = theme }

[<CustomOperation("theme")>]
member inline x.Theme(n: Notification, color: Color) =
x.Theme(n, { Color = color; Inverted = false })

[<CustomOperation("position")>]
member inline x.Position(n: Notification, position: Position) =
{ n with Position = position }

[<CustomOperation("center")>]
member inline x.Center(n: Notification, center: bool) =
{ n with CenterContent = center }

[<CustomOperation("closeIcon")>]
member inline x.CloseIcon(n: Notification, show: bool) =
{ n with CloseIcon = show }

[<CustomOperation("duration")>]
member inline x.Duration(n: Notification, duration: Duration) =
{ n with Duration = duration }

[<CustomOperation("duration")>]
member inline x.Duration(n: Notification, milliseconds: int) =
{ n with Duration = Duration.Milliseconds milliseconds }

let notification = NotificationBuilder()

[<AutoOpen>]
module Events =

/// Invoked when a notification is removed.
/// The integer argument is the ID of the removed notification.
let onRemove (callback: int -> 'msg) : Attribute<'msg> =
onEvent "onremove" [] (List.head >> int >> callback)

module Notifications =

module private Json =
open Newtonsoft.Json
open Newtonsoft.Json.Linq

module JObject =

let private theme (theme: Theme) =
let color =
match theme.Color with
| Color.Red -> "red"
| Color.Orange -> "orange"
| Color.Yellow -> "yellow"
| Color.Olive -> "olive"
| Color.Green -> "green"
| Color.Teal -> "teal"
| Color.Blue -> "blue"
| Color.Violet -> "violet"
| Color.Purple -> "purple"
| Color.Pink -> "pink"
| Color.Brown -> "brown"
| Color.Grey -> "grey"
| Color.Black -> "black"
| _ -> ""

if theme.Inverted then "inverted " + color
else color

let ofNotification (n: Notification) =
let o = JObject()

match n.Title with
| Some t -> o.["title"] <- JToken.op_Implicit t
| _ -> ()

o.["message"] <- JToken.op_Implicit n.Message

match n.Icon with
| Some i -> o.["showIcon"] <- JToken.op_Implicit i
| _ -> ()

match n.Progress with
| Some p ->
if p.Increasing then
o.["progressUp"] <- JToken.op_Implicit true

o.["showProgress"] <- JToken.op_Implicit (if p.Top then "top" else "bottom")
o.["classProgress"] <- JToken.op_Implicit (theme p.Theme)

| _ -> ()

let clazz =
[
theme n.Theme
if n.CenterContent then "centered"
]
|> String.concat " "

o.["class"] <- JToken.op_Implicit clazz

let position =
match n.Position with
| Position.TopRight -> "top right"
| Position.TopLeft -> "top left"
| Position.TopAttached -> "top attached"
| Position.TopCenter -> "top center"
| Position.BottomRight -> "bottom right"
| Position.BottomLeft -> "bottom left"
| Position.BottomAttached -> "bottom attached"
| Position.BottomCenter -> "bottom center"
| _ -> ""

o.["position"] <- JToken.op_Implicit position

o.["closeIcon"] <- JToken.op_Implicit n.CloseIcon

match n.Duration with
| Duration.Auto (min, words) ->
o.["displayTime"] <- JToken.op_Implicit "auto"
o.["minDisplayTime"] <- JToken.op_Implicit min
o.["wordsPerMinute"] <- JToken.op_Implicit words

| Duration.Milliseconds d ->
o.["displayTime"] <- JToken.op_Implicit d

o

let serialize (id: int) (notification: Notification option) =
let o = JObject()
o.["id"] <- JToken.op_Implicit id

match notification with
| Some n -> o.["data"] <- JObject.ofNotification n
| _ -> ()

o.ToString Formatting.None

type private NotificationsChannelReader(input: amap<int, Notification>) =
inherit ChannelReader()
let reader = input.GetReader()

override x.Release() = ()

override x.ComputeMessages(token: AdaptiveToken) =
let deltas = reader.GetChanges(token)

deltas
|> HashMapDelta.toHashMap
|> HashMap.toListV
|> List.map (fun (struct (id, op)) ->
match op with
| Set n -> Json.serialize id (Some n)
| Remove -> Json.serialize id None
)

type private NotificationsChannel(input: amap<int, Notification>) =
inherit Channel()
override x.GetReader() = new NotificationsChannelReader(input)

let update (message: Notifications.Message) (notifications: Notifications) : Notifications =
match message with
| Notifications.Send notification ->
{ Active = notifications.Active |> HashMap.add notifications.NextId notification
NextId = notifications.NextId + 1 }

| Notifications.Remove id ->
{ notifications with Active = notifications.Active |> HashMap.remove id }

| Notifications.Clear ->
{ notifications with Active = HashMap.empty }

let container (mapping: Notifications.Message -> 'msg)
(createContainer: Attribute<'msg> list -> DomNode<'msg>)
(notifications: AdaptiveNotifications) : DomNode<'msg> =

let dependencies =
Html.semui @ [ { name = "notifications"; url = "resources/notifications.js"; kind = Script }]

let channels : (string * Channel) list = [
"channelNotify", NotificationsChannel notifications.Active
]

let boot =
String.concat "" [
"const self = $('#__ID__')[0];"
"channelNotify.onmessage = (data) => aardvark.notifications.notify(self, data);"
]

let attributes =
[
style "position: relative"
onRemove Notifications.Remove
]
|> List.map (fun (name, value) -> name, AttributeValue.map mapping value)

require dependencies (
let node = createContainer attributes
node |> onBoot' channels boot
)
86 changes: 86 additions & 0 deletions src/Aardvark.UI.Primitives/Notifications/NotificationsModel.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
namespace Aardvark.UI.Primitives.Notifications

open Adaptify
open FSharp.Data.Adaptive

// Simple notifications using the Toast module of Fomantic UI
// https://fomantic-ui.com/modules/toast.html

type Color =
| Default = 0
| Red = 1
| Orange = 2
| Yellow = 3
| Olive = 4
| Green = 5
| Teal = 6
| Blue = 7
| Violet = 8
| Purple = 9
| Pink = 10
| Brown = 11
| Grey = 12
| Black = 13

[<Struct>]
type Theme =
{ Color : Color
Inverted : bool }

module Theme =
let Default = { Color = Color.Default; Inverted = false }

type Position =
| TopRight = 0
| TopLeft = 1
| TopCenter = 2
| TopAttached = 3
| BottomRight = 4
| BottomLeft = 5
| BottomCenter = 6
| BottomAttached = 7

[<RequireQualifiedAccess>]
type Duration =
| Milliseconds of int
| Auto of minMilliseconds: int * wordsPerMinute: int

module Duration =
let Default = Duration.Milliseconds 3000
let DefaultAuto = Duration.Auto(1000, 120)
let Infinite = Duration.Milliseconds 0

type Progress =
{ Top : bool
Theme : Theme
Increasing : bool }

module Progress =
let Default = { Top = false; Theme = Theme.Default; Increasing = false }

type Notification =
{ Title : string option
Message : string
Icon : string option
Progress : Progress option
Theme : Theme
Position : Position
CenterContent : bool
CloseIcon : bool
Duration : Duration }

[<ModelType>]
type Notifications =
{ Active : HashMap<int, Notification>
NextId : int }

module Notifications =

type Message =
| Send of Notification
| Remove of id: int
| Clear

let Empty =
{ Active = HashMap.empty
NextId = 0 }
Loading

0 comments on commit 9864912

Please sign in to comment.