1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: MIT
3
4struct Piece {
5 // col/row position of the tile in the puzzle
6 pos-x: int,
7 pos-y: int,
8 // offset in pixel from the base position for the kicking animation
9 offset-x: length,
10 offset-y: length,
11}
12
13struct Theme {
14 name: string,
15 window-background-color: brush,
16 game-background-color: brush,
17 game-use-background-image: bool,
18 game-border: length,
19 game-radius: length,
20 game-text-color: color,
21 game-highlight-color: color,
22 piece-border: length,
23 piece-background-1: brush,
24 piece-background-2: brush,
25 piece-border-color-1: brush,
26 piece-border-color-2: brush,
27 piece-text-color-1: color,
28 piece-text-color-2: color,
29 piece-text-weight-incorrect-pos: int,
30 piece-text-weight-correct-pos: int,
31 piece-text-font-family: string,
32 piece-radius: length,
33 /// Ratio of the piece size
34 piece-spacing: float,
35}
36
37component Checkbox inherits Rectangle {
38 in property <color> checked-color;
39 in property <color> unchecked-color;
40 in-out property <bool> checked;
41
42 callback toggled(bool);
43
44 states [
45 /* pressed when ta.pressed : {
46 clip.width: root.width;
47 root.border-color: checked_color;
48 root.border-width: root.width;
49 }*/
50 checked when root.checked : {
51 clip.width: root.width;
52 checkbox-rect.border-color: root.checked-color;
53 checkbox-rect.border-width: root.width;
54 in {
55 animate clip.width { duration: 200ms; easing: ease-in; }
56 animate checkbox-rect.border-width { duration: 100ms; easing: ease-out; }
57 }
58 out {
59 animate clip.width { duration: 100ms; easing: ease; }
60 animate checkbox-rect.border-width { duration: 200ms; easing: ease-in-out; }
61 animate checkbox-rect.border-color { duration: 200ms; easing: cubic-bezier(1,1,1,0); }
62 }
63 }
64 ]
65
66 hover-rect := Rectangle {
67 background: #f5f5f5;
68 x: - parent.width / 4;
69 y: - parent.height / 4;
70 width: ta.has-hover ? root.width * 1.5 : 0px;
71 height: self.width;
72 border-radius: self.width;
73 }
74
75 checkbox-rect := Rectangle {
76 border-width: self.height * 10%;
77 border-color: root.unchecked-color;
78 border-radius: 2px;
79
80 clip := Rectangle {
81 x:0;
82 width: 0px;
83 clip: true;
84
85 Text {
86 x:0;y:0;
87 width: root.width;
88 height: root.height;
89 text: "✓";
90 font-size: self.height * 80%;
91 color: white;
92 vertical-alignment: center;
93 horizontal-alignment: center;
94
95 animate color { duration: 200ms; }
96 }
97 }
98
99 ta := TouchArea {
100 clicked => {
101 root.checked = !root.checked;
102 root.toggled(root.checked);
103 }
104 }
105
106 }
107}
108
109import "./plaster-font/Plaster-Regular.ttf";
110
111export component MainWindow inherits Window {
112 in property <[Piece]> pieces: [
113 { pos-x: 0, pos-y: 0 },
114 { pos-x: 0, pos-y: 1 },
115 { pos-x: 0, pos-y: 2 },
116 { pos-x: 0, pos-y: 3 },
117 { pos-x: 1, pos-y: 0 },
118 { pos-x: 1, pos-y: 1 },
119 { pos-x: 1, pos-y: 2 },
120 { pos-x: 1, pos-y: 3 },
121 { pos-x: 2, pos-y: 0 },
122 { pos-x: 2, pos-y: 1 },
123 { pos-x: 2, pos-y: 2 },
124 { pos-x: 2, pos-y: 3 },
125 { pos-x: 3, pos-y: 0 },
126 { pos-x: 3, pos-y: 1 },
127 { pos-x: 3, pos-y: 2 },
128 ];
129 out property <int> current-theme-index;
130 in-out property <bool> auto-play;
131 in-out property <int> moves;
132 in-out property <int> tiles-left;
133
134 callback piece-clicked(int);
135 callback reset();
136 callback enable-auto-mode(bool);
137
138 private property <[Theme]> themes: [
139 {
140 name: "SIMPLE",
141 window-background-color: #ffffff,
142 game-background-color: #ffffff,
143 game-use-background-image: false,
144 game-border: 1px,
145 game-radius: 2px,
146 game-text-color: #858585,
147 game-highlight-color: #1d6aaa,
148 piece-border: 1px,
149 piece-background-1: #0d579b,
150 piece-background-2: #0d579b,
151 piece-border-color-1: #0a457b,
152 piece-border-color-2: #0a457b,
153 piece-text-color-1: #ffffff,
154 piece-text-color-2: #ffffff,
155 piece-text-weight-incorrect-pos: 400,
156 piece-text-weight-correct-pos: 700,
157 piece-radius: 5px,
158 /// Ratio of the piece size
159 piece-spacing: 10%,
160 },
161 {
162 name: "BERLIN",
163 window-background-color: #ffffff88,
164 game-background-color: #ffffffcc,
165 game-use-background-image: true,
166 game-border: 0px,
167 game-radius: 2px,
168 game-text-color: #858585,
169 game-highlight-color: #1d6aaa,
170 piece-border: 0px,
171 piece-background-1: #43689e,
172 piece-background-2: #2f2a14,
173 piece-border-color-1: #0000,
174 piece-border-color-2: #0000,
175 piece-text-color-1: #000000,
176 piece-text-color-2: #ffffff,
177 piece-text-weight-incorrect-pos: 700,
178 piece-text-weight-correct-pos: 700,
179 piece-radius: 0px,
180 /// Ratio of the piece size
181 piece-spacing: 8%,
182 },
183 {
184 name: "PLASTER",
185 window-background-color: #424244,
186 game-background-color: #f8f4e9,
187 game-use-background-image: false,
188 game-border: 5px,
189 game-radius: 20px,
190 game-text-color: #858585,
191 game-highlight-color: #e06b53,
192 piece-border: 4px,
193 piece-background-1: #e06b53,
194 piece-background-2: #f8f4e9,
195 piece-border-color-1: #424244,
196 piece-border-color-2: #e06b53,
197 piece-text-color-1: #f8f4e9,
198 piece-text-color-2: #424244,
199 piece-text-weight-incorrect-pos: 700,
200 piece-text-weight-correct-pos: 700,
201 piece-text-font-family: "Plaster",
202 piece-radius: 5px,
203 /// Ratio of the piece size
204 piece-spacing: 10%,
205 },
206 ];
207 private property <Theme> current-theme: root.themes[root.current-theme-index];
208 private property <length> pieces-size: min(root.width, root.height) / 6;
209 private property <length> pieces-spacing: root.current-theme.game-use-background-image && root.tiles-left == 0 ?
210 2px : (root.pieces-size * root.current-theme.piece-spacing);
211
212
213
214 title: "Slide Puzzle - Slint Demo";
215
216 animate pieces-spacing { duration: 500ms; easing: ease-out; }
217
218 Image {
219 // For the wasm build we want the puzzle to resize with the browser viewport, as per CSS in index.html.
220 // Our winit backend preserves the CSS set size if there's no preferred size set on the Slint window.
221 // This image propagates its preferred size and that means the window won't scale. By positioning it
222 // manually, the preferred size is ignored.
223 x: 0; y: 0;
224 height: 100%; width: 100%;
225 // https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
226 source: @image-url("berlin.jpg");
227 image-fit: cover;
228 }
229
230 Rectangle {
231 background: root.current-theme.window-background-color;
232 animate background { duration: 500ms; easing: ease-out; }
233 }
234
235 Rectangle {
236 background: root.current-theme.game-background-color;
237 border-color: root.current-theme.game-text-color;
238 border-width: root.current-theme.game-border;
239 border-radius: root.current-theme.game-radius;
240 width: root.pieces-size * 4.6;
241 height: root.pieces-size * 5.4;
242 x: (parent.width - self.width)/2;
243 y: (parent.height - self.height)/2;
244 animate background, border-color, border-width, border-radius { duration: 500ms; easing: ease-out; }
245
246 Rectangle {
247 y:0;
248 width: parent.width * 90%;
249 height: root.pieces-size/2;
250 x: (parent.width - self.width) / 2;
251
252 HorizontalLayout {
253 spacing: 0px;
254
255 for theme[idx] in root.themes: TouchArea {
256 t := Text {
257 width: 100%; height: 100%;
258 text: theme.name;
259 color: idx == root.current-theme-index ? root.current-theme.game-highlight-color : root.current-theme.game-text-color;
260 vertical-alignment: center;
261 horizontal-alignment: center;
262 }
263 Rectangle {
264 background: t.color;
265 height: idx == root.current-theme-index ? 2px: 1px;
266 y: parent.height - self.height;
267 }
268 clicked => {
269 root.current-theme = theme;
270 root.current-theme-index = idx;
271 }
272 }
273 }
274 }
275
276
277 for p[i] in root.pieces : Rectangle {
278 property <float> px: p.pos-x;
279 property <float> py: p.pos-y;
280 property <bool> is-correct: i == p.pos-x * 4 + p.pos-y;
281
282 x: self.py * (root.pieces-size + root.pieces-spacing) + p.offset-x
283 + (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2;
284 y: self.px * (root.pieces-size + root.pieces-spacing) + p.offset-y
285 + (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2;
286 width: root.pieces-size;
287 height: root.pieces-size;
288 drop-shadow-offset-x: 1px;
289 drop-shadow-offset-y: 1px;
290 drop-shadow-blur: 3px;
291 drop-shadow-color: #00000040;
292 border-radius: root.current-theme.piece-radius;
293 clip: true;
294
295 states [
296 pressed when touch.pressed : {
297 shadow.color: #0002;
298 circle.width: shadow.width * 2 * 1.4142;
299 in {
300 animate shadow.color { duration: 50ms; }
301 animate circle.width { duration: 2s; easing: ease-out; }
302 }
303 out {
304 animate shadow.color { duration: 50ms; }
305 }
306 }
307 hover when touch.has-hover: {
308 shadow.color: #0000000d;
309 }
310 ]
311
312 animate px , py { duration: 170ms; easing: cubic-bezier(0.17,0.76,0.4,1.75); }
313
314 if (root.current-theme.game-use-background-image) : Image {
315 height: 100%; width: 100%;
316 // https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
317 source: @image-url("berlin.jpg");
318 source-clip-x: mod(i, 4) * self.source.width / 4;
319 source-clip-y: floor(i / 4) * self.source.height / 4;
320 source-clip-width: self.source.width / 4;
321 source-clip-height: self.source.height / 4;
322
323 if (root.tiles-left != 0) : Rectangle {
324 width: 60%;
325 height: 60%;
326 x: (parent.width - self.width) / 2;
327 y: (parent.height - self.height) / 2;
328 border-radius: self.width;
329 background: is-correct ? #0008 : #fff8;
330 }
331 }
332
333 if (!root.current-theme.game-use-background-image) : Rectangle {
334 background: i >= 8 ? root.current-theme.piece-background-2 : root.current-theme.piece-background-1;
335 border-color: i >= 8 ? root.current-theme.piece-border-color-2 : root.current-theme.piece-border-color-1;
336 border-width: root.current-theme.piece-border;
337 border-radius: root.current-theme.piece-radius;
338
339 animate border-width, border-radius { duration: 500ms; easing: ease-out; }
340 }
341
342 if (!root.current-theme.game-use-background-image || root.tiles-left > 0) : Text {
343 text: i+1;
344 color: ((!root.current-theme.game-use-background-image && i >= 8) || (root.current-theme.game-use-background-image && is-correct)) ? root.current-theme.piece-text-color-2 : root.current-theme.piece-text-color-1;
345 font-size: root.pieces-size / 3;
346 font-weight: is-correct ? root.current-theme.piece-text-weight-correct-pos : root.current-theme.piece-text-weight-incorrect-pos;
347 font-family: root.current-theme.piece-text-font-family;
348 vertical-alignment: center;
349 horizontal-alignment: center;
350 width: 100%;
351 height: 100%;
352 }
353
354 touch := TouchArea {
355 clicked => { root.piece-clicked(i); }
356 }
357
358 shadow := Rectangle {
359 circle := Rectangle {
360 height: self.width;
361 border-radius: self.width/2;
362 background: #0002;
363 x: touch.pressed-x - self.width/2;
364 y: touch.pressed-y - self.width/2;
365 }
366 }
367 }
368
369 if (root.tiles-left == 0) : Text {
370 width: root.pieces-size;
371 height: root.pieces-size;
372 x: 3 * (root.pieces-size + root.pieces-spacing)
373 + (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2;
374 y: 3 * (root.pieces-size + root.pieces-spacing)
375 + (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2;
376
377 color: root.current-theme.game-highlight-color;
378 font-size: root.pieces-size / 2;
379 vertical-alignment: center;
380 horizontal-alignment: center;
381 text: "🖒";
382
383 if (root.current-theme.game-use-background-image) : Image {
384 height: 100%; width: 100%;
385 // https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
386 source: @image-url("berlin.jpg");
387 source-clip-x: 3 * self.source.width / 4;
388 source-clip-y: 3 * self.source.height / 4;
389 source-clip-width: self.source.width / 4;
390 source-clip-height: self.source.height / 4;
391 }
392 }
393
394 Rectangle {
395 width: parent.width;
396 height: 1px;
397 background: root.current-theme.game-text-color;
398 y: parent.height - root.pieces-size / 2;
399 }
400
401 HorizontalLayout {
402 height: root.pieces-size / 2;
403 y: parent.height - root.pieces-size / 2;
404 width: parent.width;
405 padding: self.height * 25%;
406
407 Text {
408 text: " ↻ ";
409 font-size: parent.height * 40%;
410 color: root.current-theme.game-highlight-color;
411 vertical-alignment: center;
412 TouchArea {
413 clicked => { root.reset(); }
414 }
415 }
416
417 Checkbox {
418 toggled(checked) => { root.enable-auto-mode(self.checked) }
419
420 width: parent.height - 2 * parent.padding;
421 checked <=> root.auto-play;
422 checked-color: root.current-theme.game-highlight-color;
423 unchecked-color: root.current-theme.game-text-color;
424 }
425
426 Rectangle {} // stretch
427
428 Text {
429 text: root.moves;
430 color: root.current-theme.game-highlight-color;
431 vertical-alignment: center;
432 }
433
434 Text {
435 text: "Moves ";
436 color: root.current-theme.game-text-color;
437 vertical-alignment: center;
438 }
439
440 Text {
441 text: root.tiles-left;
442 color: root.current-theme.game-highlight-color;
443 vertical-alignment: center;
444 }
445
446 Text {
447 text: "Tiles left";
448 color: root.current-theme.game-text-color;
449 vertical-alignment: center;
450 }
451 }
452 }
453}
454