├── docs ├── .gitkeep ├── Interstellar sample app.gif ├── index.md └── _template.html ├── Examples ├── Examples.SharedCode │ ├── paket.references │ ├── Examples.SharedCode.fsproj │ └── Library.fs ├── Examples.Wpf.Chromium │ ├── paket.references │ ├── Examples.Wpf.Chromium.fsproj │ └── App.fs ├── Examples.macOS.WebKit │ ├── paket.references │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── AppIcon-128.png │ │ │ ├── AppIcon-16.png │ │ │ ├── AppIcon-256.png │ │ │ ├── AppIcon-32.png │ │ │ ├── AppIcon-512.png │ │ │ ├── AppIcon-128@2x.png │ │ │ ├── AppIcon-16@2x.png │ │ │ ├── AppIcon-256@2x.png │ │ │ ├── AppIcon-32@2x.png │ │ │ ├── AppIcon-512@2x.png │ │ │ └── Contents.json │ ├── Entitlements.plist │ ├── Main.fs │ ├── AppDelegate.fs │ ├── Info.plist │ └── Examples.macOS.WebKit.fsproj └── Examples.WinForms.Chromium │ ├── paket.references │ ├── Examples.WinForms.Chromium.fsproj │ └── App.fs ├── src ├── Interstellar.MacOS.WebKit │ ├── paket.references │ ├── BrowserApp.fs │ ├── Interstellar.MacOS.WebKit.fsproj │ ├── BrowserWindow.fs │ └── Browser.fs ├── Interstellar.Chromium │ ├── paket.references │ ├── Interstellar.Chromium.fsproj │ ├── JavascriptInjectionFilter.fs │ └── Browser.fs ├── Interstellar.Wpf.Chromium │ ├── paket.references │ ├── Interstellar.Wpf.Chromium.fsproj │ ├── BrowserApp.fs │ └── BrowserWindow.fs ├── Interstellar.WinForms.Chromium │ ├── paket.references │ ├── Interstellar.WinForms.Chromium.fsproj │ ├── BrowserApp.fs │ └── BrowserWindow.fs ├── Interstellar.Core │ ├── paket.references │ ├── Interstellar.Core.fsproj │ ├── IBrowserExtensions.fs │ ├── Script1.fsx │ └── Api.fs └── Interstellar.WindowsCommon.Chromium │ └── Platform.fs ├── .nuget └── nuget.exe ├── Directory.Build.props ├── templates ├── minimal │ └── src │ │ ├── InterstellarApp.macOS │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon-128.png │ │ │ │ ├── AppIcon-16.png │ │ │ │ ├── AppIcon-256.png │ │ │ │ ├── AppIcon-32.png │ │ │ │ ├── AppIcon-512.png │ │ │ │ ├── AppIcon-128@2x.png │ │ │ │ ├── AppIcon-16@2x.png │ │ │ │ ├── AppIcon-256@2x.png │ │ │ │ ├── AppIcon-32@2x.png │ │ │ │ ├── AppIcon-512@2x.png │ │ │ │ └── Contents.json │ │ ├── Entitlements.plist │ │ ├── Main.fs │ │ ├── Info.plist │ │ ├── AppDelegate.fs │ │ └── InterstellarApp.macOS.fsproj │ │ ├── global.json │ │ ├── InterstellarApp.Core │ │ ├── InterstellarApp.Core.fsproj │ │ └── Library.fs │ │ ├── InterstellarApp.Windows │ │ ├── InterstellarApp.Windows.fsproj │ │ └── Program.fs │ │ ├── .template.config │ │ └── template.json │ │ ├── InterstellarApp.macOS.sln │ │ └── InterstellarApp.Windows.sln └── Interstellar.Template.nuspec ├── .vscode └── settings.json ├── paket.dependencies ├── .config └── dotnet-tools.json ├── AssemblyAndPackageInfo.props ├── LICENSE ├── .github └── workflows │ ├── test-templates.yml │ └── ci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── nupkg-hack.fsx ├── Interstellar.MacOS.sln ├── README.md ├── Interstellar.Windows.sln ├── paket.lock └── .gitignore /docs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Examples/Examples.SharedCode/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /Examples/Examples.Wpf.Chromium/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /src/Interstellar.MacOS.WebKit/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /Examples/Examples.WinForms.Chromium/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core -------------------------------------------------------------------------------- /src/Interstellar.Chromium/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | CefSharp.Common -------------------------------------------------------------------------------- /src/Interstellar.Wpf.Chromium/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | CefSharp.Wpf -------------------------------------------------------------------------------- /src/Interstellar.WinForms.Chromium/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | CefSharp.WinForms -------------------------------------------------------------------------------- /.nuget/nuget.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/.nuget/nuget.exe -------------------------------------------------------------------------------- /src/Interstellar.Core/paket.references: -------------------------------------------------------------------------------- 1 | FSharp.Core 2 | System.Text.Encodings.Web 3 | BlackFox.MasterOfFoo -------------------------------------------------------------------------------- /docs/Interstellar sample app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/docs/Interstellar sample app.gif -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | True 4 | 5 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "FSharp.fsiExtraParameters": [ 3 | "--compilertool:${workspaceFolder}/packages/build/FSharp.DependencyManager.Paket/lib/netstandard2.0/" 4 | ] 5 | } -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png -------------------------------------------------------------------------------- /templates/minimal/src/global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "6.0.100", 4 | "rollForward": "latestFeature" 5 | }, 6 | "msbuild-sdks": { 7 | "MSBuild.Sdk.Extras": "3.0.22" 8 | } 9 | } -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-128.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-16.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-256.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-32.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-512.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-128@2x.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-16@2x.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-256@2x.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-32@2x.png -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fsprojects/Interstellar/HEAD/templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Entitlements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Main.fs: -------------------------------------------------------------------------------- 1 | namespace Example.macOS.WebKit 2 | 3 | open AppKit 4 | open System 5 | 6 | module main = 7 | [] 8 | let main args = 9 | NSApplication.Init() 10 | NSApplication.SharedApplication.Delegate <- new AppDelegate() 11 | NSApplication.Main(args) 12 | 0 13 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Main.fs: -------------------------------------------------------------------------------- 1 | namespace InterstellarApp.macOS 2 | 3 | open AppKit 4 | open System 5 | 6 | module main = 7 | [] 8 | let main args = 9 | NSApplication.Init() 10 | NSApplication.SharedApplication.Delegate <- new AppDelegate() 11 | NSApplication.Main(args) 12 | 0 13 | -------------------------------------------------------------------------------- /paket.dependencies: -------------------------------------------------------------------------------- 1 | source https://api.nuget.org/v3/index.json 2 | framework: auto-detect 3 | 4 | nuget BlackFox.MasterOfFoo 5 | nuget CefSharp.WinForms 86.0.241 6 | nuget CefSharp.Wpf 86.0.241 7 | nuget FSharp.Core >= 4.2.3 8 | nuget NuGet.Build.Tasks.Pack 9 | nuget System.Text.Encodings.Web 10 | 11 | group Build 12 | 13 | source https://api.nuget.org/v3/index.json 14 | 15 | nuget FSharp.DependencyManager.Paket -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.Core/InterstellarApp.Core.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "fake-cli": { 6 | "version": "5.22.0", 7 | "commands": [ 8 | "fake" 9 | ] 10 | }, 11 | "paket": { 12 | "version": "7.1.5", 13 | "commands": [ 14 | "paket" 15 | ] 16 | }, 17 | "fsharp.formatting.commandtool": { 18 | "version": "11.4.3", 19 | "commands": [ 20 | "fsdocs" 21 | ] 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Examples/Examples.SharedCode/Examples.SharedCode.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Interstellar.Core/Interstellar.Core.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Interstellar.Chromium/Interstellar.Chromium.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0-windows;net472 5 | true 6 | 7 | AnyCPU 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.Windows/InterstellarApp.Windows.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0-Windows 5 | WinExe 6 | true 7 | false 8 | 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Interstellar.Core/IBrowserExtensions.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar 2 | open System 3 | open System.Runtime.CompilerServices 4 | 5 | [] 6 | type IBrowserExtensions = 7 | [] 8 | static member Load (this: IBrowser, uri: string) = this.Load (new Uri(uri)) 9 | [] 10 | static member LoadString (this: IBrowser, html: string, ?uri: string) = 11 | this.LoadString (html, ?uri = Option.map Uri uri) 12 | 13 | [] 14 | module FSharpIBrowserExtensions = 15 | open FSharp.Core.Printf 16 | 17 | type IBrowser with 18 | /// printf-style method that executes some Javascript code on a browser instance, sanitizing format parameters using . It is safe to pass in untrusted format parameters from the outside world. Think SQL prepared statements. 19 | member this.ExecuteJavascriptf (format: StringFormat<'a, unit>) = 20 | executeJavascriptf this format -------------------------------------------------------------------------------- /templates/Interstellar.Template.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Interstellar.Template 5 | Interstellar Template Pack 6 | 7 | 0.0.0 8 | John Wostenberg 9 | Templates for building cross-platform browser-based applications using Interstellar. 10 | https://fsprojects.github.io/Interstellar/ 11 | MIT 12 | Copyright 2020 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Examples/Examples.Wpf.Chromium/Examples.Wpf.Chromium.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WinExe 5 | net6.0-windows;net48 6 | true 7 | AnyCPU 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /AssemblyAndPackageInfo.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | © John Wostenberg 2019-2022 4 | Interstellar Contributors 5 | Cross-platform F# browser library 6 | John Wostenberg 7 | git 8 | https://github.com/fsprojects/Interstellar 9 | https://fsprojects.github.io/Interstellar/ 10 | fsharp;interstellar 11 | LICENSE 12 | true 13 | $(RepositoryUrl)/blob/master/CHANGELOG.md 14 | $(RepositoryUrl)/blob/master/LICENSE 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.Windows/Program.fs: -------------------------------------------------------------------------------- 1 | namespace InterstellarApp.Windows 2 | open System 3 | open System.Diagnostics 4 | open System.Threading 5 | open System.Windows 6 | open Interstellar 7 | open Interstellar.Core 8 | open Interstellar.Chromium.Wpf 9 | 10 | type App() = 11 | inherit Application(ShutdownMode = ShutdownMode.OnExplicitShutdown) 12 | 13 | override this.OnStartup (e: StartupEventArgs) = 14 | base.OnStartup e 15 | let onMainWindowCreated (w: IBrowserWindow) = 16 | let nativeWindow = w.NativeWindow 17 | // This is where you could call some WPF-specific APIs on this window 18 | () 19 | BrowserApp.run (InterstellarApp.BrowserApp.app onMainWindowCreated) 20 | 21 | module Main = 22 | [] 23 | let main argv = 24 | Thread.CurrentThread.Name <- "Main" 25 | Interstellar.Chromium.Platform.Initialize () 26 | let app = App() 27 | let result = app.Run () 28 | Debug.WriteLine "main() exiting" 29 | result -------------------------------------------------------------------------------- /Examples/Examples.WinForms.Chromium/Examples.WinForms.Chromium.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-windows;net48 5 | WinExe 6 | true 7 | AnyCPU 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/AppDelegate.fs: -------------------------------------------------------------------------------- 1 | namespace Example.macOS.WebKit 2 | open System 3 | open System.Threading 4 | open AppKit 5 | open Foundation 6 | open Interstellar 7 | open Interstellar.MacOS.WebKit 8 | open Examples.SharedCode 9 | 10 | [] 11 | type AppDelegate() = 12 | inherit NSApplicationDelegate() 13 | 14 | override this.ApplicationShouldTerminateAfterLastWindowClosed sender = false 15 | 16 | override this.DidFinishLaunching notification = 17 | printfn "DidFinishLaunching" 18 | Thread.CurrentThread.Name <- "Main" 19 | 20 | let mainCtx = SynchronizationContext.Current 21 | Async.Start <| async { 22 | let onMainWindowCreated (w: IBrowserWindow) = 23 | let nsWindow = w.NativeWindow 24 | // This is where you could call some Cocoa-specific APIs on this window 25 | () 26 | do! BrowserApp.runAsync mainCtx (SimpleBrowserApp.app ignore) 27 | NSApplication.SharedApplication.Terminate null 28 | } 29 | 30 | () -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleName 6 | InterstellarExample 7 | CFBundleIdentifier 8 | com.wostenberg.InterstellarExample 9 | CFBundleShortVersionString 10 | 1.0 11 | CFBundleVersion 12 | 1 13 | LSMinimumSystemVersion 14 | 10.14 15 | CFBundleDevelopmentRegion 16 | en 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundlePackageType 20 | APPL 21 | CFBundleSignature 22 | ???? 23 | NSHumanReadableCopyright 24 | 25 | NSPrincipalClass 26 | NSApplication 27 | XSAppIconAssets 28 | Assets.xcassets/AppIcon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Interstellar.MacOS.WebKit/BrowserApp.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.MacOS.WebKit 2 | open System 3 | open System.Threading 4 | open AppKit 5 | open Interstellar 6 | open Interstellar.MacOS.WebKit.Internal 7 | open Foundation 8 | 9 | module BrowserApp = 10 | /// Starts and runs a BrowserApp's lifecycle in a Cocoa + WebKit host, asychronously 11 | /// indicates the thread that is to be used as the UI thread 12 | /// Describes the application lifecycle 13 | let runAsync mainCtx (app: BrowserApp) = async { 14 | do! Async.SwitchToContext mainCtx 15 | let windowCreator : BrowserWindowCreator = fun config -> 16 | let w = new BrowserWindow(config) 17 | BrowserWindowConfig.applyWindowTitle mainCtx w (w :> IBrowserWindow<_>).Closed config.title 18 | upcast w 19 | do! app.onStart mainCtx windowCreator 20 | do! Async.SwitchToContext mainCtx 21 | } 22 | 23 | let run app = Async.Start <| runAsync SynchronizationContext.Current app -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleName 6 | InterstellarApp 7 | CFBundleIdentifier 8 | com.companyname.InterstellarApp 9 | CFBundleShortVersionString 10 | 1.0 11 | CFBundleVersion 12 | 1 13 | LSMinimumSystemVersion 14 | 10.14 15 | CFBundleDevelopmentRegion 16 | en 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundlePackageType 20 | APPL 21 | CFBundleSignature 22 | ???? 23 | NSHumanReadableCopyright 24 | 25 | NSPrincipalClass 26 | NSApplication 27 | XSAppIconAssets 28 | Assets.xcassets/AppIcon.appiconset 29 | 30 | 31 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/AppDelegate.fs: -------------------------------------------------------------------------------- 1 | namespace InterstellarApp.macOS 2 | open System 3 | open System.Threading 4 | open AppKit 5 | open Foundation 6 | open Interstellar 7 | open Interstellar.MacOS.WebKit 8 | open InterstellarApp 9 | 10 | [] 11 | type AppDelegate() = 12 | inherit NSApplicationDelegate() 13 | 14 | override this.ApplicationShouldTerminateAfterLastWindowClosed sender = false 15 | 16 | override this.DidFinishLaunching notification = 17 | printfn "DidFinishLaunching" 18 | Thread.CurrentThread.Name <- "Main" 19 | 20 | let mainCtx = SynchronizationContext.Current 21 | Async.Start <| async { 22 | let onMainWindowCreated (w: IBrowserWindow) = 23 | let nsWindow = w.NativeWindow 24 | // This is where you could call some Cocoa-specific APIs on this window 25 | () 26 | do! BrowserApp.runAsync mainCtx (InterstellarApp.BrowserApp.app ignore) 27 | NSApplication.SharedApplication.Terminate null 28 | } 29 | 30 | () -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 John Wostenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test-templates.yml: -------------------------------------------------------------------------------- 1 | name: Test Templates 2 | 3 | on: workflow_dispatch 4 | 5 | jobs: 6 | build-windows: 7 | name: Test Templates (Windows) 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-dotnet@v1 13 | with: 14 | dotnet-version: '6.0.300' 15 | - name: Restore 16 | run: | 17 | dotnet tool restore 18 | dotnet paket restore 19 | - name: Test templates 20 | run: | 21 | dotnet fake build -t BuildTemplateProjects -- Release 22 | 23 | build-macos: 24 | name: Test Templates (macOS) 25 | runs-on: macos-latest 26 | 27 | steps: 28 | - uses: actions/checkout@v2 29 | - uses: actions/setup-dotnet@v1 30 | with: 31 | dotnet-version: '6.0.300' 32 | - name: Install workloads 33 | run: | 34 | dotnet tool restore 35 | sudo dotnet workload install macos 36 | - name: Restore 37 | run: | 38 | dotnet tool restore 39 | dotnet paket restore 40 | - name: Test templates 41 | run: | 42 | dotnet fake build -t BuildTemplateProjects -- Release 43 | -------------------------------------------------------------------------------- /Examples/Examples.WinForms.Chromium/App.fs: -------------------------------------------------------------------------------- 1 | namespace Example.Windows.Chromium.Wpf 2 | open System 3 | open System.Diagnostics 4 | open System.Reflection 5 | open System.Threading 6 | open System.Windows 7 | open System.Windows.Forms 8 | open Interstellar 9 | open Examples.SharedCode 10 | open Interstellar.Chromium.WinForms 11 | open System.Runtime.Versioning 12 | 13 | module Main = 14 | let runApp () = 15 | Application.EnableVisualStyles () 16 | Application.SetCompatibleTextRenderingDefault true 17 | let onMainWindowCreated (w: IBrowserWindow
) = 18 | let nativeWindow = w.NativeWindow 19 | // This is where you could call some WinForms-specific APIs on this window 20 | () 21 | BrowserApp.run (SimpleBrowserApp.app onMainWindowCreated) 22 | 23 | [] 24 | let main argv = 25 | Thread.CurrentThread.Name <- "Main" 26 | Trace.WriteLine (sprintf "Starting app. Main thread id: %A" Thread.CurrentThread.ManagedThreadId) 27 | Interstellar.Chromium.Platform.Initialize () 28 | runApp () 29 | Interstellar.Chromium.Platform.Shutdown () 30 | 0 -------------------------------------------------------------------------------- /Examples/Examples.Wpf.Chromium/App.fs: -------------------------------------------------------------------------------- 1 | namespace Example.Windows.Chromium.Wpf 2 | open System 3 | open System.Diagnostics 4 | open System.Reflection 5 | open System.Runtime.Versioning 6 | open System.Threading 7 | open System.Windows 8 | open System.Windows.Controls 9 | open Examples.SharedCode 10 | open Interstellar 11 | open Interstellar.Chromium.Wpf 12 | 13 | type App() = 14 | inherit Application(ShutdownMode = ShutdownMode.OnExplicitShutdown) 15 | 16 | override this.OnStartup (e: StartupEventArgs) = 17 | base.OnStartup e 18 | let onMainWindowCreated (w: IBrowserWindow) = 19 | let nativeWindow = w.NativeWindow 20 | // This is where you could call some WPF-specific APIs on this window 21 | () 22 | BrowserApp.run (SimpleBrowserApp.app onMainWindowCreated) 23 | Trace.WriteLine "returning from OnStartup" 24 | 25 | module Main = 26 | [] 27 | let main argv = 28 | Thread.CurrentThread.Name <- "Main" 29 | Interstellar.Chromium.Platform.Initialize () 30 | let app = new App() 31 | let result = app.Run () 32 | Debug.WriteLine "main() exiting" 33 | result -------------------------------------------------------------------------------- /src/Interstellar.Wpf.Chromium/Interstellar.Wpf.Chromium.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0-windows;net48 5 | Library 6 | true 7 | true 8 | WPF 9 | netcore 10 | 11 | AnyCPU 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Interstellar.WinForms.Chromium/Interstellar.WinForms.Chromium.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0-windows;net48 5 | Library 6 | true 7 | true 8 | WINFORMS 9 | netcore 10 | 11 | AnyCPU 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Interstellar.Wpf.Chromium/BrowserApp.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.Chromium.Wpf 2 | open System 3 | open System.Windows 4 | open Interstellar 5 | open System.Threading 6 | 7 | module BrowserApp = 8 | /// Starts and runs a BrowserApp's lifecycle in a WPF + Chromium host, asychronously 9 | /// Indicates the thread that is to be used as the UI thread 10 | /// Describes the application lifecycle 11 | let runAsync mainCtx (app: BrowserApp) = async { 12 | let windowCreator : BrowserWindowCreator = fun config -> 13 | let w = new BrowserWindow(config) 14 | BrowserWindowConfig.applyWindowTitle mainCtx w w.Unloaded config.title 15 | upcast w 16 | do! Async.SwitchToContext mainCtx 17 | do! app.onStart mainCtx windowCreator 18 | do! Async.SwitchToContext mainCtx 19 | Application.Current.Shutdown () 20 | } 21 | 22 | /// Starts and runs a BrowserApp's lifecycle in a WPF + Chromium host, using the current thread as the UI thread 23 | /// Describes the application lifecycle 24 | let run app = Async.Start <| runAsync SynchronizationContext.Current app -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Examples.macOS.WebKit.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-macos 5 | Exe 6 | Example.macOS.WebKit 7 | InterstellarExample 8 | 10.14 9 | 10 | 11 | false 12 | false 13 | 14 | 15 | false 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Examples/Examples.macOS.WebKit/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "AppIcon-16.png", 5 | "size": "16x16", 6 | "scale": "1x", 7 | "idiom": "mac" 8 | }, 9 | { 10 | "filename": "AppIcon-16@2x.png", 11 | "size": "16x16", 12 | "scale": "2x", 13 | "idiom": "mac" 14 | }, 15 | { 16 | "filename": "AppIcon-32.png", 17 | "size": "32x32", 18 | "scale": "1x", 19 | "idiom": "mac" 20 | }, 21 | { 22 | "filename": "AppIcon-32@2x.png", 23 | "size": "32x32", 24 | "scale": "2x", 25 | "idiom": "mac" 26 | }, 27 | { 28 | "filename": "AppIcon-128.png", 29 | "size": "128x128", 30 | "scale": "1x", 31 | "idiom": "mac" 32 | }, 33 | { 34 | "filename": "AppIcon-128@2x.png", 35 | "size": "128x128", 36 | "scale": "2x", 37 | "idiom": "mac" 38 | }, 39 | { 40 | "filename": "AppIcon-256.png", 41 | "size": "256x256", 42 | "scale": "1x", 43 | "idiom": "mac" 44 | }, 45 | { 46 | "filename": "AppIcon-256@2x.png", 47 | "size": "256x256", 48 | "scale": "2x", 49 | "idiom": "mac" 50 | }, 51 | { 52 | "filename": "AppIcon-512.png", 53 | "size": "512x512", 54 | "scale": "1x", 55 | "idiom": "mac" 56 | }, 57 | { 58 | "filename": "AppIcon-512@2x.png", 59 | "size": "512x512", 60 | "scale": "2x", 61 | "idiom": "mac" 62 | } 63 | ], 64 | "info": { 65 | "version": 1, 66 | "author": "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "AppIcon-16.png", 5 | "size": "16x16", 6 | "scale": "1x", 7 | "idiom": "mac" 8 | }, 9 | { 10 | "filename": "AppIcon-16@2x.png", 11 | "size": "16x16", 12 | "scale": "2x", 13 | "idiom": "mac" 14 | }, 15 | { 16 | "filename": "AppIcon-32.png", 17 | "size": "32x32", 18 | "scale": "1x", 19 | "idiom": "mac" 20 | }, 21 | { 22 | "filename": "AppIcon-32@2x.png", 23 | "size": "32x32", 24 | "scale": "2x", 25 | "idiom": "mac" 26 | }, 27 | { 28 | "filename": "AppIcon-128.png", 29 | "size": "128x128", 30 | "scale": "1x", 31 | "idiom": "mac" 32 | }, 33 | { 34 | "filename": "AppIcon-128@2x.png", 35 | "size": "128x128", 36 | "scale": "2x", 37 | "idiom": "mac" 38 | }, 39 | { 40 | "filename": "AppIcon-256.png", 41 | "size": "256x256", 42 | "scale": "1x", 43 | "idiom": "mac" 44 | }, 45 | { 46 | "filename": "AppIcon-256@2x.png", 47 | "size": "256x256", 48 | "scale": "2x", 49 | "idiom": "mac" 50 | }, 51 | { 52 | "filename": "AppIcon-512.png", 53 | "size": "512x512", 54 | "scale": "1x", 55 | "idiom": "mac" 56 | }, 57 | { 58 | "filename": "AppIcon-512@2x.png", 59 | "size": "512x512", 60 | "scale": "2x", 61 | "idiom": "mac" 62 | } 63 | ], 64 | "info": { 65 | "version": 1, 66 | "author": "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build-windows: 7 | name: CI (Windows) 8 | runs-on: windows-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-dotnet@v1 13 | with: 14 | dotnet-version: '6.0.400' 15 | - name: Restore 16 | run: | 17 | dotnet tool restore 18 | dotnet paket restore 19 | - name: Build 20 | run: dotnet fake build 21 | - name: Pack 22 | run: | 23 | dotnet fake build -t PackAll 24 | - name: Upload artifacts 25 | uses: actions/upload-artifact@v2 26 | with: 27 | name: interstellar-windows 28 | path: artifacts 29 | - name: Test 30 | run: | 31 | dotnet fake build -t Test -- Release 32 | 33 | build-macos: 34 | name: CI (macOS) 35 | runs-on: macos-latest 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions/setup-dotnet@v1 40 | with: 41 | dotnet-version: '6.0.400' 42 | - name: Install workloads 43 | run: | 44 | dotnet tool restore 45 | sudo dotnet workload install macos 46 | - name: Restore 47 | run: | 48 | dotnet tool restore 49 | dotnet paket restore 50 | - name: Build 51 | run: dotnet fake build -- Release 52 | - name: Pack 53 | run: dotnet fake build -t PackAll -- Release 54 | - name: Upload artifacts 55 | uses: actions/upload-artifact@v2 56 | with: 57 | name: interstellar-macos 58 | path: artifacts/ 59 | - name: Test 60 | run: | 61 | dotnet fake build -t Test -- Release 62 | -------------------------------------------------------------------------------- /src/Interstellar.MacOS.WebKit/Interstellar.MacOS.WebKit.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-macos 5 | Library 6 | Interstellar.macOS.WebKit 7 | 8 | 9 | false 10 | false 11 | 12 | 13 | false 14 | false 15 | 16 | 17 | 18 | 19 | <_Parameter1>$(Version) 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/Interstellar.WinForms.Chromium/BrowserApp.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.Chromium.WinForms 2 | open System 3 | open System.Windows.Forms 4 | open Interstellar 5 | open System.Threading 6 | open System.Diagnostics 7 | 8 | module BrowserApp = 9 | /// Starts and runs a BrowserApp's lifecycle in a Windows Forms + Chromium host, asychronously 10 | /// Indicates the thread that is to be used as the UI thread 11 | /// Describes the application lifecycle 12 | let runAsync mainCtx (app: BrowserApp) = async { 13 | let windowCreator : BrowserWindowCreator = fun config -> 14 | let w = new BrowserWindow(config) 15 | BrowserWindowConfig.applyWindowTitle mainCtx w w.Disposed config.title 16 | upcast w 17 | do! Async.SwitchToContext mainCtx 18 | do! app.onStart mainCtx windowCreator 19 | do! Async.SwitchToContext mainCtx 20 | Interstellar.Chromium.Platform.Shutdown () 21 | Application.Exit () 22 | } 23 | 24 | /// Starts and runs a BrowserApp's lifecycle in a Windows Forms + Chromium host, using the current thread as the UI thread 25 | /// Describes the application lifecycle 26 | let run app = 27 | // call a Control ctor in order to initialize SynchronizationContext.Current. Constructing a Form, or calling Application.Run also does this. 28 | use dummyControl = new Control() in () 29 | Debug.WriteLine (sprintf "DummyControl thread: %A" Thread.CurrentThread.ManagedThreadId) 30 | Async.Start <| runAsync SynchronizationContext.Current app 31 | Application.Run () -------------------------------------------------------------------------------- /templates/minimal/src/.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "John Wostenberg", 3 | "classifications": [ 4 | "Interstellar", 5 | "Cross-platform" 6 | ], 7 | "name": "Simple Interstellar App", 8 | "tags": { 9 | "language": "F#", 10 | "type": "project" 11 | }, 12 | "identity": "Interstellar.Template", 13 | "groupIdentity": "Interstellar", 14 | "shortName": "interstellar", 15 | "sourceName": "InterstellarApp", 16 | "preferNameDirectory": true, 17 | "exclude": [ 18 | "**/[Bb]in/**", "**/[Oo]bj/**", 19 | ".template.config/**/*", "**/*.filelist", 20 | "**/*.user", "**/*.lock.json", 21 | "**/.vs/**", "**/.ionide/**", 22 | "**/GPUCache/**" 23 | ], 24 | "symbols": { 25 | "Windows": { 26 | "type": "parameter", 27 | "dataType": "bool", 28 | "defaultValue": "true", 29 | "description": "Generates a Windows host project" 30 | }, 31 | "macOS": { 32 | "type": "parameter", 33 | "dataType": "bool", 34 | "defaultValue": "true", 35 | "description": "Generates a macOS host project" 36 | } 37 | }, 38 | "sources": [ 39 | { 40 | "modifiers": [ 41 | { 42 | "condition": "(!Windows)", 43 | "exclude": ["InterstellarApp.Windows/**/*", "InterstellarApp.Windows.*"] 44 | }, 45 | { 46 | "condition": "(!macOS)", 47 | "exclude": ["InterstellarApp.macOS/**/*", "InterstellarApp.macOS.*"] 48 | } 49 | ] 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.0 4 | 5 | * Fully support .NET 6 on Windows and macOS 6 | 7 | ## 0.3.3 8 | 9 | * Fix FSharp.Core dependency version mistake in last release 10 | 11 | ## 0.3.2 12 | 13 | * WinForms, WPF - Fix a bug that happens under Windows high-DPI mode 14 | * Change targets to netcoreapp3.1 15 | 16 | ## 0.3.1 17 | 18 | * WinForms - Fixed an issue that could cause apps to crash at startup in release mode (https://github.com/jwosty/Interstellar/issues/10 ; thanks to @amaitland for the fix) 19 | 20 | ## 0.3.0 21 | 22 | * Add IBrowser.CanShowDevTools 23 | * Interstellar.MacOS.WebKit 24 | * Remove unnecessary assembly references 25 | 26 | ## 0.2.0 27 | 28 | * Add a generic parameter to IBrowserWindow that carries the type of the underlying window implementation, allowing for usages of native APIs within an Interstellar application without requiring dynamic casting 29 | * This also affects all types and functions that depend on IBrowserWindow -- so just about everything except IBrowser 30 | * Add executeJavascriptf, javascriptf, kjavascriptf, and IBrowser.ExecuteJavascriptf, which make it easy to safely format untrusted inputs into Javascript scripts for execution (a form of code injection) 31 | 32 | ## 0.1.0 33 | 34 | * Fix BrowserApp.create not waiting for the window to close 35 | * Make referencing events from non-UI threads always be a safe operation 36 | * Add IBrowserWindow.IsShowing 37 | * Add IBrowser.LoadAsync and IBrowser.LoadStringAsync 38 | * Add BrowserWindowConfig.title record field and a WindowTitle type to accompany it, and specify the default title to track the page title 39 | * (WinForms) - Fix the implementation of BrowserWindow.Dispose 40 | 41 | ## 0.0.1 42 | 43 | * Initial release 44 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.808.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "InterstellarApp.Core", "InterstellarApp.Core\InterstellarApp.Core.fsproj", "{3A2B7A09-0068-4013-BE15-D1D967858FBE}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "InterstellarApp.macOS", "InterstellarApp.macOS\InterstellarApp.macOS.fsproj", "{19D61708-80B4-4FE8-8E53-937BDC4BC011}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {19D61708-80B4-4FE8-8E53-937BDC4BC011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {19D61708-80B4-4FE8-8E53-937BDC4BC011}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {19D61708-80B4-4FE8-8E53-937BDC4BC011}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {19D61708-80B4-4FE8-8E53-937BDC4BC011}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {08DF859A-39F3-4842-9A99-327FA9195D27} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.Windows.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.808.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "InterstellarApp.Core", "InterstellarApp.Core\InterstellarApp.Core.fsproj", "{3A2B7A09-0068-4013-BE15-D1D967858FBE}" 7 | EndProject 8 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "InterstellarApp.Windows", "InterstellarApp.Windows\InterstellarApp.Windows.fsproj", "{226E8212-8C44-451A-B585-902F4EE0C739}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3A2B7A09-0068-4013-BE15-D1D967858FBE}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {226E8212-8C44-451A-B585-902F4EE0C739}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {226E8212-8C44-451A-B585-902F4EE0C739}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {226E8212-8C44-451A-B585-902F4EE0C739}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {226E8212-8C44-451A-B585-902F4EE0C739}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {08DF859A-39F3-4842-9A99-327FA9195D27} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Interstellar 2 | 3 | High-quality contributions are very much welcome. Pull requests should do one thing only and do it well; if you have multiple separate contributions, please split them 4 | into individual, independent PRs, or they may not be accepted. A good PR might consist of one of these (but is not limited to these): 5 | 6 | * A bug fix 7 | * A feature implementation 8 | * Code cleanup (with reasonable scope) 9 | * Documentation improvement (again, with reasonable scope) 10 | 11 | ## Testing 12 | 13 | Interstellar doesn't have any unit tests because it's generally too costly and impractical to unit test GUI systems. If anyone thinks they have some good tests to add 14 | to prove me wrong, certainly submit a PR and I will reconsider! End-to-end tests may be a good idea; i.e. tests that just make sure that the app starts up and a window 15 | appears would probably be prudent. 16 | 17 | Therefore, you must manually test your changes by running the example apps included in the solution files. 18 | 19 | ## Releasing 20 | 21 | To create a new release version, add a new section to [CHANGELOG.md](CHANGELOG.md). The build system uses the first section in the file to determine the version number 22 | as well as the release notes. 23 | 24 | Packaging a release is as easy as running ``dotnet fake build -t PackAll`` (they will end up in the ``artifacts/`` directory). CI will always attempt to build the 25 | NuGet packages, but does not yet automatically publish to NuGet, so to publish a new release, you must download the built packages from the GitHub CI and then manually 26 | publish (see https://github.com/jwosty/Interstellar/issues/16). 27 | 28 | After releasing the packages to NuGet, update the templates (found under templates/) to use the new package versions, and then release those. 29 | -------------------------------------------------------------------------------- /src/Interstellar.WindowsCommon.Chromium/Platform.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.Chromium 2 | open System 3 | open System.IO 4 | open System.Reflection 5 | open CefSharp 6 | #if WPF 7 | open CefSharp.Wpf 8 | #endif 9 | #if WINFORMS 10 | open CefSharp.WinForms 11 | #endif 12 | 13 | type Platform private() = 14 | static let mutable isInitialized = false 15 | static let initLock = new Object() 16 | 17 | static member Initialize () = 18 | lock initLock (fun () -> 19 | if not isInitialized then 20 | AppDomain.CurrentDomain.add_AssemblyResolve (ResolveEventHandler(Platform.ResolveCefSharpAssembly)) 21 | Platform.InitAnyCpuCefSharp () |> ignore 22 | ) 23 | 24 | // Without this inlining, WinForms builds crash in Release mode: https://github.com/fsprojects/Interstellar/issues/10 25 | [] 26 | static member Shutdown () = 27 | Cef.Shutdown () 28 | 29 | static member private GetPlatformAssemblyPath assemblyName = 30 | Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, (if Environment.Is64BitProcess then "x64" else "x86"), assemblyName) 31 | 32 | static member private InitAnyCpuCefSharp () = 33 | // Required to fix high DPI issue: https://github.com/fsprojects/Interstellar/issues/25 34 | Cef.EnableHighDPISupport() 35 | 36 | let browserSubpath = Platform.GetPlatformAssemblyPath("CefSharp.BrowserSubprocess.exe") 37 | let settings = new CefSettings(BrowserSubprocessPath = browserSubpath) 38 | //settings.RegisterExtension Browser.bridgeExtension 39 | Cef.Initialize (settings, false, (null : IBrowserProcessHandler)) 40 | 41 | static member private ResolveCefSharpAssembly sender (args: ResolveEventArgs) = 42 | if (args.Name.StartsWith("CefSharp")) then 43 | let assemblyName = args.Name.Split([|','|], 2).[0] + ".dll" 44 | let archSpecificPath = Platform.GetPlatformAssemblyPath assemblyName 45 | if File.Exists archSpecificPath then (Assembly.LoadFile archSpecificPath) else null 46 | else null -------------------------------------------------------------------------------- /nupkg-hack.fsx: -------------------------------------------------------------------------------- 1 | #r "System.IO.Compression" 2 | open System 3 | open System.IO 4 | open System.IO.Compression 5 | open System.Text 6 | open System.Text.RegularExpressions 7 | open Fake.Core 8 | open Fake.IO 9 | 10 | Environment.CurrentDirectory <- __SOURCE_DIRECTORY__ 11 | 12 | let changeVersionConstraints text = 13 | Regex("(?<=id=\"Interstellar.+?\"\\s+version=\")[^[\\]]*?(?=\")") 14 | .Replace (text, MatchEvaluator(fun m -> sprintf "[%s]" m.Value)) 15 | 16 | // """ 17 | // 18 | // 19 | // 20 | // 21 | // 22 | // 23 | // 24 | // 25 | // 26 | // 27 | // """ 28 | // |> changeVersionConstraints |> printfn "%s" 29 | 30 | let hackNupkgFromStream (_: string) (stream: Stream) = 31 | use archive = new ZipArchive(stream, ZipArchiveMode.Update) 32 | let oldEntry = archive.Entries |> Seq.find (fun e -> e.Name.EndsWith ".nuspec") 33 | let input = 34 | (use nuspecReader = new StreamReader(oldEntry.Open(), Encoding.UTF8) in nuspecReader.ReadToEnd()) 35 | let output = changeVersionConstraints input 36 | use nuspecWriter = new StreamWriter(oldEntry.Open(), Encoding.UTF8) 37 | nuspecWriter.Write output 38 | 39 | /// Cracks open a nupkg and changes all Interstellar package reference constraints from >= to = 40 | let hackNupkgAtPath (path: string) = 41 | Trace.log ("Hacking nupkg: " + path) 42 | use file = File.Open (path, FileMode.Open, FileAccess.ReadWrite, FileShare.None) 43 | hackNupkgFromStream path file 44 | 45 | //hackNupkgAtPath (Path.Combine ("artifacts", "Interstellar.Wpf.Chromium.nupkg")) -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Interstellar 2 | 3 | > NOTE: This API is not yet guarenteed to be stable or backward-compatible until v1.0, so breaking changes may occur at any time. 4 | 5 | Interstellar is an F# library providing a standard, mixed-paradigm API for accessing browser controls on various platforms. Currently, there are 3 combinations platform and browser hosts available. See [Examples](https://github.com/fsprojects/Interstellar/tree/master/Examples) for a simple sample application. See https://github.com/jwosty/InterstellarFableHelloWorld for an example of combining Interstellar with [Fable](https://fable.io/), achieving a cross-platform desktop app built completely in F#. 6 | 7 | ## Quick Start 8 | 9 | You will need the .Net 5 SDK (and the mono SDK on macOS). For the Windows projects, you should be able to use any of the standard IDEs (Visual Studio, Visual Studio Code, Rider \[untested but should work\]). For the macOS projects, you need to use Visual Studio for Mac. 10 | 11 | 12 | Create a project from the template: 13 | 14 | ```bash 15 | dotnet new -i Interstellar.Template 16 | dotnet new interstellar -n 17 | ``` 18 | 19 | On Windows, you can run it like so: 20 | 21 | ```bash 22 | dotnet restore .Windows.sln 23 | dotnet run -p .Windows\.Windows.fsproj 24 | ``` 25 | 26 | On macOS, I recommend opening ``.macOS.sln`` in Visual Studio for Mac and running it that way. It runs using the mono-based Xamarin.macOS runtime, and as a result you currently can't run it with ``dotnet run`` (see [xamarin/xamarin-macios#3955](https://github.com/xamarin/xamarin-macios/issues/8955)). This is on the roadmap for .NET 6. You'll have to use Mono's ``msbuild`` instead, if you want to use the CLI. 27 | 28 | You should end up with a simple, cross-platform sample app that opens a window built using embeded HTML, CSS, and Javascript. 29 | 30 | ![Sample Interstellar app](https://fsprojects.github.io/Interstellar/Interstellar%20sample%20app.gif) 31 | 32 | ### Customizing the template 33 | 34 | By default, this will create a core sample project, and a host project for each platform (Windows and macOS). To generate a project without a macOS host, use the following: 35 | 36 | ```bash 37 | dotnet new interstellar --macOS false 38 | ``` 39 | 40 | And to disable Windows: 41 | 42 | ```bash 43 | dotnet new interstellar --Windows false 44 | ``` 45 | 46 | To see more info about these options: 47 | 48 | ```bash 49 | dotnet new interstellar --help 50 | ``` 51 | -------------------------------------------------------------------------------- /src/Interstellar.Core/Script1.fsx: -------------------------------------------------------------------------------- 1 | #I "..\\.paket\\load\\netstandard2.0" 2 | #load "BlackFox.MasterOfFoo.fsx" 3 | #load "System.Text.Encodings.Web.fsx" 4 | #load "main.group.fsx" 5 | #load "Api.fs" 6 | 7 | open System.Text.Encodings.Web 8 | open FSharp.Core.Printf 9 | open BlackFox.MasterOfFoo 10 | open Interstellar 11 | 12 | type MockBrowser() = 13 | interface IBrowser with 14 | member this.Address = raise (System.NotImplementedException()) 15 | member this.AreDevToolsShowing = raise (System.NotImplementedException()) 16 | member this.CanGoBack = raise (System.NotImplementedException()) 17 | member this.CanGoForward = raise (System.NotImplementedException()) 18 | member this.CloseDevTools() = raise (System.NotImplementedException()) 19 | member this.Engine = raise (System.NotImplementedException()) 20 | member this.GoBack() = raise (System.NotImplementedException()) 21 | member this.GoForward() = raise (System.NotImplementedException()) 22 | [] 23 | member this.JavascriptMessageRecieved = Unchecked.defaultof> 24 | member this.Load(arg1) = raise (System.NotImplementedException()) 25 | member this.LoadAsync(arg1) = raise (System.NotImplementedException()) 26 | member this.LoadString(html, uri) = raise (System.NotImplementedException()) 27 | member this.LoadStringAsync(html, uri) = raise (System.NotImplementedException()) 28 | [] 29 | member this.PageLoaded = Unchecked.defaultof> 30 | member this.PageTitle = raise (System.NotImplementedException()) 31 | [] 32 | member this.PageTitleChanged = Unchecked.defaultof> 33 | member this.Reload() = raise (System.NotImplementedException()) 34 | member this.ShowDevTools() = raise (System.NotImplementedException()) 35 | member this.ExecuteJavascript script = printfn "%s" script 36 | 37 | let mockBrowser = new MockBrowser() :> IBrowser 38 | 39 | Interstellar.Printf.javascriptf "console.log(\"%s\")" """'hello \"world" """ |> printfn "%s" 40 | Interstellar.Printf.javascriptf "console.log('%s')" """'hello "world""" |> printfn "%s" 41 | Interstellar.Printf.javascriptf "console.log(\"%s\")" """ \"hello world" """ |> printfn "%s" 42 | do executeJavascriptf mockBrowser "%d%b" 42 true 43 | 44 | let executeJavascriptf' (browser: IBrowser) format = kjavascriptf (fun result -> browser.ExecuteJavascript result) format 45 | do executeJavascriptf' mockBrowser "console.log(\"%s\")" """ \"hello world"<%*>; """ -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.macOS/InterstellarApp.macOS.fsproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0-macos 5 | Exe 6 | InterstellarApp.macOS 7 | InterstellarApp 8 | 10.14 9 | 10 | 11 | Mac Developer 12 | false 13 | false 14 | 15 | 16 | true 17 | true 18 | SdkOnly 19 | false 20 | false 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Interstellar.WinForms.Chromium/BrowserWindow.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.Chromium.WinForms 2 | open System 3 | open System.Drawing 4 | open System.Windows 5 | open System.Windows.Forms 6 | open CefSharp 7 | open CefSharp.WinForms 8 | open Interstellar 9 | open System.Threading 10 | open System.Diagnostics 11 | 12 | type BrowserWindow(config: BrowserWindowConfig) as this = 13 | inherit Form(Visible = false) 14 | 15 | let mutable disposed = false 16 | let mutable lastKnownPageTitle = "" 17 | let owningThreadId = Thread.CurrentThread.ManagedThreadId 18 | let cefBrowser = new ChromiumWebBrowser(null: string) 19 | let browser = 20 | new Interstellar.Chromium.Browser( 21 | cefBrowser, 22 | { getPageTitle = fun () -> lastKnownPageTitle 23 | titleChanged = cefBrowser.TitleChanged |> Event.map (fun x -> x.Title) 24 | isBrowserInitializedChanged = cefBrowser.IsBrowserInitializedChanged |> Event.map ignore}, 25 | config) 26 | 27 | let titleChangedHandle = cefBrowser.TitleChanged.Subscribe (fun e -> lastKnownPageTitle <- e.Title) 28 | 29 | // (primary) constructor 30 | do 31 | this.Controls.Add cefBrowser 32 | 33 | member this.ChromiumBrowser = browser 34 | 35 | interface IBrowserWindow with 36 | member this.Browser = upcast browser 37 | member this.Close () = (this :> Form).Close () 38 | [] 39 | member val Closed : IEvent = (this :> Form).FormClosed |> Event.map ignore 40 | member this.IsShowing = 41 | seq { for frm in Application.OpenForms -> frm } 42 | |> Seq.contains (this :> Form) 43 | member this.NativeWindow = this :> Form 44 | member this.Platform = BrowserWindowPlatform.WinForms 45 | member this.Show () = 46 | if (Thread.CurrentThread.ManagedThreadId <> owningThreadId) then 47 | raise (new InvalidOperationException("Show() called from a thread other than the thread on which the BrowserWindow was constructed.")) 48 | (this :> Form).Show () 49 | async { 50 | if cefBrowser.IsBrowserInitialized then () 51 | else 52 | let! _ = Async.AwaitEvent cefBrowser.IsBrowserInitializedChanged 53 | () 54 | } 55 | [] 56 | member val Shown = (this :> Form).Shown |> Event.map ignore 57 | member this.Size 58 | with get () = 59 | let size = (this :> Form).Size 60 | float size.Width, float size.Height 61 | and set (width, height) = 62 | (this :> Form).Size <- new Size(int width, int height) 63 | member this.Title 64 | with get () = this.Text 65 | and set title = this.Text <- title 66 | 67 | override this.Dispose disposing = 68 | if disposing then 69 | titleChangedHandle.Dispose () 70 | base.Dispose disposing -------------------------------------------------------------------------------- /src/Interstellar.Wpf.Chromium/BrowserWindow.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.Chromium.Wpf 2 | open System 3 | open System.Threading 4 | open System.Diagnostics 5 | open System.Windows 6 | open System.Windows.Controls 7 | open CefSharp 8 | open CefSharp.Wpf 9 | open Interstellar 10 | open Interstellar.Chromium 11 | 12 | type BrowserWindow(config: BrowserWindowConfig) as this = 13 | inherit Window() 14 | 15 | let mainCtx = SynchronizationContext.Current 16 | let cefBrowser = new CefSharp.Wpf.ChromiumWebBrowser() 17 | let browser = 18 | new Interstellar.Chromium.Browser<_>( 19 | cefBrowser, 20 | { getPageTitle = fun () -> cefBrowser.Title 21 | titleChanged = cefBrowser.TitleChanged |> Event.map (fun x -> x.NewValue :?> string) 22 | isBrowserInitializedChanged = cefBrowser.IsBrowserInitializedChanged |> Event.map ignore}, 23 | config) 24 | let owningThreadId = Thread.CurrentThread.ManagedThreadId 25 | 26 | let mutable alreadyShown = false 27 | let shown = new Event() 28 | 29 | // (primary) constructor 30 | do 31 | this.Content <- cefBrowser 32 | 33 | interface IDisposable with 34 | member this.Dispose () = 35 | Async.StartImmediate <| async { 36 | do! Async.SwitchToContext mainCtx 37 | this.Close () 38 | } 39 | 40 | interface IBrowserWindow with 41 | member this.Browser = upcast browser 42 | member this.Close () = (this :> Window).Close () 43 | [] member val Closed = (this :> Window).Closed |> Event.map ignore 44 | member this.IsShowing = 45 | seq { for w in Application.Current.Windows -> w } 46 | |> Seq.contains (this :> Window) 47 | member this.NativeWindow = this :> Window 48 | member this.Platform = BrowserWindowPlatform.Wpf 49 | member this.Show () = 50 | if owningThreadId <> Thread.CurrentThread.ManagedThreadId then 51 | raise (new InvalidOperationException("Show() called from a thread other than the thread on which the BrowserWindow was constructed.")) 52 | (this :> Window).Show () 53 | async { 54 | if not cefBrowser.IsBrowserInitialized then 55 | let! _ = Async.AwaitEvent cefBrowser.IsBrowserInitializedChanged 56 | () 57 | } 58 | member this.Size 59 | with get () = base.Width, base.Height 60 | and set (width, height) = 61 | base.Width <- width 62 | base.Height <- height 63 | [] member val Shown = shown.Publish 64 | member this.Title 65 | with get () = (this :> Window).Title 66 | and set title = (this :> Window).Title <- title 67 | 68 | override this.OnContentRendered e = 69 | base.OnContentRendered e 70 | if not alreadyShown then 71 | alreadyShown <- true 72 | shown.Trigger () -------------------------------------------------------------------------------- /Interstellar.MacOS.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Interstellar.Core", "src\Interstellar.Core\Interstellar.Core.fsproj", "{7D096C2C-0960-4754-9954-421832426807}" 5 | EndProject 6 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.SharedCode", "Examples\Examples.SharedCode\Examples.SharedCode.fsproj", "{0332BEB2-0915-47CC-A709-49FC4C2B3DB5}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BE6ABDEA-53B5-4D1C-9E46-6B783CD97234}" 9 | ProjectSection(SolutionItems) = preProject 10 | global.json = global.json 11 | AssemblyAndPackageInfo.props = AssemblyAndPackageInfo.props 12 | paket.dependencies = paket.dependencies 13 | EndProjectSection 14 | EndProject 15 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.macOS.WebKit", "Examples\Examples.macOS.WebKit\Examples.macOS.WebKit.fsproj", "{C91E86B6-72B1-4367-9CD4-A1116FECD752}" 16 | EndProject 17 | Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Interstellar.MacOS.WebKit", "src\Interstellar.MacOS.WebKit\Interstellar.MacOS.WebKit.fsproj", "{77FAE56F-23ED-4B30-B961-BBC7B492AAFE}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {7D096C2C-0960-4754-9954-421832426807}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {7D096C2C-0960-4754-9954-421832426807}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {7D096C2C-0960-4754-9954-421832426807}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {7D096C2C-0960-4754-9954-421832426807}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {0332BEB2-0915-47CC-A709-49FC4C2B3DB5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {0332BEB2-0915-47CC-A709-49FC4C2B3DB5}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {0332BEB2-0915-47CC-A709-49FC4C2B3DB5}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {0332BEB2-0915-47CC-A709-49FC4C2B3DB5}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {C91E86B6-72B1-4367-9CD4-A1116FECD752}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {C91E86B6-72B1-4367-9CD4-A1116FECD752}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {C91E86B6-72B1-4367-9CD4-A1116FECD752}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {C91E86B6-72B1-4367-9CD4-A1116FECD752}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {14C8FADA-AD8F-421E-8FB3-F9EA797E507C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {14C8FADA-AD8F-421E-8FB3-F9EA797E507C}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {14C8FADA-AD8F-421E-8FB3-F9EA797E507C}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {14C8FADA-AD8F-421E-8FB3-F9EA797E507C}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {77FAE56F-23ED-4B30-B961-BBC7B492AAFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {77FAE56F-23ED-4B30-B961-BBC7B492AAFE}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {77FAE56F-23ED-4B30-B961-BBC7B492AAFE}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {77FAE56F-23ED-4B30-B961-BBC7B492AAFE}.Release|Any CPU.Build.0 = Release|Any CPU 45 | EndGlobalSection 46 | EndGlobal 47 | -------------------------------------------------------------------------------- /src/Interstellar.MacOS.WebKit/BrowserWindow.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.MacOS.WebKit.Internal 2 | open System 3 | open AppKit 4 | open CoreGraphics 5 | open Foundation 6 | open Interstellar 7 | open Interstellar.MacOS.WebKit 8 | open WebKit 9 | 10 | type NiblessViewController(view: NSView) = 11 | inherit NSViewController() 12 | 13 | override this.LoadView () = 14 | base.View <- view 15 | 16 | type BrowserWindow(config: BrowserWindowConfig) as this = 17 | inherit NSWindowController("BrowserWindow") 18 | 19 | let browser = new Browser(config) 20 | 21 | let closedEvt = new Event<_>() 22 | let shownEvt = new Event<_>() 23 | let disposedEvt = new Event<_>() 24 | 25 | do 26 | let wkBrowserController = { 27 | new NiblessViewController(browser.WebKitBrowser) with 28 | // This KeyDown override prevents the macOS beep (error) sound on key presses 29 | override this.KeyDown (event: NSEvent) = () 30 | override this.ViewDidAppear () = 31 | base.ViewDidAppear () 32 | shownEvt.Trigger () 33 | } 34 | this.Window <- 35 | new NSWindow(new CGRect (0., 0., 1000., 500.), 36 | NSWindowStyle.Titled ||| NSWindowStyle.Closable ||| NSWindowStyle.Miniaturizable ||| NSWindowStyle.Resizable, 37 | NSBackingStore.Buffered, false) 38 | this.Window.WillClose.Add (fun x -> closedEvt.Trigger ()) 39 | this.Window.ContentView <- browser.WebKitBrowser 40 | this.Window.ContentViewController <- wkBrowserController 41 | this.Window.Center () 42 | this.Window.AwakeFromNib () 43 | 44 | member this.WKBrowserView = browser.WebKitBrowser 45 | member this.WKBrowser = browser 46 | 47 | override this.LoadWindow () = 48 | base.LoadWindow () 49 | 50 | interface IBrowserWindow with 51 | member this.Browser = upcast browser 52 | member this.Close () = (this :> NSWindowController).Close () 53 | [] 54 | member val Closed = closedEvt.Publish 55 | member this.IsShowing = 56 | let w = (this :> NSWindowController).Window 57 | w.IsVisible || w.IsMiniaturized 58 | member this.Show () = async { 59 | (this :> NSWindowController).ShowWindow this 60 | } 61 | member this.NativeWindow = this.Window 62 | member this.Platform = BrowserWindowPlatform.MacOS 63 | [] 64 | member val Shown = shownEvt.Publish 65 | member this.Size 66 | with get () = 67 | let size = this.Window.Frame.Size 68 | float size.Width, float size.Height 69 | and set (width, height) = 70 | let oldFrame = this.Window.Frame 71 | // Cocoa uses a bottom-left origin, so we have to move the bottom-left corner in order to keep the top-right 72 | // corner in place 73 | let rect = new CGRect(float oldFrame.X, float oldFrame.Y - (height - float oldFrame.Height), width, height) 74 | this.Window.SetFrame (rect, true, true) 75 | member this.Title 76 | with get () = base.Window.Title 77 | and set x = base.Window.Title <- x -------------------------------------------------------------------------------- /src/Interstellar.Chromium/JavascriptInjectionFilter.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.Chromium 2 | open System 3 | open System.Collections.Generic 4 | open System.Diagnostics 5 | open System.IO 6 | open System.Text 7 | open CefSharp 8 | open CefSharp.Handler 9 | 10 | type ScriptLocation = Head | Body 11 | 12 | module JSInjectionHelpers = 13 | // Dealing with inserting arbitrary contents into a script tag is hard to do right, because there's really no right way to do it, since 14 | // the inside of a script tag doesn't recognize HTML entities, meaning we can't just escape it for HTML. Granted, the way that this library 15 | // is meant to be used, I don't expect it to be a vulnerability source, but we can at least try this. 16 | // see https://blog.uploadcare.com/vulnerability-in-html-design-the-script-tag-33d24642359e 17 | // and the HTML spec's recommendations: https://www.w3.org/TR/html52/semantics-scripting.html#restrictions-for-contents-of-script-elements 18 | let escapeScriptTagContents (contents: string) = 19 | contents 20 | .Replace(" 16 | 17 | 18 | 19 | 20 | 21 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 62 |
63 | 66 |
67 |
68 | {{fsdocs-content}} 69 | {{fsdocs-tooltips}} 70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
80 | 81 | 82 | -------------------------------------------------------------------------------- /Interstellar.Windows.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29503.13 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".paket", ".paket", "{2173101E-392D-4167-8928-A66A3D777746}" 7 | ProjectSection(SolutionItems) = preProject 8 | paket.dependencies = paket.dependencies 9 | EndProjectSection 10 | EndProject 11 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Interstellar.Core", "src\Interstellar.Core\Interstellar.Core.fsproj", "{7ADA52C2-A422-44C8-B14F-CE43566F8E03}" 12 | EndProject 13 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Interstellar.Wpf.Chromium", "src\Interstellar.Wpf.Chromium\Interstellar.Wpf.Chromium.fsproj", "{EA841C98-988C-4B47-9FCA-B0CED74EC958}" 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{96BB8378-7D63-49AF-BEF5-6016E234653D}" 16 | EndProject 17 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Examples.Wpf.Chromium", "Examples\Examples.Wpf.Chromium\Examples.Wpf.Chromium.fsproj", "{F8F250AA-A4E3-4DC2-863E-1348C98862D9}" 18 | EndProject 19 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Interstellar.WinForms.Chromium", "src\Interstellar.WinForms.Chromium\Interstellar.WinForms.Chromium.fsproj", "{F0D4F963-A6F1-43D1-860C-5CDE07F2244F}" 20 | EndProject 21 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Examples.WinForms.Chromium", "Examples\Examples.WinForms.Chromium\Examples.WinForms.Chromium.fsproj", "{8FD85916-B47D-4055-8661-718B9ABEFF6B}" 22 | EndProject 23 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Examples.SharedCode", "Examples\Examples.SharedCode\Examples.SharedCode.fsproj", "{7DC66E91-1D5D-4775-BE40-378AA2E432F8}" 24 | EndProject 25 | Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Interstellar.Chromium", "src\Interstellar.Chromium\Interstellar.Chromium.fsproj", "{58387294-2BDC-4799-919E-9F71883DA9B5}" 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{175A1B7C-AC1D-40C7-B857-FA30352D3430}" 28 | ProjectSection(SolutionItems) = preProject 29 | AssemblyAndPackageInfo.props = AssemblyAndPackageInfo.props 30 | build.fsx = build.fsx 31 | CHANGELOG.md = CHANGELOG.md 32 | paket.dependencies = paket.dependencies 33 | README.md = README.md 34 | EndProjectSection 35 | EndProject 36 | Global 37 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 38 | Debug|Any CPU = Debug|Any CPU 39 | Release|Any CPU = Release|Any CPU 40 | EndGlobalSection 41 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 42 | {7ADA52C2-A422-44C8-B14F-CE43566F8E03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {7ADA52C2-A422-44C8-B14F-CE43566F8E03}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {7ADA52C2-A422-44C8-B14F-CE43566F8E03}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {7ADA52C2-A422-44C8-B14F-CE43566F8E03}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {EA841C98-988C-4B47-9FCA-B0CED74EC958}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {EA841C98-988C-4B47-9FCA-B0CED74EC958}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {EA841C98-988C-4B47-9FCA-B0CED74EC958}.Release|Any CPU.ActiveCfg = Debug|Any CPU 49 | {EA841C98-988C-4B47-9FCA-B0CED74EC958}.Release|Any CPU.Build.0 = Debug|Any CPU 50 | {F8F250AA-A4E3-4DC2-863E-1348C98862D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {F8F250AA-A4E3-4DC2-863E-1348C98862D9}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {F8F250AA-A4E3-4DC2-863E-1348C98862D9}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {F8F250AA-A4E3-4DC2-863E-1348C98862D9}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {F0D4F963-A6F1-43D1-860C-5CDE07F2244F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {F0D4F963-A6F1-43D1-860C-5CDE07F2244F}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {F0D4F963-A6F1-43D1-860C-5CDE07F2244F}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {F0D4F963-A6F1-43D1-860C-5CDE07F2244F}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {8FD85916-B47D-4055-8661-718B9ABEFF6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {8FD85916-B47D-4055-8661-718B9ABEFF6B}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {8FD85916-B47D-4055-8661-718B9ABEFF6B}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {8FD85916-B47D-4055-8661-718B9ABEFF6B}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {7DC66E91-1D5D-4775-BE40-378AA2E432F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {7DC66E91-1D5D-4775-BE40-378AA2E432F8}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {7DC66E91-1D5D-4775-BE40-378AA2E432F8}.Release|Any CPU.ActiveCfg = Release|Any CPU 65 | {7DC66E91-1D5D-4775-BE40-378AA2E432F8}.Release|Any CPU.Build.0 = Release|Any CPU 66 | {58387294-2BDC-4799-919E-9F71883DA9B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 67 | {58387294-2BDC-4799-919E-9F71883DA9B5}.Debug|Any CPU.Build.0 = Debug|Any CPU 68 | {58387294-2BDC-4799-919E-9F71883DA9B5}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {58387294-2BDC-4799-919E-9F71883DA9B5}.Release|Any CPU.Build.0 = Release|Any CPU 70 | EndGlobalSection 71 | GlobalSection(SolutionProperties) = preSolution 72 | HideSolutionNode = FALSE 73 | EndGlobalSection 74 | GlobalSection(NestedProjects) = preSolution 75 | {F8F250AA-A4E3-4DC2-863E-1348C98862D9} = {96BB8378-7D63-49AF-BEF5-6016E234653D} 76 | {8FD85916-B47D-4055-8661-718B9ABEFF6B} = {96BB8378-7D63-49AF-BEF5-6016E234653D} 77 | {7DC66E91-1D5D-4775-BE40-378AA2E432F8} = {96BB8378-7D63-49AF-BEF5-6016E234653D} 78 | EndGlobalSection 79 | GlobalSection(ExtensibilityGlobals) = postSolution 80 | SolutionGuid = {A67740F5-352E-46BF-A4E3-B7D199B90811} 81 | EndGlobalSection 82 | EndGlobal 83 | -------------------------------------------------------------------------------- /src/Interstellar.Chromium/Browser.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.Chromium 2 | open System.Threading 3 | open System.Diagnostics 4 | open System 5 | open CefSharp 6 | open Interstellar 7 | open CefSharp.Web 8 | open System.Text 9 | 10 | /// CefSharp has an IBrowser interface which doesn't express all the functionality we need, but the several implementations of it do. 11 | /// They actually each share a lot of duplicated methods and code which aren't in the interface for some reason. This record extracts 12 | /// those mostly identical methods into a single shared point so we can still avoid violating DRY all the time 13 | type SharedChromiumBrowserInternals = { 14 | getPageTitle: unit -> string 15 | titleChanged: IEvent 16 | isBrowserInitializedChanged: IEvent 17 | } 18 | 19 | type Browser<'TWindow>(browser: IWebBrowser, browserInternals: SharedChromiumBrowserInternals, config: BrowserWindowConfig<'TWindow>) as this = 20 | let jsContextCreatedEvt = new Event<_>() 21 | 22 | // (primary) constructor 23 | do 24 | browser.RequestHandler <- new JSInjectionRequestHandler("window.interstellarBridge={'postMessage':function(message){CefSharp.PostMessage(message)}}") 25 | browserInternals.isBrowserInitializedChanged.Add (fun () -> 26 | if browser.IsBrowserInitialized then 27 | match config.address, config.html with 28 | | address, Some html -> (this :> IBrowser).LoadString (html, ?uri = address) 29 | | Some address, None -> (this :> IBrowser).Load address 30 | | None, None -> () 31 | if config.showDevTools then (this :> IBrowser).ShowDevTools() 32 | ) 33 | browser.RenderProcessMessageHandler <- { 34 | new IRenderProcessMessageHandler with 35 | member this.OnContextCreated(chromiumWebBrowser, browser, frame) = jsContextCreatedEvt.Trigger () 36 | member this.OnContextReleased(chromiumWebBrowser, browser, frame) = () 37 | member this.OnFocusedNodeChanged(chromiumWebBrowser, browser, frame, node) = () 38 | member this.OnUncaughtException(chromiumWebBrowser, browser, frame, exn) = () 39 | } 40 | 41 | member this.ChromiumBrowser = browser 42 | 43 | member this.AwaitPageLoadedThenReadyForJS () : Async> = async { 44 | if browser.IsLoading then 45 | do! Async.Ignore <| Async.AwaitEvent (this :> IBrowser).PageLoaded 46 | if not (browser.CanExecuteJavascriptInMainFrame) then 47 | return Async.AwaitEvent <| jsContextCreatedEvt.Publish 48 | else return async { () } 49 | } 50 | 51 | interface Interstellar.IBrowser with 52 | member this.AreDevToolsShowing = browser.GetBrowserHost().HasDevTools 53 | member this.Address = 54 | match browser.Address with 55 | | null -> None 56 | | value -> Some (new Uri(value)) 57 | member this.CloseDevTools () = browser.CloseDevTools () 58 | member this.CanGoBack = browser.GetBrowser().CanGoBack 59 | member this.CanGoForward = browser.GetBrowser().CanGoForward 60 | member this.CanShowDevTools = true 61 | member this.Engine = BrowserEngine.Chromium 62 | member this.ExecuteJavascript code = browser.ExecuteScriptAsync code 63 | member this.GoBack () = browser.GetBrowser().GoForward () 64 | member this.GoForward () = browser.GetBrowser().GoBack () 65 | member this.Load address = browser.Load address.OriginalString 66 | member this.LoadAsync address = async { 67 | (this :> IBrowser).Load address 68 | return! this.AwaitPageLoadedThenReadyForJS () 69 | } 70 | member this.LoadString (html, ?uri) = 71 | match uri with 72 | | Some uri -> 73 | // FIXME: at the moment, we can't use IWebBrowser.LoadHtml(string,string) because it installs a request handler to resolve the given URI, 74 | // which conflicts with the request handler _we_ have to install in order to inject Javascript (see JavascriptInjectionFilter and friends). 75 | // Not sure how to fix this yet, but I suspect it would involve writing our own handler that basically combines that two. 76 | raise (new PlatformNotSupportedException("Providing data along with a given origin URI is not yet supported in Chromium")) 77 | //browser.LoadHtml (html, uri) |> ignore 78 | | None -> 79 | // this one works fine because it still uses real URI behavior, thus not requiring any kind of custom URI handlers 80 | let data = new HtmlString(html, false) 81 | (this :> IBrowser).Load (new Uri(data.ToDataUriString())) 82 | member this.LoadStringAsync (html, ?uri) = async { 83 | (this :> IBrowser).LoadString (html, ?uri = uri) 84 | return! this.AwaitPageLoadedThenReadyForJS () 85 | } 86 | // note all the usages of member val to prevent cross-thread access issues 87 | [] 88 | member val JavascriptMessageRecieved = browser.JavascriptMessageReceived |> Event.map (fun x -> x.ConvertMessageTo()) 89 | member this.PageTitle = browserInternals.getPageTitle () 90 | [] 91 | member val PageTitleChanged = browserInternals.titleChanged 92 | [] 93 | member val PageLoaded = browser.FrameLoadEnd |> Event.map (fun x -> x :> EventArgs) 94 | member this.Reload () = browser.Reload () 95 | member this.ShowDevTools () = browser.ShowDevTools () -------------------------------------------------------------------------------- /src/Interstellar.MacOS.WebKit/Browser.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar.MacOS.Internal 2 | open System 3 | open AppKit 4 | open CoreGraphics 5 | open Foundation 6 | open Interstellar 7 | open WebKit 8 | type WebKitViewDialogueHandler() = 9 | inherit WKUIDelegate() 10 | 11 | override this.RunJavaScriptAlertPanel (webView, message, frame, completionHandler) = 12 | let alert = new NSAlert(AlertStyle = NSAlertStyle.Informational, InformativeText = message, MessageText = "Alert") 13 | let result = alert.RunModal () 14 | completionHandler.Invoke () 15 | 16 | namespace Interstellar.MacOS.WebKit 17 | open System 18 | open AppKit 19 | open CoreGraphics 20 | open Foundation 21 | open Interstellar 22 | open Interstellar.MacOS.Internal 23 | open WebKit 24 | 25 | module internal BrowserHelpers = 26 | let loadString (wkBrowser: WKWebView, html: string, (uri: Uri option)) = 27 | let nsUrl = match uri with | Some uri -> new NSUrl(uri.OriginalString) | None -> null 28 | wkBrowser.LoadHtmlString (html, nsUrl) |> ignore 29 | 30 | let load (wkBrowser: WKWebView, uri: Uri) = 31 | printfn "loading uri: %A" uri 32 | let nsUrl = new NSUrl(uri.OriginalString) 33 | if uri.Scheme = "file" then 34 | wkBrowser.LoadFileUrl (nsUrl, nsUrl) |> ignore 35 | else 36 | wkBrowser.LoadRequest (new NSUrlRequest(nsUrl)) |> ignore 37 | 38 | type Browser(config: BrowserWindowConfig) = 39 | let wkBrowser = new WKWebView(CGRect.Empty, new WKWebViewConfiguration()) 40 | let pageLoaded = new Event() 41 | let pageTitleChanged = new Event() 42 | let jsMsgRecieved = new Event() 43 | let mutable pageTitleObserverHandle = null 44 | 45 | static let wkBridgeName = "interstellarWkBridge" 46 | 47 | do 48 | wkBrowser.NavigationDelegate <- { 49 | new WKNavigationDelegate() with 50 | member this.DidFinishNavigation (view, nav) = 51 | pageLoaded.Trigger (new EventArgs()) 52 | pageTitleChanged.Trigger view.Title 53 | } 54 | wkBrowser.UIDelegate <- new WebKitViewDialogueHandler() 55 | 56 | // install JS->.Net bridge 57 | wkBrowser.Configuration.UserContentController.AddScriptMessageHandler ({ 58 | new WKScriptMessageHandler() with 59 | member this.DidReceiveScriptMessage (_, msg: WKScriptMessage) = 60 | let msgAsString = (msg.Body :?> NSString).ToString() 61 | jsMsgRecieved.Trigger msgAsString 62 | }, wkBridgeName) 63 | // make it accessible in JS from window.interstellarBridge 64 | wkBrowser.Configuration.UserContentController.AddUserScript 65 | (new WKUserScript(new NSString (sprintf "window.interstellarBridge={'postMessage':function(message){window.webkit.messageHandlers.%s.postMessage(message)}}" wkBridgeName), 66 | WKUserScriptInjectionTime.AtDocumentStart, false)) 67 | 68 | // TODO: dispose this 69 | pageTitleObserverHandle <- 70 | wkBrowser.AddObserver 71 | (new NSString("title"), NSKeyValueObservingOptions.New, fun x -> 72 | pageTitleChanged.Trigger (wkBrowser.Title)) 73 | 74 | match config.address, config.html with 75 | | address, Some html -> BrowserHelpers.loadString (wkBrowser, html, address) 76 | | Some address, None -> BrowserHelpers.load (wkBrowser, address) 77 | | None, None -> () 78 | 79 | member this.WebKitBrowser = wkBrowser 80 | 81 | member this.AwaitLoadedThenJSReady () = async { 82 | if wkBrowser.IsLoading then 83 | let! _ = Async.AwaitEvent (this :> IBrowser).PageLoaded 84 | () 85 | // it appears that in WebKit it's safe to execute JS right away, so I guess we're good 86 | return async { () } 87 | } 88 | 89 | interface IBrowser with 90 | member this.Address = 91 | match wkBrowser.Url with 92 | | null -> None 93 | | url -> Some (new Uri(url.AbsoluteString)) 94 | member this.AreDevToolsShowing = false 95 | member this.CloseDevTools () = () 96 | member this.CanGoBack = wkBrowser.CanGoBack 97 | member this.CanGoForward = wkBrowser.CanGoForward 98 | member this.CanShowDevTools = false 99 | member this.Engine = BrowserEngine.AppleWebKit 100 | member this.ExecuteJavascript code = wkBrowser.EvaluateJavaScript (code, null) 101 | member this.Load address = BrowserHelpers.load (wkBrowser, address) 102 | member this.LoadAsync address = async { 103 | (this :> IBrowser).Load address 104 | return! this.AwaitLoadedThenJSReady () 105 | } 106 | member this.LoadString (html, ?uri) = BrowserHelpers.loadString (wkBrowser, html, uri) 107 | member this.LoadStringAsync (html, ?uri) = async { 108 | (this :> IBrowser).LoadString (html, ?uri = uri) 109 | return! this.AwaitLoadedThenJSReady () 110 | } 111 | member this.GoBack () = wkBrowser.GoBack () |> ignore 112 | member this.GoForward () = wkBrowser.GoForward () |> ignore 113 | [] 114 | member val JavascriptMessageRecieved = jsMsgRecieved.Publish 115 | member this.PageTitle = wkBrowser.Title 116 | [] 117 | member val PageLoaded : IEvent<_> = pageLoaded.Publish 118 | member this.Reload () = wkBrowser.Reload () |> ignore 119 | [] 120 | member val PageTitleChanged : IEvent<_> = pageTitleChanged.Publish 121 | // there's no way that I know of to programmatically open the WKWebView inspector: https://stackoverflow.com/questions/25200116/how-to-show-the-inspector-within-your-wkwebview-based-desktop-app 122 | member this.ShowDevTools () = () -------------------------------------------------------------------------------- /paket.lock: -------------------------------------------------------------------------------- 1 | RESTRICTION: || (== net472) (== net48) (== net5.0-windows7.0) (== net6.0-macos) (== net6.0-windows7.0) (== netstandard2.0) 2 | NUGET 3 | remote: https://api.nuget.org/v3/index.json 4 | BlackFox.MasterOfFoo (1.0.6) 5 | FSharp.Core (>= 4.0.0.1) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= net45)) (&& (== net6.0-macos) (>= net45)) (&& (== net6.0-windows7.0) (>= net45)) (&& (== netstandard2.0) (>= net45)) 6 | FSharp.Core (>= 4.2.3) - restriction: || (&& (== net472) (< net45)) (&& (== net48) (< net45)) (== net5.0-windows7.0) (== net6.0-macos) (== net6.0-windows7.0) (== netstandard2.0) 7 | cef.redist.x64 (86.0.24) - restriction: || (== net472) (== net48) (== net5.0-windows7.0) (== net6.0-macos) (== net6.0-windows7.0) (&& (== netstandard2.0) (>= net452)) (&& (== netstandard2.0) (>= netcoreapp3.0)) 8 | cef.redist.x86 (86.0.24) - restriction: || (== net472) (== net48) (== net5.0-windows7.0) (== net6.0-macos) (== net6.0-windows7.0) (&& (== netstandard2.0) (>= net452)) (&& (== netstandard2.0) (>= netcoreapp3.0)) 9 | CefSharp.Common (86.0.241) - restriction: || (== net472) (== net48) (== net5.0-windows7.0) (== net6.0-macos) (== net6.0-windows7.0) (&& (== netstandard2.0) (>= net452)) (&& (== netstandard2.0) (>= netcoreapp3.0)) 10 | cef.redist.x64 (86.0.24) 11 | cef.redist.x86 (86.0.24) 12 | CefSharp.WinForms (86.0.241) 13 | CefSharp.Common (86.0.241) - restriction: || (== net472) (== net48) (== net5.0-windows7.0) (== net6.0-macos) (== net6.0-windows7.0) (&& (== netstandard2.0) (>= net452)) (&& (== netstandard2.0) (>= netcoreapp3.0)) 14 | CefSharp.Wpf (86.0.241) 15 | CefSharp.Common (86.0.241) - restriction: || (== net472) (== net48) (== net5.0-windows7.0) (== net6.0-macos) (== net6.0-windows7.0) (&& (== netstandard2.0) (>= net452)) (&& (== netstandard2.0) (>= netcoreapp3.0)) 16 | FSharp.Core (6.0.5) 17 | NuGet.Build.Tasks.Pack (6.2) 18 | System.Buffers (4.5.1) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net5.0-windows7.0) (< netcoreapp3.1)) (&& (== net6.0-macos) (>= net461)) (&& (== net6.0-macos) (< netcoreapp3.1)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== net6.0-windows7.0) (< netcoreapp3.1)) (== netstandard2.0) 19 | System.Memory (4.5.5) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net5.0-windows7.0) (< netcoreapp3.1)) (&& (== net6.0-macos) (>= net461)) (&& (== net6.0-macos) (< netcoreapp3.1)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== net6.0-windows7.0) (< netcoreapp3.1)) (== netstandard2.0) 20 | System.Buffers (>= 4.5.1) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= monotouch)) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net5.0-windows7.0) (< netcoreapp2.0)) (&& (== net5.0-windows7.0) (< netstandard1.1)) (&& (== net5.0-windows7.0) (< netstandard2.0)) (&& (== net5.0-windows7.0) (>= xamarinios)) (&& (== net5.0-windows7.0) (>= xamarinmac)) (&& (== net5.0-windows7.0) (>= xamarintvos)) (&& (== net5.0-windows7.0) (>= xamarinwatchos)) (== net6.0-macos) (&& (== net6.0-windows7.0) (>= monotouch)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== net6.0-windows7.0) (< netcoreapp2.0)) (&& (== net6.0-windows7.0) (< netstandard1.1)) (&& (== net6.0-windows7.0) (< netstandard2.0)) (&& (== net6.0-windows7.0) (>= xamarinios)) (&& (== net6.0-windows7.0) (>= xamarinmac)) (&& (== net6.0-windows7.0) (>= xamarintvos)) (&& (== net6.0-windows7.0) (>= xamarinwatchos)) (== netstandard2.0) 21 | System.Numerics.Vectors (>= 4.4) - restriction: || (&& (== net472) (< net45)) (&& (== net48) (< net45)) (&& (== net5.0-windows7.0) (< netcoreapp2.0)) (&& (== net6.0-macos) (< netcoreapp2.0) (< xamarinmac)) (&& (== net6.0-windows7.0) (< netcoreapp2.0)) (== netstandard2.0) 22 | System.Numerics.Vectors (>= 4.5) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net6.0-macos) (>= net461)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== netstandard2.0) (>= net461)) 23 | System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= monotouch)) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net5.0-windows7.0) (< netcoreapp2.0)) (&& (== net5.0-windows7.0) (< netcoreapp2.1)) (&& (== net5.0-windows7.0) (< netstandard1.1)) (&& (== net5.0-windows7.0) (< netstandard2.0)) (&& (== net5.0-windows7.0) (>= uap10.1)) (&& (== net5.0-windows7.0) (>= xamarinios)) (&& (== net5.0-windows7.0) (>= xamarinmac)) (&& (== net5.0-windows7.0) (>= xamarintvos)) (&& (== net5.0-windows7.0) (>= xamarinwatchos)) (== net6.0-macos) (&& (== net6.0-windows7.0) (>= monotouch)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== net6.0-windows7.0) (< netcoreapp2.0)) (&& (== net6.0-windows7.0) (< netcoreapp2.1)) (&& (== net6.0-windows7.0) (< netstandard1.1)) (&& (== net6.0-windows7.0) (< netstandard2.0)) (&& (== net6.0-windows7.0) (>= uap10.1)) (&& (== net6.0-windows7.0) (>= xamarinios)) (&& (== net6.0-windows7.0) (>= xamarinmac)) (&& (== net6.0-windows7.0) (>= xamarintvos)) (&& (== net6.0-windows7.0) (>= xamarinwatchos)) (== netstandard2.0) 24 | System.Numerics.Vectors (4.5) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net5.0-windows7.0) (< netcoreapp2.0)) (&& (== net6.0-macos) (>= net461)) (&& (== net6.0-macos) (< netcoreapp2.0) (< xamarinmac)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== net6.0-windows7.0) (< netcoreapp2.0)) (== netstandard2.0) 25 | System.Runtime.CompilerServices.Unsafe (6.0) 26 | System.Text.Encodings.Web (6.0) 27 | System.Buffers (>= 4.5.1) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net5.0-windows7.0) (< netcoreapp3.1)) (&& (== net6.0-macos) (>= net461)) (&& (== net6.0-macos) (< netcoreapp3.1)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== net6.0-windows7.0) (< netcoreapp3.1)) (== netstandard2.0) 28 | System.Memory (>= 4.5.4) - restriction: || (== net472) (== net48) (&& (== net5.0-windows7.0) (>= net461)) (&& (== net5.0-windows7.0) (< netcoreapp3.1)) (&& (== net6.0-macos) (>= net461)) (&& (== net6.0-macos) (< netcoreapp3.1)) (&& (== net6.0-windows7.0) (>= net461)) (&& (== net6.0-windows7.0) (< netcoreapp3.1)) (== netstandard2.0) 29 | System.Runtime.CompilerServices.Unsafe (>= 6.0) 30 | 31 | GROUP Build 32 | NUGET 33 | remote: https://api.nuget.org/v3/index.json 34 | FSharp.Core (5.0) - restriction: >= netstandard2.0 35 | FSharp.DependencyManager.Paket (7.1.5) 36 | FSharp.Core (5.0) - restriction: >= netstandard2.0 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # globs 2 | Makefile.in 3 | *.userprefs 4 | *.usertasks 5 | config.make 6 | config.status 7 | aclocal.m4 8 | install-sh 9 | autom4te.cache/ 10 | *.tar.gz 11 | tarballs/ 12 | test-results/ 13 | 14 | # Mac bundle stuff 15 | *.dmg 16 | *.app 17 | 18 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 19 | # General 20 | .DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | 28 | # Thumbnails 29 | ._* 30 | 31 | # Files that might appear in the root of a volume 32 | .DocumentRevisions-V100 33 | .fseventsd 34 | .Spotlight-V100 35 | .TemporaryItems 36 | .Trashes 37 | .VolumeIcon.icns 38 | .com.apple.timemachine.donotpresent 39 | 40 | # Directories potentially created on remote AFP share 41 | .AppleDB 42 | .AppleDesktop 43 | Network Trash Folder 44 | Temporary Items 45 | .apdisk 46 | 47 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 48 | # Windows thumbnail cache files 49 | Thumbs.db 50 | ehthumbs.db 51 | ehthumbs_vista.db 52 | 53 | # Dump file 54 | *.stackdump 55 | 56 | # Folder config file 57 | [Dd]esktop.ini 58 | 59 | # Recycle Bin used on file shares 60 | $RECYCLE.BIN/ 61 | 62 | # Windows Installer files 63 | *.cab 64 | *.msi 65 | *.msix 66 | *.msm 67 | *.msp 68 | 69 | # Windows shortcuts 70 | *.lnk 71 | 72 | # content below from: https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 73 | ## Ignore Visual Studio temporary files, build results, and 74 | ## files generated by popular Visual Studio add-ons. 75 | ## 76 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 77 | 78 | # User-specific files 79 | *.suo 80 | *.user 81 | *.userosscache 82 | *.sln.docstates 83 | 84 | # User-specific files (MonoDevelop/Xamarin Studio) 85 | *.userprefs 86 | 87 | # Build results 88 | [Dd]ebug/ 89 | [Dd]ebugPublic/ 90 | [Rr]elease/ 91 | [Rr]eleases/ 92 | x64/ 93 | x86/ 94 | bld/ 95 | [Bb]in/ 96 | [Oo]bj/ 97 | [Ll]og/ 98 | 99 | # Visual Studio 2015/2017 cache/options directory 100 | .vs/ 101 | # Uncomment if you have tasks that create the project's static files in wwwroot 102 | #wwwroot/ 103 | 104 | # Visual Studio 2017 auto generated files 105 | Generated\ Files/ 106 | 107 | # MSTest test Results 108 | [Tt]est[Rr]esult*/ 109 | [Bb]uild[Ll]og.* 110 | 111 | # NUNIT 112 | *.VisualState.xml 113 | TestResult.xml 114 | 115 | # Build Results of an ATL Project 116 | [Dd]ebugPS/ 117 | [Rr]eleasePS/ 118 | dlldata.c 119 | 120 | # Benchmark Results 121 | BenchmarkDotNet.Artifacts/ 122 | 123 | # .NET Core 124 | project.lock.json 125 | project.fragment.lock.json 126 | artifacts/ 127 | 128 | # StyleCop 129 | StyleCopReport.xml 130 | 131 | # Files built by Visual Studio 132 | *_i.c 133 | *_p.c 134 | *_h.h 135 | *.ilk 136 | *.meta 137 | *.obj 138 | *.iobj 139 | *.pch 140 | *.pdb 141 | *.ipdb 142 | *.pgc 143 | *.pgd 144 | *.rsp 145 | *.sbr 146 | *.tlb 147 | *.tli 148 | *.tlh 149 | *.tmp 150 | *.tmp_proj 151 | *_wpftmp.csproj 152 | *.log 153 | *.vspscc 154 | *.vssscc 155 | .builds 156 | *.pidb 157 | *.svclog 158 | *.scc 159 | 160 | # Chutzpah Test files 161 | _Chutzpah* 162 | 163 | # Visual C++ cache files 164 | ipch/ 165 | *.aps 166 | *.ncb 167 | *.opendb 168 | *.opensdf 169 | *.sdf 170 | *.cachefile 171 | *.VC.db 172 | *.VC.VC.opendb 173 | 174 | # Visual Studio profiler 175 | *.psess 176 | *.vsp 177 | *.vspx 178 | *.sap 179 | 180 | # Visual Studio Trace Files 181 | *.e2e 182 | 183 | # TFS 2012 Local Workspace 184 | $tf/ 185 | 186 | # Guidance Automation Toolkit 187 | *.gpState 188 | 189 | # ReSharper is a .NET coding add-in 190 | _ReSharper*/ 191 | *.[Rr]e[Ss]harper 192 | *.DotSettings.user 193 | 194 | # JustCode is a .NET coding add-in 195 | .JustCode 196 | 197 | # TeamCity is a build add-in 198 | _TeamCity* 199 | 200 | # DotCover is a Code Coverage Tool 201 | *.dotCover 202 | 203 | # AxoCover is a Code Coverage Tool 204 | .axoCover/* 205 | !.axoCover/settings.json 206 | 207 | # Visual Studio code coverage results 208 | *.coverage 209 | *.coveragexml 210 | 211 | # NCrunch 212 | _NCrunch_* 213 | .*crunch*.local.xml 214 | nCrunchTemp_* 215 | 216 | # MightyMoose 217 | *.mm.* 218 | AutoTest.Net/ 219 | 220 | # Web workbench (sass) 221 | .sass-cache/ 222 | 223 | # Installshield output folder 224 | [Ee]xpress/ 225 | 226 | # DocProject is a documentation generator add-in 227 | DocProject/buildhelp/ 228 | DocProject/Help/*.HxT 229 | DocProject/Help/*.HxC 230 | DocProject/Help/*.hhc 231 | DocProject/Help/*.hhk 232 | DocProject/Help/*.hhp 233 | DocProject/Help/Html2 234 | DocProject/Help/html 235 | 236 | # Click-Once directory 237 | publish/ 238 | 239 | # Publish Web Output 240 | *.[Pp]ublish.xml 241 | *.azurePubxml 242 | # Note: Comment the next line if you want to checkin your web deploy settings, 243 | # but database connection strings (with potential passwords) will be unencrypted 244 | *.pubxml 245 | *.publishproj 246 | 247 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 248 | # checkin your Azure Web App publish settings, but sensitive information contained 249 | # in these scripts will be unencrypted 250 | PublishScripts/ 251 | 252 | # NuGet Packages 253 | *.nupkg 254 | # The packages folder can be ignored because of Package Restore 255 | **/[Pp]ackages/ 256 | # except build/, which is used as an MSBuild target. 257 | !**/[Pp]ackages/build/ 258 | # Uncomment if necessary however generally it will be regenerated when needed 259 | #!**/[Pp]ackages/repositories.config 260 | # NuGet v3's project.json files produces more ignorable files 261 | *.nuget.props 262 | *.nuget.targets 263 | 264 | # Microsoft Azure Build Output 265 | csx/ 266 | *.build.csdef 267 | 268 | # Microsoft Azure Emulator 269 | ecf/ 270 | rcf/ 271 | 272 | # Windows Store app package directories and files 273 | AppPackages/ 274 | BundleArtifacts/ 275 | Package.StoreAssociation.xml 276 | _pkginfo.txt 277 | *.appx 278 | 279 | # Visual Studio cache files 280 | # files ending in .cache can be ignored 281 | *.[Cc]ache 282 | # but keep track of directories ending in .cache 283 | !*.[Cc]ache/ 284 | 285 | # Others 286 | ClientBin/ 287 | ~$* 288 | *~ 289 | *.dbmdl 290 | *.dbproj.schemaview 291 | *.jfm 292 | *.pfx 293 | *.publishsettings 294 | orleans.codegen.cs 295 | 296 | # Including strong name files can present a security risk 297 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 298 | #*.snk 299 | 300 | # Since there are multiple workflows, uncomment next line to ignore bower_components 301 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 302 | #bower_components/ 303 | 304 | # RIA/Silverlight projects 305 | Generated_Code/ 306 | 307 | # Backup & report files from converting an old project file 308 | # to a newer Visual Studio version. Backup files are not needed, 309 | # because we have git ;-) 310 | _UpgradeReport_Files/ 311 | Backup*/ 312 | UpgradeLog*.XML 313 | UpgradeLog*.htm 314 | ServiceFabricBackup/ 315 | *.rptproj.bak 316 | 317 | # SQL Server files 318 | *.mdf 319 | *.ldf 320 | *.ndf 321 | 322 | # Business Intelligence projects 323 | *.rdl.data 324 | *.bim.layout 325 | *.bim_*.settings 326 | *.rptproj.rsuser 327 | 328 | # Microsoft Fakes 329 | FakesAssemblies/ 330 | 331 | # GhostDoc plugin setting file 332 | *.GhostDoc.xml 333 | 334 | # Node.js Tools for Visual Studio 335 | .ntvs_analysis.dat 336 | node_modules/ 337 | 338 | # Visual Studio 6 build log 339 | *.plg 340 | 341 | # Visual Studio 6 workspace options file 342 | *.opt 343 | 344 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 345 | *.vbw 346 | 347 | # Visual Studio LightSwitch build output 348 | **/*.HTMLClient/GeneratedArtifacts 349 | **/*.DesktopClient/GeneratedArtifacts 350 | **/*.DesktopClient/ModelManifest.xml 351 | **/*.Server/GeneratedArtifacts 352 | **/*.Server/ModelManifest.xml 353 | _Pvt_Extensions 354 | 355 | # Paket dependency manager 356 | paket-files/ 357 | .paket/Paket.Restore.targets 358 | .paket/load/ 359 | .paket/ 360 | 361 | # FAKE - F# Make 362 | .fake/ 363 | 364 | # JetBrains Rider 365 | .idea/ 366 | *.sln.iml 367 | 368 | # CodeRush personal settings 369 | .cr/personal 370 | 371 | # Python Tools for Visual Studio (PTVS) 372 | __pycache__/ 373 | *.pyc 374 | 375 | # Cake - Uncomment if you are using it 376 | # tools/** 377 | # !tools/packages.config 378 | 379 | # Tabs Studio 380 | *.tss 381 | 382 | # Telerik's JustMock configuration file 383 | *.jmconfig 384 | 385 | # BizTalk build output 386 | *.btp.cs 387 | *.btm.cs 388 | *.odx.cs 389 | *.xsd.cs 390 | 391 | # OpenCover UI analysis results 392 | OpenCover/ 393 | 394 | # Azure Stream Analytics local run output 395 | ASALocalRun/ 396 | 397 | # MSBuild Binary and Structured Log 398 | *.binlog 399 | 400 | # NVidia Nsight GPU debugger configuration file 401 | *.nvuser 402 | 403 | # MFractors (Xamarin productivity tool) working folder 404 | .mfractor/ 405 | 406 | # Local History for Visual Studio 407 | .localhistory/ 408 | 409 | # Ionide for Visual Studio Code 410 | .ionide/ 411 | 412 | # fsdocs 413 | .fsdocs/ 414 | output/ 415 | temp/ 416 | 417 | # CEF 418 | GPUCache/ -------------------------------------------------------------------------------- /templates/minimal/src/InterstellarApp.Core/Library.fs: -------------------------------------------------------------------------------- 1 | namespace InterstellarApp 2 | open System 3 | open System.Diagnostics 4 | open System.IO 5 | open System.Reflection 6 | open System.Runtime.Versioning 7 | open System.Threading 8 | open Interstellar 9 | 10 | module AppletIds = 11 | let [] Calculator = "calculator" 12 | let [] InjectedContent = "injectedContent" 13 | let [] InterstellarDetector = "detector" 14 | let [] InterWindowCommunication = "interwindow" 15 | 16 | module BrowserApp = 17 | let runtimeFramework = Assembly.GetEntryAssembly().GetCustomAttribute().FrameworkName 18 | let detectorPageUrl = new Uri("https://gist.githack.com/jwosty/239408aaffd106a26dc2161f86caa641/raw/5af54d0f4c51634040ea3859ca86032694afc934/interstellardetector.html") 19 | 20 | let defaultBrowserWindowConfig<'TWindow> = { 21 | defaultBrowserWindowConfig<'TWindow> with 22 | title = WindowTitle.FromPageTitle (fun pageTitle w -> async { 23 | let tail = if String.IsNullOrWhiteSpace pageTitle then "" else sprintf " - %s" pageTitle 24 | return sprintf "InterstellarApp (%A)%s" w.Browser.Engine tail 25 | }) 26 | } 27 | 28 | let showCalculatorWindow mainCtx (createWindow: BrowserWindowCreator<_>) = async { 29 | let page = """ 30 | 31 | 32 | 33 | 34 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | = 65 | 66 | 67 | 68 | """ 69 | let window = createWindow { defaultBrowserWindowConfig with html = Some page } 70 | do! window.Show () 71 | return window 72 | } 73 | 74 | let fileWriteTextAsync path (string: string) = async { 75 | use fileOut = File.CreateText path 76 | do! Async.AwaitTask (fileOut.WriteAsync string) 77 | } 78 | 79 | let showInjectedContentWindow mainCtx (createWindow: BrowserWindowCreator<_>) = async { 80 | // to make this one a little more interesting, lets show the content from a temp file instead of the data-style URI like the other examples, 81 | // so it can feel a bit more like a "real page" 82 | let filePath = Path.Combine (Path.GetTempPath(), Path.ChangeExtension (Guid.NewGuid().ToString(), ".html")) 83 | let fileUri = Uri(sprintf "file://%s" filePath) 84 | let content = sprintf """ 85 | 86 | 87 | 88 | 89 | If you see this title, Javascript isn't working 90 | 91 | 97 | 98 | 99 |

This file can be found at %s 100 |

UI host: Unknown

101 |

Browser engine: Unknown

102 |

Runtime framework: Unknown

103 | 104 | """ fileUri.AbsoluteUri fileUri.AbsoluteUri 105 | Trace.WriteLine (sprintf "temp html file path: %s" filePath) 106 | do! fileWriteTextAsync filePath content 107 | let window = createWindow { defaultBrowserWindowConfig with showDevTools = true } 108 | do! window.Show () 109 | let! handleToAwaitJSReady = window.Browser.LoadAsync fileUri 110 | do! handleToAwaitJSReady 111 | // NOTE: depending on the exact platform, it may or may not be safe to assume that the DOM exists right after the 112 | // JS context has been created. We must first check whether or not it's loaded. In the event it is, we just execute 113 | // the code directly. Otherwise, we add our cofde in a handler to DOMContentLoaded. 114 | window.Browser.ExecuteJavascriptf 115 | "function injectValues() { 116 | document.getElementById('runtimeFramework').textContent='%s' 117 | document.getElementById('platform').textContent='%A' 118 | document.getElementById('browserEngine').textContent='%A' } 119 | if (document.readyState !== 'loading') { 120 | injectValues() 121 | } else { 122 | document.addEventListener('DOMContentLoaded', injectValues, false) 123 | }" 124 | runtimeFramework window.Platform window.Browser.Engine 125 | return window 126 | } 127 | 128 | let showDetectorWindow mainCtx (createWindow: BrowserWindowCreator<_>) = async { 129 | let window = createWindow { defaultBrowserWindowConfig with address = Some detectorPageUrl } 130 | do! window.Show () 131 | return window 132 | } 133 | 134 | let runCrossCommunicatingWindows (mainCtx: SynchronizationContext) (createWindow: BrowserWindowCreator<_>) = async { 135 | let inputPage = """ 136 | 137 | 138 | 139 | 140 | Input window 141 | 148 | 149 | 150 | 151 | 152 | """ 153 | let outputPage = """ 154 | 155 | 156 | 157 | 158 | 165 | 166 | 167 |

Reversed input text:

168 | 171 | 172 | """ 173 | let inputWindow = createWindow { defaultBrowserWindowConfig with html = Some inputPage } 174 | let outputWindow = createWindow { defaultBrowserWindowConfig with html = Some outputPage } 175 | 176 | inputWindow.Browser.JavascriptMessageRecieved.Add (fun msg -> 177 | mainCtx.Post (SendOrPostCallback(fun _ -> 178 | if outputWindow.IsShowing then 179 | // if we didn't use a function that escapes the payload for us, it's possible to inject arbitrary javascript. 180 | // For example, modify this line to use Browser.ExecuteJavascript and sprintf instead of ExecuteJavascriptf, 181 | // then paste this malicous payload into the input text box when you run the app: //;)'olleh'(trela;)'oof 182 | outputWindow.Browser.ExecuteJavascriptf "updateOutput('%s')" (String (Array.rev (msg.ToCharArray ()))) 183 | ), null) 184 | ) 185 | 186 | do! inputWindow.Show () 187 | do! outputWindow.Show () 188 | 189 | Async.Start <| async { 190 | do! Async.AwaitEvent inputWindow.Closed 191 | do! Async.SwitchToContext mainCtx 192 | outputWindow.Close () 193 | } 194 | Async.Start <| async { 195 | do! Async.AwaitEvent outputWindow.Closed 196 | do! Async.SwitchToContext mainCtx 197 | inputWindow.Close () 198 | } 199 | 200 | // await both closed 201 | do! Async.Ignore <| Async.Parallel [Async.AwaitEvent inputWindow.Closed; Async.AwaitEvent outputWindow.Closed] 202 | } 203 | 204 | let appletSelectorWindow onMainWindowCreated mainCtx (createWindow: BrowserWindowCreator<_>) = async { 205 | let page = sprintf """ 206 | 207 | 208 | 209 | 210 | 215 | 216 | 217 | 218 |
219 | 220 |
221 | - %s 222 |
223 | 224 | 225 | """ AppletIds.Calculator AppletIds.InjectedContent AppletIds.InterstellarDetector detectorPageUrl.AbsoluteUri AppletIds.InterWindowCommunication 226 | let selectorWindow = createWindow { defaultBrowserWindowConfig with html = Some page } 227 | onMainWindowCreated selectorWindow 228 | do! selectorWindow.Show () 229 | selectorWindow.Browser.JavascriptMessageRecieved.Add (fun msg -> 230 | Async.Start <| async { 231 | do! Async.SwitchToContext mainCtx 232 | match msg with 233 | | AppletIds.Calculator -> do! Async.Ignore (showCalculatorWindow mainCtx createWindow) 234 | | AppletIds.InjectedContent -> do! Async.Ignore (showInjectedContentWindow mainCtx createWindow) 235 | | AppletIds.InterstellarDetector -> do! Async.Ignore (showDetectorWindow mainCtx createWindow) 236 | | AppletIds.InterWindowCommunication -> do! runCrossCommunicatingWindows mainCtx createWindow 237 | | msg -> Trace.WriteLine (sprintf "Bad message: %s" msg) 238 | }) 239 | return selectorWindow 240 | } 241 | 242 | let app onMainWindowCreated : BrowserApp<'TWindow> = BrowserApp.create (fun mainCtx createWindow -> async { 243 | let! mainWindow = appletSelectorWindow onMainWindowCreated mainCtx createWindow 244 | do! Async.AwaitEvent mainWindow.Closed 245 | }) -------------------------------------------------------------------------------- /Examples/Examples.SharedCode/Library.fs: -------------------------------------------------------------------------------- 1 | namespace Examples.SharedCode 2 | open System 3 | open System.Diagnostics 4 | open System.IO 5 | open System.Reflection 6 | open System.Runtime.Versioning 7 | open System.Threading 8 | open Interstellar 9 | 10 | module AppletIds = 11 | let [] Calculator = "calculator" 12 | let [] InjectedContent = "injectedContent" 13 | let [] InterstellarDetector = "detector" 14 | let [] InterWindowCommunication = "interwindow" 15 | 16 | module SimpleBrowserApp = 17 | let runtimeFramework = Assembly.GetEntryAssembly().GetCustomAttribute().FrameworkName 18 | let detectorPageUrl = new Uri("https://gist.githack.com/jwosty/239408aaffd106a26dc2161f86caa641/raw/5af54d0f4c51634040ea3859ca86032694afc934/interstellardetector.html") 19 | 20 | let defaultBrowserWindowConfig<'TWindow> = { 21 | defaultBrowserWindowConfig<'TWindow> with 22 | title = WindowTitle.FromPageTitle (fun pageTitle w -> async { 23 | let tail = if String.IsNullOrWhiteSpace pageTitle then "" else sprintf " - %s" pageTitle 24 | return sprintf "InterstellarApp (%A)%s" w.Browser.Engine tail 25 | }) 26 | } 27 | 28 | let showCalculatorWindow mainCtx (createWindow: BrowserWindowCreator<_>) = async { 29 | let page = """ 30 | 31 | 32 | 33 | 34 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | = 65 | 66 | 67 | 68 | """ 69 | let window = createWindow { defaultBrowserWindowConfig with html = Some page } 70 | do! window.Show () 71 | return window 72 | } 73 | 74 | let fileWriteTextAsync path (string: string) = async { 75 | use fileOut = File.CreateText path 76 | do! Async.AwaitTask (fileOut.WriteAsync string) 77 | } 78 | 79 | let showInjectedContentWindow mainCtx (createWindow: BrowserWindowCreator<_>) = async { 80 | // to make this one a little more interesting, lets show the content from a temp file instead of the data-style URI like the other examples, 81 | // so it can feel a bit more like a "real page" 82 | let filePath = Path.Combine (Path.GetTempPath(), Path.ChangeExtension (Guid.NewGuid().ToString(), ".html")) 83 | let fileUri = Uri(sprintf "file://%s" filePath) 84 | let content = sprintf """ 85 | 86 | 87 | 88 | 89 | If you see this title, Javascript isn't working 90 | 91 | 97 | 98 | 99 |

This file can be found at %s 100 |

UI host: Unknown

101 |

Browser engine: Unknown

102 |

Runtime framework: Unknown

103 | 104 | """ fileUri.AbsoluteUri fileUri.AbsoluteUri 105 | Trace.WriteLine (sprintf "temp html file path: %s" filePath) 106 | do! fileWriteTextAsync filePath content 107 | let window = createWindow { defaultBrowserWindowConfig with showDevTools = true } 108 | do! window.Show () 109 | let! handleToAwaitJSReady = window.Browser.LoadAsync fileUri 110 | do! handleToAwaitJSReady 111 | // NOTE: depending on the exact platform, it may or may not be safe to assume that the DOM exists right after the 112 | // JS context has been created. We must first check whether or not it's loaded. In the event it is, we just execute 113 | // the code directly. Otherwise, we add our code in a handler to DOMContentLoaded. 114 | window.Browser.ExecuteJavascriptf 115 | "function injectValues() { 116 | document.getElementById('runtimeFramework').textContent='%s' 117 | document.getElementById('platform').textContent='%A' 118 | document.getElementById('browserEngine').textContent='%A' } 119 | if (document.readyState !== 'loading') { 120 | injectValues() 121 | } else { 122 | document.addEventListener('DOMContentLoaded', injectValues, false) 123 | }" 124 | runtimeFramework window.Platform window.Browser.Engine 125 | return window 126 | } 127 | 128 | let showDetectorWindow mainCtx (createWindow: BrowserWindowCreator<_>) = async { 129 | let window = createWindow { defaultBrowserWindowConfig with address = Some detectorPageUrl } 130 | do! window.Show () 131 | return window 132 | } 133 | 134 | let runCrossCommunicatingWindows (mainCtx: SynchronizationContext) (createWindow: BrowserWindowCreator<_>) = async { 135 | let inputPage = """ 136 | 137 | 138 | 139 | 140 | Input window 141 | 148 | 149 | 150 | 151 | 152 | """ 153 | let outputPage = """ 154 | 155 | 156 | 157 | 158 | 165 | 166 | 167 |

Reversed input text:

168 | 171 | 172 | """ 173 | let inputWindow = createWindow { defaultBrowserWindowConfig with html = Some inputPage } 174 | let outputWindow = createWindow { defaultBrowserWindowConfig with html = Some outputPage } 175 | 176 | inputWindow.Browser.JavascriptMessageRecieved.Add (fun msg -> 177 | mainCtx.Post (SendOrPostCallback(fun _ -> 178 | if outputWindow.IsShowing then 179 | // if we didn't use a function that escapes the payload for us, it's possible to inject arbitrary javascript. 180 | // For example, modify this line to use Browser.ExecuteJavascript and sprintf instead of ExecuteJavascriptf, 181 | // then paste this malicous payload into the input text box when you run the app: //;)'olleh'(trela;)'oof 182 | outputWindow.Browser.ExecuteJavascriptf "updateOutput('%s')" (String (Array.rev (msg.ToCharArray ()))) 183 | ), null) 184 | ) 185 | 186 | do! inputWindow.Show () 187 | do! outputWindow.Show () 188 | 189 | Async.Start <| async { 190 | do! Async.AwaitEvent inputWindow.Closed 191 | do! Async.SwitchToContext mainCtx 192 | outputWindow.Close () 193 | } 194 | Async.Start <| async { 195 | do! Async.AwaitEvent outputWindow.Closed 196 | do! Async.SwitchToContext mainCtx 197 | inputWindow.Close () 198 | } 199 | 200 | // await both closed 201 | do! Async.Ignore <| Async.Parallel [Async.AwaitEvent inputWindow.Closed; Async.AwaitEvent outputWindow.Closed] 202 | } 203 | 204 | let appletSelectorWindow onMainWindowCreated mainCtx (createWindow: BrowserWindowCreator<_>) = async { 205 | let page = sprintf """ 206 | 207 | 208 | 209 | 210 | 215 | 216 | 217 | 218 |
219 | 220 |
221 | - %s 222 |
223 | 224 | 225 | """ AppletIds.Calculator AppletIds.InjectedContent AppletIds.InterstellarDetector detectorPageUrl.AbsoluteUri AppletIds.InterWindowCommunication 226 | let selectorWindow = createWindow { defaultBrowserWindowConfig with html = Some page } 227 | onMainWindowCreated selectorWindow 228 | do! selectorWindow.Show () 229 | selectorWindow.Browser.JavascriptMessageRecieved.Add (fun msg -> 230 | Async.Start <| async { 231 | do! Async.SwitchToContext mainCtx 232 | match msg with 233 | | AppletIds.Calculator -> do! Async.Ignore (showCalculatorWindow mainCtx createWindow) 234 | | AppletIds.InjectedContent -> do! Async.Ignore (showInjectedContentWindow mainCtx createWindow) 235 | | AppletIds.InterstellarDetector -> do! Async.Ignore (showDetectorWindow mainCtx createWindow) 236 | | AppletIds.InterWindowCommunication -> do! runCrossCommunicatingWindows mainCtx createWindow 237 | | msg -> Trace.WriteLine (sprintf "Bad message: %s" msg) 238 | }) 239 | return selectorWindow 240 | } 241 | 242 | let app onMainWindowCreated : BrowserApp<'TWindow> = BrowserApp.create (fun mainCtx createWindow -> async { 243 | let! mainWindow = appletSelectorWindow onMainWindowCreated mainCtx createWindow 244 | do! Async.AwaitEvent mainWindow.Closed 245 | }) 246 | -------------------------------------------------------------------------------- /src/Interstellar.Core/Api.fs: -------------------------------------------------------------------------------- 1 | namespace Interstellar 2 | open System 3 | open System.Threading 4 | 5 | /// Identifies a browser engine, which have web layout and Javascript execution capabilities 6 | type BrowserEngine = 7 | /// 8 | /// Google's open source Chromium browser engine, embedded and bundled along with the application using the 9 | /// ChromiumEmbeddedFramework 10 | /// 11 | | Chromium = 0 12 | /// Apple's native WebKit browser engine built-in to macOS 13 | | AppleWebKit = 0b1 14 | /// Indicates a host GUI framework, which is whatever will be used to create new windows and interact with the graphical system of the OS. 15 | type BrowserWindowPlatform = | Wpf = 0b01 | WinForms = 0b11 | MacOS = 0b100 16 | 17 | type IBrowser = 18 | /// The address which the browser is currently displaying, if any 19 | abstract Address : Uri option 20 | /// Whether or not this browser instance is showing any dev tools 21 | abstract AreDevToolsShowing : bool 22 | /// Indicates whether or not there is a previous page to go to 23 | abstract CanGoBack : bool 24 | /// Indicates whether or not there is a next page to go to 25 | abstract CanGoForward : bool 26 | /// Whether or not this browser supports dynamic showing of dev tools. macOS WebKit browsers do not seem to support this. 27 | abstract CanShowDevTools : bool 28 | /// Closes the developer tools for this browser if they are open 29 | abstract CloseDevTools : unit -> unit 30 | /// Identifies the browser engine that this Browser is using 31 | abstract Engine : BrowserEngine 32 | /// 33 | /// Executes some Javascript in the browser, returning immediately. NOTE: Prefer the printf-style ExecuteJavascriptf extension method whenever possible. 34 | /// 35 | /// 36 | /// NOTE: Prefer ExecuteJavascriptf (notice the "f") for formatting untrusted inputs into the script, as using ExecuteJavascript directly could 37 | /// render your app vulnerable to script injection. For example, instead of doing this: 38 | /// browser.ExecuteJavascript (sprintf "console.log('%s')" unsafeUserInput) 39 | /// which is not safe, do this: 40 | /// browser.ExecuteJavascriptf "console.log('%s')" unsafeUserInput 41 | /// which is safe. Or, equivalently: 42 | /// executeJavascriptf browser "console.log('%s')" unsafeUserInput 43 | /// 44 | abstract ExecuteJavascript : string -> unit 45 | /// 46 | /// Loads a page from a given Uri, returning nested asyncs to signal changes in state relating to loading status of the page: the first async calls back when the 47 | /// page finishes loading, and the second async calls back when the page is ready to start executing Javascript. 48 | /// 49 | abstract LoadAsync : Uri -> Async> 50 | /// Starts loading a page from a given Uri, returning immediately. 51 | abstract Load : Uri -> unit 52 | /// 53 | /// Directly loads the string as content for display. If a is given, it is used as the origin page. 54 | /// Any Javascript AJAX calls will communicate using that URI. This method rturns nested asyncs to signal changes in 55 | /// state relating to loading status of the page: the first async calls back when the page finishes loading, and the 56 | /// second async calls back when the page is ready to start executing Javascript. 57 | /// 58 | abstract LoadStringAsync : html: string * ?uri: Uri -> Async> 59 | /// 60 | /// Directly loads the string as content for display, returning immediately. If a is given, it is 61 | /// used as the origin page. Any Javascript AJAX calls will communicate using that URI. 62 | /// 63 | abstract LoadString : html:string * ?uri: Uri -> unit 64 | /// Attempts to navigate to the previous page in the history stack 65 | abstract GoBack : unit -> unit 66 | /// Attempts to navigate to the next page in the history stack 67 | abstract GoForward : unit -> unit 68 | /// Event handler that is called whenever a message sent from Javascript is recieved. It is safe to reference this event from a non-main thread. 69 | [] abstract JavascriptMessageRecieved : IEvent 70 | /// The title of the currently loaded page 71 | abstract PageTitle : string 72 | [] 73 | /// Event handler that is called whenever a page load is completed. It is safe to reference this event from a non-main thread. 74 | abstract PageLoaded : IEvent 75 | /// Initiates a reload of the current page 76 | abstract Reload : unit -> unit 77 | /// Event handler that is called whenever changes. It is safe to reference this event from a non-main thread. 78 | [] abstract PageTitleChanged : IEvent 79 | /// Shows the developer tools for this browser if they are not already showing. Does nothing if this feature is not supported on the current environment (see ) 80 | abstract ShowDevTools : unit -> unit 81 | 82 | /// 83 | /// A natively-hosted graphical window that hosts a 84 | /// 85 | type IBrowserWindow<'TWindow> = 86 | inherit IDisposable 87 | /// The browser instance that this window is hosting 88 | abstract Browser : IBrowser 89 | /// Closes the window 90 | abstract Close : unit -> unit 91 | /// Whether or not the window has been shown but not yet closed. 92 | abstract IsShowing : bool 93 | /// Gets the underlying platform window object 94 | abstract NativeWindow : 'TWindow 95 | /// Indicates the GUI platform that is hosting this window 96 | abstract Platform : BrowserWindowPlatform 97 | /// An event handler that is called when the window closes for any reason. It is safe to reference this event from a non-main thread. 98 | [] abstract Closed : IEvent 99 | /// Shows the window, asynchronously returning when has finished initializing and is ready to use 100 | abstract Show : unit -> Async 101 | /// The size of the window, in pixels 102 | abstract Size : (float * float) with get, set 103 | /// An event handler this is called when the window is shown. It is safe to reference this event from a non-main thread. 104 | [] abstract Shown : IEvent 105 | /// Gets or sets text in the window's title bar 106 | abstract Title : string with get, set 107 | 108 | [] 109 | type WindowTitle<'TWindow> = 110 | /// Specifies an undefined/unset window title. 111 | | Unset 112 | /// Specifies an ordinary string to use for the window title. This is only set once upon window initialization. 113 | | FromString of string 114 | /// Specifies a function to be used to determine the window title as a function of the page title 115 | | FromPageTitle of titleMapping: (string -> IBrowserWindow<'TWindow> -> Async) 116 | 117 | type BrowserWindowConfig<'TWindow> = { 118 | /// 119 | /// When set to Some on its own with a value of None for , it specifies the address for an to initially load. 120 | /// When both and are values of Some, they specify the html content to load directly in to a browser instance 121 | /// when it first shows. 122 | /// 123 | address: Uri option; 124 | /// 125 | /// When set, indicates the html to display in the browser directly when it is first shown. If is also set, 126 | /// will be used as the origin URI for any requests made by AJAX calls in Javascript. 127 | /// 128 | html: string option 129 | /// Whether or not to show the dev tools when the browser window first opens 130 | showDevTools: bool 131 | /// Defines how the window title should be determined 132 | title: WindowTitle<'TWindow> 133 | } 134 | 135 | module BrowserWindowConfig = 136 | let defaultValue<'TWindow> : BrowserWindowConfig<'TWindow> = { 137 | address = None 138 | html = None 139 | showDevTools = false 140 | title = WindowTitle.FromPageTitle(fun title _ -> async.Return title ) 141 | } 142 | /// 143 | /// Intended for use by platform implementations; applications generally shouldn't need this function. Attaches 144 | /// a title mapping function to a browser window, making sure that the installed event handler gets cleaned up. 145 | /// Must be called from the UI thread. 146 | /// 147 | let attachTitleMappingHandler mainCtx (browserWindow: IBrowserWindow<'TWindow>) (disposed: IEvent<_,_>) titleMapping = 148 | let titleMappingHandler = Handler(fun sender pageTitle -> 149 | Async.StartImmediate <| async { 150 | let! newTitle = titleMapping pageTitle browserWindow 151 | do! Async.SwitchToContext mainCtx 152 | browserWindow.Title <- newTitle 153 | } 154 | ) 155 | let pageTitleChanged = browserWindow.Browser.PageTitleChanged 156 | pageTitleChanged.AddHandler titleMappingHandler 157 | // I believe this is necessary because out titleMapping function definitely references some other objects 158 | disposed.Add (fun _ -> 159 | try pageTitleChanged.RemoveHandler titleMappingHandler 160 | // if this log line ever shows up, I'm betting that we've gotten some threading wrong or something 161 | with e -> eprintfn "Exception while removing titleMappingHandler. This is likely indicates a bug in the library and may cause other issues; please contact the Interstellar maintainer(s) or add an issue on GitHub to fix the problem: https://github.com/fsprojects/Interstellar\nFull exception: %A" e 162 | ) 163 | /// 164 | /// Intended for use by platform implementations; applications generally shouldn't need this function. Applies 165 | /// a WindowTitle to a BrowserWindow, attaching any event handlers and setting up clean up operations if necessary. 166 | /// Must be called from the UI thread. 167 | /// 168 | let applyWindowTitle mainCtx browserWindow disposed title = 169 | match title with 170 | | WindowTitle.Unset -> () 171 | | WindowTitle.FromPageTitle titleMapping -> attachTitleMappingHandler mainCtx browserWindow disposed titleMapping 172 | | WindowTitle.FromString title -> browserWindow.Title <- title 173 | 174 | /// Represents a factory function that is used to instantiate a browser window for some host platform and engine 175 | type BrowserWindowCreator<'TWindow> = BrowserWindowConfig<'TWindow> -> IBrowserWindow<'TWindow> 176 | 177 | type BrowserApp<'TWindow> = { 178 | /// 179 | /// A function that describes the entire asynchronous lifecycle of an browser application. Once all libraries and dependencies have loaded, and the program is 180 | /// ready to create browser windows, this function will be called to kick off the user application's behavior. The first 181 | /// parameter captures the GUI thread context, which can be used to safely call methods. The second paramter is a function which 182 | /// can be used to instantiate browser windows for a given platform host and engine combination. 183 | /// 184 | onStart : SynchronizationContext -> BrowserWindowCreator<'TWindow> -> Async 185 | } 186 | 187 | module BrowserApp = 188 | /// A do-nothing application 189 | let zero = { onStart = fun _ _ -> async { () } } 190 | /// Creates an application with the given onStart function 191 | let create onStart : BrowserApp<'TWindow> = { onStart = onStart } 192 | let openAddress address = { onStart = fun mainCtx createWindow -> async { 193 | do! Async.SwitchToContext mainCtx 194 | let window = createWindow { BrowserWindowConfig.defaultValue with address = Some address } 195 | do! window.Show () 196 | do! Async.AwaitEvent window.Closed 197 | } 198 | } 199 | 200 | [] 201 | module Core = 202 | let defaultBrowserWindowConfig<'TWindow> = BrowserWindowConfig.defaultValue<'TWindow> 203 | 204 | [] 205 | module Printf = 206 | open System.Text 207 | open System.Text.Encodings.Web 208 | open BlackFox.MasterOfFoo 209 | open FSharp.Core.Printf 210 | 211 | type private TextEncoderPrintfEnv<'Result>(k: string -> 'Result, encoder: TextEncoder) = 212 | inherit PrintfEnv() 213 | 214 | let sb = new StringBuilder() 215 | 216 | override this.Finalize () = k (sb.ToString ()) 217 | override this.Write (s: PrintableElement) = 218 | let value = 219 | match s.ElementType with 220 | | PrintableElementType.FromFormatSpecifier -> encoder.Encode (s.FormatAsPrintF ()) 221 | | _ -> s.FormatAsPrintF () 222 | sb.Append value |> ignore 223 | override this.WriteT (s: string) = sb.Append (encoder.Encode s) |> ignore 224 | 225 | /// Like sprintf, but escapes the format parameters for Javascript to prevent code injection, allowing you to safely deal with untrusted format parameters. Think SQL prepared statements. 226 | let javascriptf (format: StringFormat<'T, string>) = 227 | doPrintf format (fun n -> TextEncoderPrintfEnv(id, JavaScriptEncoder.Default)) 228 | 229 | /// Like ksprintf, but escapes the format parameters for Javascript to prevent code injection, allowing you to safely deal with untrusted format parameters. Think SQL prepared statements. 230 | let kjavascriptf (continuation: string -> 'Result) (format: StringFormat<'T, 'Result>) = 231 | doPrintf format (fun n -> TextEncoderPrintfEnv(continuation, JavaScriptEncoder.Default)) 232 | 233 | /// Printf-style function that executes some Javascript code on a browser instance, sanitizing format parameters using . It is safe to pass in untrusted format parameters from the outside world. Think SQL prepared statements. 234 | let executeJavascriptf (browser: IBrowser) (format: StringFormat<_,_>) = 235 | doPrintf format (fun n -> 236 | TextEncoderPrintfEnv(browser.ExecuteJavascript, JavaScriptEncoder.Default) :> PrintfEnv<_,_,_> 237 | ) --------------------------------------------------------------------------------