mirror of
https://github.com/slint-ui/slint.git
synced 2025-10-02 06:41:14 +00:00
Initial conversion of the memory game tutorial to mdbook
This is the Rust version and it's still missing the wasm chapter. There's also other bits missing, such as syntax checking, highlighting, etc.
This commit is contained in:
parent
c9d1319ddf
commit
ae0a08ecdd
12 changed files with 462 additions and 0 deletions
1
docs/tutorial/.gitignore
vendored
Normal file
1
docs/tutorial/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
book
|
17
docs/tutorial/README.md
Normal file
17
docs/tutorial/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Requirements
|
||||
|
||||
Building the tutorial requires `mdbook`, which you can install with `cargo`:
|
||||
|
||||
```sh
|
||||
cargo install mdbook
|
||||
```
|
||||
|
||||
# Building
|
||||
|
||||
To build the tutorial, type:
|
||||
|
||||
```sh
|
||||
mdbook build
|
||||
```
|
||||
|
||||
The output will be in the `book/html` subdirectory. To check it out, open it in your web browser.
|
12
docs/tutorial/book.toml
Normal file
12
docs/tutorial/book.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[book]
|
||||
authors = ["SixtyFPS <info@sixtyfps.io>"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "SixtyFPS Memory Game Tutorial"
|
||||
|
||||
[output.html]
|
||||
# additional-js = ["highlight_60.js"]
|
||||
|
||||
[output.linkcheck] # enable the "mdbook-linkcheck" renderer
|
||||
optional = true
|
10
docs/tutorial/src/SUMMARY.md
Normal file
10
docs/tutorial/src/SUMMARY.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Summary
|
||||
|
||||
- [Introduction](./introduction.md)
|
||||
- [Getting Started](./getting_started.md)
|
||||
- [Memory Tile](./memory_tile.md)
|
||||
- [Polishing the Tile](./polishing_the_tile.md)
|
||||
- [From One To Multiple Tiles](./from_one_to_multiple_tiles.md)
|
||||
- [Creating The Tiles From Rust](./creating_the_tiles_from_rust.md)
|
||||
- [Game Logic In Rust](./game_logic_in_rust.md)
|
||||
- [Ideas For The Reader](./ideas_for_the_reader.md)
|
55
docs/tutorial/src/creating_the_tiles_from_rust.md
Normal file
55
docs/tutorial/src/creating_the_tiles_from_rust.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Creating The Tiles From Rust
|
||||
|
||||
The tiles in the game should have a random placement. We'll need to add the <`rand` dependency to
|
||||
`Cargo.toml` for the randomization.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sixtyfps = "0.0.6"
|
||||
rand = "0.8" # Added
|
||||
```
|
||||
|
||||
What we'll do is take the list of tiles declared in the .60 language, duplicate it, and shuffle it.
|
||||
We'll do so by accessing the `memory_tiles` property through the Rust code. For each top-level property,
|
||||
a getter and a setter function is generated - in our case `get_memory_tiles` and `set_memory_tiles`.
|
||||
Since `memory_tiles` is an array in the `.60` language, it is represented as a [`Rc<dyn sixtyfps::Model>`](https://sixtyfps.io/docs/rust/sixtyfps/trait.model).
|
||||
We can't modify the model generated by the .60, but we can extract the tiles from it, and put it
|
||||
in a [`VecModel`](https://sixtyfps.io/docs/rust/sixtyfps/struct.vecmodel) which implements the `Model` trait.
|
||||
`VecModel` allows us to make modifications and we can use it to replace the static generated model.
|
||||
|
||||
We modify the main function like so:
|
||||
|
||||
```rust
|
||||
fn main() {
|
||||
use sixtyfps::Model;
|
||||
|
||||
let main_window = MainWindow::new();
|
||||
|
||||
// Fetch the tiles from the model
|
||||
let mut tiles: Vec<TileData> =
|
||||
main_window.get_memory_tiles().iter().collect();
|
||||
// Duplicate them to ensure that we have pairs
|
||||
tiles.extend(tiles.clone());
|
||||
|
||||
// Randomly mix the tiles
|
||||
use rand::seq::SliceRandom;
|
||||
let mut rng = rand::thread_rng();
|
||||
tiles.shuffle(&mut rng);
|
||||
|
||||
// Assign the shuffled Vec to the model property
|
||||
let tiles_model =
|
||||
std::rc::Rc::new(sixtyfps::VecModel::from(tiles));
|
||||
main_window.set_memory_tiles(
|
||||
sixtyfps::ModelHandle::new(tiles_model.clone()));
|
||||
|
||||
main_window.run();
|
||||
}
|
||||
```
|
||||
|
||||
Note that we clone the `tiles_model` because we'll use it later to update the game logic.
|
||||
|
||||
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.
|
||||
|
||||
<video autoplay loop muted playsinline src="https://sixtyfps.io/blog/memory-game-tutorial/creating-the-tiles-from-rust.mp4"></video>
|
||||
|
70
docs/tutorial/src/from_one_to_multiple_tiles.md
Normal file
70
docs/tutorial/src/from_one_to_multiple_tiles.md
Normal file
|
@ -0,0 +1,70 @@
|
|||
# From One To Multiple Tiles
|
||||
|
||||
After modeling a single tile, let's create a grid of them. For the grid to be our game board, we need two features:
|
||||
|
||||
1. 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 Rust code.
|
||||
1. A way of creating many instances of the tiles, with the above `.60` markup code.
|
||||
|
||||
In SixtyFPS we can declare an array of structures using brackets, to create a model. We can use the `for` loop
|
||||
to create many instances of the same element. In `.60` the for loop is declarative and automatically updates when
|
||||
the model changes. We instantiate all the different `MemoryTile` elements and place them on a grid based on their
|
||||
index with a little bit of spacing between the tiles.
|
||||
|
||||
First, we copy the tile data structure definition and paste it at top inside the `sixtyfps!` macro:
|
||||
|
||||
```60
|
||||
sixtyfps::sixtyfps!{
|
||||
|
||||
// Added:
|
||||
struct TileData := {
|
||||
image: image,
|
||||
image_visible: bool,
|
||||
solved: bool,
|
||||
}
|
||||
|
||||
MemoryTile := Rectangle {
|
||||
// ...
|
||||
```
|
||||
|
||||
Next, we replace the *`MainWindow` := { ... }* section at the bottom of the `sixtyfps!` macro with the following snippet:
|
||||
|
||||
```60
|
||||
MainWindow := Window {
|
||||
width: 326px;
|
||||
height: 326px;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `for tile[i] in memory_tiles :` syntax declares a variable `tile` which contains the data of one element from the `memory_tiles` array,
|
||||
and a variable `i` which is the index of the tile. We use the `i` 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.
|
||||
|
||||
Running this gives us a window that shows 8 tiles, which can be opened individually.
|
||||
|
||||
<video autoplay loop muted playsinline src="htts://sixtyfps.io/blog/memory-game-tutorial/from-one-to-multiple-tiles.mp4"></video>
|
110
docs/tutorial/src/game_logic_in_rust.md
Normal file
110
docs/tutorial/src/game_logic_in_rust.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
# Game Logic In Rust
|
||||
|
||||
We'll implement the rules of the game in Rust as well. The general philosophy of SixtyFPS is that merely the user
|
||||
interface is implemented in the `.60` 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.
|
||||
|
||||
We'll modify the `.60` markup inside the `sixtyfps!` macro to signal to the Rust code when the user clicks on a tile.
|
||||
Two changes to `MainWindow` are needed: We need to add a way for the MainWindow to call to the Rust code that it should
|
||||
check if a pair of tiles has been solved. And we need to add a property that Rust 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 `MainWindow`:
|
||||
|
||||
|
||||
```60
|
||||
...
|
||||
MainWindow := Window {
|
||||
callback check_if_pair_solved(); // Added
|
||||
property <bool> disable_tiles; // Added
|
||||
|
||||
width: 326px;
|
||||
height: 326px;
|
||||
|
||||
property <[TileData]> memory_tiles: [
|
||||
{ image: img!"icons/at.png" },
|
||||
...
|
||||
```
|
||||
|
||||
The last change to the `.60` markup is to act when the `MemoryTile` signals that it was clicked on. We add the following handler:
|
||||
|
||||
```60
|
||||
...
|
||||
MainWindow := Window {
|
||||
...
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On the Rust side, we can now add an handler to the `check_if_pair_solved` callback, that will check if
|
||||
two tiles are opened. If they match, the `solved` 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 cannot click anything during this time.
|
||||
|
||||
Insert this code before the `main_window.run()` call:
|
||||
|
||||
```rust
|
||||
// ...
|
||||
let main_window_weak = main_window.as_weak();
|
||||
main_window.on_check_if_pair_solved(move || {
|
||||
let mut flipped_tiles =
|
||||
tiles_model.iter().enumerate().filter(|(_, tile)| {
|
||||
tile.image_visible && !tile.solved
|
||||
});
|
||||
|
||||
if let (Some((t1_idx, mut t1)), Some((t2_idx, mut t2))) =
|
||||
(flipped_tiles.next(), flipped_tiles.next())
|
||||
{
|
||||
let is_pair_solved = t1 == t2;
|
||||
if is_pair_solved {
|
||||
t1.solved = true;
|
||||
tiles_model.set_row_data(t1_idx, t1.clone());
|
||||
t2.solved = true;
|
||||
tiles_model.set_row_data(t2_idx, t2.clone());
|
||||
} else {
|
||||
let main_window = main_window_weak.unwrap();
|
||||
main_window.set_disable_tiles(true);
|
||||
let tiles_model = tiles_model.clone();
|
||||
sixtyfps::Timer::single_shot(
|
||||
std::time::Duration::from_secs(1),
|
||||
move || {
|
||||
main_window
|
||||
.set_disable_tiles(false);
|
||||
t1.image_visible = false;
|
||||
tiles_model.set_row_data(t1_idx, t1);
|
||||
t2.image_visible = false;
|
||||
tiles_model.set_row_data(t2_idx, t2);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
main_window.run();
|
||||
```
|
||||
|
||||
Notice that we take a [Weak](https://sixtyfps.io/docs/rust/sixtyfps/struct.weak) pointer of our `main_window`. This is very
|
||||
important because capturing a copy of the `main_window` itself within the callback handler would result in a circular ownership.
|
||||
The `MainWindow` owns the callback handler, which itself owns a reference to the `MainWindow`, which must be weak
|
||||
instead of strong to avoid a memory leak.
|
||||
|
||||
And that's it, now we can run the game!
|
38
docs/tutorial/src/getting_started.md
Normal file
38
docs/tutorial/src/getting_started.md
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Getting Started
|
||||
|
||||
We assume that you are a somewhat familiar with Rust, and that you know how to create a Rust application with
|
||||
`cargo new`. The [Rust Getting Started Guide](https://www.rust-lang.org/learn/get-started) can help you get set up.
|
||||
|
||||
First, we create a new cargo project:
|
||||
|
||||
```sh
|
||||
cargo new memory
|
||||
cd memory
|
||||
```
|
||||
|
||||
Then we edit `Cargo.toml` to add the sixtyfps dependency:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
sixtyfps = "0.0.6"
|
||||
```
|
||||
|
||||
Finally we copy the hello world program from the [SixtyFPS documentation](https://sixtyfps.io/docs/rust/sixtyfps/) into our `src/main.rs`:
|
||||
|
||||
```rust
|
||||
sixtyfps::sixtyfps!{
|
||||
MainWindow := Window {
|
||||
Text {
|
||||
text: "hello world";
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn main() {
|
||||
MainWindow::new().run();
|
||||
}
|
||||
```
|
||||
|
||||
We run this example with `cargo run` and a window will appear with the green "Hello World" greeting.
|
||||
|
||||

|
15
docs/tutorial/src/ideas_for_the_reader.md
Normal file
15
docs/tutorial/src/ideas_for_the_reader.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Ideas For The Reader
|
||||
|
||||
The game is visually a little bare. Here are some ideas how you could make further changes to enhance it:
|
||||
|
||||
* The tiles could have rounded corners, to look a little less sharp. The [border-radius](https://sixtyfps.io/docs/rust/sixtyfps/docs/builtin_elements/index.html#rectangle)
|
||||
property of *Rectangle* can be used to achieve that.
|
||||
|
||||
* 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 *[Image](https://sixtyfps.io/docs/rust/sixtyfps/docs/builtin_elements/index.html#image)*
|
||||
element. Note that you may have to use *Rectangle*'s *[clip](https://sixtyfps.io/docs/rust/sixtyfps/docs/builtin_elements/index.html#properties-1) property*
|
||||
element around it to ensure that the image is clipped away when the curtain effect opens.
|
||||
|
||||
Let us know in the comments on Github Discussions how you polished your code, or feel free to ask questions about
|
||||
how to implement something.
|
||||
|
8
docs/tutorial/src/introduction.md
Normal file
8
docs/tutorial/src/introduction.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Introduction
|
||||
|
||||
This tutorial will introduce you to the SixtyFPS UI framework in a playful way by implementing a little memory game. We are going to combine the `.60` language for the graphics with the game rules implemented in the Rust programming language.
|
||||
|
||||
Before we start, here's a sneak preview of how the game is going to look when finished:
|
||||
|
||||
<video autoplay loop muted playsinline src="https://sixtyfps.io/blog/memory-game-tutorial/memory_clip.mp4"
|
||||
class="img-fluid img-thumbnail rounded"></video>
|
45
docs/tutorial/src/memory_tile.md
Normal file
45
docs/tutorial/src/memory_tile.md
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Memory Tile
|
||||
|
||||
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 is filled with a soothing tone of blue. Note how lengths in the `.60` language have a unit, here
|
||||
the `px` suffix. That makes the code easier to read and the compiler can detect when your're accidentally
|
||||
mixing values with different units attached to them.
|
||||
|
||||
We copy the following code inside of the `sixtyfps!` macro:
|
||||
|
||||
```60
|
||||
MemoryTile := Rectangle {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #3960D5;
|
||||
|
||||
Image {
|
||||
source: @image-url("icons/bus.png");
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow := Window {
|
||||
MemoryTile {}
|
||||
}
|
||||
```
|
||||
|
||||
Inside the `Rectangle` we place an `Image` element that loads an icon with the `@image-url()` macro. The path is
|
||||
relative to the folder in which the `Cargo.toml` is located. This icon and others we're going to
|
||||
use later need to be installed first. You can download a [Zip archive](https://sixtyfps.io/blog/memory-game-tutorial/icons.zip)
|
||||
that we have prepared and extract it with the following two commands:
|
||||
|
||||
```sh
|
||||
curl -O https://sixtyfps.io/blog/memory-game-tutorial/icons.zip
|
||||
unzip icons.zip
|
||||
```
|
||||
|
||||
This should unpack an `icons` directory containing a bunch of icons.
|
||||
|
||||
Running the program with `cargo run` gives us a window on the screen that shows the icon of a bus on a
|
||||
blue background.
|
||||
|
||||

|
81
docs/tutorial/src/polishing_the_tile.md
Normal file
81
docs/tutorial/src/polishing_the_tile.md
Normal file
|
@ -0,0 +1,81 @@
|
|||
# Polishing the Tile
|
||||
|
||||
Next, let's add a curtain like cover that opens up when clicking. We achieve this by declaring two rectangles
|
||||
below the <code class="hljs-built_in">Image</code>, so that they are drawn afterwards and thus on top of the image.
|
||||
The <code class="hljs-built_in">TouchArea</code> 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:
|
||||
|
||||
|*open_curtain* value: |false|true|
|
||||
|-----------------------|-----|----|
|
||||
|Left curtain rectangle |Fill the left half by setting the width *width* to half the parent's width|Width of zero makes the rectangle invisible|
|
||||
|Right curtain rectangle|Fill the right half by setting *x* and *width* to half of the parent's width|*width* of zero makes the rectangle invisible. *x* is moved to the right, to slide the curtain open when animated|
|
||||
|
||||
In order to make our tile extensible, the hard-coded icon name is replaced with an *icon*
|
||||
property that can be set from the outside when instantiating the element. For the final polish, we add a
|
||||
*solved* 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 `sixtyfps!` macro with the following:
|
||||
|
||||
```60
|
||||
MemoryTile := Rectangle {
|
||||
callback clicked;
|
||||
property <bool> open_curtain;
|
||||
property <bool> solved;
|
||||
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;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
MainWindow := Window {
|
||||
MemoryTile {
|
||||
icon: @image-url("icons/bus.png");
|
||||
clicked => {
|
||||
self.open_curtain = !self.open_curtain;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note the use of `root` and `self` in the code. `root` refers to the outermost
|
||||
element in the component, that's the `MemoryTile` in this case. `self` refers
|
||||
to the current element.
|
||||
|
||||
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.
|
||||
|
||||
<video autoplay loop muted playsinline src="https://sixtyfps.io/blog/memory-game-tutorial/polishing-the-tile.mp4"></video>
|
Loading…
Add table
Add a link
Reference in a new issue