Skip to content

Commit

Permalink
- Environment State:
Browse files Browse the repository at this point in the history
    - allow values to not conform to IWritable. This makes dealing with
      values that don't change nicer.

- Component Base:
    - extract core component logic into reusable base class.
    - expose `ForceRender` method.

- DSL:
    - Panel add create
    - Text Block add padding overload

- TODO App:
    - Add modal host sample
  • Loading branch information
JaggerJo committed Jan 6, 2024
1 parent a4bb918 commit 7f88c91
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 116 deletions.
1 change: 1 addition & 0 deletions src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<Compile Include="Components\Context\Context.EffectsHook.fs" />
<Compile Include="Components\Context\Context.StateHook.fs" />
<Compile Include="Components\Context\Context.fs" />
<Compile Include="Components\ComponentBase.fs" />
<Compile Include="Components\Component.fs" />
<Compile Include="Hosts.fs" />
<Compile Include="DataTemplateView.fs" />
Expand Down
57 changes: 3 additions & 54 deletions src/Avalonia.FuncUI/Components/Component.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,61 +11,10 @@ open Avalonia.Threading
[<AllowNullLiteral>]
[<DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)>]
type Component (render: IComponentContext -> IView) as this =
inherit Border ()
let context = new Context(this)
let componentId = Guid.Unique
inherit ComponentBase ()

let mutable lastViewElement : IView option = None
let mutable lastViewAttrs: IAttr list = List.empty

member internal this.Context with get () = context
member internal this.ComponentId with get () = componentId

member private this.UIThreadUpdate() : unit =
let nextViewElement = Some (render context)

// reset internal context counter
context.AfterRender ()

// update view
VirtualDom.updateBorderRoot (this, lastViewElement, nextViewElement)
lastViewElement <- nextViewElement

let nextViewAttrs = context.ComponentAttrs

// update attrs
Patcher.patch (
this,
{ Delta.ViewDelta.ViewType = typeof<Border>
Delta.ViewDelta.ConstructorArgs = null
Delta.ViewDelta.KeyDidChange = false
Delta.ViewDelta.Outlet = ValueNone
Delta.ViewDelta.Attrs = Differ.diffAttributes (lastViewAttrs, nextViewAttrs) }
)

lastViewAttrs <- nextViewAttrs

context.EffectQueue.ProcessAfterRender ()

member private this.Update () : unit =
Dispatcher.UIThread.Post (fun _ -> this.UIThreadUpdate ())

override this.OnInitialized () =
base.OnInitialized ()

(context :> IComponentContext).trackDisposable (
context.OnRender.Subscribe (fun _ ->
this.Update ()
)
)

this.UIThreadUpdate ()

override this.OnDetachedFromLogicalTree (eventArgs: Avalonia.LogicalTree.LogicalTreeAttachmentEventArgs) =
base.OnDetachedFromLogicalTree eventArgs
(context :> IDisposable).Dispose ()

override this.StyleKeyOverride = typeof<Border>
override this.Render ctx =
render ctx

type Component with

Expand Down
74 changes: 74 additions & 0 deletions src/Avalonia.FuncUI/Components/ComponentBase.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
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

[<AllowNullLiteral>]
[<AbstractClass>]
[<DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)>]
type ComponentBase() as this =
inherit Border ()
let context = new Context(this)
let componentId = Guid.Unique

let mutable lastViewElement : IView option = None
let mutable lastViewAttrs: IAttr list = List.empty

member internal this.Context with get () = context
member internal this.ComponentId with get () = componentId

abstract member Render : IComponentContext -> IView

member private this.UIThreadUpdate() : unit =
let nextViewElement = Some (this.Render context)

// reset internal context counter
context.AfterRender ()

// update view
VirtualDom.updateBorderRoot (this, lastViewElement, nextViewElement)
lastViewElement <- nextViewElement

let nextViewAttrs = context.ComponentAttrs

// update attrs
Patcher.patch (
this,
{ Delta.ViewDelta.ViewType = typeof<Border>
Delta.ViewDelta.ConstructorArgs = null
Delta.ViewDelta.KeyDidChange = false
Delta.ViewDelta.Outlet = ValueNone
Delta.ViewDelta.Attrs = Differ.diffAttributes (lastViewAttrs, nextViewAttrs) }
)

lastViewAttrs <- nextViewAttrs

context.EffectQueue.ProcessAfterRender ()

member private this.Update () : unit =
Dispatcher.UIThread.Post (fun _ -> this.UIThreadUpdate ())

member this.ForceRender () =
this.Update ()

override this.OnInitialized () =
base.OnInitialized ()

(context :> IComponentContext).trackDisposable (
context.OnRender.Subscribe (fun _ ->
this.Update ()
)
)

this.UIThreadUpdate ()

override this.OnDetachedFromLogicalTree (eventArgs: Avalonia.LogicalTree.LogicalTreeAttachmentEventArgs) =
base.OnDetachedFromLogicalTree eventArgs
(context :> IDisposable).Dispose ()

override this.StyleKeyOverride = typeof<Border>
14 changes: 8 additions & 6 deletions src/Avalonia.FuncUI/DSL/Base/Panel.fs
Original file line number Diff line number Diff line change
@@ -1,25 +1,27 @@
namespace Avalonia.FuncUI.DSL

[<AutoOpen>]
module Panel =
module Panel =
open Avalonia.Controls
open Avalonia.FuncUI.Types
open Avalonia.FuncUI.Builder
open Avalonia.Media.Immutable
open Avalonia.Media

let create (attrs: IAttr<Panel> list): IView<Panel> =
ViewBuilder.Create<Panel>(attrs)

type Panel with

static member children<'t when 't :> Panel>(value: IView list) : IAttr<'t> =
let getter : ('t -> obj) = (fun control -> control.Children :> obj)

AttrBuilder<'t>.CreateContentMultiple("Children", ValueSome getter, ValueNone, value)

static member background<'t when 't :> Panel>(value: IBrush) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<IBrush>(Panel.BackgroundProperty, value, ValueNone)

static member background<'t when 't :> Panel>(color: string) : IAttr<'t> =
color |> Color.Parse |> ImmutableSolidColorBrush |> Panel.background
color |> Color.Parse |> ImmutableSolidColorBrush |> Panel.background

static member background<'t when 't :> Panel>(color: Color) : IAttr<'t> =
color |> ImmutableSolidColorBrush |> Panel.background
33 changes: 18 additions & 15 deletions src/Avalonia.FuncUI/DSL/TextBlock.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,46 @@ namespace Avalonia.FuncUI.DSL
open Avalonia.Media.Immutable

[<AutoOpen>]
module TextBlock =
module TextBlock =
open Avalonia
open Avalonia.Controls
open Avalonia.Controls.Documents
open Avalonia.Media
open Avalonia.Media
open Avalonia.FuncUI.Builder
open Avalonia.FuncUI.Types

let create (attrs: IAttr<TextBlock> list): IView<TextBlock> =
ViewBuilder.Create<TextBlock>(attrs)

type TextBlock with

static member text<'t when 't :> TextBlock>(value: string) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<string>(TextBlock.TextProperty, value, ValueNone)

static member background<'t when 't :> TextBlock>(value: IBrush) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<IBrush>(TextBlock.BackgroundProperty, value, ValueNone)

static member background<'t when 't :> TextBlock>(color: string) : IAttr<'t> =
color |> Color.Parse |> ImmutableSolidColorBrush |> TextBlock.background

static member background<'t when 't :> TextBlock>(color: Color) : IAttr<'t> =
color |> ImmutableSolidColorBrush |> TextBlock.background

static member fontFamily<'t when 't :> TextBlock>(value: FontFamily) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<FontFamily>(TextBlock.FontFamilyProperty, value, ValueNone)

static member fontSize<'t when 't :> TextBlock>(value: double) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<double>(TextBlock.FontSizeProperty, value, ValueNone)

static member fontStyle<'t when 't :> TextBlock>(value: FontStyle) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<FontStyle>(TextBlock.FontStyleProperty, value, ValueNone)

static member fontWeight<'t when 't :> TextBlock>(value: FontWeight) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<FontWeight>(TextBlock.FontWeightProperty, value, ValueNone)

static member foreground<'t when 't :> TextBlock>(value: IBrush) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<IBrush>(TextBlock.ForegroundProperty, value, ValueNone)

static member foreground<'t when 't :> TextBlock>(color: string) : IAttr<'t> =
color |> Color.Parse |> ImmutableSolidColorBrush |> TextBlock.foreground

Expand All @@ -50,20 +50,23 @@ module TextBlock =

static member inlines<'t when 't :> TextBlock>(value: InlineCollection) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<InlineCollection>(TextBlock.InlinesProperty, value, ValueNone)

static member inlines<'t when 't :> TextBlock>(values: IView list (* TODO: Change to IView<Inline> *)) : IAttr<'t> =
let getter : ('t -> obj) = (fun control -> control.Inlines :> obj)
AttrBuilder<'t>.CreateContentMultiple("Inlines", ValueSome getter, ValueNone, values)

static member lineHeight<'t when 't :> TextBlock>(value: float) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<float>(TextBlock.LineHeightProperty, value, ValueNone)

static member maxLines<'t when 't :> TextBlock>(value: int) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<int>(TextBlock.MaxLinesProperty, value, ValueNone)

static member padding<'t when 't :> TextBlock>(value: Thickness) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<Thickness>(TextBlock.PaddingProperty, value, ValueNone)

static member padding<'t when 't :> TextBlock>(padding: float) : IAttr<'t> =
padding |> Thickness |> TextBlock.padding

static member padding<'t when 't :> TextBlock>(horizontal: float, vertical: float) : IAttr<'t> =
(horizontal, vertical) |> Thickness |> TextBlock.padding

Expand All @@ -78,6 +81,6 @@ module TextBlock =

static member textTrimming<'t when 't :> TextBlock>(value: TextTrimming) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<TextTrimming>(TextBlock.TextTrimmingProperty, value, ValueNone)

static member textWrapping<'t when 't :> TextBlock>(value: TextWrapping) : IAttr<'t> =
AttrBuilder<'t>.CreateProperty<TextWrapping>(TextBlock.TextWrappingProperty, value, ValueNone)
25 changes: 13 additions & 12 deletions src/Avalonia.FuncUI/Experimental/Experimental.EnvironmentState.fs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ open Avalonia.LogicalTree
type EnvironmentState<'value> =
internal {
Name: string
DefaultValue: IWritable<'value> option
DefaultValue: 'value option
}

static member Create (name: string, ?defaultValue: IWritable<'value>) : EnvironmentState<'value> =
static member Create (name: string, ?defaultValue: 'value) : EnvironmentState<'value> =
{ Name = name
DefaultValue = defaultValue }

[<AllowNullLiteral>]
type EnvironmentStateProvider<'value>
( state: EnvironmentState<'value>,
providedState: IWritable<'value>) as this =
providedState: 'value) =

inherit ContentControl ()

Expand All @@ -34,7 +34,7 @@ type EnvironmentStateProvider<'value>

type EnvironmentStateProvider<'value> with

static member create (state: EnvironmentState<'value>, providedValue: IWritable<'value>, content: IView) =
static member create (state: EnvironmentState<'value>, providedValue: 'value, content: IView) =
{ View.ViewType = typeof<EnvironmentStateProvider<'value>>
View.ViewKey = ValueNone
View.Attrs = [ ContentControl.content content ]
Expand All @@ -44,7 +44,7 @@ type EnvironmentStateProvider<'value> with

type EnvironmentState<'value> with

member this.provide (providedValue: IWritable<'value>, content: IView) =
member this.provide (providedValue: 'value, content: IView) =
EnvironmentStateProvider<'value>.create(this, providedValue, content)

[<RequireQualifiedAccess>]
Expand Down Expand Up @@ -73,14 +73,15 @@ module __ContextExtensions_useEnvHook =

type IComponentContext with

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
member this.readEnvValue(state: EnvironmentState<'value>) : 'value =
match EnvironmentStateConsumer.tryFind (this.control, state), state.DefaultValue with
| ValueSome value, _ -> value
| ValueNone, Some defaultValue -> defaultValue
| ValueNone, None -> failwithf "No value provided for environment value '%s'" state.Name

member this.useEnvState(state: EnvironmentState<IWritable<'value>>, ?renderOnChange: bool) : IWritable<'value> =
this.usePassedLazy (
obtainValue = obtainValue,
obtainValue = (fun () -> this.readEnvValue(state)),
?renderOnChange = renderOnChange
)

4 changes: 2 additions & 2 deletions src/Examples/Component Examples/Examples.EnvApp/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ open Avalonia.FuncUI.DSL
[<RequireQualifiedAccess>]
module SharedState =

let brush = EnvironmentState<IBrush>.Create "brush"
let size = EnvironmentState<int>.Create "size"
let brush = EnvironmentState<IWritable<IBrush>>.Create "brush"
let size = EnvironmentState<IWritable<int>>.Create "size"

[<AbstractClass; Sealed>]
type Views =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="ModalHost.fs" />
<Compile Include="Program.fs" />
<AvaloniaResource Include="Assets\Icons\edit.png" />
<AvaloniaResource Include="Assets\Icons\trash.png" />
Expand Down
Loading

0 comments on commit 7f88c91

Please sign in to comment.