Super Editor layout refactor - Feb, 2022 #427
matthew-carroll
started this conversation in
Design Doc
Replies: 1 comment 1 reply
-
The "presentation pipeline" will be great concept for developers 👍 About the open issues:
Could there be a builder that takes the current and next nodes and returns a padding. For example: componentVerticalSpacingBuilder: (node, nextNode) {
if(node is ImageNode && nextNode is ParagraphNode) {
return 16;
}
if(node is ParagraphNode && nextNode is ImageNode) {
return 12;
}
// default value
return 8.0;
}, |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
I'm refactoring the layout presentation implementation for Super Editor. This document explains the motivation for these changes and outlines what some of them look like.
Motivation
This effort got started with a very simple goal: Let the user make an image take up the full width of the layout.
But this goal introduced a philosophical question: Where should we store the image's width configuration?
Answering this question lead me down a rabbit hole, which resulted in significant refactoring that includes the following side effects:
SuperEditor
widget)The image width problem
Technically, making an image full-width is trivial. But conceptually, where should such a setting live?
The most obvious place to store an image's width is in the document, itself. However, a document should be layout agnostic. The idea of a full-bleed image makes sense in a single-column layout, but what about a newspaper layout, or an academic paper layout? Those layouts don't have any concept of full-bleed sizing. Thus, if we extend this idea in general, we could end up with a document that's full of metadata intended for different layouts. We might not know which piece of metadata is intended for which layout, some of the metadata might conflict, and we're stuck pushing around a document that carries a bunch of presentation configuration. This seemed like a subpar approach.
Instead, why don't we store presentation configuration on it's own? That way, logical documents remain devoid of layout configuration, and a system only needs to send and understand the layout configuration that's intended for the given layout/editor. This approach is roughly analogous to the separation of HTML and CSS files. The HTML (in theory) represents logical documents, and CSS represents layout and styles that should be applied to that document. One document could be styled by many different CSS configurations.
Where do we store styles?
The existing
SuperEditor
implementation used a few widget properties and a couple builders to configure all layout details. For example, theSuperEditor
widget takes inpadding
,componentVerticalSpacing
,selectionStyle
, andtextStyleBuilder
as properties.These properties create two issues. First, if a bunch of style values are passed around in various properties, then there's no easy way to select, assign, or store values for these properties. Second, builders are functions, so they can't be serialized for storage. If we want to make it easy to transfer, use, and change styles then we need a presentation solution that's more centralized.
Stylesheets and component styles
There are two distinct types of style rules. There are rules that apply in general, such as "Every H1 should use font size 48". Then there are component-specific rules, such as "The image with ID '73jgj3w4' should be full-width".
I call the general rules "stylesheets" and I call the specific rules "component styles".
The
SuperEditor
widget now takes in a stylesheet and a set of component styles.The old
SuperEditor
widget looked like this:The new
SuperEditor
widget looks like this:The client is responsible for mainting the desired stylesheet, component styles, and selection styles, but those styles are now centralized, with a clear place to pass those details to
SuperEditor
.From styles to rendering
With changes to the style structures, I had to revisit how the styles were being applied to the widget tree. This seemed like a good opportunity to address one of the known causes of unnecessary widget rebuilds and repaints.
If a single document node changes, then we'd like for just that node's UI component to rebuild, and nothing else. Currently, we reflow the entire layout whenever anything changes, so this improvement requires a different approach. I decided to introduce a Presenter, which would produce a View Model for the entire layout. The layout widget could then selectively rebuild specific component widgets, and only reflow the layout when components are added or removed.
The general process would look like:
The presentation pipeline
The Presenter object seemed to offer another opportunity for optimization. We're rendering with a set of styles that are largely independent: document-wide styles, component-specific styles, and selection styles. These styles change at very different frequencies. For example, a user almost never changes an image's width, but a user is constantly moving the caret. Therefore, it's a poor use of resources to re-calculate the component-specific styles every time the user moves the caret.
To reduce unnecessary re-calculations, I implemented the Presenter with a pipeline of style phases:
In addition to the performance improvement, this pipeline makes it trivial to add new style phases, if desired.
Open Issues
How do we apply padding on a contextual basis, e.g., paragraph after a header vs paragraph after a paragraph?
How do we make it easy to re-style based on device form factor? The answer might be to pass in a different stylesheet, but we should verify.
How do we support custom types of content blocks? The current stylesheet approach is strongly typed against a very specific set of blocks.
Beta Was this translation helpful? Give feedback.
All reactions