From 6500decb7ca7835bb55fb7e3703a98fa25c4be1a Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Thu, 11 Sep 2025 23:57:01 -0400 Subject: [PATCH 01/11] Began work on Window type in DSL. --- src/Avalonia.FuncUI.Elmish/Library.fs | 12 ++++- src/Avalonia.FuncUI.sln | 9 +++- src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj | 1 + src/Avalonia.FuncUI/DSL/Window.fs | 23 ++++++++ src/Avalonia.FuncUI/VirtualDom/VirtualDom.fs | 46 ++++++++-------- .../Assets/Icons/icon.ico | Bin 0 -> 165662 bytes .../Examples.Elmish.HelloWorld.fsproj | 27 ++++++++++ .../Examples.Elmish.HelloWorld/HelloWorld.fs | 29 +++++++++++ .../Examples.Elmish.HelloWorld/Program.fs | 49 ++++++++++++++++++ 9 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 src/Avalonia.FuncUI/DSL/Window.fs create mode 100644 src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Assets/Icons/icon.ico create mode 100644 src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Examples.Elmish.HelloWorld.fsproj create mode 100644 src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs create mode 100644 src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs diff --git a/src/Avalonia.FuncUI.Elmish/Library.fs b/src/Avalonia.FuncUI.Elmish/Library.fs index 3d6ed35c..398f0d2f 100644 --- a/src/Avalonia.FuncUI.Elmish/Library.fs +++ b/src/Avalonia.FuncUI.Elmish/Library.fs @@ -1,6 +1,8 @@ namespace Avalonia.FuncUI.Elmish open Elmish +open Avalonia.Controls +open Avalonia.FuncUI.DSL open Avalonia.FuncUI.Hosts open Avalonia.FuncUI.Types open Avalonia.Threading @@ -15,8 +17,14 @@ module Program = if stateDiffers then stateRef.Value <- Some state - let view = ((Program.view program) state dispatch) - host.Update (Some (view :> IView)) + let userView = ((Program.view program) state dispatch) :> IView + + match userView with + | :? IView as windowView -> + host.Update(Some windowView) + | controlView -> + let wrappedView = Window.create [ Window.child controlView ] + host.Update(Some wrappedView) program |> Program.withSetState setState diff --git a/src/Avalonia.FuncUI.sln b/src/Avalonia.FuncUI.sln index 5550061d..a22f5a42 100644 --- a/src/Avalonia.FuncUI.sln +++ b/src/Avalonia.FuncUI.sln @@ -49,9 +49,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props NuGet.config = NuGet.config - ..\README.md = ..\README.md ..\.github\workflows\publish.yml = ..\.github\workflows\publish.yml ..\.github\workflows\pull-requests.yml = ..\.github\workflows\pull-requests.yml + ..\README.md = ..\README.md EndProjectSection EndProject Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "Examples.InlineText", "Examples\Component Examples\Examples.InlineText\Examples.InlineText.fsproj", "{B8D8C84B-05AD-475B-BE81-A30544CE0149}" @@ -90,6 +90,8 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.Elmish.Tetris", "E EndProject Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.SettingFocus", "Examples\Examples.SettingFocus\Examples.SettingFocus.fsproj", "{2C7F4542-65CE-4EC6-9876-C1C2215EE006}" EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Examples.Elmish.HelloWorld", "Examples\Elmish Examples\Examples.Elmish.HelloWorld\Examples.Elmish.HelloWorld.fsproj", "{72F1A6FA-492E-0E7A-08CF-93C5737B11BB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -212,6 +214,10 @@ Global {2C7F4542-65CE-4EC6-9876-C1C2215EE006}.Debug|Any CPU.Build.0 = Debug|Any CPU {2C7F4542-65CE-4EC6-9876-C1C2215EE006}.Release|Any CPU.ActiveCfg = Release|Any CPU {2C7F4542-65CE-4EC6-9876-C1C2215EE006}.Release|Any CPU.Build.0 = Release|Any CPU + {72F1A6FA-492E-0E7A-08CF-93C5737B11BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {72F1A6FA-492E-0E7A-08CF-93C5737B11BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {72F1A6FA-492E-0E7A-08CF-93C5737B11BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {72F1A6FA-492E-0E7A-08CF-93C5737B11BB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -249,6 +255,7 @@ Global {6D2C62FC-5634-4997-AF1F-2E8A5D27E117} = {84811DB3-C276-4F0D-B3BA-78B88E2C6EF0} {EC63B886-E809-4B74-B533-BFF3D60017C9} = {6D2C62FC-5634-4997-AF1F-2E8A5D27E117} {2C7F4542-65CE-4EC6-9876-C1C2215EE006} = {F50826CE-D9BC-45CF-A110-C42225B75AD3} + {72F1A6FA-492E-0E7A-08CF-93C5737B11BB} = {F6F4AAF7-2BDA-4D2F-B78D-F6D8A03F660E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4630E817-6780-4C98-9379-EA3B45224339} diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj index fdaf4b7e..d85328e1 100644 --- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj +++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj @@ -133,6 +133,7 @@ + diff --git a/src/Avalonia.FuncUI/DSL/Window.fs b/src/Avalonia.FuncUI/DSL/Window.fs new file mode 100644 index 00000000..a685d2db --- /dev/null +++ b/src/Avalonia.FuncUI/DSL/Window.fs @@ -0,0 +1,23 @@ +namespace Avalonia.FuncUI.DSL + +[] +module Window = + open Avalonia.Controls + open Avalonia.FuncUI.Types + open Avalonia.FuncUI.Builder + + let create (attrs: IAttr list) : IView = + ViewBuilder.Create(attrs) + + type Window with + static member child(value: IView) : IAttr = + AttrBuilder.CreateContentSingle(Window.ContentProperty, Some value) + + static member title<'t when 't :> Window>(value: string) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.TitleProperty, value, ValueNone) + + static member canResize<'t when 't :> Window>(value: bool) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.CanResizeProperty, value, ValueNone) + + static member sizeToContent<'t when 't :> Window>(value: SizeToContent) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.SizeToContentProperty, value, ValueNone) diff --git a/src/Avalonia.FuncUI/VirtualDom/VirtualDom.fs b/src/Avalonia.FuncUI/VirtualDom/VirtualDom.fs index 17f08a94..25d3d019 100644 --- a/src/Avalonia.FuncUI/VirtualDom/VirtualDom.fs +++ b/src/Avalonia.FuncUI/VirtualDom/VirtualDom.fs @@ -17,14 +17,6 @@ module rec VirtualDom = Patcher.patch(root, delta) let updateRoot (host: ContentControl, last: IView option, next: IView option) = - let root : Control voption = - if host.Content <> null then - match host.Content with - | :? Control as control -> ValueSome control - | _ -> ValueNone - else - ValueNone - let delta : ViewDelta voption = match last with | Some last -> @@ -35,21 +27,33 @@ module rec VirtualDom = match next with | Some next -> ViewDelta.From next |> ValueSome | None -> ValueNone + + match host, delta with + | :? Window as windowHost, ValueSome viewDelta when viewDelta.ViewType.IsSubclassOf(typeof) || viewDelta.ViewType = typeof -> + Patcher.patch(windowHost, viewDelta) + | _ -> + let root : Control voption = + if host.Content <> null then + match host.Content with + | :? Control as control -> ValueSome control + | _ -> ValueNone + else + ValueNone - match root with - | ValueSome control -> - match delta with - | ValueSome delta -> - match control.GetType () = delta.ViewType && not delta.KeyDidChange with - | true -> Patcher.patch (control, delta) - | false -> host.Content <- Patcher.create delta - | ValueNone -> - host.Content <- null + match root with + | ValueSome control -> + match delta with + | ValueSome delta -> + match control.GetType () = delta.ViewType && not delta.KeyDidChange with + | true -> Patcher.patch (control, delta) + | false -> host.Content <- Patcher.create delta + | ValueNone -> + host.Content <- null - | ValueNone -> - match delta with - | ValueSome delta -> host.Content <- Patcher.create delta - | ValueNone -> host.Content <- null + | ValueNone -> + match delta with + | ValueSome delta -> host.Content <- Patcher.create delta + | ValueNone -> host.Content <- null // TODO: share code with updateRoot let internal updateBorderRoot (host: Border, last: IView option, next: IView option) = diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Assets/Icons/icon.ico b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Assets/Icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9fd305d05014179e7bf7ac901042d0e2e2fa7d8e GIT binary patch literal 165662 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 literal 0 HcmV?d00001 diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Examples.Elmish.HelloWorld.fsproj b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Examples.Elmish.HelloWorld.fsproj new file mode 100644 index 00000000..edd161f0 --- /dev/null +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Examples.Elmish.HelloWorld.fsproj @@ -0,0 +1,27 @@ + + + + Exe + net8.0 + Examples.CounterApp + + + + + PreserveNewest + + + + + + + + + + + + + + + + diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs new file mode 100644 index 00000000..76ea6a4c --- /dev/null +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -0,0 +1,29 @@ +namespace Examples.CounterApp + +open Avalonia.FuncUI.DSL + +module Counter = + open Avalonia.Controls + open Avalonia.Layout + + type State = NoState + let init() = NoState + + type Msg = NoMsg + + let update (msg: Msg) (state: State) : State = + match msg with + | NoMsg -> state + + let view (state: State) (dispatch) = + Window.create [ + Window.title "Hello World" + Window.width 400 + Window.height 200 + Window.child ( + TextBox.create [ + TextBox.text "Hello from FuncUI!" + TextBox.isReadOnly true + ] + ) + ] diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs new file mode 100644 index 00000000..c97b9699 --- /dev/null +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs @@ -0,0 +1,49 @@ +namespace Examples.CounterApp + +open Avalonia +open Avalonia.Controls +open Avalonia.Themes.Fluent +open Elmish +open Avalonia.FuncUI.Hosts +open Avalonia.FuncUI +open Avalonia.FuncUI.Elmish +open Avalonia.Controls.ApplicationLifetimes + +type MainWindow() as this = + inherit HostWindow() + do + base.Title <- "Counter Example" + base.Icon <- WindowIcon(System.IO.Path.Combine("Assets","Icons", "icon.ico")) + base.Height <- 400.0 + base.Width <- 400.0 + + //this.VisualRoot.VisualRoot.Renderer.DrawFps <- true + //this.VisualRoot.VisualRoot.Renderer.DrawDirtyRects <- true + Elmish.Program.mkSimple Counter.init Counter.update Counter.view + |> Program.withHost this + |> Program.withConsoleTrace + |> Program.run + +type App() = + inherit Application() + + override this.Initialize() = + this.Styles.Add (FluentTheme()) + this.RequestedThemeVariant <- Styling.ThemeVariant.Dark + + override this.OnFrameworkInitializationCompleted() = + match this.ApplicationLifetime with + | :? IClassicDesktopStyleApplicationLifetime as desktopLifetime -> + let mainWindow = MainWindow() + desktopLifetime.MainWindow <- mainWindow + | _ -> () + +module Program = + + [] + let main(args: string[]) = + AppBuilder + .Configure() + .UsePlatformDetect() + .UseSkia() + .StartWithClassicDesktopLifetime(args) \ No newline at end of file From 3ea7bf2de95888cf087b69c33a1f30cd8cc4d120 Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 00:01:26 -0400 Subject: [PATCH 02/11] Made UI interactive. --- .../Examples.Elmish.HelloWorld/HelloWorld.fs | 17 ++++++++--------- .../Examples.Elmish.HelloWorld/Program.fs | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs index 76ea6a4c..bf588aa8 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -2,28 +2,27 @@ namespace Examples.CounterApp open Avalonia.FuncUI.DSL -module Counter = +module HelloWorld = open Avalonia.Controls - open Avalonia.Layout - type State = NoState - let init() = NoState + type State = string + let init() = "World" - type Msg = NoMsg + type Msg = Update of string let update (msg: Msg) (state: State) : State = match msg with - | NoMsg -> state + | Update str -> str let view (state: State) (dispatch) = Window.create [ - Window.title "Hello World" + Window.title $"Hello {state}" Window.width 400 Window.height 200 Window.child ( TextBox.create [ - TextBox.text "Hello from FuncUI!" - TextBox.isReadOnly true + TextBox.text state + TextBox.onTextChanged (Update >> dispatch) ] ) ] diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs index c97b9699..b63af5a7 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs @@ -19,7 +19,7 @@ type MainWindow() as this = //this.VisualRoot.VisualRoot.Renderer.DrawFps <- true //this.VisualRoot.VisualRoot.Renderer.DrawDirtyRects <- true - Elmish.Program.mkSimple Counter.init Counter.update Counter.view + Elmish.Program.mkSimple HelloWorld.init HelloWorld.update HelloWorld.view |> Program.withHost this |> Program.withConsoleTrace |> Program.run From 8ca5ca9adbe5489c350745f15ea49b7bfbc39b2d Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 00:06:28 -0400 Subject: [PATCH 03/11] Improved usability. --- .../Examples.Elmish.HelloWorld/HelloWorld.fs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs index bf588aa8..1de84f85 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -16,13 +16,22 @@ module HelloWorld = let view (state: State) (dispatch) = Window.create [ - Window.title $"Hello {state}" + Window.title $"Hello {state}!" Window.width 400 - Window.height 200 + Window.height 100 Window.child ( - TextBox.create [ - TextBox.text state - TextBox.onTextChanged (Update >> dispatch) + StackPanel.create [ + StackPanel.margin 10 + StackPanel.spacing 10 + StackPanel.children [ + TextBlock.create [ + TextBlock.text "Type your name here to change the window title:" + ] + TextBox.create [ + TextBox.text state + TextBox.onTextChanged (Update >> dispatch) + ] + ] ] ) ] From 880bc61fb88b8f9abcb7612f546dae66a2a9a24e Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 00:08:34 -0400 Subject: [PATCH 04/11] A bit more polish for clarity. --- .../Examples.Elmish.HelloWorld/HelloWorld.fs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs index 1de84f85..40a7cc89 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -8,11 +8,11 @@ module HelloWorld = type State = string let init() = "World" - type Msg = Update of string + type Msg = NameChanged of string - let update (msg: Msg) (state: State) : State = + let update (msg: Msg) (_state: State) : State = match msg with - | Update str -> str + | NameChanged name -> name let view (state: State) (dispatch) = Window.create [ @@ -29,7 +29,7 @@ module HelloWorld = ] TextBox.create [ TextBox.text state - TextBox.onTextChanged (Update >> dispatch) + TextBox.onTextChanged (NameChanged >> dispatch) ] ] ] From 570062be81d308a3da01d5382332c3bf567b7b22 Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 08:49:24 -0400 Subject: [PATCH 05/11] Added support for window state and key bindings. --- src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj | 3 +- src/Avalonia.FuncUI/DSL/KeyBinding.fs | 54 +++++++++++++++++++ src/Avalonia.FuncUI/DSL/Window.fs | 3 ++ .../Examples.Elmish.HelloWorld/HelloWorld.fs | 49 ++++++++++++++--- 4 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 src/Avalonia.FuncUI/DSL/KeyBinding.fs diff --git a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj index d85328e1..b85b32af 100644 --- a/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj +++ b/src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj @@ -133,7 +133,6 @@ - @@ -182,6 +181,8 @@ + + diff --git a/src/Avalonia.FuncUI/DSL/KeyBinding.fs b/src/Avalonia.FuncUI/DSL/KeyBinding.fs new file mode 100644 index 00000000..2d31e47a --- /dev/null +++ b/src/Avalonia.FuncUI/DSL/KeyBinding.fs @@ -0,0 +1,54 @@ +namespace Avalonia.FuncUI.DSL + +open System +open System.Windows.Input + +open Avalonia.Input +open Avalonia.FuncUI.DSL +open Avalonia.FuncUI.Types +open Avalonia.FuncUI.Builder + +[] +module KeyBinding = + + let create (attrs: IAttr list) : IView = + ViewBuilder.Create(attrs) + + let private toCommand action = + let canExecuteChanged = Event() + { + new ICommand with + member _.CanExecute(_) = true + member _.Execute(parameter) = action parameter + [] + member _.CanExecuteChanged = + canExecuteChanged.Publish + } + + type KeyBinding with + + static member gesture<'t when 't :> KeyBinding> (value: KeyGesture) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(KeyBinding.GestureProperty, value, ValueNone) + + static member key<'t when 't :> KeyBinding> (value: Key) : IAttr<'t> = + KeyBinding.gesture (KeyGesture(value)) + + static member command<'t when 't :> KeyBinding> (value: ICommand) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(KeyBinding.CommandProperty, value, ValueNone) + + static member execute<'t when 't :> KeyBinding> (value : obj -> unit) : IAttr<'t> = + KeyBinding.command (toCommand value) + + static member commandParameter<'t when 't :> KeyBinding> (value: obj) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(KeyBinding.CommandParameterProperty, value, ValueNone) + + type InputElement with + + static member keyBindings<'t when 't :> InputElement> (bindings: IView list) : IAttr<'t> = + let getter: 't -> obj = fun control -> control.KeyBindings :> obj + + AttrBuilder<'t>.CreateContentMultiple( + "KeyBindings", + ValueSome getter, + ValueNone, + bindings |> List.map (fun v -> v :> IView)) diff --git a/src/Avalonia.FuncUI/DSL/Window.fs b/src/Avalonia.FuncUI/DSL/Window.fs index a685d2db..43893bd2 100644 --- a/src/Avalonia.FuncUI/DSL/Window.fs +++ b/src/Avalonia.FuncUI/DSL/Window.fs @@ -21,3 +21,6 @@ module Window = static member sizeToContent<'t when 't :> Window>(value: SizeToContent) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Window.SizeToContentProperty, value, ValueNone) + + static member windowState<'t when 't :> Window>(value: WindowState) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.WindowStateProperty, value, ValueNone) diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs index 40a7cc89..e39f225b 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -1,24 +1,40 @@ namespace Examples.CounterApp open Avalonia.FuncUI.DSL +open Avalonia.Input module HelloWorld = open Avalonia.Controls - type State = string - let init() = "World" + type State = + { + Name : string + FullScreen : bool + } + let init() = + { + Name = "World" + FullScreen = false + } - type Msg = NameChanged of string + type Msg = + | NameChanged of string + | FullScreen of bool - let update (msg: Msg) (_state: State) : State = + let update (msg: Msg) (state: State) : State = match msg with - | NameChanged name -> name + | NameChanged name -> + { state with Name = name } + | FullScreen fullScreen -> + { state with FullScreen = fullScreen } let view (state: State) (dispatch) = Window.create [ Window.title $"Hello {state}!" - Window.width 400 - Window.height 100 + Window.sizeToContent SizeToContent.WidthAndHeight + Window.windowState ( + if state.FullScreen then WindowState.FullScreen + else WindowState.Normal) Window.child ( StackPanel.create [ StackPanel.margin 10 @@ -28,10 +44,27 @@ module HelloWorld = TextBlock.text "Type your name here to change the window title:" ] TextBox.create [ - TextBox.text state + TextBox.text state.Name TextBox.onTextChanged (NameChanged >> dispatch) ] + TextBlock.create [ + TextBlock.text "Or use F11 key to enter full screen mode and Esc key to exit." + ] ] ] ) + Window.keyBindings [ + KeyBinding.create [ + KeyBinding.key Key.F11 + KeyBinding.execute (fun _ -> + FullScreen true + |> dispatch) + ] + KeyBinding.create [ + KeyBinding.key Key.Escape + KeyBinding.execute (fun _ -> + FullScreen false + |> dispatch) + ] + ] ] From ad2220f821f70fece8c405888e6ebf728b661347 Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 08:54:08 -0400 Subject: [PATCH 06/11] Added Window.icon. --- src/Avalonia.FuncUI/DSL/Window.fs | 14 +++++++++----- .../Examples.Elmish.HelloWorld/HelloWorld.fs | 7 +++++++ .../Examples.Elmish.HelloWorld/Program.fs | 18 +++++------------- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Window.fs b/src/Avalonia.FuncUI/DSL/Window.fs index 43893bd2..3c8b8116 100644 --- a/src/Avalonia.FuncUI/DSL/Window.fs +++ b/src/Avalonia.FuncUI/DSL/Window.fs @@ -10,17 +10,21 @@ module Window = ViewBuilder.Create(attrs) type Window with - static member child(value: IView) : IAttr = - AttrBuilder.CreateContentSingle(Window.ContentProperty, Some value) - - static member title<'t when 't :> Window>(value: string) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(Window.TitleProperty, value, ValueNone) static member canResize<'t when 't :> Window>(value: bool) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Window.CanResizeProperty, value, ValueNone) + + static member child(value: IView) : IAttr = + AttrBuilder.CreateContentSingle(Window.ContentProperty, Some value) + + static member icon<'t when 't :> Window>(value: WindowIcon) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.IconProperty, value, ValueNone) static member sizeToContent<'t when 't :> Window>(value: SizeToContent) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Window.SizeToContentProperty, value, ValueNone) + static member title<'t when 't :> Window>(value: string) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.TitleProperty, value, ValueNone) + static member windowState<'t when 't :> Window>(value: WindowState) : IAttr<'t> = AttrBuilder<'t>.CreateProperty(Window.WindowStateProperty, value, ValueNone) diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs index e39f225b..617608c6 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -1,5 +1,7 @@ namespace Examples.CounterApp +open System.IO + open Avalonia.FuncUI.DSL open Avalonia.Input @@ -28,9 +30,14 @@ module HelloWorld = | FullScreen fullScreen -> { state with FullScreen = fullScreen } + let icon = + Path.Combine("Assets", "Icons", "icon.ico") + |> WindowIcon + let view (state: State) (dispatch) = Window.create [ Window.title $"Hello {state}!" + Window.icon icon Window.sizeToContent SizeToContent.WidthAndHeight Window.windowState ( if state.FullScreen then WindowState.FullScreen diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs index b63af5a7..b194fbae 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/Program.fs @@ -1,24 +1,16 @@ namespace Examples.CounterApp open Avalonia -open Avalonia.Controls -open Avalonia.Themes.Fluent -open Elmish +open Avalonia.Controls.ApplicationLifetimes open Avalonia.FuncUI.Hosts -open Avalonia.FuncUI open Avalonia.FuncUI.Elmish -open Avalonia.Controls.ApplicationLifetimes +open Avalonia.Themes.Fluent + +open Elmish type MainWindow() as this = inherit HostWindow() do - base.Title <- "Counter Example" - base.Icon <- WindowIcon(System.IO.Path.Combine("Assets","Icons", "icon.ico")) - base.Height <- 400.0 - base.Width <- 400.0 - - //this.VisualRoot.VisualRoot.Renderer.DrawFps <- true - //this.VisualRoot.VisualRoot.Renderer.DrawDirtyRects <- true Elmish.Program.mkSimple HelloWorld.init HelloWorld.update HelloWorld.view |> Program.withHost this |> Program.withConsoleTrace @@ -46,4 +38,4 @@ module Program = .Configure() .UsePlatformDetect() .UseSkia() - .StartWithClassicDesktopLifetime(args) \ No newline at end of file + .StartWithClassicDesktopLifetime(args) From 1a7792b78255b20288b40b261a8c19d89ddfb03a Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 09:05:49 -0400 Subject: [PATCH 07/11] Added properties and removed boilerplate. --- src/Avalonia.FuncUI/DSL/Window.fs | 51 ++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Window.fs b/src/Avalonia.FuncUI/DSL/Window.fs index 3c8b8116..1601b1a8 100644 --- a/src/Avalonia.FuncUI/DSL/Window.fs +++ b/src/Avalonia.FuncUI/DSL/Window.fs @@ -1,30 +1,47 @@ namespace Avalonia.FuncUI.DSL +open Avalonia.Controls +open Avalonia.FuncUI.Types +open Avalonia.FuncUI.Builder +open Avalonia.Platform + [] module Window = - open Avalonia.Controls - open Avalonia.FuncUI.Types - open Avalonia.FuncUI.Builder let create (attrs: IAttr list) : IView = ViewBuilder.Create(attrs) type Window with - static member canResize<'t when 't :> Window>(value: bool) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(Window.CanResizeProperty, value, ValueNone) + static member canResize<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.CanResizeProperty, value, ValueNone) static member child(value: IView) : IAttr = AttrBuilder.CreateContentSingle(Window.ContentProperty, Some value) - - static member icon<'t when 't :> Window>(value: WindowIcon) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(Window.IconProperty, value, ValueNone) - - static member sizeToContent<'t when 't :> Window>(value: SizeToContent) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(Window.SizeToContentProperty, value, ValueNone) - - static member title<'t when 't :> Window>(value: string) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(Window.TitleProperty, value, ValueNone) - - static member windowState<'t when 't :> Window>(value: WindowState) : IAttr<'t> = - AttrBuilder<'t>.CreateProperty(Window.WindowStateProperty, value, ValueNone) + + static member closingBehavior<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.ClosingBehaviorProperty, value, ValueNone) + + static member extendClientAreaChromeHints<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.ExtendClientAreaChromeHintsProperty, value, ValueNone) + + static member extendClientAreaTitleBarHeightHint<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.ExtendClientAreaTitleBarHeightHintProperty, value, ValueNone) + + static member extendClientAreaToDecorationsHint<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.ExtendClientAreaToDecorationsHintProperty, value, ValueNone) + + static member isExtendedIntoWindowDecorationsProperty<'t when 't :> Window>(func, ?subPatchOptions) : IAttr<'t> = + AttrBuilder<'t>.CreateSubscription(Window.IsExtendedIntoWindowDecorationsProperty, func, ?subPatchOptions = subPatchOptions) + + static member icon<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.IconProperty, value, ValueNone) + + static member sizeToContent<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.SizeToContentProperty, value, ValueNone) + + static member title<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.TitleProperty, value, ValueNone) + + static member windowState<'t when 't :> Window>(value) : IAttr<'t> = + AttrBuilder<'t>.CreateProperty(Window.WindowStateProperty, value, ValueNone) From 7f450d45c8ced8064527b8cf6a437a80a5b55ec3 Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 09:11:32 -0400 Subject: [PATCH 08/11] Added more properties. --- src/Avalonia.FuncUI/DSL/Window.fs | 40 ++++++++++++++----- .../Examples.Elmish.HelloWorld/HelloWorld.fs | 2 +- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.FuncUI/DSL/Window.fs b/src/Avalonia.FuncUI/DSL/Window.fs index 1601b1a8..74d0bc39 100644 --- a/src/Avalonia.FuncUI/DSL/Window.fs +++ b/src/Avalonia.FuncUI/DSL/Window.fs @@ -13,35 +13,53 @@ module Window = type Window with - static member canResize<'t when 't :> Window>(value) : IAttr<'t> = + static member canResize<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.CanResizeProperty, value, ValueNone) - static member child(value: IView) : IAttr = + static member child(value: IView) = AttrBuilder.CreateContentSingle(Window.ContentProperty, Some value) - static member closingBehavior<'t when 't :> Window>(value) : IAttr<'t> = + static member closingBehavior<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.ClosingBehaviorProperty, value, ValueNone) - static member extendClientAreaChromeHints<'t when 't :> Window>(value) : IAttr<'t> = + static member extendClientAreaChromeHints<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.ExtendClientAreaChromeHintsProperty, value, ValueNone) - static member extendClientAreaTitleBarHeightHint<'t when 't :> Window>(value) : IAttr<'t> = + static member extendClientAreaTitleBarHeightHint<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.ExtendClientAreaTitleBarHeightHintProperty, value, ValueNone) - static member extendClientAreaToDecorationsHint<'t when 't :> Window>(value) : IAttr<'t> = + static member extendClientAreaToDecorationsHint<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.ExtendClientAreaToDecorationsHintProperty, value, ValueNone) - static member isExtendedIntoWindowDecorationsProperty<'t when 't :> Window>(func, ?subPatchOptions) : IAttr<'t> = + static member isExtendedIntoWindowDecorations<'t when 't :> Window>(func, ?subPatchOptions) = AttrBuilder<'t>.CreateSubscription(Window.IsExtendedIntoWindowDecorationsProperty, func, ?subPatchOptions = subPatchOptions) - static member icon<'t when 't :> Window>(value) : IAttr<'t> = + static member icon<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.IconProperty, value, ValueNone) - static member sizeToContent<'t when 't :> Window>(value) : IAttr<'t> = + static member offScreenMargin<'t when 't :> Window>(func, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(Window.OffScreenMarginProperty, func, ?subPatchOptions = subPatchOptions) + + static member showActivated<'t when 't :> Window>(value) = + AttrBuilder<'t>.CreateProperty(Window.ShowActivatedProperty, value, ValueNone) + + static member showInTaskbar<'t when 't :> Window>(value) = + AttrBuilder<'t>.CreateProperty(Window.ShowInTaskbarProperty, value, ValueNone) + + static member sizeToContent<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.SizeToContentProperty, value, ValueNone) - static member title<'t when 't :> Window>(value) : IAttr<'t> = + static member systemDecorations<'t when 't :> Window>(value) = + AttrBuilder<'t>.CreateProperty(Window.SystemDecorationsProperty, value, ValueNone) + + static member title<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.TitleProperty, value, ValueNone) - static member windowState<'t when 't :> Window>(value) : IAttr<'t> = + static member windowDecorationMargin<'t when 't :> Window>(func, ?subPatchOptions) = + AttrBuilder<'t>.CreateSubscription(Window.WindowDecorationMarginProperty, func, ?subPatchOptions = subPatchOptions) + + static member windowStartupLocation<'t when 't :> Window>(value) = + AttrBuilder<'t>.CreateProperty(Window.WindowStartupLocationProperty, value, ValueNone) + + static member windowState<'t when 't :> Window>(value) = AttrBuilder<'t>.CreateProperty(Window.WindowStateProperty, value, ValueNone) diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs index 617608c6..ae7fda5a 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -36,7 +36,7 @@ module HelloWorld = let view (state: State) (dispatch) = Window.create [ - Window.title $"Hello {state}!" + Window.title $"Hello {state.Name}!" Window.icon icon Window.sizeToContent SizeToContent.WidthAndHeight Window.windowState ( From 6be3e048ce4a5fe0685366bee43c6de1836e3e42 Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 09:24:24 -0400 Subject: [PATCH 09/11] Added Window documentation. --- docs/controls/window.md | 64 +++++++++++++++++++++++++++++++ src/Avalonia.FuncUI/DSL/Window.fs | 1 - 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 docs/controls/window.md diff --git a/docs/controls/window.md b/docs/controls/window.md new file mode 100644 index 00000000..87ec2695 --- /dev/null +++ b/docs/controls/window.md @@ -0,0 +1,64 @@ +# Window + +> _Note_: You can check the Avalonia docs for the [Window](https://docs.avaloniaui.net/docs/controls/window) and [Window API](http://reference.avaloniaui.net/api/Avalonia.Controls/Window/) if you need more information. +> +> For Avalonia.FuncUI's DSL properties you can check [Window.fs](https://github.com/AvaloniaCommunity/Avalonia.FuncUI/blob/master/src/Avalonia.FuncUI.DSL/Window.fs) + +Window corresponds to your application's host window, and allows you to set top-level properties like window title and key bindings. + +### Usage + +**Create a Window** + +```fsharp +Window.create [] +``` + +**Set Window Title** + +```fsharp +Window.title "My Application" +``` + +**Set Window Icon** + +```fsharp +let icon = + Path.Combine("Assets", "Icons", "icon.ico") + |> WindowIcon + +Window.icon icon +``` + +**Size Window to Content** + +```fsharp +Window.sizeToContent SizeToContent.WidthAndHeight +``` + +**Switch to Full-Screen Mode** + +```fsharp +Window.windowState ( + if state.FullScreen then WindowState.FullScreen + else WindowState.Normal) +``` + +**Create Key Bindings** + +```fsharp +Window.keyBindings [ + KeyBinding.create [ + KeyBinding.key Key.F11 + KeyBinding.execute (fun _ -> + FullScreen true + |> dispatch) + ] + KeyBinding.create [ + KeyBinding.key Key.Escape + KeyBinding.execute (fun _ -> + FullScreen false + |> dispatch) + ] +] +``` diff --git a/src/Avalonia.FuncUI/DSL/Window.fs b/src/Avalonia.FuncUI/DSL/Window.fs index 74d0bc39..8201601f 100644 --- a/src/Avalonia.FuncUI/DSL/Window.fs +++ b/src/Avalonia.FuncUI/DSL/Window.fs @@ -3,7 +3,6 @@ namespace Avalonia.FuncUI.DSL open Avalonia.Controls open Avalonia.FuncUI.Types open Avalonia.FuncUI.Builder -open Avalonia.Platform [] module Window = From 7ba49b7cb98099e4e7f1cfd2556a4d2db11aa61c Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 09:32:04 -0400 Subject: [PATCH 10/11] Added KeyBinding documentation. --- docs/controls/keybinding.md | 37 +++++++++++++++++++++++++++++++++++++ docs/controls/window.md | 2 +- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 docs/controls/keybinding.md diff --git a/docs/controls/keybinding.md b/docs/controls/keybinding.md new file mode 100644 index 00000000..f94d74b9 --- /dev/null +++ b/docs/controls/keybinding.md @@ -0,0 +1,37 @@ +# KeyBinding + +> _Note_: You can check the Avalonia docs for [KeyBinding](https://docs.avaloniaui.net/docs/concepts/input/binding-key-and-mouse) if you need more information. +> +> For Avalonia.FuncUI's DSL properties you can check [KeyBinding.fs](https://github.com/AvaloniaCommunity/Avalonia.FuncUI/blob/master/src/Avalonia.FuncUI.DSL/KeyBinding.fs). + +A key binding allows you to dispatch a message (or trigger some other action) when the user presses a key on the keyboard. For example: + +```fsharp +KeyBinding.create [ + KeyBinding.key Key.F11 + KeyBinding.execute (fun _ -> + FullScreen true + |> dispatch) +] +``` + +### Usage + +**Create a Key Binding** + +```fsharp +KeyBinding.create [] +``` + +**Bind to a Specific Key** + +```fsharp +KeyBinding.key Key.Escape +``` + +**Trigger a Message** + +```fsharp +KeyBinding.execute (fun _ -> + dispatch MyMessage) +``` diff --git a/docs/controls/window.md b/docs/controls/window.md index 87ec2695..f06a7e86 100644 --- a/docs/controls/window.md +++ b/docs/controls/window.md @@ -2,7 +2,7 @@ > _Note_: You can check the Avalonia docs for the [Window](https://docs.avaloniaui.net/docs/controls/window) and [Window API](http://reference.avaloniaui.net/api/Avalonia.Controls/Window/) if you need more information. > -> For Avalonia.FuncUI's DSL properties you can check [Window.fs](https://github.com/AvaloniaCommunity/Avalonia.FuncUI/blob/master/src/Avalonia.FuncUI.DSL/Window.fs) +> For Avalonia.FuncUI's DSL properties you can check [Window.fs](https://github.com/AvaloniaCommunity/Avalonia.FuncUI/blob/master/src/Avalonia.FuncUI.DSL/Window.fs). Window corresponds to your application's host window, and allows you to set top-level properties like window title and key bindings. From c98929ab864ec49f54adeff7db9826d876b77b0c Mon Sep 17 00:00:00 2001 From: Brian Berns Date: Fri, 12 Sep 2025 09:56:17 -0400 Subject: [PATCH 11/11] Cleanup. --- .../Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs index ae7fda5a..e08e6ed9 100644 --- a/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs +++ b/src/Examples/Elmish Examples/Examples.Elmish.HelloWorld/HelloWorld.fs @@ -2,11 +2,11 @@ namespace Examples.CounterApp open System.IO +open Avalonia.Controls open Avalonia.FuncUI.DSL open Avalonia.Input module HelloWorld = - open Avalonia.Controls type State = {