init
Some checks failed
Docker. / Ubuntu (push) Has been cancelled
User-agent updater. / User-agent (push) Failing after 15s
Lock Threads / lock (push) Failing after 10s
Waiting for answer. / waiting-for-answer (push) Failing after 22s
Close stale issues and PRs / stale (push) Successful in 13s
Needs user action. / needs-user-action (push) Failing after 8s
Can't reproduce. / cant-reproduce (push) Failing after 8s

This commit is contained in:
allhaileris
2026-02-16 15:50:16 +03:00
commit afb81b8278
13816 changed files with 3689732 additions and 0 deletions

View File

@@ -0,0 +1,328 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/color_picker.h"
#include "ui/painter.h"
#include "ui/rp_widget.h"
#include "styles/style_editor.h"
#include <QtGui/QLinearGradient>
namespace Editor {
namespace {
constexpr auto kPrecision = 1000;
constexpr auto kMinBrushSize = 0.1f;
constexpr auto kMouseSkip = 1.4;
constexpr auto kMinInnerHeight = 0.2;
constexpr auto kMaxInnerHeight = 0.8;
constexpr auto kCircleDuration = crl::time(200);
constexpr auto kMax = 1.0;
ColorPicker::OutlinedStop FindOutlinedStop(
const QColor &color,
const QGradientStops &stops,
int width) {
for (auto i = 0; i < stops.size(); i++) {
const auto &current = stops[i];
if (current.second == color) {
const auto prev = ((i - 1) < 0)
? std::nullopt
: std::make_optional<int>(stops[i - 1].first * width);
const auto next = ((i + 1) >= stops.size())
? std::nullopt
: std::make_optional<int>(stops[i + 1].first * width);
return ColorPicker::OutlinedStop{
.stopPos = (current.first * width),
.prevStopPos = prev,
.nextStopPos = next,
};
}
}
return ColorPicker::OutlinedStop();
}
QGradientStops Colors() {
return QGradientStops{
{ 0.00f, QColor(234, 39, 57) },
{ 0.14f, QColor(219, 58, 210) },
{ 0.24f, QColor(48, 81, 227) },
{ 0.39f, QColor(73, 197, 237) },
{ 0.49f, QColor(128, 200, 100) },
{ 0.62f, QColor(252, 222, 101) },
{ 0.73f, QColor(252, 150, 77) },
{ 0.85f, QColor(0, 0, 0) },
{ 1.00f, QColor(255, 255, 255) } };
}
QBrush GradientBrush(const QPoint &p, const QGradientStops &stops) {
auto gradient = QLinearGradient(0, p.y(), p.x(), p.y());
gradient.setStops(stops);
return QBrush(std::move(gradient));
}
float RatioPrecise(float a) {
return int(a * kPrecision) / float(kPrecision);
}
inline float64 InterpolateF(float a, float b, float64 b_ratio) {
return a + float64(b - a) * b_ratio;
};
inline float64 InterpolationRatio(int from, int to, int result) {
return (result - from) / float64(to - from);
};
} // namespace
ColorPicker::ColorPicker(
not_null<Ui::RpWidget*> parent,
const Brush &savedBrush)
: _circleColor(Qt::white)
, _width(st::photoEditorColorPickerWidth)
, _lineHeight(st::photoEditorColorPickerLineHeight)
, _colorLine(base::make_unique_q<Ui::RpWidget>(parent))
, _canvasForCircle(base::make_unique_q<Ui::RpWidget>(parent))
, _gradientStops(Colors())
, _outlinedStop(FindOutlinedStop(_circleColor, _gradientStops, _width))
, _gradientBrush(
GradientBrush(QPoint(_width, _lineHeight / 2), _gradientStops))
, _brush(Brush{
.sizeRatio = (savedBrush.sizeRatio
? savedBrush.sizeRatio
: kMinBrushSize),
.color = (savedBrush.color.isValid()
? savedBrush.color
: _gradientStops.front().second),
}) {
_colorLine->resize(_width, _lineHeight);
_canvasForCircle->resize(
_width + circleHeight(kMax),
st::photoEditorColorPickerCanvasHeight);
_canvasForCircle->setAttribute(Qt::WA_TransparentForMouseEvents);
_down.pos = QPoint(colorToPosition(savedBrush.color), 0);
_colorLine->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(_colorLine);
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(_gradientBrush);
const auto radius = _colorLine->height() / 2.;
p.drawRoundedRect(_colorLine->rect(), radius, radius);
}, _colorLine->lifetime());
_canvasForCircle->paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(_canvasForCircle);
paintCircle(p);
}, _canvasForCircle->lifetime());
_colorLine->events(
) | rpl::on_next([=](not_null<QEvent*> event) {
const auto type = event->type();
const auto isPress = (type == QEvent::MouseButtonPress)
|| (type == QEvent::MouseButtonDblClick);
const auto isMove = (type == QEvent::MouseMove);
const auto isRelease = (type == QEvent::MouseButtonRelease);
if (!isPress && !isMove && !isRelease) {
return;
}
_down.pressed = !isRelease;
const auto progress = _circleAnimation.value(isPress ? 0. : 1.);
if (!isMove) {
const auto from = progress;
const auto to = isPress ? 1. : 0.;
_circleAnimation.stop();
_circleAnimation.start(
[=] { _canvasForCircle->update(); },
from,
to,
kCircleDuration * std::abs(to - from),
anim::easeOutCirc);
}
const auto e = static_cast<QMouseEvent*>(event.get());
updateMousePosition(e->pos(), progress);
if (isRelease) {
_saveBrushRequests.fire_copy(_brush);
}
_canvasForCircle->update();
}, _colorLine->lifetime());
}
void ColorPicker::updateMousePosition(const QPoint &pos, float64 progress) {
const auto mapped = _canvasForCircle->mapFromParent(
_colorLine->mapToParent(pos));
const auto height = circleHeight(progress);
const auto mappedY = int(mapped.y() - height * kMouseSkip);
const auto bottom = _canvasForCircle->height() - circleHeight(kMax);
const auto &skip = st::photoEditorColorPickerCircleSkip;
_down.pos = QPoint(
std::clamp(pos.x(), 0, _width),
std::clamp(mappedY, 0, bottom - skip));
// Convert Y to the brush size.
const auto from = 0;
const auto to = bottom - skip;
// Don't change the brush size when we are on the color line.
if (mappedY <= to) {
_brush.sizeRatio = std::clamp(
float(1. - InterpolationRatio(from, to, _down.pos.y())),
kMinBrushSize,
1.f);
}
_brush.color = positionToColor(_down.pos.x());
}
void ColorPicker::moveLine(const QPoint &position) {
_colorLine->move(position
- QPoint(_colorLine->width() / 2, _colorLine->height() / 2));
_canvasForCircle->move(
_colorLine->x() - circleHeight(kMax) / 2,
_colorLine->y()
+ _colorLine->height()
+ ((circleHeight() - _colorLine->height()) / 2)
- _canvasForCircle->height());
}
QColor ColorPicker::positionToColor(int x) const {
const auto from = 0;
const auto to = _width;
const auto gradientRatio = InterpolationRatio(from, to, x);
for (auto i = 1; i < _gradientStops.size(); i++) {
const auto &previous = _gradientStops[i - 1];
const auto &current = _gradientStops[i];
const auto &fromStop = previous.first;
const auto &toStop = current.first;
const auto &fromColor = previous.second;
const auto &toColor = current.second;
if ((fromStop <= gradientRatio) && (toStop >= gradientRatio)) {
const auto stopRatio = RatioPrecise(
(gradientRatio - fromStop) / float64(toStop - fromStop));
return anim::color(fromColor, toColor, stopRatio);
}
}
return QColor();
}
void ColorPicker::paintCircle(QPainter &p) {
PainterHighQualityEnabler hq(p);
p.setPen(Qt::NoPen);
p.setBrush(_circleColor);
const auto progress = _circleAnimation.value(_down.pressed ? 1. : 0.);
const auto h = circleHeight(progress);
const auto bottom = _canvasForCircle->height() - h;
const auto circleX = _down.pos.x() + (circleHeight(kMax) - h) / 2;
const auto circleY = _circleAnimation.animating()
? anim::interpolate(bottom, _down.pos.y(), progress)
: _down.pressed
? _down.pos.y()
: bottom;
const auto r = QRect(circleX, circleY, h, h);
p.drawEllipse(r);
const auto innerH = InterpolateF(
h * kMinInnerHeight,
h * kMaxInnerHeight,
_brush.sizeRatio);
p.setBrush(_brush.color);
const auto innerRect = QRectF(
r.x() + (r.width() - innerH) / 2.,
r.y() + (r.height() - innerH) / 2.,
innerH,
innerH);
paintOutline(p, innerRect);
p.drawEllipse(innerRect);
}
void ColorPicker::paintOutline(QPainter &p, const QRectF &rect) {
const auto &s = _outlinedStop;
if (!s.stopPos) {
return;
}
const auto draw = [&](float opacity) {
p.save();
p.setOpacity(opacity);
p.setPen(Qt::lightGray);
p.setPen(Qt::NoBrush);
p.drawEllipse(rect);
p.restore();
};
const auto x = _down.pos.x();
if (s.prevStopPos && (x >= s.prevStopPos && x <= s.stopPos)) {
const auto from = *s.prevStopPos;
const auto to = *s.stopPos;
const auto ratio = InterpolationRatio(from, to, x);
if (ratio >= 0. && ratio <= 1.) {
draw(ratio);
}
} else if (s.nextStopPos && (x >= s.stopPos && x <= s.nextStopPos)) {
const auto from = *s.stopPos;
const auto to = *s.nextStopPos;
const auto ratio = InterpolationRatio(from, to, x);
if (ratio >= 0. && ratio <= 1.) {
draw(1. - ratio);
}
}
}
int ColorPicker::circleHeight(float64 progress) const {
return anim::interpolate(
st::photoEditorColorPickerCircleSize,
st::photoEditorColorPickerCircleBigSize,
progress);
}
void ColorPicker::setVisible(bool visible) {
_colorLine->setVisible(visible);
_canvasForCircle->setVisible(visible);
}
rpl::producer<Brush> ColorPicker::saveBrushRequests() const {
return _saveBrushRequests.events_starting_with_copy(_brush);
}
int ColorPicker::colorToPosition(const QColor &color) const {
const auto step = 1. / kPrecision;
for (auto i = 0.; i <= 1.; i += step) {
if (positionToColor(i * _width) == color) {
return i * _width;
}
}
return 0;
}
bool ColorPicker::preventHandleKeyPress() const {
return _canvasForCircle->isVisible()
&& (_circleAnimation.animating() || _down.pressed);
}
} // namespace Editor

View File

@@ -0,0 +1,67 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "editor/photo_editor_inner_common.h"
#include "ui/effects/animations.h"
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Editor {
class ColorPicker final {
public:
struct OutlinedStop {
std::optional<int> stopPos = std::nullopt;
std::optional<int> prevStopPos = std::nullopt;
std::optional<int> nextStopPos = std::nullopt;
};
ColorPicker(not_null<Ui::RpWidget*> parent, const Brush &savedBrush);
void moveLine(const QPoint &position);
void setVisible(bool visible);
bool preventHandleKeyPress() const;
rpl::producer<Brush> saveBrushRequests() const;
private:
void paintCircle(QPainter &p);
void paintOutline(QPainter &p, const QRectF &rect);
QColor positionToColor(int x) const;
int colorToPosition(const QColor &color) const;
int circleHeight(float64 progress = 0.) const;
void updateMousePosition(const QPoint &pos, float64 progress);
const QColor _circleColor;
const int _width;
const int _lineHeight;
const base::unique_qptr<Ui::RpWidget> _colorLine;
const base::unique_qptr<Ui::RpWidget> _canvasForCircle;
const QGradientStops _gradientStops;
const OutlinedStop _outlinedStop;
const QBrush _gradientBrush;
struct {
QPoint pos;
bool pressed = false;
} _down;
Brush _brush;
Ui::Animations::Simple _circleAnimation;
rpl::event_stream<Brush> _saveBrushRequests;
};
} // namespace Editor

View File

@@ -0,0 +1,33 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/controllers/stickers_panel_controller.h"
#include "editor/controllers/undo_controller.h"
#include "ui/layers/show.h"
namespace Editor {
struct Controllers final {
Controllers(
std::unique_ptr<StickersPanelController> stickersPanelController,
std::unique_ptr<UndoController> undoController,
std::shared_ptr<Ui::Show> show)
: stickersPanelController(std::move(stickersPanelController))
, undoController(std::move(undoController))
, show(std::move(show)) {
}
~Controllers() {
};
const std::unique_ptr<StickersPanelController> stickersPanelController;
const std::unique_ptr<UndoController> undoController;
const std::shared_ptr<Ui::Show> show;
};
} // namespace Editor

View File

@@ -0,0 +1,89 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/controllers/stickers_panel_controller.h"
#include "chat_helpers/tabbed_panel.h"
#include "chat_helpers/tabbed_selector.h"
#include "window/window_session_controller.h" // Window::GifPauseReason
#include "styles/style_chat_helpers.h"
#include "styles/style_media_view.h"
namespace Editor {
StickersPanelController::StickersPanelController(
not_null<Ui::RpWidget*> panelContainer,
std::shared_ptr<ChatHelpers::Show> show)
: _stickersPanel(
base::make_unique_q<ChatHelpers::TabbedPanel>(
panelContainer,
ChatHelpers::TabbedPanelDescriptor{
.ownedSelector = object_ptr<ChatHelpers::TabbedSelector>(
nullptr,
ChatHelpers::TabbedSelectorDescriptor{
.show = show,
.st = st::storiesComposeControls.tabbed,
.level = Window::GifPauseReason::Layer,
.mode = ChatHelpers::TabbedSelector::Mode::MediaEditor,
.features = {
.megagroupSet = false,
.stickersSettings = false,
.openStickerSets = false,
},
}),
})) {
_stickersPanel->setDesiredHeightValues(
1.,
st::emojiPanMinHeight / 2,
st::emojiPanMinHeight);
_stickersPanel->hide();
}
auto StickersPanelController::stickerChosen() const
-> rpl::producer<not_null<DocumentData*>> {
return _stickersPanel->selector()->fileChosen(
) | rpl::map([](const ChatHelpers::FileChosen &data) {
return data.document;
});
}
rpl::producer<bool> StickersPanelController::panelShown() const {
return _stickersPanel->shownValue();
}
void StickersPanelController::setShowRequestChanges(
rpl::producer<ShowRequest> &&showRequest) {
std::move(
showRequest
) | rpl::on_next([=](ShowRequest show) {
if (show == ShowRequest::ToggleAnimated) {
_stickersPanel->toggleAnimated();
_stickersPanel->raise();
} else if (show == ShowRequest::ShowAnimated) {
_stickersPanel->showAnimated();
_stickersPanel->raise();
} else if (show == ShowRequest::HideAnimated) {
_stickersPanel->hideAnimated();
} else if (show == ShowRequest::HideFast) {
_stickersPanel->hideFast();
}
}, _stickersPanel->lifetime());
}
void StickersPanelController::setMoveRequestChanges(
rpl::producer<QPoint> &&moveRequest) {
std::move(
moveRequest
) | rpl::on_next([=](const QPoint &point) {
_stickersPanel->moveBottomRight(
point.y(),
point.x() + _stickersPanel->width() / 2);
}, _stickersPanel->lifetime());
}
} // namespace Editor

View File

@@ -0,0 +1,49 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
namespace ChatHelpers {
class TabbedPanel;
class Show;
} // namespace ChatHelpers
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Editor {
class StickersPanelController final {
public:
enum class ShowRequest {
ToggleAnimated,
ShowAnimated,
HideAnimated,
HideFast,
};
StickersPanelController(
not_null<Ui::RpWidget*> panelContainer,
std::shared_ptr<ChatHelpers::Show> show);
[[nodiscard]] auto stickerChosen() const
-> rpl::producer<not_null<DocumentData*>>;
[[nodiscard]] rpl::producer<bool> panelShown() const;
void setShowRequestChanges(rpl::producer<ShowRequest> &&showRequest);
// Middle x and plain y position.
void setMoveRequestChanges(rpl::producer<QPoint> &&moveRequest);
private:
const base::unique_qptr<ChatHelpers::TabbedPanel> _stickersPanel;
};
} // namespace Editor

View File

@@ -0,0 +1,39 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/controllers/undo_controller.h"
namespace Editor {
namespace {
using EnableRequest = UndoController::EnableRequest;
} // namespace
UndoController::UndoController() {
}
void UndoController::setCanPerformChanges(
rpl::producer<EnableRequest> &&command) {
std::move(
command
) | rpl::start_to_stream(_enable, _lifetime);
}
void UndoController::setPerformRequestChanges(rpl::producer<Undo> &&command) {
std::move(
command
) | rpl::start_to_stream(_perform, _lifetime);
}
rpl::producer<EnableRequest> UndoController::canPerformChanges() const {
return _enable.events();
}
rpl::producer<Undo> UndoController::performRequestChanges() const {
return _perform.events();
}
} // namespace Editor

View File

@@ -0,0 +1,41 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Editor {
enum class Undo {
Undo,
Redo,
};
class UndoController final {
public:
struct EnableRequest {
Undo command = Undo::Undo;
bool enable = true;
};
UndoController();
void setCanPerformChanges(rpl::producer<EnableRequest> &&command);
void setPerformRequestChanges(rpl::producer<Undo> &&command);
[[nodiscard]] rpl::producer<EnableRequest> canPerformChanges() const;
[[nodiscard]] rpl::producer<Undo> performRequestChanges() const;
private:
rpl::event_stream<Undo> _perform;
rpl::event_stream<EnableRequest> _enable;
rpl::lifetime _lifetime;
};
} // namespace Editor

View File

@@ -0,0 +1,104 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
using "ui/basic.style";
using "window/window.style";
using "ui/widgets/widgets.style";
using "ui/chat/chat.style";
// photoEditorControlsBottomSkip * 2 + photoEditorControlsCenterSkip + photoEditorButtonBarHeight * 2
photoEditorControlsHeight: 146px;
photoEditorControlsBottomSkip: 20px;
photoEditorControlsCenterSkip: 6px;
photoEditorContentMargins: margins(
photoEditorControlsBottomSkip,
photoEditorControlsBottomSkip,
photoEditorControlsBottomSkip,
photoEditorControlsHeight);
photoEditorBarAnimationDuration: 200;
photoEditorButtonIconFg: mediaviewPipControlsFg;
photoEditorButtonIconFgOver: mediaviewPipControlsFgOver;
photoEditorButtonIconFgActive: lightButtonFg;
photoEditorButtonIconFgInactive: mediaviewPipPlaybackInactive;
photoEditorButtonBarHeight: 48px;
photoEditorButtonBarWidth: 422px;
photoEditorButtonBarPadding: margins(2px, 0px, 2px, 0px);
photoEditorTextButtonPadding: margins(22px, 0px, 22px, 0px);
photoEditorButtonStyle: TextStyle(semiboldTextStyle) {
font: font(14px semibold);
}
photoEditorButtonTextTop: 15px;
photoEditorAbout: FlatLabel(defaultFlatLabel) {
textFg: mediaviewCaptionFg;
minWidth: 240px;
align: align(top);
}
photoEditorAboutMargin: margins(10px, 22px, 10px, 0px);
photoEditorRotateButton: IconButton(defaultIconButton) {
width: photoEditorButtonBarHeight;
height: photoEditorButtonBarHeight;
icon: icon {{ "photo_editor/rotate-flip_horizontal", photoEditorButtonIconFg }};
iconOver: icon {{ "photo_editor/rotate-flip_horizontal", photoEditorButtonIconFgOver }};
rippleAreaPosition: point(4px, 4px);
rippleAreaSize: 40px;
ripple: universalRippleAnimation;
}
photoEditorFlipButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "photo_editor/flip", photoEditorButtonIconFg }};
iconOver: icon {{ "photo_editor/flip", photoEditorButtonIconFgOver }};
}
photoEditorFlipIconActive: icon {{ "photo_editor/flip", photoEditorButtonIconFgActive }};
photoEditorPaintModeButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "photo_editor/paint", photoEditorButtonIconFg }};
iconOver: icon {{ "photo_editor/paint", photoEditorButtonIconFgOver }};
}
photoEditorPaintIconActive: icon {{ "photo_editor/paint", photoEditorButtonIconFgActive }};
photoEditorUndoButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "photo_editor/undo", photoEditorButtonIconFg }};
iconOver: icon {{ "photo_editor/undo", photoEditorButtonIconFgOver }};
}
photoEditorRedoButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFg }};
iconOver: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgOver }};
}
photoEditorStickersButton: IconButton(photoEditorRotateButton) {
icon: icon {{ "settings/settings_stickers", photoEditorButtonIconFg }};
iconOver: icon {{ "settings/settings_stickers", photoEditorButtonIconFgOver }};
}
photoEditorStickersIconActive: icon {{ "settings/settings_stickers", photoEditorButtonIconFgActive }};
photoEditorUndoButtonInactive: icon {{ "photo_editor/undo", photoEditorButtonIconFgInactive }};
photoEditorRedoButtonInactive: icon {{ "photo_editor/undo-flip_horizontal", photoEditorButtonIconFgInactive }};
photoEditorColorPickerWidth: 250px;
photoEditorColorPickerLineHeight: 10px;
photoEditorColorPickerCanvasHeight: 300px;
photoEditorColorPickerCircleSize: 24px;
photoEditorColorPickerCircleBigSize: 50px;
photoEditorColorPickerCircleSkip: 50px;
photoEditorCropPointSize: 10px;
photoEditorCropMinSize: 20px;
photoEditorItemHandleSize: 10px;

View File

@@ -0,0 +1,358 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_crop.h"
#include "ui/userpic_view.h"
#include "styles/style_editor.h"
#include "styles/style_basic.h"
#include "styles/style_dialogs.h"
namespace Editor {
namespace {
constexpr auto kETL = Qt::TopEdge | Qt::LeftEdge;
constexpr auto kETR = Qt::TopEdge | Qt::RightEdge;
constexpr auto kEBL = Qt::BottomEdge | Qt::LeftEdge;
constexpr auto kEBR = Qt::BottomEdge | Qt::RightEdge;
constexpr auto kEAll = Qt::TopEdge
| Qt::LeftEdge
| Qt::BottomEdge
| Qt::RightEdge;
std::tuple<int, int, int, int> RectEdges(const QRectF &r) {
return { r.left(), r.top(), r.left() + r.width(), r.top() + r.height() };
}
QPoint PointOfEdge(Qt::Edges e, const QRectF &r) {
switch(e) {
case kETL: return QPoint(r.x(), r.y());
case kETR: return QPoint(r.x() + r.width(), r.y());
case kEBL: return QPoint(r.x(), r.y() + r.height());
case kEBR: return QPoint(r.x() + r.width(), r.y() + r.height());
default: return QPoint();
}
}
QSizeF FlipSizeByRotation(const QSizeF &size, int angle) {
return (((angle / 90) % 2) == 1) ? size.transposed() : size;
}
[[nodiscard]] QRectF OriginalCrop(QSize outer, QSize inner) {
const auto size = inner.scaled(outer, Qt::KeepAspectRatio);
return QRectF(
(outer.width() - size.width()) / 2,
(outer.height() - size.height()) / 2,
size.width(),
size.height());
}
} // namespace
Crop::Crop(
not_null<Ui::RpWidget*> parent,
const PhotoModifications &modifications,
const QSize &imageSize,
EditorData data)
: RpWidget(parent)
, _pointSize(st::photoEditorCropPointSize)
, _pointSizeH(_pointSize / 2.)
, _innerMargins(QMarginsF(_pointSizeH, _pointSizeH, _pointSizeH, _pointSizeH)
.toMargins())
, _offset(_innerMargins.left(), _innerMargins.top())
, _edgePointMargins(_pointSizeH, _pointSizeH, -_pointSizeH, -_pointSizeH)
, _imageSize(imageSize)
, _data(std::move(data))
, _cropOriginal(modifications.crop.isValid()
? modifications.crop
: !_data.exactSize.isEmpty()
? OriginalCrop(_imageSize, _data.exactSize)
: QRectF(QPoint(), _imageSize))
, _angle(modifications.angle)
, _flipped(modifications.flipped)
, _keepAspectRatio(_data.keepAspectRatio) {
setMouseTracking(true);
paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(this);
p.fillPath(_painterPath, st::photoCropFadeBg);
paintPoints(p);
}, lifetime());
}
void Crop::applyTransform(
const QRect &geometry,
int angle,
bool flipped,
const QSizeF &scaledImageSize) {
if (geometry.isEmpty()) {
return;
}
setGeometry(geometry);
_innerRect = QRectF(_offset, FlipSizeByRotation(scaledImageSize, angle));
_ratio.w = scaledImageSize.width() / float64(_imageSize.width());
_ratio.h = scaledImageSize.height() / float64(_imageSize.height());
_flipped = flipped;
_angle = angle;
const auto cropHolder = QRectF(QPointF(), scaledImageSize);
const auto cropHolderCenter = cropHolder.center();
auto matrix = QTransform()
.translate(cropHolderCenter.x(), cropHolderCenter.y())
.scale(flipped ? -1 : 1, 1)
.rotate(angle)
.translate(-cropHolderCenter.x(), -cropHolderCenter.y());
const auto cropHolderRotated = matrix.mapRect(cropHolder);
auto cropPaint = matrix
.scale(_ratio.w, _ratio.h)
.mapRect(_cropOriginal)
.translated(
-cropHolderRotated.x() + _offset.x(),
-cropHolderRotated.y() + _offset.y());
// Check boundaries.
const auto min = float64(st::photoEditorCropMinSize);
if ((cropPaint.width() < min) || (cropPaint.height() < min)) {
cropPaint.setWidth(std::max(min, cropPaint.width()));
cropPaint.setHeight(std::max(min, cropPaint.height()));
const auto p = cropPaint.center().toPoint();
setCropPaint(std::move(cropPaint));
computeDownState(p);
performMove(p);
clearDownState();
convertCropPaintToOriginal();
} else {
setCropPaint(std::move(cropPaint));
}
}
void Crop::paintPoints(QPainter &p) {
p.save();
p.setPen(Qt::NoPen);
p.setBrush(st::photoCropPointFg);
for (const auto &r : ranges::views::values(_edges)) {
p.drawRect(r);
}
p.restore();
}
void Crop::setCropPaint(QRectF &&rect) {
_cropPaint = std::move(rect);
updateEdges();
_painterPath.clear();
_painterPath.addRect(_innerRect);
if (_data.cropType == EditorData::CropType::Ellipse) {
_painterPath.addEllipse(_cropPaint);
} else if (_data.cropType == EditorData::CropType::RoundedRect) {
const auto radius = std::min(_cropPaint.width(), _cropPaint.height())
* Ui::ForumUserpicRadiusMultiplier();
_painterPath.addRoundedRect(_cropPaint, radius, radius);
} else {
_painterPath.addRect(_cropPaint);
}
}
void Crop::convertCropPaintToOriginal() {
const auto cropHolder = QTransform()
.scale(_ratio.w, _ratio.h)
.mapRect(QRectF(QPointF(), FlipSizeByRotation(_imageSize, _angle)));
const auto cropHolderCenter = cropHolder.center();
const auto matrix = QTransform()
.translate(cropHolderCenter.x(), cropHolderCenter.y())
.rotate(-_angle)
.scale((_flipped ? -1 : 1) * 1. / _ratio.w, 1. / _ratio.h)
.translate(-cropHolderCenter.x(), -cropHolderCenter.y());
const auto cropHolderRotated = matrix.mapRect(cropHolder);
_cropOriginal = matrix
.mapRect(QRectF(_cropPaint).translated(-_offset))
.translated(
-cropHolderRotated.x(),
-cropHolderRotated.y());
}
void Crop::updateEdges() {
const auto &s = _pointSize;
const auto &m = _edgePointMargins;
const auto &r = _cropPaint;
for (const auto &e : { kETL, kETR, kEBL, kEBR }) {
_edges[e] = QRectF(PointOfEdge(e, r), QSize(s, s)) + m;
}
}
Qt::Edges Crop::mouseState(const QPoint &p) {
for (const auto &[e, r] : _edges) {
if (r.contains(p)) {
return e;
}
}
if (_cropPaint.contains(p)) {
return kEAll;
}
return Qt::Edges();
}
void Crop::mousePressEvent(QMouseEvent *e) {
computeDownState(e->pos());
}
void Crop::mouseReleaseEvent(QMouseEvent *e) {
clearDownState();
convertCropPaintToOriginal();
}
void Crop::computeDownState(const QPoint &p) {
const auto edge = mouseState(p);
const auto &inner = _innerRect;
const auto &crop = _cropPaint;
const auto &[iLeft, iTop, iRight, iBottom] = RectEdges(inner);
const auto &[cLeft, cTop, cRight, cBottom] = RectEdges(crop);
_down = InfoAtDown{
.rect = crop,
.edge = edge,
.point = (p - PointOfEdge(edge, crop)),
.cropRatio = (_cropOriginal.width() / _cropOriginal.height()),
.borders = InfoAtDown::Borders{
.left = iLeft - cLeft,
.right = iRight - cRight,
.top = iTop - cTop,
.bottom = iBottom - cBottom,
}
};
if (_keepAspectRatio && (edge != kEAll)) {
const auto hasLeft = (edge & Qt::LeftEdge);
const auto hasTop = (edge & Qt::TopEdge);
const auto xSign = hasLeft ? -1 : 1;
const auto ySign = hasTop ? -1 : 1;
auto &xSide = (hasLeft ? _down.borders.left : _down.borders.right);
auto &ySide = (hasTop ? _down.borders.top : _down.borders.bottom);
const auto min = std::abs(std::min(xSign * xSide, ySign * ySide));
const auto xIsMin = ((xSign * xSide) < (ySign * ySide));
xSide = xSign * min;
ySide = ySign * min;
if (!xIsMin) {
xSide *= _down.cropRatio;
} else {
ySide /= _down.cropRatio;
}
}
}
void Crop::clearDownState() {
_down = InfoAtDown();
}
void Crop::performCrop(const QPoint &pos) {
const auto &crop = _down.rect;
const auto &pressedEdge = _down.edge;
const auto hasLeft = (pressedEdge & Qt::LeftEdge);
const auto hasTop = (pressedEdge & Qt::TopEdge);
const auto hasRight = (pressedEdge & Qt::RightEdge);
const auto hasBottom = (pressedEdge & Qt::BottomEdge);
const auto diff = [&] {
auto diff = pos - PointOfEdge(pressedEdge, crop) - _down.point;
const auto xFactor = hasLeft ? 1 : -1;
const auto yFactor = hasTop ? 1 : -1;
const auto &borders = _down.borders;
const auto &cropRatio = _down.cropRatio;
if (_keepAspectRatio) {
const auto diffSign = xFactor * yFactor;
diff = (cropRatio != 1.)
? QPoint(diff.x(), (1. / cropRatio) * diff.x() * diffSign)
// For square/circle.
: ((diff.x() * xFactor) < (diff.y() * yFactor))
? QPoint(diff.x(), diff.x() * diffSign)
: QPoint(diff.y() * diffSign, diff.y());
}
const auto &minSize = st::photoEditorCropMinSize;
const auto xMin = xFactor * int(crop.width() - minSize);
// const auto xMin = int(xFactor * crop.width()
// - xFactor * minSize * ((cropRatio > 1.) ? cropRatio : 1.));
const auto yMin = yFactor * int(crop.height() - minSize);
// const auto yMin = int(yFactor * crop.height()
// - yFactor * minSize * ((cropRatio < 1.) ? (1. / cropRatio) : 1.));
const auto x = std::clamp(
diff.x(),
hasLeft ? borders.left : xMin,
hasLeft ? xMin : borders.right);
const auto y = std::clamp(
diff.y(),
hasTop ? borders.top : yMin,
hasTop ? yMin : borders.bottom);
return QPoint(x, y);
}();
setCropPaint(crop - QMargins(
hasLeft ? diff.x() : 0,
hasTop ? diff.y() : 0,
hasRight ? -diff.x() : 0,
hasBottom ? -diff.y() : 0));
}
void Crop::performMove(const QPoint &pos) {
const auto &inner = _down.rect;
const auto &b = _down.borders;
const auto diffX = std::clamp(pos.x() - _down.point.x(), b.left, b.right);
const auto diffY = std::clamp(pos.y() - _down.point.y(), b.top, b.bottom);
setCropPaint(inner.translated(diffX, diffY));
}
void Crop::mouseMoveEvent(QMouseEvent *e) {
const auto pos = e->pos();
const auto pressedEdge = _down.edge;
if (pressedEdge) {
if (pressedEdge == kEAll) {
performMove(pos);
} else if (pressedEdge) {
performCrop(pos);
}
update();
}
const auto edge = pressedEdge ? pressedEdge : mouseState(pos);
const auto cursor = ((edge == kETL) || (edge == kEBR))
? style::cur_sizefdiag
: ((edge == kETR) || (edge == kEBL))
? style::cur_sizebdiag
: (edge == kEAll)
? style::cur_sizeall
: style::cur_default;
setCursor(cursor);
}
style::margins Crop::cropMargins() const {
return _innerMargins;
}
QRect Crop::saveCropRect() {
const auto savedCrop = _cropOriginal.toRect();
return (!savedCrop.topLeft().isNull() || (savedCrop.size() != _imageSize))
? savedCrop
: QRect();
}
} // namespace Editor

View File

@@ -0,0 +1,97 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "base/flat_map.h"
#include "editor/photo_editor_common.h"
namespace Editor {
// Crop control.
class Crop final : public Ui::RpWidget {
public:
Crop(
not_null<Ui::RpWidget*> parent,
const PhotoModifications &modifications,
const QSize &imageSize,
EditorData type);
void applyTransform(
const QRect &geometry,
int angle,
bool flipped,
const QSizeF &scaledImageSize);
[[nodiscard]] QRect saveCropRect();
[[nodiscard]] style::margins cropMargins() const;
protected:
void mousePressEvent(QMouseEvent *e) override;
void mouseReleaseEvent(QMouseEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
private:
struct InfoAtDown {
QRectF rect;
Qt::Edges edge;
QPoint point;
float64 cropRatio = 0.;
struct Borders {
int left = 0;
int right = 0;
int top = 0;
int bottom = 0;
} borders;
};
void paintPoints(QPainter &p);
void updateEdges();
[[nodiscard]] QPoint pointOfEdge(Qt::Edges e) const;
void setCropPaint(QRectF &&rect);
void convertCropPaintToOriginal();
void computeDownState(const QPoint &p);
void clearDownState();
[[nodiscard]] Qt::Edges mouseState(const QPoint &p);
void performCrop(const QPoint &pos);
void performMove(const QPoint &pos);
const int _pointSize;
const float _pointSizeH;
const style::margins _innerMargins;
const QPoint _offset;
const QMarginsF _edgePointMargins;
const QSize _imageSize;
const EditorData _data;
base::flat_map<Qt::Edges, QRectF> _edges;
struct {
float64 w = 0.;
float64 h = 0.;
} _ratio;
QRectF _cropPaint;
QRectF _cropOriginal;
QRectF _innerRect;
QPainterPath _painterPath;
InfoAtDown _down;
int _angle = 0;
bool _flipped = false;
bool _keepAspectRatio = false;
};
} // namespace Editor

View File

@@ -0,0 +1,222 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_layer_widget.h"
#include "ui/painter.h"
#include "ui/ui_utility.h"
#include <QtGui/QGuiApplication>
namespace Editor {
namespace {
constexpr auto kCacheBackgroundFastTimeout = crl::time(200);
constexpr auto kCacheBackgroundFullTimeout = crl::time(1000);
constexpr auto kFadeBackgroundDuration = crl::time(200);
// Thread: Main.
[[nodiscard]] bool IsNightMode() {
return (st::windowBg->c.lightnessF() < 0.5);
}
[[nodiscard]] QColor BlurOverlayColor(bool night) {
return QColor(16, 16, 16, night ? 128 : 192);
}
[[nodiscard]] QImage ProcessBackground(QImage image, bool night) {
const auto size = image.size();
auto p = QPainter(&image);
p.fillRect(
QRect(QPoint(), image.size() / image.devicePixelRatio()),
BlurOverlayColor(night));
p.end();
return Images::DitherImage(
Images::BlurLargeImage(
std::move(image).scaled(
size / style::ConvertScale(4),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation),
24).scaled(
size,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation));
}
} // namespace
LayerWidget::LayerWidget(
not_null<QWidget*> parent,
base::unique_qptr<Ui::RpWidget> content)
: Ui::LayerWidget(parent)
, _content(std::move(content))
, _backgroundTimer([=] { checkCacheBackground(); }) {
_content->setParent(this);
_content->show();
paintRequest(
) | rpl::on_next([=](const QRect &clip) {
auto p = QPainter(this);
const auto faded = _backgroundFade.value(1.);
if (faded < 1.) {
p.drawImage(rect(), _backgroundBack);
if (faded > 0.) {
p.setOpacity(faded);
p.drawImage(rect(), _background);
}
} else {
p.drawImage(rect(), _background);
}
}, lifetime());
}
bool LayerWidget::eventHook(QEvent *e) {
return RpWidget::eventHook(e);
}
void LayerWidget::start() {
_backgroundNight = IsNightMode();
_background = ProcessBackground(renderBackground(), _backgroundNight);
sizeValue(
) | rpl::on_next([=](const QSize &size) {
checkBackgroundStale();
_content->resize(size);
}, lifetime());
style::PaletteChanged() | rpl::on_next([=] {
checkBackgroundStale();
}, lifetime());
}
void LayerWidget::checkBackgroundStale() {
const auto ratio = style::DevicePixelRatio();
const auto &ready = _backgroundNext.isNull()
? _background
: _backgroundNext;
if (ready.size() == size() * ratio
&& _backgroundNight == IsNightMode()) {
_backgroundTimer.cancel();
} else if (!_backgroundCaching && !_backgroundTimer.isActive()) {
_lastAreaChangeTime = crl::now();
_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);
}
}
QImage LayerWidget::renderBackground() {
const auto parent = parentWidget();
const auto target = parent->parentWidget();
Ui::SendPendingMoveResizeEvents(target);
const auto ratio = style::DevicePixelRatio();
auto image = QImage(size() * ratio, QImage::Format_ARGB32_Premultiplied);
image.setDevicePixelRatio(ratio);
const auto shown = !parent->isHidden();
const auto focused = shown && Ui::InFocusChain(parent);
if (shown) {
if (focused) {
target->setFocus();
}
parent->hide();
}
auto p = QPainter(&image);
Ui::RenderWidget(p, target, QPoint(), geometry());
p.end();
if (shown) {
parent->show();
if (focused) {
if (isHidden()) {
parent->setFocus();
} else {
setInnerFocus();
}
}
}
return image;
}
void LayerWidget::checkCacheBackground() {
if (_backgroundCaching || _backgroundTimer.isActive()) {
return;
}
const auto now = crl::now();
if (now - _lastAreaChangeTime < kCacheBackgroundFullTimeout
&& QGuiApplication::mouseButtons() != 0) {
_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);
return;
}
cacheBackground();
}
void LayerWidget::cacheBackground() {
_backgroundCaching = true;
const auto weak = base::make_weak(this);
const auto night = IsNightMode();
crl::async([weak, night, image = renderBackground()]() mutable {
auto result = ProcessBackground(image, night);
crl::on_main([weak, night, result = std::move(result)]() mutable {
if (const auto strong = weak.get()) {
strong->backgroundReady(std::move(result), night);
}
});
});
}
void LayerWidget::backgroundReady(QImage background, bool night) {
_backgroundCaching = false;
const auto required = size() * style::DevicePixelRatio();
if (background.size() == required && night == IsNightMode()) {
_backgroundNext = std::move(background);
_backgroundNight = night;
if (!_backgroundFade.animating()) {
startBackgroundFade();
}
update();
} else if (_background.size() != required) {
_backgroundTimer.callOnce(kCacheBackgroundFastTimeout);
}
}
void LayerWidget::startBackgroundFade() {
if (_backgroundNext.isNull()) {
return;
}
_backgroundBack = std::move(_background);
_background = base::take(_backgroundNext);
_backgroundFade.start([=] {
update();
if (!_backgroundFade.animating()) {
_backgroundBack = QImage();
startBackgroundFade();
}
}, 0., 1., kFadeBackgroundDuration);
}
void LayerWidget::parentResized() {
resizeToWidth(parentWidget()->width());
if (_background.isNull()) {
start();
}
}
void LayerWidget::keyPressEvent(QKeyEvent *e) {
QGuiApplication::sendEvent(_content.get(), e);
}
int LayerWidget::resizeGetHeight(int newWidth) {
return parentWidget()->height();
}
bool LayerWidget::closeByOutsideClick() const {
return false;
}
} // namespace Editor

View File

@@ -0,0 +1,59 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/layers/layer_widget.h"
#include "ui/image/image.h"
#include "editor/photo_editor_common.h"
#include "base/unique_qptr.h"
#include "base/timer.h"
enum class ImageRoundRadius;
namespace Window {
class Controller;
class SessionController;
} // namespace Window
namespace Editor {
class LayerWidget final : public Ui::LayerWidget {
public:
LayerWidget(
not_null<QWidget*> parent,
base::unique_qptr<Ui::RpWidget> content);
void parentResized() override;
bool closeByOutsideClick() const override;
private:
bool eventHook(QEvent *e) override;
void keyPressEvent(QKeyEvent *e) override;
int resizeGetHeight(int newWidth) override;
void start();
void cacheBackground();
void checkBackgroundStale();
void checkCacheBackground();
[[nodiscard]] QImage renderBackground();
void backgroundReady(QImage background, bool night);
void startBackgroundFade();
const base::unique_qptr<Ui::RpWidget> _content;
QImage _backgroundBack;
QImage _background;
QImage _backgroundNext;
Ui::Animations::Simple _backgroundFade;
base::Timer _backgroundTimer;
crl::time _lastAreaChangeTime = 0;
bool _backgroundCaching = false;
bool _backgroundNight = false;
};
} // namespace Editor

View File

@@ -0,0 +1,240 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/editor_paint.h"
#include "core/mime_type.h"
#include "ui/boxes/confirm_box.h"
#include "editor/controllers/controllers.h"
#include "editor/scene/scene.h"
#include "editor/scene/scene_item_canvas.h"
#include "editor/scene/scene_item_image.h"
#include "editor/scene/scene_item_sticker.h"
#include "lang/lang_keys.h"
#include "lottie/lottie_single_player.h"
#include "storage/storage_media_prepare.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/ui_utility.h"
#include <QGraphicsView>
#include <QtCore/QMimeData>
namespace Editor {
namespace {
constexpr auto kMaxBrush = 25.;
constexpr auto kMinBrush = 1.;
std::shared_ptr<Scene> EnsureScene(
PhotoModifications &mods,
const QSize &size) {
if (!mods.paint) {
mods.paint = std::make_shared<Scene>(QRectF(QPointF(), size));
}
return mods.paint;
}
} // namespace
using ItemPtr = std::shared_ptr<QGraphicsItem>;
Paint::Paint(
not_null<Ui::RpWidget*> parent,
PhotoModifications &modifications,
const QSize &imageSize,
std::shared_ptr<Controllers> controllers)
: RpWidget(parent)
, _controllers(controllers)
, _scene(EnsureScene(modifications, imageSize))
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
, _imageSize(imageSize) {
Expects(modifications.paint != nullptr);
keepResult();
_view->show();
_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
_view->setFrameStyle(int(QFrame::NoFrame) | QFrame::Plain);
_view->viewport()->setAutoFillBackground(false);
// Undo / Redo.
controllers->undoController->performRequestChanges(
) | rpl::on_next([=](const Undo &command) {
if (command == Undo::Undo) {
_scene->performUndo();
} else {
_scene->performRedo();
}
_hasUndo = _scene->hasUndo();
_hasRedo = _scene->hasRedo();
}, lifetime());
controllers->undoController->setCanPerformChanges(rpl::merge(
_hasUndo.value() | rpl::map([](bool enable) {
return UndoController::EnableRequest{
.command = Undo::Undo,
.enable = enable,
};
}),
_hasRedo.value() | rpl::map([](bool enable) {
return UndoController::EnableRequest{
.command = Undo::Redo,
.enable = enable,
};
})));
if (controllers->stickersPanelController) {
using ShowRequest = StickersPanelController::ShowRequest;
controllers->stickersPanelController->setShowRequestChanges(
controllers->stickersPanelController->stickerChosen(
) | rpl::map_to(ShowRequest::HideAnimated));
controllers->stickersPanelController->stickerChosen(
) | rpl::on_next([=](not_null<DocumentData*> document) {
const auto item = std::make_shared<ItemSticker>(
document,
itemBaseData());
_scene->addItem(item);
_scene->clearSelection();
}, lifetime());
}
rpl::merge(
controllers->stickersPanelController
? controllers->stickersPanelController->stickerChosen(
) | rpl::to_empty
: rpl::never<>() | rpl::type_erased,
_scene->addsItem()
) | rpl::on_next([=] {
clearRedoList();
updateUndoState();
}, lifetime());
_scene->removesItem(
) | rpl::on_next([=] {
updateUndoState();
}, lifetime());
}
void Paint::applyTransform(QRect geometry, int angle, bool flipped) {
if (geometry.isEmpty()) {
return;
}
setGeometry(geometry);
const auto size = geometry.size();
const auto rotatedImageSize = QTransform()
.rotate(angle)
.mapRect(QRect(QPoint(), _imageSize));
const auto ratioW = size.width() / float64(rotatedImageSize.width())
* (flipped ? -1 : 1);
const auto ratioH = size.height() / float64(rotatedImageSize.height());
_view->setTransform(QTransform().scale(ratioW, ratioH).rotate(angle));
_view->setGeometry(QRect(QPoint(), size));
_transform = {
.angle = angle,
.flipped = flipped,
.zoom = size.width() / float64(_scene->sceneRect().width()),
};
_scene->updateZoom(_transform.zoom);
}
std::shared_ptr<Scene> Paint::saveScene() const {
_scene->save(SaveState::Save);
return _scene->items().empty()
? nullptr
: _scene;
}
void Paint::restoreScene() {
_scene->restore(SaveState::Save);
}
void Paint::cancel() {
_scene->restore(SaveState::Keep);
}
void Paint::keepResult() {
_scene->save(SaveState::Keep);
}
void Paint::clearRedoList() {
_scene->clearRedoList();
_hasRedo = false;
}
void Paint::updateUndoState() {
_hasUndo = _scene->hasUndo();
_hasRedo = _scene->hasRedo();
}
void Paint::applyBrush(const Brush &brush) {
_scene->applyBrush(
brush.color,
(kMinBrush + float64(kMaxBrush - kMinBrush) * brush.sizeRatio));
}
void Paint::handleMimeData(const QMimeData *data) {
const auto add = [&](QImage image) {
if (image.isNull()) {
return;
}
if (!Ui::ValidateThumbDimensions(image.width(), image.height())) {
_controllers->show->showBox(
Ui::MakeInformBox(tr::lng_edit_media_invalid_file()));
return;
}
const auto item = std::make_shared<ItemImage>(
Ui::PixmapFromImage(std::move(image)),
itemBaseData());
_scene->addItem(item);
_scene->clearSelection();
};
using Error = Ui::PreparedList::Error;
const auto premium = false; // Don't support > 2GB files here.
const auto list = Core::ReadMimeUrls(data);
auto result = !list.isEmpty()
? Storage::PrepareMediaList(
list.mid(0, 1),
_imageSize.width() / 2,
premium)
: Ui::PreparedList(Error::EmptyFile, QString());
if (result.error == Error::None) {
add(base::take(result.files.front().preview));
} else if (auto read = Core::ReadMimeImage(data)) {
add(std::move(read.image));
}
}
ItemBase::Data Paint::itemBaseData() const {
const auto s = _scene->sceneRect().toRect().size();
const auto size = std::min(s.width(), s.height()) / 2;
const auto x = s.width() / 2;
const auto y = s.height() / 2;
return ItemBase::Data{
.initialZoom = _transform.zoom,
.zPtr = _scene->lastZ(),
.size = size,
.x = x,
.y = y,
.flipped = _transform.flipped,
.rotation = -_transform.angle,
.imageSize = _imageSize,
};
}
} // namespace Editor

View File

@@ -0,0 +1,71 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_inner_common.h"
#include "editor/scene/scene_item_base.h"
class QGraphicsItem;
class QGraphicsView;
namespace Editor {
struct Controllers;
class Scene;
// Paint control.
class Paint final : public Ui::RpWidget {
public:
Paint(
not_null<Ui::RpWidget*> parent,
PhotoModifications &modifications,
const QSize &imageSize,
std::shared_ptr<Controllers> controllers);
[[nodiscard]] std::shared_ptr<Scene> saveScene() const;
void restoreScene();
void applyTransform(QRect geometry, int angle, bool flipped);
void applyBrush(const Brush &brush);
void cancel();
void keepResult();
void updateUndoState();
void handleMimeData(const QMimeData *data);
private:
struct SavedItem {
std::shared_ptr<QGraphicsItem> item;
bool undid = false;
};
ItemBase::Data itemBaseData() const;
void clearRedoList();
const std::shared_ptr<Controllers> _controllers;
const std::shared_ptr<Scene> _scene;
const base::unique_qptr<QGraphicsView> _view;
const QSize _imageSize;
struct {
int angle = 0;
bool flipped = false;
float64 zoom = 0.;
} _transform;
rpl::variable<bool> _hasUndo = true;
rpl::variable<bool> _hasRedo = true;
};
} // namespace Editor

View File

@@ -0,0 +1,242 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor.h"
#include "core/application.h"
#include "core/core_settings.h"
#include "editor/color_picker.h"
#include "editor/controllers/controllers.h"
#include "editor/photo_editor_content.h"
#include "editor/photo_editor_controls.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include "ui/layers/layer_widget.h"
#include "styles/style_editor.h"
namespace Editor {
namespace {
constexpr auto kPrecision = 100000;
[[nodiscard]] QByteArray Serialize(const Brush &brush) {
auto result = QByteArray();
auto stream = QDataStream(&result, QIODevice::WriteOnly);
stream.setVersion(QDataStream::Qt_5_3);
stream << qint32(brush.sizeRatio * kPrecision) << brush.color;
stream.device()->close();
return result;
}
[[nodiscard]] Brush Deserialize(const QByteArray &data) {
auto stream = QDataStream(data);
auto result = Brush();
auto size = qint32(0);
stream >> size >> result.color;
result.sizeRatio = size / float(kPrecision);
return (stream.status() != QDataStream::Ok)
? Brush()
: result;
}
} // namespace
PhotoEditor::PhotoEditor(
not_null<QWidget*> parent,
not_null<Window::Controller*> controller,
std::shared_ptr<Image> photo,
PhotoModifications modifications,
EditorData data)
: PhotoEditor(
parent,
controller->uiShow(),
(controller->sessionController()
? controller->sessionController()->uiShow()
: nullptr),
std::move(photo),
std::move(modifications),
std::move(data)) {
}
PhotoEditor::PhotoEditor(
not_null<QWidget*> parent,
std::shared_ptr<Ui::Show> show,
std::shared_ptr<ChatHelpers::Show> sessionShow,
std::shared_ptr<Image> photo,
PhotoModifications modifications,
EditorData data)
: RpWidget(parent)
, _modifications(std::move(modifications))
, _controllers(std::make_shared<Controllers>(
sessionShow
? std::make_unique<StickersPanelController>(
this,
std::move(sessionShow))
: nullptr,
std::make_unique<UndoController>(),
std::move(show)))
, _content(base::make_unique_q<PhotoEditorContent>(
this,
photo,
_modifications,
_controllers,
data))
, _controls(base::make_unique_q<PhotoEditorControls>(
this,
_controllers,
_modifications,
data))
, _colorPicker(std::make_unique<ColorPicker>(
this,
Deserialize(Core::App().settings().photoEditorBrush()))) {
sizeValue(
) | rpl::on_next([=](const QSize &size) {
if (size.isEmpty()) {
return;
}
_content->setGeometry(rect() - st::photoEditorContentMargins);
}, lifetime());
_content->innerRect(
) | rpl::on_next([=](QRect inner) {
if (inner.isEmpty()) {
return;
}
const auto innerTop = _content->y() + inner.top();
const auto skip = st::photoEditorCropPointSize;
const auto controlsRect = rect()
- style::margins(0, innerTop + inner.height() + skip, 0, 0);
_controls->setGeometry(controlsRect);
}, lifetime());
_controls->colorLinePositionValue(
) | rpl::on_next([=](const QPoint &p) {
_colorPicker->moveLine(p);
}, _controls->lifetime());
_controls->colorLineShownValue(
) | rpl::on_next([=](bool shown) {
_colorPicker->setVisible(shown);
}, _controls->lifetime());
_mode.value(
) | rpl::on_next([=](const PhotoEditorMode &mode) {
_content->applyMode(mode);
_controls->applyMode(mode);
}, lifetime());
_controls->rotateRequests(
) | rpl::on_next([=](int angle) {
_modifications.angle += 90;
if (_modifications.angle >= 360) {
_modifications.angle -= 360;
}
_content->applyModifications(_modifications);
}, lifetime());
_controls->flipRequests(
) | rpl::on_next([=] {
_modifications.flipped = !_modifications.flipped;
_content->applyModifications(_modifications);
}, lifetime());
_controls->paintModeRequests(
) | rpl::on_next([=] {
_mode = PhotoEditorMode{
.mode = PhotoEditorMode::Mode::Paint,
.action = PhotoEditorMode::Action::None,
};
}, lifetime());
_controls->doneRequests(
) | rpl::on_next([=] {
const auto mode = _mode.current().mode;
if (mode == PhotoEditorMode::Mode::Paint) {
_mode = PhotoEditorMode{
.mode = PhotoEditorMode::Mode::Transform,
.action = PhotoEditorMode::Action::Save,
};
} else if (mode == PhotoEditorMode::Mode::Transform) {
_mode = PhotoEditorMode{
.mode = PhotoEditorMode::Mode::Out,
.action = PhotoEditorMode::Action::Save,
};
save();
}
}, lifetime());
_controls->cancelRequests(
) | rpl::on_next([=] {
const auto mode = _mode.current().mode;
if (mode == PhotoEditorMode::Mode::Paint) {
_mode = PhotoEditorMode{
.mode = PhotoEditorMode::Mode::Transform,
.action = PhotoEditorMode::Action::Discard,
};
} else if (mode == PhotoEditorMode::Mode::Transform) {
_mode = PhotoEditorMode{
.mode = PhotoEditorMode::Mode::Out,
.action = PhotoEditorMode::Action::Discard,
};
_cancel.fire({});
}
}, lifetime());
_colorPicker->saveBrushRequests(
) | rpl::on_next([=](const Brush &brush) {
_content->applyBrush(brush);
const auto serialized = Serialize(brush);
if (Core::App().settings().photoEditorBrush() != serialized) {
Core::App().settings().setPhotoEditorBrush(serialized);
Core::App().saveSettingsDelayed();
}
}, lifetime());
}
void PhotoEditor::keyPressEvent(QKeyEvent *e) {
if (!_colorPicker->preventHandleKeyPress()) {
_content->handleKeyPress(e) || _controls->handleKeyPress(e);
}
}
void PhotoEditor::save() {
_content->save(_modifications);
_done.fire_copy(_modifications);
}
rpl::producer<PhotoModifications> PhotoEditor::doneRequests() const {
return _done.events();
}
rpl::producer<> PhotoEditor::cancelRequests() const {
return _cancel.events();
}
void InitEditorLayer(
not_null<Ui::LayerWidget*> layer,
not_null<PhotoEditor*> editor,
Fn<void(PhotoModifications)> doneCallback) {
editor->cancelRequests(
) | rpl::on_next([=] {
layer->closeLayer();
}, editor->lifetime());
const auto weak = base::make_weak(layer.get());
editor->doneRequests(
) | rpl::on_next([=, done = std::move(doneCallback)](
const PhotoModifications &mods) {
done(mods);
if (const auto strong = weak.get()) {
strong->closeLayer();
}
}, editor->lifetime());
}
} // namespace Editor

View File

@@ -0,0 +1,81 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/image/image.h"
#include "base/unique_qptr.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_inner_common.h"
namespace Ui {
class LayerWidget;
class Show;
} // namespace Ui
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Window {
class Controller;
} // namespace Window
namespace Editor {
class ColorPicker;
class PhotoEditorContent;
class PhotoEditorControls;
struct Controllers;
class PhotoEditor final : public Ui::RpWidget {
public:
PhotoEditor(
not_null<QWidget*> parent,
not_null<Window::Controller*> controller,
std::shared_ptr<Image> photo,
PhotoModifications modifications,
EditorData data = EditorData());
PhotoEditor(
not_null<QWidget*> parent,
std::shared_ptr<Ui::Show> show,
std::shared_ptr<ChatHelpers::Show> sessionShow,
std::shared_ptr<Image> photo,
PhotoModifications modifications,
EditorData data = EditorData());
void save();
[[nodiscard]] rpl::producer<PhotoModifications> doneRequests() const;
[[nodiscard]] rpl::producer<> cancelRequests() const;
private:
void keyPressEvent(QKeyEvent *e) override;
PhotoModifications _modifications;
const std::shared_ptr<Controllers> _controllers;
base::unique_qptr<PhotoEditorContent> _content;
base::unique_qptr<PhotoEditorControls> _controls;
const std::unique_ptr<ColorPicker> _colorPicker;
rpl::variable<PhotoEditorMode> _mode = PhotoEditorMode{
.mode = PhotoEditorMode::Mode::Transform,
.action = PhotoEditorMode::Action::None,
};
rpl::event_stream<PhotoModifications> _done;
rpl::event_stream<> _cancel;
};
void InitEditorLayer(
not_null<Ui::LayerWidget*> layer,
not_null<PhotoEditor*> editor,
Fn<void(PhotoModifications)> doneCallback);
} // namespace Editor

View File

@@ -0,0 +1,59 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor_common.h"
#include "editor/scene/scene.h"
#include "ui/painter.h"
namespace Editor {
QImage ImageModified(QImage image, const PhotoModifications &mods) {
Expects(!image.isNull());
if (!mods) {
return image;
}
if (mods.paint) {
if (image.format() != QImage::Format_ARGB32_Premultiplied) {
image = image.convertToFormat(
QImage::Format_ARGB32_Premultiplied);
}
Painter p(&image);
PainterHighQualityEnabler hq(p);
mods.paint->render(&p, image.rect());
}
auto cropped = mods.crop.isValid()
? image.copy(mods.crop)
: image;
QTransform transform;
if (mods.flipped) {
transform.scale(-1, 1);
}
if (mods.angle) {
transform.rotate(mods.angle);
}
return cropped.transformed(transform);
}
bool PhotoModifications::empty() const {
return !angle && !flipped && !crop.isValid() && !paint;
}
PhotoModifications::operator bool() const {
return !empty();
}
PhotoModifications::~PhotoModifications() {
if (paint && (paint.use_count() == 1)) {
paint->deleteLater();
}
}
} // namespace Editor

View File

@@ -0,0 +1,44 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Editor {
class Scene;
struct PhotoModifications {
int angle = 0;
bool flipped = false;
QRect crop;
std::shared_ptr<Scene> paint = nullptr;
[[nodiscard]] bool empty() const;
[[nodiscard]] explicit operator bool() const;
~PhotoModifications();
};
struct EditorData {
enum class CropType {
Rect,
Ellipse,
RoundedRect,
};
TextWithEntities about;
QString confirm;
QSize exactSize;
CropType cropType = CropType::Rect;
bool keepAspectRatio = false;
};
[[nodiscard]] QImage ImageModified(
QImage image,
const PhotoModifications &mods);
} // namespace Editor

View File

@@ -0,0 +1,166 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor_content.h"
#include "editor/editor_crop.h"
#include "editor/editor_paint.h"
#include "history/history_drag_area.h"
#include "media/view/media_view_pip.h"
#include "storage/storage_media_prepare.h"
namespace Editor {
using Media::View::FlipSizeByRotation;
using Media::View::RotatedRect;
PhotoEditorContent::PhotoEditorContent(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Image> photo,
PhotoModifications modifications,
std::shared_ptr<Controllers> controllers,
EditorData data)
: RpWidget(parent)
, _photoSize(photo->size())
, _paint(base::make_unique_q<Paint>(
this,
modifications,
_photoSize,
std::move(controllers)))
, _crop(base::make_unique_q<Crop>(
this,
modifications,
_photoSize,
std::move(data)))
, _photo(std::move(photo))
, _modifications(modifications) {
rpl::combine(
_modifications.value(),
sizeValue()
) | rpl::on_next([=](
const PhotoModifications &mods, const QSize &size) {
if (size.isEmpty()) {
return;
}
const auto imageSizeF = [&] {
const auto rotatedSize
= FlipSizeByRotation(size, mods.angle);
const auto m = _crop->cropMargins();
const auto sizeForCrop = rotatedSize
- QSize(m.left() + m.right(), m.top() + m.bottom());
const auto originalSize = QSizeF(_photoSize);
if ((originalSize.width() > sizeForCrop.width())
|| (originalSize.height() > sizeForCrop.height())) {
return originalSize.scaled(
sizeForCrop,
Qt::KeepAspectRatio);
}
return originalSize;
}();
const auto imageSize = QSize(imageSizeF.width(), imageSizeF.height());
_imageRect = QRect(
QPoint(-imageSize.width() / 2, -imageSize.height() / 2),
imageSize);
_imageMatrix.reset();
_imageMatrix.translate(size.width() / 2, size.height() / 2);
if (mods.flipped) {
_imageMatrix.scale(-1, 1);
}
_imageMatrix.rotate(mods.angle);
const auto geometry = _imageMatrix.mapRect(_imageRect);
_crop->applyTransform(
geometry + _crop->cropMargins(),
mods.angle,
mods.flipped, imageSizeF);
_paint->applyTransform(geometry, mods.angle, mods.flipped);
_innerRect = geometry;
}, lifetime());
paintRequest(
) | rpl::on_next([=](const QRect &clip) {
auto p = QPainter(this);
p.fillRect(clip, Qt::transparent);
p.setTransform(_imageMatrix);
p.drawPixmap(_imageRect, _photo->pix(_imageRect.size()));
}, lifetime());
setupDragArea();
}
void PhotoEditorContent::applyModifications(
PhotoModifications modifications) {
_modifications = std::move(modifications);
update();
}
void PhotoEditorContent::save(PhotoModifications &modifications) {
modifications.crop = _crop->saveCropRect();
_paint->keepResult();
const auto savedScene = _paint->saveScene();
if (!modifications.paint) {
modifications.paint = savedScene;
}
}
void PhotoEditorContent::applyMode(const PhotoEditorMode &mode) {
if (mode.mode == PhotoEditorMode::Mode::Out) {
if (mode.action == PhotoEditorMode::Action::Discard) {
_paint->restoreScene();
}
return;
}
const auto isTransform = (mode.mode == PhotoEditorMode::Mode::Transform);
_crop->setVisible(isTransform);
_paint->setAttribute(Qt::WA_TransparentForMouseEvents, isTransform);
if (!isTransform) {
_paint->updateUndoState();
}
if (mode.action == PhotoEditorMode::Action::Discard) {
_paint->cancel();
} else if (mode.action == PhotoEditorMode::Action::Save) {
_paint->keepResult();
}
_mode = mode;
}
void PhotoEditorContent::applyBrush(const Brush &brush) {
_paint->applyBrush(brush);
}
bool PhotoEditorContent::handleKeyPress(not_null<QKeyEvent*> e) const {
return false;
}
void PhotoEditorContent::setupDragArea() {
auto dragEnterFilter = [=](const QMimeData *data) {
return (_mode.mode == PhotoEditorMode::Mode::Paint)
? Storage::ValidatePhotoEditorMediaDragData(data)
: false;
};
const auto areas = DragArea::SetupDragAreaToContainer(
this,
std::move(dragEnterFilter),
nullptr,
nullptr,
[](const QMimeData *d) { return Storage::MimeDataState::Image; },
true);
areas.photo->setDroppedCallback([=](const QMimeData *data) {
_paint->handleMimeData(data);
});
}
} // namespace Editor

View File

@@ -0,0 +1,61 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_inner_common.h"
#include "ui/image/image.h"
namespace Editor {
class Crop;
class Paint;
struct Controllers;
class PhotoEditorContent final : public Ui::RpWidget {
public:
PhotoEditorContent(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Image> photo,
PhotoModifications modifications,
std::shared_ptr<Controllers> controllers,
EditorData data);
void applyModifications(PhotoModifications modifications);
void applyMode(const PhotoEditorMode &mode);
void applyBrush(const Brush &brush);
void save(PhotoModifications &modifications);
bool handleKeyPress(not_null<QKeyEvent*> e) const;
void setupDragArea();
[[nodiscard]] rpl::producer<QRect> innerRect() const {
return _innerRect.value();
}
private:
const QSize _photoSize;
const base::unique_qptr<Paint> _paint;
const base::unique_qptr<Crop> _crop;
const std::shared_ptr<Image> _photo;
rpl::variable<QRect> _innerRect;
rpl::variable<PhotoModifications> _modifications;
rpl::event_stream<int> _keyPresses;
QRect _imageRect;
QTransform _imageMatrix;
PhotoEditorMode _mode;
};
} // namespace Editor

View File

@@ -0,0 +1,548 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor_controls.h"
#include "editor/controllers/controllers.h"
#include "lang/lang_keys.h"
#include "ui/image/image_prepare.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "ui/wrap/fade_wrap.h"
#include "ui/painter.h"
#include "styles/style_editor.h"
namespace Editor {
class EdgeButton final : public Ui::RippleButton {
public:
EdgeButton(
not_null<Ui::RpWidget*> parent,
const QString &text,
int height,
bool left,
const style::color &bg,
const style::color &fg,
const style::RippleAnimation &st);
protected:
QImage prepareRippleMask() const override;
QPoint prepareRippleStartPosition() const override;
private:
void init();
const style::color &_fg;
Ui::Text::String _text;
const int _width;
const QRect _rippleRect;
const QColor _bg;
const bool _left;
QImage rounded(std::optional<QColor> color) const;
};
EdgeButton::EdgeButton(
not_null<Ui::RpWidget*> parent,
const QString &text,
int height,
bool left,
const style::color &bg,
const style::color &fg,
const style::RippleAnimation &st)
: Ui::RippleButton(parent, st)
, _fg(fg)
, _text(st::photoEditorButtonStyle, text)
, _width(_text.maxWidth()
+ st::photoEditorTextButtonPadding.left()
+ st::photoEditorTextButtonPadding.right())
, _rippleRect(QRect(0, 0, _width, height))
, _bg(bg->c)
, _left(left) {
resize(_width, height);
init();
}
void EdgeButton::init() {
// const auto bg = rounded(_bg);
paintRequest(
) | rpl::on_next([=] {
Painter p(this);
// p.drawImage(QPoint(), bg);
paintRipple(p, _rippleRect.x(), _rippleRect.y());
p.setPen(_fg);
const auto textTop = st::photoEditorButtonTextTop;
_text.draw(p, 0, textTop, width(), style::al_center);
}, lifetime());
}
QImage EdgeButton::rounded(std::optional<QColor> color) const {
auto result = QImage(
_rippleRect.size() * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(color.value_or(Qt::white));
const auto parts = RectPart::None
| (_left ? RectPart::TopLeft : RectPart::TopRight)
| (_left ? RectPart::BottomLeft : RectPart::BottomRight);
return Images::Round(std::move(result), ImageRoundRadius::Large, parts);
}
QImage EdgeButton::prepareRippleMask() const {
return rounded(std::nullopt);
}
QPoint EdgeButton::prepareRippleStartPosition() const {
return mapFromGlobal(QCursor::pos()) - _rippleRect.topLeft();
}
class ButtonBar final : public Ui::RpWidget {
public:
ButtonBar(
not_null<Ui::RpWidget*> parent,
const style::color &bg);
private:
QImage _roundedBg;
};
ButtonBar::ButtonBar(
not_null<Ui::RpWidget*> parent,
const style::color &bg)
: RpWidget(parent) {
sizeValue(
) | rpl::on_next([=](const QSize &size) {
const auto children = RpWidget::children();
const auto widgets = ranges::views::all(
children
) | ranges::views::filter([](not_null<const QObject*> object) {
return object->isWidgetType();
}) | ranges::views::transform([](not_null<QObject*> object) {
return static_cast<QWidget*>(object.get());
}) | ranges::to_vector;
if (widgets.size() < 2) {
return;
}
const auto layout = [&](bool symmetrical) {
auto widths = widgets | ranges::views::transform(
&QWidget::width
) | ranges::to_vector;
const auto count = int(widths.size());
const auto middle = count / 2;
if (symmetrical) {
for (auto i = 0; i != middle; ++i) {
const auto j = count - i - 1;
widths[i] = widths[j] = std::max(widths[i], widths[j]);
}
}
const auto residualWidth = size.width()
- ranges::accumulate(widths, 0);
if (symmetrical && residualWidth < 0) {
return false;
}
const auto step = residualWidth / float(count - 1);
auto left = 0.;
auto &&ints = ranges::views::ints(0, ranges::unreachable);
auto &&list = ranges::views::zip(widgets, widths, ints);
for (const auto &[widget, width, index] : list) {
widget->move(int((index >= middle)
? (left + width - widget->width())
: left), 0);
left += width + step;
}
return true;
};
if (!layout(true)) {
layout(false);
}
auto result = QImage(
size * style::DevicePixelRatio(),
QImage::Format_ARGB32_Premultiplied);
result.setDevicePixelRatio(style::DevicePixelRatio());
result.fill(bg->c);
_roundedBg = Images::Round(
std::move(result),
ImageRoundRadius::Large);
}, lifetime());
paintRequest(
) | rpl::on_next([=] {
auto p = QPainter(this);
p.drawImage(QPoint(), _roundedBg);
}, lifetime());
}
PhotoEditorControls::PhotoEditorControls(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Controllers> controllers,
const PhotoModifications modifications,
const EditorData &data)
: RpWidget(parent)
, _bg(st::roundedBg)
, _buttonHeight(st::photoEditorButtonBarHeight)
, _transformButtons(base::make_unique_q<ButtonBar>(this, _bg))
, _paintTopButtons(base::make_unique_q<ButtonBar>(this, _bg))
, _paintBottomButtons(base::make_unique_q<ButtonBar>(this, _bg))
, _about(data.about.empty()
? nullptr
: base::make_unique_q<Ui::FadeWrap<Ui::FlatLabel>>(
this,
object_ptr<Ui::FlatLabel>(
this,
rpl::single(data.about),
st::photoEditorAbout)))
, _transformCancel(base::make_unique_q<EdgeButton>(
_transformButtons,
tr::lng_cancel(tr::now),
_buttonHeight,
true,
_bg,
st::mediaviewCaptionFg,
st::photoEditorRotateButton.ripple))
, _flipButton(base::make_unique_q<Ui::IconButton>(
_transformButtons,
st::photoEditorFlipButton))
, _rotateButton(base::make_unique_q<Ui::IconButton>(
_transformButtons,
st::photoEditorRotateButton))
, _paintModeButton(base::make_unique_q<Ui::IconButton>(
_transformButtons,
st::photoEditorPaintModeButton))
, _transformDone(base::make_unique_q<EdgeButton>(
_transformButtons,
(data.confirm.isEmpty() ? tr::lng_box_done(tr::now) : data.confirm),
_buttonHeight,
false,
_bg,
st::mediaviewTextLinkFg,
st::photoEditorRotateButton.ripple))
, _paintCancel(base::make_unique_q<EdgeButton>(
_paintBottomButtons,
tr::lng_cancel(tr::now),
_buttonHeight,
true,
_bg,
st::mediaviewCaptionFg,
st::photoEditorRotateButton.ripple))
, _undoButton(base::make_unique_q<Ui::IconButton>(
_paintTopButtons,
st::photoEditorUndoButton))
, _redoButton(base::make_unique_q<Ui::IconButton>(
_paintTopButtons,
st::photoEditorRedoButton))
, _paintModeButtonActive(base::make_unique_q<Ui::IconButton>(
_paintBottomButtons,
st::photoEditorPaintModeButton))
, _stickersButton(controllers->stickersPanelController
? base::make_unique_q<Ui::IconButton>(
_paintBottomButtons,
st::photoEditorStickersButton)
: nullptr)
, _paintDone(base::make_unique_q<EdgeButton>(
_paintBottomButtons,
tr::lng_box_done(tr::now),
_buttonHeight,
false,
_bg,
st::mediaviewTextLinkFg,
st::photoEditorRotateButton.ripple)) {
{
const auto icon = &st::photoEditorPaintIconActive;
_paintModeButtonActive->setIconOverride(icon, icon);
}
_paintModeButtonActive->setAttribute(Qt::WA_TransparentForMouseEvents);
sizeValue(
) | rpl::on_next([=](const QSize &size) {
if (size.isEmpty()) {
return;
}
const auto &padding = st::photoEditorButtonBarPadding;
const auto w = std::min(st::photoEditorButtonBarWidth, size.width())
- padding.left()
- padding.right();
_transformButtons->resize(w, _buttonHeight);
_paintBottomButtons->resize(w, _buttonHeight);
_paintTopButtons->resize(w, _buttonHeight);
const auto buttonsTop = bottomButtonsTop();
const auto &current = _transformButtons->isHidden()
? _paintBottomButtons
: _transformButtons;
current->moveToLeft(
(size.width() - current->width()) / 2,
buttonsTop);
if (_about) {
const auto &margin = st::photoEditorAboutMargin;
const auto skip = st::photoEditorCropPointSize;
_about->resizeToWidth(
size.width() - margin.left() - margin.right());
_about->moveToLeft(
(size.width() - _about->width()) / 2,
margin.top() - skip);
}
}, lifetime());
_mode.changes(
) | rpl::on_next([=](const PhotoEditorMode &mode) {
if (mode.mode == PhotoEditorMode::Mode::Out) {
return;
}
const auto animated = (_paintBottomButtons->isVisible()
== _transformButtons->isVisible())
? anim::type::instant
: anim::type::normal;
showAnimated(mode.mode, animated);
}, lifetime());
_paintBottomButtons->positionValue(
) | rpl::on_next([=](const QPoint &containerPos) {
_paintTopButtons->moveToLeft(
containerPos.x(),
containerPos.y()
- st::photoEditorControlsCenterSkip
- _paintTopButtons->height());
}, _paintBottomButtons->lifetime());
_paintBottomButtons->shownValue(
) | rpl::on_next([=](bool shown) {
_paintTopButtons->setVisible(shown);
}, _paintBottomButtons->lifetime());
controllers->undoController->setPerformRequestChanges(rpl::merge(
_undoButton->clicks() | rpl::map_to(Undo::Undo),
_redoButton->clicks() | rpl::map_to(Undo::Redo),
_keyPresses.events(
) | rpl::filter([=](not_null<QKeyEvent*> e) {
using Mode = PhotoEditorMode::Mode;
return (e->matches(QKeySequence::Undo)
&& !_undoButton->isHidden()
&& !_undoButton->testAttribute(
Qt::WA_TransparentForMouseEvents)
&& (_mode.current().mode == Mode::Paint))
|| (e->matches(QKeySequence::Redo)
&& !_redoButton->isHidden()
&& !_redoButton->testAttribute(
Qt::WA_TransparentForMouseEvents)
&& (_mode.current().mode == Mode::Paint));
}) | rpl::map([=](not_null<QKeyEvent*> e) {
return e->matches(QKeySequence::Undo) ? Undo::Undo : Undo::Redo;
})));
controllers->undoController->canPerformChanges(
) | rpl::on_next([=](const UndoController::EnableRequest &r) {
const auto isUndo = (r.command == Undo::Undo);
const auto &button = isUndo ? _undoButton : _redoButton;
button->setAttribute(Qt::WA_TransparentForMouseEvents, !r.enable);
if (!r.enable) {
button->clearState();
}
button->setIconOverride(r.enable
? nullptr
: isUndo
? &st::photoEditorUndoButtonInactive
: &st::photoEditorRedoButtonInactive);
}, lifetime());
if (_stickersButton) {
using ShowRequest = StickersPanelController::ShowRequest;
controllers->stickersPanelController->setShowRequestChanges(
rpl::merge(
_mode.value(
) | rpl::map_to(ShowRequest::HideFast),
_stickersButton->clicks(
) | rpl::map_to(ShowRequest::ToggleAnimated)
));
controllers->stickersPanelController->setMoveRequestChanges(
_paintBottomButtons->positionValue(
) | rpl::map([=](const QPoint &containerPos) {
return QPoint(
(x() + width()) / 2,
y() + containerPos.y() + _stickersButton->y());
}));
controllers->stickersPanelController->panelShown(
) | rpl::on_next([=](bool shown) {
const auto icon = shown
? &st::photoEditorStickersIconActive
: nullptr;
_stickersButton->setIconOverride(icon, icon);
}, _stickersButton->lifetime());
}
rpl::single(rpl::empty) | rpl::skip(
modifications.flipped ? 0 : 1
) | rpl::then(
_flipButton->clicks() | rpl::to_empty
) | rpl::on_next([=] {
_flipped = !_flipped;
const auto icon = _flipped ? &st::photoEditorFlipIconActive : nullptr;
_flipButton->setIconOverride(icon, icon);
}, _flipButton->lifetime());
}
rpl::producer<int> PhotoEditorControls::rotateRequests() const {
return _rotateButton->clicks() | rpl::map_to(90);
}
rpl::producer<> PhotoEditorControls::flipRequests() const {
return _flipButton->clicks() | rpl::to_empty;
}
rpl::producer<> PhotoEditorControls::paintModeRequests() const {
return _paintModeButton->clicks() | rpl::to_empty;
}
rpl::producer<> PhotoEditorControls::doneRequests() const {
return rpl::merge(
_transformDone->clicks() | rpl::to_empty,
_paintDone->clicks() | rpl::to_empty,
_keyPresses.events(
) | rpl::filter([=](not_null<QKeyEvent*> e) {
const auto key = e->key();
return ((key == Qt::Key_Enter) || (key == Qt::Key_Return))
&& !_toggledBarAnimation.animating();
}) | rpl::to_empty);
}
rpl::producer<> PhotoEditorControls::cancelRequests() const {
return rpl::merge(
_transformCancel->clicks() | rpl::to_empty,
_paintCancel->clicks() | rpl::to_empty,
_keyPresses.events(
) | rpl::filter([=](not_null<QKeyEvent*> e) {
const auto key = e->key();
return (key == Qt::Key_Escape)
&& !_toggledBarAnimation.animating();
}) | rpl::to_empty);
}
int PhotoEditorControls::bottomButtonsTop() const {
return height()
- st::photoEditorControlsBottomSkip
- _transformButtons->height();
}
void PhotoEditorControls::showAnimated(
PhotoEditorMode::Mode mode,
anim::type animated) {
using Mode = PhotoEditorMode::Mode;
const auto duration = st::photoEditorBarAnimationDuration;
const auto isTransform = (mode == Mode::Transform);
if (_about) {
_about->toggle(isTransform, animated);
}
const auto buttonsLeft = (width() - _transformButtons->width()) / 2;
const auto buttonsTop = bottomButtonsTop();
const auto visibleBar = _transformButtons->isVisible()
? _transformButtons.get()
: _paintBottomButtons.get();
const auto shouldVisibleBar = isTransform
? _transformButtons.get()
: _paintBottomButtons.get(); // Mode::Paint
const auto computeTop = [=](float64 progress) {
return anim::interpolate(buttonsTop, height() * 2, progress);
};
const auto showShouldVisibleBar = [=] {
_toggledBarAnimation.stop();
auto callback = [=](float64 value) {
shouldVisibleBar->moveToLeft(buttonsLeft, computeTop(value));
};
if (animated == anim::type::instant) {
callback(1.);
} else {
_toggledBarAnimation.start(
std::move(callback),
1.,
0.,
duration);
}
};
auto animationCallback = [=](float64 value) {
if (shouldVisibleBar == visibleBar) {
showShouldVisibleBar();
return;
}
visibleBar->moveToLeft(buttonsLeft, computeTop(value));
if (value == 1.) {
shouldVisibleBar->show();
shouldVisibleBar->moveToLeft(buttonsLeft, computeTop(1.));
visibleBar->hide();
showShouldVisibleBar();
}
};
if (animated == anim::type::instant) {
animationCallback(1.);
} else {
_toggledBarAnimation.start(
std::move(animationCallback),
0.,
1.,
duration);
}
}
void PhotoEditorControls::applyMode(const PhotoEditorMode &mode) {
_mode = mode;
}
rpl::producer<QPoint> PhotoEditorControls::colorLinePositionValue() const {
return rpl::merge(
geometryValue() | rpl::to_empty,
_paintTopButtons->geometryValue() | rpl::to_empty
) | rpl::map([=] {
const auto r = _paintTopButtons->geometry();
return mapToParent(r.topLeft())
+ QPoint(r.width() / 2, r.height() / 2);
});
}
rpl::producer<bool> PhotoEditorControls::colorLineShownValue() const {
return _paintTopButtons->shownValue();
}
bool PhotoEditorControls::handleKeyPress(not_null<QKeyEvent*> e) const {
_keyPresses.fire(std::move(e));
return true;
}
bool PhotoEditorControls::animating() const {
return _toggledBarAnimation.animating();
}
} // namespace Editor

View File

@@ -0,0 +1,89 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/rp_widget.h"
#include "ui/effects/animations.h"
#include "editor/photo_editor_common.h"
#include "editor/photo_editor_inner_common.h"
namespace Ui {
class IconButton;
class FlatLabel;
template <typename Widget>
class FadeWrap;
} // namespace Ui
namespace Editor {
class EdgeButton;
class ButtonBar;
struct Controllers;
struct EditorData;
class PhotoEditorControls final : public Ui::RpWidget {
public:
PhotoEditorControls(
not_null<Ui::RpWidget*> parent,
std::shared_ptr<Controllers> controllers,
const PhotoModifications modifications,
const EditorData &data);
[[nodiscard]] rpl::producer<int> rotateRequests() const;
[[nodiscard]] rpl::producer<> flipRequests() const;
[[nodiscard]] rpl::producer<> paintModeRequests() const;
[[nodiscard]] rpl::producer<> doneRequests() const;
[[nodiscard]] rpl::producer<> cancelRequests() const;
[[nodiscard]] rpl::producer<QPoint> colorLinePositionValue() const;
[[nodiscard]] rpl::producer<bool> colorLineShownValue() const;
[[nodiscard]] bool animating() const;
bool handleKeyPress(not_null<QKeyEvent*> e) const;
void applyMode(const PhotoEditorMode &mode);
private:
void showAnimated(
PhotoEditorMode::Mode mode,
anim::type animated = anim::type::normal);
int bottomButtonsTop() const;
const style::color &_bg;
const int _buttonHeight;
const base::unique_qptr<ButtonBar> _transformButtons;
const base::unique_qptr<ButtonBar> _paintTopButtons;
const base::unique_qptr<ButtonBar> _paintBottomButtons;
const base::unique_qptr<Ui::FadeWrap<Ui::FlatLabel>> _about;
const base::unique_qptr<EdgeButton> _transformCancel;
const base::unique_qptr<Ui::IconButton> _flipButton;
const base::unique_qptr<Ui::IconButton> _rotateButton;
const base::unique_qptr<Ui::IconButton> _paintModeButton;
const base::unique_qptr<EdgeButton> _transformDone;
const base::unique_qptr<EdgeButton> _paintCancel;
const base::unique_qptr<Ui::IconButton> _undoButton;
const base::unique_qptr<Ui::IconButton> _redoButton;
const base::unique_qptr<Ui::IconButton> _paintModeButtonActive;
const base::unique_qptr<Ui::IconButton> _stickersButton;
const base::unique_qptr<EdgeButton> _paintDone;
bool _flipped = false;
Ui::Animations::Simple _toggledBarAnimation;
rpl::variable<PhotoEditorMode> _mode;
rpl::event_stream<not_null<QKeyEvent*>> _keyPresses;
};
} // namespace Editor

View File

@@ -0,0 +1,38 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
namespace Editor {
class Scene;
struct PhotoEditorMode {
enum class Mode {
Transform,
Paint,
Out,
} mode = Mode::Transform;
enum class Action {
None,
Save,
Discard,
} action = Action::None;
};
struct Brush {
float sizeRatio = 0.;
QColor color;
};
enum class SaveState {
Save,
Keep,
};
} // namespace Editor

View File

@@ -0,0 +1,171 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/photo_editor_layer_widget.h"
#include "lang/lang_keys.h"
#include "ui/boxes/confirm_box.h" // InformBox
#include "editor/editor_layer_widget.h"
#include "editor/photo_editor.h"
#include "storage/localimageloader.h"
#include "storage/storage_media_prepare.h"
#include "ui/chat/attach/attach_prepare.h"
#include "window/window_controller.h"
#include "window/window_session_controller.h"
#include <QtGui/QGuiApplication>
namespace Editor {
void OpenWithPreparedFile(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<Ui::PreparedFile*> file,
int previewWidth,
Fn<void(bool ok)> &&doneCallback,
QSize exactSize) {
using ImageInfo = Ui::PreparedFileInformation::Image;
const auto image = std::get_if<ImageInfo>(&file->information->media);
if (!image) {
doneCallback(false);
return;
}
const auto photoType = (file->type == Ui::PreparedFile::Type::Photo);
const auto modifiedFileType = (file->type == Ui::PreparedFile::Type::File)
&& !image->modifications.empty();
if (!photoType && !modifiedFileType) {
doneCallback(false);
return;
}
const auto sideLimit = PhotoSideLimit();
const auto accepted = std::make_shared<bool>();
auto callback = [=](const PhotoModifications &mods) {
*accepted = true;
image->modifications = mods;
Storage::UpdateImageDetails(*file, previewWidth, sideLimit);
{
using namespace Ui;
const auto size = file->preview.size();
file->type = ValidateThumbDimensions(size.width(), size.height())
? PreparedFile::Type::Photo
: PreparedFile::Type::File;
}
doneCallback(true);
};
auto copy = image->data;
const auto fileImage = std::make_shared<Image>(std::move(copy));
const auto keepRatio = !exactSize.isEmpty();
auto editor = base::make_unique_q<PhotoEditor>(
parent,
show,
show,
fileImage,
image->modifications,
EditorData{ .exactSize = exactSize, .keepAspectRatio = keepRatio });
const auto raw = editor.get();
auto layer = std::make_unique<LayerWidget>(parent, std::move(editor));
InitEditorLayer(layer.get(), raw, std::move(callback));
QObject::connect(layer.get(), &QObject::destroyed, [=] {
if (!*accepted) {
doneCallback(false);
}
});
show->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
}
void PrepareProfilePhoto(
not_null<QWidget*> parent,
not_null<Window::Controller*> controller,
EditorData data,
Fn<void(QImage &&image)> &&doneCallback,
QImage &&image) {
const auto resizeToMinSize = [=](
QImage &&image,
Qt::AspectRatioMode mode) {
const auto &minSize = kProfilePhotoSize;
if ((image.width() < minSize) || (image.height() < minSize)) {
return image.scaled(
minSize,
minSize,
mode,
Qt::SmoothTransformation);
}
return std::move(image);
};
if (image.isNull()
|| (image.width() > (10 * image.height()))
|| (image.height() > (10 * image.width()))) {
controller->show(Ui::MakeInformBox(tr::lng_bad_photo()));
return;
}
image = resizeToMinSize(
std::move(image),
Qt::KeepAspectRatioByExpanding);
const auto fileImage = std::make_shared<Image>(std::move(image));
auto applyModifications = [=, done = std::move(doneCallback)](
const PhotoModifications &mods) {
done(resizeToMinSize(
ImageModified(fileImage->original(), mods),
Qt::KeepAspectRatio));
};
auto crop = [&] {
const auto &i = fileImage;
const auto minSide = std::min(i->width(), i->height());
return QRect(
(i->width() - minSide) / 2,
(i->height() - minSide) / 2,
minSide,
minSide);
}();
auto editor = base::make_unique_q<PhotoEditor>(
parent,
controller,
fileImage,
PhotoModifications{ .crop = std::move(crop) },
data);
const auto raw = editor.get();
auto layer = std::make_unique<LayerWidget>(parent, std::move(editor));
InitEditorLayer(layer.get(), raw, std::move(applyModifications));
controller->showLayer(std::move(layer), Ui::LayerOption::KeepOther);
}
void PrepareProfilePhotoFromFile(
not_null<QWidget*> parent,
not_null<Window::Controller*> controller,
EditorData data,
Fn<void(QImage &&image)> &&doneCallback) {
const auto callback = [=, done = std::move(doneCallback)](
const FileDialog::OpenResult &result) mutable {
if (result.paths.isEmpty() && result.remoteContent.isEmpty()) {
return;
}
auto image = Images::Read({
.path = result.paths.isEmpty() ? QString() : result.paths.front(),
.content = result.remoteContent,
.forceOpaque = true,
}).image;
PrepareProfilePhoto(
parent,
controller,
data,
std::move(done),
std::move(image));
};
FileDialog::GetOpenPath(
parent.get(),
tr::lng_choose_image(tr::now),
FileDialog::ImagesOrAllFilter(),
crl::guard(parent, callback));
}
} // namespace Editor

View File

@@ -0,0 +1,53 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
enum class ImageRoundRadius;
namespace ChatHelpers {
class Show;
} // namespace ChatHelpers
namespace Ui {
class RpWidget;
struct PreparedFile;
} // namespace Ui
namespace Window {
class Controller;
class SessionController;
} // namespace Window
namespace Editor {
constexpr auto kProfilePhotoSize = int(640);
struct EditorData;
void OpenWithPreparedFile(
not_null<QWidget*> parent,
std::shared_ptr<ChatHelpers::Show> show,
not_null<Ui::PreparedFile*> file,
int previewWidth,
Fn<void(bool ok)> &&doneCallback,
QSize exactSize = {});
void PrepareProfilePhoto(
not_null<QWidget*> parent,
not_null<Window::Controller*> controller,
EditorData data,
Fn<void(QImage &&image)> &&doneCallback,
QImage &&image);
void PrepareProfilePhotoFromFile(
not_null<QWidget*> parent,
not_null<Window::Controller*> controller,
EditorData data,
Fn<void(QImage &&image)> &&doneCallback);
} // namespace Editor

View File

@@ -0,0 +1,220 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene.h"
#include "editor/scene/scene_item_canvas.h"
#include "editor/scene/scene_item_line.h"
#include "editor/scene/scene_item_sticker.h"
#include "ui/rp_widget.h"
#include <QGraphicsSceneMouseEvent>
namespace Editor {
namespace {
using ItemPtr = std::shared_ptr<NumberedItem>;
bool SkipMouseEvent(not_null<QGraphicsSceneMouseEvent*> event) {
return event->isAccepted() || (event->button() == Qt::RightButton);
}
} // namespace
Scene::Scene(const QRectF &rect)
: QGraphicsScene(rect)
, _canvas(std::make_shared<ItemCanvas>())
, _lastZ(std::make_shared<float64>(9000.)) {
QGraphicsScene::addItem(_canvas.get());
_canvas->clearPixmap();
_canvas->grabContentRequests(
) | rpl::on_next([=](ItemCanvas::Content &&content) {
const auto item = std::make_shared<ItemLine>(
std::move(content.pixmap));
item->setPos(content.position);
addItem(item);
_canvas->setZValue(++_lastLineZ);
}, _lifetime);
}
void Scene::cancelDrawing() {
_canvas->cancelDrawing();
}
void Scene::addItem(ItemPtr item) {
if (!item) {
return;
}
item->setNumber(_itemNumber++);
QGraphicsScene::addItem(item.get());
_items.push_back(std::move(item));
_addsItem.fire({});
}
void Scene::removeItem(not_null<QGraphicsItem*> item) {
const auto it = ranges::find_if(_items, [&](const ItemPtr &i) {
return i.get() == item;
});
if (it == end(_items)) {
return;
}
removeItem(*it);
}
void Scene::removeItem(const ItemPtr &item) {
item->setStatus(NumberedItem::Status::Removed);
_removesItem.fire({});
}
void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event) {
QGraphicsScene::mousePressEvent(event);
if (SkipMouseEvent(event)) {
return;
}
_canvas->handleMousePressEvent(event);
}
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
QGraphicsScene::mouseReleaseEvent(event);
if (SkipMouseEvent(event)) {
return;
}
_canvas->handleMouseReleaseEvent(event);
}
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
QGraphicsScene::mouseMoveEvent(event);
if (SkipMouseEvent(event)) {
return;
}
_canvas->handleMouseMoveEvent(event);
}
void Scene::applyBrush(const QColor &color, float size) {
_canvas->applyBrush(color, size);
}
rpl::producer<> Scene::addsItem() const {
return _addsItem.events();
}
rpl::producer<> Scene::removesItem() const {
return _removesItem.events();
}
std::vector<ItemPtr> Scene::items(
Qt::SortOrder order) const {
auto copyItems = _items;
ranges::sort(copyItems, [&](ItemPtr a, ItemPtr b) {
const auto numA = a->number();
const auto numB = b->number();
return (order == Qt::AscendingOrder) ? (numA < numB) : (numA > numB);
});
return copyItems;
}
std::shared_ptr<float64> Scene::lastZ() const {
return _lastZ;
}
void Scene::updateZoom(float64 zoom) {
_canvas->updateZoom(zoom);
for (const auto &item : items()) {
if (item->type() >= ItemBase::Type) {
static_cast<ItemBase*>(item.get())->updateZoom(zoom);
}
}
}
bool Scene::hasUndo() const {
return ranges::any_of(_items, &NumberedItem::isNormalStatus);
}
bool Scene::hasRedo() const {
return ranges::any_of(_items, &NumberedItem::isUndidStatus);
}
void Scene::performUndo() {
const auto filtered = items(Qt::DescendingOrder);
const auto it = ranges::find_if(filtered, &NumberedItem::isNormalStatus);
if (it != filtered.end()) {
(*it)->setStatus(NumberedItem::Status::Undid);
}
}
void Scene::performRedo() {
const auto filtered = items(Qt::AscendingOrder);
const auto it = ranges::find_if(filtered, &NumberedItem::isUndidStatus);
if (it != filtered.end()) {
(*it)->setStatus(NumberedItem::Status::Normal);
}
}
void Scene::removeIf(Fn<bool(const ItemPtr &)> proj) {
auto copy = std::vector<ItemPtr>();
for (const auto &item : _items) {
const auto toRemove = proj(item);
if (toRemove) {
// Scene loses ownership of an item.
// It seems for some reason this line causes a crash. =(
// QGraphicsScene::removeItem(item.get());
} else {
copy.push_back(item);
}
}
_items = std::move(copy);
}
void Scene::clearRedoList() {
for (const auto &item : _items) {
if (item->isUndidStatus()) {
item->setStatus(NumberedItem::Status::Removed);
}
}
}
void Scene::save(SaveState state) {
removeIf([](const ItemPtr &item) {
return item->isRemovedStatus()
&& !item->hasState(SaveState::Keep)
&& !item->hasState(SaveState::Save);
});
for (const auto &item : _items) {
item->save(state);
}
clearSelection();
cancelDrawing();
}
void Scene::restore(SaveState state) {
removeIf([=](const ItemPtr &item) {
return !item->hasState(state);
});
for (const auto &item : _items) {
item->restore(state);
}
clearSelection();
cancelDrawing();
}
Scene::~Scene() {
// Prevent destroying by scene of all items.
QGraphicsScene::removeItem(_canvas.get());
for (const auto &item : items()) {
// Scene loses ownership of an item.
QGraphicsScene::removeItem(item.get());
}
}
} // namespace Editor

View File

@@ -0,0 +1,77 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include <base/unique_qptr.h>
#include <editor/photo_editor_inner_common.h>
#include <QGraphicsScene>
class QGraphicsSceneMouseEvent;
namespace Ui {
class RpWidget;
} // namespace Ui
namespace Editor {
class ItemCanvas;
class NumberedItem;
class Scene final : public QGraphicsScene {
public:
using ItemPtr = std::shared_ptr<NumberedItem>;
Scene(const QRectF &rect);
~Scene();
void applyBrush(const QColor &color, float size);
[[nodiscard]] std::vector<ItemPtr> items(
Qt::SortOrder order = Qt::DescendingOrder) const;
void addItem(ItemPtr item);
void removeItem(not_null<QGraphicsItem*> item);
void removeItem(const ItemPtr &item);
[[nodiscard]] rpl::producer<> addsItem() const;
[[nodiscard]] rpl::producer<> removesItem() const;
[[nodiscard]] std::shared_ptr<float64> lastZ() const;
void updateZoom(float64 zoom);
void cancelDrawing();
[[nodiscard]] bool hasUndo() const;
[[nodiscard]] bool hasRedo() const;
void performUndo();
void performRedo();
void save(SaveState state);
void restore(SaveState state);
void clearRedoList();
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
private:
void removeIf(Fn<bool(const ItemPtr &)> proj);
const std::shared_ptr<ItemCanvas> _canvas;
const std::shared_ptr<float64> _lastZ;
std::vector<ItemPtr> _items;
float64 _lastLineZ = 0.;
int _itemNumber = 0;
rpl::event_stream<> _addsItem, _removesItem;
rpl::lifetime _lifetime;
};
} // namespace Editor

View File

@@ -0,0 +1,456 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_base.h"
#include "editor/scene/scene.h"
#include "lang/lang_keys.h"
#include "ui/widgets/popup_menu.h"
#include "ui/painter.h"
#include "styles/style_editor.h"
#include "styles/style_menu_icons.h"
#include <QGraphicsScene>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneMouseEvent>
#include <QStyleOptionGraphicsItem>
#include <QtMath>
namespace Editor {
namespace {
constexpr auto kSnapAngle = 45.;
const auto kDuplicateSequence = QKeySequence("ctrl+d");
const auto kFlipSequence = QKeySequence("ctrl+s");
const auto kDeleteSequence = QKeySequence("delete");
constexpr auto kMinSizeRatio = 0.05;
constexpr auto kMaxSizeRatio = 1.00;
auto Normalized(float64 angle) {
return angle
+ ((std::abs(angle) < 360) ? 0 : (-360 * (angle < 0 ? -1 : 1)));
}
} // namespace
int NumberedItem::type() const {
return NumberedItem::Type;
}
int NumberedItem::number() const {
return _number;
}
void NumberedItem::setNumber(int number) {
_number = number;
}
NumberedItem::Status NumberedItem::status() const {
return _status;
}
bool NumberedItem::isNormalStatus() const {
return _status == Status::Normal;
}
bool NumberedItem::isUndidStatus() const {
return _status == Status::Undid;
}
bool NumberedItem::isRemovedStatus() const {
return _status == Status::Removed;
}
void NumberedItem::save(SaveState state) {
}
void NumberedItem::restore(SaveState state) {
}
bool NumberedItem::hasState(SaveState state) const {
return false;
}
void NumberedItem::setStatus(Status status) {
if (status != _status) {
_status = status;
setVisible(status == Status::Normal);
}
}
ItemBase::ItemBase(Data data)
: _lastZ(data.zPtr)
, _imageSize(data.imageSize)
, _horizontalSize(data.size) {
setFlags(QGraphicsItem::ItemIsMovable
| QGraphicsItem::ItemIsSelectable
| QGraphicsItem::ItemIsFocusable);
setAcceptHoverEvents(true);
applyData(data);
}
QRectF ItemBase::boundingRect() const {
return innerRect() + _scaledInnerMargins;
}
QRectF ItemBase::contentRect() const {
return innerRect() - _scaledInnerMargins;
}
QRectF ItemBase::innerRect() const {
const auto &hSize = _horizontalSize;
const auto &vSize = _verticalSize;
return QRectF(-hSize / 2, -vSize / 2, hSize, vSize);
}
void ItemBase::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *) {
if (!(option->state & QStyle::State_Selected)) {
return;
}
PainterHighQualityEnabler hq(*p);
const auto hasFocus = (option->state & QStyle::State_HasFocus);
p->setPen(hasFocus ? _pens.select : _pens.selectInactive);
p->drawRect(innerRect());
p->setPen(hasFocus ? _pens.handle : _pens.handleInactive);
p->setBrush(st::photoEditorItemBaseHandleFg);
p->drawEllipse(rightHandleRect());
p->drawEllipse(leftHandleRect());
}
void ItemBase::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
if (isHandling()) {
const auto mousePos = event->pos();
const auto shift = event->modifiers().testFlag(Qt::ShiftModifier);
const auto isLeft = (_handle == HandleType::Left);
if (!shift) {
// Resize.
const auto p = isLeft ? (mousePos * -1) : mousePos;
const auto dx = int(2.0 * p.x());
const auto dy = int(2.0 * p.y());
prepareGeometryChange();
_horizontalSize = std::clamp(
(dx > dy ? dx : dy),
_sizeLimits.min,
_sizeLimits.max);
updateVerticalSize();
}
// Rotate.
const auto origin = mapToScene(boundingRect().center());
const auto pos = mapToScene(mousePos);
const auto diff = pos - origin;
const auto angle = Normalized((isLeft ? 180 : 0)
+ (std::atan2(diff.y(), diff.x()) * 180 / M_PI));
setRotation(shift
? (base::SafeRound(angle / kSnapAngle) * kSnapAngle)
: angle);
} else {
QGraphicsItem::mouseMoveEvent(event);
}
}
void ItemBase::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
setCursor(isHandling()
? Qt::ClosedHandCursor
: (handleType(event->pos()) != HandleType::None) && isSelected()
? Qt::OpenHandCursor
: Qt::ArrowCursor);
QGraphicsItem::hoverMoveEvent(event);
}
void ItemBase::mousePressEvent(QGraphicsSceneMouseEvent *event) {
setZValue((*_lastZ)++);
if (event->button() == Qt::LeftButton) {
_handle = handleType(event->pos());
}
if (isHandling()) {
setCursor(Qt::ClosedHandCursor);
} else {
QGraphicsItem::mousePressEvent(event);
}
}
void ItemBase::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
if ((event->button() == Qt::LeftButton) && isHandling()) {
_handle = HandleType::None;
} else {
QGraphicsItem::mouseReleaseEvent(event);
}
}
void ItemBase::contextMenuEvent(QGraphicsSceneContextMenuEvent *event) {
if (scene()) {
scene()->clearSelection();
setSelected(true);
}
const auto add = [&](
auto base,
const QKeySequence &sequence,
Fn<void()> callback,
const style::icon *icon) {
// TODO: refactor.
const auto sequenceText = QChar('\t')
+ sequence.toString(QKeySequence::NativeText);
_menu->addAction(
base(tr::now) + sequenceText,
std::move(callback),
icon);
};
_menu = base::make_unique_q<Ui::PopupMenu>(
nullptr,
st::popupMenuWithIcons);
add(
tr::lng_photo_editor_menu_delete,
kDeleteSequence,
[=] { actionDelete(); },
&st::menuIconDelete);
add(
tr::lng_photo_editor_menu_flip,
kFlipSequence,
[=] { actionFlip(); },
&st::menuIconFlip);
add(
tr::lng_photo_editor_menu_duplicate,
kDuplicateSequence,
[=] { actionDuplicate(); },
&st::menuIconCopy);
_menu->popup(event->screenPos());
}
void ItemBase::performForSelectedItems(Action action) {
if (const auto s = scene()) {
for (const auto item : s->selectedItems()) {
if (const auto base = static_cast<ItemBase*>(item)) {
(base->*action)();
}
}
}
}
void ItemBase::actionFlip() {
setFlip(!flipped());
}
void ItemBase::actionDelete() {
if (const auto s = static_cast<Scene*>(scene())) {
s->removeItem(this);
}
}
void ItemBase::actionDuplicate() {
if (const auto s = static_cast<Scene*>(scene())) {
auto data = generateData();
data.x += int(_horizontalSize / 3);
data.y += int(_verticalSize / 3);
const auto newItem = duplicate(std::move(data));
if (hasFocus()) {
newItem->setFocus();
}
const auto selected = isSelected();
newItem->setSelected(selected);
setSelected(false);
s->addItem(newItem);
}
}
void ItemBase::keyPressEvent(QKeyEvent *e) {
if (e->key() == Qt::Key_Escape) {
if (const auto s = scene()) {
s->clearSelection();
s->clearFocus();
return;
}
}
handleActionKey(e);
}
void ItemBase::handleActionKey(not_null<QKeyEvent*> e) {
const auto matches = [&](const QKeySequence &sequence) {
const auto searchKey = (e->modifiers() | e->key())
& ~(Qt::KeypadModifier | Qt::GroupSwitchModifier);
const auto events = QKeySequence(searchKey);
return sequence.matches(events) == QKeySequence::ExactMatch;
};
if (matches(kDuplicateSequence)) {
performForSelectedItems(&ItemBase::actionDuplicate);
} else if (matches(kDeleteSequence)) {
performForSelectedItems(&ItemBase::actionDelete);
} else if (matches(kFlipSequence)) {
performForSelectedItems(&ItemBase::actionFlip);
}
}
QRectF ItemBase::rightHandleRect() const {
return QRectF(
(_horizontalSize / 2) - (_scaledHandleSize / 2),
0 - (_scaledHandleSize / 2),
_scaledHandleSize,
_scaledHandleSize);
}
QRectF ItemBase::leftHandleRect() const {
return QRectF(
(-_horizontalSize / 2) - (_scaledHandleSize / 2),
0 - (_scaledHandleSize / 2),
_scaledHandleSize,
_scaledHandleSize);
}
bool ItemBase::isHandling() const {
return _handle != HandleType::None;
}
float64 ItemBase::size() const {
return _horizontalSize;
}
void ItemBase::updateVerticalSize() {
const auto verticalSize = _horizontalSize * _aspectRatio;
_verticalSize = std::max(
verticalSize,
float64(_sizeLimits.min));
if (verticalSize < _sizeLimits.min) {
_horizontalSize = _verticalSize / _aspectRatio;
}
}
void ItemBase::setAspectRatio(float64 aspectRatio) {
_aspectRatio = aspectRatio;
updateVerticalSize();
}
ItemBase::HandleType ItemBase::handleType(const QPointF &pos) const {
return rightHandleRect().contains(pos)
? HandleType::Right
: leftHandleRect().contains(pos)
? HandleType::Left
: HandleType::None;
}
bool ItemBase::flipped() const {
return _flipped;
}
void ItemBase::setFlip(bool value) {
if (_flipped != value) {
performFlip();
_flipped = value;
}
}
int ItemBase::type() const {
return ItemBase::Type;
}
void ItemBase::updateZoom(float64 zoom) {
_scaledHandleSize = st::photoEditorItemHandleSize / zoom;
_scaledInnerMargins = QMarginsF(
_scaledHandleSize,
_scaledHandleSize,
_scaledHandleSize,
_scaledHandleSize) * 0.5;
const auto maxSide = std::max(
_imageSize.width(),
_imageSize.height());
_sizeLimits = {
.min = int(maxSide * kMinSizeRatio),
.max = int(maxSide * kMaxSizeRatio),
};
_horizontalSize = std::clamp(
_horizontalSize,
float64(_sizeLimits.min),
float64(_sizeLimits.max));
updateVerticalSize();
updatePens(QPen(
QBrush(),
1 / zoom,
Qt::DashLine,
Qt::SquareCap,
Qt::RoundJoin));
}
void ItemBase::performFlip() {
}
void ItemBase::updatePens(QPen pen) {
_pens = {
.select = pen,
.selectInactive = pen,
.handle = pen,
.handleInactive = pen,
};
_pens.select.setColor(Qt::white);
_pens.selectInactive.setColor(Qt::gray);
_pens.handle.setColor(Qt::white);
_pens.handleInactive.setColor(Qt::gray);
_pens.handle.setStyle(Qt::SolidLine);
_pens.handleInactive.setStyle(Qt::SolidLine);
}
ItemBase::Data ItemBase::generateData() const {
return {
.initialZoom = (st::photoEditorItemHandleSize / _scaledHandleSize),
.zPtr = _lastZ,
.size = int(_horizontalSize),
.x = int(scenePos().x()),
.y = int(scenePos().y()),
.flipped = flipped(),
.rotation = int(rotation()),
.imageSize = _imageSize,
};
}
void ItemBase::applyData(const Data &data) {
// _lastZ is const.
// _imageSize is const.
_horizontalSize = data.size;
setPos(data.x, data.y);
setZValue((*_lastZ)++);
setFlip(data.flipped);
setRotation(data.rotation);
updateZoom(data.initialZoom);
update();
}
void ItemBase::save(SaveState state) {
const auto z = zValue();
auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
saved = {
.data = generateData(),
.zValue = z,
.status = status(),
};
}
void ItemBase::restore(SaveState state) {
if (!hasState(state)) {
return;
}
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
applyData(saved.data);
setZValue(saved.zValue);
setStatus(saved.status);
}
bool ItemBase::hasState(SaveState state) const {
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
return saved.zValue;
}
} // namespace Editor

View File

@@ -0,0 +1,159 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "base/unique_qptr.h"
#include "editor/photo_editor_inner_common.h"
#include <QGraphicsItem>
class QGraphicsSceneHoverEvent;
class QGraphicsSceneMouseEvent;
class QStyleOptionGraphicsItem;
namespace Ui {
class PopupMenu;
} // namespace Ui
namespace Editor {
class NumberedItem : public QGraphicsItem {
public:
enum class Status {
Normal,
Undid,
Removed,
};
enum { Type = UserType + 1 };
using QGraphicsItem::QGraphicsItem;
int type() const override;
void setNumber(int number);
[[nodiscard]] int number() const;
[[nodiscard]] Status status() const;
void setStatus(Status status);
[[nodiscard]] bool isNormalStatus() const;
[[nodiscard]] bool isUndidStatus() const;
[[nodiscard]] bool isRemovedStatus() const;
virtual void save(SaveState state);
virtual void restore(SaveState state);
virtual bool hasState(SaveState state) const;
private:
int _number = 0;
Status _status = Status::Normal;
};
class ItemBase : public NumberedItem {
public:
enum { Type = UserType + 2 };
struct Data {
float64 initialZoom = 0.;
std::shared_ptr<float64> zPtr;
int size = 0;
int x = 0;
int y = 0;
bool flipped = false;
int rotation = 0;
QSize imageSize;
};
ItemBase(Data data);
QRectF boundingRect() const override;
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
int type() const override;
bool flipped() const;
void setFlip(bool value);
void updateZoom(float64 zoom);
bool hasState(SaveState state) const override;
void save(SaveState state) override;
void restore(SaveState state) override;
protected:
enum HandleType {
None,
Left,
Right,
};
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override;
void hoverMoveEvent(QGraphicsSceneHoverEvent *event) override;
void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;
void contextMenuEvent(QGraphicsSceneContextMenuEvent *event) override;
void keyPressEvent(QKeyEvent *e) override;
using Action = void(ItemBase::*)();
void performForSelectedItems(Action action);
void actionFlip();
void actionDelete();
void actionDuplicate();
QRectF contentRect() const;
QRectF innerRect() const;
float64 size() const;
float64 horizontalSize() const;
float64 verticalSize() const;
void setAspectRatio(float64 aspectRatio);
virtual void performFlip();
virtual std::shared_ptr<ItemBase> duplicate(Data data) const = 0;
private:
HandleType handleType(const QPointF &pos) const;
QRectF rightHandleRect() const;
QRectF leftHandleRect() const;
bool isHandling() const;
void updateVerticalSize();
void updatePens(QPen pen);
void handleActionKey(not_null<QKeyEvent*> e);
Data generateData() const;
void applyData(const Data &data);
const std::shared_ptr<float64> _lastZ;
const QSize _imageSize;
struct {
QPen select;
QPen selectInactive;
QPen handle;
QPen handleInactive;
} _pens;
base::unique_qptr<Ui::PopupMenu> _menu;
struct {
Data data;
float64 zValue = 0.;
NumberedItem::Status status;
} _saved, _keeped;
struct {
int min = 0;
int max = 0;
} _sizeLimits;
float64 _scaledHandleSize = 1.0;
QMarginsF _scaledInnerMargins;
float64 _horizontalSize = 0;
float64 _verticalSize = 0;
float64 _aspectRatio = 1.0;
HandleType _handle = HandleType::None;
bool _flipped = false;
};
} // namespace Editor

View File

@@ -0,0 +1,324 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_canvas.h"
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
namespace Editor {
namespace {
constexpr auto kMinPointDistanceBase = 2.0;
constexpr auto kMaxPointDistance = 15.0;
constexpr auto kSmoothingStrength = 0.5;
constexpr auto kPressureDecay = 0.95;
constexpr auto kMinPressure = 0.3;
constexpr auto kBatchUpdateInterval = 16;
constexpr auto kSegmentOverlap = 3;
constexpr auto kHalfStrength = kSmoothingStrength / 2.0;
constexpr auto kInvStrength = 1.0 - kSmoothingStrength;
[[nodiscard]] float64 PointDistance(const QPointF &a, const QPointF &b) {
const auto dx = a.x() - b.x();
const auto dy = a.y() - b.y();
return std::sqrt(dx * dx + dy * dy);
}
} // namespace
ItemCanvas::ItemCanvas() {
setAcceptedMouseButtons({});
}
void ItemCanvas::clearPixmap() {
_hq = nullptr;
_p = nullptr;
_pixmap = QPixmap(
(scene()->sceneRect().size() * style::DevicePixelRatio()).toSize());
_pixmap.setDevicePixelRatio(style::DevicePixelRatio());
_pixmap.fill(Qt::transparent);
_p = std::make_unique<Painter>(&_pixmap);
_hq = std::make_unique<PainterHighQualityEnabler>(*_p);
_p->setPen(Qt::NoPen);
_p->setBrush(_brushData.color);
}
void ItemCanvas::applyBrush(const QColor &color, float size) {
_brushData.color = color;
_brushData.size = size;
_p->setBrush(color);
_brushMargins = QMarginsF(size, size, size, size);// / 2.;
}
QRectF ItemCanvas::boundingRect() const {
return scene()->sceneRect();
}
void ItemCanvas::computeContentRect(const QPointF &p) {
if (!scene()) {
return;
}
const auto sceneSize = scene()->sceneRect().size();
_contentRect = QRectF(
QPointF(
std::clamp(p.x() - _brushMargins.left(), 0., _contentRect.x()),
std::clamp(p.y() - _brushMargins.top(), 0., _contentRect.y())),
QPointF(
std::clamp(
p.x() + _brushMargins.right(),
_contentRect.x() + _contentRect.width(),
sceneSize.width()),
std::clamp(
p.y() + _brushMargins.bottom(),
_contentRect.y() + _contentRect.height(),
sceneSize.height())));
}
std::vector<ItemCanvas::StrokePoint> ItemCanvas::smoothStroke(
const std::vector<StrokePoint> &points) const {
if (points.size() < 4) {
return points;
}
auto result = std::vector<StrokePoint>();
result.reserve(points.size());
result.push_back(points[0]);
result.push_back(points[1]);
for (auto i = 2; i < int(points.size()) - 1; ++i) {
const auto &prev = points[i - 1].pos;
const auto &curr = points[i].pos;
const auto &next = points[i + 1].pos;
const auto smoothed = curr * kInvStrength
+ (prev + next) * kHalfStrength;
result.push_back({
.pos = smoothed,
.pressure = points[i].pressure,
.time = points[i].time,
});
}
result.push_back(points.back());
return result;
}
void ItemCanvas::renderSegment(
const std::vector<StrokePoint> &points,
int startIdx) {
if (points.size() < 2 || startIdx >= int(points.size()) - 1) {
return;
}
auto path = QPainterPath();
const auto effectiveStart = std::max(0, startIdx);
path.moveTo(points[effectiveStart].pos);
for (auto i = effectiveStart; i < int(points.size()) - 1; ++i) {
const auto &p0 = points[i].pos;
const auto &p1 = points[i + 1].pos;
const auto ctrl = (p0 + p1) / 2.0;
if (i == effectiveStart) {
path.lineTo(ctrl);
} else {
path.quadTo(p0, ctrl);
}
}
path.lineTo(points.back().pos);
const auto count = points.size() - std::max(0, startIdx);
const auto avgPressure = count > 0
? std::accumulate(
points.begin() + std::max(0, startIdx),
points.end(),
0.0,
[](float64 sum, const StrokePoint &p) {
return sum + p.pressure;
}) / count
: 1.0;
const auto width = _brushData.size * avgPressure;
auto stroker = QPainterPathStroker();
stroker.setWidth(width);
stroker.setCapStyle(Qt::RoundCap);
stroker.setJoinStyle(Qt::RoundJoin);
const auto outline = stroker.createStroke(path);
_p->fillPath(outline, _brushData.color);
_rectToUpdate |= outline.boundingRect() + _brushMargins;
}
void ItemCanvas::drawIncrementalStroke() {
if (_currentStroke.size() < 2) {
return;
}
const auto startIdx = std::max(
0,
_lastRenderedIndex - kSegmentOverlap);
auto segment = std::vector<StrokePoint>(
_currentStroke.begin() + startIdx,
_currentStroke.end());
if (segment.size() < 2) {
return;
}
if (segment.size() >= 4) {
for (auto i = 0; i < 2; ++i) {
segment = smoothStroke(segment);
}
}
renderSegment(
segment,
std::min(kSegmentOverlap, int(segment.size()) - 1));
_lastRenderedIndex = std::max(
0,
int(_currentStroke.size()) - kSegmentOverlap);
}
void ItemCanvas::addStrokePoint(const QPointF &point, int64 time) {
if (!_currentStroke.empty()) {
const auto distance = PointDistance(
point,
_currentStroke.back().pos);
const auto minDistance = kMinPointDistanceBase * std::min(1.0, _zoom);
if (distance < minDistance) {
return;
}
if (distance > kMaxPointDistance) {
const auto steps = int(std::ceil(distance / kMaxPointDistance));
const auto &lastPos = _currentStroke.back().pos;
const auto &lastPressure = _currentStroke.back().pressure;
for (auto i = 1; i < steps; ++i) {
const auto t = float64(i) / steps;
const auto interpolated = lastPos * (1.0 - t) + point * t;
const auto interpTime = _lastPointTime
+ int64((time - _lastPointTime) * t);
_currentStroke.push_back({
.pos = interpolated,
.pressure = lastPressure,
.time = interpTime,
});
}
}
}
const auto timeDelta = _lastPointTime
? std::max(int64(1), time - _lastPointTime)
: kBatchUpdateInterval;
const auto speed = !_currentStroke.empty()
? PointDistance(point, _currentStroke.back().pos) / timeDelta
: 0.0;
const auto pressureFromSpeed = std::clamp(
1.0 - speed * 0.1,
kMinPressure,
1.0);
const auto pressure = _currentStroke.empty()
? 1.0
: _currentStroke.back().pressure * kPressureDecay
+ pressureFromSpeed * (1.0 - kPressureDecay);
_currentStroke.push_back({
.pos = point,
.pressure = pressure,
.time = time,
});
_lastPointTime = time;
computeContentRect(point);
}
void ItemCanvas::handleMousePressEvent(
not_null<QGraphicsSceneMouseEvent*> e) {
_lastPoint = e->scenePos();
_rectToUpdate = QRectF();
_currentStroke.clear();
_lastRenderedIndex = 0;
_lastPointTime = 0;
const auto now = crl::now();
addStrokePoint(_lastPoint, now);
_contentRect = QRectF(_lastPoint, _lastPoint) + _brushMargins;
_drawing = true;
}
void ItemCanvas::handleMouseMoveEvent(
not_null<QGraphicsSceneMouseEvent*> e) {
if (!_drawing) {
return;
}
const auto scenePos = e->scenePos();
const auto now = crl::now();
addStrokePoint(scenePos, now);
_lastPoint = scenePos;
if (_currentStroke.size() - _lastRenderedIndex >= 3) {
drawIncrementalStroke();
update(_rectToUpdate);
}
}
void ItemCanvas::handleMouseReleaseEvent(
not_null<QGraphicsSceneMouseEvent*> e) {
if (!_drawing) {
return;
}
_drawing = false;
drawIncrementalStroke();
update(_rectToUpdate);
if (_contentRect.isValid()) {
const auto scaledContentRect = QRectF(
_contentRect.x() * style::DevicePixelRatio(),
_contentRect.y() * style::DevicePixelRatio(),
_contentRect.width() * style::DevicePixelRatio(),
_contentRect.height() * style::DevicePixelRatio());
_grabContentRequests.fire({
.pixmap = _pixmap.copy(scaledContentRect.toRect()),
.position = _contentRect.topLeft(),
});
}
_currentStroke.clear();
_lastRenderedIndex = 0;
_lastPointTime = 0;
_currentPath = QPainterPath();
clearPixmap();
update();
}
void ItemCanvas::paint(
QPainter *p,
const QStyleOptionGraphicsItem *,
QWidget *) {
p->fillRect(_rectToUpdate, Qt::transparent);
p->drawPixmap(0, 0, _pixmap);
_rectToUpdate = QRectF();
}
rpl::producer<ItemCanvas::Content> ItemCanvas::grabContentRequests() const {
return _grabContentRequests.events();
}
bool ItemCanvas::collidesWithItem(
const QGraphicsItem *,
Qt::ItemSelectionMode) const {
return false;
}
bool ItemCanvas::collidesWithPath(
const QPainterPath &,
Qt::ItemSelectionMode) const {
return false;
}
void ItemCanvas::cancelDrawing() {
_drawing = false;
_currentStroke.clear();
_lastRenderedIndex = 0;
_lastPointTime = 0;
_currentPath = QPainterPath();
_contentRect = QRectF();
clearPixmap();
update();
}
void ItemCanvas::updateZoom(float64 zoom) {
_zoom = zoom;
}
ItemCanvas::~ItemCanvas() {
_hq = nullptr;
_p = nullptr;
}
} // namespace Editor

View File

@@ -0,0 +1,96 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "ui/painter.h"
#include <QGraphicsItem>
class QGraphicsSceneMouseEvent;
namespace Editor {
class ItemCanvas : public QGraphicsItem {
public:
struct Content {
QPixmap pixmap;
QPointF position;
};
ItemCanvas();
~ItemCanvas();
void applyBrush(const QColor &color, float size);
void clearPixmap();
void cancelDrawing();
void updateZoom(float64 zoom);
QRectF boundingRect() const override;
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
void handleMousePressEvent(not_null<QGraphicsSceneMouseEvent*> event);
void handleMouseReleaseEvent(not_null<QGraphicsSceneMouseEvent*> event);
void handleMouseMoveEvent(not_null<QGraphicsSceneMouseEvent*> event);
[[nodiscard]] rpl::producer<Content> grabContentRequests() const;
protected:
bool collidesWithItem(
const QGraphicsItem *,
Qt::ItemSelectionMode) const override;
bool collidesWithPath(
const QPainterPath &,
Qt::ItemSelectionMode) const override;
private:
struct StrokePoint {
QPointF pos;
float64 pressure = 1.0;
int64 time = 0;
};
void computeContentRect(const QPointF &p);
void addStrokePoint(const QPointF &point, int64 time);
void drawIncrementalStroke();
std::vector<StrokePoint> smoothStroke(
const std::vector<StrokePoint> &points) const;
void renderSegment(
const std::vector<StrokePoint> &points,
int startIdx);
bool _drawing = false;
std::vector<StrokePoint> _currentStroke;
int _lastRenderedIndex = 0;
float64 _zoom = 1.0;
int64 _lastPointTime = 0;
std::unique_ptr<PainterHighQualityEnabler> _hq;
std::unique_ptr<Painter> _p;
QRectF _rectToUpdate;
QRectF _contentRect;
QMarginsF _brushMargins;
QPointF _lastPoint;
QPixmap _pixmap;
QPainterPath _currentPath;
struct {
float size = 1.;
QColor color;
} _brushData;
rpl::event_stream<Content> _grabContentRequests;
};
} // namespace Editor

View File

@@ -0,0 +1,46 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_image.h"
namespace Editor {
ItemImage::ItemImage(
QPixmap &&pixmap,
ItemBase::Data data)
: ItemBase(std::move(data))
, _pixmap(std::move(pixmap)) {
setAspectRatio(_pixmap.isNull()
? 1.0
: (_pixmap.height() / float64(_pixmap.width())));
}
void ItemImage::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
const auto rect = contentRect();
const auto pixmapSize = QSizeF(_pixmap.size() / style::DevicePixelRatio())
.scaled(rect.size(), Qt::KeepAspectRatio);
const auto resultRect = QRectF(rect.topLeft(), pixmapSize).translated(
(rect.width() - pixmapSize.width()) / 2.,
(rect.height() - pixmapSize.height()) / 2.);
p->drawPixmap(resultRect.toRect(), _pixmap);
ItemBase::paint(p, option, w);
}
void ItemImage::performFlip() {
_pixmap = _pixmap.transformed(QTransform().scale(-1, 1));
update();
}
std::shared_ptr<ItemBase> ItemImage::duplicate(ItemBase::Data data) const {
auto pixmap = _pixmap;
return std::make_shared<ItemImage>(std::move(pixmap), std::move(data));
}
} // namespace Editor

View File

@@ -0,0 +1,31 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/scene/scene_item_base.h"
namespace Editor {
class ItemImage : public ItemBase {
public:
ItemImage(
QPixmap &&pixmap,
ItemBase::Data data);
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
protected:
void performFlip() override;
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
private:
QPixmap _pixmap;
};
} // namespace Editor

View File

@@ -0,0 +1,62 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_line.h"
#include <QGraphicsScene>
namespace Editor {
ItemLine::ItemLine(QPixmap &&pixmap)
: _pixmap(std::move(pixmap))
, _rect(QPointF(), _pixmap.size() / float64(style::DevicePixelRatio())) {
}
QRectF ItemLine::boundingRect() const {
return _rect;
}
void ItemLine::paint(
QPainter *p,
const QStyleOptionGraphicsItem *,
QWidget *) {
p->drawPixmap(0, 0, _pixmap);
}
bool ItemLine::collidesWithItem(
const QGraphicsItem *,
Qt::ItemSelectionMode) const {
return false;
}
bool ItemLine::collidesWithPath(
const QPainterPath &,
Qt::ItemSelectionMode) const {
return false;
}
void ItemLine::save(SaveState state) {
auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
saved = {
.saved = true,
.status = status(),
};
}
void ItemLine::restore(SaveState state) {
if (!hasState(state)) {
return;
}
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
setStatus(saved.status);
}
bool ItemLine::hasState(SaveState state) const {
const auto &saved = (state == SaveState::Keep) ? _keeped : _saved;
return saved.saved;
}
} // namespace Editor

View File

@@ -0,0 +1,44 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/scene/scene_item_base.h"
namespace Editor {
class ItemLine : public NumberedItem {
public:
ItemLine(QPixmap &&pixmap);
QRectF boundingRect() const override;
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
bool hasState(SaveState state) const override;
void save(SaveState state) override;
void restore(SaveState state) override;
protected:
bool collidesWithItem(
const QGraphicsItem *,
Qt::ItemSelectionMode) const override;
bool collidesWithPath(
const QPainterPath &,
Qt::ItemSelectionMode) const override;
private:
const QPixmap _pixmap;
const QRectF _rect;
struct {
bool saved = false;
NumberedItem::Status status;
} _saved, _keeped;
};
} // namespace Editor

View File

@@ -0,0 +1,140 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/scene/scene_item_sticker.h"
#include "chat_helpers/stickers_lottie.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "data/data_session.h"
#include "lottie/lottie_common.h"
#include "lottie/lottie_single_player.h"
#include "main/main_session.h"
#include "ui/ui_utility.h"
#include "styles/style_editor.h"
namespace Editor {
namespace {
} // namespace
ItemSticker::ItemSticker(
not_null<DocumentData*> document,
ItemBase::Data data)
: ItemBase(std::move(data))
, _document(document)
, _mediaView(_document->createMediaView()) {
const auto stickerData = document->sticker();
if (!stickerData) {
return;
}
const auto updateThumbnail = [=] {
const auto guard = gsl::finally([&] {
if (_image.isNull()) {
setAspectRatio(1.);
}
});
if (stickerData->isLottie()) {
_lottie.player = ChatHelpers::LottiePlayerFromDocument(
_mediaView.get(),
ChatHelpers::StickerLottieSize::MessageHistory,
QSize(kStickerSideSize, kStickerSideSize)
* style::DevicePixelRatio(),
Lottie::Quality::High);
_lottie.player->updates(
) | rpl::on_next([=] {
updatePixmap(_lottie.player->frame());
_lottie.player = nullptr;
_lottie.lifetime.destroy();
update();
}, _lottie.lifetime);
return true;
} else if (stickerData->isWebm()
&& !_document->dimensions.isEmpty()) {
const auto callback = [=](::Media::Clip::Notification) {
const auto size = _document->dimensions;
if (_webm && _webm->ready() && !_webm->started()) {
_webm->start({ .frame = size, .keepAlpha = true });
}
if (_webm && _webm->started()) {
updatePixmap(_webm->current(
{ .frame = size, .keepAlpha = true },
0));
_webm = nullptr;
}
};
_webm = ::Media::Clip::MakeReader(
_mediaView->owner()->location(),
_mediaView->bytes(),
callback);
return true;
}
const auto sticker = _mediaView->getStickerLarge();
if (!sticker) {
return false;
}
const auto ratio = style::DevicePixelRatio();
auto pixmap = sticker->pixNoCache(sticker->size() * ratio);
pixmap.setDevicePixelRatio(ratio);
updatePixmap(pixmap.toImage());
return true;
};
if (!updateThumbnail()) {
_document->session().downloaderTaskFinished(
) | rpl::on_next([=] {
if (updateThumbnail()) {
_loadingLifetime.destroy();
update();
}
}, _loadingLifetime);
}
}
void ItemSticker::updatePixmap(QImage &&image) {
_image = std::move(image);
if (flipped()) {
performFlip();
} else {
update();
}
if (!_image.isNull()) {
setAspectRatio(_image.height() / float64(_image.width()));
}
}
void ItemSticker::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
const auto rect = contentRect();
const auto imageSize = QSizeF(_image.size() / style::DevicePixelRatio())
.scaled(rect.size(), Qt::KeepAspectRatio);
const auto resultRect = QRectF(rect.topLeft(), imageSize).translated(
(rect.width() - imageSize.width()) / 2.,
(rect.height() - imageSize.height()) / 2.);
p->drawImage(resultRect, _image);
ItemBase::paint(p, option, w);
}
not_null<DocumentData*> ItemSticker::sticker() const {
return _document;
}
int ItemSticker::type() const {
return Type;
}
void ItemSticker::performFlip() {
_image = _image.transformed(QTransform().scale(-1, 1));
update();
}
std::shared_ptr<ItemBase> ItemSticker::duplicate(ItemBase::Data data) const {
return std::make_shared<ItemSticker>(_document, std::move(data));
}
} // namespace Editor

View File

@@ -0,0 +1,58 @@
/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#pragma once
#include "editor/scene/scene_item_base.h"
#include "media/clip/media_clip_reader.h"
namespace Data {
class DocumentMedia;
} // namespace Data
namespace Lottie {
class SinglePlayer;
} // namespace Lottie
class DocumentData;
namespace Editor {
class ItemSticker : public ItemBase {
public:
enum { Type = ItemBase::Type + 1 };
ItemSticker(
not_null<DocumentData*> document,
ItemBase::Data data);
void paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *widget) override;
[[nodiscard]] not_null<DocumentData*> sticker() const;
int type() const override;
protected:
void performFlip() override;
std::shared_ptr<ItemBase> duplicate(ItemBase::Data data) const override;
private:
const not_null<DocumentData*> _document;
const std::shared_ptr<::Data::DocumentMedia> _mediaView;
void updatePixmap(QImage &&image);
struct {
std::unique_ptr<Lottie::SinglePlayer> player;
rpl::lifetime lifetime;
} _lottie;
::Media::Clip::ReaderPointer _webm;
QImage _image;
rpl::lifetime _loadingLifetime;
};
} // namespace Editor