slint/examples/iot-dashboard/iot-dashboard.slint
Simon Hausmann 68083243b2
Add an in-out boolean color-scheme property to Palette (#4701)
This allows applications to force dark/light mode, as well as determine
which mode is active.
2024-03-26 15:44:22 +01:00

877 lines
27 KiB
Text

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
/*
The design from this file is inspired from the design in
https://github.com/peter-ha/qskinny/tree/iot-dashboard/examples/iot-dashboard
Original license:
/****************************************************************************
**
** Copyright 2021 Edelhirsch Software GmbH. All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
**
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of the copyright holder nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**
****************************************************************************/
*/
import { Palette } from "std-widgets.slint";
struct Palette {
menuBar : brush,
mainContent : brush,
box : brush,
lightDisplay : brush,
pieChart : brush,
roundButton : brush,
weekdayBox : brush,
text : brush,
shadow : brush,
}
global Skin {
in property <bool> day: Palette.color-scheme != ColorScheme.dark;
out property <Palette> palette : root.day ? {
menuBar : #6D7BFB,
mainContent : #fbfbfb,
box : #ffffff,
lightDisplay : #ffffff,
pieChart : #ffffff,
roundButton : #f7f7f7,
weekdayBox : #f4f4f4,
text : #000,
shadow : #0001, // ### added alpha
} : {
menuBar : #2937A7,
mainContent : #040404,
box : #000000,
lightDisplay : #000000,
pieChart : #000000,
roundButton : #0a0a0a,
weekdayBox : #0c0c0c,
text : #fff,
shadow : #fff1, // ### added alpha
};
// From Skin::initHints in Skin.cpp
out property <length> DefaultFont: 12px;
out property <length> TinyFont: 9px;
out property <length> SmallFont: 10px;
out property <length> MediumFont: 13px;
out property <length> LargeFont: 20px;
out property <length> HugeFont: 27px; // (also, bold)
out property <length> TitleFont: 10px; // (also, bold)
}
export component Clock inherits VerticalLayout {
in property <string> time <=> time-label.text;
Text {
text: "Current time";
font-size: Skin.TitleFont;
font-weight: 700;
}
time-label := Text {
// FIXME: actual time
text: "10:02:45";
font-size: Skin.HugeFont;
font-weight: 700;
color: #6776FF;
}
}
component PieChartBackground inherits Path {
in property <float> thickness;
in property <float> inner-radius;
fill: #aaaaaa40;
viewbox-width: 100;
viewbox-height: 100;
MoveTo {
x: 50;
y: 0;
}
ArcTo {
radius-x: 50;
radius-y: 50;
x: 50;
y: 100;
sweep: true;
}
ArcTo {
radius-x: 50;
radius-y: 50;
x: 50;
y: 0;
sweep: true;
}
LineTo {
x: 50;
y: root.thickness;
}
ArcTo {
radius-x: root.inner-radius;
radius-y: root.inner-radius;
x: 50;
y: 100 - root.thickness;
}
ArcTo {
radius-x: root.inner-radius;
radius-y: root.inner-radius;
x: 50;
y: root.thickness;
}
}
component PieChartFill inherits Path {
in property <float> thickness;
in property <float> inner-radius;
in property <float> progress;
in property <float> start : 0;
viewbox-width: 100;
viewbox-height: 100;
MoveTo {
y: 50 - 50 * cos(-root.start * 360deg);
x: 50 - 50 * sin(-root.start * 360deg);
}
LineTo {
y: 50 - root.inner-radius * cos(-root.start * 360deg);
x: 50 - root.inner-radius * sin(-root.start * 360deg);
}
ArcTo {
radius-x: root.inner-radius;
radius-y: root.inner-radius;
y: 50 - root.inner-radius*cos(-(root.start + root.progress) * 360deg);
x: 50 - root.inner-radius*sin(-(root.start + root.progress) * 360deg);
sweep: root.progress > 0;
large-arc: root.progress > 0.5;
}
LineTo {
y: 50 - 50*cos(-(root.start + root.progress) * 360deg);
x: 50 - 50*sin(-(root.start + root.progress) * 360deg);
}
ArcTo {
radius-x: 50;
radius-y: 50;
y: 50 - 50 * cos(-root.start * 360deg);
x: 50 - 50 * sin(-root.start * 360deg);
sweep: root.progress < 0;
large-arc: root.progress > 0.5;
}
LineTo {
y: 50 - 50 * cos(-root.start * 360deg);
x: 50 - 50 * sin(-root.start * 360deg);
}
}
component PieChartPainted inherits Rectangle {
in property <brush> brush <=> p.fill;
in property <float> progress;
in property <float> thickness: 15;
in property <float> inner-radius: 50 - root.thickness;
back := PieChartBackground {
width: 100%;
height: 100%;
thickness: root.thickness;
inner-radius: root.inner-radius;
}
p := PieChartFill {
width: 100%;
height: 100%;
thickness: root.thickness;
inner-radius: root.inner-radius;
progress: root.progress;
}
}
// From TopBar.cpp
export component TopBar inherits HorizontalLayout {
padding-left: 25px;
padding-top: 35px;
padding-right: 25px;
padding-bottom: 0px;
spacing: 0px;
for item in [
{ string: "Living Room", progress: 25, value: 175, color: #ff3122, gradient: @linear-gradient(0deg, #FF5C00, #FF3122) },
{ string: "Bedroom", progress: 45, value: 205, color: #6776ff, gradient: @linear-gradient(0deg, #6776FF, #6100FF) },
{ string: "Bathroom", progress: 15, value: 115, color: #f99055, gradient: @linear-gradient(0deg, #FFCE50, #FF3122) },
{ string: "Kitchen", progress: 86, value: 289, color: #6776ff, gradient: @linear-gradient(0deg, #6776FF, #6100FF) },
] : VerticalLayout {
padding: 0px;
spacing: 0px;
Text {
font-size: Skin.SmallFont;
text: item.string;
}
HorizontalLayout {
PieChartPainted {
brush: item.gradient;
progress: item.progress / 100;
Text {
width: 100%;
height: 100%;
vertical-alignment: center;
horizontal-alignment: center;
text: item.progress + "%";
color: item.color;
font-size: Skin.TinyFont;
}
}
VerticalLayout {
Text {
text: item.value;
font-size: Skin.MediumFont;
}
Text {
text: "kwH";
font-size: Skin.SmallFont;
}
}
Rectangle {}
}
}
@children
}
// From Box.cpp
// This element is not in the C++ version, created to share code between Box and the Usage element
component BoxBase inherits Rectangle {
background: Skin.palette.box;
drop-shadow-offset-x: 6px;
drop-shadow-offset-y: 6px;
drop-shadow-blur: 6px;
drop-shadow-color: Skin.palette.shadow;
}
component Box inherits BoxBase {
in property <string> title;
VerticalLayout {
if (root.title != "") : Text {
text <=> root.title;
font-size: Skin.TitleFont;
font-weight: 700;
}
spacing: 10px;
padding: 15px;
@children
}
}
// From RoundedIcon.cpp
component RoundedIcon inherits Rectangle {
in property <bool> isBright;
in property <bool> isSmall;
in property <image> iconName <=> m-graphicLabel.source;
in property <float> background-opacity <=> background-fill.opacity;
height: root.isSmall ? 60px : 68px;
width: root.isSmall ? 60px : 68px;
background-fill := Rectangle {
background: root.isBright ? @linear-gradient(180deg, #ff7d34, #ff3122) : @linear-gradient(180deg, #6776FF, #6100FF);
border-radius: 6px;
opacity: 1.0;
}
m-graphicLabel := Image {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
}
//from Usage.cpp
component UsageSpacer inherits Text {
text: "_____";
font-size: Skin.SmallFont;
color: #dddddd;
horizontal-stretch: 2;
}
// Deviation: To align the items visually better, this is using a grid layout
export component Usage inherits Box {
title: "Usage";
horizontal-stretch: 1;
GridLayout {
spacing: 0px;
vertical-stretch: 1;
Row { Rectangle { vertical-stretch: 0; } }
Row {
Text { text: "Usage Today"; font-size: Skin.SmallFont; }
UsageSpacer { }
Text { text: "0,5 kwH"; font-size: Skin.SmallFont; }
}
Row {
Text { text: "Usage this month"; font-size: Skin.SmallFont; }
UsageSpacer { }
Text { text: "60 kwH"; font-size: Skin.SmallFont; }
}
Row {
Text { text: "Total working hours"; font-size: Skin.SmallFont; }
UsageSpacer { }
Text { text: "125 hrs"; font-size: Skin.SmallFont; }
}
}
}
// From UpAndDownButton.cpp
component RoundButton inherits Image { //### QskPushButton
in property <bool> is-up; // ### QskAspect
in property <color> color: #929CB2; // Taken from the fill in the svg itself.
callback clicked <=> ta.clicked;
width: 30px;
Image {
source: root.is-up ? @image-url("images/up.svg") : @image-url("images/down.svg");
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
// Deviation from qskinny: Show a darker color when pressing the button to provide feedback.
colorize: ta.pressed ? root.color.darker(80%) : root.color;
}
ta := TouchArea { }
}
component UpAndDownButton inherits Rectangle {
callback changed(int);
// FIXME: this is actually on the RoundButton
border-radius: root.width / 2;
background: Skin.palette.roundButton;
VerticalLayout {
u := RoundButton { is-up: true; clicked => { root.changed(+1) }}
d := RoundButton { is-up: false; clicked => { root.changed(-1) }}
}
}
// From BoxWithButtons.cpp
component ButtonValueLabel inherits Text {
in property <string> value <=> root.text;
font-size: Skin.HugeFont;
font-weight: 700;
color: #929cb2;
}
component TitleAndValueBox inherits VerticalLayout {
padding: 8px;
spacing: 8px;
horizontal-stretch: 100;
}
component BoxWithButtons inherits Box {
in property <image> iconFile <=> ri.iconName; //### in original, this is derived from title
in property <bool> isBright <=> ri.isBright;
in property <string> title- <=> titleLabel.text;
in-out property <string> value <=> val.value;
callback changed <=> btns.changed;
HorizontalLayout {
spacing: 20px;
ri := RoundedIcon { }
TitleAndValueBox {
titleLabel := Text {
font-size: Skin.TitleFont;
font-weight: 700;
}
val := ButtonValueLabel { }
}
btns := UpAndDownButton { }
}
}
export component IndoorTemperature inherits BoxWithButtons {
in-out property <int> temperature: 24;
changed(delta) => { root.temperature += delta; }
title-: "Indoor Temperature";
iconFile: @image-url("images/indoor-temperature.png");
value: (root.temperature < 0 ? "" : "+") + root.temperature;
isBright: true;
}
export component Humidity inherits BoxWithButtons {
in-out property <int> humidity-percent : 30;
changed(delta) => { root.humidity-percent += delta; }
title-: "Humidity";
iconFile: @image-url("images/humidity.png");
value: root.humidity-percent + "%";
isBright: false;
}
// from MyDevices.cpp
component Device inherits VerticalLayout {
in property <string> name <=> t.text;
in property <image> iconName <=> ri.iconName; // ### based on the name in the original
in property <bool> isBright <=> ri.isBright;
spacing: 5px;
ri := RoundedIcon {
background-opacity: 0.15;
isSmall: true;
}
t := Text {
font-size: Skin.TinyFont;
horizontal-alignment: center;
}
}
export component MyDevices inherits Box {
title: "My devices";
GridLayout {
spacing: 5px;
Row {
Device{
name: "Lamps";
iconName: @image-url("images/lamps.png");
isBright: true;
}
Device{
name: "Music System";
iconName: @image-url("images/music-system.png");
isBright: false;
}
}
Row {
Device{
name: "AC";
iconName: @image-url("images/ac.png");
isBright: false;
}
Device{
name: "Router";
iconName: @image-url("images/router.png");
isBright: true;
}
}
}
}
export component UsageDiagram inherits Box {
// WeekDayBox
boxes := HorizontalLayout {
padding: 0px;
padding-bottom: 6px;
spacing: 6px;
for _ in 7 : Rectangle {
background: Skin.palette.box;
drop-shadow-offset-x: 6px;
drop-shadow-offset-y: 6px;
drop-shadow-blur: 6px;
drop-shadow-color: Skin.palette.weekdayBox;
min-height: 50px;
}
}
Rectangle {
// ### This is somehow a hack to have another rectangle on top of the boxes
height: 0;
VerticalLayout {
x:0;
y: -boxes.height;
height: boxes.height;
width: boxes.width;
padding: 0px;
spacing: 0px;
HorizontalLayout {
alignment: end;
spacing: 10px;
// CaptionItem
for caption in [
{ text: "Water", color: #6776ff, },
{ text: "Electricity", color: #ff3122, },
{ text: "Gas", color: #ff7d34, },
] : HorizontalLayout {
spacing: 10px;
padding-top: 10px;
padding-right: 20px;
VerticalLayout {
padding: 0px;
alignment: center;
Rectangle {
height: 8px;
width: 9px;
border-radius: 4px;
background: caption.color;
}
}
Text {
text: caption.text;
horizontal-alignment: center;
font-size: Skin.TinyFont;
}
}
}
Rectangle {
// The datapoint is
// FIXME: make it more curve, also fix the color
for datapoints in [
{
values: { a: 40, b: 55, c: 60, d: 50, e: 40, f:50, g: 75, h: 80, i: 100, j: 90 },
color: #6776ff
}, {
values: { a: 30, b: 15, c: 30, d: 40, e: 60, f: 10, g: 70, h: 20, i: 40, j: 45 },
color: #ff3122
} , {
values: { a: 60, b: 45, c: 60, d: 70, e: 10, f: 70, g: 20, h: 50, i: 20, j: 30 },
color: #ff7d34,
}
] : Path {
opacity: 0.7;
fill: @linear-gradient(180deg, datapoints.color, transparent 100%);
viewbox-width: self.width/1px;
viewbox-height: self.height/1px;
MoveTo {
x: 0;
y: parent.viewbox-height;
}
LineTo {
x: 0;
y: parent.viewbox-height - datapoints.values.a / 100 * parent.viewbox-height;
}
QuadraticTo {
x: 0.5/7 * parent.viewbox-width;
y: parent.viewbox-height - datapoints.values.b / 100 * parent.viewbox-height;
control-x: 0/7 * parent.viewbox-width;
control-y: parent.viewbox-height - datapoints.values.b / 100 * parent.viewbox-height;
}
CubicTo {
x: 1.5/7 * parent.viewbox-width;
control-1-x: 1/7 * parent.viewbox-width;
control-2-x: 1/7 * parent.viewbox-width;
y: parent.viewbox-height - datapoints.values.c / 100 * parent.viewbox-height;
control-1-y: parent.viewbox-height - datapoints.values.b / 100 * parent.viewbox-height;
control-2-y: parent.viewbox-height - datapoints.values.c / 100 * parent.viewbox-height;
}
CubicTo {
x: 3.5/7 * parent.viewbox-width;
control-1-x: 3/7 * parent.viewbox-width;
control-2-x: 3/7 * parent.viewbox-width;
y: parent.viewbox-height - datapoints.values.e / 100 * parent.viewbox-height;
control-1-y: parent.viewbox-height - datapoints.values.d / 100 * parent.viewbox-height;
control-2-y: parent.viewbox-height - datapoints.values.e / 100 * parent.viewbox-height;
}
CubicTo {
x: 4.5/7 * parent.viewbox-width;
control-1-x: 4/7 * parent.viewbox-width;
control-2-x: 4/7 * parent.viewbox-width;
y: parent.viewbox-height - datapoints.values.f / 100 * parent.viewbox-height;
control-1-y: parent.viewbox-height - datapoints.values.e / 100 * parent.viewbox-height;
control-2-y: parent.viewbox-height - datapoints.values.f / 100 * parent.viewbox-height;
}
CubicTo {
x: 5.5/7 * parent.viewbox-width;
control-1-x: 5/7 * parent.viewbox-width;
control-2-x: 5/7 * parent.viewbox-width;
y: parent.viewbox-height - datapoints.values.g / 100 * parent.viewbox-height;
control-1-y: parent.viewbox-height - datapoints.values.f / 100 * parent.viewbox-height;
control-2-y: parent.viewbox-height - datapoints.values.g / 100 * parent.viewbox-height;
}
CubicTo {
x: 6.5/7 * parent.viewbox-width;
y: parent.viewbox-height - datapoints.values.h / 100 * parent.viewbox-height;
control-1-x: 6/7 * parent.viewbox-width;
control-1-y: parent.viewbox-height - datapoints.values.g / 100 * parent.viewbox-height;
control-2-x: 6/7 * parent.viewbox-width;
control-2-y: parent.viewbox-height - datapoints.values.h / 100 * parent.viewbox-height;
}
QuadraticTo {
x: parent.viewbox-width;
y: parent.viewbox-height - datapoints.values.i / 100 * parent.viewbox-height;
control-x: 7/7 * parent.viewbox-width;
control-y: parent.viewbox-height - datapoints.values.h / 100 * parent.viewbox-height;
}
LineTo {
x: parent.viewbox-width;
y: parent.viewbox-height;
}
LineTo {
x: 0;
y: parent.viewbox-height;
}
}
}
}
}
HorizontalLayout {
padding: 0px;
padding-top: 5px;
// WeekDay
for day in ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] : Text {
//background: blue;
color: Skin.palette.text;
text: day;
font-size: Skin.TinyFont;
horizontal-alignment: center;
}
}
}
// From LightIntensity.cpp
component LightDimmer inherits Rectangle {
in property <brush> coldGradient <=> cold.fill;
in property <brush> warmGradient <=> warm.fill;
in property <float> thickness: 8;
in property <float> inner-radius: 50 - root.thickness;
out property <float> value : 50%;
out property <float> display-value : touch.pressed ? touch.new-value : root.value;
out property <angle> angle : -180deg + 180deg * root.display-value;
back := PieChartBackground {
width: 100%;
height: 100%;
thickness: root.thickness;
inner-radius: root.inner-radius;
}
warm := PieChartFill {
width: 100%;
height: 100%;
thickness: root.thickness;
inner-radius: root.inner-radius;
start: (root.display-value - 0.5) / 2;
progress: 0.25 - self.start;
}
cold := PieChartFill {
width: 100%;
height: 100%;
thickness: root.thickness;
inner-radius: root.inner-radius;
start: (root.display-value - 0.5) / 2;
progress: -0.25 - self.start;
}
knob := Path {
width: 100%;
height: 100%;
fill: white;
stroke-width: 1px;
stroke: #929cb2;
viewbox-width: 100;
viewbox-height: 100;
MoveTo {
x: 50 + (50 + root.thickness / 4) * cos(root.angle);
y: 50 + (50 + root.thickness / 4) * sin(root.angle);
}
ArcTo {
radius-x: root.thickness / 4;
radius-y: root.thickness / 4;
x: 50 + (50 - root.thickness * 1.25) * cos(root.angle);
y: 50 + (50 - root.thickness * 1.25) * sin(root.angle);
}
ArcTo {
radius-x: root.thickness / 4;
radius-y: root.thickness / 4;
x: 50 + (50 + root.thickness * 0.25) * cos(root.angle);
y: 50 + (50 + root.thickness * 0.25) * sin(root.angle);
}
}
touch := TouchArea {
property <float> new-value : min(1, max(0, self.mouse-x / self.height));
clicked => {
root.value = self.new-value;
}
}
}
export component LightIntensity inherits Box {
title: "Light intensity";
preferred-height: root.width;
Rectangle {
vertical-stretch: 1;
HorizontalLayout {
leftLabel := Text {
text: " 0";
font-size: Skin.SmallFont;
vertical-alignment: center;
}
dimmer := LightDimmer {
warmGradient: @linear-gradient(0deg, #ff3122, #feeeb7);
coldGradient: @linear-gradient(0deg, #a7b0ff, #6776ff);
}
rightLabel := Text {
text: "100";
font-size: Skin.SmallFont;
vertical-alignment: center;
}
}
centreLabel := Text {
width: dimmer.width;
height: dimmer.height;
x: dimmer.x;
y: dimmer.y;
color: #929cb2;
text: "\{round(dimmer.display-value * 100)}%";
font-size: Skin.MediumFont;
vertical-alignment: center;
horizontal-alignment: center;
}
}
}
// From MenuBar.cpp
component MenuItem inherits Rectangle {
in property <image> icon <=> i.source;
in property <string> name <=> t.text;
in-out property <bool> active;
background: root.active ? rgba(100%, 100%, 100%, 14%) : ma.has-hover ? rgba(100%, 100%, 100%, 9%) : transparent;
ma := TouchArea {}
HorizontalLayout {
alignment: start;
spacing: 6px;
padding: 8px;
padding-left: 30px;
padding-right: 30px;
i := Image {
width: 14px; // Skin.cpp sets 14 pixels for MenuBarGraphicLabel::Graphic
height: self.source.height * 1px;
}
t := Text {
color: white;
font-size: Skin.SmallFont;
}
}
}
// From MenuBar.cpp
export component MenuBar inherits Rectangle {
out property <int> active: 0;
background: Skin.palette.menuBar;
min-width: 140px;
VerticalLayout {
padding-left: 0px;
padding-top: 35px;
padding-right: 0px;
padding-bottom: 12px;
spacing: 8px;
VerticalLayout {
// Margin hint for MenuBarTopLabel::Graphic
padding-left: 50px;
padding-top: 0px;
padding-right: 50px;
padding-bottom: 54px;
Image {
source: @image-url("images/main-icon.png");
height: self.source.height * 1px;
}
}
//### In the original, the icon is derived from the name
for entry[idx] in [
{ name: "Dashboard", icon: @image-url("images/dashboard.png") },
{ name: "Rooms", icon: @image-url("images/rooms.png") },
{ name: "Devices", icon: @image-url("images/devices.png") },
{ name: "Statistics", icon: @image-url("images/statistics.png") },
{ name: "Storage", icon: @image-url("images/storage.png") },
{ name: "Members", icon: @image-url("images/members.png") },
] : MenuItem {
name: entry.name;
icon: entry.icon;
active: root.active == idx;
}
Rectangle {}
MenuItem { name: "Logout"; icon: @image-url("images/logout.png"); }
}
}