Faces for the selected letters, project format version 11 (13.2)
This commit is contained in:
1 parent
802ae82f72
commit
dcedfea53f
29 files changed
+869
-139
No files matched your search
@@ -0,0 +1,210 @@
|
||||
#include "Document/ColorPalette.h"
|
||||
#include "Document/TypeTool.h"
|
||||
#include <QTextBoundaryFinder>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
// LayerTextStyle's letters in colours and faces of their own.
|
||||
bool LayerTextStyle::colorRunsAreValid() const
|
||||
{
|
||||
if (!colorRuns)
|
||||
return true;
|
||||
qint64 end = 0;
|
||||
for (const LayerTextColorRun &run : *colorRuns) {
|
||||
// Each after the last, not empty, its end within Int.
|
||||
if (run.location < end || run.length <= 0 || run.location > std::numeric_limits<qint64>::max() - run.length)
|
||||
return false;
|
||||
for (const double channel : {run.red, run.green, run.blue}) {
|
||||
if (!(channel >= 0 && channel <= 1))
|
||||
return false;
|
||||
}
|
||||
end = run.location + run.length;
|
||||
}
|
||||
return !colorRuns->empty() && end <= content.size();
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Swift's guard: a name, 200 characters at most, no newline.
|
||||
bool usableFontName(const QString &name)
|
||||
{
|
||||
if (name.isEmpty())
|
||||
return false;
|
||||
for (const QChar unit : name) {
|
||||
if (unit == u'\n' || unit == u'\r' || unit == u'\v' || unit == u'\f' || unit == QChar(0x85) || unit == QChar::LineSeparator || unit == QChar::ParagraphSeparator)
|
||||
return false;
|
||||
}
|
||||
QTextBoundaryFinder characters(QTextBoundaryFinder::Grapheme, name);
|
||||
int count = 0;
|
||||
while (characters.toNextBoundary() >= 0)
|
||||
++count;
|
||||
return count <= 200;
|
||||
}
|
||||
}
|
||||
|
||||
bool LayerTextStyle::fontRunsAreValid() const
|
||||
{
|
||||
if (!fontRuns)
|
||||
return true;
|
||||
qint64 end = 0;
|
||||
for (const LayerTextFontRun &run : *fontRuns) {
|
||||
if (run.location < end || run.length <= 0 || run.location > std::numeric_limits<qint64>::max() - run.length || !usableFontName(run.fontName))
|
||||
return false;
|
||||
end = run.location + run.length;
|
||||
}
|
||||
return !fontRuns->empty() && end <= content.size();
|
||||
}
|
||||
|
||||
PaletteColor LayerTextStyle::color(qint64 index) const
|
||||
{
|
||||
for (const LayerTextColorRun &run : colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
if (run.location <= index && index < run.location + run.length)
|
||||
return {run.red, run.green, run.blue};
|
||||
}
|
||||
return {red, green, blue};
|
||||
}
|
||||
|
||||
void LayerTextStyle::setColor(const PaletteColor &color, TextSpan span)
|
||||
{
|
||||
const qint64 count = content.size();
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
if (start == end || (start == 0 && end == count)) {
|
||||
red = color.red;
|
||||
green = color.green;
|
||||
blue = color.blue;
|
||||
colorRuns = std::nullopt;
|
||||
return;
|
||||
}
|
||||
std::vector<PaletteColor> colors = unitColors();
|
||||
std::fill(colors.begin() + start, colors.begin() + end, color);
|
||||
setUnitColors(colors);
|
||||
}
|
||||
|
||||
QString LayerTextStyle::fontNameAt(qint64 index) const
|
||||
{
|
||||
for (const LayerTextFontRun &run : fontRuns.value_or(std::vector<LayerTextFontRun>())) {
|
||||
if (run.location <= index && index < run.location + run.length)
|
||||
return run.fontName;
|
||||
}
|
||||
return fontName;
|
||||
}
|
||||
|
||||
std::optional<QString> LayerTextStyle::uniformFontName(TextSpan span) const
|
||||
{
|
||||
const qint64 count = content.size();
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
if (end <= start)
|
||||
return std::nullopt;
|
||||
const QString face = fontNameAt(start);
|
||||
qint64 index = start;
|
||||
for (const LayerTextFontRun &run : fontRuns.value_or(std::vector<LayerTextFontRun>())) {
|
||||
if (run.location >= end || run.location + run.length <= index)
|
||||
continue;
|
||||
// A gap in the base face, or another run face.
|
||||
if ((run.location > index && fontName != face) || run.fontName != face)
|
||||
return std::nullopt;
|
||||
index = std::min(end, std::max(index, run.location + run.length));
|
||||
}
|
||||
if (index < end && fontName != face)
|
||||
return std::nullopt;
|
||||
return face;
|
||||
}
|
||||
|
||||
void LayerTextStyle::setFont(const QString &name, TextSpan span)
|
||||
{
|
||||
if (!usableFontName(name))
|
||||
return;
|
||||
const qint64 count = content.size();
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
if (start == end || (start == 0 && end == count)) {
|
||||
fontName = name;
|
||||
fontRuns = std::nullopt;
|
||||
return;
|
||||
}
|
||||
std::vector<QString> fonts = unitFonts();
|
||||
std::fill(fonts.begin() + start, fonts.begin() + end, name);
|
||||
setUnitFonts(fonts);
|
||||
}
|
||||
|
||||
void LayerTextStyle::replaceCharacters(TextSpan span, qint64 length)
|
||||
{
|
||||
const qint64 count = content.size();
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
// New letters take the colour and face before them.
|
||||
if (colorRuns) {
|
||||
std::vector<PaletteColor> colors = unitColors();
|
||||
const PaletteColor inherited = start > 0 ? colors[size_t(start - 1)] : end > start ? colors[size_t(start)] : !colors.empty() ? colors.front() : PaletteColor{red, green, blue};
|
||||
colors.erase(colors.begin() + start, colors.begin() + end);
|
||||
colors.insert(colors.begin() + start, size_t(std::max<qint64>(0, length)), inherited);
|
||||
setUnitColors(colors);
|
||||
}
|
||||
if (fontRuns) {
|
||||
std::vector<QString> fonts = unitFonts();
|
||||
const QString inherited = start > 0 ? fonts[size_t(start - 1)] : end > start ? fonts[size_t(start)] : !fonts.empty() ? fonts.front() : fontName;
|
||||
fonts.erase(fonts.begin() + start, fonts.begin() + end);
|
||||
fonts.insert(fonts.begin() + start, size_t(std::max<qint64>(0, length)), inherited);
|
||||
setUnitFonts(fonts);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<PaletteColor> LayerTextStyle::unitColors() const
|
||||
{
|
||||
std::vector<PaletteColor> colors(size_t(content.size()), PaletteColor{red, green, blue});
|
||||
for (const LayerTextColorRun &run : colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
const qint64 from = std::max<qint64>(0, run.location), to = std::min<qint64>(qint64(colors.size()), run.location + run.length);
|
||||
for (qint64 index = from; index < to; ++index)
|
||||
colors[size_t(index)] = {run.red, run.green, run.blue};
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
void LayerTextStyle::setUnitColors(const std::vector<PaletteColor> &colors)
|
||||
{
|
||||
const PaletteColor base{red, green, blue};
|
||||
std::vector<LayerTextColorRun> runs;
|
||||
for (size_t index = 0; index < colors.size(); ++index) {
|
||||
const PaletteColor &color = colors[index];
|
||||
if (color == base)
|
||||
continue;
|
||||
if (!runs.empty() && size_t(runs.back().location + runs.back().length) == index
|
||||
&& PaletteColor{runs.back().red, runs.back().green, runs.back().blue} == color)
|
||||
runs.back().length += 1;
|
||||
else
|
||||
runs.push_back({qint64(index), 1, color.red, color.green, color.blue});
|
||||
}
|
||||
colorRuns = runs.empty() ? std::nullopt : std::optional(runs);
|
||||
}
|
||||
|
||||
std::vector<QString> LayerTextStyle::unitFonts() const
|
||||
{
|
||||
std::vector<QString> fonts(size_t(content.size()), fontName);
|
||||
for (const LayerTextFontRun &run : fontRuns.value_or(std::vector<LayerTextFontRun>())) {
|
||||
const qint64 from = std::max<qint64>(0, run.location), to = std::min<qint64>(qint64(fonts.size()), run.location + run.length);
|
||||
for (qint64 index = from; index < to; ++index)
|
||||
fonts[size_t(index)] = run.fontName;
|
||||
}
|
||||
return fonts;
|
||||
}
|
||||
|
||||
void LayerTextStyle::setUnitFonts(const std::vector<QString> &fonts)
|
||||
{
|
||||
// One face throughout becomes the text's own.
|
||||
if (!fonts.empty() && std::all_of(fonts.begin(), fonts.end(), [&](const QString &name) { return name == fonts.front(); })) {
|
||||
fontName = fonts.front();
|
||||
fontRuns = std::nullopt;
|
||||
return;
|
||||
}
|
||||
std::vector<LayerTextFontRun> runs;
|
||||
for (size_t index = 0; index < fonts.size(); ++index) {
|
||||
if (fonts[index] == fontName)
|
||||
continue;
|
||||
if (!runs.empty() && size_t(runs.back().location + runs.back().length) == index && runs.back().fontName == fonts[index])
|
||||
runs.back().length += 1;
|
||||
else
|
||||
runs.push_back({qint64(index), 1, fonts[index]});
|
||||
}
|
||||
fontRuns = runs.empty() ? std::nullopt : std::optional(runs);
|
||||
}
|
||||
@@ -46,94 +46,7 @@ bool LayerTextStyle::isValid() const
|
||||
// Ranges refuse NaN and infinities: Swift's isFinite adds nothing.
|
||||
const auto within = [](double value, double low, double high) { return value >= low && value <= high; };
|
||||
return content.size() <= 100'000 && boxIsValid() && within(fontSize, 1, 2000) && within(red, 0, 1) && within(green, 0, 1)
|
||||
&& within(blue, 0, 1) && within(tracking, -100, 1000) && within(leading, 0, 5000) && colorRunsAreValid();
|
||||
}
|
||||
|
||||
bool LayerTextStyle::colorRunsAreValid() const
|
||||
{
|
||||
if (!colorRuns)
|
||||
return true;
|
||||
qint64 end = 0;
|
||||
for (const LayerTextColorRun &run : *colorRuns) {
|
||||
// Each after the last, not empty, its end within Int.
|
||||
if (run.location < end || run.length <= 0 || run.location > std::numeric_limits<qint64>::max() - run.length)
|
||||
return false;
|
||||
for (const double channel : {run.red, run.green, run.blue}) {
|
||||
if (!(channel >= 0 && channel <= 1))
|
||||
return false;
|
||||
}
|
||||
end = run.location + run.length;
|
||||
}
|
||||
return !colorRuns->empty() && end <= content.size();
|
||||
}
|
||||
|
||||
PaletteColor LayerTextStyle::color(qint64 index) const
|
||||
{
|
||||
for (const LayerTextColorRun &run : colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
if (run.location <= index && index < run.location + run.length)
|
||||
return {run.red, run.green, run.blue};
|
||||
}
|
||||
return {red, green, blue};
|
||||
}
|
||||
|
||||
void LayerTextStyle::setColor(const PaletteColor &color, TextSpan span)
|
||||
{
|
||||
const qint64 count = content.size();
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
if (start == end || (start == 0 && end == count)) {
|
||||
red = color.red;
|
||||
green = color.green;
|
||||
blue = color.blue;
|
||||
colorRuns = std::nullopt;
|
||||
return;
|
||||
}
|
||||
std::vector<PaletteColor> colors = unitColors();
|
||||
std::fill(colors.begin() + start, colors.begin() + end, color);
|
||||
setUnitColors(colors);
|
||||
}
|
||||
|
||||
void LayerTextStyle::replaceCharacters(TextSpan span, qint64 length)
|
||||
{
|
||||
if (!colorRuns)
|
||||
return;
|
||||
std::vector<PaletteColor> colors = unitColors();
|
||||
const qint64 count = qint64(colors.size());
|
||||
const qint64 start = std::clamp<qint64>(span.location, 0, count);
|
||||
const qint64 end = std::max(start, std::min(span.location + span.length, count));
|
||||
// New letters take the colour before them, as typing does.
|
||||
const PaletteColor inherited = start > 0 ? colors[size_t(start - 1)] : end > start ? colors[size_t(start)] : !colors.empty() ? colors.front() : PaletteColor{red, green, blue};
|
||||
colors.erase(colors.begin() + start, colors.begin() + end);
|
||||
colors.insert(colors.begin() + start, size_t(std::max<qint64>(0, length)), inherited);
|
||||
setUnitColors(colors);
|
||||
}
|
||||
|
||||
std::vector<PaletteColor> LayerTextStyle::unitColors() const
|
||||
{
|
||||
std::vector<PaletteColor> colors(size_t(content.size()), PaletteColor{red, green, blue});
|
||||
for (const LayerTextColorRun &run : colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
const qint64 from = std::max<qint64>(0, run.location), to = std::min<qint64>(qint64(colors.size()), run.location + run.length);
|
||||
for (qint64 index = from; index < to; ++index)
|
||||
colors[size_t(index)] = {run.red, run.green, run.blue};
|
||||
}
|
||||
return colors;
|
||||
}
|
||||
|
||||
void LayerTextStyle::setUnitColors(const std::vector<PaletteColor> &colors)
|
||||
{
|
||||
const PaletteColor base{red, green, blue};
|
||||
std::vector<LayerTextColorRun> runs;
|
||||
for (size_t index = 0; index < colors.size(); ++index) {
|
||||
const PaletteColor &color = colors[index];
|
||||
if (color == base)
|
||||
continue;
|
||||
if (!runs.empty() && size_t(runs.back().location + runs.back().length) == index
|
||||
&& PaletteColor{runs.back().red, runs.back().green, runs.back().blue} == color)
|
||||
runs.back().length += 1;
|
||||
else
|
||||
runs.push_back({qint64(index), 1, color.red, color.green, color.blue});
|
||||
}
|
||||
colorRuns = runs.empty() ? std::nullopt : std::optional(runs);
|
||||
&& within(blue, 0, 1) && within(tracking, -100, 1000) && within(leading, 0, 5000) && colorRunsAreValid() && fontRunsAreValid();
|
||||
}
|
||||
|
||||
std::optional<LayerText> LayerText::loaded(const std::optional<LayerTextStyle> &style, const std::optional<ImportedImage> &image)
|
||||
@@ -255,8 +168,9 @@ bool EditorSession::applyText(TextDraft draft)
|
||||
addPixelLayer(image, draft.origin, layerName(draft.style.content), QStringLiteral("New Text Layer"), false, std::nullopt, draft.style);
|
||||
}
|
||||
m_textDefaults = draft.style;
|
||||
// The next text starts in one colour.
|
||||
// The next text starts in one colour and face.
|
||||
m_textDefaults.colorRuns = std::nullopt;
|
||||
m_textDefaults.fontRuns = std::nullopt;
|
||||
requestCanvasFocus();
|
||||
return succeed();
|
||||
} catch (const ProjectError &error) {
|
||||
|
||||
+21
-2
@@ -32,6 +32,14 @@ struct LayerTextColorRun {
|
||||
friend bool operator==(const LayerTextColorRun &, const LayerTextColorRun &) = default;
|
||||
};
|
||||
|
||||
// Letters in a face of their own, as Swift's LayerTextFontRun.
|
||||
struct LayerTextFontRun {
|
||||
qint64 location = 0;
|
||||
qint64 length = 0;
|
||||
QString fontName;
|
||||
friend bool operator==(const LayerTextFontRun &, const LayerTextFontRun &) = default;
|
||||
};
|
||||
|
||||
// What a text layer says and how, in layer pixels.
|
||||
struct LayerTextStyle {
|
||||
QString content = QStringLiteral("Text");
|
||||
@@ -49,6 +57,8 @@ struct LayerTextStyle {
|
||||
std::optional<QSizeF> boxSize = std::nullopt;
|
||||
// Sorted, apart, within the content; none is one colour.
|
||||
std::optional<std::vector<LayerTextColorRun>> colorRuns = std::nullopt;
|
||||
// The same rules; none is one face.
|
||||
std::optional<std::vector<LayerTextFontRun>> fontRuns = std::nullopt;
|
||||
// The gap between the text and its box, either kind.
|
||||
static constexpr double padding = 12;
|
||||
double autoLeading() const { return fontSize * 1.2; }
|
||||
@@ -59,14 +69,23 @@ struct LayerTextStyle {
|
||||
PaletteColor color(qint64 index) const;
|
||||
// Paints `span`; empty or whole, it recolours all the text.
|
||||
void setColor(const PaletteColor &color, TextSpan span);
|
||||
// Moves the colours as `span` becomes `length` units.
|
||||
// The face of the UTF-16 unit at `index`.
|
||||
QString fontNameAt(qint64 index) const;
|
||||
// The one face over `span`; none when empty or mixed.
|
||||
std::optional<QString> uniformFontName(TextSpan span) const;
|
||||
// Sets `span`'s face; empty or whole, all the text's.
|
||||
void setFont(const QString &name, TextSpan span);
|
||||
// Moves colours and faces as `span` becomes `length` units.
|
||||
void replaceCharacters(TextSpan span, qint64 length);
|
||||
friend bool operator==(const LayerTextStyle &, const LayerTextStyle &) = default;
|
||||
|
||||
private:
|
||||
bool colorRunsAreValid() const;
|
||||
bool fontRunsAreValid() const;
|
||||
std::vector<PaletteColor> unitColors() const;
|
||||
void setUnitColors(const std::vector<PaletteColor> &colors);
|
||||
std::vector<QString> unitFonts() const;
|
||||
void setUnitFonts(const std::vector<QString> &fonts);
|
||||
};
|
||||
|
||||
// A text layer: its style and the image it drew.
|
||||
@@ -85,7 +104,7 @@ struct TextDraft {
|
||||
QPointF origin;
|
||||
std::optional<LayerTransform> transform = std::nullopt;
|
||||
LayerTextStyle style;
|
||||
// What the editor selects; colour applies to it alone.
|
||||
// What the editor selects; colour and face apply to it.
|
||||
TextSpan selection;
|
||||
friend bool operator==(const TextDraft &, const TextDraft &) = default;
|
||||
};
|
||||
@@ -120,6 +120,22 @@ std::vector<LayerTextColorRun> colorRuns(const QJsonValue &value)
|
||||
return runs;
|
||||
}
|
||||
|
||||
LayerTextFontRun fontRun(const QJsonValue &value)
|
||||
{
|
||||
const QJsonObject object = value.toObject();
|
||||
return {integer(object.value("location")), integer(object.value("length")), string(object.value("fontName"))};
|
||||
}
|
||||
|
||||
std::vector<LayerTextFontRun> fontRuns(const QJsonValue &value)
|
||||
{
|
||||
if (!value.isArray())
|
||||
refuse();
|
||||
std::vector<LayerTextFontRun> runs;
|
||||
for (const QJsonValue &each : value.toArray())
|
||||
runs.push_back(fontRun(each));
|
||||
return runs;
|
||||
}
|
||||
|
||||
// Synthesized Codable: all required but the box and runs.
|
||||
LayerTextStyle textStyle(const QJsonValue &value)
|
||||
{
|
||||
@@ -130,7 +146,8 @@ LayerTextStyle textStyle(const QJsonValue &value)
|
||||
return {.content = string(object.value("content")), .fontName = string(object.value("fontName")), .fontSize = number(object.value("fontSize")),
|
||||
.red = number(object.value("red")), .green = number(object.value("green")), .blue = number(object.value("blue")),
|
||||
.alignment = *alignment, .tracking = number(object.value("tracking")), .leading = number(object.value("leading")),
|
||||
.boxSize = optional(object, "boxSize", size), .colorRuns = optional(object, "colorRuns", colorRuns)};
|
||||
.boxSize = optional(object, "boxSize", size), .colorRuns = optional(object, "colorRuns", colorRuns),
|
||||
.fontRuns = optional(object, "fontRuns", fontRuns)};
|
||||
}
|
||||
|
||||
CanvasGuide guide(const QJsonValue &value)
|
||||
@@ -200,6 +217,12 @@ QJsonObject encoded(const LayerTextStyle &text)
|
||||
runs.append(QJsonObject{{"location", run.location}, {"length", run.length}, {"red", run.red}, {"green", run.green}, {"blue", run.blue}});
|
||||
object.insert("colorRuns", runs);
|
||||
}
|
||||
if (text.fontRuns) {
|
||||
QJsonArray runs;
|
||||
for (const LayerTextFontRun &run : *text.fontRuns)
|
||||
runs.append(QJsonObject{{"location", run.location}, {"length", run.length}, {"fontName", run.fontName}});
|
||||
object.insert("fontRuns", runs);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,8 +73,9 @@ void validate(const ProjectManifest &manifest)
|
||||
if (manifest.width < 1 || manifest.width > DocumentLimits::maxSide || manifest.height < 1 || manifest.height > DocumentLimits::maxSide || manifest.layers.size() > 10'000)
|
||||
throw ProjectError(ProjectError::Kind::tooLarge);
|
||||
for (const ProjectLayerRecord &layer : manifest.layers) {
|
||||
// Text needs a valid style, pixels; runs from 10.
|
||||
if (layer.text && (!layer.text->isValid() || !layer.imageFile || (layer.text->colorRuns && manifest.version < 10)))
|
||||
// Valid text with pixels; colours from 10, faces from 11.
|
||||
if (layer.text && (!layer.text->isValid() || !layer.imageFile || (layer.text->colorRuns && manifest.version < 10)
|
||||
|| (layer.text->fontRuns && manifest.version < 11)))
|
||||
throw ProjectError(ProjectError::Kind::invalid);
|
||||
// Version 7's adjustments hold settings, never pixels or children.
|
||||
if (layer.adjustment && (manifest.version < 7 || layer.isGroup == true || layer.imageFile || !layer.adjustment->isValid()))
|
||||
|
||||
@@ -51,7 +51,7 @@ QString uuidString(const QUuid &id);
|
||||
|
||||
struct ProjectManifest {
|
||||
// Swift's current and supported: what saves write, what loads accept.
|
||||
static constexpr qint64 current = 10;
|
||||
static constexpr qint64 current = 11;
|
||||
static constexpr std::pair<qint64, qint64> supported{1, current};
|
||||
static bool supports(qint64 version) { return version >= supported.first && version <= supported.second; }
|
||||
QString format = QStringLiteral("com.compositor.project");
|
||||
|
||||
@@ -42,14 +42,19 @@ const std::map<QString, std::pair<QString, QString>> &faces()
|
||||
}
|
||||
|
||||
QFont TextLayout::font(const LayerTextStyle &style)
|
||||
{
|
||||
return font(style, style.fontName);
|
||||
}
|
||||
|
||||
QFont TextLayout::font(const LayerTextStyle &style, const QString &face)
|
||||
{
|
||||
QFont font;
|
||||
if (faces().contains(style.fontName)) {
|
||||
font.setFamily(faces().at(style.fontName).first);
|
||||
font.setStyleName(faces().at(style.fontName).second);
|
||||
if (faces().contains(face)) {
|
||||
font.setFamily(faces().at(face).first);
|
||||
font.setStyleName(faces().at(face).second);
|
||||
} else {
|
||||
// Not installed: Qt's match, where macOS takes its own.
|
||||
font.setFamily(style.fontName);
|
||||
font.setFamily(face);
|
||||
}
|
||||
// A point is a pixel here; Qt's faces come whole.
|
||||
font.setPixelSize(qRound(style.fontSize));
|
||||
@@ -93,8 +98,16 @@ TextLines::TextLines(const LayerTextStyle &style, QSizeF container)
|
||||
const QString paragraph = style.content.mid(range.start, range.end - range.start);
|
||||
auto layout = std::make_unique<QTextLayout>(paragraph, font);
|
||||
layout->setTextOption(option);
|
||||
// Run letters draw once in their colour, as CoreText's.
|
||||
// Run letters draw once in their face and colour.
|
||||
QList<QTextLayout::FormatRange> runs;
|
||||
for (const LayerTextFontRun &run : style.fontRuns.value_or(std::vector<LayerTextFontRun>())) {
|
||||
const qint64 start = std::max<qint64>(run.location, range.start), end = std::min<qint64>(run.location + run.length, range.end);
|
||||
if (start >= end)
|
||||
continue;
|
||||
QTextCharFormat format;
|
||||
format.setFont(TextLayout::font(style, run.fontName));
|
||||
runs << QTextLayout::FormatRange{int(start - range.start), int(end - start), format};
|
||||
}
|
||||
for (const LayerTextColorRun &run : style.colorRuns.value_or(std::vector<LayerTextColorRun>())) {
|
||||
const qint64 start = std::max<qint64>(run.location, range.start), end = std::min<qint64>(run.location + run.length, range.end);
|
||||
if (start >= end)
|
||||
|
||||
@@ -13,6 +13,8 @@ class QPainter;
|
||||
namespace TextLayout {
|
||||
// The face a PostScript name names, or Qt's match.
|
||||
QFont font(const LayerTextStyle &style);
|
||||
// The style's size and spacing in another face.
|
||||
QFont font(const LayerTextStyle &style, const QString &face);
|
||||
// Every installed face's PostScript name, sorted.
|
||||
QStringList availableFonts();
|
||||
}
|
||||
|
||||
+50
-7
@@ -96,10 +96,37 @@ TypeFontPicker::TypeFontPicker(QWidget *parent) : QComboBox(parent)
|
||||
setAccessibleName(QStringLiteral("Font"));
|
||||
}
|
||||
|
||||
namespace {
|
||||
const QString multiple = QStringLiteral("(Multiple)");
|
||||
}
|
||||
|
||||
bool TypeFontPicker::isMultiple(int index) const
|
||||
{
|
||||
return itemData(index).toString() == multiple;
|
||||
}
|
||||
|
||||
void TypeFontPicker::showMultiple()
|
||||
{
|
||||
if (!isMultiple(0))
|
||||
insertItem(0, multiple, multiple);
|
||||
setCurrentIndex(0);
|
||||
}
|
||||
|
||||
void TypeFontPicker::hideMultiple()
|
||||
{
|
||||
if (isMultiple(0))
|
||||
removeItem(0);
|
||||
}
|
||||
|
||||
void TypeFontPicker::sync(const QString &name)
|
||||
{
|
||||
if (view()->isVisible())
|
||||
return;
|
||||
if (name.isEmpty()) {
|
||||
showMultiple();
|
||||
return;
|
||||
}
|
||||
hideMultiple();
|
||||
if (findText(name) < 0)
|
||||
addItem(name);
|
||||
setCurrentIndex(findText(name));
|
||||
@@ -109,15 +136,18 @@ void TypeFontPicker::showPopup()
|
||||
{
|
||||
if (!m_loaded) {
|
||||
// Every installed face, and the style's own though missing.
|
||||
const QString selected = currentText();
|
||||
const QString selected = isMultiple(currentIndex()) ? QString() : currentText();
|
||||
QStringList names = TextLayout::availableFonts();
|
||||
if (!names.contains(selected))
|
||||
if (!selected.isEmpty() && !names.contains(selected))
|
||||
names << selected;
|
||||
names.sort();
|
||||
const QSignalBlocker quiet(this);
|
||||
clear();
|
||||
addItems(names);
|
||||
setCurrentIndex(findText(selected));
|
||||
if (selected.isEmpty())
|
||||
showMultiple();
|
||||
else
|
||||
setCurrentIndex(findText(selected));
|
||||
m_loaded = true;
|
||||
}
|
||||
QComboBox::showPopup();
|
||||
@@ -193,10 +223,11 @@ TypeControls::TypeControls(EditorSession &session, QWidget *parent)
|
||||
m_font->setToolTip(QStringLiteral("Font face, including bold and italic variants"));
|
||||
connect(m_font, &QComboBox::activated, this, [this](int index) {
|
||||
const QString name = m_font->itemText(index);
|
||||
// The face already shown changes nothing, as Swift's choose.
|
||||
if (name == m_session.currentTextStyle().fontName)
|
||||
// (Multiple), or the face already shown, changes nothing.
|
||||
if (m_font->isMultiple(index) || name == shownFont())
|
||||
return;
|
||||
m_session.changeTextStyle([&name](LayerTextStyle &style) { style.fontName = name; });
|
||||
const TextSpan selection = m_session.textDraft() ? m_session.textDraft().value().selection : TextSpan();
|
||||
m_session.changeTextStyle([&name, selection](LayerTextStyle &style) { style.setFont(name, selection); });
|
||||
});
|
||||
m_size->setObjectName(QStringLiteral("typeSize"));
|
||||
m_size->setFixedWidth(52);
|
||||
@@ -287,6 +318,18 @@ QToolButton *TypeControls::alignment(TextAlignment value)
|
||||
return button;
|
||||
}
|
||||
|
||||
QString TypeControls::shownFont() const
|
||||
{
|
||||
const std::optional<TextDraft> &draft = m_session.textDraft();
|
||||
if (!draft)
|
||||
return m_session.currentTextStyle().fontName;
|
||||
const TextSpan selection = draft->selection;
|
||||
if (selection.length == 0)
|
||||
return draft->style.fontNameAt(std::max<qint64>(0, selection.location - 1));
|
||||
// Several faces: (Multiple), so any face chosen applies.
|
||||
return draft->style.uniformFontName(selection).value_or(QString());
|
||||
}
|
||||
|
||||
void TypeControls::synchronize()
|
||||
{
|
||||
// Swift's onChange: open text previews the picker's colour.
|
||||
@@ -296,7 +339,7 @@ void TypeControls::synchronize()
|
||||
m_session.previewTextColor();
|
||||
}
|
||||
const LayerTextStyle style = m_session.currentTextStyle();
|
||||
m_font->sync(style.fontName);
|
||||
m_font->sync(shownFont());
|
||||
for (size_t index = 0; index < m_alignments.size(); ++index)
|
||||
m_alignments[index]->setChecked(style.alignment == allTextAlignments[index]);
|
||||
for (TextStyleField *field : {m_size, m_tracking, m_leading})
|
||||
|
||||
@@ -40,15 +40,19 @@ class TypeFontPicker : public QComboBox {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit TypeFontPicker(QWidget *parent = nullptr);
|
||||
// Shows `name`; an open menu is left alone.
|
||||
// Shows `name`, or (Multiple) when empty; an open menu stays.
|
||||
void sync(const QString &name);
|
||||
void showPopup() override;
|
||||
// Swift's isMultiple: the top item that is no font.
|
||||
bool isMultiple(int index) const;
|
||||
|
||||
protected:
|
||||
// A long name is cut short, never widening the control.
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
|
||||
private:
|
||||
void showMultiple();
|
||||
void hideMultiple();
|
||||
bool m_loaded = false;
|
||||
};
|
||||
|
||||
@@ -65,6 +69,8 @@ private:
|
||||
QToolButton *alignment(TextAlignment value);
|
||||
void applyGlyphs();
|
||||
void synchronize();
|
||||
// Swift's binding: the caret's face, the selection's, or none.
|
||||
QString shownFont() const;
|
||||
|
||||
EditorSession &m_session;
|
||||
TypeFontPicker *const m_font;
|
||||
|
||||
Reference in new issue
Block a user