Typed text drawn as its pixels; effects kept while typing, painting masks and undoing (12.12a)

This commit is contained in:
雷电芽衣 committed 2026-09-29 12:47:36 -04:00
1 parent 8fbb104c48
commit 136ee65a48
23 files changed
+1125 -153

No files matched your search

+13
View File
@@ -229,3 +229,16 @@ bool CanvasView::brushBracket(const QString &text)
m_session.changeBrushHardness(text == u"}");
return true;
}
// The stroke's dirty rect in view points, two points out.
std::optional<QRectF> CanvasView::strokeDirtyRect() const
{
const BrushStroke *stroke = m_session.brushStroke();
const std::optional<CanvasDocument> &document = m_session.document();
if (!stroke || !document || !stroke->dirtyDocumentRect())
return std::nullopt;
const QRectF &dirty = *stroke->dirtyDocumentRect();
const QPointF origin = m_session.viewport.viewPoint(dirty.topLeft(), document->size());
const double scale = m_session.viewport.pointsPerPixel();
return QRectF(origin, QSizeF(dirty.width() * scale, dirty.height() * scale)).adjusted(-2, -2, 2, 2);
}
+86
View File
@@ -0,0 +1,86 @@
#include "Document/LayerEffects+Renderer.h"
#include "Logging.h"
#include "Rendering/EditorCanvas.h"
#include "Rendering/InlineTextEditor.h"
// Typed text, drawn as the pixels it commits to.
std::optional<CanvasView::DraftText> CanvasView::draftText()
{
const std::optional<TextDraft> &draft = m_session.textDraft();
if (!draft || !m_inlineTextEditor) {
m_draftTextCache.reset();
return std::nullopt;
}
// Remade only when the style changes.
if (!m_draftTextCache || m_draftTextCache->style != draft->style) {
try {
m_draftTextCache = DraftTextCache{draft->style, EditorSession::textImage(draft->style)};
qCDebug(lcRendering) << "typed text remade";
} catch (const std::runtime_error &error) {
qCWarning(lcRendering) << "typed text could not be drawn:" << error.what();
m_draftTextCache.reset();
return std::nullopt;
}
}
return DraftText{m_draftTextCache->image, m_inlineTextEditor->shownTransform()};
}
// In its layer's place, effects redone as it is typed.
void CanvasView::drawTypedText(const ImageLayer &layer, const LayerRenderer::Options &options, const Center &center, QPainter &target)
{
const std::optional<DraftText> text = draftText();
if (!text)
return;
const LayerEffects effects = layer.effects.value_or(LayerEffects()).visible();
if (!effects.isEmpty() && effects.isValid()) {
// Redone only when the pixels, their place or effects change.
if (!m_draftEffects || m_draftEffects->image.cacheKey() != text->image.cacheKey() || m_draftEffects->effects != effects
|| m_draftEffects->transform != text->transform) {
std::optional<QImage> mask;
if (layer.mask && layer.mask->placement)
mask = layer.mask->clipImage(*layer.mask->placement, text->transform, text->image.width(), text->image.height(), 2048);
else if (layer.mask)
mask = layer.mask->enabledImage();
const std::optional<EffectsPreviewCache::Result> built = m_session.effectsPreviews.renderNow(text->image, mask, effects);
m_draftEffects = built ? std::optional(DraftEffects{text->image, effects, text->transform, built->image, built->inset}) : std::nullopt;
m_draftEffectsSource = DraftEffectsSource{layer.id, m_session.textDraft().value().style};
}
// The last effects stand in until a change is redone.
if (m_draftEffects) {
const LayerTransform grown = LayerEffectsRenderer::placed(text->transform, m_draftEffects->rendered, m_draftEffects->inset);
LayerRenderer::draw(m_draftEffects->rendered, grown, center(grown.center()), target,
{.scale = options.scale, .opacity = options.opacity, .blendMode = options.blendMode, .clip = options.clip});
return;
}
}
LayerRenderer::draw(text->image, text->transform, center(text->transform.center()), target,
{.scale = options.scale, .opacity = options.opacity, .blendMode = options.blendMode, .clip = options.clip});
}
// New text: above the active layer, else on top.
void CanvasView::drawNewText(bool &drawn, double scale, const Center &center, QPainter &target, const QImage &clip)
{
// Asked without a draft too: that lets the cache go.
const std::optional<TextDraft> &draft = m_session.textDraft();
if (drawn || (draft && draft->layerID))
return;
const std::optional<DraftText> text = draftText();
if (!text)
return;
drawn = true;
LayerRenderer::draw(text->image, text->transform, center(text->transform.center()), target, {.scale = scale, .clip = clip});
}
// Once committed, the typed text's effects stand in: nothing blinks.
void CanvasView::handOnDraftEffects(const CanvasDocument &document)
{
if (m_session.textDraft() || !m_draftEffects || !m_draftEffectsSource)
return;
const int index = indexOf(document.layers, m_draftEffectsSource->layerID);
if (index >= 0 && document.layers[size_t(index)].liveText() && document.layers[size_t(index)].liveText()->style == m_draftEffectsSource->style)
m_session.effectsPreviews.seed(m_draftEffectsSource->layerID, m_draftEffects->rendered,
LayerEffectsRenderer::placed(m_draftEffects->transform, m_draftEffects->rendered, m_draftEffects->inset));
m_draftEffects.reset();
m_draftEffectsSource.reset();
}
+60 -2
View File
@@ -29,18 +29,32 @@ void CanvasView::drawStroke(const BrushStroke &stroke, const ImageLayer &layer,
target, options);
return;
}
// With effects on, a surface redoes them per mask change.
const auto drawSurface = [&](LayerEffectsSurface *surface) {
if (!surface || !surface->image())
return false;
const LayerTransform grown = LayerEffectsRenderer::placed(transform, *surface->image(), surface->margin);
surface->placement = grown;
LayerRenderer::draw(*surface->image(), grown, center(grown.center()), target, bare);
return true;
};
if (stroke.layer.mask && stroke.layer.mask->placement) {
// A placed mask: the layer draws through its own grid.
if (!layer.asset)
return;
const std::optional<QImage> preview = stroke.placedMaskPreview(*stroke.layer.mask->placement);
if (preview && drawSurface(placedMaskSurface(layer, stroke, *stroke.layer.mask->placement, *preview)))
return;
LayerRenderer::Options through = options;
through.mask = stroke.placedMaskPreview(*stroke.layer.mask->placement).value_or(QImage());
through.mask = preview.value_or(QImage());
if (layer.asset->raster)
TiledLayerRenderer::drawRaster(layer.asset->raster, transform, center(transform.center()), target, through);
else
LayerRenderer::draw(layer.asset->image(), transform, center(transform.center()), target, through);
return;
}
if (drawSurface(strokeSurface(layer, stroke, std::nullopt)))
return;
TiledLayerRenderer::drawMaskStroke(stroke.width, stroke.height, stroke.sourceRect, stroke.patches(),
stroke.layer.mask ? std::optional(stroke.layer.mask->asset) : std::nullopt, image, raster, transform,
center(transform.center()), target, bare);
@@ -57,7 +71,51 @@ LayerEffectsSurface *CanvasView::strokeSurface(const ImageLayer &layer, const Br
m_strokeSurface = LayerEffectsSurface::make(layer.id, effects, grid, stroke.sourceRect);
if (!m_strokeSurface)
return nullptr;
m_strokeSurface->update(stroke.layer.asset, stroke.patches(), mask);
if (!stroke.isMask) {
m_strokeSurface->update(stroke.layer.asset, stroke.patches(), mask);
return m_strokeSurface.get();
}
// The old mask, then the stroke's tiles over it.
const std::optional<QImage> old = stroke.layer.mask ? std::optional(stroke.layer.mask->asset.image()) : std::nullopt;
const std::vector<BrushPatch> patches = stroke.patches();
const QRectF sourceRect = stroke.sourceRect;
m_strokeSurface->update(stroke.layer.asset, {}, std::nullopt, LayerEffectsSurface::MaskStroke{patches, QTransform(), [old, patches, sourceRect](const QRectF &region) {
QImage coverage = BrushRaster::context(int(region.width()), int(region.height()), true);
QPainter painter(&coverage);
painter.translate(-region.left(), -region.top());
if (old)
BrushRaster::draw(*old, sourceRect, painter);
for (const BrushPatch &patch : patches) {
if (patch.rect.intersects(region))
BrushRaster::draw(patch.image, patch.rect, painter);
}
return coverage;
}});
return m_strokeSurface.get();
}
// A placed mask paints its grid; the surface, the layer's.
LayerEffectsSurface *CanvasView::placedMaskSurface(const ImageLayer &layer, const BrushStroke &stroke, const LayerTransform &placement, const QImage &preview)
{
const LayerEffects effects = layer.effects.value_or(LayerEffects()).visible();
if (effects.isEmpty() || !effects.isValid() || !stroke.layer.asset)
return nullptr;
const QSize base = stroke.layer.asset->size();
const QRectF full(QPointF(0, 0), QSizeF(base));
if (!m_strokeSurface || !m_strokeSurface->matches(layer.id, effects, full.size(), full))
m_strokeSurface = LayerEffectsSurface::make(layer.id, effects, full.size(), full);
if (!m_strokeSurface)
return nullptr;
const QTransform toGrid = BrushRaster::pixelToDocument(placement, stroke.width, stroke.height)
* BrushRaster::pixelToDocument(stroke.layer.transform, base.width(), base.height()).inverted();
m_strokeSurface->update(stroke.layer.asset, {}, std::nullopt, LayerEffectsSurface::MaskStroke{stroke.patches(), toGrid, [preview, full](const QRectF &region) {
QImage coverage = BrushRaster::context(int(region.width()), int(region.height()), true);
QPainter painter(&coverage);
painter.translate(-region.left(), -region.top());
painter.setRenderHint(QPainter::SmoothPixmapTransform);
painter.drawImage(full, preview);
return coverage;
}});
return m_strokeSurface.get();
}
+17 -18
View File
@@ -83,7 +83,9 @@ CanvasView::DisplayState CanvasView::displayState() const
.renderBounds = renderBounds(),
.viewport = m_session.viewport,
.layers = {},
.folderMasks = {}};
.folderMasks = {},
.textStyle = m_session.textDraft() ? std::optional(m_session.textDraft()->style) : std::nullopt,
.textTransform = m_session.textDraft() && m_inlineTextEditor ? std::optional(m_inlineTextEditor->shownTransform()) : std::nullopt};
if (!document)
return state;
// A hidden source still clips: live masks count every layer.
@@ -111,19 +113,6 @@ CanvasView::DisplayState CanvasView::displayState() const
return state;
}
// The stroke's dirty rect in view points, two points out.
std::optional<QRectF> CanvasView::strokeDirtyRect() const
{
const BrushStroke *stroke = m_session.brushStroke();
const std::optional<CanvasDocument> &document = m_session.document();
if (!stroke || !document || !stroke->dirtyDocumentRect())
return std::nullopt;
const QRectF &dirty = *stroke->dirtyDocumentRect();
const QPointF origin = m_session.viewport.viewPoint(dirty.topLeft(), document->size());
const double scale = m_session.viewport.pointsPerPixel();
return QRectF(origin, QSizeF(dirty.width() * scale, dirty.height() * scale)).adjusted(-2, -2, 2, 2);
}
bool CanvasView::synchronizeDisplay()
{
handOnStrokeSurface();
@@ -310,6 +299,7 @@ FolderMaskClip::Applier CanvasView::liveFolderMaskClip(const BrushStroke &edit,
void CanvasView::drawLayers(const CanvasDocument &document, double scale, const Center &center, QPainter &context)
{
handOnDraftEffects(document);
m_session.effectsPreviews.prepare(document.layers);
std::map<QUuid, ImageLayer> byID;
for (const ImageLayer &layer : document.layers) {
@@ -322,12 +312,16 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
canvas->update();
};
const auto drawOwn = [&](QUuid id, QPainter &target, const QImage &clip) {
// Open text shows in its editor instead.
if (!byID.contains(id) || (m_session.textDraft() && m_session.textDraft().value().layerID == id))
if (!byID.contains(id))
return;
const ImageLayer &layer = byID.at(id);
// Its folders dim it with everything inside them.
const double opacity = layer.effectiveOpacity(byID);
// Text being edited draws as it will be committed.
if (m_session.textDraft() && m_session.textDraft()->layerID == id) {
drawTypedText(layer, {.scale = scale, .opacity = opacity, .blendMode = m_session.displayedBlendMode(layer), .clip = clip}, center, target);
return;
}
// A stroke or pixel move stands in for the pixels.
const PixelMove *move = m_session.pixelMove();
const std::optional<GradientEdit> &gradient = m_session.gradientEdit();
@@ -412,10 +406,13 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
LayerRenderer::draw(layer.asset->image(), transform, center(transform.center()), target, options);
};
// Above the active layer, where the new layer will go.
bool drewNewText = false;
const auto drawOwnWithDraft = [&](QUuid id, QPainter &target, const QImage &clip) {
drawOwn(id, target, clip);
if (id == m_session.activeLayerID())
drawShapeDraft(scale, center, target, clip);
if (id != m_session.activeLayerID())
return;
drawShapeDraft(scale, center, target, clip);
drawNewText(drewNewText, scale, center, target, clip);
};
const auto parent = [&](QUuid id) { return byID.contains(id) ? byID.at(id).parentID : std::nullopt; };
// A mask being painted exists as the edit's tiles alone.
@@ -460,6 +457,8 @@ void CanvasView::drawLayers(const CanvasDocument &document, double scale, const
const QPointF origin = center(transform.center());
return [clip, origin, scale](const QPainter &painter, QImage &coverage) { clip.apply(origin, painter, coverage, scale); };
}, context, [&](QUuid id, const QImage &clip) { live.drawComposite(id, context, clip); });
// Else on top: the active layer, a folder or hidden.
drawNewText(drewNewText, scale, center, context, QImage());
}
void CanvasView::drawDocumentPixels(const QRectF &view, const QRectF &pixels, const CanvasDocument &document, QPainter &context)
+33
View File
@@ -119,6 +119,9 @@ private:
CanvasViewport viewport;
std::vector<Layer> layers;
std::vector<FolderMask> folderMasks;
// Typed text, which the canvas draws as pixels.
std::optional<LayerTextStyle> textStyle;
std::optional<LayerTransform> textTransform;
friend bool operator==(const DisplayState &, const DisplayState &) = default;
};
// A Zoom press: drags zoom about it, clicks step.
@@ -143,7 +146,17 @@ private:
void drawStroke(const BrushStroke &stroke, const ImageLayer &layer, const LayerTransform &transform, const std::optional<QImage> &mask,
const LayerRenderer::Options &options, double scale, const Center &center, QPainter &target);
void drawLayers(const CanvasDocument &document, double scale, const Center &center, QPainter &context);
// Typed text as pixels, where the editor shows it.
struct DraftText {
QImage image;
LayerTransform transform;
};
std::optional<DraftText> draftText();
void drawTypedText(const ImageLayer &layer, const LayerRenderer::Options &options, const Center &center, QPainter &target);
void drawNewText(bool &drawn, double scale, const Center &center, QPainter &target, const QImage &clip);
void handOnDraftEffects(const CanvasDocument &document);
// Swift's effects surface on the canvas (EditorCanvas+Effects.cpp).
LayerEffectsSurface *placedMaskSurface(const ImageLayer &layer, const BrushStroke &stroke, const LayerTransform &placement, const QImage &preview);
LayerEffectsSurface *strokeSurface(const ImageLayer &layer, const BrushStroke &stroke, const std::optional<QImage> &mask);
// The stroke is over: its surface stands in meanwhile.
void handOnStrokeSurface();
@@ -359,6 +372,26 @@ private:
SampleRingOverlay m_sampleRing;
// The painted layer's effects, made as its stroke starts.
std::unique_ptr<LayerEffectsSurface> m_strokeSurface;
// Typed text rendered, remade when its style changes.
struct DraftTextCache {
LayerTextStyle style;
QImage image;
};
std::optional<DraftTextCache> m_draftTextCache;
// Its effects, what they came from, and for which text.
struct DraftEffects {
QImage image;
LayerEffects effects;
LayerTransform transform;
QImage rendered;
double inset;
};
std::optional<DraftEffects> m_draftEffects;
struct DraftEffectsSource {
QUuid layerID;
LayerTextStyle style;
};
std::optional<DraftEffectsSource> m_draftEffectsSource;
// The colour a sampling press began from, for the ring.
PaletteColor m_samplingOriginal = PaletteColor::black();
bool m_samplingColor = false;
+74 -40
View File
@@ -57,6 +57,7 @@ void EffectsPreviewCache::prepare(const std::vector<ImageLayer> &layers)
entry = m_entries.erase(entry);
}
std::erase_if(m_seeds, [&](const auto &seed) { return !ids.contains(seed.first); });
std::erase_if(m_recent, [&](const auto &recent) { return !ids.contains(recent.first); });
// About 64 MiB of output shared by every effect layer.
m_sideLimit = std::min(1536, std::max(32, int(std::sqrt(16'777'216.0 / double(std::max<size_t>(1, ids.size()))))));
}
@@ -80,10 +81,22 @@ std::optional<EffectsPreviewCache::Result> EffectsPreviewCache::preview(const Im
if (m_entries.contains(layer.id)) {
const Entry &old = m_entries.at(layer.id);
old.request.cancelled->store(true);
// Kept on the same pixels, an effect hidden or not.
if (old.request.image.identity() == request.image.identity() && old.request.maskSource == maskSource)
// Kept on the same pixels, an effect or mask changed.
if (old.request.image.identity() == request.image.identity())
previous = old.result;
}
// Pixels an undo put back: their effects are known.
if (m_recent.contains(layer.id)) {
const std::vector<Entry> &recent = m_recent.at(layer.id);
for (size_t index = recent.size(); index-- > 0;) {
if (!recent[index].request.matches(request))
continue;
const Entry found = recent[index];
m_entries.insert_or_assign(layer.id, found);
m_seeds.erase(layer.id);
return found.result;
}
}
if (!previous && m_seeds.contains(layer.id))
previous = m_seeds.at(layer.id);
m_entries.insert_or_assign(layer.id, Entry{request, previous});
@@ -107,8 +120,14 @@ void EffectsPreviewCache::land(QUuid layerID, QUuid requestID, const std::option
if (!m_entries.contains(layerID) || m_entries.at(layerID).request.id != requestID)
return;
m_entries.at(layerID).result = result;
if (result)
if (result) {
m_seeds.erase(layerID);
// A match returns before rendering: none repeats here.
std::vector<Entry> &recent = m_recent[layerID];
recent.push_back(m_entries.at(layerID));
if (recent.size() > 3)
recent.erase(recent.begin());
}
completion();
}
@@ -124,45 +143,60 @@ std::optional<EffectsPreviewCache::Result> EffectsPreviewCache::render(const Req
{
try {
// A painted layer flattens here, off the UI thread.
const QImage image = request.image.image();
const double margin = LayerEffectsRenderer::margin(request.effects);
// The margins count too; even a 500px stroke stays bounded.
const double factor = std::min(1.0, double(request.sideLimit - 8) / (double(std::max(image.width(), image.height())) + 2 * margin));
const int width = std::max(1, int(std::round(image.width() * factor)));
const int height = std::max(1, int(std::round(image.height() * factor)));
// Swift's high quality: each axis filtered by its own measure.
const auto resized = [&](const QImage &source, bool mask) {
// A mask travels as alpha, so its values pass exactly.
const QImage scaled = (mask ? BrushRaster::alphaView(source) : source)
.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)
.convertToFormat(mask ? QImage::Format_Alpha8 : QImage::Format_RGBA8888_Premultiplied);
const QImage result = mask ? QImage(scaled.constBits(), width, height, scaled.bytesPerLine(), QImage::Format_Grayscale8).copy() : scaled;
if (result.isNull())
throw ExportError(ExportError::Kind::render);
return result;
};
const QImage pixels = factor == 1 ? image : resized(image, false);
const std::optional<QImage> mask = request.mask ? std::optional(factor == 1 ? *request.mask : resized(*request.mask, true)) : std::nullopt;
LayerEffects effects = request.effects;
if (effects.stroke)
effects.stroke->size *= factor;
if (effects.shadow) {
effects.shadow->distance *= factor;
effects.shadow->blur *= factor;
}
// Swift forgets the inner shadow and glows: too wide.
if (effects.innerShadow) {
effects.innerShadow->distance *= factor;
effects.innerShadow->blur *= factor;
}
if (effects.outerGlow)
effects.outerGlow->size *= factor;
if (effects.innerGlow)
effects.innerGlow->size *= factor;
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(pixels, mask, effects);
return Result{rendered.image, rendered.inset, std::nullopt};
return scaled(request.image.image(), request.mask, request.effects, request.sideLimit);
} catch (const ExportError &error) {
qCWarning(lcRendering) << "an effects preview could not be made:" << error.what();
}
return std::nullopt;
}
std::optional<EffectsPreviewCache::Result> EffectsPreviewCache::renderNow(const QImage &image, const std::optional<QImage> &mask,
const LayerEffects &effects) const
{
try {
return scaled(image, mask, effects, m_sideLimit);
} catch (const ExportError &error) {
qCWarning(lcRendering) << "typed text's effects could not be made:" << error.what();
}
return std::nullopt;
}
// Scaled to the side limit, rendered, its settings scaled alike.
EffectsPreviewCache::Result EffectsPreviewCache::scaled(const QImage &image, const std::optional<QImage> &requestMask, LayerEffects effects, int sideLimit)
{
const double margin = LayerEffectsRenderer::margin(effects);
// The margins count too; even a 500px stroke stays bounded.
const double factor = std::min(1.0, double(sideLimit - 8) / (double(std::max(image.width(), image.height())) + 2 * margin));
const int width = std::max(1, int(std::round(image.width() * factor)));
const int height = std::max(1, int(std::round(image.height() * factor)));
// Swift's high quality: each axis filtered by its own measure.
const auto resized = [&](const QImage &source, bool mask) {
// A mask travels as alpha, so its values pass exactly.
const QImage scaled = (mask ? BrushRaster::alphaView(source) : source)
.scaled(width, height, Qt::IgnoreAspectRatio, Qt::SmoothTransformation)
.convertToFormat(mask ? QImage::Format_Alpha8 : QImage::Format_RGBA8888_Premultiplied);
const QImage result = mask ? QImage(scaled.constBits(), width, height, scaled.bytesPerLine(), QImage::Format_Grayscale8).copy() : scaled;
if (result.isNull())
throw ExportError(ExportError::Kind::render);
return result;
};
const QImage pixels = factor == 1 ? image : resized(image, false);
const std::optional<QImage> mask = requestMask ? std::optional(factor == 1 ? *requestMask : resized(*requestMask, true)) : std::nullopt;
if (effects.stroke)
effects.stroke->size *= factor;
if (effects.shadow) {
effects.shadow->distance *= factor;
effects.shadow->blur *= factor;
}
// Swift forgets the inner shadow and glows: too wide.
if (effects.innerShadow) {
effects.innerShadow->distance *= factor;
effects.innerShadow->blur *= factor;
}
if (effects.outerGlow)
effects.outerGlow->size *= factor;
if (effects.innerGlow)
effects.innerGlow->size *= factor;
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(pixels, mask, effects);
return Result{rendered.image, rendered.inset, std::nullopt};
}
+5
View File
@@ -28,6 +28,8 @@ public:
// The last result; `completion` runs once a new one lands.
std::optional<Result> preview(const ImageLayer &layer, const std::optional<QImage> &mask, const LayerTransform &transform,
const std::optional<LayerTransform> &maskPlacement, const std::function<void()> &completion);
// Rendered at once, at the preview size: typed text's.
std::optional<Result> renderNow(const QImage &image, const std::optional<QImage> &mask, const LayerEffects &effects) const;
private:
struct Request {
@@ -48,12 +50,15 @@ private:
std::optional<Result> result;
};
static std::optional<Result> render(const Request &request);
static Result scaled(const QImage &image, const std::optional<QImage> &mask, LayerEffects effects, int sideLimit);
void cancel(QUuid id);
void land(QUuid layerID, QUuid requestID, const std::optional<Result> &result, const std::function<void()> &completion);
std::map<QUuid, Entry> m_entries;
// Warped effects handed in, shown until the worker catches up.
std::map<QUuid, Result> m_seeds;
// Three finished previews a layer, newest last, for undo.
std::map<QUuid, std::vector<Entry>> m_recent;
int m_sideLimit = 1536;
// Swift's serial queue: one render at a time.
QThreadPool m_worker;
+9 -2
View File
@@ -143,18 +143,25 @@ void InlineTextEditor::draw(QPainter &painter) const
const double padding = LayerTextStyle::padding;
painter.setTransform(textTransform());
const QColor ink = QColor::fromRgbF(float(style().red), float(style().green), float(style().blue));
painter.setPen(ink);
const QColor highlight = palette.color(m_focused ? QPalette::Active : QPalette::Inactive, QPalette::Highlight);
// Clear glyphs: the canvas draws the text's own pixels beneath.
painter.setPen(Qt::transparent);
// The selection shows through to those pixels.
QColor highlight = palette.color(m_focused ? QPalette::Active : QPalette::Inactive, QPalette::Highlight);
highlight.setAlphaF(0.45f);
// The input method's formats, else NSTextView's underline.
QList<QTextLayout::FormatRange> formats;
if (m_composing) {
for (QTextLayout::FormatRange format : m_composing->formats) {
format.start += m_composing->marked.start;
// Clear glyphs: an underline keeps the text's colour.
if (!format.format.underlineColor().isValid())
format.format.setUnderlineColor(ink);
formats << format;
}
if (formats.isEmpty()) {
QTextCharFormat underline;
underline.setFontUnderline(true);
underline.setUnderlineColor(ink);
formats << QTextLayout::FormatRange{m_composing->marked.start, m_composing->marked.end - m_composing->marked.start, underline};
}
}
+20 -4
View File
@@ -59,15 +59,20 @@ double LayerEffectsSurface::reach() const
return reach;
}
void LayerEffectsSurface::update(const std::optional<ImportedImage> &base, const std::vector<BrushPatch> &patches, const std::optional<QImage> &mask)
void LayerEffectsSurface::update(const std::optional<ImportedImage> &base, const std::vector<BrushPatch> &patches, const std::optional<QImage> &mask,
std::optional<MaskStroke> maskStroke)
{
m_maskStroke = std::move(maskStroke);
std::optional<QRectF> dirty;
std::map<std::pair<qint64, qint64>, qint64> seen;
for (const BrushPatch &patch : patches) {
for (const BrushPatch &patch : m_maskStroke ? m_maskStroke->patches : patches) {
const std::pair<qint64, qint64> key(qint64(patch.rect.left()), qint64(patch.rect.top()));
seen.insert_or_assign(key, patch.image.cacheKey());
if (!m_taken.contains(key) || m_taken.at(key) != patch.image.cacheKey())
dirty = dirty ? dirty->united(patch.rect) : patch.rect;
if (m_taken.contains(key) && m_taken.at(key) == patch.image.cacheKey())
continue;
// A placed mask paints its grid; mapped into the layer's.
const QRectF rect = m_maskStroke ? m_maskStroke->toGrid.mapRect(patch.rect).adjusted(-1, -1, 1, 1) : patch.rect;
dirty = dirty ? dirty->united(rect) : rect;
}
const bool first = !m_image;
const std::optional<QRectF> region = first ? std::optional(QRectF(QPointF(0, 0), grid).adjusted(-margin, -margin, margin, margin)) : dirty;
@@ -112,6 +117,17 @@ QImage LayerEffectsSurface::window(const QRectF &region, const std::optional<Imp
QImage window = BrushRaster::context(int(region.width()), int(region.height()), false);
QPainter painter(&window);
painter.translate(-region.left(), -region.top());
if (m_maskStroke) {
// The pixels through the mask as the stroke leaves it.
const QImage live = m_maskStroke->coverage(region);
if (base && base->raster)
base->raster->draw(sourceRect, painter);
else if (base)
BrushRaster::draw(base->image(), sourceRect, painter);
painter.setCompositionMode(QPainter::CompositionMode_DestinationIn);
painter.drawImage(region, BrushRaster::alphaView(live));
return window;
}
if (base && base->raster)
base->raster->draw(sourceRect, painter);
else if (base)
+12 -1
View File
@@ -2,7 +2,9 @@
#include "Document/BrushStroke.h"
#include "Document/LayerEffects.h"
#include <QImage>
#include <QTransform>
#include <QUuid>
#include <functional>
#include <map>
#include <memory>
#include <optional>
@@ -14,8 +16,16 @@ public:
static std::unique_ptr<LayerEffectsSurface> make(QUuid layerID, const LayerEffects &effects, QSizeF grid, QRectF sourceRect);
// Whether it still fits the stroke and settings.
bool matches(QUuid layerID, const LayerEffects &effects, QSizeF grid, QRectF sourceRect) const;
// A mask painted: tiles, their map into the grid, coverage.
struct MaskStroke {
std::vector<BrushPatch> patches;
QTransform toGrid;
// The mask over a grid region, gray, white showing; throws.
std::function<QImage(const QRectF &)> coverage;
};
// Everything at first, then only where the paint changed.
void update(const std::optional<ImportedImage> &base, const std::vector<BrushPatch> &patches, const std::optional<QImage> &mask);
void update(const std::optional<ImportedImage> &base, const std::vector<BrushPatch> &patches, const std::optional<QImage> &mask,
std::optional<MaskStroke> maskStroke = std::nullopt);
const std::optional<QImage> &image() const { return m_image; }
const QUuid layerID;
@@ -40,4 +50,5 @@ private:
// Each tile's origin and the pixels last taken from it.
std::map<std::pair<qint64, qint64>, qint64> m_taken;
std::optional<QImage> m_image;
std::optional<MaskStroke> m_maskStroke;
};