[img-editor] Added dedicated brush size control.

This commit is contained in:
23rd
2026-03-11 09:42:48 +03:00
committed by John Preston
parent 898eebbf4b
commit 6ed80c9706
5 changed files with 372 additions and 10 deletions
+333 -9
View File
@@ -7,7 +7,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "editor/color_picker.h"
#include "base/basic_types.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include "ui/rp_widget.h"
#include "styles/style_editor.h"
@@ -17,8 +19,10 @@ namespace Editor {
namespace {
constexpr auto kPrecision = 1000;
constexpr auto kMinBrushSize = 0.1f;
constexpr auto kMinBrushSize = 0.1;
constexpr auto kMouseSkip = 1.4;
constexpr auto kMinBrushWidth = 1.;
constexpr auto kMaxBrushWidth = 25.;
constexpr auto kMinInnerHeight = 0.2;
constexpr auto kMaxInnerHeight = 0.8;
@@ -69,11 +73,11 @@ QBrush GradientBrush(const QPoint &p, const QGradientStops &stops) {
return QBrush(std::move(gradient));
}
float RatioPrecise(float a) {
return int(a * kPrecision) / float(kPrecision);
float64 RatioPrecise(float64 a) {
return int(a * kPrecision) / float64(kPrecision);
}
inline float64 InterpolateF(float a, float b, float64 b_ratio) {
inline float64 InterpolateF(float64 a, float64 b, float64 b_ratio) {
return a + float64(b - a) * b_ratio;
};
@@ -86,11 +90,14 @@ inline float64 InterpolationRatio(int from, int to, int result) {
ColorPicker::ColorPicker(
not_null<Ui::RpWidget*> parent,
const Brush &savedBrush)
: _circleColor(Qt::white)
: _parent(parent)
, _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))
, _sizeControlHoverArea(std::in_place, parent)
, _sizeControl(std::in_place, parent)
, _gradientStops(Colors())
, _outlinedStop(FindOutlinedStop(_circleColor, _gradientStops, _width))
, _gradientBrush(
@@ -107,10 +114,19 @@ ColorPicker::ColorPicker(
_canvasForCircle->resize(
_width + circleHeight(kMax),
st::photoEditorColorPickerCanvasHeight);
_sizeControl->resize(
st::photoEditorBrushSizeControlHitPadding * 2
+ st::photoEditorBrushSizeControlExpandShift
+ st::photoEditorBrushSizeControlExpandedTopWidth,
st::photoEditorBrushSizeControlHeight);
_sizeControlHoverArea->setMouseTracking(true);
_sizeControl->setMouseTracking(true);
_canvasForCircle->setAttribute(Qt::WA_TransparentForMouseEvents);
_down.pos = QPoint(colorToPosition(savedBrush.color), 0);
updateSizeControlPositionFromRatio();
moveSizeControl(_parent->size());
_colorLine->paintRequest(
) | rpl::on_next([=] {
@@ -130,6 +146,27 @@ ColorPicker::ColorPicker(
paintCircle(p);
}, _canvasForCircle->lifetime());
_sizeControl->paintOn([=](QPainter &p) {
paintSizeControl(p);
});
_parent->sizeValue(
) | rpl::on_next([=](const QSize &size) {
moveSizeControl(size);
}, _sizeControl->lifetime());
_sizeControlHoverArea->events(
) | rpl::on_next([=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Enter) {
_sizeHoverAreaHovered = true;
updateSizeControlExpanded();
} else if (type == QEvent::Leave) {
_sizeHoverAreaHovered = false;
updateSizeControlExpanded();
}
}, _sizeControlHoverArea->lifetime());
_colorLine->events(
) | rpl::on_next([=](not_null<QEvent*> event) {
const auto type = event->type();
@@ -162,7 +199,68 @@ ColorPicker::ColorPicker(
}
_canvasForCircle->update();
_sizeControl->update();
}, _colorLine->lifetime());
_sizeControl->events(
) | rpl::on_next([=](not_null<QEvent*> event) {
const auto type = event->type();
if (type == QEvent::Enter) {
_sizeControlHovered = true;
updateSizeControlExpanded();
return;
} else if (type == QEvent::Leave) {
_sizeControlHovered = false;
updateSizeControlExpanded();
return;
}
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;
}
const auto e = static_cast<QMouseEvent*>(event.get());
if (isPress) {
if (e->button() != Qt::LeftButton) {
return;
}
const auto progress = _sizeControlAnimation.value(
_sizeControlExpanded ? 1. : 0.);
const auto inHandle = sizeControlHandleRect(progress).contains(
e->pos());
const auto inControl = sizeControlHitRect(progress).contains(
e->pos());
if (!inHandle && !inControl) {
return;
}
_sizeDown.pressed = true;
updateSizeControlMousePosition(e->pos().y());
updateSizeControlExpanded();
_canvasForCircle->update();
_sizeControl->update();
return;
}
if (!_sizeDown.pressed) {
return;
}
if (isMove) {
updateSizeControlMousePosition(e->pos().y());
_canvasForCircle->update();
_sizeControl->update();
return;
}
if (isRelease && (e->button() == Qt::LeftButton)) {
updateSizeControlMousePosition(e->pos().y());
_sizeDown.pressed = false;
updateSizeControlExpanded();
_saveBrushRequests.fire_copy(_brush);
_canvasForCircle->update();
_sizeControl->update();
}
}, _sizeControl->lifetime());
}
void ColorPicker::updateMousePosition(const QPoint &pos, float64 progress) {
@@ -185,9 +283,10 @@ void ColorPicker::updateMousePosition(const QPoint &pos, float64 progress) {
// 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())),
float64(1. - InterpolationRatio(from, to, _down.pos.y())),
kMinBrushSize,
1.f);
1.);
updateSizeControlPositionFromRatio();
}
_brush.color = positionToColor(_down.pos.x());
}
@@ -204,6 +303,11 @@ void ColorPicker::moveLine(const QPoint &position) {
- _canvasForCircle->height());
}
void ColorPicker::setCanvasRect(const QRect &rect) {
_canvasRect = rect;
moveSizeControl(_parent->size());
}
QColor ColorPicker::positionToColor(int x) const {
const auto from = 0;
const auto to = _width;
@@ -263,12 +367,29 @@ void ColorPicker::paintCircle(QPainter &p) {
p.drawEllipse(innerRect);
}
void ColorPicker::paintSizeControl(QPainter &p) {
auto hq = PainterHighQualityEnabler(p);
const auto progress = _sizeControlAnimation.value(_sizeControlExpanded
? 1.
: 0.);
const auto path = sizeControlShapePath(progress);
p.setPen(Qt::NoPen);
p.setBrush(QColor(255, 255, 255, anim::interpolate(96, 176, progress)));
p.drawPath(path);
const auto handleRect = sizeControlHandleRect(progress);
p.setBrush(QColor(255, 255, 255, 244));
p.drawEllipse(handleRect);
}
void ColorPicker::paintOutline(QPainter &p, const QRectF &rect) {
const auto &s = _outlinedStop;
if (!s.stopPos) {
return;
}
const auto draw = [&](float opacity) {
const auto draw = [&](float64 opacity) {
p.save();
p.setOpacity(opacity);
p.setPen(Qt::lightGray);
@@ -302,8 +423,17 @@ int ColorPicker::circleHeight(float64 progress) const {
}
void ColorPicker::setVisible(bool visible) {
if (!visible) {
_sizeDown.pressed = false;
_sizeHoverAreaHovered = false;
_sizeControlHovered = false;
_sizeControlExpanded = false;
_sizeControlAnimation.stop();
}
_colorLine->setVisible(visible);
_canvasForCircle->setVisible(visible);
_sizeControlHoverArea->setVisible(visible);
_sizeControl->setVisible(visible);
}
rpl::producer<Brush> ColorPicker::saveBrushRequests() const {
@@ -322,7 +452,201 @@ int ColorPicker::colorToPosition(const QColor &color) const {
bool ColorPicker::preventHandleKeyPress() const {
return _canvasForCircle->isVisible()
&& (_circleAnimation.animating() || _down.pressed);
&& (_circleAnimation.animating()
|| _sizeControlAnimation.animating()
|| _down.pressed
|| _sizeDown.pressed);
}
void ColorPicker::moveSizeControl(const QSize &size) {
if (size.isEmpty()) {
return;
}
const auto areaWidth = std::min(
size.width(),
st::photoEditorBrushSizeControlHitPadding);
const auto areaTop = (_canvasRect.height() > 0)
? _canvasRect.y()
: (size.height() - _sizeControl->height()) / 2;
const auto areaHeight = (_canvasRect.height() > 0)
? _canvasRect.height()
: _sizeControl->height();
_sizeControlHoverArea->setGeometry(
0,
std::clamp(areaTop, 0, std::max(0, size.height() - areaHeight)),
areaWidth,
std::min(areaHeight, size.height()));
const auto collapsedCenterX = sizeControlCurrentCenterX(0.);
const auto collapsedLeft = collapsedCenterX
- (float64(st::photoEditorBrushSizeControlCollapsedWidth) / 2.);
const auto y = (_canvasRect.height() > 0)
? (rect::center(_canvasRect).y() - _sizeControl->height() / 2)
: ((size.height() - _sizeControl->height()) / 2);
const auto diff = size.height() - _sizeControl->height();
_sizeControl->move(
-int(base::SafeRound(collapsedLeft)),
std::clamp(y, 0, std::max(0, diff)));
}
void ColorPicker::updateSizeControlExpanded() {
const auto expanded = _sizeDown.pressed
|| _sizeHoverAreaHovered
|| _sizeControlHovered;
if (_sizeControlExpanded == expanded
&& !_sizeControlAnimation.animating()) {
return;
}
_sizeControlExpanded = expanded;
const auto from = _sizeControlAnimation.value(expanded ? 0. : 1.);
const auto to = expanded ? 1. : 0.;
_sizeControlAnimation.stop();
_sizeControlAnimation.start(
[=] { _sizeControl->update(); },
from,
to,
kCircleDuration * std::abs(to - from),
anim::easeOutCirc);
_sizeControl->update();
}
void ColorPicker::updateSizeControlMousePosition(int y) {
_sizeDown.y = std::clamp(y, sizeControlTop(), sizeControlBottom());
_brush.sizeRatio = sizeControlRatioFromY(_sizeDown.y);
}
void ColorPicker::updateSizeControlPositionFromRatio() {
_sizeDown.y = sizeControlYFromRatio(_brush.sizeRatio);
}
int ColorPicker::sizeControlShapeTop() const {
return st::photoEditorBrushSizeControlHitPadding;
}
int ColorPicker::sizeControlShapeBottom() const {
return _sizeControl->height() - st::photoEditorBrushSizeControlHitPadding;
}
int ColorPicker::sizeControlTop() const {
return sizeControlShapeTop()
+ (st::photoEditorBrushSizeControlExpandedTopWidth / 2);
}
int ColorPicker::sizeControlBottom() const {
return sizeControlShapeBottom()
- (st::photoEditorBrushSizeControlExpandedBottomWidth / 2);
}
float64 ColorPicker::sizeControlRatioFromY(int y) const {
const auto top = sizeControlTop();
const auto bottom = sizeControlBottom();
if (bottom <= top) {
return 1.;
}
return std::clamp(
float64(1. - InterpolationRatio(top, bottom, y)),
kMinBrushSize,
1.);
}
int ColorPicker::sizeControlYFromRatio(float64 ratio) const {
const auto top = sizeControlTop();
const auto bottom = sizeControlBottom();
if (bottom <= top) {
return top;
}
const auto normalized = std::clamp(
(ratio - kMinBrushSize) / (1. - kMinBrushSize),
0.,
1.);
return std::clamp(
anim::interpolate(bottom, top, normalized),
top,
bottom);
}
QRectF ColorPicker::sizeControlHandleRect(float64 progress) const {
const auto handleSize = sizeControlHandleSize();
const auto centerX = sizeControlCurrentCenterX(progress);
return QRectF(
centerX - handleSize / 2.,
_sizeDown.y - handleSize / 2.,
handleSize,
handleSize);
}
QRectF ColorPicker::sizeControlHitRect(float64 progress) const {
const auto collapsed = st::photoEditorBrushSizeControlCollapsedWidth;
const auto width = float64(anim::interpolate(
collapsed,
st::photoEditorBrushSizeControlExpandedTopWidth,
progress));
const auto centerX = sizeControlCurrentCenterX(progress);
const auto top = float64(sizeControlShapeTop());
const auto bottom = float64(sizeControlShapeBottom());
return QRectF(
centerX - width / 2.,
top,
width,
bottom - top);
}
QPainterPath ColorPicker::sizeControlShapePath(float64 progress) const {
const auto collapsed = st::photoEditorBrushSizeControlCollapsedWidth;
const auto topWidth = float64(anim::interpolate(
collapsed,
st::photoEditorBrushSizeControlExpandedTopWidth,
progress));
const auto topInset = st::photoEditorBrushSizeControlTopInset;
const auto adjustedTopWidth = std::max(
0.,
topWidth - float64(topInset * 2));
const auto bottomWidth = float64(anim::interpolate(
collapsed,
st::photoEditorBrushSizeControlExpandedBottomWidth,
progress));
const auto centerX = sizeControlCurrentCenterX(progress);
const auto top = float64(sizeControlShapeTop()) + topInset;
const auto bottom = float64(sizeControlShapeBottom());
const auto topRadius = adjustedTopWidth / 2.;
const auto bottomRadius = bottomWidth / 2.;
auto path = QPainterPath();
const auto topRect = QRectF(
centerX - topRadius,
top,
adjustedTopWidth,
adjustedTopWidth);
const auto bottomRect = QRectF(
centerX - bottomRadius,
bottom - bottomWidth,
bottomWidth,
bottomWidth);
path.moveTo(centerX - topRadius, top + topRadius);
path.arcTo(topRect, 180., -180.);
path.lineTo(centerX + bottomRadius, bottom - bottomRadius);
path.arcTo(bottomRect, 0., -180.);
path.lineTo(centerX - topRadius, top + topRadius);
path.closeSubpath();
return path;
}
float64 ColorPicker::sizeControlCurrentCenterX(float64 progress) const {
const auto from = float64(st::photoEditorBrushSizeControlCollapsedWidth)
/ 2.;
const auto to = float64(st::photoEditorBrushSizeControlExpandShift)
+ float64(st::photoEditorBrushSizeControlExpandedTopWidth) / 2.;
return float64(st::photoEditorBrushSizeControlHitPadding)
+ InterpolateF(from, to, progress);
}
float64 ColorPicker::sizeControlHandleSize() const {
const auto width = kMinBrushWidth
+ (kMaxBrushWidth - kMinBrushWidth) * _brush.sizeRatio;
return std::clamp(
width,
float64(st::photoEditorBrushSizeControlExpandedBottomWidth),
float64(st::photoEditorBrushSizeControlExpandedTopWidth));
}
} // namespace Editor
@@ -28,6 +28,7 @@ public:
ColorPicker(not_null<Ui::RpWidget*> parent, const Brush &savedBrush);
void moveLine(const QPoint &position);
void setCanvasRect(const QRect &rect);
void setVisible(bool visible);
bool preventHandleKeyPress() const;
@@ -35,18 +36,37 @@ public:
private:
void paintCircle(QPainter &p);
void paintSizeControl(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);
void moveSizeControl(const QSize &size);
void updateSizeControlExpanded();
void updateSizeControlMousePosition(int y);
void updateSizeControlPositionFromRatio();
[[nodiscard]] int sizeControlShapeTop() const;
[[nodiscard]] int sizeControlShapeBottom() const;
[[nodiscard]] int sizeControlTop() const;
[[nodiscard]] int sizeControlBottom() const;
[[nodiscard]] float64 sizeControlRatioFromY(int y) const;
[[nodiscard]] int sizeControlYFromRatio(float64 ratio) const;
[[nodiscard]] QRectF sizeControlHandleRect(float64 progress) const;
[[nodiscard]] QRectF sizeControlHitRect(float64 progress) const;
[[nodiscard]] QPainterPath sizeControlShapePath(float64 progress) const;
[[nodiscard]] float64 sizeControlCurrentCenterX(float64 progress) const;
[[nodiscard]] float64 sizeControlHandleSize() const;
const not_null<Ui::RpWidget*> _parent;
const QColor _circleColor;
const int _width;
const int _lineHeight;
const base::unique_qptr<Ui::RpWidget> _colorLine;
const base::unique_qptr<Ui::RpWidget> _canvasForCircle;
const base::unique_qptr<Ui::RpWidget> _sizeControlHoverArea;
const base::unique_qptr<Ui::RpWidget> _sizeControl;
const QGradientStops _gradientStops;
const OutlinedStop _outlinedStop;
@@ -56,9 +76,18 @@ private:
QPoint pos;
bool pressed = false;
} _down;
struct {
int y = 0;
bool pressed = false;
} _sizeDown;
bool _sizeHoverAreaHovered = false;
bool _sizeControlHovered = false;
bool _sizeControlExpanded = false;
QRect _canvasRect;
Brush _brush;
Ui::Animations::Simple _circleAnimation;
Ui::Animations::Simple _sizeControlAnimation;
rpl::event_stream<Brush> _saveBrushRequests;
+8
View File
@@ -98,6 +98,14 @@ photoEditorColorPickerCircleBigSize: 50px;
photoEditorColorPickerCircleSkip: 50px;
photoEditorBrushSizeControlHeight: 280px;
photoEditorBrushSizeControlCollapsedWidth: 2px;
photoEditorBrushSizeControlExpandedTopWidth: 25px;
photoEditorBrushSizeControlExpandedBottomWidth: 4px;
photoEditorBrushSizeControlExpandShift: 14px;
photoEditorBrushSizeControlHitPadding: 24px;
photoEditorBrushSizeControlTopInset: 1px;
photoEditorCropPointSize: 10px;
photoEditorCropMinSize: 20px;
@@ -108,6 +108,7 @@ PhotoEditor::PhotoEditor(
if (inner.isEmpty()) {
return;
}
_colorPicker->setCanvasRect(inner.translated(_content->pos()));
const auto innerTop = _content->y() + inner.top();
const auto skip = st::photoEditorCropPointSize;
const auto controlsRect = rect()
@@ -26,7 +26,7 @@ struct PhotoEditorMode {
};
struct Brush {
float sizeRatio = 0.;
float64 sizeRatio = 0.;
QColor color;
};