Colour only the selected letters of text being edited; project format version 10 (12.14d1)

This commit is contained in:
雷电芽衣 committed 2026-09-30 05:15:51 -04:00
1 parent 2fa8d9921d
commit 27a4fe8428
32 files changed
+1398 -82

No files matched your search

+4 -1
View File
@@ -27,6 +27,8 @@ public:
void previewEffectColor();
// The open text draft previews the picker's working colour.
void previewTextColor();
// Paints the open text's selected letters, or all of it.
void setDraftTextColor(const PaletteColor &color);
// Swift writes the picker's hsb in place; here, the session.
void setColorPickerHSB(const PickerHSB &hsb);
void sampleIntoColorPicker(QPointF point);
@@ -34,6 +36,7 @@ public:
std::optional<PaletteColor> sampleCompositeColor(QPointF point) const;
private:
void paintText(const PaletteColor &color);
// Puts back the open text's colours from the picker's opening.
void restoreDraftTextColors(const LayerTextStyle &original);
void setGradientMapColor(const PaletteColor &color, bool highlights);
void setVignetteColor(const PaletteColor &color);
+31 -19
View File
@@ -156,13 +156,8 @@ void EditorSession::setPaletteColor(const PaletteColor &color, bool background)
} else {
setForegroundColor(color);
// Open text follows the swatch; drafts live under Type.
if (m_textDraft) {
changeTextStyle([&color](LayerTextStyle &style) {
style.red = color.red;
style.green = color.green;
style.blue = color.blue;
});
}
if (m_textDraft)
setDraftTextColor(color);
}
}
@@ -198,7 +193,7 @@ void EditorSession::openColorPicker(bool background)
ColorPickerState picker({ColorPickerTarget::Kind::palette, background}, paletteColor(background));
// Open text follows the foreground: it previews, Cancel restores.
if (!background && m_tool == NavigationTool::type && m_textDraft)
picker.editedText = ColorPickerState::EditedText{m_textDraft->id, {m_textDraft->style.red, m_textDraft->style.green, m_textDraft->style.blue}};
picker.editedText = ColorPickerState::EditedText{m_textDraft->id, m_textDraft->style};
m_colorPicker = picker;
notify();
}
@@ -207,7 +202,9 @@ PaletteColor EditorSession::typeColor() const
{
if (!m_textDraft)
return foregroundColor();
return {m_textDraft->style.red, m_textDraft->style.green, m_textDraft->style.blue};
// The first selected letter, else the one before the caret.
const TextSpan selection = m_textDraft->selection;
return m_textDraft->style.color(selection.length > 0 ? selection.location : std::max<qint64>(0, selection.location - 1));
}
void EditorSession::openTextColorPicker()
@@ -215,7 +212,10 @@ void EditorSession::openTextColorPicker()
if (!canEditPalette() || m_colorPicker || m_tool != NavigationTool::type)
return;
const std::optional<QUuid> draftID = m_textDraft ? std::optional(m_textDraft->id) : std::nullopt;
m_colorPicker = ColorPickerState({ColorPickerTarget::Kind::text, false, draftID}, typeColor());
ColorPickerState picker({ColorPickerTarget::Kind::text, false, draftID}, typeColor());
if (m_textDraft)
picker.editedText = ColorPickerState::EditedText{m_textDraft->id, m_textDraft->style};
m_colorPicker = picker;
notify();
}
@@ -231,12 +231,15 @@ void EditorSession::closeColorPicker(bool commit)
setPaletteColor(color, picker.target.background);
} else if (!commit && picker.target.kind == ColorPickerTarget::Kind::palette && picker.editedText && m_tool == NavigationTool::type
&& draftID == picker.editedText->draftID) {
paintText(picker.editedText->color);
restoreDraftTextColors(picker.editedText->style);
} else if (picker.target.kind == ColorPickerTarget::Kind::text && m_tool == NavigationTool::type && draftID == picker.target.draftID) {
// The text previewed the working colour; Cancel puts it back.
const PaletteColor chosen = commit ? color : picker.original;
if (draftID) {
paintText(chosen);
if (commit)
setDraftTextColor(chosen);
else if (picker.editedText)
restoreDraftTextColors(picker.editedText->style);
} else if (commit) {
m_textDefaults.red = chosen.red;
m_textDefaults.green = chosen.green;
@@ -295,16 +298,25 @@ void EditorSession::previewTextColor()
? std::optional(m_colorPicker->editedText->draftID)
: std::nullopt;
if (draftID && m_tool == NavigationTool::type && m_textDraft && m_textDraft->id == *draftID)
paintText(m_colorPicker->color());
setDraftTextColor(m_colorPicker->color());
}
// The open draft's colour.
void EditorSession::paintText(const PaletteColor &color)
void EditorSession::setDraftTextColor(const PaletteColor &color)
{
changeTextStyle([&color](LayerTextStyle &style) {
style.red = color.red;
style.green = color.green;
style.blue = color.blue;
if (!m_textDraft)
return;
const TextSpan selection = m_textDraft->selection;
changeTextStyle([&](LayerTextStyle &style) { style.setColor(color, selection); });
}
void EditorSession::restoreDraftTextColors(const LayerTextStyle &original)
{
changeTextStyle([&original](LayerTextStyle &style) {
style.red = original.red;
style.green = original.green;
style.blue = original.blue;
// Retyped since, the runs cannot come back letter for letter.
style.colorRuns = original.content == style.content ? original.colorRuns : std::nullopt;
});
}
+3 -2
View File
@@ -1,4 +1,5 @@
#pragma once
#include "Document/TypeTool.h"
#include <QColor>
#include <QString>
#include <QUuid>
@@ -57,10 +58,10 @@ struct ColorPickerState {
PickerHSB hsb;
// Swift's object identity: a new picker opens its panel anew.
QUuid id = QUuid::createUuid();
// The foreground picker over open text: that text, its colour.
// The picker over open text: that text, its colours then.
struct EditedText {
QUuid draftID;
PaletteColor color;
LayerTextStyle style;
};
std::optional<EditedText> editedText;
PaletteColor color() const { return hsb.rgb().quantized(); }
+96 -7
View File
@@ -6,7 +6,9 @@
#include "Rendering/TextLayout.h"
#include <QPainter>
#include <QTextBoundaryFinder>
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
QString rawValue(TextAlignment alignment)
@@ -44,7 +46,94 @@ 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);
&& 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);
}
std::optional<LayerText> LayerText::loaded(const std::optional<LayerTextStyle> &style, const std::optional<ImportedImage> &image)
@@ -102,7 +191,7 @@ void EditorSession::beginText(QPointF point, bool newLayer)
const QPointF origin = target ? target->origin() : point - QPointF(padding, padding + TextLines(style, QSizeF(100'000, 100'000)).baseline(0));
setTextDraft(TextDraft{.documentID = m_document->id, .layerID = target ? std::optional(target->id) : std::nullopt,
.origin = origin,
.transform = target ? std::optional(target->transform) : std::nullopt, .style = style});
.transform = target ? std::optional(target->transform) : std::nullopt, .style = style, .selection = TextSpan()});
}
void EditorSession::editActiveText()
@@ -112,7 +201,7 @@ void EditorSession::editActiveText()
return;
m_tool = NavigationTool::type;
setTextDraft(TextDraft{.documentID = m_document->id, .layerID = layer->id, .origin = layer->origin(), .transform = layer->transform,
.style = layer->liveText()->style});
.style = layer->liveText()->style, .selection = TextSpan()});
}
bool EditorSession::applyText(TextDraft draft)
@@ -166,6 +255,8 @@ 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.
m_textDefaults.colorRuns = std::nullopt;
requestCanvasFocus();
return succeed();
} catch (const ProjectError &error) {
@@ -221,11 +312,9 @@ bool EditorSession::recolorText(QUuid id, const PaletteColor &color)
return false;
const ImageLayer layer = m_document->layers[size_t(index)];
LayerTextStyle style = layer.liveText()->style;
if (style.red == color.red && style.green == color.green && style.blue == color.blue)
if (style.red == color.red && style.green == color.green && style.blue == color.blue && !style.colorRuns)
return true;
style.red = color.red;
style.green = color.green;
style.blue = color.blue;
style.setColor(color, TextSpan());
QImage image, thumbnail;
try {
image = textImage(style);
+35
View File
@@ -6,12 +6,32 @@
#include <QUuid>
#include <array>
#include <optional>
#include <vector>
enum class TextAlignment { left, center, right };
inline constexpr std::array allTextAlignments{TextAlignment::left, TextAlignment::center, TextAlignment::right};
QString rawValue(TextAlignment alignment);
std::optional<TextAlignment> textAlignment(const QString &text);
struct PaletteColor;
// Swift's NSRange: UTF-16 units into a text's content.
struct TextSpan {
qint64 location = 0;
qint64 length = 0;
friend bool operator==(const TextSpan &, const TextSpan &) = default;
};
// Letters in a colour of their own, as Swift's LayerTextColorRun.
struct LayerTextColorRun {
qint64 location = 0;
qint64 length = 0;
double red = 0;
double green = 0;
double blue = 0;
friend bool operator==(const LayerTextColorRun &, const LayerTextColorRun &) = default;
};
// What a text layer says and how, in layer pixels.
struct LayerTextStyle {
QString content = QStringLiteral("Text");
@@ -27,13 +47,26 @@ struct LayerTextStyle {
double leading = 0;
// A paragraph's fixed box; none makes point text.
std::optional<QSizeF> boxSize = std::nullopt;
// Sorted, apart, within the content; none is one colour.
std::optional<std::vector<LayerTextColorRun>> colorRuns = std::nullopt;
// The gap between the text and its box, either kind.
static constexpr double padding = 12;
double autoLeading() const { return fontSize * 1.2; }
double lineHeight() const { return leading > 0 ? leading : autoLeading(); }
bool boxIsValid() const;
bool isValid() const;
// The colour of the UTF-16 unit at `index`.
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.
void replaceCharacters(TextSpan span, qint64 length);
friend bool operator==(const LayerTextStyle &, const LayerTextStyle &) = default;
private:
bool colorRunsAreValid() const;
std::vector<PaletteColor> unitColors() const;
void setUnitColors(const std::vector<PaletteColor> &colors);
};
// A text layer: its style and the image it drew.
@@ -52,5 +85,7 @@ struct TextDraft {
QPointF origin;
std::optional<LayerTransform> transform = std::nullopt;
LayerTextStyle style;
// What the editor selects; colour applies to it alone.
TextSpan selection;
friend bool operator==(const TextDraft &, const TextDraft &) = default;
};