mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/telegramdesktop/tdesktop
synced 2026-09-21 00:23:56 +08:00
456 lines
11 KiB
C++
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
|