linebender / linebender/druid

text work planning & discussion

Open
#883 8 comments 19 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

architecture discussion write-up
Dominant language
Rust
Stars
9.7k
Forks
565
PR merge metrics
No merged PRs in 30d

Description

# Druid text planning

This is intended as a medium-level sketch for how we will implement multi-line text, text editing, and (eventually) rich text in druid.

This covers a number of things: the storage of text itself, the storage of style and color information, the storage of selection state, and the storage of line breaks and width measurement, as well as the relationships between these, as is required for things like interactive editing.

## Representation of text

We are going to adapt the [xi-rope] crate as our main way of representing text buffers. This is a not-insignificant depedency, but ultimately it feels worthwhile; the design of the rope (using smart pointers under the hood) makes it very well suited to druid's `Data` model, and xi-rope includes a large API for things like representing and applying edits, diffing, text search, and rich
text spans.

We will use xi-rope's `Rope` to store our text, and the `Spans` type to (later) represent rich text information.

These two types will be used together, and will be wrapped up in some druid type, with a name like `TextBuffer`.

## Line breaking and width measurement

In order to display text to the user in a meaningful way, we need to be able to do line breaking and width measurement. A version of this is provided by piet; we will wrap this API in druid, providing our own `TextLayout` type.

This type will own a copy of the `TextBuffer`, and will also have a width; it will be responsible for both painting text as well as for converting from points on the screen (e.g. from a mouse click) to offsets in the buffer.

You will create a `TextLayout` from a `TextBuffer` and a `PietText` object (discussion: can we make `PietText` not be bound to a lifetime?); you will then be able to paint it, using a convenience method looking something like, `TextLayout::draw(&self, ctx: &mut PaintCtx, point: Point, env: &Env)`. You will also be able to query the layout for information like the offset in the buffer that corresponds to a given point in the layout.

When the buffer changes elsewhere, you will update the `TextLayout`, by calling a method like (e.g.) `TextLayout::update_buffer(&mut self, buffer: &TextBuffer)` and then the `TextLayout` will update its internal state. An important part of this design is that the layout will be able to update itself incrementally, by comparing the old and the new buffers; however for our first pass we may just recompute the entire layout from scratch.

This `TextLayout` type will be the basis for all text that is displayed in druid; we will discourage the use of piet directly.

## Selection state and editing

The last major component to manage will be selection state. This is slightly tricky; in xi, for instance, we treat selection state as a property of the 'view' (which corresponds to our `TextLayout`, above) but this might not work in druid. The main question is whether or not selection state should be part of the data model, or whether it is just part of the view state. I'm of two minds. The main question seems to be whether or not it is reasonable during normal use for the selection state to be modified elsewhere in the application.

I see a number of options: the selection state could be part of the `TextBuffer`, it could be part of the `TextLayout`, or it could be independent?

In any case, selection exists at the intersection of the buffer and the layout; it is stored in terms of offsets into the buffer, but it can only be modified with access to the layout (for instance: if we press the 'down' arrow, how do we know what offset in the buffer corresponds to our current horizontal position on the subsequent line?)

Regardless of where the selection is stored, edit operations can be considered as functions of the form, `Fn(TextBuffer, Selection, &TextLayout, &EditOp) -> (TextBuffer, Selection)`. That is: it takes a buffer and a selection, and referencing the layout produces a new buffer and a new selection.

## Rich text

`TextBuffer` will be able to store style (font, color, size) information for regions of its content. This is not going to be fully exposed initially; for the time being a given buffer + layout must use a single style.

As follow-up work, however, we will want to allow proper rich text. This is going to require some additional work in piet, and also a major design decision: should piet handle the layout of rich text (requiring us to expose our `TextBuffer` and related styling types there) or should we implmenet a lower-level piet api that operates on [glyph][glyphruns1] [runs][glyphruns2], but which requires more work in druid in order to handle actual line breaking?

This is a complicated question; it makes sense for piet to be as low level an api as is reasonable, but moving to using runs is potentially a reasonable large project, and it may not offer any real efficiency improvements over an approach that just relies on directwrite/coretext/etc to do more of the heavy lifting.

## Next steps:

I'm going to play around with this general design, and see if it feels like a reasonable direction.

## Open questions:

**where does selection state live?**: I think it should be part of either `TextBuffer` or of `TextLayout` (which owns a copy of the `TextBuffer`), largely because it makes it easy for `TextLayout` to also be responsible for drawing the selection. Between these two it comes down to whether or not we want selection state to be part of the data model, or not.

**How do we handle layout for rich text**? we'll cross this bridge when we get to it.

**What do we need in a font api?** We didn't touch on this, but an additional aspect of this work is going to be a fleshing out of the piet api for resolving and using fonts.

**What other minor changes might we want in the piet api?** For instance, can we make `PietText` not be bound by a lifetime? This may not be necessary, but might be a quality of life improvement?

[xi-rope]: https://docs.rs/xi-rope/0.3.0/xi_rope/
[glyphruns1]: https://docs.microsoft.com/en-us/windows/win32/directwrite/glyphs-and-glyph-runs#glyph-runs
[glyphruns2]: https://developer.apple.com/documentation/coretext/ctrun-61n

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with this issue's proposed TextBuffer and TextLayout design, including xi-rope and PietText, and review the listed open questions about selection state, rich-text layout, fonts, and the piet API. The issue does not identify implementation files, tests, or a bounded first milestone, so completion criteria would need to be established before work can begin.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.