[img-editor] Added wheel zoom and middle-button panning for scene items.

This commit is contained in:
23rd
2026-04-14 08:31:57 +03:00
parent 2a816019b6
commit d7cfa68a3f
4 changed files with 118 additions and 6 deletions
+63 -4
View File
@@ -36,6 +36,9 @@ constexpr auto kMinCanvasZoom = 1.;
constexpr auto kMaxCanvasZoom = 8.;
constexpr auto kCanvasZoomStep = 1.15;
constexpr auto kZoomEpsilon = 0.0001;
constexpr auto kMinItemZoom = 0.1;
constexpr auto kMaxItemZoom = 10.;
constexpr auto kCanvasZoomStepFine = 1.015;
std::shared_ptr<Scene> EnsureScene(
PhotoModifications &mods,
@@ -55,13 +58,15 @@ Paint::Paint(
PhotoModifications &modifications,
const QSize &imageSize,
std::shared_ptr<Controllers> controllers,
Fn<QImage(QRect)> blurSource)
Fn<QImage(QRect)> blurSource,
bool fixedCrop)
: RpWidget(parent)
, _controllers(controllers)
, _scene(EnsureScene(modifications, imageSize))
, _view(base::make_unique_q<QGraphicsView>(_scene.get(), this))
, _viewport(_view->viewport())
, _imageSize(imageSize) {
, _imageSize(imageSize)
, _fixedCrop(fixedCrop) {
Expects(modifications.paint != nullptr);
_scene->setBlurSource(std::move(blurSource));
@@ -165,6 +170,51 @@ Paint::Paint(
}
bool Paint::zoomSceneItems(float64 wheelDelta, bool fine) {
if (!wheelDelta) {
return false;
}
const auto step = wheelDelta
/ float64(QWheelEvent::DefaultDeltasPerStep);
const auto base = fine ? kCanvasZoomStepFine : kCanvasZoomStep;
const auto factor = std::pow(base, step);
const auto center = rect::center(_scene->sceneRect());
auto applied = false;
for (const auto &item : _scene->items()) {
const auto raw = item.get();
const auto oldScale = raw->scale();
const auto newScale = std::clamp(
oldScale * factor,
kMinItemZoom,
kMaxItemZoom);
if (std::abs(newScale - oldScale) < kZoomEpsilon) {
continue;
}
const auto ratio = newScale / oldScale;
raw->setScale(newScale);
const auto pos = raw->pos();
raw->setPos(center + (pos - center) * ratio);
applied = true;
}
return applied;
}
void Paint::panSceneItems(QPointF sceneDelta) {
if (sceneDelta.isNull()) {
return;
}
for (const auto &item : _scene->items()) {
item->setPos(item->pos() + sceneDelta);
}
}
QPointF Paint::mapWidgetDeltaToScene(QPoint delta) const {
if (!_view) {
return QPointF(delta);
}
return _view->mapToScene(delta) - _view->mapToScene(QPoint());
}
Paint::~Paint() {
if (_viewport) {
_viewport->removeEventFilter(this);
@@ -393,6 +443,12 @@ bool Paint::eventFilter(QObject *obj, QEvent *e) {
return true;
}
if (_fixedCrop) {
zoomSceneItems(
delta,
wheel->modifiers().testFlag(Qt::ShiftModifier));
return true;
}
const auto step = delta / float64(QWheelEvent::DefaultDeltasPerStep);
const auto factor = std::pow(kCanvasZoomStep, step);
const auto newZoom = std::clamp(
@@ -421,7 +477,8 @@ bool Paint::eventFilter(QObject *obj, QEvent *e) {
const auto mouse = static_cast<QMouseEvent*>(e);
if (mouse->button() == Qt::MiddleButton) {
_pan = {
.active = (_transform.userZoom > kMinCanvasZoom),
.active = (_fixedCrop
|| _transform.userZoom > kMinCanvasZoom),
.point = mouse->pos(),
};
if (_pan.active) {
@@ -436,7 +493,9 @@ bool Paint::eventFilter(QObject *obj, QEvent *e) {
const auto delta = point - _pan.point;
_pan.point = point;
if (_transform.userZoom > kMinCanvasZoom) {
if (_fixedCrop) {
panSceneItems(mapWidgetDeltaToScene(delta));
} else if (_transform.userZoom > kMinCanvasZoom) {
view->horizontalScrollBar()->setValue(
view->horizontalScrollBar()->value() - delta.x());
view->verticalScrollBar()->setValue(
+7 -1
View File
@@ -29,7 +29,8 @@ public:
PhotoModifications &modifications,
const QSize &imageSize,
std::shared_ptr<Controllers> controllers,
Fn<QImage(QRect)> blurSource);
Fn<QImage(QRect)> blurSource,
bool fixedCrop = false);
~Paint() override;
[[nodiscard]] std::shared_ptr<Scene> saveScene() const;
@@ -55,6 +56,10 @@ public:
void paintImage(QPainter &p, const QPixmap &image) const;
void resetView();
bool zoomSceneItems(float64 wheelDelta, bool fine = false);
void panSceneItems(QPointF sceneDelta);
[[nodiscard]] QPointF mapWidgetDeltaToScene(QPoint delta) const;
private:
bool eventFilter(QObject *obj, QEvent *e) override;
void updateViewGeometry();
@@ -74,6 +79,7 @@ private:
const base::unique_qptr<QGraphicsView> _view;
QPointer<QWidget> _viewport;
const QSize _imageSize;
const bool _fixedCrop = false;
QRect _imageGeometry;
QRect _outerGeometry;
@@ -13,6 +13,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
#include "media/view/media_view_pip.h"
#include "storage/storage_media_prepare.h"
#include <QtGui/QMouseEvent>
#include <QtGui/QWheelEvent>
namespace Editor {
using Media::View::FlipSizeByRotation;
@@ -26,6 +29,7 @@ PhotoEditorContent::PhotoEditorContent(
EditorData data)
: RpWidget(parent)
, _photoSize(photo->size())
, _fixedCrop(data.fixedCrop)
, _paint(base::make_unique_q<Paint>(
this,
modifications,
@@ -42,7 +46,8 @@ PhotoEditorContent::PhotoEditorContent(
auto result = img.copy(pixelRect.intersected(img.rect()));
result.setDevicePixelRatio(dpr);
return result;
}))
},
data.fixedCrop))
, _crop(base::make_unique_q<Crop>(
this,
modifications,
@@ -110,6 +115,47 @@ PhotoEditorContent::PhotoEditorContent(
}, lifetime());
setupDragArea();
if (_fixedCrop) {
const auto pan = _crop->lifetime().make_state<
std::optional<QPoint>
>();
_crop->events(
) | rpl::on_next([=](not_null<QEvent*> e) {
const auto type = e->type();
if (type == QEvent::Wheel) {
const auto wheel = static_cast<QWheelEvent*>(e.get());
_paint->zoomSceneItems(
wheel->angleDelta().y(),
wheel->modifiers().testFlag(Qt::ShiftModifier));
e->accept();
} else if (type == QEvent::MouseButtonPress) {
const auto mouse = static_cast<QMouseEvent*>(e.get());
if (mouse->button() == Qt::MiddleButton) {
*pan = mouse->pos();
_crop->setCursor(Qt::ClosedHandCursor);
e->accept();
}
} else if (type == QEvent::MouseMove) {
if (pan->has_value()) {
const auto mouse = static_cast<QMouseEvent*>(e.get());
const auto point = mouse->pos();
const auto delta = point - **pan;
*pan = point;
_paint->panSceneItems(
_paint->mapWidgetDeltaToScene(delta));
e->accept();
}
} else if (type == QEvent::MouseButtonRelease) {
const auto mouse = static_cast<QMouseEvent*>(e.get());
if (mouse->button() == Qt::MiddleButton && pan->has_value()) {
pan->reset();
_crop->unsetCursor();
e->accept();
}
}
}, _crop->lifetime());
}
}
void PhotoEditorContent::applyModifications(
@@ -54,6 +54,7 @@ public:
private:
const QSize _photoSize;
const bool _fixedCrop = false;
const base::unique_qptr<Paint> _paint;
const base::unique_qptr<Crop> _crop;
const std::shared_ptr<Image> _photo;