Avalonia.FuncUI icon indicating copy to clipboard operation
Avalonia.FuncUI copied to clipboard

Style DSL

Open bradphelan opened this issue 4 years ago • 7 comments

I had a go at hacking a style DSL. It seems to work. I'm sure you could pick it apart but maybe it's a start for some ideas. XAML really should die!!

module Control =
    open Avalonia.Styling
    open Avalonia.Controls
    open FSharpx
    let styling stylesList = 
        let styles = Styles()
        for style in stylesList do
            styles.Add style
        Control.styles styles


    let style (selector:Selector->Selector) (setters:IAttr<'a> seq) =
        let s = Style(fun x -> selector x )
        for attr in setters do
            match attr.Property with
            | Some p -> 
                match p.accessor with
                | InstanceProperty x -> failwith "Can't support instance property" 
                | AvaloniaProperty x -> s.Setters.Add(Setter(x,p.value))
            | None -> ()
        s

and then in my view

  let private binFileTemplate (indexedBinFile: IndexedBinFile) (binFileViewer:BinFile->unit) (dispatch: Msg -> unit) =
        let (id, binFile) = indexedBinFile
        let foreground = 
            match binFile.eq_status with
            | DEGREE_EQUAL                                             -> "green" 
            | DEGREE_DIFFERENT                                         -> "red"
            | DEGREE_EXCEPTION_0|DEGREE_EXCEPTION_1|DEGREE_EXCEPTION_2 -> "yellow"
            | DEGREE_SIMILAR|DEGREE_EQUAL_IN_TOLERANCE                 -> "darkgreen"
            | _                                                        -> "brightred"

        (* create row for name, eq_status, deviation *)
        StackPanel.create [
            StackPanel.orientation Orientation.Horizontal
            StackPanel.background "black"
            StackPanel.onDoubleTapped (fun _ -> ViewBinFile(binFileViewer, binFile) |> dispatch )
            let style = [
                TextBlock.width columnWidth
                TextBlock.foreground foreground
                TextBlock.horizontalAlignment HorizontalAlignment.Left
            ]
            StackPanel.children [
                TextBlock.createFromSeq <| seq {
                    yield TextBlock.text binFile.name
                    yield! style
                } 
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.eq_status |> DU.toString )
                    yield! style
                }
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.deviation |> sprintf "%g") 
                    yield! style
                }
            ] 
        ]

bradphelan avatar Mar 17 '20 16:03 bradphelan

Strangely the above didn't quite work. On the first rendering of the list box, that the above was a data template for, it rendered the correct colors for each row. On the second rendering after filtering or sorting the style was locked to the index of the list and not reapplied. For the moment I have used a more FPish strategy for styling.

    let private binFileTemplate (indexedBinFile: IndexedBinFile) (binFileViewer:BinFile->unit) (dispatch: Msg -> unit) =
        let (id, binFile) = indexedBinFile
        let foreground = 
            match binFile.eq_status with
            | DEGREE_EQUAL                                             -> "green" 
            | DEGREE_DIFFERENT                                         -> "red"
            | DEGREE_EXCEPTION_0|DEGREE_EXCEPTION_1|DEGREE_EXCEPTION_2 -> "yellow"
            | DEGREE_SIMILAR|DEGREE_EQUAL_IN_TOLERANCE                 -> "darkgreen"
            | _                                                        -> "brightred"

        (* create row for name, eq_status, deviation *)
        StackPanel.create [
            StackPanel.orientation Orientation.Horizontal
            StackPanel.background "black"
            StackPanel.onDoubleTapped (fun _ -> ViewBinFile(binFileViewer, binFile) |> dispatch )
            let style = [
                TextBlock.width columnWidth
                TextBlock.foreground foreground
                TextBlock.horizontalAlignment HorizontalAlignment.Left
            ]
            StackPanel.children [
                TextBlock.createFromSeq <| seq {
                    yield TextBlock.text binFile.name
                    yield! style
                } 
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.eq_status |> DU.toString )
                    yield! style
                }
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.deviation |> sprintf "%g") 
                    yield! style
                }
            ] 
        ]

It's a pity that the create methods accept List<'a> rather than Seq<'a>. I've had to create an overload

module TextBlock =
    let createFromSeq s = TextBlock.create (s |> Seq.toList)

but that would have to be done for all controls which is unfortunate. Is there any way to change the create methods to Seq<'a> and have them continue to work. I don't think so because create is a module function and you can't overload module functions and F# doesn't understand covariance :(

bradphelan avatar Mar 17 '20 17:03 bradphelan

Ahhh...I just realized you can use yield and yield inside a list builder so I can write

            StackPanel.children [
                TextBlock.create [
                    yield TextBlock.text binFile.name
                    yield! style
                ] 
                TextBlock.create [
                    TextBlock.text (binFile.eq_status |> DU.toString )
                    yield! style
                ]
                TextBlock.create [
                    TextBlock.text (binFile.deviation |> sprintf "%g") 
                    yield! style
                ]
            ] 

and no need for anything to be changed.

bradphelan avatar Mar 17 '20 18:03 bradphelan

Strangely the above didn't quite work. On the first rendering of the list box, that the above was a data template for, it rendered the correct colors for each row. On the second rendering after filtering or sorting the style was locked to the index of the list and not reapplied. For the moment I have used a more FPish strategy for styling.

    let private binFileTemplate (indexedBinFile: IndexedBinFile) (binFileViewer:BinFile->unit) (dispatch: Msg -> unit) =
        let (id, binFile) = indexedBinFile
        let foreground = 
            match binFile.eq_status with
            | DEGREE_EQUAL                                             -> "green" 
            | DEGREE_DIFFERENT                                         -> "red"
            | DEGREE_EXCEPTION_0|DEGREE_EXCEPTION_1|DEGREE_EXCEPTION_2 -> "yellow"
            | DEGREE_SIMILAR|DEGREE_EQUAL_IN_TOLERANCE                 -> "darkgreen"
            | _                                                        -> "brightred"

        (* create row for name, eq_status, deviation *)
        StackPanel.create [
            StackPanel.orientation Orientation.Horizontal
            StackPanel.background "black"
            StackPanel.onDoubleTapped (fun _ -> ViewBinFile(binFileViewer, binFile) |> dispatch )
            let style = [
                TextBlock.width columnWidth
                TextBlock.foreground foreground
                TextBlock.horizontalAlignment HorizontalAlignment.Left
            ]
            StackPanel.children [
                TextBlock.createFromSeq <| seq {
                    yield TextBlock.text binFile.name
                    yield! style
                } 
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.eq_status |> DU.toString )
                    yield! style
                }
                TextBlock.createFromSeq <| seq {
                    TextBlock.text (binFile.deviation |> sprintf "%g") 
                    yield! style
                }
            ] 
        ]

Hmm, need to look into this further. I think patching / diffing Styles is the hard part. Classes do work well if they styles need to change.

It would be really nice to have this for style classes.

It's a pity that the create methods accept List<'a> rather than Seq<'a>. I've had to create an overload

module TextBlock =
    let createFromSeq s = TextBlock.create (s |> Seq.toList)

but that would have to be done for all controls which is unfortunate. Is there any way to change the create methods to Seq<'a> and have them continue to work. I don't think so because create is a module function and you can't overload module functions and F# doesn't understand covariance :(

Hmm, yeah. maybe it would make sense to change it everywhere to:

#seq<IAttr<'view>> -> IView<'view>

JaggerJo avatar Mar 17 '20 18:03 JaggerJo

See comment above..... you can use yield and yield! inside list builders.

bradphelan avatar Mar 17 '20 18:03 bradphelan

I don't really understand how the virtual dom / diffing / patching works. If you happen to write a blog post on what's going on then I would love to read it.

bradphelan avatar Mar 17 '20 18:03 bradphelan

Yeah, I should do that (and maybe add it to the Wiki - not just because I don't have a blog 😃). It's actually not that complicated (or at least I try to keep things simple).

JaggerJo avatar Mar 17 '20 18:03 JaggerJo

Another (maybe) bug with style diffing. If I add duplicate properties to a textblock then I would assume that the last one wins. ie

TextBlock.create [
    TextBlock.width 100.0
    TextBlock.width 200.0
]

However I've seen, at least in the context of my datatemplate that it seems to randomly pick between the two. This is important when using yield! to do styling. My code looked like

                TextBlock.create [
                    yield! style
                    TextBlock.text binFile.name
                    ToolTip.tip binFile.name
                    TextBlock.width columnWidth
                ] 

with the defaults in style and overrides in the array.

bradphelan avatar Mar 18 '20 07:03 bradphelan