mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 18:58:36 +00:00
md: add ripple effect (#1892)
* Add `StateLayer` component * Add `Ripple` component (used by StateLayer) * use `StateLayer` in material `Button` * use `StateLayer` in material `Item`
This commit is contained in:
parent
784ea30bc7
commit
0b66628fc4
4 changed files with 132 additions and 60 deletions
|
@ -0,0 +1,93 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
|
||||
|
||||
import { md } from "md.slint";
|
||||
|
||||
export Ripple := Rectangle {
|
||||
in property<length> ripple-x;
|
||||
in property<length> ripple-y;
|
||||
in property<bool> active;
|
||||
in property<bool> has-effect;
|
||||
in property<brush> ripple-color <=> circle.background;
|
||||
|
||||
circle := Rectangle {
|
||||
x: root.ripple-x - width / 2;
|
||||
y: root.ripple-y - width / 2;
|
||||
height: width;
|
||||
border-radius: width / 2;
|
||||
}
|
||||
|
||||
states [
|
||||
active when root.active && root.has-effect: {
|
||||
circle.width: root.width * 2 * 1.4142;
|
||||
}
|
||||
]
|
||||
|
||||
transitions [
|
||||
in active : {
|
||||
animate circle.width { duration: 2s; easing: ease-out; }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// A touch area that also represents a visual state.
|
||||
export StateLayer := TouchArea {
|
||||
in property<bool> focusable;
|
||||
in property<brush> selection-background;
|
||||
in property<brush> ripple-color;
|
||||
in property<bool> has-ripple;
|
||||
in property<length> border-radius;
|
||||
|
||||
out property<bool> has-focus <=> fs.has-focus;
|
||||
|
||||
in-out property<brush> background;
|
||||
in-out property<bool> checked;
|
||||
|
||||
forward-focus: fs;
|
||||
|
||||
ripple := Ripple {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
active: root.pressed;
|
||||
ripple-x: root.pressed-x;
|
||||
ripple-y: root.pressed-y;
|
||||
clip: true;
|
||||
border-radius: root.border-radius;
|
||||
background: root.background;
|
||||
ripple-color: root.ripple-color;
|
||||
has-effect: root.has-ripple;
|
||||
|
||||
animate opacity { duration: 250ms; easing: ease; }
|
||||
animate background { duration: 250ms; }
|
||||
}
|
||||
|
||||
fs := FocusScope {
|
||||
width: 0px; // Do not react on clicks
|
||||
enabled: root.enabled && root.focusable;
|
||||
|
||||
key-pressed(event) => {
|
||||
if (event.text == " " || event.text == "\n") {
|
||||
root.clicked();
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
}
|
||||
}
|
||||
|
||||
states [
|
||||
pressed when root.pressed: {
|
||||
ripple.opacity: 0.12;
|
||||
}
|
||||
checked when root.checked: {
|
||||
ripple.opacity: 1.0;
|
||||
ripple.background: selection-background;
|
||||
}
|
||||
hover when root.has-hover: {
|
||||
ripple.opacity: 0.08;
|
||||
}
|
||||
focused when root.has-focus: {
|
||||
ripple.opacity: 0.12;
|
||||
}
|
||||
]
|
||||
}
|
|
@ -13,11 +13,13 @@ struct Color := {
|
|||
on-surface-variant: color,
|
||||
primary: color,
|
||||
on-primary: color,
|
||||
primary-ripple: color,
|
||||
shadow: color,
|
||||
outline: color,
|
||||
outline-variant: color,
|
||||
secondary-container: color,
|
||||
on-secondary-container: color,
|
||||
secondary-ripple: color,
|
||||
}
|
||||
|
||||
// typo settings
|
||||
|
@ -62,12 +64,14 @@ export global md := {
|
|||
on-surface-variant: !dark-color-scheme ? #49454E : #CAC4D0,
|
||||
surface-tint: !dark-color-scheme ? #6750A4 : #D0BCFF,
|
||||
primary: !dark-color-scheme ? #6750A4 : #D0BCFF,
|
||||
primary-ripple: !dark-color-scheme ? #D0BCFF : #6750A4,
|
||||
on-primary: !dark-color-scheme ? #FFFFFF : #371E73,
|
||||
shadow: #000000,
|
||||
outline: !dark-color-scheme ? #79747E : #938F99,
|
||||
outline-variant: !dark-color-scheme ? #C4C7C5 : #444746,
|
||||
secondary-container: !dark-color-scheme ? #E8DEF8 : #4A4458,
|
||||
on-secondary-container: !dark-color-scheme ? #1E192B : #E8DEF8,
|
||||
secondary-ripple: !dark-color-scheme ? #fffc : #000000,
|
||||
},
|
||||
|
||||
elevation: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
|
||||
|
||||
|
||||
import { StateLayer } from "comp-state-layer.slint";
|
||||
import { md } from "md.slint";
|
||||
|
||||
// Default button widget with Material Design Filled Button look and feel.
|
||||
|
@ -9,9 +9,9 @@ export Button := Rectangle {
|
|||
callback clicked;
|
||||
|
||||
in-out property<string> text <=> label.text;
|
||||
out property<bool> has-focus: fs.has-focus;
|
||||
out property<bool> pressed: self.enabled && touch.pressed;
|
||||
in property<bool> enabled <=> touch.enabled;
|
||||
out property<bool> has-focus: state-layer.has-focus;
|
||||
out property<bool> pressed: self.enabled && state-layer.pressed;
|
||||
in property<bool> enabled <=> state-layer.enabled;
|
||||
in property<bool> checkable;
|
||||
in-out property<bool> checked;
|
||||
in property<image> icon;
|
||||
|
@ -19,6 +19,7 @@ export Button := Rectangle {
|
|||
|
||||
accessible-label <=> label.text;
|
||||
accessible-role: button;
|
||||
forward-focus: state-layer;
|
||||
|
||||
height: 40px;
|
||||
|
||||
|
@ -32,14 +33,21 @@ export Button := Rectangle {
|
|||
drop-shadow-offset-y: 1px;
|
||||
}
|
||||
|
||||
state-layer := Rectangle {
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
state-layer := StateLayer {
|
||||
has-ripple: true;
|
||||
border-radius: container.border-radius;
|
||||
background: md.sys.color.on-secondary-container;
|
||||
animate opacity { duration: 250ms; easing: ease; }
|
||||
}
|
||||
ripple-color: md.sys.color.secondary-ripple;
|
||||
selection-background: md.sys.color.primary;
|
||||
focusable: true;
|
||||
|
||||
clicked => {
|
||||
if (root.checkable) {
|
||||
root.checked = !root.checked;
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: 24px;
|
||||
|
@ -56,29 +64,9 @@ export Button := Rectangle {
|
|||
color: md.sys.color.on-secondary-container;
|
||||
vertical-alignment: center;
|
||||
horizontal-alignment: center;
|
||||
font-size: md.sys.typescale.label-large.size;
|
||||
font-weight: md.sys.typescale.label-large.weight;
|
||||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
clicked => {
|
||||
if (root.checkable) {
|
||||
root.checked = !root.checked;
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
fs := FocusScope {
|
||||
width: 0px; // Do not react on clicks
|
||||
enabled <=> root.enabled;
|
||||
key-pressed(event) => {
|
||||
if (event.text == " " || event.text == "\n") {
|
||||
touch.clicked();
|
||||
return accept;
|
||||
}
|
||||
return reject;
|
||||
animate color { duration: 250ms; easing: ease; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,16 +77,8 @@ export Button := Rectangle {
|
|||
label.opacity: 0.38;
|
||||
label.color: md.sys.color.on-surface;
|
||||
}
|
||||
pressed when touch.pressed : {
|
||||
state-layer.opacity: 0.12;
|
||||
}
|
||||
hover when touch.has-hover : {
|
||||
state-layer.opacity: 0.08;
|
||||
container.drop-shadow-blur: md.sys.elevation.level1;
|
||||
container.drop-shadow-color: md.sys.color.shadow;
|
||||
}
|
||||
focused when fs.has-focus : {
|
||||
state-layer.opacity: 0.12;
|
||||
checked when root.checked: {
|
||||
label.color: md.sys.color.on-primary;
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,24 +1,25 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint-ui.com>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-commercial
|
||||
|
||||
|
||||
import { StateLayer } from "comp-state-layer.slint";
|
||||
import { md } from "md.slint";
|
||||
|
||||
// A selectable item that is used by `StandardListView` and `ComboBox`.
|
||||
export Item := Rectangle {
|
||||
callback clicked <=> touch.clicked;
|
||||
callback clicked <=> state-layer.clicked;
|
||||
|
||||
property<bool> selected: false;
|
||||
property<string> text <=> label.text;
|
||||
in property<bool> selected;
|
||||
in property<string> text;
|
||||
|
||||
// background: md.sys.color.background;
|
||||
height: 48px;
|
||||
|
||||
state-layer := Rectangle {
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
state-layer := StateLayer {
|
||||
checked: root.selected;
|
||||
background: md.sys.color.primary;
|
||||
animate opacity { duration: 250ms; easing: ease; }
|
||||
selection-background: md.sys.color.secondary-container;
|
||||
ripple-color: md.sys.color.primary-ripple;
|
||||
has-ripple: true;
|
||||
}
|
||||
|
||||
HorizontalLayout {
|
||||
|
@ -26,6 +27,7 @@ export Item := Rectangle {
|
|||
padding-right: 12px;
|
||||
|
||||
label := Text {
|
||||
text: root.text;
|
||||
color: md.sys.color.on-surface;
|
||||
vertical-alignment: center;
|
||||
// FIXME after Roboto font can be loaded
|
||||
|
@ -35,18 +37,11 @@ export Item := Rectangle {
|
|||
}
|
||||
}
|
||||
|
||||
touch := TouchArea {}
|
||||
|
||||
states [
|
||||
selected when selected : {
|
||||
state-layer.opacity: 1;
|
||||
state-layer.background: md.sys.color.secondary-container;
|
||||
}
|
||||
pressed when touch.pressed : {
|
||||
state-layer.opacity: 0.12;
|
||||
}
|
||||
hover when touch.has-hover : {
|
||||
state-layer.opacity: 0.08;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue