diff --git a/src/Aardvark.Media.sln b/src/Aardvark.Media.sln index 92cbdee4..58e99594 100644 --- a/src/Aardvark.Media.sln +++ b/src/Aardvark.Media.sln @@ -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 @@ -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 @@ -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} diff --git a/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj b/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj index 67a451f2..62a7b6d6 100644 --- a/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj +++ b/src/Aardvark.UI.Primitives/Aardvark.UI.Primitives.fsproj @@ -62,6 +62,8 @@ + + @@ -79,6 +81,7 @@ + diff --git a/src/Aardvark.UI.Primitives/Notifications/Notifications.fs b/src/Aardvark.UI.Primitives/Notifications/Notifications.fs new file mode 100644 index 00000000..2860ec2e --- /dev/null +++ b/src/Aardvark.UI.Primitives/Notifications/Notifications.fs @@ -0,0 +1,238 @@ +namespace Aardvark.UI.Primitives.Notifications + +open Aardvark.UI +open FSharp.Data.Adaptive + +[] +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 } + + [] + member inline x.Title(n: Notification, title: string) = + { n with Title = Some title } + + [] + member inline x.Message(n: Notification, message: string) = + { n with Message = message } + + [] + member inline x.Icon(n: Notification, icon: string) = + { n with Icon = Some icon } + + [] + member inline x.Progress(n: Notification, progress: Progress) = + { n with Progress = Some progress } + + [] + member inline x.Progress(n: Notification, show: bool) = + { n with Progress = if show then Some Progress.Default else None } + + [] + member inline x.Theme(n: Notification, theme: Theme) = + { n with Theme = theme } + + [] + member inline x.Theme(n: Notification, color: Color) = + x.Theme(n, { Color = color; Inverted = false }) + + [] + member inline x.Position(n: Notification, position: Position) = + { n with Position = position } + + [] + member inline x.Center(n: Notification, center: bool) = + { n with CenterContent = center } + + [] + member inline x.CloseIcon(n: Notification, show: bool) = + { n with CloseIcon = show } + + [] + member inline x.Duration(n: Notification, duration: Duration) = + { n with Duration = duration } + + [] + member inline x.Duration(n: Notification, milliseconds: int) = + { n with Duration = Duration.Milliseconds milliseconds } + + let notification = NotificationBuilder() + + [] + 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) = + 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) = + 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 + ) \ No newline at end of file diff --git a/src/Aardvark.UI.Primitives/Notifications/NotificationsModel.fs b/src/Aardvark.UI.Primitives/Notifications/NotificationsModel.fs new file mode 100644 index 00000000..b002c431 --- /dev/null +++ b/src/Aardvark.UI.Primitives/Notifications/NotificationsModel.fs @@ -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 + +[] +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 + +[] +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 } + +[] +type Notifications = + { Active : HashMap + NextId : int } + +module Notifications = + + type Message = + | Send of Notification + | Remove of id: int + | Clear + + let Empty = + { Active = HashMap.empty + NextId = 0 } \ No newline at end of file diff --git a/src/Aardvark.UI.Primitives/resources/notifications.js b/src/Aardvark.UI.Primitives/resources/notifications.js new file mode 100644 index 00000000..355930ea --- /dev/null +++ b/src/Aardvark.UI.Primitives/resources/notifications.js @@ -0,0 +1,35 @@ +if (!aardvark.notifications) { + aardvark.notifications = { + toasts: new Map() + }; + + /** + * @param {HTMLElement} container + * @param {int} id + */ + const removeToast = function (container, id) { + aardvark.processEvent(container.id, 'onremove', id); + aardvark.notifications.toasts.delete(id); + } + + /** + * @param {HTMLElement} container + * @param {{id: int}} data + */ + aardvark.notifications.notify = function (container, data) { + const $container = $(container); + + if (data.data) { + const params = Object.assign(data.data, { context: $container, onRemove: () => removeToast(container, data.id) }); + const toast = $.toast(params); + aardvark.notifications.toasts.set(data.id, toast); + + } else { + const toast = aardvark.notifications.toasts.get(data.id); + if (toast !== undefined) { + toast.toast('close'); + aardvark.notifications.toasts.delete(data.id); + } + } + }; +} \ No newline at end of file diff --git a/src/Examples (dotnetcore)/28 - Notifications/28 - Notifications.fsproj b/src/Examples (dotnetcore)/28 - Notifications/28 - Notifications.fsproj new file mode 100644 index 00000000..9bf52757 --- /dev/null +++ b/src/Examples (dotnetcore)/28 - Notifications/28 - Notifications.fsproj @@ -0,0 +1,30 @@ + + + + Exe + net6.0 + True + $(MSBuildProjectName.Replace(" ", "_")) + + + ..\..\..\bin\Debug\ + portable + + + ..\..\..\bin\Release\ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Examples (dotnetcore)/28 - Notifications/App.config b/src/Examples (dotnetcore)/28 - Notifications/App.config new file mode 100644 index 00000000..434e45fb --- /dev/null +++ b/src/Examples (dotnetcore)/28 - Notifications/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Examples (dotnetcore)/28 - Notifications/App.fs b/src/Examples (dotnetcore)/28 - Notifications/App.fs new file mode 100644 index 00000000..91b1c939 --- /dev/null +++ b/src/Examples (dotnetcore)/28 - Notifications/App.fs @@ -0,0 +1,168 @@ +module NotificationsExample.App + +open Aardvark.UI +open Aardvark.UI.Primitives +open Aardvark.UI.Primitives.Notifications + +open System +open Aardvark.Base +open FSharp.Data.Adaptive +open Aardvark.Rendering +open NotificationsExample.Model + +module private Notification = + let private rnd = RandomSystem() + + let private getRandom (cases : 'T[]) = + fun () -> cases.[rnd.UniformInt cases.Length] + + let private getTitle = + [| + Some "Attention" + Some "Warning" + Some "Error" + Some "Information" + Some "As per my last email" + None + |] + |> getRandom + + let private getMessage = + [| + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + "Ceterum censeo Carthaginem esse delendam." + "The quick brown fox jumps over the lazy dog." + "Pack my box with five dozen liquor jugs." + "Sphinx of black quartz, judge my vow." + |] + |> getRandom + + let private getIcon = + [| + Some "exclamation circle" + Some "exclamation triangle" + Some "bell outline" + None + |] + |> getRandom + + let private getColor = + Enum.GetValues() + |> getRandom + + let private getBool = + [| false; true |] + |> getRandom + + let private getTheme() = + { Color = getColor(); Inverted = getBool() } + + let private getProgress() = + if getBool() then None + else + Some { + Top = getBool() + Theme = getTheme() + Increasing = getBool() + } + + let private getDuration = + [| Duration.Default; Duration.DefaultAuto; Duration.Milliseconds 1500 |] + |> getRandom + + let generate (position: Position) = + { Title = getTitle() + Message = getMessage() + Icon = getIcon() + Progress = getProgress() + Theme = getTheme() + Position = position + CenterContent = getBool() + CloseIcon = getBool() + Duration = getDuration() } + +let initialCamera = { + FreeFlyController.initial with + view = CameraView.lookAt (V3d.III * 3.0) V3d.OOO V3d.OOI + } + +let update (model : Model) (msg : Message) = + match msg with + | SetPosition p -> + { model with position = p } + + | Notify m -> + { model with notifications = model.notifications |> Notifications.update m } + +let view (model : AdaptiveModel) = + body [ + style "width: 100%; height: 100%; border: 0; padding: 0; margin: 0; overflow: hidden" + style $"display: flex; flex-direction: column; background: linen" + ] [ + model.notifications |> Notifications.container Notify (fun attributes -> + div (attributes @ [ style $"flex: 1 1 auto; margin: 32px; background: aliceblue; border-style: dashed" ]) [ + ] + ) + + div [ style $"flex: 0 0 40px; background: darkslategray" ] [ + button [ + clazz "ui button inverted" + style "margin: 10px" + onClick (fun _ -> Notify <| Notifications.Send (Notification.generate <| model.position.GetValue())) + ] [ + text "Send" + ] + + button [ + clazz "ui button inverted" + style "margin: 10px" + onClick (fun _ -> Notify <| Notifications.Clear) + ] [ + text "Clear" + ] + + button [ + clazz "ui button inverted" + style "margin: 10px" + onClick (fun _ -> + let keys = model.notifications.Active.Content.GetValue() |> HashMap.toKeyList + let id = match keys with [] -> 0 | _ -> Seq.max keys + Notify <| Notifications.Remove id + ) + ] [ + text "Remove" + ] + + let values = + Enum.GetValues() + |> Array.map (fun p -> + let n = + match p with + | Position.TopRight -> text "Top Right" + | Position.TopLeft -> text "Top Left" + | Position.TopCenter -> text "Top Center" + | Position.TopAttached -> text "Top Attached" + | Position.BottomRight -> text "Bottom Right" + | Position.BottomLeft -> text "Bottom Left" + | Position.BottomCenter -> text "Bottom Center" + | _ -> text "Bottom Attached" + p, n + ) + |> AMap.ofArray + + dropdownUnclearable [clazz "selection"; style "margin: 10px"] values model.position SetPosition + ] + ] + +let app = + { + unpersist = Unpersist.instance + threads = fun _ -> ThreadPool.empty + initial = + { + notifications = Notifications.Empty + position = Position.TopRight + } + update = update + view = view + } \ No newline at end of file diff --git a/src/Examples (dotnetcore)/28 - Notifications/AssemblyInfo.fs b/src/Examples (dotnetcore)/28 - Notifications/AssemblyInfo.fs new file mode 100644 index 00000000..0fef1dbc --- /dev/null +++ b/src/Examples (dotnetcore)/28 - Notifications/AssemblyInfo.fs @@ -0,0 +1,41 @@ +namespace _01___Inc.AssemblyInfo + +open System.Reflection +open System.Runtime.CompilerServices +open System.Runtime.InteropServices + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[] +[] +[] +[] +[] +[] +[] +[] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [] +[] +[] + +do + () \ No newline at end of file diff --git a/src/Examples (dotnetcore)/28 - Notifications/Model.fs b/src/Examples (dotnetcore)/28 - Notifications/Model.fs new file mode 100644 index 00000000..a287c175 --- /dev/null +++ b/src/Examples (dotnetcore)/28 - Notifications/Model.fs @@ -0,0 +1,16 @@ +namespace NotificationsExample.Model + +open Aardvark.UI.Primitives +open Aardvark.UI.Primitives.Notifications +open Adaptify + +type Message = + | SetPosition of Position + | Notify of Notifications.Message + +[] +type Model = + { + notifications : Notifications + position : Position + } \ No newline at end of file diff --git a/src/Examples (dotnetcore)/28 - Notifications/Program.fs b/src/Examples (dotnetcore)/28 - Notifications/Program.fs new file mode 100644 index 00000000..637214a6 --- /dev/null +++ b/src/Examples (dotnetcore)/28 - Notifications/Program.fs @@ -0,0 +1,45 @@ +open System + +open Aardvark.Base +open Aardvark.Rendering +open Aardvark.UI +open Aardium +open NotificationsExample + +[] +let main argv = + Aardvark.Init() + Aardium.init() + + // media apps require a runtime, which serves as renderer for your render controls. + // you can use OpenGL or VulkanApplication. + let useVulkan = false + + let runtime, disposable = + if useVulkan then + let app = new Aardvark.Rendering.Vulkan.HeadlessVulkanApplication() + app.Runtime :> IRuntime, app :> IDisposable + else + let app = new Aardvark.Application.Slim.OpenGlApplication() + app.Runtime :> IRuntime, app :> IDisposable + use __ = disposable + + let app = App.app + + let instance = + app |> App.start + + Suave.WebPart.startServerLocalhost 4321 [ + MutableApp.toWebPart runtime instance + Suave.Reflection.assemblyWebPart typeof.Assembly + ] |> ignore + + Aardium.run { + url "http://localhost:4321/" + width 1024 + height 768 + title "28 - Notifications" + debug true + } + + 0 \ No newline at end of file diff --git a/src/Examples (dotnetcore)/28 - Notifications/paket.references b/src/Examples (dotnetcore)/28 - Notifications/paket.references new file mode 100644 index 00000000..84bdf318 --- /dev/null +++ b/src/Examples (dotnetcore)/28 - Notifications/paket.references @@ -0,0 +1,20 @@ +Aardvark.Base +FSharp.Data.Adaptive +Aardvark.Base.FSharp + +Aardvark.Rendering +Aardvark.Base.Incremental +Aardvark.Rendering.Vulkan +Aardvark.Application.Slim.GL +Aardvark.SceneGraph +Aardvark.SceneGraph.IO +Aardvark.Rendering.Text +Adaptify.MSBuild + +FsPickler +FsPickler.Json + +Aardium + +Giraffe +FSharp.Core