-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Multiple text formats for symbols via "format" expression #6994
Conversation
We currently rely on static analysis of the style to determine which font stacks are used, so that they can be cached for offline use (mapbox/mapbox-gl-native#9939) or evicted from memory when no longer needed (mapbox/mapbox-gl-native#12388). Is that still possible under this proposal? |
Limiting the |
Is the thinking to extend this to include |
Yes, although not in this PR. Layout properties are a nice place to start because all of the layout information is already encoded per-glyph in the layout vertex buffers. To make this work with paint properties, we'll have to use something like the existing DDS paint property binders, even if the paint properties aren't actually data driven. |
@ChrisLoer @jfirebaugh Yeah, I think the static analysis for text-font is still possible, it'll just require an approach that's more specialized than the current, general
I don't think we would -- when we're traversing the tree, any instance of |
The required I'm currently using an interface where style sections are identified by an integer, and we annotate text strings by passing them around with a 1:1 matching array of style integers (one per character). It looks funky and duplicates a bunch of data in the common case, but since most of the logic involved wants to work character-by-character, it felt like a natural fit, and I doubt the overhead is that high... but it still looks odd and I'm waiting for someone to tell me I should do it differently. ;) |
@anandthakker I made the change so that we try doing coercions for all possible overloads before giving up -- that makes Separate issue: there are some subtle differences between what
For comparison, here's a scale |
@ChrisLoer the compound expression parsing change looks good. The affected existing tests are getting updated in #6961 anyway -- maybe just rebase onto there instead of updating them here? |
b807828
to
1c43c07
Compare
@anandthakker Writing tests and validation code brought up two more little challenges:
|
Just talked to Anand, he gave helpful advice: For the type of For Current implementation
would become:
The alternate approach is a bit more concise, it's a little more explicit about "default formatting", and I think it might feel more natural? On the con side, it pushes us away from the pattern "you can use a @nickidlugash or @samanpwbb, any opinions on the two options? |
I lean towards the alternate approach. This downside doesn't seem too bad, since formatted text is very likely to be used as the final output of an expression rather than an intermediate value. By the way -- what about |
@ChrisLoer I think the alternative approach looks good. Just so I can better envision how the syntax would be used in practice, can you confirm whether the style definitions based on this syntax would look more like this:
or if "foo" was just a literal:
rather than the example above, since it seems like it would usually make sense to use the default styling on as many of the strings as possible? |
@nickidlugash Yeah I think you would usually have at least one section that used the default options (e.g. In terms of expression parsing, we could support omitting the options entirely, so it would instead be something like:
That might look more concise/prettier, at the expense of making the expression a little less explicit? |
I think from a user perspective, this is simpler to reason about, though the more explicit version seems fine, too.
👍 |
👍 I'll implement:
I don't have strong feelings on whether to make the default formatting an optional argument, but I don't think we currently have anything that follows the kind of "varargs with optional arguments" pattern so I'm leaning towards holding off. @anandthakker after this change we'll no longer need the changes to allow argument coercion in overloads, since |
9a2c511
to
be75fe9
Compare
Tagging @mapbox/studio and @mapbox/maps-design: I think we've settled on a final syntax. One thing @nickidlugash and I discussed is that we might subtly alter the behavior of the font scaling in future implementations. The current implementation depends on modifying the positions of the layout vertices, which has the effect of stretching out the halo as well. When we do the |
Hmm, good question. I suppose we might as well keep it (and port to native)? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implementation looks great to me!
build/generate-style-code.js
Outdated
@@ -5,6 +5,7 @@ const fs = require('fs'); | |||
const ejs = require('ejs'); | |||
const spec = require('../src/style-spec/reference/v8'); | |||
const Color = require('../src/style-spec/util/color'); | |||
const {Formatted} = require('../src/style-spec/expression/definitions/formatted'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup!
flow-typed/style-spec.js
Outdated
@@ -222,7 +225,7 @@ declare type SymbolLayerSpecification = {| | |||
"icon-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, | |||
"text-pitch-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, | |||
"text-rotation-alignment"?: PropertyValueSpecification<"map" | "viewport" | "auto">, | |||
"text-field"?: DataDrivenPropertyValueSpecification<string>, | |||
"text-field"?: DataDrivenPropertyValueSpecification<FormattedSpecification>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heads up there may be some file location weirdness when you rebase onto master, due to #7031
font: Expression | null; | ||
} | ||
|
||
export class FormattedExpression implements Expression { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename to just Format
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
if (!text) return null; | ||
const kind = text.type.kind; | ||
if (kind !== 'string' && kind !== 'value' && kind !== 'null') | ||
return context.error(`Formatted text type must be 'string', 'value', or 'null'.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 should we just accept any type here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Basically support anything that has a string coercion? I'm ambivalent here, but I'll follow your guidance since you've been so involved in the explicit vs. implicit coercion design choices.
My starting position would be:
- It's easier to reason about the implementation without automatic coercion
- I can't immediately think of a "gotcha" case where this would be super unergonomic. If you want to format a plain number, you'll get an error message, but it'll be pretty clear what the error message means and there's a straightforward coercion call to address it.
- We can always relax the requirements in the future
|
||
let font = null; | ||
if (options['text-font']) { | ||
font = context.parse(options['text-font'], 1, ValueType); // Require array of strings? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'd say require an array of strings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure how to do this, it would be something like array(StringType)
? Will that work as we expect if the members of the array are ValueType
(i.e. parsing will coerce to StringType
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Made the change.
src/style-spec/validate/validate.js
Outdated
valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec | ||
})); | ||
if (!valid) { | ||
console.log("not valid"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😬
src/symbol/shaping.js
Outdated
function breakLines(text: string, lineBreakPoints: Array<number>) { | ||
class TaggedString { | ||
text: string; | ||
sectionIndex: Array<number> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// maps each character in 'text' to its corresponding entry in 'sections'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 The Formatted
and TaggedString
types kind of evolved separately as I worked on the implementation, but in the final result they end up looking pretty darn similar. I wonder if it makes sense to combine them? I don't think the cost of converting between the two is a big deal, and TaggedString
has a lot of shaping-focused details, so I guess my inclination is to leave as is (but also probably I'm just lazy so good to get another pair of eyes on the decision).
src/symbol/transform_text.js
Outdated
|
||
export default function(text: string | Formatted, layer: SymbolStyleLayer, feature: Feature) { | ||
if (text instanceof Formatted) { | ||
// OK to transform in place? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's probably fine... especially since we probably want to move towards having text transforms happen via expressions rather than through text-transform
anyway
Returns "formatted" type with formatting annotations applied to subsections. "text-field" now accepts formatted text, allowing symbols to use multiple fonts within the same label.
…tiple overloads. Support string->formatted coercion (currently unused).
Add test using new formatted RTL functionality.
- "FormattedExpression" -> "FormatExpression" (reasoning: the expression itself is a verb, the result is a noun) - Remove dead/debug code - Add comments
cc46fea
to
e9bbaac
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
This PR introduces a
format
expression that creates a new formatted text type, which can be passed to thetext-field
property of symbols. This is the first part of addressing #4366 (aka "multi-icon/multi-text symbols").The scope for this PR is to support two formatting options:
font-scale
: a ratio to thetext-size
of the parent symbol. Defaults to 1. This must be expressed as a ratio in order for zoom-dependent symbol resizing to work effectively.text-font
: if present, overrides the base font of the symbol.Further layout and paint properties will go in separate PRs.
This branch's current version of
debug/debug.html
uses a locally stored style sheet that exercises the variable formatting for bothplace-city-lg-s
and road labels.TODO
mapbox-gl-rtl-text
pluginSwitch internals from labeling each character with a section to tracking sections with "ranges"?string
/Formatted
to automatically coerce toFormatted
Reduce fontStack tracking overhead (this might not actually be a big deal, no immediately discernible difference shows up in the Layout benchmark)Launch Checklist
@mapbox/studio
and/or@mapbox/maps-design
if this PR includes style spec changes/cc @anandthakker @hannahgelfond @samanpwbb @nickidlugash