mirror of
https://github.com/slint-ui/slint.git
synced 2025-11-25 13:43:50 +00:00
Move the Rust tutorial into a sub-directory
To make room for the C++ version next to it :-)
This commit is contained in:
parent
a012809bd0
commit
7b95371c78
26 changed files with 10 additions and 10 deletions
17
docs/tutorial/rust/README.md
Normal file
17
docs/tutorial/rust/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/rust/book.toml
Normal file
12
docs/tutorial/rust/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
|
||||
59
docs/tutorial/rust/highlight_60.js
Normal file
59
docs/tutorial/rust/highlight_60.js
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/* LICENSE BEGIN
|
||||
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
This file is also available under commercial licensing terms.
|
||||
Please contact info@sixtyfps.io for more information.
|
||||
LICENSE END */
|
||||
|
||||
hljs.registerLanguage("60", function (hljs) {
|
||||
const KEYWORDS = {
|
||||
keyword:
|
||||
'struct export import signal property animate for in if states transitions parent root self',
|
||||
literal:
|
||||
'true false',
|
||||
built_in:
|
||||
'Rectangle Image Text TouchArea Flickable Clip TextInput Window',
|
||||
type:
|
||||
'bool string int float length logical_length duration resource',
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'sixtyfps',
|
||||
aliases: ['60'],
|
||||
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: /@/
|
||||
};
|
||||
});
|
||||
|
||||
document
|
||||
.querySelectorAll("code.language-60")
|
||||
.forEach((block) => hljs.highlightBlock(block));
|
||||
35
docs/tutorial/rust/src/Cargo.toml
Normal file
35
docs/tutorial/rust/src/Cargo.toml
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "src"
|
||||
version = "0.1.0"
|
||||
authors = ["SixtyFPS <info@sixtyfps.io>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[[bin]]
|
||||
name = "memory_tutorial_initial"
|
||||
path = "main_initial.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "memory_tutorial_tile"
|
||||
path = "main_memory_tile.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "memory_tutorial_polishing_the_tile"
|
||||
path = "main_polishing_the_tile.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "memory_tutorial_multiple_tiles"
|
||||
path = "main_multiple_tiles.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "memory_tutorial_tiles_from_rust"
|
||||
path = "main_tiles_from_rust.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "memory_tutorial_game_logic_in_rust"
|
||||
path = "main_game_logic_in_rust.rs"
|
||||
|
||||
[dependencies]
|
||||
sixtyfps = { path = "../../../../api/sixtyfps-rs" }
|
||||
rand = "0.8"
|
||||
12
docs/tutorial/rust/src/SUMMARY.md
Normal file
12
docs/tutorial/rust/src/SUMMARY.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# 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)
|
||||
- [Running In A Browser Using WebAssembly](./running_in_a_browser.md)
|
||||
- [Conclusion](./conclusion.md)
|
||||
10
docs/tutorial/rust/src/conclusion.md
Normal file
10
docs/tutorial/rust/src/conclusion.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Conclusion
|
||||
|
||||
In this tutorial, we have demonstrated how to combine some built-in SixtyFPS elements with Rust code to build a little
|
||||
game. There are many more features that we have not talked about, such as layouts, widgets, or styling. Have a look
|
||||
at the [examples](https://github.com/sixtyfpsui/sixtyfps/tree/master/examples) in the SixtyFPS repo to
|
||||
see how these look like and can be used, such as the [todo example](https://github.com/sixtyfpsui/sixtyfps/tree/master/examples/todo).
|
||||
|
||||
A slightly more polished version of this memory puzzle game is [available in the SixtyFPS repository](
|
||||
https://github.com/sixtyfpsui/sixtyfps/tree/master/examples/memory). And you can <a href="https://sixtyfps.io/demos/memory/" target="_blank">play the wasm version</a> in your browser.
|
||||
|
||||
32
docs/tutorial/rust/src/creating_the_tiles_from_rust.md
Normal file
32
docs/tutorial/rust/src/creating_the_tiles_from_rust.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 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,noplayground
|
||||
{{#include main_tiles_from_rust.rs:tiles}}
|
||||
```
|
||||
|
||||
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>
|
||||
|
||||
33
docs/tutorial/rust/src/from_one_to_multiple_tiles.md
Normal file
33
docs/tutorial/rust/src/from_one_to_multiple_tiles.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# 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
|
||||
{{#include main_multiple_tiles.rs:tile_data}}
|
||||
```
|
||||
|
||||
Next, we replace the *`MainWindow` := { ... }* section at the bottom of the `sixtyfps!` macro with the following snippet:
|
||||
|
||||
```60
|
||||
{{#include main_multiple_tiles.rs:main_window}}
|
||||
```
|
||||
|
||||
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="https://sixtyfps.io/blog/memory-game-tutorial/from-one-to-multiple-tiles.mp4"></video>
|
||||
74
docs/tutorial/rust/src/game_logic_in_rust.md
Normal file
74
docs/tutorial/rust/src/game_logic_in_rust.md
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# 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,noplayground
|
||||
{{#include main_game_logic_in_rust.rs:game_logic}}
|
||||
```
|
||||
|
||||
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!
|
||||
28
docs/tutorial/rust/src/getting_started.md
Normal file
28
docs/tutorial/rust/src/getting_started.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
# 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,noplayground
|
||||
{{#include main_initial.rs:main}}
|
||||
```
|
||||
|
||||
We run this example with `cargo run` and a window will appear with the green "Hello World" greeting.
|
||||
|
||||

|
||||
1
docs/tutorial/rust/src/icons
Symbolic link
1
docs/tutorial/rust/src/icons
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../../../examples/memory/icons
|
||||
15
docs/tutorial/rust/src/ideas_for_the_reader.md
Normal file
15
docs/tutorial/rust/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/rust/src/introduction.md
Normal file
8
docs/tutorial/rust/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>
|
||||
146
docs/tutorial/rust/src/main_game_logic_in_rust.rs
Normal file
146
docs/tutorial/rust/src/main_game_logic_in_rust.rs
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/* LICENSE BEGIN
|
||||
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
This file is also available under commercial licensing terms.
|
||||
Please contact info@sixtyfps.io for more information.
|
||||
LICENSE END */
|
||||
#[allow(dead_code)]
|
||||
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);
|
||||
|
||||
// ANCHOR: game_logic
|
||||
// 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()));
|
||||
|
||||
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();
|
||||
// ANCHOR_END: game_logic
|
||||
}
|
||||
sixtyfps::sixtyfps! {
|
||||
struct TileData := {
|
||||
image: image,
|
||||
image_visible: bool,
|
||||
solved: bool,
|
||||
}
|
||||
|
||||
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 {
|
||||
width: 326px;
|
||||
height: 326px;
|
||||
|
||||
callback check_if_pair_solved();
|
||||
property <bool> disable_tiles;
|
||||
|
||||
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 => {
|
||||
if (!root.disable_tiles) {
|
||||
tile.image_visible = !tile.image_visible;
|
||||
root.check_if_pair_solved();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
docs/tutorial/rust/src/main_initial.rs
Normal file
23
docs/tutorial/rust/src/main_initial.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/* LICENSE BEGIN
|
||||
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
This file is also available under commercial licensing terms.
|
||||
Please contact info@sixtyfps.io for more information.
|
||||
LICENSE END */
|
||||
#[allow(dead_code)]
|
||||
// ANCHOR: main
|
||||
fn main() {
|
||||
MainWindow::new().run();
|
||||
}
|
||||
sixtyfps::sixtyfps! {
|
||||
MainWindow := Window {
|
||||
Text {
|
||||
text: "hello world";
|
||||
color: green;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: main
|
||||
32
docs/tutorial/rust/src/main_memory_tile.rs
Normal file
32
docs/tutorial/rust/src/main_memory_tile.rs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* LICENSE BEGIN
|
||||
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
This file is also available under commercial licensing terms.
|
||||
Please contact info@sixtyfps.io for more information.
|
||||
LICENSE END */
|
||||
#[allow(dead_code)]
|
||||
fn main() {
|
||||
MainWindow::new().run();
|
||||
}
|
||||
sixtyfps::sixtyfps! {
|
||||
// ANCHOR: tile
|
||||
MemoryTile := Rectangle {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: #3960D5;
|
||||
|
||||
Image {
|
||||
source: @image-url("icons/bus.png");
|
||||
width: parent.width;
|
||||
height: parent.height;
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow := Window {
|
||||
MemoryTile {}
|
||||
}
|
||||
// ANCHOR_END: tile
|
||||
}
|
||||
98
docs/tutorial/rust/src/main_multiple_tiles.rs
Normal file
98
docs/tutorial/rust/src/main_multiple_tiles.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/* LICENSE BEGIN
|
||||
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
This file is also available under commercial licensing terms.
|
||||
Please contact info@sixtyfps.io for more information.
|
||||
LICENSE END */
|
||||
#[allow(dead_code)]
|
||||
fn main() {
|
||||
MainWindow::new().run();
|
||||
}
|
||||
// ANCHOR: tile_data
|
||||
sixtyfps::sixtyfps! {
|
||||
|
||||
// Added:
|
||||
struct TileData := {
|
||||
image: image,
|
||||
image_visible: bool,
|
||||
solved: bool,
|
||||
}
|
||||
|
||||
MemoryTile := Rectangle {
|
||||
|
||||
// ANCHOR_END: tile_data
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
// ANCHOR: main_window
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: main_window
|
||||
}
|
||||
67
docs/tutorial/rust/src/main_polishing_the_tile.rs
Normal file
67
docs/tutorial/rust/src/main_polishing_the_tile.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* LICENSE BEGIN
|
||||
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
This file is also available under commercial licensing terms.
|
||||
Please contact info@sixtyfps.io for more information.
|
||||
LICENSE END */
|
||||
#[allow(dead_code)]
|
||||
fn main() {
|
||||
MainWindow::new().run();
|
||||
}
|
||||
sixtyfps::sixtyfps! {
|
||||
// ANCHOR: tile
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ANCHOR_END: tile
|
||||
}
|
||||
111
docs/tutorial/rust/src/main_tiles_from_rust.rs
Normal file
111
docs/tutorial/rust/src/main_tiles_from_rust.rs
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/* LICENSE BEGIN
|
||||
This file is part of the SixtyFPS Project -- https://sixtyfps.io
|
||||
Copyright (c) 2020 Olivier Goffart <olivier.goffart@sixtyfps.io>
|
||||
Copyright (c) 2020 Simon Hausmann <simon.hausmann@sixtyfps.io>
|
||||
|
||||
SPDX-License-Identifier: GPL-3.0-only
|
||||
This file is also available under commercial licensing terms.
|
||||
Please contact info@sixtyfps.io for more information.
|
||||
LICENSE END */
|
||||
#[allow(dead_code)]
|
||||
// ANCHOR: tiles
|
||||
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();
|
||||
}
|
||||
// ANCHOR_END: tiles
|
||||
sixtyfps::sixtyfps! {
|
||||
struct TileData := {
|
||||
image: image,
|
||||
image_visible: bool,
|
||||
solved: bool,
|
||||
}
|
||||
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
docs/tutorial/rust/src/memory_tile.md
Normal file
32
docs/tutorial/rust/src/memory_tile.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 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
|
||||
{{#include main_memory_tile.rs:tile}}
|
||||
```
|
||||
|
||||
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 `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.
|
||||
|
||||

|
||||
32
docs/tutorial/rust/src/polishing_the_tile.md
Normal file
32
docs/tutorial/rust/src/polishing_the_tile.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# 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 <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:
|
||||
|
||||
|*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
|
||||
{{#include main_polishing_the_tile.rs:tile}}
|
||||
```
|
||||
|
||||
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>
|
||||
92
docs/tutorial/rust/src/running_in_a_browser.md
Normal file
92
docs/tutorial/rust/src/running_in_a_browser.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Running In A Browser Using WebAssembly
|
||||
|
||||
Right now, we used `cargo run` to build and run our program as a native application.
|
||||
Native applications are the primary target of the SixtyFPS framework, but we also support WebAssembly
|
||||
for demonstration purposes. So in this section we'll use the standard rust tool `wasm-bindgen` and
|
||||
`wasm-pack` to run the game in the browser. The [wasm-bindgen](https://rustwasm.github.io/docs/wasm-bindgen/examples/without-a-bundler.html)
|
||||
documentation explains all you need to know about using wasm and rust.
|
||||
|
||||
Make sure to have wasm-back installed using
|
||||
|
||||
```sh
|
||||
cargo install wasm-pack
|
||||
```
|
||||
|
||||
You'll need to edit your `Cargo.toml` to add the dependencies.
|
||||
|
||||
```toml
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = { version = "0.2" }
|
||||
getrandom = { version = "0.2.2", features = ["js"] }
|
||||
```
|
||||
|
||||
And it is also necessary to switch to Cargo's new feature resolver, by adding a `resolver = "2"` switch at the
|
||||
top of `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[package]
|
||||
name = "memory"
|
||||
version = "0.1.0"
|
||||
authors = ["..."]
|
||||
edition = "2018"
|
||||
|
||||
resolver = "2" # This line added
|
||||
|
||||
[dependencies]
|
||||
```
|
||||
|
||||
The `'cfg(target_arch = "wasm32")'` ensures that these dependencies will only be active
|
||||
when compiling for the wasm32 architecture. Note that the `rand` dependency is now duplicated,
|
||||
in order to enable the `"wasm-bindgen"` feature.
|
||||
|
||||
While you are editing the Cargo.toml, one last change is needed: you need to turn the binary into
|
||||
a library by adding the following:
|
||||
|
||||
```toml
|
||||
[lib]
|
||||
path = "src/main.rs"
|
||||
crate-type = ["cdylib"]
|
||||
```
|
||||
|
||||
This is required because wasm-pack require rust to generate a `"cdylib"`.
|
||||
|
||||
You also need to modity the `main.rs` by adding the `wasm_bindgen(start)`
|
||||
attribute to the main function and export it with the `pub` keyword:
|
||||
|
||||
```rust,noplayground
|
||||
#[cfg_attr(target_arch = "wasm32",
|
||||
wasm_bindgen::prelude::wasm_bindgen(start))]
|
||||
pub fn main() {
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
Now, we can compile our program with `wasm-pack build --release --target web`. This
|
||||
will create a `pkg` directory containing a few files, including a `.js` file
|
||||
named after your program name. We just have to import that from a HTML file. So let's create a minimal
|
||||
`index.html` that declares a `<canvas>` element for rendering and loads our generated wasm
|
||||
file. The SixtyFPS runtime expects the `<canvas>` element to have the id `id = "canvas"`.
|
||||
(Replace `memory.js` by the correct file name).
|
||||
|
||||
```html
|
||||
<body>
|
||||
<!-- canvas required by the SixtyFPS runtime -->
|
||||
<canvas id="canvas">>/canvas>
|
||||
<script type="module">
|
||||
// import the generated file.
|
||||
import init from './pkg/memory.js';
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Unfortunately, loading ES modules is not allowed for files on the file system when accessed from a
|
||||
`file://` URL, so we can't simply open the index.html. Instead we need to serve it through a web server.
|
||||
For example, using Python, it is as simple as running
|
||||
|
||||
```sh
|
||||
python3 -m http.server
|
||||
```
|
||||
|
||||
and then you can now access the game on [http://localhost:8000](http://localhost:8000/).
|
||||
Loading…
Add table
Add a link
Reference in a new issue