fsprojects / Avalonia.FuncUI

Develop cross-plattform GUI Applications using F# and Avalonia!
https://funcui.avaloniaui.net/
MIT License
948 stars 74 forks source link

Flyout component support #227

Closed picolino closed 1 year ago

picolino commented 1 year ago

According to docs (https://docs.avaloniaui.net/docs/controls/flyouts) Avalonia has Flyout components, but in Avalonia.FuncUI no implementation for that component, so I tried to add it by myself and I've got a problem that I'm stuck with.

I've implemented basic frame for new component:

namespace Avalonia.FuncUI.DSL

[<AutoOpen>]
module Flyout =
    open Avalonia.Controls
    open Avalonia.FuncUI.Types
    open Avalonia.FuncUI.Builder
    open Avalonia.Controls.Primitives

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

    type FlyoutBase with

        /// <summary>
        /// A value indicating whether the flyout is visible.
        /// </summary>
        static member isOpen<'t when 't :> FlyoutBase>(value: bool) : IAttr<'t> =
            AttrBuilder<'t>.CreateProperty<bool>(FlyoutBase.IsOpenProperty, value, ValueNone)

        /// <summary>
        /// A value indicating how the flyout is positioned.
        /// </summary>
        static member placement<'t when 't :> FlyoutBase>(value: FlyoutPlacementMode) : IAttr<'t> =
            AttrBuilder<'t>.CreateProperty<FlyoutPlacementMode>(Flyout.PlacementProperty, value, ValueNone)

        /// <summary>
        /// A value indicating flyout show mode.
        /// </summary>
        static member showMode<'t when 't :> FlyoutBase>(value: FlyoutShowMode) : IAttr<'t> =
            AttrBuilder<'t>.CreateProperty<FlyoutShowMode>(Flyout.ShowModeProperty, value, ValueNone)

    type Flyout with

        static member content<'t when 't :> Flyout>(value: IView option) : IAttr<'t> =
            AttrBuilder<'t>.CreateContentSingle(Flyout.ContentProperty, value)

        static member content<'t when 't :> Flyout>(value: IView) : IAttr<'t> =
            value
            |> Some
            |> Flyout.content

    type Button with

        /// <summary>
        /// A value for flyout view placement for button.
        /// </summary>
        static member flyout<'t when 't :> Button>(value: IView option) : IAttr<'t> =
            AttrBuilder<'t>.CreateContentSingle(Button.FlyoutProperty, value)

        /// <summary>
        /// A value for flyout view placement for button.
        /// </summary>
        static member flyout<'t when 't :> Button>(value: IView) : IAttr<'t> =
            value
            |> Some
            |> Button.flyout

And when I'm trying to use it:

Button.create [
    Button.content "Hello there"
    Button.flyout (
        Flyout.create [
            Flyout.isOpen true
            Flyout.content (
                TextBox.create [
                    TextBox.text "Hello there"
                ]
            )
        ]
    )
]

I've got an error:

System.InvalidCastException: Unable to cast object of type 'Avalonia.Controls.Flyout' to type 'Avalonia.Controls.IControl'.
   at Avalonia.FuncUI.VirtualDom.Patcher.create(ViewDelta viewElement)
   at Avalonia.FuncUI.VirtualDom.Patcher.patch_avalonia@165(IControl view, FSharpOption`1 viewElement, AvaloniaProperty property)
   at Avalonia.FuncUI.VirtualDom.Patcher.patchContentSingle(IControl view, Accessor accessor, FSharpOption`1 viewElement)
   ...

Library can't render Button.flyout because Flyout class doesn't implement IControl interface (reference).

For now I don't see any possible workarounds to use this control with FuncUI library, also IControl interface has many references in core of FuncUI library, so I don't see any quick fixes for that. Any thoughts?

sleepyfran commented 1 year ago

Since #220 we should now support creating any IAvaloniaObject (which I see the Flyout implements). Maybe try to change the declaration of the flyout method to:

static member flyout<'t when 't :> Button>(value: IView option) : IAttr<'t> =
    let getter : ('t -> obj) = (fun control -> control.Flyout :> obj)
    AttrBuilder<'t>.CreateContentSingle("Flyout", ValueSome getter, ValueNone, values)

Not sure if it'd work exactly like this since I haven't tested it, but that's the workaround we have for inlines, which are also not Controls but can be put inside of an IView (see here).

I'll be able to take a look at this later, but bindings for this would be awesome, thanks for the report! 😃

picolino commented 1 year ago

@sleepyfran Tried your suggestion, but still got same error, but from a little bit different place (see stack trace)

System.InvalidCastException: Unable to cast object of type 'Avalonia.Controls.Flyout' to type 'Avalonia.Controls.IControl'.
   at Avalonia.FuncUI.VirtualDom.Patcher.create(ViewDelta viewElement)
   at Avalonia.FuncUI.VirtualDom.Patcher.patch_instance@179(IControl view, FSharpOption`1 viewElement, PropertyAccessor property)
   at Avalonia.FuncUI.VirtualDom.Patcher.patch(IControl view, ViewDelta viewElement)
   at Avalonia.FuncUI.VirtualDom.Patcher.create(ViewDelta viewElement)
   at Avalonia.FuncUI.VirtualDom.Patcher.patch_IList@90-1.Invoke(Int32 index, ViewDelta viewElement)
sleepyfran commented 1 year ago

@picolino just had time to try this out: are you in the latest master? I just copy-pasted your code and it's working. The only thing I removed is the isOpen property, since it's not settable and it errors; other than that the flyout appears when clicking the button. I modified the inline example and it works:

image

Feel free to create a PR with your changes if you manage to make it work, otherwise I'll try to open a PR whenever I get some time 😃

picolino commented 1 year ago

Oh, yeah, I used 0.5.3 version, when I've installed 0.6.0-preview3 version it start work like a charm. Thank you for help! I'll prepare PR for Flyout component support and will link with this issue then.

sleepyfran commented 1 year ago

Closed in #229.