<!DOCTYPE HTML>
<html lang="en" class="sidebar-visible no-js light">
<head>
<!-- Book generated using mdBook -->
<meta charset="UTF-8">
<title>Slint Memory Game Tutorial (node)</title>
<meta name="robots" content="noindex" />
<!-- Custom HTML head -->
<!--
This file is used to add syntax highlighting of the `.slint` snippets in the generated rustdoc, sphinx and mdbook documentation.
It can be injected via the `--html-in-header slint-docs-highlight.html` option of rustdoc, is included via _templates/layout.html
in sphinx and via head.hbs in mdbook.
-->
<link rel="stylesheet" href="https://slint.dev/resources/highlightjs/11.0.1/default.min.css">
<script src="https://slint.dev/resources/highlightjs/11.0.1/highlight.min.js"></script>
<script>
function highlight_slint(hljs) {
const KEYWORDS = {
keyword:
"animate callback component export for function global if import in in-out inherits out parent private property public pure root self signal states struct transitions",
literal: "false true",
built_in:
"ArcTo Clip Close Colors CubicTo Flickable FocusScope GridLayout HorizontalLayout Image LineTo Math MoveTo Path PopupWindow QuadraticTo Rectangle Row Text TextInput TouchArea VerticalLayout Window animation-tick debug",
type: "bool duration easing float int length logical_length relative-font-size resource string",
};
return {
name: 'slint',
case_insensitive: false,
keywords: KEYWORDS,
contains: [
hljs.QUOTE_STRING_MODE,
hljs.C_LINE_COMMENT_MODE,
hljs.C_BLOCK_COMMENT_MODE,
hljs.COMMENT('/\\*', '\\*/', {
contains: ['self']
}),
{
className: 'number',
begin: '\\b\\d+(\\.\\d+)?(\\w+)?',
relevance: 0
},
{
className: 'title',
begin: '\\b[_a-zA-Z][_\\-a-zA-Z0-9]* *:=',
},
{
className: 'symbol',
begin: '\\b[_a-zA-Z][_\\-a-zA-Z0-9]*(:| *=>)',
},
{
className: 'built_in',
begin: '\\b[_a-zA-Z][_\\-a-zA-Z0-9]*!',
},
],
illegal: /@/
};
};
// Tags used in fenced code blocks
for (let tag of ["slint", "slint,no-preview", "slint,no-auto-preview", "slint,ignore"]) {
hljs.registerLanguage(tag, highlight_slint);
}
window.addEventListener("DOMContentLoaded", () => {
const rustDoc = document.querySelector('meta[name="generator"]')?.content == "rustdoc";
if (rustDoc) {
// Only highlight .slint blocks, leave the others to rustdoc
for (slintBlock of document.querySelectorAll("[class*=language-slint]")) {
hljs.highlightElement(slintBlock)
}
// Some of the rustdoc selectors require the pre element to have the rust class
for (codeBlock of document.querySelectorAll(".language-slint.hljs")) {
codeBlock.parentElement.classList.add("rust")
}
// Change the hljs generated classes to the rustdoc
// ones, so that the highlighting adjusts to the theme correctly.
const highlightJSToRustDoc = [
["comment", "comment"],
["number", "number"],
["symbol", "struct"], // width:
["keyword", "kw"],
["built_in", "primitive"],
["string", "string"],
["title", "fnname"], // Foo :=
["type", "type"]
];
for ([hljs_class, rustdoc_class] of highlightJSToRustDoc) {
for (titleElement of document.querySelectorAll(`.hljs-${hljs_class}`)) {
titleElement.classList.remove(`hljs-${hljs_class}`);
titleElement.classList.add(rustdoc_class);
}
}
} else {
// For use with the mdbook Tutorial
hljs.highlightAll();
// The Sphinx/my_st generated HTML for code blocks doesn't use <code> tags, so highlight.js'
// default selector "pre code" doesn't match. Let's do it by hand:
for (block of document.querySelectorAll("div[class*=highlight-slint] div.highlight pre")) {
hljs.highlightElement(block)
}
}
});
</script>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
<link rel="icon" href="favicon.svg">
<link rel="shortcut icon" href="favicon.png">
<link rel="stylesheet" href="css/variables.css">
<link rel="stylesheet" href="css/general.css">
<link rel="stylesheet" href="css/chrome.css">
<link rel="stylesheet" href="css/print.css" media="print">
<!-- Fonts -->
<link rel="stylesheet" href="FontAwesome/css/font-awesome.css">
<link rel="stylesheet" href="fonts/fonts.css">
<!-- Highlight.js Stylesheets -->
<link rel="stylesheet" href="highlight.css">
<link rel="stylesheet" href="tomorrow-night.css">
<link rel="stylesheet" href="ayu-highlight.css">
<!-- Custom theme stylesheets -->
</head>
<body>
<div id="body-container">
<!-- Provide site root to javascript -->
<script>
var path_to_root = "";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "navy" : "light";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
if (theme.startsWith('"') && theme.endsWith('"')) {
localStorage.setItem('mdbook-theme', theme.slice(1, theme.length - 1));
}
if (sidebar.startsWith('"') && sidebar.endsWith('"')) {
localStorage.setItem('mdbook-sidebar', sidebar.slice(1, sidebar.length - 1));
}
} catch (e) { }
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
var html = document.querySelector('html');
html.classList.remove('no-js')
html.classList.remove('light')
html.classList.add(theme);
html.classList.add('js');
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script>
var html = document.querySelector('html');
var sidebar = null;
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
</script>
<nav id="sidebar" class="sidebar" aria-label="Table of contents">
<div class="sidebar-scrollbox">
<ol class="chapter"><li class="chapter-item expanded "><a href="introduction.html"><strong aria-hidden="true">1.</strong> Introduction</a></li><li class="chapter-item expanded "><a href="getting_started.html"><strong aria-hidden="true">2.</strong> Getting Started</a></li><li class="chapter-item expanded "><a href="memory_tile.html"><strong aria-hidden="true">3.</strong> Memory Tile</a></li><li class="chapter-item expanded "><a href="polishing_the_tile.html"><strong aria-hidden="true">4.</strong> Polishing the Tile</a></li><li class="chapter-item expanded "><a href="from_one_to_multiple_tiles.html"><strong aria-hidden="true">5.</strong> From One To Multiple Tiles</a></li><li class="chapter-item expanded "><a href="creating_the_tiles_from_js.html"><strong aria-hidden="true">6.</strong> Creating The Tiles From JavaScript</a></li><li class="chapter-item expanded "><a href="game_logic_in_js.html"><strong aria-hidden="true">7.</strong> Game Logic In JavaScript</a></li><li class="chapter-item expanded "><a href="ideas_for_the_reader.html"><strong aria-hidden="true">8.</strong> Ideas For The Reader</a></li><li class="chapter-item expanded "><a href="conclusion.html"><strong aria-hidden="true">9.</strong> Conclusion</a></li></ol>
</div>
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
</button>
<button id="theme-toggle" class="icon-button" type="button" title="Change theme" aria-label="Change theme" aria-haspopup="true" aria-expanded="false" aria-controls="theme-list">
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
</ul>
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
<i class="fa fa-search"></i>
</button>
</div>
<h1 class="menu-title">Slint Memory Game Tutorial (node)</h1>
<div class="right-buttons">
<a href="print.html" title="Print this book" aria-label="Print this book">
<i id="print-button" class="fa fa-print"></i>
</a>
</div>
</div>
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
<ul id="searchresults">
</ul>
</div>
</div>
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
link.setAttribute('tabIndex', sidebar === 'visible' ? 0 : -1);
});
</script>
<div id="content" class="content">
<main>
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="introduction"><a class="header" href="#introduction">Introduction</a></h1>
<p>This tutorial will introduce you to the Slint UI framework in a playful way by implementing a little memory game. We're going to combine the <code>.slint</code> language for the graphics with the game rules implemented in JavaScript.</p>
<p>The game consists of a grid of 16 rectangular tiles. Clicking on a tile uncovers an icon underneath.
We know that there are 8 different icons in total, so each tile has a sibling somewhere in the grid with the
same icon. The objective is to locate all icon pairs. You can uncover two tiles at the same time. If they
aren't the same, the icons will be obscured again.
If you uncover two tiles with the same icon, then they remain visible - they're solved.</p>
<p>This is how the game looks like in action:</p>
<p><video autoplay loop muted playsinline src="https://slint.dev/blog/memory-game-tutorial/memory_clip.mp4"
class="img-fluid img-thumbnail rounded"></video></p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="getting-started"><a class="header" href="#getting-started">Getting Started</a></h1>
<p>In this tutorial, we use JavaScript as the host programming language. We also support other programming languages like
<a href="https://slint.dev/docs/rust/slint/">Rust</a> or <a href="https://slint.dev/docs/cpp/">C++</a>.</p>
<p>You'll need a development environment with <a href="https://nodejs.org/download/release/v16.19.1/">Node.js 16</a> and <a href="https://www.npmjs.com/">npm</a> installed. More recent
versions of NodeJS are currently not supported, for details check <a href="https://github.com/slint-ui/slint/issues/961">Issue #961</a>.
Since Slint is implemented in the Rust programming language, you also need to install a Rust compiler (1.70 or newer). You can easily install a Rust compiler
following the instruction from <a href="https://www.rust-lang.org/learn/get-started">the Rust website</a>.
You will also need some additional platform-specific dependencies, see <a href="https://github.com/slint-ui/slint/blob/master/docs/building.md#prerequisites">https://github.com/slint-ui/slint/blob/master/docs/building.md#prerequisites</a></p>
<p>We're going to use <code>slint-ui</code> as <code>npm</code> dependency.</p>
<p>In a new directory, we create a new <code>package.json</code> file.</p>
<pre><code class="language-json">{
"name": "memory",
"version": "1.3.0",
"main": "main.js",
"type": "module",
"dependencies": {
"slint-ui": "^1.0.0"
},
"scripts": {
"start": "node ."
}
}
</code></pre>
<p>This should look familiar to people familiar with NodeJS. We see that this package.json
references a <code>main.js</code>, which we will add later. We must then create, in the same directory,
the <code>memory.slint</code> file. Let's just fill it with a hello world for now:</p>
<pre><code class="language-slint">// memory.slint
export component MainWindow inherits Window {
Text {
text: "hello world";
color: green;
}
}
</code></pre>
<p>What's still missing is the <code>main.js</code>:</p>
<pre><code class="language-js">// main.js
import * as slint from "slint-ui";
let ui = slint.loadFile("./memory.slint");
let mainWindow = new ui.MainWindow();
mainWindow.run();
</code></pre>
<p>To recap, we now have a directory with a <code>package.json</code>, <code>memory.slint</code>, and <code>main.js</code>.</p>
<p>We can now compile and run the program:</p>
<pre><code class="language-sh">npm install
npm start
</code></pre>
<p>and a window will appear with the green "Hello World" greeting.</p>
<p><img src="https://slint.dev/blog/memory-game-tutorial/getting-started.png" alt="Screenshot of initial tutorial app showing Hello World" title="Hello World" /></p>
<p>Feel free to use your favorite IDE for this purpose.</p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="memory-tile"><a class="header" href="#memory-tile">Memory Tile</a></h1>
<p>With the skeleton in place, let's look at the first element of the game, the memory tile. It will be the
visual building block that consists of an underlying filled rectangle background, the icon image. Later we'll add a
covering rectangle that acts as a curtain. The background rectangle is declared to be 64 logical pixels wide and tall,
and it's filled with a soothing tone of blue. Note how lengths in the <code>.slint</code> language have a unit, here
the <code>px</code> suffix. That makes the code easier to read and the compiler can detect when your are accidentally
mixing values with different units attached to them.</p>
<p>We copy the following code into the <code>memory.slint</code> file:</p>
<pre><code class="language-slint">component MemoryTile inherits Rectangle {
width: 64px;
height: 64px;
background: #3960D5;
Image {
source: @image-url("icons/bus.png");
width: parent.width;
height: parent.height;
}
}
export component MainWindow inherits Window {
MemoryTile {}
}
</code></pre>
<p>Note that we export the <span class="hljs-title">MainWindow</span> component. This is necessary so that we can later access it
from our business logic.</p>
<p>Inside the <span class="hljs-built_in">Rectangle</span> we place an <span class="hljs-built_in">Image</span> element that
loads an icon with the <span class="hljs-built_in">@image-url()</span> macro. The path is relative to the folder in which
the <code>memory.slint</code> is located. This icon and others we're going to use later need to be installed first. You can download a
<a href="https://slint.dev/blog/memory-game-tutorial/icons.zip">Zip archive</a> that we have prepared.</p>
<p>If you are on Linux or macOS, download and extract it with the following two commands:</p>
<pre><code class="language-sh">curl -O https://slint.dev/blog/memory-game-tutorial/icons.zip
unzip icons.zip
</code></pre>
<p>If you are on Windows, use the following commands:</p>
<pre><code>powershell curl -Uri https://slint.dev/blog/memory-game-tutorial/icons.zip -Outfile icons.zip
powershell Expand-Archive -Path icons.zip -DestinationPath .
</code></pre>
<p>This should unpack an <code>icons</code> directory containing a bunch of icons.</p>
<p>We running the program with <code>npm start</code> and it gives us a window on the screen that shows the icon of a bus on a blue background.</p>
<p><img src="https://slint.dev/blog/memory-game-tutorial/memory-tile.png" alt="Screenshot of the first tile" title="Memory Tile Screenshot" /></p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="polishing-the-tile"><a class="header" href="#polishing-the-tile">Polishing the Tile</a></h1>
<p>Next, let's add a curtain like cover that opens up when clicking. We achieve this by declaring two rectangles
below the <span class="hljs-built_in">Image</span>, so that they are drawn afterwards and thus on top of the image.
The <span class="hljs-built_in">TouchArea</span> element declares a transparent rectangular region that allows
reacting to user input such as a mouse click or tap. We use that to forward a callback to the <em>MainWindow</em>
that the tile was clicked on. In the <em>MainWindow</em> we react by flipping a custom <em>open_curtain</em> property.
That in turn is used in property bindings for the animated width and x properties. Let's look at the two states a bit
more in detail:</p>
<div class="table-wrapper"><table><thead><tr><th><em>open_curtain</em> value:</th><th>false</th><th>true</th></tr></thead><tbody>
<tr><td>Left curtain rectangle</td><td>Fill the left half by setting the width <em>width</em> to half the parent's width</td><td>Width of zero makes the rectangle invisible</td></tr>
<tr><td>Right curtain rectangle</td><td>Fill the right half by setting <em>x</em> and <em>width</em> to half of the parent's width</td><td><em>width</em> of zero makes the rectangle invisible. <em>x</em> is moved to the right, to slide the curtain open when animated</td></tr>
</tbody></table>
</div>
<p>In order to make our tile extensible, the hard-coded icon name is replaced with an <em>icon</em>
property that can be set from the outside when instantiating the element. For the final polish, we add a
<em>solved</em> property that we use to animate the color to a shade of green when we've found a pair, later. We
replace the code inside the <code>memory.slint</code> file with the following:</p>
<pre><code class="language-slint">component MemoryTile inherits Rectangle {
callback clicked;
in property <bool> open_curtain;
in property <bool> solved;
in property <image> icon;
height: 64px;
width: 64px;
background: solved ? #34CE57 : #3960D5;
animate background { duration: 800ms; }
Image {
source: icon;
width: parent.width;
height: parent.height;
}
// Left curtain
Rectangle {
background: #193076;
x: 0px;
width: open_curtain ? 0px : (parent.width / 2);
height: parent.height;
animate width { duration: 250ms; easing: ease-in; }
}
// Right curtain
Rectangle {
background: #193076;
x: open_curtain ? parent.width : (parent.width / 2);
width: open_curtain ? 0px : (parent.width / 2);
height: parent.height;
animate width { duration: 250ms; easing: ease-in; }
animate x { duration: 250ms; easing: ease-in; }
}
TouchArea {
clicked => {
// Delegate to the user of this element
root.clicked();
}
}
}
export component MainWindow inherits Window {
MemoryTile {
icon: @image-url("icons/bus.png");
clicked => {
self.open_curtain = !self.open_curtain;
}
}
}
</code></pre>
<p>Note the use of <code>root</code> and <code>self</code> in the code. <code>root</code> refers to the outermost
element in the component, that's the <span class="hljs-title">MemoryTile</span> in this case. <code>self</code> refers
to the current element.</p>
<p>Running this gives us a window on the screen with a rectangle that opens up to show us the bus icon, when clicking on
it. Subsequent clicks will close and open the curtain again.</p>
<p><video autoplay loop muted playsinline src="https://slint.dev/blog/memory-game-tutorial/polishing-the-tile.mp4"></video></p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="from-one-to-multiple-tiles"><a class="header" href="#from-one-to-multiple-tiles">From One To Multiple Tiles</a></h1>
<p>After modeling a single tile, let's create a grid of them. For the grid to be our game board, we need two features:</p>
<ol>
<li>A data model: This shall be an array where each element describes the tile data structure, such as the
url of the image, whether the image shall be visible and if this tile has been solved. We modify the model
from JS code.</li>
<li>A way of creating many instances of the tiles, with the above <code>.slint</code> markup code.</li>
</ol>
<p>In Slint we can declare an array of structures using brackets, to create a model. We can use the <span class="hljs-keyword">for</span> loop
to create many instances of the same element. In <code>.slint</code> the for loop is declarative and automatically updates when
the model changes. We instantiate all the different <span class="hljs-title">MemoryTile</span> elements and place them on a grid based on their
index with a little bit of spacing between the tiles.</p>
<p>First, we copy the tile data structure definition and paste it at top inside the <code>memory.slint</code> file:</p>
<pre><code class="language-slint">
// Added:
struct TileData {
image: image,
image_visible: bool,
solved: bool,
}
component MemoryTile inherits Rectangle {
</code></pre>
<p>Next, we replace the <em>export component <span class="hljs-title">MainWindow</span> inherits Window { ... }</em> section at the bottom of the <code>memory.slint</code> file with the following snippet:</p>
<pre><code class="language-slint">export component MainWindow inherits Window {
width: 326px;
height: 326px;
in property <[TileData]> memory_tiles: [
{ image: @image-url("icons/at.png") },
{ image: @image-url("icons/balance-scale.png") },
{ image: @image-url("icons/bicycle.png") },
{ image: @image-url("icons/bus.png") },
{ image: @image-url("icons/cloud.png") },
{ image: @image-url("icons/cogs.png") },
{ image: @image-url("icons/motorcycle.png") },
{ image: @image-url("icons/video.png") },
];
for tile[i] in memory_tiles : MemoryTile {
x: mod(i, 4) * 74px;
y: floor(i / 4) * 74px;
width: 64px;
height: 64px;
icon: tile.image;
open_curtain: tile.image_visible || tile.solved;
// propagate the solved status from the model to the tile
solved: tile.solved;
clicked => {
tile.image_visible = !tile.image_visible;
}
}
}
</code></pre>
<p>The <code><span class="hljs-keyword">for</span> tile[i] <span class="hljs-keyword">in</span> memory_tiles:</code> syntax declares a variable <code>tile</code> which contains the data of one element from the <code>memory_tiles</code> array,
and a variable <code>i</code> which is the index of the tile. We use the <code>i</code> index to calculate the position of tile based on its row and column,
using the modulo and integer division to create a 4 by 4 grid.</p>
<p>Running this gives us a window that shows 8 tiles, which can be opened individually.</p>
<p><video autoplay loop muted playsinline src="https://slint.dev/blog/memory-game-tutorial/from-one-to-multiple-tiles.mp4"></video></p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="creating-the-tiles-from-javascript"><a class="header" href="#creating-the-tiles-from-javascript">Creating The Tiles From JavaScript</a></h1>
<p>What we'll do is take the list of tiles declared in the .slint language, duplicate it, and shuffle it.
We'll do so by accessing the <code>memory_tiles</code> property through the JavaScript code - in our case <code>memory_tiles</code>.
Since <code>memory_tiles</code> is an array in the <code>.slint</code> language, it's represented as a JavaScript <a href="https://slint.dev/docs/node/"><code>Array</code></a>.
We can't modify the model generated by the .slint, but we can extract the tiles from it, and put it
in a <a href="https://slint.dev/docs/node/classes/arraymodel.html"><code>slint.ArrayModel</code></a> which implements the <a href="https://slint.dev/docs/node/interfaces/model.html"><code>Model</code></a> interface.
<code>ArrayModel</code> allows us to make modifications and we can use it to replace the static generated model.</p>
<p>We modify the main function like so:</p>
<pre><code class="language-js">// main.js
import * as slint from "slint-ui";
let ui = slint.loadFile("./memory.slint");
let mainWindow = new ui.MainWindow();
let initial_tiles = mainWindow.memory_tiles;
let tiles = initial_tiles.concat(initial_tiles.map((tile) => Object.assign({}, tile)));
for (let i = tiles.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * i);
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
}
let model = new slint.ArrayModel(tiles);
mainWindow.memory_tiles = model;
mainWindow.run();
</code></pre>
<p>Running this gives us a window on the screen that now shows a 4 by 4 grid of rectangles, which can show or obscure
the icons when clicking. There's only one last aspect missing now, the rules for the game.</p>
<p><video autoplay loop muted playsinline src="https://slint.dev/blog/memory-game-tutorial/creating-the-tiles-from-rust.mp4"></video></p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="game-logic-in-javascript"><a class="header" href="#game-logic-in-javascript">Game Logic In JavaScript</a></h1>
<p>We'll implement the rules of the game in JavaScript as well. The general philosophy of Slint is that merely the user
interface is implemented in the <code>.slint</code> language and the business logic in your favorite programming
language. The game rules shall enforce that at most two tiles have their curtain open. If the tiles match, then we
consider them solved and they remain open. Otherwise we wait for a little while, so the player can memorize
the location of the icons, and then close them again.</p>
<p>We'll modify the <code>.slint</code> markup in the <code>memory.slint</code> file to signal to the JavaScript code when the user clicks on a tile.
Two changes to <span class="hljs-title">MainWindow</span> are needed: We need to add a way for the MainWindow to call to the JavaScript code that it should
check if a pair of tiles has been solved. And we need to add a property that JavaScript code can toggle to disable further
tile interaction, to prevent the player from opening more tiles than allowed. No cheating allowed! First, we paste
the callback and property declarations into <span class="hljs-title">MainWindow</span>:</p>
<pre><code class="language-slint"> export component MainWindow inherits Window {
width: 326px;
height: 326px;
callback check_if_pair_solved(); // Added
in property <bool> disable_tiles; // Added
in-out property <[TileData]> memory_tiles: [
{ image: @image-url("icons/at.png") },
</code></pre>
<p>The last change to the <code>.slint</code> markup is to act when the <span class="hljs-title">MemoryTile</span> signals that it was clicked on.
We add the following handler in <span class="hljs-title">MainWindow</span>:</p>
<pre><code class="language-slint"> for tile[i] in memory_tiles : MemoryTile {
x: mod(i, 4) * 74px;
y: floor(i / 4) * 74px;
width: 64px;
height: 64px;
icon: tile.image;
open_curtain: tile.image_visible || tile.solved;
// propagate the solved status from the model to the tile
solved: tile.solved;
clicked => {
// old: tile.image_visible = !tile.image_visible;
// new:
if (!root.disable_tiles) {
tile.image_visible = !tile.image_visible;
root.check_if_pair_solved();
}
}
}
</code></pre>
<p>On the JavaScript side, we can now add an handler to the <code>check_if_pair_solved</code> callback, that will check if
two tiles are opened. If they match, the <code>solved</code> property is set to true in the model. If they don't
match, start a timer that will close them after one second. While the timer is running, we disable every tile so
one can't click anything during this time.</p>
<p>Insert this code before the <code>mainWindow.run()</code> call:</p>
<pre><code class="language-js">let model = new slint.ArrayModel(tiles);
mainWindow.memory_tiles = model;
mainWindow.check_if_pair_solved = function () {
let flipped_tiles = [];
tiles.forEach((tile, index) => {
if (tile.image_visible && !tile.solved) {
flipped_tiles.push({
index,
tile
});
}
});
if (flipped_tiles.length == 2) {
let {
tile: tile1,
index: tile1_index
} = flipped_tiles[0];
let {
tile: tile2,
index: tile2_index
} = flipped_tiles[1];
let is_pair_solved = tile1.image.path === tile2.image.path;
if (is_pair_solved) {
tile1.solved = true;
model.setRowData(tile1_index, tile1);
tile2.solved = true;
model.setRowData(tile2_index, tile2);
} else {
mainWindow.disable_tiles = true;
setTimeout(() => {
mainWindow.disable_tiles = false;
tile1.image_visible = false;
model.setRowData(tile1_index, tile1);
tile2.image_visible = false;
model.setRowData(tile2_index, tile2);
}, 1000)
}
}
};
mainWindow.run();
</code></pre>
<p>These were the last changes and running the result gives us a window on the screen that allows us
to play the game by the rules.</p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="ideas-for-the-reader"><a class="header" href="#ideas-for-the-reader">Ideas For The Reader</a></h1>
<p>The game is visually a little bare. Here are some ideas how you could make further changes to enhance it:</p>
<ul>
<li>
<p>The tiles could have rounded corners, to look a little less sharp. The <a href="https://slint.dev/docs/slint/src/builtins/elements.html#rectangle">border-radius</a>
property of <em>Rectangle</em> can be used to achieve that.</p>
</li>
<li>
<p>In real world memory games, the back of the tiles often have some common graphic. You could add an image with
the help of another <em><a href="https://slint.dev/docs/slint/src/builtins/elements.html#image">Image</a></em>
element. Note that you may have to use <em>Rectangle</em>'s <em>clip property</em>
element around it to ensure that the image is clipped away when the curtain effect opens.</p>
</li>
</ul>
<p>Let us know in the comments on <a href="https://github.com/slint-ui/slint/discussions">Github Discussions</a>
how you polished your code, or feel free to ask questions about how to implement something.</p>
<div style="break-before: page; page-break-before: always;"></div><!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
<h1 id="conclusion"><a class="header" href="#conclusion">Conclusion</a></h1>
<p>In this tutorial, we have demonstrated how to combine some built-in Slint elements with JavaScript code to build a little
game. There are many more features that we haven't talked about, such as layouts, widgets, or styling.</p>
<p>We recommend the following links to continue:</p>
<ul>
<li><a href="https://github.com/slint-ui/slint/tree/master/examples">Examples</a>: In the Slint repository we have collected a few demos and examples. These are a great starting point to learn how to use many Slint features.
<ul>
<li><a href="https://github.com/slint-ui/slint/tree/master/examples/todo">Todo Example</a>: This is one of the examples that implements a classic use-case.</li>
<li><a href="https://github.com/slint-ui/slint/tree/master/examples/memory">Memory Puzzle</a>: This is a slightly more polished version of the code in this example. And you can <a href="https://slint.dev/demos/memory/" target="_blank">play the wasm version</a> in your browser.</li>
</ul>
</li>
<li><a href="https://slint.dev/docs/node/">Slint API Docs</a>: The reference documentation for the NodeJS library.</li>
</ul>
</main>
<nav class="nav-wrapper" aria-label="Page navigation">
<!-- Mobile navigation buttons -->
<div style="clear: both"></div>
</nav>
</div>
</div>
<nav class="nav-wide-wrapper" aria-label="Page navigation">
</nav>
</div>
<script>
window.playground_copyable = true;
</script>
<script src="elasticlunr.min.js"></script>
<script src="mark.min.js"></script>
<script src="searcher.js"></script>
<script src="clipboard.min.js"></script>
<script src="highlight.js"></script>
<script src="book.js"></script>
<!-- Custom JS scripts -->
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
</script>
</div>
</body>
</html>