// Copyright © SixtyFPS GmbH // 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 checked-color; in property unchecked-color; in-out property 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 current-theme-index; in-out property auto-play; in-out property moves; in-out property 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 current-theme: root.themes[root.current-theme-index]; private property pieces-size: min(root.width, root.height) / 6; private property 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 px: p.pos-x; property py: p.pos-y; property 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; } } } }