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

Add support for RichTextBlock

Open sleepyfran opened this issue 3 years ago β€’ 5 comments

This PR attempts to add support for the new RichTextBlock introduced in Avalonia v11. Although adding the new control is pretty straightforward, the main challenge comes from the new inline elements that were introduced for the new control, since these should be used the same way as controls are used but are not controls per se.

How the new elements work

The new control contains a property with a collection of Inlines, which are themselves subclasses of StyledElements. Each inline then defines how that portion of the text should be styled, with Run being a portion of text and Span being a group for other inline elements. All these are then collected by the control and rendered.

Implementation for FuncUI

With these changes, the usage of inline elements would look like this:

RichTextBlock.create [
    RichTextBlock.inlines [
        Run.create [
            Run.text "You"
        ]
        Run.create [
            Run.text "Inline"
            Run.background "red"
        ]
    ]
]

Challenges/things to discuss

  • These changes require the patcher of properties to accept a IAvaloniaObject (base class for both controls and styled elements) instead of a plain control. Since this is the base interface that allows us to set properties on an object I think there shouldn't be any problems (tests seem to pass and examples are working) but if anyone has any objection please let me know πŸ˜„
  • The changes to the differ might not be in the best shape that they could right now, suggestions welcomed!
  • Inline elements have a lot of styling properties in common with TemplatedControls, however since they're not themselves inheriting from them we don't have access to those. Initially here I added the background property to have a working example, but I haven't found a way to make it work nicely with the current bindings we provide. Suggestions welcomed here as well!

Example

Demo

(Once we have everything set-up I'm planning to modify the ChordParser example to highlight chords in the text)

To Do

  • [ ] Add styling support for Run
  • [ ] Add bindings for Span
  • [ ] Add bindings for Bold
  • [ ] Add bindings for Italic
  • [ ] Add bindings for Underline
  • [ ] Add bindings for LineBreak
  • [ ] UTs for the diffing changes
  • [ ] Validate that the patcher changes don't break anything (so far I've just run a couple of examples, but I'd like to validate the rest)

sleepyfran avatar Oct 08 '22 19:10 sleepyfran

Hey @sleepyfran, nice work!

I've been waiting for more advanced text formatting support in Avalonia for a while now 😁 nice that it's finally here.

-> Yes, think we need to use IAvaloniaObject everywhere in the differ/patcher.

I think it would be possible to treat Inlines as a kind of Content / expand what we see as content.

https://github.com/fsprojects/Avalonia.FuncUI/blob/3a32ee2bc887118878e4be85cb8a0a1dd41044dc/src/Avalonia.FuncUI/Types.fs#L89-L92

https://github.com/fsprojects/Avalonia.FuncUI/blob/3a32ee2bc887118878e4be85cb8a0a1dd41044dc/src/Avalonia.FuncUI/Types.fs#L53-L59

https://github.com/fsprojects/Avalonia.FuncUI/blob/3a32ee2bc887118878e4be85cb8a0a1dd41044dc/src/Avalonia.FuncUI/Types.fs#L126-L135

JaggerJo avatar Oct 09 '22 21:10 JaggerJo

(IView might be a bit misleading then, but we could consider renaming it and creating an alias for compatibility)

It should be possible to only allow certain elements as inlines by using a IView<Inline> list

Could look something like this:


    type RichTextBlock with
            
        static member inlines<'t when 't :> RichTextBlock>(value: IView<Inline> list) : IAttr<'t> =
            let getter : ('t -> obj) = (fun control -> control.Inlines :> obj)
             
            AttrBuilder<'t>.CreateContentMultiple("Inlines", ValueSome getter, ValueNone, value)

JaggerJo avatar Oct 09 '22 21:10 JaggerJo

Thanks @JaggerJo! That's a really good idea, I'm not sure why I went the way of creating a new category when expanding the content would fit nicely, I did so many blind changes in the beginning trying to make it work that I somehow missed it πŸ˜…

Will try to expand the content to accept inline elements, should be pretty straightforward and most of the types I've defined so far should be reusable πŸ˜„

sleepyfran avatar Oct 09 '22 21:10 sleepyfran

@JaggerJo please take a look at the last two commits πŸ˜„ I've pretty much switched to IAvaloniaObject everywhere it was possible and switched to IView for the inline elements. There's just a few buts I've found on the way, I've annotated them with TODOs inside the code, but to have them easily listed:

  • Because Inline is an abstract class we can't return it as the generic type of IView<> in the inline elements, since otherwise the activator won't be able to create the instance later in the patcher. This makes it impossible (at least that I've found) to use IView<Inline> in the actual RichTextBlocksince we can't do the cast to IView<Inline> later on. I've made it IView list for now, but it'd be great to narrow it to only inline elements.
  • I haven't found a way of getting rid of a couple IControl references, namely subscriptions have a hard requirement of IInteractive and IAvaloniaObject that is only found on controls. I've just done a pattern match on the casting and fail if we're attempting to subscribe to a non-control, which I believe should be fine.
  • There's a couple of ugly casts to IControl in the VirtualDom root that I'm not sure if we can somehow remove. I'm pretty sure I'm missing something here, so if you see a way please let me know!

Other than that, inlines are working as ContentMultiple, so awesome job making the initial architecture so damn extensible!

sleepyfran avatar Oct 10 '22 19:10 sleepyfran

@JaggerJo please take a look at the last two commits πŸ˜„ I've pretty much switched to IAvaloniaObject everywhere it was possible and switched to IView for the inline elements. There's just a few buts I've found on the way, I've annotated them with TODOs inside the code, but to have them easily listed:

  • Because Inline is an abstract class we can't return it as the generic type of IView<> in the inline elements, since otherwise the activator won't be able to create the instance later in the patcher. This makes it impossible (at least that I've found) to use IView<Inline> in the actual RichTextBlocksince we can't do the cast to IView<Inline> later on. I've made it IView list for now, but it'd be great to narrow it to only inline elements.

Hmm, yeah - need to take a deeper look. Would be really nice if we could constrain the type of View we support here (and in. a few other places).

  • I haven't found a way of getting rid of a couple IControl references, namely subscriptions have a hard requirement of IInteractive and IAvaloniaObject that is only found on controls. I've just done a pattern match on the casting and fail if we're attempting to subscribe to a non-control, which I believe should be fine.

agree πŸ‘

  • There's a couple of ugly casts to IControl in the VirtualDom root that I'm not sure if we can somehow remove. I'm pretty sure I'm missing something here, so if you see a way please let me know!

Think this is fine. In some cases we really expect a Control, not an AvaloniaObject.

Other than that, inlines are working as ContentMultiple, so awesome job making the initial architecture so damn extensible!

Thanks ☺️

JaggerJo avatar Oct 12 '22 08:10 JaggerJo

@JaggerJo with the latest commits all the elements are implemented and all of them stylable πŸ˜ƒ I've made a utility function for Bold, Italic, Underline and LineBreak so that they don't require the whole ordeal to set-up a simple text line, but I'm not so sure about the name simple. If you have any suggestions here let me know!

So with this I think the only thing left is to check if we could somehow remove the IView requirement, but not sure if we should do this here or try to address it in another PR (iirc there was already a ticket/PR for unifying both of them)

sleepyfran avatar Oct 15 '22 15:10 sleepyfran