Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Interactive use with FSI? #147

Closed
EHotwagner opened this issue Jan 1, 2021 · 8 comments
Closed

Interactive use with FSI? #147

EHotwagner opened this issue Jan 1, 2021 · 8 comments
Labels
question Further information is requested

Comments

@EHotwagner
Copy link

Happy new year,

I am currently looking at various gui frameworks to find one to use interactively with fsi. Preferable OS agnostic but windows might be sufficient. So naturally i looked at winforms first since it's supported out of the box. Works with VS2019 but not with VSCode because of dotnet/fsharp#9417 and Intellisense is still somewhat brittle.

So my question is, what would it take to get Avalonia and maybe funcui to work with fsi? Not sure how it could fit with the elmish way. I think one thing needed would be custom os specific event loops like https://stackoverflow.com/questions/5723823/fsi-wpf-event-loop since avalonia uses win32 windows on Win. I am just reading up on the Avalonia application lifecycle and any pointer/ideas would be helpful. Thx.

@AngelMunoz
Copy link
Contributor

Hi @EHotwagner this is an interesting question

I don't have any idea right now but the general gist is that FuncUI is a DSL over Avalonia controls, so the question would be more about

How to make Avalonia run in an F# script?

I just tried something but I get this error
image

here's the script

#r "nuget: Avalonia.Desktop, 0.9.11"
#r "nuget: JaggerJo.Avalonia.FuncUI, 0.4.1"
#r "nuget: JaggerJo.Avalonia.FuncUI.DSL, 0.4.3"
#r "nuget: JaggerJo.Avalonia.FuncUI.Elmish, 0.4.0"


open Elmish
open Avalonia
open Avalonia.Controls
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.Input
open Avalonia.Layout
open Avalonia.FuncUI
open Avalonia.FuncUI.Components.Hosts
open Avalonia.FuncUI.DSL
open Avalonia.FuncUI.Elmish

module Counter =
    
    type State = { count : int }
    let init = { count = 0 }

    type Msg = Increment | Decrement | Reset

    let update (msg: Msg) (state: State) : State =
        match msg with
        | Increment -> { state with count = state.count + 1 }
        | Decrement -> { state with count = state.count - 1 }
        | Reset -> init
    
    let view (state: State) (dispatch) =
        DockPanel.create [
            DockPanel.children [
                Button.create [
                    Button.dock Dock.Bottom
                    Button.onClick (fun _ -> dispatch Reset)
                    Button.content "reset"
                ]                
                Button.create [
                    Button.dock Dock.Bottom
                    Button.onClick (fun _ -> dispatch Decrement)
                    Button.content "-"
                ]
                Button.create [
                    Button.dock Dock.Bottom
                    Button.onClick (fun _ -> dispatch Increment)
                    Button.content "+"
                ]
                TextBlock.create [
                    TextBlock.dock Dock.Top
                    TextBlock.fontSize 48.0
                    TextBlock.verticalAlignment VerticalAlignment.Center
                    TextBlock.horizontalAlignment HorizontalAlignment.Center
                    TextBlock.text (string state.count)
                ]
            ]
        ]

type MainWindow() as this =
    inherit HostWindow()
    do
        base.Title <- "BasicTemplate"
        base.Width <- 400.0
        base.Height <- 400.0
        
        //this.VisualRoot.VisualRoot.Renderer.DrawFps <- true
        //this.VisualRoot.VisualRoot.Renderer.DrawDirtyRects <- true

#if DEBUG
        this.AttachDevTools(KeyGesture(Key.F12))
#endif

        Elmish.Program.mkSimple (fun () -> Counter.init) Counter.update Counter.view
        |> Program.withHost this
#if DEBUG
        |> Program.withConsoleTrace
#endif
        |> Program.run

        
type App() =
    inherit Application()

    override this.Initialize() =
        this.Styles.Load "avares://Avalonia.Themes.Default/DefaultTheme.xaml"
        this.Styles.Load "avares://Avalonia.Themes.Default/Accents/BaseDark.xaml"

    override this.OnFrameworkInitializationCompleted() =
        match this.ApplicationLifetime with
        | :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
            desktopLifetime.MainWindow <- MainWindow()
        | _ -> ()

let main(args: string[]) =
    AppBuilder
        .Configure<App>()
        .UsePlatformDetect()
        .UseSkia()
        .StartWithClassicDesktopLifetime(args)

main [||]
System.NotSupportedException: The invoked member is not supported in a dynamic assembly.
   at System.Reflection.Emit.InternalAssemblyBuilder.GetManifestResourceNames()
   at Avalonia.Shared.PlatformSupport.AssetLoader.AssemblyDescriptor..ctor(Assembly assembly) in D:\a\1\s\src\Shared\PlatformSupport\AssetLoader.cs:line 369
   at Avalonia.Shared.PlatformSupport.AssetLoader..ctor(Assembly assembly) in D:\a\1\s\src\Shared\PlatformSupport\AssetLoader.cs:line 36
   at Avalonia.Shared.PlatformSupport.StandardRuntimePlatformServices.Register(Assembly assembly) in D:\a\1\s\src\Shared\PlatformSupport\StandardRuntimePlatformServices.cs:line 14
   at Avalonia.AppBuilder.<>c.<.ctor>b__0_0(AppBuilder builder) in D:\a\1\s\src\Avalonia.DesktopRuntime\AppBuilder.cs:line 21
   at Avalonia.Controls.AppBuilderBase`1.<>c__DisplayClass44_0.<.ctor>b__0() in D:\a\1\s\src\Avalonia.Controls\AppBuilderBase.cs:line 75
   at Avalonia.Controls.AppBuilderBase`1.Setup() in D:\a\1\s\src\Avalonia.Controls\AppBuilderBase.cs:line 288
   at Avalonia.Controls.AppBuilderBase`1.SetupWithLifetime(IApplicationLifetime lifetime) in D:\a\1\s\src\Avalonia.Controls\AppBuilderBase.cs:line 165
   at Avalonia.ClassicDesktopStyleApplicationLifetimeExtensions.StartWithClassicDesktopLifetime[T](T builder, String[] args, ShutdownMode shutdownMode) in D:\a\1\s\src\Avalonia.Controls\ApplicationLifetimes\ClassicDesktopStyleApplicationLifetime.cs:line 127
   at <StartupCode$FSI_0002>.$FSI_0002.main@()
Stopped due to error

My suggestion would be to ask this question in the Avalonia repository itself, you can take the F# script as a sample

@AngelMunoz AngelMunoz added the question Further information is requested label Jan 1, 2021
@FoggyFinder
Copy link
Contributor

Quick search gives

https://github.com/moloneymb/AvaloniaLinuxFSIExample

This is for plain Avalonia but I suppose it shouldn't be too complicated to adopt it for FuncUi

If it won't be enough to make it work I'll try to take a look closer to this topic this evening or tomorrow.

@EHotwagner
Copy link
Author

Thx a lot for the quick answer. Not sure how that slipped by. Will report back when i got it to work.

@EHotwagner
Copy link
Author

EHotwagner commented Jan 6, 2021

This seems to work. Any suggestions for improvements?

#r "nuget: JaggerJo.Avalonia.FuncUI"
open Avalonia
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.Threading
open Avalonia.FuncUI
open FSharp.Compiler.Interactive

type App() =
    inherit Application()
    override this.Initialize() =
            this.Styles.Load "resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"
            this.Styles.Load "resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default"
    override x.OnFrameworkInitializationCompleted() =
        match x.ApplicationLifetime with
        | :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
            desktopLifetime.ShutdownMode <- Controls.ShutdownMode.OnExplicitShutdown
        | _ -> ()

let createApp(args) = 
     AppBuilder
            .Configure<App>()
            .UsePlatformDetect()
            .UseSkia()
            .StartWithClassicDesktopLifetime(args) |> ignore

let disp (f:unit -> 'a) = 
    Dispatcher.UIThread.InvokeAsync(f) 
    |> Async.AwaitTask 
    |> Async.RunSynchronously 

fsi.EventLoop <- {new IEventLoop with 
                        member x.Run() = 
                            createApp ([|""|])
                            false //dummy
                        member x.Invoke(f) = disp f
                        member x.ScheduleRestart() = () //dummy
                            }


//Test
let w1 = Controls.Window()
w1.Width <- 600.
w1.Height <- 800.
w1.Background <- Media.Brushes.Aquamarine
let button1 = Controls.Button()
button1.Width <- 400.
button1.Height <- 100.
button1.Background <- Media.Brushes.Red
button1.Content <- "huhu"
button1.Click |> Observable.subscribe (fun _ -> button1.Content <- "hahahahahah")
w1.Content <- button1
w1.Show()
w1.Hide()

@EHotwagner
Copy link
Author

EHotwagner commented Jan 6, 2021

Hmm, no longer works in a single fsx file and complains about application already running but Application.Current = null. Putting

module AvEventLoop
open Avalonia
open Avalonia.Controls.ApplicationLifetimes
open Avalonia.Threading
open Avalonia.FuncUI

type App() =
    inherit Application()
    override this.Initialize() =
            this.Styles.Load "resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"
            this.Styles.Load "resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default"
    override x.OnFrameworkInitializationCompleted() =
        match x.ApplicationLifetime with
        | :? IClassicDesktopStyleApplicationLifetime as desktopLifetime ->
            desktopLifetime.ShutdownMode <- Controls.ShutdownMode.OnExplicitShutdown
        | _ -> ()
let createApp(args) = 
     AppBuilder
            .Configure<App>()
            .UsePlatformDetect()
            .UseSkia()
            .StartWithClassicDesktopLifetime(args) |> ignore
let disp (f:unit -> 'a) = 
    Dispatcher.UIThread.InvokeAsync(f) 
    |> Async.AwaitTask 
    |> Async.RunSynchronously 

in a classlib, packing into a nuget and referencing in the fsx file:

#i @"nuget: C:\Users\ehotw\source\repos\EHotwagner\AVEventLoop\AVLoop\bin\Debug"
#r "nuget: AVLoop"
open Avalonia
open FSharp.Compiler.Interactive
open AvEventLoop

fsi.EventLoop <- {new IEventLoop with 
                        member x.Run() = 
                            createApp ([|""|])
                            false //dummy
                        member x.Invoke(f) = disp f
                        member x.ScheduleRestart() = () //dummy
                            }


//Test. Wait first for the event loop to spin up.
let w1 = Controls.Window()
w1.Width <- 600.
w1.Height <- 800.
w1.Background <- Media.Brushes.Aquamarine
let button1 = Controls.Button()
button1.Width <- 400.
button1.Height <- 100.
button1.Background <- Media.Brushes.Red
button1.Content <- "huhu"
button1.Click |> Observable.subscribe (fun _ -> button1.Content <- "hahahahahah")
w1.Content <- button1
w1.Show()
// w1.Hide()

seems ok.

@EHotwagner
Copy link
Author

EHotwagner commented Jan 6, 2021

Maybe need to make sure that the application shuts down correctly with fsi.

@EHotwagner
Copy link
Author

After installing the Eventloop and waiting for it to start up funcui is ready to go.

#load @"C:\Users\ehotw\source\repos\EHotwagner\AVEventLoop\AVLoop\EventLoop.fsx"
open Elmish
open Avalonia.FuncUI.Elmish
open Avalonia
open Avalonia.Controls
open Avalonia.Layout
open Avalonia.FuncUI
open Avalonia.FuncUI.Components.Hosts
open Avalonia.FuncUI.DSL

module Counter =
    
    type State = { count : int }
    let init = { count = 0 }

    type Msg = Increment | Decrement | Reset

    let update (msg: Msg) (state: State) : State =
        match msg with
        | Increment -> { state with count = state.count + 1 }
        | Decrement -> { state with count = state.count - 1 }
        | Reset -> init
    
    let view (state: State) (dispatch) =
        DockPanel.create [
            DockPanel.children [
                Button.create [
                    Button.dock Dock.Bottom
                    Button.onClick (fun _ -> dispatch Reset)
                    Button.content "reset"
                ]                
                Button.create [
                    Button.dock Dock.Bottom
                    Button.onClick (fun _ -> dispatch Decrement)
                    Button.content "-"
                ]
                Button.create [
                    Button.dock Dock.Bottom
                    Button.onClick (fun _ -> dispatch Increment)
                    Button.content "+"
                ]
                TextBlock.create [
                    TextBlock.dock Dock.Top
                    TextBlock.fontSize 48.0
                    TextBlock.verticalAlignment VerticalAlignment.Center
                    TextBlock.horizontalAlignment HorizontalAlignment.Center
                    TextBlock.text (string state.count)
                ]
            ]
        ]
type MainWindow() as this =
    inherit HostWindow()
    do
        base.Title <- "BasicTemplate"
        base.Width <- 400.0
        base.Height <- 400.0
        Elmish.Program.mkSimple (fun () -> Counter.init) Counter.update Counter.view
        |> Program.withHost this
        |> Program.run

let win = MainWindow()
win.Show()

Relevant points:

  • IEventLoop is only accessible from fsx files.
  • I was not able load the EventLoop fsx at fsi startup by: --use: since in that case fsi does not seem to know #i :nuget.
  • Having the execution wait for complete initialization would be nice.
  • It worked first in a single fsx file, then didnt.
  • fsac and fsi got sometimes confused with elmish and funcui.elmish. Explicitly adding Elmish reference to the classlib worked.
  • #i "nuget: " does not know relative paths.

@fwaris
Copy link

fwaris commented Jan 15, 2022

@EHotwagner thanks for the initial work on this. I have put the EventLoop in a nuget package AVLoop with a few enhancements. The repo includes samples for DataGrid and FuncUI.

Unfortunately, still need to install EventLoop in a separate submit to FSI. It seems FSI main loop thread needs to be free to actually start the event loop.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

4 participants