Skip to content

Commit

Permalink
Allows controls to pass state to descendants (similar to swift UI env…
Browse files Browse the repository at this point in the history
…ironment objects & react contexts) (#278)
  • Loading branch information
JaggerJo authored Mar 10, 2023
1 parent 02b62ca commit 5627ca6
Show file tree
Hide file tree
Showing 7 changed files with 376 additions and 3 deletions.
7 changes: 7 additions & 0 deletions src/Avalonia.FuncUI.sln
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples.Mobile", "Examples
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.Mobile.iOS", "Examples\Examples.Mobile.iOS\Examples.Mobile.iOS.fsproj", "{44E60326-957D-4022-8E67-669161AFF957}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.EnvApp", "Examples\Component Examples\Examples.EnvApp\Examples.EnvApp.fsproj", "{90E18FDC-8B2A-4C21-B60D-B5B7A14648A3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -161,6 +163,10 @@ Global
{44E60326-957D-4022-8E67-669161AFF957}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44E60326-957D-4022-8E67-669161AFF957}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44E60326-957D-4022-8E67-669161AFF957}.Release|Any CPU.Build.0 = Release|Any CPU
{90E18FDC-8B2A-4C21-B60D-B5B7A14648A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90E18FDC-8B2A-4C21-B60D-B5B7A14648A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90E18FDC-8B2A-4C21-B60D-B5B7A14648A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90E18FDC-8B2A-4C21-B60D-B5B7A14648A3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -188,6 +194,7 @@ Global
{01631D41-54F9-4815-8B26-9F1BEEC22F7A} = {5DB968C6-935E-41AC-A252-644B4886AF8A}
{AD6796F9-45CA-4FBA-9673-E6FC9214C80D} = {84811DB3-C276-4F0D-B3BA-78B88E2C6EF0}
{44E60326-957D-4022-8E67-669161AFF957} = {AD6796F9-45CA-4FBA-9673-E6FC9214C80D}
{90E18FDC-8B2A-4C21-B60D-B5B7A14648A3} = {F50826CE-D9BC-45CF-A110-C42225B75AD3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {4630E817-6780-4C98-9379-EA3B45224339}
Expand Down
1 change: 1 addition & 0 deletions src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
<Compile Include="DSL\TickBar.fs" />
<Compile Include="DSL\Viewbox.fs" />
<Compile Include="DSL\SelectableTextBlock.fs" />
<Compile Include="Environment.fs" />
</ItemGroup>

<ItemGroup>
Expand Down
29 changes: 27 additions & 2 deletions src/Avalonia.FuncUI/Components/Context/Context.fs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ type IComponentContext =
/// <example>
/// <code>
/// Component (fun ctx ->
/// (* expensive operation that should only happen once on initialisation *)
/// (* expensive operation that should only happen once on initialisation *)
/// let count = ctx.useStateLazy (fun () -> Math.Sqrt(42))
/// ..
/// // id will have the same value during the whole component lifetime. (unless changed via 'id.Set ..')
Expand Down Expand Up @@ -88,6 +88,23 @@ type IComponentContext =
/// <param name="renderOnChange">re-render component on change (default: true).</param>
abstract usePassed<'value> : value: IWritable<'value> * ?renderOnChange: bool -> IWritable<'value>

/// <summary>
/// Attaches a passed <c>IWritable&lt;'value&gt;</c> value to the component context.
/// <example>
/// <code>
/// let countView (count: IWritable&lt;int&gt;) =
/// Component (fun ctx ->
/// // attach passed writable value to the component context
/// let count = ctx.usePassedLazy (fun _ -> (* expensive function that returns IWritable*))
/// ..
/// )
/// </code>
/// </example>
/// </summary>
/// <param name="obtainValue">(expensive) function that returns an IWritable.</param>
/// <param name="renderOnChange">re-render component on change (default: true).</param>
abstract usePassedLazy<'value> : obtainValue: (unit -> IWritable<'value>) * ?renderOnChange: bool -> IWritable<'value>

/// <summary>
/// Attaches a passed <c>IReadable&lt;'value&gt;</c> value to the component context.
/// <example>
Expand Down Expand Up @@ -318,6 +335,15 @@ type Context (componentControl: Avalonia.Controls.Border) =
)
) :?> IWritable<'value>

member this.usePassedLazy (obtainState: unit -> IWritable<'value>, ?renderOnChange: bool) =
this.useStateHook<'value>(
StateHook.Create (
identity = callingIndex,
state = StateHookValue.Lazy (fun _ -> obtainState() :> IAnyReadable),
renderOnChange = defaultArg renderOnChange true
)
) :?> IWritable<'value>

member this.usePassedRead(value: IReadable<'t>, ?renderOnChange: bool) =
this.useStateHook<'value>(
StateHook.Create (
Expand Down Expand Up @@ -345,7 +371,6 @@ type Context (componentControl: Avalonia.Controls.Border) =
)
) :?> IWritable<'value>


member this.control = componentControl

interface IDisposable with
Expand Down
91 changes: 91 additions & 0 deletions src/Avalonia.FuncUI/Environment.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
namespace Avalonia.FuncUI

open System
open Avalonia.Controls
open Avalonia.FuncUI.Types
open Avalonia.FuncUI.DSL
open Avalonia.LogicalTree
open Avalonia.Styling
#nowarn "57"

type EnvironmentState<'value> =
internal {
Name: string
DefaultValue: IWritable<'value> option
}

[<Experimental "this feature is experimental. The API might change.">]
static member Create (name: string, ?defaultValue: IWritable<'value>) : EnvironmentState<'value> =
{ Name = name
DefaultValue = defaultValue }

[<Experimental "this feature is experimental. The API might change.">]
[<AllowNullLiteral>]
type EnvironmentStateProvider<'value>
( state: EnvironmentState<'value>,
providedState: IWritable<'value>) as this =

inherit ContentControl ()

member this.EnvironmentState with get () = state

member this.ProvidedState with get () = providedState

interface IStyleable with
member this.StyleKey = typeof<ContentControl>

type EnvironmentStateProvider<'value> with

[<Experimental "this feature is experimental. The API might change.">]
static member create (state: EnvironmentState<'value>, providedValue: IWritable<'value>, content: IView) =
{ View.ViewType = typeof<EnvironmentStateProvider<'value>>
View.ViewKey = ValueNone
View.Attrs = [ ContentControl.content content ]
View.Outlet = ValueNone
View.ConstructorArgs = [| state :> obj; providedValue :> obj |] }
:> IView<EnvironmentStateProvider<'value>>

type EnvironmentState<'value> with

[<Experimental "this feature is experimental. The API might change.">]
member this.provide (providedValue: IWritable<'value>, content: IView) =
EnvironmentStateProvider<'value>.create(this, providedValue, content)

[<RequireQualifiedAccess>]
module private EnvironmentStateConsumer =

let rec private tryFindNext (control: EnvironmentStateProvider<'value>, state: EnvironmentState<'value>) =
match control.FindLogicalAncestorOfType<EnvironmentStateProvider<'value>>(includeSelf = false) with
| null -> ValueNone
| ancestor ->
if ancestor.EnvironmentState.Name.Equals(state.Name, StringComparison.Ordinal) then
ValueSome ancestor.ProvidedState
else
tryFindNext (ancestor, state)

let tryFind (control: Control, state: EnvironmentState<'value>) =
match control.FindLogicalAncestorOfType<EnvironmentStateProvider<'value>>(includeSelf = false) with
| null -> ValueNone
| ancestor ->
if ancestor.EnvironmentState.Name.Equals(state.Name, StringComparison.Ordinal) then
ValueSome ancestor.ProvidedState
else
tryFindNext (ancestor, state)

[<AutoOpen>]
module __ContextExtensions_useEnvHook =

type IComponentContext with

[<Experimental "this feature is experimental. The API might change.">]
member this.useEnvState (state: EnvironmentState<'value>, ?renderOnChange: bool) =
let obtainValue () =
match EnvironmentStateConsumer.tryFind (this.control, state), state.DefaultValue with
| ValueSome value, _ -> value
| ValueNone, Some defaultValue -> defaultValue
| ValueNone, None -> failwithf "No value provided for environment state '%s'" state.Name

this.usePassedLazy (
obtainValue = obtainValue,
?renderOnChange = renderOnChange
)
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<AvaloniaVersion>11.0.0-preview5</AvaloniaVersion>
<FuncUIVersion>0.6.0-preview8</FuncUIVersion>
<FuncUIVersion>0.6.0-preview9</FuncUIVersion>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="Program.fs"/>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="$(AvaloniaVersion)" />
<ProjectReference Include="..\..\..\Avalonia.FuncUI.Elmish\Avalonia.FuncUI.Elmish.fsproj" />
<ProjectReference Include="..\..\..\Avalonia.FuncUI\Avalonia.FuncUI.fsproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 5627ca6

Please sign in to comment.