red / REP

Red Enhancement Process
BSD 3-Clause "New" or "Revised" License
11 stars 4 forks source link

How can we improve the design of rich-text? #124

Open hiiamboris opened 2 years ago

hiiamboris commented 2 years ago

I have been using rich-text for a while, and am ready to discuss shortcomings and propose improvements.

I suppose reason for the current design is the flexibility it gives us: styling each letter separately with it's own font. However, I think for the majority of use cases font and style can be assumed constant for the whole paragraph.

The main issue of current design is a requirement of a face object. In more detail:

  1. For each paragraph drawn, a separate rich-text face has to be created.

    Done naively (code: [r: rtd-layout [""] r/text: "abc" r/size: 50x50 r/data: [] r/font: none]) initialization of a single face takes ~250us. On a 20x500 table (10000 texts) it adds up to 2.5 seconds. Nobody wants to wait that much, plus some machines are much slower.

    I've fought this design a lot: pre-created my own variant of rich-text face, with hackishly "disabled" on-change & on-deep-change handlers, used set-quiet to set it's facets, and that dramatically improved the performance of such face creation (however now I have to call size-text after setting /text and before calling any offset-to-caret or caret-to-offset or they will not work - seems there's an internal cache updated by size-text only). But it's still dead weight and has to be fought against by everyone who is going to apply it to similar task.

    I have also created a per-space map to hold the cache of rich-text faces: one face per desired width. To avoid extra face objects.

    This just goes against the goal of Draw: it should be as fast as possible.

    I propose extending text Draw command with an optional wrap width, so inlined strings can be wrapped too.

  2. Face conflicts with Draw commands.

    • font command is currently ignored when rich-text face is given, only /font facet from the face is used. In Spaces I have separated styles from widget logic, but this forces me to have a font object in each text space (it's logic part), while it really belongs to the style.
    • font object has it's style (bold/italic, no underline or strikethrough), which can be applied from both font command and /font facet, while rich-text face also has positional overrides in /data (bold/italic/underline, no strikethrough); total mess!
    • GTK backend ignores pen currently: https://github.com/red/red/issues/4901 which is a natural consequence of so many levers controlling the same property
    • we have now: pen, font/color of font command, and face/font/color of the face (go figure now without strict rules!)
    • fill-pen is completely ignored (should it work? how does it play together with rich-text face/color? face/color takes priority when given?)
    • maybe fill-pen should control the filling of letters, and pen - the outline, line-width - thickness of the outline, like for shapes? graphic editors support this, but not sure if low-level rich-text implementation does

    text command extended with wrap width could leverage pen, fill-pen, font/style, which is the way Draw was designed to work. Moreover, pattern/bitmap pen is applicable to text as well, which is much more powerful than just face/font/color.

  3. Unlike "inlined text" this rich-text face is prone to change from under you:

    Try:

    • create a draw block with rich-text face in it
    • change the rich-text face
    • force a redraw of the same block (e.g. by a deep unrelated change in the draw block if it's owned by a face) and the result will change
    • changes to the rich-text face itself cannot be detected by the owner of draw block in the current ownership design

    This limits the cacheability of such faces. E.g. I can't have a global cache of them, have to have single cache per space.

    Obviously such unexpected changes lead to surprise bugs. Been there.

    Plus Draw's designed to have no external state, for reproducibility, and this breaks the design. font is an object too, but it can be assumed constant, while rich-text's text, data and size are one-off properties.

  4. Face creation is currently the only way to measure paragraph's extent.

    Imagine a word processor. Let's have a single paragraph big enough to fill half of the screen, e.g. 3k chars. Imagine we insert an image (maybe a raster emoji, or anything that's not supported by rich-text alone) into every line of such paragraph, e.g. we have 20 lines and 20 images. To illustrate:

    How do we render this paragraph now? How do we wrap lines?

    Best idea I could come up with is this:

    • split it on images, resulting in slices that will cross the line boundary
    • place each slice on a canvas with width extending from the right corner of the image up to the wrap margin
    • use offset-to-caret on top right corner of the canvas to find out at which index the wrapping occurred
    • split the slice at the obtained index
    • render only the part that fits within the line
    • continue this until whole paragraph is rendered

    Clearly, not the most straighforward way. Could there be a function that takes string and width (as integer) and returns same string offset by the number of chars that fit into given width? I can do that with the idea described above, but this should be in the language. Officially.

    Could size-text be enhanced to measure text without faces or other intermediate objects?\ This is the biggest issue I'm seeing with command-based approach. If text inherits Draw state, how can we export that state to measure the text? Or if we measure the text with Draw's default state, we'll need to insert state reset (pen, font) before Draw's text command, to sync what was measured with what is about to be rendered.

  5. Draw requires rich-text face as an object!, which requires compose or (usually) compose/deep.

    Would be nice to have :get-words in Draw for this.

  6. I can imagine Draw's text also accepting the data block of rich-text for full control over each letter. Then it will even be superior to the face.