Skip to content

Commit

Permalink
feat(ui): LoadingButton component (#37)
Browse files Browse the repository at this point in the history
* feat: basic `LoadingButton`
* feat: add custom animate
* feat: export component in index, clean up code
  • Loading branch information
cEvolve05 authored Aug 26, 2024
1 parent 08de0cc commit 3154411
Show file tree
Hide file tree
Showing 3 changed files with 166 additions and 3 deletions.
72 changes: 72 additions & 0 deletions ui/components/animation.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
a loading animation intended for replacing build-in component Spinner
it repeat a same animate forever.
the arc in animation controlled by head and tail, assert(head>=tail).
head and tail follow a same easing, but not in same time by using different offset.
the `easing` function will compress given easing function according to offset.
given easing function require `float(float)` function signature, input 0~1, output 0~1
*/
import { Token } from "../global.slint";
component LoadingAnimation inherits Path {
// available option
// the time of a repetition
in property <duration> circle-time: 1000ms;
// the min distance between head and tail (percentage of a circle)
in property <float> distance: 0.1;
// width
stroke-width: 3px;
// color
stroke: Token.color.on-primary-container;
// implement detail
viewbox-height: 1;
viewbox-width: 1;
private property <float> radius: 0.5;
// 0~1, means the progress of a repetition
private property <float> progress: Math.mod(animation-tick() / 1ms,circle-time / 1ms) / (circle-time / 1ms);
private property <float> tail: easing(progress, distance) - distance / 2;
private property <float> head: easing(progress, - distance) + distance / 2;
private property <{x: float, y: float}> tail-point: polar-to-cartesian(radius, tail * 1turn);
private property <{x: float, y: float}> head-point: polar-to-cartesian(radius, head * 1turn);
MoveTo {
x: tail-point.x;
y: tail-point.y;
}

ArcTo {
sweep: true;
large-arc: head - tail > 0.5;
radius-x: radius;
radius-y: radius;
x: head-point.x;
y: head-point.y;
}

pure function easing(x: float, offset: float) -> float {
if (offset > 0) {
return x > offset ? _inner-easing((x - offset) / (1 - offset)) : 0;
} else if (offset < 0) {
return x < 1 + offset ? _inner-easing(x / (1 + offset)) : 1;
} else {
return _inner-easing(x);
}
}
// convenient easing change
pure function _inner-easing(x: float) -> float {
return ease-in-out-quint(x);
}
// easing function from https://easings.net/
pure function ease-in-out-sine(x: float) -> float {
return -(Math.cos(3.1415926 * x * 1rad) - 1) / 2;
}
pure function ease-in-out-quint(x: float) -> float {
return x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2;
}
// polar to cartesian coordinate system
pure function polar-to-cartesian(r: float, theta: angle) -> {x: float, y: float} {
return {
x: 0.5 + r * Math.cos(theta - 90deg),
y: 0.5 + r * Math.sin(theta - 90deg),
};
}
}
83 changes: 83 additions & 0 deletions ui/components/button.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { StateLayer } from "state_layer.slint";
import { Token } from "../global.slint";
import { LoadingAnimation } from "animation.slint";

export component LoadingButton inherits Rectangle {
// available option
in property <color> on-surface: Token.color.on-surface;
in property <color> surface: Token.color.surface-container;
in property <bool> is-loading: false;
in property <string> content: "No Content";
in property <length> font-size: Token.font.body.large.size;
height: 40px;
callback clicked <=> area.clicked;
// implement detail
private property <length> max-radius: 20px;
private property <length> icon-size: 18px;
private property <length> default-padding: 24px;
private property <length> icon-padding: 16px;
private property <length> default-spacing: 8px;
clip: true;
background: surface;
border-radius: self.height / 2 > max-radius ? max-radius : self.height / 2;
StateLayer {
background: on-surface;
is-hover: area.has-hover;
is-press: area.pressed;
}

_spinner := LoadingAnimation {
x: icon-padding;
width: icon-size;
stroke: on-surface;
}

_text := Text {
x: (root.width - self.width) / 2;
text: content;
font-size: font-size;
vertical-alignment: center;
horizontal-alignment: center;
}

area := TouchArea {
mouse-cursor: pointer;
}

states [
// width = icon-padding + icon-size + default-spacing + _text.width + default-padding
loading when is-loading: {
width: icon-padding + icon-size + default-spacing + _text.width + default-padding;
_text.x: icon-padding + icon-size + default-spacing;
_spinner.opacity: 1;
in {
animate width, _text.x, _spinner.opacity {
duration: 200ms;
easing: ease-in-out-quint;
}
}
out {
animate width, _text.x, _spinner.opacity {
duration: 200ms;
easing: ease-in-out-quint;
}
}
}
// width = default-padding + _text.width + default-padding
normal when !is-loading: {
width: default-padding + _text.width + default-padding;
_text.x: default-padding;
_spinner.opacity: 0;
}
]
}

// quick preview
component Example {
LoadingButton {
content: "click me";
clicked => {
self.is-loading = !self.is-loading;
}
}
}
14 changes: 11 additions & 3 deletions ui/components/index.slint
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export {
export {
Page,
Overlay,
Empty
Expand All @@ -8,6 +8,14 @@ export {
StateLayer
} from "./state_layer.slint";

export {
export {
Toast
} from "./toast.slint";
} from "./toast.slint";

export {
LoadingAnimation
} from "./animation.slint";

export {
LoadingButton
} from "./button.slint";

0 comments on commit 3154411

Please sign in to comment.