mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-31 15:47:26 +00:00
453 lines
16 KiB
Text
453 lines
16 KiB
Text
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
struct Piece {
|
|
// col/row position of the tile in the puzzle
|
|
pos-x: int,
|
|
pos-y: int,
|
|
// offset in pixel from the base position for the kicking animation
|
|
offset-x: length,
|
|
offset-y: length,
|
|
}
|
|
|
|
struct Theme {
|
|
name: string,
|
|
window-background-color: brush,
|
|
game-background-color: brush,
|
|
game-use-background-image: bool,
|
|
game-border: length,
|
|
game-radius: length,
|
|
game-text-color: color,
|
|
game-highlight-color: color,
|
|
piece-border: length,
|
|
piece-background-1: brush,
|
|
piece-background-2: brush,
|
|
piece-border-color-1: brush,
|
|
piece-border-color-2: brush,
|
|
piece-text-color-1: color,
|
|
piece-text-color-2: color,
|
|
piece-text-weight-incorrect-pos: int,
|
|
piece-text-weight-correct-pos: int,
|
|
piece-text-font-family: string,
|
|
piece-radius: length,
|
|
/// Ratio of the piece size
|
|
piece-spacing: float,
|
|
}
|
|
|
|
component Checkbox inherits Rectangle {
|
|
in property <color> checked-color;
|
|
in property <color> unchecked-color;
|
|
in-out property <bool> checked;
|
|
|
|
callback toggled(bool);
|
|
|
|
states [
|
|
/* pressed when ta.pressed : {
|
|
clip.width: root.width;
|
|
root.border-color: checked_color;
|
|
root.border-width: root.width;
|
|
}*/
|
|
checked when root.checked : {
|
|
clip.width: root.width;
|
|
checkbox-rect.border-color: root.checked-color;
|
|
checkbox-rect.border-width: root.width;
|
|
in {
|
|
animate clip.width { duration: 200ms; easing: ease-in; }
|
|
animate checkbox-rect.border-width { duration: 100ms; easing: ease-out; }
|
|
}
|
|
out {
|
|
animate clip.width { duration: 100ms; easing: ease; }
|
|
animate checkbox-rect.border-width { duration: 200ms; easing: ease-in-out; }
|
|
animate checkbox-rect.border-color { duration: 200ms; easing: cubic-bezier(1,1,1,0); }
|
|
}
|
|
}
|
|
]
|
|
|
|
hover-rect := Rectangle {
|
|
background: #f5f5f5;
|
|
x: - parent.width / 4;
|
|
y: - parent.height / 4;
|
|
width: ta.has-hover ? root.width * 1.5 : 0px;
|
|
height: self.width;
|
|
border-radius: self.width;
|
|
}
|
|
|
|
checkbox-rect := Rectangle {
|
|
border-width: self.height * 10%;
|
|
border-color: root.unchecked-color;
|
|
border-radius: 2px;
|
|
|
|
clip := Rectangle {
|
|
x:0;
|
|
width: 0px;
|
|
clip: true;
|
|
|
|
Text {
|
|
x:0;y:0;
|
|
width: root.width;
|
|
height: root.height;
|
|
text: "✓";
|
|
font-size: self.height * 80%;
|
|
color: white;
|
|
vertical-alignment: center;
|
|
horizontal-alignment: center;
|
|
|
|
animate color { duration: 200ms; }
|
|
}
|
|
}
|
|
|
|
ta := TouchArea {
|
|
clicked => {
|
|
root.checked = !root.checked;
|
|
root.toggled(root.checked);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
import "./plaster-font/Plaster-Regular.ttf";
|
|
|
|
export component MainWindow inherits Window {
|
|
in property <[Piece]> pieces: [
|
|
{ pos-x: 0, pos-y: 0 },
|
|
{ pos-x: 0, pos-y: 1 },
|
|
{ pos-x: 0, pos-y: 2 },
|
|
{ pos-x: 0, pos-y: 3 },
|
|
{ pos-x: 1, pos-y: 0 },
|
|
{ pos-x: 1, pos-y: 1 },
|
|
{ pos-x: 1, pos-y: 2 },
|
|
{ pos-x: 1, pos-y: 3 },
|
|
{ pos-x: 2, pos-y: 0 },
|
|
{ pos-x: 2, pos-y: 1 },
|
|
{ pos-x: 2, pos-y: 2 },
|
|
{ pos-x: 2, pos-y: 3 },
|
|
{ pos-x: 3, pos-y: 0 },
|
|
{ pos-x: 3, pos-y: 1 },
|
|
{ pos-x: 3, pos-y: 2 },
|
|
];
|
|
out property <int> current-theme-index;
|
|
in-out property <bool> auto-play;
|
|
in-out property <int> moves;
|
|
in-out property <int> tiles-left;
|
|
|
|
callback piece-clicked(int);
|
|
callback reset();
|
|
callback enable-auto-mode(bool);
|
|
|
|
private property <[Theme]> themes: [
|
|
{
|
|
name: "SIMPLE",
|
|
window-background-color: #ffffff,
|
|
game-background-color: #ffffff,
|
|
game-use-background-image: false,
|
|
game-border: 1px,
|
|
game-radius: 2px,
|
|
game-text-color: #858585,
|
|
game-highlight-color: #1d6aaa,
|
|
piece-border: 1px,
|
|
piece-background-1: #0d579b,
|
|
piece-background-2: #0d579b,
|
|
piece-border-color-1: #0a457b,
|
|
piece-border-color-2: #0a457b,
|
|
piece-text-color-1: #ffffff,
|
|
piece-text-color-2: #ffffff,
|
|
piece-text-weight-incorrect-pos: 400,
|
|
piece-text-weight-correct-pos: 700,
|
|
piece-radius: 5px,
|
|
/// Ratio of the piece size
|
|
piece-spacing: 10%,
|
|
},
|
|
{
|
|
name: "BERLIN",
|
|
window-background-color: #ffffff88,
|
|
game-background-color: #ffffffcc,
|
|
game-use-background-image: true,
|
|
game-border: 0px,
|
|
game-radius: 2px,
|
|
game-text-color: #858585,
|
|
game-highlight-color: #1d6aaa,
|
|
piece-border: 0px,
|
|
piece-background-1: #43689e,
|
|
piece-background-2: #2f2a14,
|
|
piece-border-color-1: #0000,
|
|
piece-border-color-2: #0000,
|
|
piece-text-color-1: #000000,
|
|
piece-text-color-2: #ffffff,
|
|
piece-text-weight-incorrect-pos: 700,
|
|
piece-text-weight-correct-pos: 700,
|
|
piece-radius: 0px,
|
|
/// Ratio of the piece size
|
|
piece-spacing: 8%,
|
|
},
|
|
{
|
|
name: "PLASTER",
|
|
window-background-color: #424244,
|
|
game-background-color: #f8f4e9,
|
|
game-use-background-image: false,
|
|
game-border: 5px,
|
|
game-radius: 20px,
|
|
game-text-color: #858585,
|
|
game-highlight-color: #e06b53,
|
|
piece-border: 4px,
|
|
piece-background-1: #e06b53,
|
|
piece-background-2: #f8f4e9,
|
|
piece-border-color-1: #424244,
|
|
piece-border-color-2: #e06b53,
|
|
piece-text-color-1: #f8f4e9,
|
|
piece-text-color-2: #424244,
|
|
piece-text-weight-incorrect-pos: 700,
|
|
piece-text-weight-correct-pos: 700,
|
|
piece-text-font-family: "Plaster",
|
|
piece-radius: 5px,
|
|
/// Ratio of the piece size
|
|
piece-spacing: 10%,
|
|
},
|
|
];
|
|
private property <Theme> current-theme: root.themes[root.current-theme-index];
|
|
private property <length> pieces-size: min(root.width, root.height) / 6;
|
|
private property <length> pieces-spacing: root.current-theme.game-use-background-image && root.tiles-left == 0 ?
|
|
2px : (root.pieces-size * root.current-theme.piece-spacing);
|
|
|
|
|
|
|
|
title: "Slide Puzzle - Slint Demo";
|
|
|
|
animate pieces-spacing { duration: 500ms; easing: ease-out; }
|
|
|
|
Image {
|
|
// For the wasm build we want the puzzle to resize with the browser viewport, as per CSS in index.html.
|
|
// Our winit backend preserves the CSS set size if there's no preferred size set on the Slint window.
|
|
// This image propagates its preferred size and that means the window won't scale. By positioning it
|
|
// manually, the preferred size is ignored.
|
|
x: 0; y: 0;
|
|
height: 100%; width: 100%;
|
|
// https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
|
|
source: @image-url("berlin.jpg");
|
|
image-fit: cover;
|
|
}
|
|
|
|
Rectangle {
|
|
background: root.current-theme.window-background-color;
|
|
animate background { duration: 500ms; easing: ease-out; }
|
|
}
|
|
|
|
Rectangle {
|
|
background: root.current-theme.game-background-color;
|
|
border-color: root.current-theme.game-text-color;
|
|
border-width: root.current-theme.game-border;
|
|
border-radius: root.current-theme.game-radius;
|
|
width: root.pieces-size * 4.6;
|
|
height: root.pieces-size * 5.4;
|
|
x: (parent.width - self.width)/2;
|
|
y: (parent.height - self.height)/2;
|
|
animate background, border-color, border-width, border-radius { duration: 500ms; easing: ease-out; }
|
|
|
|
Rectangle {
|
|
y:0;
|
|
width: parent.width * 90%;
|
|
height: root.pieces-size/2;
|
|
x: (parent.width - self.width) / 2;
|
|
|
|
HorizontalLayout {
|
|
spacing: 0px;
|
|
|
|
for theme[idx] in root.themes: TouchArea {
|
|
t := Text {
|
|
width: 100%; height: 100%;
|
|
text: theme.name;
|
|
color: idx == root.current-theme-index ? root.current-theme.game-highlight-color : root.current-theme.game-text-color;
|
|
vertical-alignment: center;
|
|
horizontal-alignment: center;
|
|
}
|
|
Rectangle {
|
|
background: t.color;
|
|
height: idx == root.current-theme-index ? 2px: 1px;
|
|
y: parent.height - self.height;
|
|
}
|
|
clicked => {
|
|
root.current-theme = theme;
|
|
root.current-theme-index = idx;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
for p[i] in root.pieces : Rectangle {
|
|
property <float> px: p.pos-x;
|
|
property <float> py: p.pos-y;
|
|
property <bool> is-correct: i == p.pos-x * 4 + p.pos-y;
|
|
|
|
x: self.py * (root.pieces-size + root.pieces-spacing) + p.offset-x
|
|
+ (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2;
|
|
y: self.px * (root.pieces-size + root.pieces-spacing) + p.offset-y
|
|
+ (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2;
|
|
width: root.pieces-size;
|
|
height: root.pieces-size;
|
|
drop-shadow-offset-x: 1px;
|
|
drop-shadow-offset-y: 1px;
|
|
drop-shadow-blur: 3px;
|
|
drop-shadow-color: #00000040;
|
|
border-radius: root.current-theme.piece-radius;
|
|
clip: true;
|
|
|
|
states [
|
|
pressed when touch.pressed : {
|
|
shadow.color: #0002;
|
|
circle.width: shadow.width * 2 * 1.4142;
|
|
in {
|
|
animate shadow.color { duration: 50ms; }
|
|
animate circle.width { duration: 2s; easing: ease-out; }
|
|
}
|
|
out {
|
|
animate shadow.color { duration: 50ms; }
|
|
}
|
|
}
|
|
hover when touch.has-hover: {
|
|
shadow.color: #0000000d;
|
|
}
|
|
]
|
|
|
|
animate px , py { duration: 170ms; easing: cubic-bezier(0.17,0.76,0.4,1.75); }
|
|
|
|
if (root.current-theme.game-use-background-image) : Image {
|
|
height: 100%; width: 100%;
|
|
// https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
|
|
source: @image-url("berlin.jpg");
|
|
source-clip-x: mod(i, 4) * self.source.width / 4;
|
|
source-clip-y: floor(i / 4) * self.source.height / 4;
|
|
source-clip-width: self.source.width / 4;
|
|
source-clip-height: self.source.height / 4;
|
|
|
|
if (root.tiles-left != 0) : Rectangle {
|
|
width: 60%;
|
|
height: 60%;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
border-radius: self.width;
|
|
background: is-correct ? #0008 : #fff8;
|
|
}
|
|
}
|
|
|
|
if (!root.current-theme.game-use-background-image) : Rectangle {
|
|
background: i >= 8 ? root.current-theme.piece-background-2 : root.current-theme.piece-background-1;
|
|
border-color: i >= 8 ? root.current-theme.piece-border-color-2 : root.current-theme.piece-border-color-1;
|
|
border-width: root.current-theme.piece-border;
|
|
border-radius: root.current-theme.piece-radius;
|
|
|
|
animate border-width, border-radius { duration: 500ms; easing: ease-out; }
|
|
}
|
|
|
|
if (!root.current-theme.game-use-background-image || root.tiles-left > 0) : Text {
|
|
text: i+1;
|
|
color: ((!root.current-theme.game-use-background-image && i >= 8) || (root.current-theme.game-use-background-image && is-correct)) ? root.current-theme.piece-text-color-2 : root.current-theme.piece-text-color-1;
|
|
font-size: root.pieces-size / 3;
|
|
font-weight: is-correct ? root.current-theme.piece-text-weight-correct-pos : root.current-theme.piece-text-weight-incorrect-pos;
|
|
font-family: root.current-theme.piece-text-font-family;
|
|
vertical-alignment: center;
|
|
horizontal-alignment: center;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
touch := TouchArea {
|
|
clicked => { root.piece-clicked(i); }
|
|
}
|
|
|
|
shadow := Rectangle {
|
|
circle := Rectangle {
|
|
height: self.width;
|
|
border-radius: self.width/2;
|
|
background: #0002;
|
|
x: touch.pressed-x - self.width/2;
|
|
y: touch.pressed-y - self.width/2;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (root.tiles-left == 0) : Text {
|
|
width: root.pieces-size;
|
|
height: root.pieces-size;
|
|
x: 3 * (root.pieces-size + root.pieces-spacing)
|
|
+ (parent.width - (4*root.pieces-size + 3*root.pieces-spacing))/2;
|
|
y: 3 * (root.pieces-size + root.pieces-spacing)
|
|
+ (parent.height - (4*root.pieces-size + 3*root.pieces-spacing))/2;
|
|
|
|
color: root.current-theme.game-highlight-color;
|
|
font-size: root.pieces-size / 2;
|
|
vertical-alignment: center;
|
|
horizontal-alignment: center;
|
|
text: "🖒";
|
|
|
|
if (root.current-theme.game-use-background-image) : Image {
|
|
height: 100%; width: 100%;
|
|
// https://commons.wikimedia.org/wiki/File:Berlin_potsdamer_platz.jpg Belappetit, CC BY-SA 3.0
|
|
source: @image-url("berlin.jpg");
|
|
source-clip-x: 3 * self.source.width / 4;
|
|
source-clip-y: 3 * self.source.height / 4;
|
|
source-clip-width: self.source.width / 4;
|
|
source-clip-height: self.source.height / 4;
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width;
|
|
height: 1px;
|
|
background: root.current-theme.game-text-color;
|
|
y: parent.height - root.pieces-size / 2;
|
|
}
|
|
|
|
HorizontalLayout {
|
|
height: root.pieces-size / 2;
|
|
y: parent.height - root.pieces-size / 2;
|
|
width: parent.width;
|
|
padding: self.height * 25%;
|
|
|
|
Text {
|
|
text: " ↻ ";
|
|
font-size: parent.height * 40%;
|
|
color: root.current-theme.game-highlight-color;
|
|
vertical-alignment: center;
|
|
TouchArea {
|
|
clicked => { root.reset(); }
|
|
}
|
|
}
|
|
|
|
Checkbox {
|
|
toggled(checked) => { root.enable-auto-mode(self.checked) }
|
|
|
|
width: parent.height - 2 * parent.padding;
|
|
checked <=> root.auto-play;
|
|
checked-color: root.current-theme.game-highlight-color;
|
|
unchecked-color: root.current-theme.game-text-color;
|
|
}
|
|
|
|
Rectangle {} // stretch
|
|
|
|
Text {
|
|
text: root.moves;
|
|
color: root.current-theme.game-highlight-color;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
Text {
|
|
text: "Moves ";
|
|
color: root.current-theme.game-text-color;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
Text {
|
|
text: root.tiles-left;
|
|
color: root.current-theme.game-highlight-color;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
Text {
|
|
text: "Tiles left";
|
|
color: root.current-theme.game-text-color;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
}
|