diff --git a/src/Avalonia.FuncUI.sln b/src/Avalonia.FuncUI.sln
index bba84e6e..d498ba85 100644
--- a/src/Avalonia.FuncUI.sln
+++ b/src/Avalonia.FuncUI.sln
@@ -88,6 +88,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Elmish Examples", "Elmish E
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.Elmish.Tetris", "Examples\Elmish Examples\Examples.Elmish.Tetris\Examples.Elmish.Tetris.fsproj", "{EC63B886-E809-4B74-B533-BFF3D60017C9}"
EndProject
+Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.ComponentPlayground", "Examples\Examples.ComponentPlayground\Examples.ComponentPlayground.fsproj", "{FA4E81D2-1083-43E1-99E3-0F4EA31B3701}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -206,6 +208,10 @@ Global
{EC63B886-E809-4B74-B533-BFF3D60017C9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EC63B886-E809-4B74-B533-BFF3D60017C9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EC63B886-E809-4B74-B533-BFF3D60017C9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FA4E81D2-1083-43E1-99E3-0F4EA31B3701}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FA4E81D2-1083-43E1-99E3-0F4EA31B3701}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FA4E81D2-1083-43E1-99E3-0F4EA31B3701}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FA4E81D2-1083-43E1-99E3-0F4EA31B3701}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -242,6 +248,7 @@ Global
{70BDCE72-149A-435A-9910-E79DE329F978} = {F50826CE-D9BC-45CF-A110-C42225B75AD3}
{6D2C62FC-5634-4997-AF1F-2E8A5D27E117} = {84811DB3-C276-4F0D-B3BA-78B88E2C6EF0}
{EC63B886-E809-4B74-B533-BFF3D60017C9} = {6D2C62FC-5634-4997-AF1F-2E8A5D27E117}
+ {FA4E81D2-1083-43E1-99E3-0F4EA31B3701} = {F50826CE-D9BC-45CF-A110-C42225B75AD3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4630E817-6780-4C98-9379-EA3B45224339}
diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj
index 64cdb065..99c9bf4b 100644
--- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj
+++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj
@@ -56,6 +56,7 @@
+
@@ -169,6 +170,7 @@
+
diff --git a/src/Avalonia.FuncUI/Components/Component.fs b/src/Avalonia.FuncUI/Components/Component.fs
index ad313375..035db30f 100644
--- a/src/Avalonia.FuncUI/Components/Component.fs
+++ b/src/Avalonia.FuncUI/Components/Component.fs
@@ -1,12 +1,8 @@
namespace Avalonia.FuncUI
-open System
open System.Diagnostics.CodeAnalysis
-open Avalonia.Controls
open Avalonia.FuncUI
open Avalonia.FuncUI.Types
-open Avalonia.FuncUI.VirtualDom
-open Avalonia.Threading
[]
[]
@@ -14,14 +10,4 @@ type Component (render: IComponentContext -> IView) as this =
inherit ComponentBase ()
override this.Render ctx =
- render ctx
-
-type Component with
-
- static member create(key: string, render: IComponentContext -> IView) : IView =
- { View.ViewType = typeof
- View.ViewKey = ValueSome key
- View.Attrs = list.Empty
- View.Outlet = ValueNone
- View.ConstructorArgs = [| render :> obj |] }
- :> IView
\ No newline at end of file
+ render ctx
\ No newline at end of file
diff --git a/src/Avalonia.FuncUI/Components/Lib/Lib.Common.fs b/src/Avalonia.FuncUI/Components/Lib/Lib.Common.fs
index 9af5016a..20ca6e3d 100644
--- a/src/Avalonia.FuncUI/Components/Lib/Lib.Common.fs
+++ b/src/Avalonia.FuncUI/Components/Lib/Lib.Common.fs
@@ -2,6 +2,7 @@ namespace Avalonia.FuncUI
open System
open System.Collections.Generic
+open Microsoft.FSharp.Core
[]
module internal ComponentHelpers =
@@ -38,3 +39,32 @@ module internal CommonExtensions =
type Guid with
member this.StringValue with get () = this.ToString()
static member Unique with get () = Guid.NewGuid()
+
+
+[]
+module internal RenderFunctionAnalysis =
+ open System
+ open System.Reflection
+ open System.Collections.Concurrent
+
+ let internal cache = ConcurrentDictionary()
+
+ let private flags =
+ BindingFlags.Instance |||
+ BindingFlags.NonPublic |||
+ BindingFlags.Public
+
+ let capturesState (func : obj) : bool =
+ let type' = func.GetType()
+
+ let hasValue, value = cache.TryGetValue type'
+
+ match hasValue with
+ | true -> value
+ | false ->
+ let capturesState =
+ type'.GetConstructors(flags)
+ |> Array.map (fun info -> info.GetParameters().Length)
+ |> Array.exists (fun parameterLength -> parameterLength > 0)
+
+ cache.AddOrUpdate(type', capturesState, (fun identifier lastValue -> capturesState))
\ No newline at end of file
diff --git a/src/Avalonia.FuncUI/DSL/Component.fs b/src/Avalonia.FuncUI/DSL/Component.fs
new file mode 100644
index 00000000..bd3fa6c6
--- /dev/null
+++ b/src/Avalonia.FuncUI/DSL/Component.fs
@@ -0,0 +1,18 @@
+[]
+module Avalonia.FuncUI.DSL.__ComponentExtensions
+
+open Avalonia.FuncUI
+open Avalonia.FuncUI.Builder
+open Avalonia.FuncUI.Types
+
+type Component with
+
+ static member create(key: string, render: IComponentContext -> IView) : IView =
+ { View.ViewType = typeof
+ View.ViewKey = ValueSome key
+ View.Attrs = [
+ //Component.renderFunction render
+ ]
+ View.Outlet = ValueNone
+ View.ConstructorArgs = [| render :> obj |] }
+ :> IView
\ No newline at end of file
diff --git a/src/Avalonia.FuncUI/Experimental/Experimental.ClosureComponent.fs b/src/Avalonia.FuncUI/Experimental/Experimental.ClosureComponent.fs
new file mode 100644
index 00000000..0399397e
--- /dev/null
+++ b/src/Avalonia.FuncUI/Experimental/Experimental.ClosureComponent.fs
@@ -0,0 +1,79 @@
+namespace Avalonia.FuncUI.Experimental
+
+open System
+open System.Diagnostics.CodeAnalysis
+open Avalonia
+open Avalonia.FuncUI
+open Avalonia.FuncUI.Types
+open Avalonia.FuncUI.Builder
+
+/// Component that works well with a render function that captures state.
+[]
+[]
+type ClosureComponent (render: IComponentContext -> IView) as this =
+ inherit ComponentBase ()
+
+ static let _RenderFunctionProperty =
+ AvaloniaProperty.RegisterDirect IView>(
+ name = "RenderFunction",
+ getter = Func IView>(_.RenderFunction),
+ setter = (fun this value -> this.RenderFunction <- value)
+ )
+
+ static do
+ let _ = _RenderFunctionProperty.Changed.AddClassHandler IView>(fun this e ->
+ let capturesState = RenderFunctionAnalysis.capturesState(e.NewValue.Value)
+
+ if capturesState then
+ this.ForceRender()
+ )
+ ()
+
+ let mutable _renderFunction = render
+
+ static member RenderFunctionProperty = _RenderFunctionProperty
+
+ member this.RenderFunction
+ with get() = _renderFunction
+ and set(value) =
+ let oldValue = _renderFunction
+ _renderFunction <- value
+ let _ = this.RaisePropertyChanged(ClosureComponent.RenderFunctionProperty, oldValue, value)
+ ()
+
+ override this.Render ctx =
+ _renderFunction ctx
+
+
+
+[]
+module __ClosureComponentExtensions =
+
+ type ClosureComponent with
+
+ static member internal renderFunction<'t when 't :> ClosureComponent>(value: IComponentContext -> IView) : IAttr<'t> =
+ AttrBuilder<'t>.CreateProperty IView>(ClosureComponent.RenderFunctionProperty, value, ValueNone)
+
+ static member create(key: string, render: IComponentContext -> IView) : IView =
+ let view: View =
+ { View.ViewType = typeof
+ View.ViewKey = ValueSome key
+ View.Attrs = [
+ ClosureComponent.renderFunction render
+ ]
+ View.Outlet = ValueNone
+ View.ConstructorArgs = [| render :> obj |] }
+
+ view :> IView
+
+ static member create(render: IComponentContext -> IView) : IView =
+ let view: View =
+ { View.ViewType = typeof
+ View.ViewKey = ValueNone
+ View.Attrs = [
+ ClosureComponent.renderFunction render
+ ]
+ View.Outlet = ValueNone
+ View.ConstructorArgs = [| render :> obj |] }
+
+ view :> IView
\ No newline at end of file
diff --git a/src/Avalonia.FuncUI/Experimental/Experimental.EnvironmentState.fs b/src/Avalonia.FuncUI/Experimental/Experimental.EnvironmentState.fs
index a9d6f3ed..c8fec5a6 100644
--- a/src/Avalonia.FuncUI/Experimental/Experimental.EnvironmentState.fs
+++ b/src/Avalonia.FuncUI/Experimental/Experimental.EnvironmentState.fs
@@ -68,17 +68,27 @@ module private EnvironmentStateConsumer =
else
tryFindNext (ancestor, state)
+
[]
-module __ContextExtensions_useEnvHook =
+module __Control_useEnvValue =
- type IComponentContext with
+ type Control with
member this.readEnvValue(state: EnvironmentState<'value>) : 'value =
- match EnvironmentStateConsumer.tryFind (this.control, state), state.DefaultValue with
+ match EnvironmentStateConsumer.tryFind (this, state), state.DefaultValue with
| ValueSome value, _ -> value
| ValueNone, Some defaultValue -> defaultValue
| ValueNone, None -> failwithf "No value provided for environment value '%s'" state.Name
+
+[]
+module __ContextExtensions_useEnvHook =
+
+ type IComponentContext with
+
+ member this.readEnvValue(state: EnvironmentState<'value>) : 'value =
+ this.control.readEnvValue(state)
+
member this.useEnvState(state: EnvironmentState>, ?renderOnChange: bool) : IWritable<'value> =
this.usePassedLazy (
obtainValue = (fun () -> this.readEnvValue(state)),
diff --git a/src/Avalonia.FuncUI/Library.fs b/src/Avalonia.FuncUI/Library.fs
index a5d75a1e..221f78fc 100644
--- a/src/Avalonia.FuncUI/Library.fs
+++ b/src/Avalonia.FuncUI/Library.fs
@@ -44,10 +44,10 @@ module internal Extensions =
let handler = EventHandler<'args>(fun _ e ->
observer.OnNext e
)
-
+
// subscribe to event changes so they can be pushed to subscribers
this.AddDisposableHandler(routedEvent, handler, routedEvent.RoutingStrategies)
)
-
+
{ new IObservable<'args>
- with member this.Subscribe(observer: IObserver<'args>) = sub.Invoke(observer) }
\ No newline at end of file
+ with member this.Subscribe(observer: IObserver<'args>) = sub.Invoke(observer) }
diff --git a/src/Examples/Component Examples/Examples.TodoApp/ModalHost.fs b/src/Examples/Component Examples/Examples.TodoApp/ModalHost.fs
index 3214209d..343cec35 100644
--- a/src/Examples/Component Examples/Examples.TodoApp/ModalHost.fs
+++ b/src/Examples/Component Examples/Examples.TodoApp/ModalHost.fs
@@ -102,7 +102,4 @@ module __ContextExtensions_useModal =
type IComponentContext with
member this.useModalState() : ModalHostState =
- this.readEnvValue ModalHost.State
-
-
-
+ this.readEnvValue ModalHost.State
\ No newline at end of file
diff --git a/src/Examples/Examples.ComponentPlayground/Examples.ComponentPlayground.fsproj b/src/Examples/Examples.ComponentPlayground/Examples.ComponentPlayground.fsproj
new file mode 100644
index 00000000..0f3d1a0a
--- /dev/null
+++ b/src/Examples/Examples.ComponentPlayground/Examples.ComponentPlayground.fsproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Examples/Examples.ComponentPlayground/Program.fs b/src/Examples/Examples.ComponentPlayground/Program.fs
new file mode 100644
index 00000000..3c80e77d
--- /dev/null
+++ b/src/Examples/Examples.ComponentPlayground/Program.fs
@@ -0,0 +1,121 @@
+namespace Examples.CounterApp
+open System
+open Avalonia
+open Avalonia.Controls.ApplicationLifetimes
+open Avalonia.Media
+open Avalonia.Themes.Fluent
+open Avalonia.FuncUI.Hosts
+open Avalonia.FuncUI.Experimental
+open Avalonia.Controls
+
+module Helpers =
+
+ let randomColor () : string =
+ String.Format("#{0:X6}", Random.Shared.Next(0x1000000));
+
+module Main =
+ open Avalonia.Controls
+ open Avalonia.FuncUI
+ open Avalonia.FuncUI.DSL
+ open Avalonia.Layout
+
+ let counterViewNegativeIndicator () =
+ Component.create ("indicator", fun _ ->
+ TextBlock.create [
+ TextBlock.fontSize 24.0
+ TextBlock.verticalAlignment VerticalAlignment.Center
+ TextBlock.horizontalAlignment HorizontalAlignment.Center
+ TextBlock.background (SolidColorBrush.Parse (Helpers.randomColor()))
+ TextBlock.text "Negative"
+ ]
+ )
+
+ let counterView (count: int) =
+ ClosureComponent.create (fun ctx ->
+ let innerCount = ctx.useState count
+
+ ctx.useEffect (
+ handler = (fun () ->
+ Console.WriteLine($"CounterView rendered (count: {count}, innerCount: {innerCount.Current})")
+
+ if innerCount.Current <> count then
+ innerCount .= count
+ ),
+ triggers = [ EffectTrigger.AfterRender ]
+ )
+
+ StackPanel.create [
+ StackPanel.dock Dock.Top
+ StackPanel.spacing 5
+ StackPanel.orientation Orientation.Horizontal
+ StackPanel.horizontalAlignment HorizontalAlignment.Center
+ StackPanel.children [
+
+ TextBlock.create [
+ TextBlock.fontSize 48.0
+ TextBlock.verticalAlignment VerticalAlignment.Center
+ TextBlock.horizontalAlignment HorizontalAlignment.Center
+ TextBlock.text (string count)
+ ]
+
+ if count < 0 then
+ counterViewNegativeIndicator ()
+ ]
+ ]
+ )
+
+ let view () =
+ Component (fun ctx ->
+ let state = ctx.useState 0
+
+ DockPanel.create [
+ DockPanel.children [
+ Button.create [
+ Button.dock Dock.Bottom
+ Button.onClick (fun _ -> state.Current - 1 |> state.Set)
+ Button.content "-"
+ Button.horizontalAlignment HorizontalAlignment.Stretch
+ ]
+ Button.create [
+ Button.dock Dock.Bottom
+ Button.onClick (fun _ -> state.Current + 1 |> state.Set)
+ Button.content "+"
+ Button.horizontalAlignment HorizontalAlignment.Stretch
+ ]
+ counterView state.Current
+ ]
+ ]
+ )
+
+
+type MainWindow() =
+ inherit HostWindow()
+ do
+ base.Title <- "Counter Example"
+ base.Height <- 400.0
+ base.Width <- 400.0
+ base.Content <- Main.view ()
+
+type App() =
+ inherit Application()
+
+ override this.Initialize() =
+ this.Styles.Add (FluentTheme())
+ this.RequestedThemeVariant <- Styling.ThemeVariant.Dark
+
+ override this.OnFrameworkInitializationCompleted() =
+ match this.ApplicationLifetime with
+ | :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
+ let mainWindow = MainWindow()
+ desktopLifetime.MainWindow <- mainWindow
+ | _ -> ()
+
+module Program =
+
+ []
+ let main(args: string[]) =
+ AppBuilder
+ .Configure()
+ .UsePlatformDetect()
+ .UseSkia()
+ .StartWithClassicDesktopLifetime(args)