Skip to content

Commit

Permalink
Add simple virtualized list example
Browse files Browse the repository at this point in the history
  • Loading branch information
hyazinthh committed Jul 24, 2023
1 parent 422a7be commit a4e2324
Show file tree
Hide file tree
Showing 7 changed files with 382 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 @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31912.275
MinimumVisualStudioVersion = 10.0.40219.1
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "30 - VirtualList", "Scratch\30 - VirtualList\30 - VirtualList.fsproj", "{F18A02B9-F0FA-42A6-91A8-88203E0D59B8}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "28 - Giraffe", "Scratch\28 - Giraffe\28 - Giraffe.fsproj", "{1A788AF4-257B-4C4C-8C5A-E379373B5838}"
EndProject
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "27 - NavigationDemoNew", "Scratch\27 - NavigationDemoNew\27 - NavigationDemoNew.fsproj", "{5BD4DD0F-92E4-4EE9-A9C0-AE3E187B2F92}"
Expand Down Expand Up @@ -154,6 +156,10 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{F18A02B9-F0FA-42A6-91A8-88203E0D59B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F18A02B9-F0FA-42A6-91A8-88203E0D59B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F18A02B9-F0FA-42A6-91A8-88203E0D59B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F18A02B9-F0FA-42A6-91A8-88203E0D59B8}.Release|Any CPU.Build.0 = Release|Any CPU
{1A788AF4-257B-4C4C-8C5A-E379373B5838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1A788AF4-257B-4C4C-8C5A-E379373B5838}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1A788AF4-257B-4C4C-8C5A-E379373B5838}.Release|Any CPU.ActiveCfg = Release|Any CPU
Expand Down Expand Up @@ -399,6 +405,7 @@ Global
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F18A02B9-F0FA-42A6-91A8-88203E0D59B8} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
{1A788AF4-257B-4C4C-8C5A-E379373B5838} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
{5BD4DD0F-92E4-4EE9-A9C0-AE3E187B2F92} = {49FCD64D-3937-4F2E-BA36-D5B1837D4E5F}
{70C08F03-F4A5-414A-ABD6-CC07DC42E07C} = {2FE8734A-BF34-43DE-8731-5D580FE4E83B}
Expand Down
28 changes: 28 additions & 0 deletions src/Scratch/30 - VirtualList/30 - VirtualList.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<DisableImplicitFSharpCoreReference>True</DisableImplicitFSharpCoreReference>
<AssemblyName>$(MSBuildProjectName.Replace(" ", "_"))</AssemblyName>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\..\bin\Debug\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\..\bin\Release\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Include="Model.fs" />
<Compile Include="App.fs" />
<Compile Include="Program.fs" />
<None Include="App.config" />
<None Include="paket.references" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Aardvark.Service\Aardvark.Service.fsproj" />
<ProjectReference Include="..\..\Aardvark.UI.Primitives\Aardvark.UI.Primitives.fsproj" />
<ProjectReference Include="..\..\Aardvark.UI\Aardvark.UI.fsproj" />
</ItemGroup>
<Import Project="..\..\..\.paket\Paket.Restore.targets" />
</Project>
6 changes: 6 additions & 0 deletions src/Scratch/30 - VirtualList/App.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>
207 changes: 207 additions & 0 deletions src/Scratch/30 - VirtualList/App.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
module VirtualListExample.App
open Aardvark.UI
open Aardvark.UI.Primitives

open Aardvark.Base
open FSharp.Data.Adaptive
open VirtualListExample.Model

module VirtualList =

// Channel that triggers whenever the adaptive value changes (ignoring the actual value)
type private SignalChannelReader<'T>(value : aval<'T>) =
inherit ChannelReader()

override x.Release() =()
override x.ComputeMessages t =
value.GetValue t |> ignore
[ "0" ]

type private SignalChannel<'T>(value : aval<'T>) =
inherit Channel()
override x.GetReader() = new SignalChannelReader<_>(value) :> ChannelReader


let update (message : VirtualList.Message) (list : VirtualList<'T>) =
match message with
| VirtualList.Message.Resize height ->
Log.line "%A" height
{ list with height = height }

| VirtualList.Message.Scroll offset ->
Log.line "Scroll %A" offset
{ list with scrollOffset = offset }

let view (message : VirtualList.Message -> 'msg) (itemNode : 'T -> DomNode<'msg>) (list : AdaptiveVirtualList<'T, _, _>) : DomNode<'msg> =
let onUpdate =
onEvent "onupdate" [ ] (
List.head >> Pickler.unpickleOfJson >> VirtualList.Message.Resize >> message
)

let onScroll =
onEvent "onscroll" [ "event.target.scrollTop" ] (
List.head >> Pickler.unpickleOfJson >> VirtualList.Message.Scroll >> message
)

let bootJs =
String.concat "" [
"const self = $('#__ID__')[0];"

"const getItemHeight = () => {"
" const $item = $(self).children('.item:not(.virtual):first');"
" return $item.length > 0 ? $item[0].clientHeight : 0;"
"};"

"let lastHeight = '';"
"const updateHeight = () => {"
" const height = {"
" itemHeight: getItemHeight(),"
" clientHeight: self.clientHeight"
" };"

" if (height.itemHeight !== lastHeight.itemHeight ||"
" height.clientHeight !== lastHeight.clientHeight)"
" {"
" aardvark.processEvent('__ID__', 'onupdate', height);"
" lastHeight = height;"
" }"
"};"

"const scrollTo = (offset) => {"
" self.scrollTo({ top: offset, behavior: 'smooth'});"
"};"
"scrollCh.onmessage = scrollTo;"

// Client and item height may change, update it when the elements or the size change
"elementsCh.onmessage = updateHeight;"
"const resizeObserver = new ResizeObserver(updateHeight);"
"resizeObserver.observe(self);"
]

let item (element : 'T) =
div [clazz "item"; style "color: white"] [itemNode element]

let virtualItem (height : int) =
div [clazz "item virtual"; style $"height: {height}px; visibility: hidden"] []

// Number of elements the window extends beyond the actual visible space
let border = 5

let elements =
alist {
let! itemHeight = list.height.itemHeight

if itemHeight = 0 then
let! elements = list.elements

// We do not know the item height yet, render two elements to determine it.
// Also we need the client height of the list, expand it to the max with a big virtual item.
if elements.Length > 0 then
yield virtualItem 9999

for i = 0 to (min elements.Length 2) - 1 do
yield item elements.[i]
else
let! elements = list.elements
let! clientHeight = list.height.clientHeight
let! scrollOffset = list.scrollOffset

let first = ((scrollOffset / itemHeight) - border) |> clamp 0 (elements.Length - 1)
let last = (first + (clientHeight / itemHeight) + 2 * border) |> min (elements.Length - 1)

if first > 0 then
yield virtualItem (first * itemHeight)

for i = first to last do
yield item elements.[i]

if last < elements.Length - 1 then
yield virtualItem ((elements.Length - last - 1) * itemHeight)
}

let channels : (string * Channel) list = [
"elementsCh", SignalChannel list.elements
"scrollCh", AVal.channel list.scrollTarget
]

onBoot' channels bootJs (
elements |> Incremental.div (AttributeMap.ofList [
clazz "ui inverted divided list"
style "margin: 10px; padding: 5px; height: 100%; max-height: 400px; overflow-y: auto; border-style: solid; border-width: 1px; border-color: gray"
onUpdate; onScroll
])
)

let private rnd = RandomSystem()

let update (model : Model) (msg : Message) =
match msg with
| SetCount count ->
{ model with count = count}

| Generate ->
let elements = Array.init model.count (fun i -> $"#{i}")
{ model with elements = model.elements |> VirtualList.set elements }

| Scroll ->
let target = model.elements |> VirtualList.length |> rnd.UniformInt
{ model with elements = model.elements |> VirtualList.scrollTo target }

| VirtualListAction message ->
{ model with elements = model.elements |> VirtualList.update message }

let view (model : AdaptiveModel) =
body [ style "background: #1B1C1E"] [
require Html.semui (
div [clazz "ui"; style "background: #1B1C1E; overflow-y: hidden"] [
div [clazz "ui right labeled input"; style "margin: 10px"] [
simplenumeric {
attributes [clazz "ui inverted input"]
value model.count
update SetCount
min 1
max 10000000
}

div [clazz "ui basic label"] [
text "Elements"
]
]

button [
clazz "ui button"
style "margin: 10px"
onClick (fun _ -> Generate)
] [
text "Generate"
]

button [
clazz "ui button"
style "margin: 10px"
onClick (fun _ -> Scroll)
] [
text "Scroll"
]

model.elements |> VirtualList.view VirtualListAction text
]
)
]


let threads (model : Model) =
ThreadPool.empty

let app =
{
unpersist = Unpersist.instance
threads = threads
initial =
{
count = 100
elements = VirtualList.empty
}
update = update
view = view
}
66 changes: 66 additions & 0 deletions src/Scratch/30 - VirtualList/Model.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
namespace VirtualListExample.Model

open Aardvark.Base
open FSharp.Data.Adaptive
open Adaptify

[<Struct; ModelType>]
type VirtualHeight =
{
itemHeight : int
clientHeight : int
}

[<ModelType>]
type VirtualList<'T> =
{
height : VirtualHeight
elements : 'T[]
scrollOffset : int
scrollTarget : int
}

module VirtualList =

[<RequireQualifiedAccess>]
type Message =
| Resize of height: VirtualHeight
| Scroll of offset: int

let inline length (list : VirtualList<'T>) =
list.elements.Length

let inline ofArray (elements :'T[]) =
{ height = Unchecked.defaultof<_>
elements = elements
scrollOffset = 0
scrollTarget = -1 }

let inline set (elements : 'T seq) (list : VirtualList<'T>) =
let elements =
match elements with
| :? array<'T> as arr -> arr
| _ -> Array.ofSeq elements

{ list with elements = elements }

let inline empty<'T> : VirtualList<'T> =
ofArray Array.empty

let inline scrollTo (index : int) (list : VirtualList<'T>) =
let offset = list.height.itemHeight * index
{ list with scrollTarget = offset }


type Message =
| SetCount of count: int
| Generate
| Scroll
| VirtualListAction of VirtualList.Message

[<ModelType>]
type Model =
{
count : int
elements : VirtualList<string>
}
49 changes: 49 additions & 0 deletions src/Scratch/30 - VirtualList/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
open System
open Aardvark.Base
open Aardvark.Application
open Aardvark.Application.Slim
open Aardvark.UI
open Aardium
open VirtualListExample

open Suave
open Suave.WebPart

[<EntryPoint; STAThread>]
let main argv =
Aardvark.Init()
Aardium.init()

use app = new OpenGlApplication()
let instance = App.app |> App.start

// use can use whatever suave server to start you mutable app.
// startServerLocalhost is one of the convinience functions which sets up
// a server without much boilerplate.
// there is also WebPart.startServer and WebPart.runServer.
// look at their implementation here: https://github.com/aardvark-platform/aardvark.media/blob/master/src/Aardvark.Service/Suave.fs#L10
// if you are unhappy with them, you can always use your own server config.
// the localhost variant does not require to allow the port through your firewall.
// the non localhost variant runs in 127.0.0.1 which enables remote acces (e.g. via your mobile phone)
WebPart.startServerLocalhost 4321 [
MutableApp.toWebPart' app.Runtime false instance
Suave.Files.browseHome
] |> ignore

Aardium.run {
url "http://localhost:4321/"
width 1024
height 768
debug true
}

//use ctrl = new AardvarkCefBrowser()
//ctrl.Dock <- DockStyle.Fill
//form.Controls.Add ctrl
//ctrl.StartUrl <- "http://localhost:4321/"
//ctrl.ShowDevTools()
//form.Text <- "Examples"
//form.Icon <- Icons.aardvark

//Application.Run form
0
Loading

0 comments on commit a4e2324

Please sign in to comment.