Skip to content

Commit cbcaa21

Browse files
committed
Docs - code cards for the tutorial, examples and Lang, matching web
1 parent b2b4c99 commit cbcaa21

37 files changed

Lines changed: 3739 additions & 1804 deletions

‎app/gui-tests/CMakeLists.txt‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,14 @@ add_executable(${PROJECT_NAME}
6565
${GUI_TESTS_ROOT}/loopscope_lifecycle.test.cpp
6666
${GUI_TESTS_ROOT}/linkaudio_engine_ready.test.cpp
6767
${GUI_TESTS_ROOT}/plugins.test.cpp
68+
${GUI_TESTS_ROOT}/card_deck.test.cpp
69+
${GUI_TESTS_ROOT}/quickstart_card.test.cpp
70+
${GUI_TESTS_ROOT}/tutorial_cards.test.cpp
6871
${GUI_ROOT}/widgets/nodetreegraph.cpp
6972
${GUI_ROOT}/widgets/trackspanel.cpp
73+
${GUI_ROOT}/widgets/quickstartpane.cpp
74+
${GUI_ROOT}/widgets/codecard.cpp
75+
${GUI_ROOT}/widgets/carddeck.cpp
7076
${GUI_ROOT}/widgets/linkaudiostreamswidget.cpp
7177
${GUI_ROOT}/widgets/linkvisibilitytoggle.cpp
7278
${GUI_ROOT}/widgets/tutorialpane.cpp

‎app/gui-tests/card_deck.test.cpp‎

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
//--
2+
// This file is part of Sonic Pi: http://sonic-pi.net
3+
// Full project source: https://github.com/sonic-pi-net/sonic-pi
4+
// License: https://github.com/sonic-pi-net/sonic-pi/blob/main/LICENSE.md
5+
//
6+
// Copyright 2026 by Sam Aaron (http://sam.aaron.name).
7+
// All rights reserved.
8+
//
9+
// Permission is granted for use, copying, modification, and
10+
// distribution of modified versions of this work as long as this
11+
// notice is included.
12+
//++
13+
14+
// A deck: the runs a host's code cards start, as the web's deck.js keeps
15+
// them. Each card's Play and Stop are the deck's; the session's run events
16+
// come back to the card whose run they are.
17+
18+
#include <catch2/catch_test_macros.hpp>
19+
20+
#include <QAbstractButton>
21+
#include <QApplication>
22+
#include <QLabel>
23+
#include <QPushButton>
24+
#include <QSignalSpy>
25+
#include <QTest>
26+
#include <QVBoxLayout>
27+
28+
#include <memory>
29+
30+
#include "model/sonicpitheme.h"
31+
#include "widgets/carddeck.h"
32+
#include "widgets/codecard.h"
33+
#include "widgets/tutorialwidgets.h"
34+
35+
namespace {
36+
37+
struct Host
38+
{
39+
SonicPiTheme* theme = new SonicPiTheme(nullptr, "", QStringLiteral(SP_ROOT));
40+
QWidget page;
41+
QVBoxLayout* column = new QVBoxLayout(&page);
42+
43+
Host() { page.resize(800, 900); }
44+
~Host() { delete theme; }
45+
46+
CodeCard* card(const QString& title, const QString& code, unsigned int scopeSlot = 1)
47+
{
48+
CodeCard::Spec spec;
49+
spec.title = title;
50+
spec.code = code;
51+
spec.scopeSlot = scopeSlot;
52+
CodeCard::Metrics m;
53+
m.codeFontPx = 14;
54+
m.titlePx = 16;
55+
CodeCard* c = new CodeCard(spec, m, theme, &page);
56+
column->addWidget(c);
57+
return c;
58+
}
59+
};
60+
61+
void play(CodeCard* c) { c->playButton()->click(); }
62+
void stop(CodeCard* c) { c->stopButton()->click(); }
63+
64+
QSet<int> stopped(const QSignalSpy& spy)
65+
{
66+
QSet<int> jobs;
67+
for (const QList<QVariant>& call : spy)
68+
jobs.insert(call[0].toInt());
69+
return jobs;
70+
}
71+
72+
QString shown(QWidget* card, const char* name)
73+
{
74+
QLabel* l = card->findChild<QLabel*>(QString::fromLatin1(name));
75+
return l && !l->isHidden() ? l->text() : QString();
76+
}
77+
78+
} // namespace
79+
80+
TEST_CASE("a deck runs a card's code and stops its runs", "[carddeck]")
81+
{
82+
Host host;
83+
CardDeck deck(&host.page, CardDeck::Playing::Layered);
84+
CodeCard* a = host.card("A", "play 60", 3);
85+
deck.add(a, "ws-a");
86+
QSignalSpy runs(&deck, &CardDeck::runRequested);
87+
QSignalSpy stops(&deck, &CardDeck::stopJobRequested);
88+
89+
play(a);
90+
REQUIRE(runs.count() == 1);
91+
CHECK(runs[0][0].toString() == "A");
92+
CHECK(runs[0][1].toString() == "play 60");
93+
CHECK(runs[0][2].toString() == "ws-a");
94+
CHECK(runs[0][3].toInt() == 3);
95+
CHECK_FALSE(a->isPlaying());
96+
97+
deck.runStarted(7, "ws-a");
98+
CHECK(a->isPlaying());
99+
deck.runStarted(99, "somebody-else"); // not the deck's
100+
stop(a);
101+
CHECK(stopped(stops) == QSet<int>{ 7 });
102+
deck.runEnded(7);
103+
CHECK_FALSE(a->isPlaying());
104+
}
105+
106+
TEST_CASE("a layered deck plays its cards together", "[carddeck]")
107+
{
108+
Host host;
109+
CardDeck deck(&host.page, CardDeck::Playing::Layered);
110+
CodeCard* drums = host.card("Drums", "live_loop :drums do\n sample :bd_haus\n sleep 1\nend");
111+
CodeCard* bass = host.card("Bass", "live_loop :bass do\n play :e1\n sleep 1\nend");
112+
deck.add(drums, "ws-d");
113+
deck.add(bass, "ws-b");
114+
QSignalSpy stops(&deck, &CardDeck::stopJobRequested);
115+
116+
play(drums);
117+
deck.runStarted(7, "ws-d");
118+
play(bass);
119+
deck.runStarted(8, "ws-b");
120+
CHECK(drums->isPlaying());
121+
CHECK(bass->isPlaying());
122+
CHECK(stops.isEmpty());
123+
124+
stop(bass);
125+
CHECK(stopped(stops) == QSet<int>{ 8 });
126+
}
127+
128+
TEST_CASE("a card that takes over another's live_loops takes its runs too", "[carddeck]")
129+
{
130+
Host host;
131+
CardDeck deck(&host.page, CardDeck::Playing::Layered);
132+
CodeCard* first = host.card("Beat", "live_loop :beat do\n sample :bd_haus\n sleep 1\nend");
133+
CodeCard* second = host.card("Beat, faster", "live_loop :beat do\n sample :bd_haus\n sleep 0.5\nend");
134+
deck.add(first, "ws-1");
135+
deck.add(second, "ws-2");
136+
QSignalSpy stops(&deck, &CardDeck::stopJobRequested);
137+
138+
play(first);
139+
deck.runStarted(7, "ws-1");
140+
play(second);
141+
deck.runStarted(8, "ws-2");
142+
// live_loop redefines the running thread, which stays the first run's:
143+
// the second card plays it now, so its Stop is the one that ends it.
144+
CHECK_FALSE(first->isPlaying());
145+
CHECK(second->isPlaying());
146+
deck.runEnded(8); // the redefinition lands and its own run ends
147+
CHECK(second->isPlaying());
148+
stop(second);
149+
CHECK(stopped(stops) == QSet<int>{ 7 });
150+
}
151+
152+
TEST_CASE("a deck that plays one card at a time stops the last for the next", "[carddeck]")
153+
{
154+
Host host;
155+
CardDeck deck(&host.page, CardDeck::Playing::OneAtATime);
156+
CodeCard* a = host.card("A", "live_loop :a do\n play 60\n sleep 1\nend");
157+
CodeCard* b = host.card("B", "live_loop :b do\n play 64\n sleep 1\nend");
158+
deck.add(a, "ws-a");
159+
deck.add(b, "ws-b");
160+
QSignalSpy stops(&deck, &CardDeck::stopJobRequested);
161+
162+
play(a);
163+
deck.runStarted(7, "ws-a");
164+
play(b);
165+
CHECK(stopped(stops) == QSet<int>{ 7 });
166+
CHECK_FALSE(a->isPlaying());
167+
168+
// Play again on the card that plays is a run again, not a stop.
169+
deck.runStarted(8, "ws-b");
170+
stops.clear();
171+
play(b);
172+
CHECK(stops.isEmpty());
173+
}
174+
175+
TEST_CASE("a card made again while its runs play is still playing", "[carddeck]")
176+
{
177+
Host host;
178+
CardDeck deck(&host.page, CardDeck::Playing::Layered);
179+
CodeCard* a = host.card("A", "play 60");
180+
deck.add(a, "ws-a");
181+
play(a);
182+
deck.runStarted(7, "ws-a");
183+
184+
// A rebuild (a zoom, a theme) makes the cards again.
185+
deck.clear();
186+
delete a;
187+
CodeCard* again = host.card("A", "play 60");
188+
deck.add(again, "ws-a");
189+
CHECK(again->isPlaying());
190+
QSignalSpy stops(&deck, &CardDeck::stopJobRequested);
191+
stop(again);
192+
CHECK(stopped(stops) == QSet<int>{ 7 });
193+
}
194+
195+
TEST_CASE("a run that starts after its card went is still the deck's", "[carddeck]")
196+
{
197+
Host host;
198+
CardDeck deck(&host.page, CardDeck::Playing::Layered);
199+
CodeCard* a = host.card("A", "play 60");
200+
deck.add(a, "ws-a");
201+
play(a);
202+
// The host moves on (another deck) before the engine starts the run.
203+
deck.clear();
204+
delete a;
205+
deck.runStarted(7, "ws-a");
206+
// Back again: the card made again plays it, and stops it.
207+
CodeCard* again = host.card("A", "play 60");
208+
deck.add(again, "ws-a");
209+
CHECK(again->isPlaying());
210+
QSignalSpy stops(&deck, &CardDeck::stopJobRequested);
211+
stop(again);
212+
CHECK(stopped(stops) == QSet<int>{ 7 });
213+
}
214+
215+
TEST_CASE("a deck shows a run's output, error and sounding line on its card", "[carddeck]")
216+
{
217+
Host host;
218+
host.page.show();
219+
CardDeck deck(&host.page, CardDeck::Playing::Layered);
220+
CodeCard* a = host.card("A", "puts :hi\nplay 60");
221+
CodeCard* b = host.card("B", "play 72");
222+
deck.add(a, "ws-a");
223+
deck.add(b, "ws-b");
224+
play(a);
225+
deck.runStarted(7, "ws-a");
226+
227+
deck.runOutput(7, "hi");
228+
deck.runOutput(99, "not the deck's");
229+
CHECK(shown(a, "qsCardOutput").contains("hi"));
230+
CHECK(shown(b, "qsCardOutput").isEmpty());
231+
232+
// A run's lines count the with_fx :scope_out the code is wrapped in.
233+
deck.flashLine("ws-a", 3);
234+
CHECK(a->codeText()->washedLine() == 1);
235+
deck.runError(7, "Unknown function `plya`", 3);
236+
CHECK(shown(a, "qsCardError") == "Unknown function `plya` (line 2)");
237+
}

‎app/gui-tests/linkaudio_engine_ready.test.cpp‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
#include <catch2/catch_test_macros.hpp>
2121

2222
#include <QApplication>
23-
#include <QTemporaryDir>
2423

2524
#include <memory>
2625
#include <string>
2726
#include <vector>
2827

2928
#include "api/osc/osc_pkt.hh"
3029
#include "api/sonicpi_api.h"
30+
#include "own_settings.h"
3131
#include "utils/gui_settings.h"
3232
#include "widgets/linkaudiostreamswidget.h"
3333

@@ -80,15 +80,6 @@ struct ListeningEngine : SonicPiAPI
8080
std::vector<oscpkt::Message> sent;
8181
};
8282

83-
// A gui.ini of the test's own, put back as it was afterwards.
84-
struct OwnSettings
85-
{
86-
QTemporaryDir dir;
87-
QString before = SonicPi::guiSettingsPath();
88-
OwnSettings() { SonicPi::setGuiSettingsPath(dir.filePath("gui.ini")); }
89-
~OwnSettings() { SonicPi::setGuiSettingsPath(before); }
90-
};
91-
9283
const std::string kPublish = "/clockwork/clock/audio/publish/set";
9384

9485
} // namespace

‎app/gui-tests/own_settings.h‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//--
2+
// This file is part of Sonic Pi: http://sonic-pi.net
3+
// Full project source: https://github.com/sonic-pi-net/sonic-pi
4+
// License: https://github.com/sonic-pi-net/sonic-pi/blob/main/LICENSE.md
5+
//
6+
// Copyright 2026 by Sam Aaron (http://sam.aaron.name).
7+
// All rights reserved.
8+
//
9+
// Permission is granted for use, copying, modification, and
10+
// distribution of modified versions of this work as long as this
11+
// notice is included.
12+
//++
13+
14+
#pragma once
15+
16+
#include <QTemporaryDir>
17+
18+
#include "utils/gui_settings.h"
19+
20+
// A gui.ini of the test's own, put back as it was afterwards: what a widget
21+
// keeps between launches never reaches the user's.
22+
struct OwnSettings
23+
{
24+
QTemporaryDir dir;
25+
QString before = SonicPi::guiSettingsPath();
26+
OwnSettings() { SonicPi::setGuiSettingsPath(dir.filePath("gui.ini")); }
27+
~OwnSettings() { SonicPi::setGuiSettingsPath(before); }
28+
};

0 commit comments

Comments
 (0)