Skip to content

Commit b032bbe

Browse files
committed
Initial steps.
1 parent 2e61f3f commit b032bbe

File tree

8 files changed

+858
-0
lines changed

8 files changed

+858
-0
lines changed

demo/SquarePineDemo.jucer

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@
9494
file="source/demos/EffectChainDemo.cpp"/>
9595
<FILE id="KagqFb" name="EffectChainDemo.h" compile="0" resource="0"
9696
file="source/demos/EffectChainDemo.h"/>
97+
<FILE id="ndT6eG" name="FigmaClientDemo.cpp" compile="0" resource="0"
98+
file="source/demos/FigmaClientDemo.cpp"/>
99+
<FILE id="KmImnB" name="FigmaClientDemo.h" compile="0" resource="0"
100+
file="source/demos/FigmaClientDemo.h"/>
97101
<FILE id="brHreQ" name="iCUESDKDemo.h" compile="0" resource="0" file="source/demos/iCUESDKDemo.h"/>
98102
<FILE id="vSHEQC" name="ImageDemo.h" compile="0" resource="0" file="source/demos/ImageDemo.h"/>
99103
<FILE id="uzQ1sq" name="MarkdownDemo.h" compile="0" resource="0" file="source/demos/MarkdownDemo.h"/>

demo/source/MainModule.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "core/SharedObjects.cpp"
44
#include "demos/AnimationDemo.cpp"
55
#include "demos/EffectChainDemo.cpp"
6+
#include "demos/FigmaClientDemo.cpp"
67
#include "demos/ParticleSystemDemo.cpp"
78
#include "main/DemoLookAndFeel.cpp"
89
#include "main/MainComponent.cpp"

demo/source/MainModule.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "demos/iCUESDKDemo.h"
2727
#include "demos/EasingsDemo.h"
2828
#include "demos/EffectChainDemo.h"
29+
#include "demos/FigmaClientDemo.h"
2930
#include "demos/ImageDemo.h"
3031
#include "demos/MarkdownDemo.h"
3132
#include "demos/MediaDeviceListerDemo.h"

demo/source/demos/FigmaClientDemo.cpp

Whitespace-only changes.
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
/** */
2+
class FigmaClientDemo final : public DemoBase,
3+
private Timer
4+
{
5+
public:
6+
/** */
7+
FigmaClientDemo (SharedObjects& sharedObjs) :
8+
DemoBase (sharedObjs, NEEDS_TRANS ("Figma Client")),
9+
threadPool (sharedObjs.threadPool)
10+
{
11+
// Configure input fields
12+
tokenInput.setMultiLine (false);
13+
tokenInput.setReturnKeyStartsNewLine (false);
14+
tokenInput.setTextToShowWhenEmpty (TRANS ("Enter your Figma personal access token..."), Colours::grey);
15+
tokenInput.setPasswordCharacter ('*');
16+
17+
fileKeyInput.setMultiLine (false);
18+
fileKeyInput.setReturnKeyStartsNewLine (false);
19+
fileKeyInput.setTextToShowWhenEmpty (TRANS ("Enter Figma file key (from URL)..."), Colours::grey);
20+
21+
// Configure buttons
22+
loadButton.onClick = [this] { loadFigmaFile(); };
23+
clearButton.onClick = [this] { clearContent(); };
24+
25+
// Add components
26+
addAndMakeVisible (tokenInput);
27+
addAndMakeVisible (fileKeyInput);
28+
addAndMakeVisible (loadButton);
29+
addAndMakeVisible (clearButton);
30+
addAndMakeVisible (statusLabel);
31+
addAndMakeVisible (figmaComponent);
32+
33+
// Set initial state
34+
statusLabel.setJustificationType (Justification::centred);
35+
statusLabel.setColour (Label::textColourId, Colours::white);
36+
37+
figmaComponent.setVisible (false);
38+
}
39+
40+
~FigmaClientDemo() override
41+
{
42+
if (loadingJob != nullptr
43+
&& ! threadPool->removeJob (loadingJob.get(), true, 5000))
44+
{
45+
jassertfalse;
46+
}
47+
}
48+
49+
//==============================================================================
50+
/** @internal */
51+
void paint (Graphics& g) override
52+
{
53+
if (! figmaComponent.isVisible())
54+
{
55+
g.setColour (Colours::white);
56+
sp::drawFittedText (g, TRANS ("Enter your Figma token and file key to load a design!"),
57+
getLocalBounds(), Justification::centred);
58+
}
59+
}
60+
61+
/** @internal */
62+
void resized() override
63+
{
64+
auto bounds = getLocalBounds().reduced (dims::marginPx);
65+
66+
// Top controls area
67+
auto controlsArea = bounds.removeFromTop (120);
68+
69+
// Token input
70+
auto tokenArea = controlsArea.removeFromTop (30);
71+
tokenInput.setBounds (tokenArea);
72+
73+
// File key input
74+
auto fileKeyArea = controlsArea.removeFromTop (30);
75+
fileKeyInput.setBounds (fileKeyArea);
76+
77+
// Buttons
78+
auto buttonArea = controlsArea.removeFromTop (30);
79+
loadButton.setBounds (buttonArea.removeFromLeft (120));
80+
clearButton.setBounds (buttonArea.removeFromLeft (120));
81+
82+
// Status label
83+
auto statusArea = controlsArea.removeFromTop (30);
84+
statusLabel.setBounds (statusArea);
85+
86+
// Figma content area
87+
figmaComponent.setBounds (bounds);
88+
}
89+
90+
void updateWithNewTranslations() override
91+
{
92+
tokenInput.setTextToShowWhenEmpty (TRANS ("Enter your Figma personal access token..."), Colours::grey);
93+
fileKeyInput.setTextToShowWhenEmpty (TRANS ("Enter Figma file key (from URL)..."), Colours::grey);
94+
loadButton.setButtonText (TRANS ("Load File"));
95+
clearButton.setButtonText (TRANS ("Clear"));
96+
statusLabel.setText (TRANS ("Ready to load Figma file"), dontSendNotification);
97+
repaint();
98+
}
99+
100+
void timerCallback() override
101+
{
102+
}
103+
104+
private:
105+
//==============================================================================
106+
class FigmaLoadingJob final : public ThreadPoolJob
107+
{
108+
public:
109+
FigmaLoadingJob (const String& token, const String& fileKey) :
110+
ThreadPoolJob ("FigmaLoadingJob"),
111+
personalAccessToken (token),
112+
figmaFileKey (fileKey)
113+
{
114+
}
115+
116+
std::function<void(var, String)> onComplete;
117+
118+
JobStatus runJob() override
119+
{
120+
FigmaClient client (personalAccessToken);
121+
String errorMessage;
122+
123+
auto fileData = client.fetchFile (figmaFileKey, &errorMessage);
124+
125+
if (onComplete != nullptr)
126+
onComplete (fileData, errorMessage);
127+
128+
return jobHasFinished;
129+
}
130+
131+
private:
132+
const String personalAccessToken, figmaFileKey;
133+
134+
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FigmaLoadingJob)
135+
};
136+
137+
class FigmaDisplayComponent final : public Component
138+
{
139+
public:
140+
FigmaDisplayComponent() = default;
141+
142+
void setFigmaData (const var& fileData, FigmaClient& client, const String& fileKey)
143+
{
144+
if (fileData.isVoid())
145+
{
146+
setVisible (false);
147+
rootNode = nullptr;
148+
figmaClient = nullptr;
149+
return;
150+
}
151+
152+
rootNode = client.parseDocumentTree (fileData);
153+
figmaClient = &client;
154+
figmaFileKey = fileKey;
155+
156+
// Calculate bounds and set component size
157+
if (rootNode != nullptr && ! rootNode->children.isEmpty())
158+
{
159+
auto bounds = rootNode->children.getFirst()->bounds;
160+
for (const auto& child : rootNode->children)
161+
bounds = bounds.getUnion (child.bounds);
162+
163+
// Scale to fit reasonably in the demo
164+
const auto maxWidth = 800.0f;
165+
const auto maxHeight = 600.0f;
166+
const auto scaleX = maxWidth / bounds.getWidth();
167+
const auto scaleY = maxHeight / bounds.getHeight();
168+
const auto scale = jmin (scaleX, scaleY, 1.0f);
169+
170+
setSize ((int) (bounds.getWidth() * scale), (int) (bounds.getHeight() * scale));
171+
this->scale = scale;
172+
}
173+
174+
setVisible (true);
175+
repaint();
176+
}
177+
178+
void paint (Graphics& g) override
179+
{
180+
g.fillAll (Colours::darkgrey);
181+
182+
if (rootNode == nullptr || rootNode->id.isEmpty())
183+
return;
184+
185+
g.saveState();
186+
g.addTransform (AffineTransform::scale (scale));
187+
paintNode (g, *rootNode);
188+
g.restoreState();
189+
}
190+
191+
private:
192+
std::unique_ptr<FigmaNode> rootNode;
193+
FigmaClient* figmaClient = nullptr;
194+
String figmaFileKey;
195+
float scale = 1.0f;
196+
197+
void paintNode (Graphics& g, const FigmaNode& node)
198+
{
199+
const auto& bounds = node.bounds;
200+
201+
// Paint fills
202+
for (const auto& fill : node.fills)
203+
{
204+
if (fill.type == FigmaPaint::Type::solid)
205+
{
206+
g.setColour (fill.colour.withAlpha (fill.opacity));
207+
208+
if (node.type == "RECTANGLE")
209+
g.fillRect (bounds);
210+
else if (node.type == "ELLIPSE")
211+
g.fillEllipse (bounds);
212+
}
213+
else if (fill.type == FigmaPaint::Type::linearGradient || fill.type == FigmaPaint::Type::radialGradient)
214+
{
215+
g.setGradientFill (fill.gradient);
216+
217+
if (node.type == "RECTANGLE")
218+
g.fillRect (bounds);
219+
else if (node.type == "ELLIPSE")
220+
g.fillEllipse (bounds);
221+
}
222+
}
223+
224+
// Paint strokes
225+
if (! node.strokes.isEmpty() && node.strokeWeight > 0.0f)
226+
{
227+
Path path;
228+
if (node.type == "RECTANGLE")
229+
path.addRectangle (bounds);
230+
else if (node.type == "ELLIPSE")
231+
path.addEllipse (bounds);
232+
233+
for (const auto& stroke : node.strokes)
234+
{
235+
g.setColour (stroke.colour.withAlpha (stroke.opacity));
236+
PathStrokeType strokeType (node.strokeWeight);
237+
g.strokePath (path, strokeType);
238+
}
239+
}
240+
241+
// Paint text
242+
if (node.type == "TEXT" && node.textContent.isNotEmpty())
243+
{
244+
Font font (node.textStyle.fontFamily, node.textStyle.fontSize, Font::plain);
245+
g.setFont (font);
246+
g.setColour (node.textStyle.fillColour);
247+
g.drawText (node.textContent, bounds, Justification::centred);
248+
}
249+
250+
// Recursively paint children
251+
for (const auto& child : node.children)
252+
paintNode (g, *child);
253+
}
254+
255+
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FigmaDisplayComponent)
256+
};
257+
258+
//==============================================================================
259+
std::shared_ptr<ThreadPool> threadPool;
260+
261+
TextEditor tokenInput;
262+
TextEditor fileKeyInput;
263+
TextButton loadButton;
264+
TextButton clearButton;
265+
Label statusLabel;
266+
FigmaDisplayComponent figmaComponent;
267+
268+
std::unique_ptr<FigmaLoadingJob> loadingJob;
269+
std::unique_ptr<FigmaClient> figmaClient;
270+
271+
//==============================================================================
272+
void loadFigmaFile()
273+
{
274+
const auto token = tokenInput.getText().trim();
275+
const auto fileKey = fileKeyInput.getText().trim();
276+
277+
if (token.isEmpty() || fileKey.isEmpty())
278+
{
279+
statusLabel.setText (TRANS ("Please enter both token and file key"), dontSendNotification);
280+
statusLabel.setColour (Label::textColourId, Colours::orange);
281+
return;
282+
}
283+
284+
if (loadingJob != nullptr)
285+
{
286+
if (! threadPool->removeJob (loadingJob.get(), true, 2000))
287+
return;
288+
}
289+
290+
statusLabel.setText (TRANS ("Loading Figma file..."), dontSendNotification);
291+
statusLabel.setColour (Label::textColourId, Colours::lightblue);
292+
293+
loadingJob = std::make_unique<FigmaLoadingJob> (token, fileKey);
294+
295+
SafePointer sp (this);
296+
loadingJob->onComplete = [sp] (var fileData, String errorMessage)
297+
{
298+
MessageManager::callAsync ([sp, fileData, errorMessage]()
299+
{
300+
if (sp == nullptr)
301+
return;
302+
303+
if (errorMessage.isNotEmpty())
304+
{
305+
sp->statusLabel.setText (TRANS ("Error: ") + errorMessage, dontSendNotification);
306+
sp->statusLabel.setColour (Label::textColourId, Colours::red);
307+
sp->figmaComponent.setVisible (false);
308+
}
309+
else
310+
{
311+
sp->statusLabel.setText (TRANS ("File loaded successfully!"), dontSendNotification);
312+
sp->statusLabel.setColour (Label::textColourId, Colours::lightgreen);
313+
314+
sp->figmaClient = std::make_unique<FigmaClient> (sp->tokenInput.getText().trim());
315+
sp->figmaComponent.setFigmaData (fileData, *sp->figmaClient, sp->fileKeyInput.getText().trim());
316+
}
317+
318+
sp->loadingJob = nullptr;
319+
sp->repaint();
320+
});
321+
};
322+
323+
threadPool->addJob (loadingJob.get(), false);
324+
}
325+
326+
void clearContent()
327+
{
328+
tokenInput.clear();
329+
fileKeyInput.clear();
330+
figmaComponent.setVisible (false);
331+
statusLabel.setText (TRANS ("Ready to load Figma file"), dontSendNotification);
332+
statusLabel.setColour (Label::textColourId, Colours::white);
333+
figmaClient = nullptr;
334+
repaint();
335+
}
336+
337+
//==============================================================================
338+
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FigmaClientDemo)
339+
};

demo/source/main/MainComponent.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ MainComponent::MainComponent (SharedObjects& sharedObjs) :
2929
addDemo (new CRCDemo (sharedObjs));
3030
addDemo (new EaseListComponent (sharedObjs));
3131
addDemo (new ImageDemo (sharedObjs));
32+
addDemo (new FigmaClientDemo (sharedObjs));
3233
addDemo (new AnimationDemo (sharedObjs));
3334
addDemo (new ParticleSystemDemo (sharedObjs));
3435
addDemo (new EffectChainDemo (sharedObjs));

0 commit comments

Comments
 (0)