Skip to content

Commit 8efe24f

Browse files
authored
Merge pull request #86 from scratchcpp/bubbles
Implement text bubbles
2 parents daede71 + 6071533 commit 8efe24f

26 files changed

+945
-18
lines changed

src/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ qt_add_qml_module(scratchcpp-render
1212
internal/ValueMonitor.qml
1313
internal/MonitorSlider.qml
1414
internal/ListMonitor.qml
15+
internal/TextBubble.qml
1516
shaders/sprite.vert
1617
shaders/sprite.frag
1718
SOURCES
@@ -62,6 +63,10 @@ qt_add_qml_module(scratchcpp-render
6263
shadermanager.h
6364
graphicseffect.cpp
6465
graphicseffect.h
66+
textbubbleshape.cpp
67+
textbubbleshape.h
68+
textbubblepainter.cpp
69+
textbubblepainter.h
6570
blocks/penextension.cpp
6671
blocks/penextension.h
6772
blocks/penblocks.cpp

src/ProjectPlayer.qml

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,20 @@ ProjectScene {
109109
onStageModelChanged: stageModel.renderedTarget = this
110110
}
111111

112+
Loader {
113+
readonly property alias model: stageTarget.stageModel
114+
active: model ? model.bubbleText !== "" : false
115+
116+
sourceComponent: TextBubble {
117+
type: model ? model.bubbleType : TextBubbleShape.Say
118+
text: model ? model.bubbleText : ""
119+
target: stageTarget
120+
stageScale: root.stageScale
121+
stageWidth: root.stageWidth
122+
stageHeight: root.stageHeight
123+
}
124+
}
125+
112126
PenLayer {
113127
id: projectPenLayer
114128
engine: loader.engine
@@ -118,16 +132,34 @@ ProjectScene {
118132
Component {
119133
id: renderedSprite
120134

121-
RenderedTarget {
122-
id: target
123-
mouseArea: sceneMouseArea
124-
stageScale: root.stageScale
125-
transform: Scale { xScale: mirrorHorizontally ? -1 : 1 }
126-
Component.onCompleted: {
127-
engine = loader.engine;
128-
spriteModel = modelData;
129-
spriteModel.renderedTarget = this;
130-
spriteModel.penLayer = projectPenLayer;
135+
Item {
136+
anchors.fill: parent
137+
138+
RenderedTarget {
139+
id: targetItem
140+
mouseArea: sceneMouseArea
141+
stageScale: root.stageScale
142+
transform: Scale { xScale: targetItem.mirrorHorizontally ? -1 : 1 }
143+
Component.onCompleted: {
144+
engine = loader.engine;
145+
spriteModel = modelData;
146+
spriteModel.renderedTarget = this;
147+
spriteModel.penLayer = projectPenLayer;
148+
}
149+
}
150+
151+
Loader {
152+
readonly property alias model: targetItem.spriteModel
153+
active: model ? model.bubbleText !== "" : false
154+
155+
sourceComponent: TextBubble {
156+
type: model ? model.bubbleType : TextBubbleShape.Say
157+
text: model ? model.bubbleText : ""
158+
target: targetItem
159+
stageScale: root.stageScale
160+
stageWidth: root.stageWidth
161+
stageHeight: root.stageHeight
162+
}
131163
}
132164
}
133165
}

src/internal/TextBubble.qml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// SPDX-License-Identifier: LGPL-3.0-or-later
2+
3+
import QtQuick
4+
import ScratchCPP.Render
5+
6+
TextBubbleShape {
7+
id: root
8+
property string text: ""
9+
property RenderedTarget target: null
10+
property double stageWidth: 0
11+
property double stageHeight: 0
12+
13+
QtObject {
14+
// https://github.com/scratchfoundation/scratch-render/blob/ac935423afe3ba79235750eecb1e443474c6eb09/src/TextBubbleSkin.js#L7-L26
15+
id: priv
16+
readonly property int maxLineWidth: 170
17+
readonly property int minWidth: 50
18+
readonly property int padding: 10
19+
readonly property int tailHeight: 12
20+
21+
readonly property string fontFamily: "Helvetica"
22+
readonly property int fontPixelSize: 14
23+
readonly property int lineHeight: 16
24+
25+
readonly property color textFill: '#575E75'
26+
27+
function translateX(x) {
28+
// Translates Scratch X-coordinate to the scene coordinate system
29+
return root.stageScale * (root.stageWidth / 2 + x)
30+
}
31+
32+
function translateY(y) {
33+
// Translates Scratch Y-coordinate to the scene coordinate system
34+
return root.stageScale * (root.stageHeight / 2 - y)
35+
}
36+
}
37+
38+
nativeWidth: Math.max(bubbleText.contentWidth, priv.minWidth) + 2 * priv.padding
39+
nativeHeight: bubbleText.height + 2 * priv.padding + priv.tailHeight
40+
41+
function positionBubble() {
42+
// https://github.com/scratchfoundation/scratch-vm/blob/7313ce5199f8a3da7850085d0f7f6a3ca2c89bf6/src/blocks/scratch3_looks.js#L158
43+
if(!target.visible)
44+
return;
45+
46+
const targetBounds = target.getBoundsForBubble();
47+
const stageBounds = Qt.rect(-root.stageWidth / 2, root.stageHeight / 2, root.stageWidth, root.stageHeight);
48+
49+
if (onSpriteRight && nativeWidth + targetBounds.right > stageBounds.right &&
50+
(targetBounds.left - nativeWidth > stageBounds.left)) { // Only flip if it would fit
51+
onSpriteRight = false;
52+
} else if (!onSpriteRight && targetBounds.left - nativeWidth < stageBounds.left &&
53+
(nativeWidth + targetBounds.right < stageBounds.right)) { // Only flip if it would fit
54+
onSpriteRight = true;
55+
}
56+
57+
const pos = [
58+
onSpriteRight ? (
59+
Math.max(
60+
stageBounds.left, // Bubble should not extend past left edge of stage
61+
Math.min(stageBounds.right - nativeWidth, targetBounds.right)
62+
)
63+
) : (
64+
Math.min(
65+
stageBounds.right - nativeWidth, // Bubble should not extend past right edge of stage
66+
Math.max(stageBounds.left, targetBounds.left - nativeWidth)
67+
)
68+
),
69+
// Bubble should not extend past the top of the stage
70+
Math.min(stageBounds.top, targetBounds.bottom + nativeHeight)
71+
];
72+
73+
x = priv.translateX(pos[0]);
74+
y = priv.translateY(pos[1]);
75+
}
76+
77+
Connections {
78+
target: root.target
79+
80+
function onXChanged() { positionBubble() }
81+
function onYChanged() { positionBubble() }
82+
function onRotationChanged() { positionBubble() }
83+
function onWidthChanged() { positionBubble() }
84+
function onHeightChanged() { positionBubble() }
85+
function onScaleChanged() { positionBubble() }
86+
}
87+
88+
Text {
89+
id: bubbleText
90+
anchors.left: parent.left
91+
anchors.top: parent.top
92+
anchors.margins: priv.padding * root.stageScale
93+
width: priv.maxLineWidth
94+
scale: root.stageScale
95+
transformOrigin: Item.TopLeft
96+
text: root.text
97+
wrapMode: Text.Wrap
98+
color: priv.textFill
99+
lineHeight: priv.lineHeight
100+
lineHeightMode: Text.FixedHeight
101+
font.family: priv.fontFamily
102+
font.pixelSize: priv.fontPixelSize
103+
}
104+
105+
Component.onCompleted: positionBubble()
106+
}

src/irenderedtarget.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class IRenderedTarget : public QNanoQuickItem
7070
virtual void setHeight(qreal width) = 0;
7171

7272
virtual libscratchcpp::Rect getBounds() const = 0;
73+
virtual QRectF getBoundsForBubble() const = 0;
7374

7475
virtual QPointF mapFromScene(const QPointF &point) const = 0;
7576

src/renderedtarget.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,19 @@ Rect RenderedTarget::getBounds() const
366366
return Rect(left + m_x, top + m_y, right + m_x, bottom + m_y);
367367
}
368368

369+
QRectF RenderedTarget::getBoundsForBubble() const
370+
{
371+
// https://github.com/scratchfoundation/scratch-render/blob/86dcb0151a04bc8c1ff39559e8531e7921102b56/src/Drawable.js#L536-L551
372+
Rect rect = getBounds();
373+
const int slice = 8; // px, how tall the top slice to measure should be
374+
375+
if (rect.height() > slice)
376+
rect.setBottom(rect.top() - slice);
377+
378+
Q_ASSERT(rect.height() <= 8);
379+
return QRectF(QPointF(rect.left(), rect.top()), QPointF(rect.right(), rect.bottom()));
380+
}
381+
369382
QPointF RenderedTarget::mapFromScene(const QPointF &point) const
370383
{
371384
return QNanoQuickItem::mapFromScene(point);
@@ -463,9 +476,6 @@ void RenderedTarget::clearGraphicEffects()
463476

464477
void RenderedTarget::updateHullPoints(QOpenGLFramebufferObject *fbo)
465478
{
466-
if (m_stageModel)
467-
return; // hull points are useless for the stage
468-
469479
Q_ASSERT(fbo);
470480
int width = fbo->width();
471481
int height = fbo->height();

src/renderedtarget.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ class RenderedTarget : public IRenderedTarget
7575
void setHeight(qreal height) override;
7676

7777
libscratchcpp::Rect getBounds() const override;
78+
Q_INVOKABLE QRectF getBoundsForBubble() const override;
7879

7980
QPointF mapFromScene(const QPointF &point) const override;
8081

src/spritemodel.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,33 @@ void SpriteModel::onGraphicsEffectsCleared()
113113
m_renderedTarget->clearGraphicEffects();
114114
}
115115

116+
void SpriteModel::onBubbleTypeChanged(libscratchcpp::Target::BubbleType type)
117+
{
118+
if (type == libscratchcpp::Target::BubbleType::Say) {
119+
if (m_bubbleType == TextBubbleShape::Type::Say)
120+
return;
121+
122+
m_bubbleType = TextBubbleShape::Type::Say;
123+
} else {
124+
if (m_bubbleType == TextBubbleShape::Type::Think)
125+
return;
126+
127+
m_bubbleType = TextBubbleShape::Type::Think;
128+
}
129+
130+
emit bubbleTypeChanged();
131+
}
132+
133+
void SpriteModel::onBubbleTextChanged(const std::string &text)
134+
{
135+
QString newText = QString::fromStdString(text);
136+
137+
if (m_bubbleText != newText) {
138+
m_bubbleText = newText;
139+
emit bubbleTextChanged();
140+
}
141+
}
142+
116143
libscratchcpp::Rect SpriteModel::boundingRect() const
117144
{
118145
return m_renderedTarget->getBounds();
@@ -187,4 +214,14 @@ SpriteModel *SpriteModel::cloneRoot() const
187214
return m_cloneRoot;
188215
}
189216

217+
const TextBubbleShape::Type &SpriteModel::bubbleType() const
218+
{
219+
return m_bubbleType;
220+
}
221+
222+
const QString &SpriteModel::bubbleText() const
223+
{
224+
return m_bubbleText;
225+
}
226+
190227
} // namespace scratchcpprender

src/spritemodel.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <scratchcpp/ispritehandler.h>
88

99
#include "penstate.h"
10+
#include "textbubbleshape.h"
1011

1112
Q_MOC_INCLUDE("renderedtarget.h");
1213
Q_MOC_INCLUDE("ipenlayer.h");
@@ -25,6 +26,8 @@ class SpriteModel
2526
QML_ELEMENT
2627
Q_PROPERTY(IRenderedTarget *renderedTarget READ renderedTarget WRITE setRenderedTarget NOTIFY renderedTargetChanged)
2728
Q_PROPERTY(IPenLayer *penLayer READ penLayer WRITE setPenLayer NOTIFY penLayerChanged)
29+
Q_PROPERTY(TextBubbleShape::Type bubbleType READ bubbleType NOTIFY bubbleTypeChanged)
30+
Q_PROPERTY(QString bubbleText READ bubbleText NOTIFY bubbleTextChanged)
2831

2932
public:
3033
SpriteModel(QObject *parent = nullptr);
@@ -48,6 +51,9 @@ class SpriteModel
4851
void onGraphicsEffectChanged(libscratchcpp::IGraphicsEffect *effect, double value) override;
4952
void onGraphicsEffectsCleared() override;
5053

54+
void onBubbleTypeChanged(libscratchcpp::Target::BubbleType type) override;
55+
void onBubbleTextChanged(const std::string &text) override;
56+
5157
libscratchcpp::Rect boundingRect() const override;
5258

5359
libscratchcpp::Sprite *sprite() const;
@@ -66,9 +72,15 @@ class SpriteModel
6672

6773
SpriteModel *cloneRoot() const;
6874

75+
const TextBubbleShape::Type &bubbleType() const;
76+
77+
const QString &bubbleText() const;
78+
6979
signals:
7080
void renderedTargetChanged();
7181
void penLayerChanged();
82+
void bubbleTypeChanged();
83+
void bubbleTextChanged();
7284
void cloned(SpriteModel *cloneModel);
7385
void cloneDeleted(SpriteModel *clone);
7486

@@ -78,6 +90,8 @@ class SpriteModel
7890
IPenLayer *m_penLayer = nullptr;
7991
PenState m_penState;
8092
SpriteModel *m_cloneRoot = nullptr;
93+
TextBubbleShape::Type m_bubbleType = TextBubbleShape::Type::Say;
94+
QString m_bubbleText;
8195
};
8296

8397
} // namespace scratchcpprender

src/stagemodel.cpp

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,33 @@ void StageModel::onGraphicsEffectsCleared()
5050
m_renderedTarget->clearGraphicEffects();
5151
}
5252

53+
void StageModel::onBubbleTypeChanged(libscratchcpp::Target::BubbleType type)
54+
{
55+
if (type == libscratchcpp::Target::BubbleType::Say) {
56+
if (m_bubbleType == TextBubbleShape::Type::Say)
57+
return;
58+
59+
m_bubbleType = TextBubbleShape::Type::Say;
60+
} else {
61+
if (m_bubbleType == TextBubbleShape::Type::Think)
62+
return;
63+
64+
m_bubbleType = TextBubbleShape::Type::Think;
65+
}
66+
67+
emit bubbleTypeChanged();
68+
}
69+
70+
void StageModel::onBubbleTextChanged(const std::string &text)
71+
{
72+
QString newText = QString::fromStdString(text);
73+
74+
if (m_bubbleText != newText) {
75+
m_bubbleText = newText;
76+
emit bubbleTextChanged();
77+
}
78+
}
79+
5380
void StageModel::loadCostume()
5481
{
5582
if (m_renderedTarget && m_stage) {
@@ -78,3 +105,13 @@ void StageModel::setRenderedTarget(IRenderedTarget *newRenderedTarget)
78105

79106
emit renderedTargetChanged();
80107
}
108+
109+
const TextBubbleShape::Type &StageModel::bubbleType() const
110+
{
111+
return m_bubbleType;
112+
}
113+
114+
const QString &StageModel::bubbleText() const
115+
{
116+
return m_bubbleText;
117+
}

0 commit comments

Comments
 (0)