Files
tdesktop/Telegram/SourceFiles/editor/scene/scene_item_shape.cpp

456 lines
11 KiB
C++

/*
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_shape.h"
#include "ui/painter.h"
#include "ui/rect.h"
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsSceneMouseEvent>
#include <QStyleOptionGraphicsItem>
#include <QtMath>
namespace Editor {
namespace {
constexpr auto kStarInnerRatio = 0.4316;
constexpr auto kRoundingRatio = 0.234;
constexpr auto kBubbleBodyRatio = 2. / 2.25;
constexpr auto kBubbleTailRatio = 0.75;
constexpr auto kArrowHeadRatio = 0.16;
constexpr auto kArrowHeadStrokeFactor = 2.5;
constexpr auto kArrowHeadAngle = 30. * M_PI / 180.;
constexpr auto kArrowMaxBendRatio = 0.35;
constexpr auto kMinShapeSide = 1.;
constexpr auto kStarBoxWidth = 1.902113;
constexpr auto kStarBoxHeight = 1.809017;
} // namespace
ItemShape::ItemShape(
ShapeType shape,
const QColor &color,
float64 strokeWidth,
bool fill,
Data data)
: ItemBase(std::move(data))
, _shape(shape)
, _color(color)
, _strokeWidth(strokeWidth)
, _fill(fill) {
if (_shape == ShapeType::Star) {
setAspectRatio(kStarBoxHeight / kStarBoxWidth);
} else if (_shape == ShapeType::Arrow) {
setVerticalMinimumEnabled(false);
updateArrowFrame();
}
}
void ItemShape::paint(
QPainter *p,
const QStyleOptionGraphicsItem *option,
QWidget *w) {
PainterHighQualityEnabler hq(*p);
p->save();
p->setPen(QPen(
_color,
_strokeWidth,
Qt::SolidLine,
Qt::RoundCap,
Qt::RoundJoin));
if (_shape == ShapeType::Arrow) {
p->setBrush(Qt::NoBrush);
p->drawPath(arrowShaftPath());
p->setBrush(_color);
p->drawPath(arrowHeadPath());
} else {
p->setBrush(_fill ? QBrush(_color) : QBrush());
p->drawPath(shapePath());
}
p->restore();
ItemBase::paint(p, option, w);
if ((_shape == ShapeType::Arrow)
&& (option->state & QStyle::State_Selected)) {
paintHandle(
p,
bendHandleRect(),
option->state & QStyle::State_HasFocus);
}
}
QRectF ItemShape::boundingRect() const {
const auto result = ItemBase::boundingRect();
if (!_belowMinimum) {
return result;
}
return result + Margins(_strokeWidth / 2. + 1.);
}
int ItemShape::type() const {
return Type;
}
QColor ItemShape::color() const {
return _color;
}
void ItemShape::setColor(const QColor &color) {
_color = color;
update();
}
void ItemShape::setStrokeWidth(float64 width) {
_strokeWidth = width;
updateArrowFrame();
update();
}
void ItemShape::setBend(float64 bend) {
_bend = std::clamp(bend, -1., 1.);
updateArrowFrame();
update();
}
float64 ItemShape::defaultAspectRatio() const {
return (_shape == ShapeType::Star)
? (kStarBoxHeight / kStarBoxWidth)
: 1.;
}
void ItemShape::applyFrame(float64 width, float64 height) {
applyFrameSize(width, height, false);
}
bool ItemShape::applyDraftFrame(float64 width, float64 height) {
return applyFrameSize(width, height, true);
}
bool ItemShape::applyFrameSize(float64 width, float64 height, bool draft) {
if (_shape == ShapeType::Arrow) {
applyStretch(width, verticalSize(), draft);
updateArrowFrame();
} else {
applyStretch(width, height, draft);
}
const auto fits = fitsMinimumSize();
if (_belowMinimum != (draft && !fits)) {
prepareGeometryChange();
_belowMinimum = (draft && !fits);
}
update();
return fits;
}
QRectF ItemShape::shapeRect() const {
const auto content = contentRect();
const auto inset = _strokeWidth;
auto result = Rect(QSizeF(
std::max(content.width() - inset, kMinShapeSide),
std::max(content.height() - inset, kMinShapeSide)));
result.moveCenter(rect::center(content));
return result;
}
float64 ItemShape::direction() const {
return flipped() ? -1. : 1.;
}
QPainterPath ItemShape::shapePath() const {
const auto rect = shapeRect();
auto path = QPainterPath();
switch (_shape) {
case ShapeType::Circle:
path.addEllipse(rect);
break;
case ShapeType::Rectangle: {
const auto rounding = kRoundingRatio
* std::min(rect.width(), rect.height())
/ 2.;
path.addRoundedRect(rect, rounding, rounding);
break;
}
case ShapeType::Star: {
const auto outer = std::min(
rect.width() / kStarBoxWidth,
rect.height() / kStarBoxHeight);
const auto inner = outer * kStarInnerRatio;
const auto center = rect::center(rect)
- QPointF(0., kStarBoxHeight * outer / 2. - outer);
for (auto i = 0; i != 5; ++i) {
const auto angle = -M_PI_2 + i * 2. * M_PI / 5.;
const auto innerAngle = angle + M_PI / 5.;
const auto outerPoint = center + QPointF(
outer * std::cos(angle),
outer * std::sin(angle));
const auto innerPoint = center + QPointF(
inner * std::cos(innerAngle),
inner * std::sin(innerAngle));
if (!i) {
path.moveTo(outerPoint);
} else {
path.lineTo(outerPoint);
}
path.lineTo(innerPoint);
}
path.closeSubpath();
break;
}
case ShapeType::Bubble: {
const auto body = QRectF(
rect.topLeft(),
QSizeF(rect.width(), rect.height() * kBubbleBodyRatio));
const auto rounding = kRoundingRatio
* std::min(body.width(), body.height())
/ 2.;
path.addRoundedRect(body, rounding, rounding);
const auto tip = QPointF(
rect::center(rect).x()
+ direction() * kBubbleTailRatio * rect.width() / 2.,
rect.bottom());
const auto center = rect::center(body);
const auto tail = tip - center;
const auto length = std::hypot(tail.x(), tail.y());
if (length > 0) {
const auto normal = QPointF(
-tail.y() / length,
tail.x() / length);
const auto halfBase = std::min(body.width(), body.height())
/ 4.;
auto tailPath = QPainterPath();
tailPath.moveTo(center + normal * halfBase);
tailPath.lineTo(tip);
tailPath.lineTo(center - normal * halfBase);
tailPath.closeSubpath();
path = path.united(tailPath);
}
break;
}
case ShapeType::Arrow:
break;
}
return path;
}
float64 ItemShape::arrowHeadSide(float64 width) const {
if (width <= 0) {
return 0.;
}
return std::clamp(
std::max(
kArrowHeadRatio * width,
kArrowHeadStrokeFactor * _strokeWidth),
0.,
width / 3.);
}
ItemShape::ArrowPoints ItemShape::arrowPoints() const {
const auto rect = shapeRect();
const auto side = arrowHeadSide(rect.width());
const auto center = rect::center(rect);
const auto dir = direction();
const auto middle = _bend * kArrowMaxBendRatio * rect.width();
return {
.start = QPointF(
center.x() - dir * rect.width() / 2.,
center.y() - middle),
.control = QPointF(
center.x(),
center.y() + 3. * middle),
.end = QPointF(
center.x() + dir * (rect.width() / 2. - side / 2.),
center.y() - middle),
.headSide = side,
};
}
void ItemShape::updateArrowFrame() {
if (_shape != ShapeType::Arrow) {
return;
}
const auto width = horizontalSize();
if (width <= 0) {
return;
}
const auto side = arrowHeadSide(width);
const auto middle = std::abs(_bend) * kArrowMaxBendRatio * width;
const auto padding = side * 0.75 + _strokeWidth;
setAspectRatio(2. * (middle + padding) / width);
}
QPainterPath ItemShape::arrowShaftPath() const {
const auto points = arrowPoints();
auto path = QPainterPath();
path.moveTo(points.start);
path.quadTo(points.control, points.end);
return path;
}
QPainterPath ItemShape::arrowHeadPath() const {
const auto points = arrowPoints();
const auto delta = points.end - points.control;
const auto length = std::hypot(delta.x(), delta.y());
if (length <= 0) {
return QPainterPath();
}
const auto forward = delta / length;
const auto rotated = [&](float64 angle) {
return QPointF(
forward.x() * std::cos(angle) - forward.y() * std::sin(angle),
forward.x() * std::sin(angle) + forward.y() * std::cos(angle));
};
const auto apex = points.end + forward * (points.headSide / 2.);
auto path = QPainterPath();
path.moveTo(apex);
path.lineTo(apex - rotated(kArrowHeadAngle) * points.headSide);
path.lineTo(apex - rotated(-kArrowHeadAngle) * points.headSide);
path.closeSubpath();
return path;
}
QRectF ItemShape::bendHandleRect() const {
const auto points = arrowPoints();
auto result = Rect(Size(scaledHandleSize()));
result.moveCenter((points.start + points.end) * 0.25
+ points.control * 0.5);
return result;
}
bool ItemShape::overBendHandle(const QPointF &pos) const {
return (_shape == ShapeType::Arrow)
&& isSelected()
&& bendHandleRect().contains(pos);
}
void ItemShape::updateBend(const QPointF &pos) {
const auto rect = shapeRect();
const auto limit = kArrowMaxBendRatio * rect.width();
if (limit <= 0) {
return;
}
setBend((pos.y() - rect::center(rect).y()) / limit);
}
void ItemShape::performFlip() {
update();
}
std::shared_ptr<ItemBase> ItemShape::duplicate(Data data) const {
auto result = std::make_shared<ItemShape>(
_shape,
_color,
_strokeWidth,
_fill,
std::move(data));
if (horizontalSize() > 0) {
result->setAspectRatio(verticalSize() / horizontalSize());
}
result->setBend(_bend);
return result;
}
void ItemShape::mousePressEvent(QGraphicsSceneMouseEvent *event) {
if ((event->button() == Qt::LeftButton)
&& overBendHandle(event->pos())) {
raiseToTop();
resetDragging();
_bendDragging = true;
setCursor(Qt::ClosedHandCursor);
return;
}
ItemBase::mousePressEvent(event);
}
void ItemShape::mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
if (!dragThresholdPassed(event)) {
return;
}
if (_bendDragging) {
updateBend(event->pos());
return;
}
if (isHandling()
&& !event->modifiers().testFlag(Qt::ShiftModifier)
&& event->modifiers().testFlag(Qt::ControlModifier)) {
applyStretch(
std::abs(2. * event->pos().x()),
(_shape == ShapeType::Arrow)
? verticalSize()
: std::abs(2. * event->pos().y()));
updateArrowFrame();
update();
return;
}
ItemBase::mouseMoveEvent(event);
}
void ItemShape::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) {
if (_bendDragging && (event->button() == Qt::LeftButton)) {
_bendDragging = false;
setCursor(Qt::OpenHandCursor);
return;
}
ItemBase::mouseReleaseEvent(event);
}
void ItemShape::hoverMoveEvent(QGraphicsSceneHoverEvent *event) {
if (overBendHandle(event->pos())) {
setCursor(_bendDragging
? Qt::ClosedHandCursor
: Qt::OpenHandCursor);
return;
}
ItemBase::hoverMoveEvent(event);
}
ItemBase::Placement ItemShape::placement() const {
auto result = ItemBase::placement();
result.bend = _bend;
return result;
}
void ItemShape::applyPlacement(const Placement &placement) {
_bend = placement.bend;
ItemBase::applyPlacement(placement);
updateArrowFrame();
update();
}
void ItemShape::save(SaveState state) {
ItemBase::save(state);
auto &saved = (state == SaveState::Keep) ? _keepedShape : _savedShape;
saved = {
.color = _color,
.strokeWidth = _strokeWidth,
.bend = _bend,
.aspectRatio = (horizontalSize() > 0)
? (verticalSize() / horizontalSize())
: 1.,
.fill = _fill,
};
}
void ItemShape::restore(SaveState state) {
if (!hasState(state)) {
return;
}
const auto &saved = (state == SaveState::Keep)
? _keepedShape
: _savedShape;
_color = saved.color;
_strokeWidth = saved.strokeWidth;
_bend = saved.bend;
_fill = saved.fill;
ItemBase::restore(state);
setAspectRatio(saved.aspectRatio);
updateArrowFrame();
update();
}
} // namespace Editor