Initial commit

This commit is contained in:
zervo 2025-11-07 14:04:55 +01:00
commit 7244831bb7
99 changed files with 12973 additions and 0 deletions

12
.cargo/config.toml Normal file
View file

@ -0,0 +1,12 @@
[target.x86_64-pc-windows-msvc]
# Increase default stack size to avoid running out of stack
# space in debug builds. The size matches Linux's default.
rustflags = [
"-C", "link-arg=/STACK:8000000"
]
[target.aarch64-pc-windows-msvc]
# Increase default stack size to avoid running out of stack
# space in debug builds. The size matches Linux's default.
rustflags = [
"-C", "link-arg=/STACK:8000000"
]

21
.gitignore vendored Normal file
View file

@ -0,0 +1,21 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Generated by cargo mutants
# Contains mutation testing data
**/mutants.out*/
# RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

5
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"recommendations": [
"Slint.slint"
]
}

5
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,5 @@
{
"slint.libraryPaths": {
"material": "./material-1.0/material.slint"
}
}

6069
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

30
Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "parts_rs"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["lib", "cdylib"]
path = "src/lib.rs"
name = "parts_rs_lib"
[[bin]]
path = "src/main.rs"
name = "parts_rs"
[dependencies]
slint = { version = "1.13", features = ["backend-android-activity-06"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }
console_error_panic_hook = "0.1.5"
[build-dependencies]
slint-build = { version = "1.13" }
[package.metadata.android]
package = "org.zervo.partsrs"
apk_name = "parts_rs"
[package.metadata.android.application]
label = "PartsRS"

9
LICENSE.txt Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

7
README.md Normal file
View file

@ -0,0 +1,7 @@
# PartsRS
PartsRS is a simple desktop parts management / inventory tracking system intended for use by hobbyists and smaller companies.
It is designed for tracking inventory of electronic components but can probably be used for other stuff as well.
NOTE: Early WIP, does not work yet.

13
build.rs Normal file
View file

@ -0,0 +1,13 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
fn main() {
let config = slint_build::CompilerConfiguration::new().with_library_paths(
std::collections::HashMap::from([(
"material".to_string(),
std::path::Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").unwrap())
.join("material-1.0/material.slint"),
)]),
);
slint_build::compile_with_config("ui/main.slint", config).unwrap();
}

33
index.html Normal file
View file

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>PartsRS</title>
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
/>
<style>
* {
margin: 0;
padding: 0;
overflow: hidden;
}
canvas {
width: 100dvw !important;
height: 100dvh !important;
background: black;
}
</style>
</head>
<body>
<!-- canvas required by the Slint runtime -->
<canvas id="canvas"></canvas>
<script type="module">
// import the generated file.
import init from "./pkg/parts_rs_lib.js";
init();
</script>
</body>
</html>

11
material-1.0/LICENSE.md Normal file
View file

@ -0,0 +1,11 @@
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -->
MIT License
Copyright (c) 2025 SixtyFPS GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

30
material-1.0/README.md Normal file
View file

@ -0,0 +1,30 @@
<!-- Copyright © SixtyFPS GmbH <info@slint.dev> ; SPDX-License-Identifier: MIT -->
# Material Design 3 component set for Slint
[![Gallery image](https://material.slint.dev/tablet-material.webp)](examples/gallery)
Welcome to the official Material Design 3 component set for [Slint](https://slint.dev). These UI components adhere to [Material Design 3 guidelines](https://m3.material.io/).
The components are intended to use for development of user interfaces with Slint for Android apps, touch friendly interfaces for embedded devices and even for desktop application development.
Contributions and feedback from the community are welcome.
## Demos
[WebAssembly build in the web browser](https://material.slint.dev/wasm/)
Download and install the [APK for android](https://material.slint.dev/apk/slint_material.apk)
## Documentation
View the documentation online at https://material.slint.dev/getting-started/
### Get Started
Clone one of our Material Components for Slint templates and follow the instruction from their README:
- Rust: https://github.com/slint-ui/material-rust-template
- C++: https://github.com/slint-ui/material-cpp-template
- Node.js/Deno: https://github.com/slint-ui/material-nodejs-template
- Python: https://github.com/slint-ui/material-python-template

View file

@ -0,0 +1,66 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
// components
export { AppBar, SmallAppBar, MediumAppBar, LargeAppBar } from "./ui/components/app_bar.slint";
export { Badge } from "./ui/components/badge.slint";
export { BottomAppBar } from "./ui/components/bottom_app_bar.slint";
export { CheckState, CheckBox, CheckBoxTile } from "./ui/components/check_box.slint";
export { ActionChip, FilterChip, InputChip } from "./ui/components/chip.slint";
export { DatePickerPopup, DatePickerAdapter } from "./ui/components/date_picker.slint";
export { Dialog, FullscreenDialog } from "./ui/components/dialog.slint";
export { Drawer, ModalDrawer } from "./ui/components/drawer.slint";
export { DropDownMenu } from "./ui/components/drop_down_menu.slint";
export { VerticalDivider, HorizontalDivider } from "./ui/components/divider.slint";
export { ModalBottomSheet } from "./ui/components/bottom_sheet.slint";
export { ElevatedCard, FilledCard, OutlinedCard } from "./ui/components/card.slint";
export { ElevatedButton } from "./ui/components/elevated_button.slint";
export { Elevation } from "./ui/components/elevation.slint";
export { ExtendedTouchArea } from "./ui/components/extended_touch_area.slint";
export { FilledButton } from "./ui/components/filled_button.slint";
export { FilledIconButton } from "./ui/components/filled_icon_button.slint";
export { FloatingActionButton, FABStyle } from "./ui/components/floating_action_button.slint";
export { Grid } from "./ui/components/grid.slint";
export { Horizontal } from "./ui/components/horizontal.slint";
export { Icon } from "./ui/components/icon.slint";
export { IconButton } from "./ui/components/icon_button.slint";
export { OutlineButton } from "./ui/components/outline_button.slint";
export { OutlineIconButton } from "./ui/components/outline_icon_button.slint";
export { Avatar, ListTile } from "./ui/components/list.slint";
export { ListView } from "./ui/components/list_view.slint";
export { MaterialText } from "./ui/components/material_text.slint";
export { MaterialWindow, MaterialWindowAdapter } from "./ui/components/material_window.slint";
export { PopupMenu } from "./ui/components/menu.slint";
export { NavigationBar } from "./ui/components/navigation_bar.slint";
export { NavigationDrawer, ModalNavigationDrawer } from "./ui/components/navigation_drawer.slint";
export { NavigationRail } from "./ui/components/navigation_rail.slint";
export { CircularProgressIndicator, LinearProgressIndicator } from "./ui/components/progress_indicator.slint";
export { RadioButton, RadioButtonTile } from "./ui/components/radio_button.slint";
export { SearchBar } from "./ui/components/search_bar.slint";
export { ScrollView } from "./ui/components/scroll_view.slint";
export { Slider } from "./ui/components/slider.slint";
export { SnackBar } from "./ui/components/snack_bar.slint";
export { StateLayerArea, StateLayer, Ripple } from "./ui/components/state_layer.slint";
export { SegmentedButton } from "./ui/components/segmented_button.slint";
export { Switch } from "./ui/components/switch.slint";
export { TabBar, SecondaryTabBar } from "./ui/components/tab_bar.slint";
export { TextButton } from "./ui/components/text_button.slint";
export { TextField } from "./ui/components/text_field.slint";
export { TimePickerPopup, Time } from "./ui/components/time_picker.slint";
export { TonalButton } from "./ui/components/tonal_button.slint";
export { TonalIconButton } from "./ui/components/tonal_icon_button.slint";
export { ToolTip } from "./ui/components/tooltip.slint";
export { Vertical } from "./ui/components/vertical.slint";
export { Modal } from "./ui/components/modal.slint";
// items
export { ListItem } from "./ui/items/list_item.slint";
export { NavigationItem, NavigationGroup } from "./ui/items/navigation_item.slint";
export { MenuItem } from "./ui/items/menu_item.slint";
// styles
export { MaterialAnimations } from "./ui/styling/material_animations.slint";
export { MaterialScheme, MaterialSchemes } from "./ui/styling/material_schemes.slint";
export { MaterialStyleMetrics } from "./ui/styling/material_style_metrics.slint";
export { MaterialPalette } from "./ui/styling/material_palette.slint";
export { MaterialTypography } from "./ui/styling/material_typography.slint";

View file

@ -0,0 +1,256 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { IconButton } from "./icon_button.slint";
import { MaterialText } from "./material_text.slint";
import { MaterialTypography, TextStyle } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { IconButtonItem } from "./bottom_app_bar.slint";
component BaseAppBar {
in property <string> title;
in property <IconButtonItem> leading_button;
in property <bool> show_background;
in property <[IconButtonItem]> trailing_buttons;
in property <TextHorizontalAlignment> horizontal_alignment;
in property <length> spacing;
in property <length> vertical_spacing;
in property <bool> two_rows;
in property <TextStyle> text_style;
in property <length> text_padding;
in property <length> appbar_padding_right: 0;
in property <length> appbar_padding_top: 0;
in property <length> appbar_padding_bottom: 0;
callback leading_button_clicked();
callback trailing_button_clicked(index: int);
min_width: layout.min_width;
min_height: layout.min_height;
Rectangle {
background: root.show_background ? MaterialPalette.surface_container : MaterialPalette.surface;
layout := VerticalLayout {
padding_top: root.appbar_padding_top;
padding_bottom: root.appbar_padding_bottom;
min_width: top_layout.min_width;
spacing: root.vertical_spacing;
top_layout := HorizontalLayout {
padding_left: root.spacing;
padding_right: root.appbar_padding_right;
spacing: MaterialStyleMetrics.spacing_6;
if root.leading_button.icon.width > 0 && root.leading_button.icon.height > 0 : VerticalLayout {
alignment: center;
IconButton {
icon: root.leading_button.icon;
enabled: root.leading_button.enabled;
tooltip: root.leading_button.tooltip;
clicked => {
root.leading_button_clicked();
}
}
}
if !root.two_rows : MaterialText {
horizontal_stretch: 1;
text: root.title;
vertical_alignment: center;
horizontal_alignment: root.horizontal_alignment;
color: MaterialPalette.on_surface;
overflow: elide;
style: root.text_style;
}
// spacer
if root.two_rows : Rectangle {
horizontal_stretch: 1;
}
if root.trailing_buttons.length > 0 : VerticalLayout {
alignment: center;
HorizontalLayout {
spacing: top_layout.spacing;
for item[index] in root.trailing_buttons : IconButton {
icon: item.icon;
enabled: item.enabled;
tooltip: item.tooltip;
clicked => {
root.trailing_button_clicked(index);
}
}
}
}
}
if root.two_rows && root.title != "" : HorizontalLayout {
padding_left: root.text_padding;
padding_right: self.padding_left;
MaterialText {
text: root.title;
horizontal_alignment: root.horizontal_alignment;
color: MaterialPalette.on_surface;
overflow: elide;
style: root.text_style;
}
}
}
}
}
export component AppBar {
in property <string> title;
in property <IconButtonItem> leading_button;
in property <IconButtonItem> trailing_button;
in property <bool> show_background;
callback leading_button_clicked();
callback trailing_button_clicked();
min_height: max(MaterialStyleMetrics.size_64, base.min_height);
HorizontalLayout {
base := BaseAppBar {
title: root.title;
horizontal_alignment: center;
leading_button: root.leading_button;
trailing_buttons: [root.trailing_button];
show_background: root.show_background;
appbar_padding_right: self.padding_left;
appbar_padding_top: MaterialStyleMetrics.padding_8;
appbar_padding_bottom: self.padding_top;
spacing: MaterialStyleMetrics.spacing_6;
text_style: MaterialTypography.title_large;
leading_button_clicked => {
root.leading_button_clicked();
}
trailing_button_clicked(index) => {
root.trailing_button_clicked();
}
}
}
}
export component SmallAppBar {
in property <string> title;
in property <IconButtonItem> leading_button;
in property <[IconButtonItem]> trailing_buttons;
in property <bool> show_background;
callback leading_button_clicked();
callback trailing_button_clicked(index: int);
min_height: max(MaterialStyleMetrics.size_64, base.min_height);
HorizontalLayout {
base := BaseAppBar {
title: root.title;
horizontal_alignment: left;
leading_button: root.leading_button;
trailing_buttons: root.trailing_buttons;
show_background: root.show_background;
appbar_padding_right: self.padding_left;
appbar_padding_top: MaterialStyleMetrics.padding_8;
appbar_padding_bottom: self.padding_top;
spacing: MaterialStyleMetrics.spacing_4;
text_style: MaterialTypography.title_large;
leading_button_clicked => {
root.leading_button_clicked();
}
trailing_button_clicked(index) => {
root.trailing_button_clicked(index);
}
}
}
}
export component MediumAppBar {
in property <string> title;
in property <IconButtonItem> leading_button;
in property <[IconButtonItem]> trailing_buttons;
in property <bool> show_background;
callback leading_button_clicked();
callback trailing_button_clicked(index: int);
min_height: max(MaterialStyleMetrics.size_64, base.min_height);
HorizontalLayout {
base := BaseAppBar {
title: root.title;
horizontal_alignment: left;
leading_button: root.leading_button;
trailing_buttons: root.trailing_buttons;
show_background: root.show_background;
appbar_padding_right: self.padding_left;
appbar_padding_top: MaterialStyleMetrics.padding_8;
appbar_padding_bottom: MaterialStyleMetrics.padding_24;
vertical_spacing: MaterialStyleMetrics.spacing_4;
spacing: MaterialStyleMetrics.spacing_4;
two_rows: true;
text_style: MaterialTypography.headline_small;
text_padding: MaterialStyleMetrics.padding_16;
leading_button_clicked => {
root.leading_button_clicked();
}
trailing_button_clicked(index) => {
root.trailing_button_clicked(index);
}
}
}
}
export component LargeAppBar {
in property <string> title;
in property <IconButtonItem> leading_button;
in property <[IconButtonItem]> trailing_buttons;
in property <bool> show_background;
callback leading_button_clicked();
callback trailing_button_clicked(index: int);
min_height: max(MaterialStyleMetrics.size_64, base.min_height);
HorizontalLayout {
base := BaseAppBar {
title: root.title;
horizontal_alignment: left;
leading_button: root.leading_button;
trailing_buttons: root.trailing_buttons;
show_background: root.show_background;
appbar_padding_right: self.padding_left;
appbar_padding_top: MaterialStyleMetrics.padding_8;
appbar_padding_bottom: MaterialStyleMetrics.padding_28;
vertical_spacing: MaterialStyleMetrics.spacing_40;
spacing: MaterialStyleMetrics.spacing_4;
two_rows: true;
text_style: MaterialTypography.headline_medium;
text_padding: MaterialStyleMetrics.padding_16;
leading_button_clicked => {
root.leading_button_clicked();
}
trailing_button_clicked(index) => {
root.trailing_button_clicked(index);
}
}
}
}

View file

@ -0,0 +1,36 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialText } from "material_text.slint";
export component Badge {
in property <string> text;
width: max(MaterialStyleMetrics.size_16, label.width + 2 * MaterialStyleMetrics.padding_4);
height: max(MaterialStyleMetrics.size_16, label.height);
background_layer := Rectangle {
width: parent.width;
height: parent.height;
border_radius: self.height / 2;
background: MaterialPalette.error;
}
label := MaterialText {
text: root.text;
color: MaterialPalette.on_error;
vertical_alignment: center;
horizontal_alignment: center;
style: MaterialTypography.label_small;
}
states [
small when root.text == "" : {
background_layer.width: MaterialStyleMetrics.size_6;
background_layer.height: MaterialStyleMetrics.size_6;
}
]
}

View file

@ -0,0 +1,72 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { StateLayerArea } from "./state_layer.slint";
import { MaterialText } from "./material_text.slint";
import { Icon } from "./icon.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { Avatar } from "./list.slint";
export component BaseButton inherits StateLayerArea {
in property <image> icon;
in property <color> icon_color: self.color;
in property <string> text;
in property <length> button_horizontal_padding: MaterialStyleMetrics.padding_24;
in property <length> button_vertical_padding: MaterialStyleMetrics.padding_10;
in property <length> button_padding_left: self.button_horizontal_padding;
in property <length> button_padding_right: self.button_horizontal_padding;
in property <length> button_padding_top: self.button_vertical_padding;
in property <length> button_padding_bottom: self.button_vertical_padding;
in property <length> spacing: MaterialStyleMetrics.spacing_8;
in property <length> min_layout_width: MaterialStyleMetrics.size_40;
in property <length> min_layout_height: MaterialStyleMetrics.size_40;
in property <length> icon_size: MaterialStyleMetrics.icon_size_18;
in property <image> avatar_icon;
in property <length> avatar_size;
in property <color> avatar_background: #00000000;
out property <bool> has_icon: root.icon.width > 0 && root.icon.height > 0;
out property <bool> has_avatar: root.avatar_icon.width > 0 && root.avatar_icon.height > 0;
min_width: max(root.min_layout_width, layout.min_width);
min_height: max(root.min_layout_height, layout.min_height);
tooltip_offset: root.height / 2 + root.icon_size / 2 + MaterialStyleMetrics.padding_14;
layout := HorizontalLayout {
padding_left: root.button_padding_left;
padding_right: root.button_padding_right;
padding_top: root.button_padding_top;
padding_bottom: root.button_padding_bottom;
spacing: root.spacing;
alignment: center;
if root.has_icon || root.has_avatar : VerticalLayout {
alignment: center;
if root.has_avatar && root.avatar_size > 0 : Avatar {
width: root.avatar_size;
height: self.width;
image: root.avatar_icon;
background: root.avatar_background;
}
if root.has_icon : Icon {
source: root.icon;
colorize: root.icon_color;
opacity: root.enabled ? 100% : 38%;
width: root.icon_size;
}
}
if root.text != "" : MaterialText {
text: root.text;
style: MaterialTypography.label_large;
color: root.color;
opacity: root.enabled ? 100% : 38%;
overflow: clip;
vertical_alignment: center;
}
@children
}
}

View file

@ -0,0 +1,51 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { NavigationItem } from "../items/navigation_item.slint";
export component BaseNavigationItemTemplate {
in property <image> icon;
in property <image> selected_icon;
in property <string> text;
in property <int> index;
in property <bool> selected;
in property <bool> show_badge;
in property <string> badge;
callback clicked;
callback pointer_event(event: PointerEvent, position: Point);
accessible-role: tab;
accessible-label: root.text;
accessible-item-index: root.index;
accessible-item-selectable: true;
accessible-item-selected: root.selected;
accessible-action-default => { self.clicked(); }
@children
}
export component BaseNavigation {
in property <[NavigationItem]> items;
in_out property <int> current_index;
callback index_changed(index: int);
accessible-role: tab-list;
// accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-index;
// accessible-label: root.title;
accessible-item-count: root.items.length;
@children
protected function select(index: int) {
if index < 0 || index >= root.items.length {
return;
}
root.current_index = index;
root.index_changed(index);
}
}

View file

@ -0,0 +1,64 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { IconButton } from "./icon_button.slint";
import { FloatingActionButton, FABStyle } from "./floating_action_button.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export struct IconButtonItem {
icon: image,
tooltip: string,
enabled: bool
}
export component BottomAppBar {
in property <[IconButtonItem]> icon_buttons;
in property <image> fab_icon;
callback fab_clicked();
callback icon_button_clicked(index: int);
min_height: max(MaterialStyleMetrics.size_80, layout.min_height);
Rectangle {
background: MaterialPalette.surface_container;
layout := HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_4;
padding_right: MaterialStyleMetrics.padding_16;
VerticalLayout {
alignment: center;
HorizontalLayout {
for button[index] in root.icon_buttons : IconButton {
icon: button.icon;
enabled: button.enabled;
tooltip: button.tooltip;
clicked => {
root.icon_button_clicked(index);
}
}
}
}
// spacer
Rectangle {}
if root.fab_icon.width > 0 && root.fab_icon.height > 0 : VerticalLayout {
alignment: center;
FloatingActionButton {
icon: root.fab_icon;
style: FABStyle.standard;
clicked => {
root.fab_clicked();
}
}
}
}
}
}

View file

@ -0,0 +1,125 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { Horizontal } from "./horizontal.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { Modal } from "./modal.slint";
component BottomSheet {
callback drag_moved(offset_y: length);
callback pressed();
callback released();
elevation := Elevation {
width: 100%;
height: 100%;
level: 3;
border_radius: MaterialStyleMetrics.border_radius_16;
}
TouchArea {
Rectangle {
background: MaterialPalette.surface_container_low;
border_top_left_radius: MaterialStyleMetrics.border_radius_16;
border_top_right_radius: self.border_top_left_radius;
VerticalLayout {
alignment: start;
drag_handle := TouchArea {
height: MaterialStyleMetrics.size_36;
Rectangle {
width: MaterialStyleMetrics.size_32;
height: MaterialStyleMetrics.size_4;
background: MaterialPalette.outline;
border_radius: self.height / 2;
}
moved => {
root.drag_moved(self.pressed_y - self.mouse_y);
}
changed pressed => {
if self.pressed {
root.pressed();
return;
}
root.released();
}
}
Horizontal {
@children
}
}
}
}
}
export component ModalBottomSheet inherits PopupWindow {
close_policy: no_auto_close;
property <length> drag_margin: MaterialStyleMetrics.padding_56;
modal := Modal {
width: 100%;
height: 100%;
sheet := BottomSheet {
property <duration> y_duration: MaterialAnimations.standard_accelerate_duration;
property <length> y_drag_start;
property <length> height_drag_start;
x: (parent.width - self.width) / 2;
y: parent.height;
width: min(MaterialStyleMetrics.size_640, parent.width - 2 * MaterialStyleMetrics.padding_56);
@children
drag_moved(offset_y) => {
self.y_duration = 0;
self.y = max(self.y - offset_y, self.y_drag_start);
self.height += offset_y;
}
pressed => {
self.y_drag_start = self.y;
self.height_drag_start = self.height;
}
released => {
if self.y >= root.height - root.drag_margin {
root.close();
return;
}
self.y_duration = MaterialAnimations.standard_accelerate_duration;
self.height = self.height_drag_start;
self.y = self.y_drag_start;
}
animate y, height {
duration: self.y_duration;
easing: MaterialAnimations.standard_easing;
}
}
clicked => {
root.close();
}
}
Timer {
interval: 50ms;
triggered => {
sheet.y = modal.height - sheet.height;
self.running = false;
}
}
}

View file

@ -0,0 +1,112 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { Elevation } from "./elevation.slint";
import { StateLayerArea } from "./state_layer.slint";
component BaseCard {
in property <bool> clickable;
in property <bool> has_elevation;
in property <color> background;
in property <color> border_color;
in property <length> border_width;
callback clicked();
forward_focus: state_layer;
if root.has_elevation : Elevation {
width: 100%;
height: 100%;
border_radius: background_layer.border_radius;
level: 1;
}
background_layer := Rectangle {
background: root.background;
border_radius: MaterialStyleMetrics.border_radius_12;
border_width: root.border_width;
border_color: root.border_color;
}
state_layer := StateLayerArea {
width: 100%;
height: 100%;
border_radius: background_layer.border_radius;
visible: root.clickable;
enabled: root.visible;
clicked => {
root.clicked();
}
}
Rectangle {
clip: true;
border_radius: MaterialStyleMetrics.border_radius_12;
@children
}
}
export component ElevatedCard {
in property <bool> clickable <=> base.clickable;
callback clicked <=> base.clicked;
forward_focus: base;
accessible-role: button;
accessible-enabled: root.clickable;
accessible-action-default => { base.clicked(); }
base := BaseCard {
width: 100%;
height: 100%;
has_elevation: true;
background: MaterialPalette.surface;
@children
}
}
export component FilledCard {
in property <bool> clickable <=> base.clickable;
callback clicked <=> base.clicked;
forward_focus: base;
accessible-role: button;
accessible-enabled: root.clickable;
accessible-action-default => { base.clicked(); }
base := BaseCard {
width: 100%;
height: 100%;
background: MaterialPalette.surface_container_highest;
@children
}
}
export component OutlinedCard {
in property <bool> clickable <=> base.clickable;
callback clicked <=> base.clicked;
forward_focus: base;
accessible-role: button;
accessible-enabled: root.clickable;
accessible-action-default => { base.clicked(); }
base := BaseCard {
width: 100%;
height: 100%;
background: MaterialPalette.surface_container_low;
border_width: 1px;
border_color: MaterialPalette.outline;
@children
}
}

View file

@ -0,0 +1,130 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { Icons } from "../icons/icons.slint";
import { ListTile } from "./list.slint";
import { StateLayerArea } from "./state_layer.slint";
import { Icon } from "./icon.slint";
export enum CheckState {
unchecked,
partially_checked,
checked,
}
export component CheckBox {
in_out property <CheckState> check_state;
in_out property <bool> tristate;
in property <bool> has_error;
in property <bool> enabled <=> state_area.enabled;
property <bool> checked: root.check_state == CheckState.checked || root.check_state == CheckState.partially_checked;
callback checked_state_changed(check_state: CheckState);
min_width: MaterialStyleMetrics.size_48;
min_height: self.min_width;
accessible-enabled: root.enabled;
accessible-checkable: true;
accessible-checked <=> root.checked;
accessible-role: checkbox;
accessible-action-default => { state_area.clicked(); }
forward_focus: state_area;
state_area := StateLayerArea {
width: 100%;
height: 100%;
border_radius: max(self.width, self.height) / 2;
color: MaterialPalette.on_surface;
background_layer := Rectangle {
width: MaterialStyleMetrics.size_18;
height: self.width;
border_radius: MaterialStyleMetrics.border_radius_2;
border_width: root.checked ? 0 : 2px;
border_color: root.has_error ? MaterialPalette.error : MaterialPalette.on_surface_variant;
if root.checked : Icon {
colorize: MaterialPalette.on_primary;
source: Icons.check;
states [
partial_checked when root.check_state == CheckState.partially-checked : {
source: Icons.remove;
}
]
}
animate background, border_width {
duration: MaterialAnimations.opacity_duration;
easing: MaterialAnimations.opacity_easing;
}
}
clicked => {
root.toggle();
}
}
changed check_state => {
root.checked_state_changed(root.check_state);
}
public function toggle() {
if !root.tristate {
root.check_state = root.check_state != CheckState.checked ? CheckState.checked : CheckState.unchecked;
return;
}
root.check_state = root.check_state == CheckState.checked ? CheckState.partially_checked :
root.check_state == CheckState.partially_checked ? CheckState.unchecked : CheckState.checked;
}
public function set_check_state(check_state: CheckState) {
if check_state == CheckState.partially-checked {
root.tristate = true;
}
root.check_state = check_state;
}
states [
disabled when !root.enabled : {
state_area.display_background: false;
background_layer.border_color: root.checked ? transparent : MaterialPalette.on_surface.with_alpha(38%);
background_layer.background: root.checked ? MaterialPalette.on_surface.with_alpha(38%) : transparent;
}
checked when root.checked : {
background_layer.border_width: 0;
background_layer.background: root.has_error ? MaterialPalette.error : MaterialPalette.primary;
state_area.color: background_layer.background;
}
]
}
export component CheckBoxTile inherits ListTile {
in_out property <CheckState> check_state <=> check_box.check_state;
in_out property <bool> tristate <=> check_box.tristate;
in property <bool> has_error <=> check_box.has_error;
callback checked_state_changed <=> check_box.checked_state_changed;
Rectangle {
horizontal-stretch: 0;
check_box := CheckBox {
enabled: root.enabled;
}
}
clicked => {
check_box.toggle();
}
public function set_check_state(check-state: CheckState) {
check_box.set_check_state(check_state);
}
}

View file

@ -0,0 +1,205 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { Icons } from "../icons/icons.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { IconButton } from "icon_button.slint";
export component ActionChip {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <bool> enabled <=> base.enabled;
in property <string> tooltip <=> base.tooltip;
in property <image> avatar_icon <=> base.avatar-icon;
in property <color> avatar_background <=> base.avatar_background;
callback clicked <=> base.clicked;
forward-focus: base;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-action-default => { base.clicked(); }
border := Rectangle {
border_radius: base.border_radius;
border_width: 1px;
border_color: MaterialPalette.outline;
base := BaseButton {
min_layout_height: MaterialStyleMetrics.size_32;
border_radius: MaterialStyleMetrics.border_radius_8;
color: MaterialPalette.on_surface;
icon_color: MaterialPalette.primary;
spacing: MaterialStyleMetrics.spacing_8;
button_padding_left: self.has_icon ? MaterialStyleMetrics.padding_8 : MaterialStyleMetrics.padding_16;
button_padding_right:MaterialStyleMetrics.padding_16;
button_vertical_padding: MaterialStyleMetrics.padding_6;
avatar_size: MaterialStyleMetrics.size_18;
}
}
states [
disabled when !root.enabled : {
base.display_background: false;
base.icon_color: MaterialPalette.on_surface;
}
]
}
export component FilterChip {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <bool> enabled <=> base.enabled;
in property <string> tooltip <=> base.tooltip;
in_out property <bool> checked;
forward-focus: base;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-checkable: true;
accessible-checked: root.checked;
accessible-action-default => { base.clicked(); }
border := Rectangle {
border_radius: base.border_radius;
border_width: 1px;
border_color: MaterialPalette.outline;
base := BaseButton {
min_layout_height: MaterialStyleMetrics.size_32;
border_radius: MaterialStyleMetrics.border_radius_8;
color: MaterialPalette.on_surface;
icon_color: MaterialPalette.primary;
spacing: MaterialStyleMetrics.spacing_8;
button_padding_left: self.has_icon ? MaterialStyleMetrics.padding_8 : MaterialStyleMetrics.padding_16;
button_padding_right:MaterialStyleMetrics.padding_16;
button_vertical_padding: MaterialStyleMetrics.padding_6;
avatar_size: MaterialStyleMetrics.size_18;
clicked => {
root.toggle();
}
}
states [
checked when root.checked : {
base.icon: Icons.check;
base.icon_color: MaterialPalette.on_secondary_container;
base.color: MaterialPalette.on_secondary_container;
border.border_width: 0;
border.background: MaterialPalette.secondary_container;
}
]
animate width { duration: MaterialAnimations.standard_accelerate_duration; easing: MaterialAnimations.standard_easing; }
}
function toggle() {
root.checked = !root.checked;
}
states [
disabled when !root.enabled : {
base.display_background: false;
base.icon_color: MaterialPalette.on_surface;
}
]
}
export component InputChip {
in property <image> leading_icon <=> base.icon;
in property <image> trailing_icon;
in property <image> avatar <=> base.avatar-icon;
in property <color> avatar_background <=> base.avatar_background;
in property <string> text <=> base.text;
in property <bool> enabled <=> base.enabled;
in property <string> tooltip <=> base.tooltip;
in property <bool> checkable;
in_out property <bool> checked;
property <bool> has_avatar: root.avatar.width > 0 && root.avatar.height > 0;
callback clicked();
callback trailing_icon_clicked();
forward-focus: base;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-checkable: root.checkable;
accessible-checked: root.checked;
accessible-action-default => { base.clicked(); }
border := Rectangle {
border_radius: base.border_radius;
border_width: 1px;
border_color: MaterialPalette.outline;
clip: true;
base := BaseButton {
min_layout_height: MaterialStyleMetrics.size_32;
border_radius: root.has_avatar ? self.height / 2 : MaterialStyleMetrics.border_radius_8;
color: MaterialPalette.on_surface;
icon_color: MaterialPalette.primary;
spacing: MaterialStyleMetrics.spacing_8;
button_padding_left: root.has_avatar ? MaterialStyleMetrics.padding_4 : self.has_icon ? MaterialStyleMetrics.padding_8 : MaterialStyleMetrics.padding_12;
button_padding_right: root.trailing_icon.width > 0 && root.trailing_icon.height > 0 ? MaterialStyleMetrics.padding_8 : MaterialStyleMetrics.padding_12;
button_vertical_padding: MaterialStyleMetrics.padding_6;
avatar_size: MaterialStyleMetrics.size_18;
if root.trailing_icon.width > 0 && root.trailing_icon.height > 0 : VerticalLayout {
alignment: center;
IconButton {
icon: root.trailing_icon;
inline: true;
enabled: root.enabled;
clicked => {
root.trailing_icon_clicked();
}
}
}
clicked => {
root.clicked();
root.toggle();
}
}
states [
checked when root.checked : {
base.icon_color: MaterialPalette.on_secondary_container;
base.color: MaterialPalette.on_secondary_container;
border.border_width: 0;
border.background: MaterialPalette.secondary_container;
}
]
animate width { duration: MaterialAnimations.standard_accelerate_duration; easing: MaterialAnimations.standard_easing; }
}
function toggle() {
if !root.checkable {
return;
}
root.checked = !root.checked;
}
states [
disabled when !root.enabled : {
base.display_background: false;
base.icon_color: MaterialPalette.on_surface;
}
]
}

View file

@ -0,0 +1,568 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { BaseDialog } from "./dialog.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { Icons } from "../icons/icons.slint";
import { MaterialText } from "./material_text.slint";
import { StateLayer } from "./state_layer.slint";
import { ScrollView } from "./scroll_view.slint";
import { IconButton } from "./icon_button.slint";
import { TextField } from "./text_field.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { ExtendedTouchArea } from "./extended_touch_area.slint";
// defines the interface to get the data for the DatePicker from native code.
export global DatePickerAdapter {
// returns the number of days for the given month in the given year.
pure callback month_day_count(month: int, year: int) -> int;
// return the numbers of day to the first monday of the month.
pure callback month_offset(month: int, year: int) -> int;
// used to format a date that is defined by day month and year.
pure callback format_date(format: string, day: int, month: int, year: int) -> string;
// parses the given date string and returns a list of day, month and year.
pure callback parse_date(date: string, format: string) -> [int];
// returns true if the given date is valid.
pure callback valid_date(date: string, format: string) -> bool;
// returns the current date as list of day, month and year.
pure callback date_now() -> [int];
}
export struct Date {
year: int,
month: int,
day: int,
}
component CalendarHeaderDelegate {
in property <string> text <=> text_label.text;
min_width: max(MaterialStyleMetrics.size_40, content_layer.min_width);
min_height: max(MaterialStyleMetrics.size_40, content_layer.min_height);
content_layer := VerticalLayout {
text_label := MaterialText {
vertical_alignment: center;
horizontal_alignment: center;
style: MaterialTypography.body_large;
color: MaterialPalette.on_surface;
}
}
}
component CalendarDelegate {
in property <bool> selected;
in property <bool> today;
in property <string> text <=> text_label.text;
in property <bool> enabled: true;
callback clicked <=> touch_area.clicked;
min_width: max(MaterialStyleMetrics.size_40, content_layer.min_width);
min_height: max(MaterialStyleMetrics.size_40, content_layer.min_height);
forward_focus: focus_scope;
accessible_role: button;
accessible_checkable: true;
accessible_checked: root.selected;
accessible_label: root.text;
accessible_action_default => { touch_area.clicked(); }
touch_area := TouchArea {
enabled: root.enabled;
}
focus_scope := FocusScope {
width: 0px;
enabled: root.enabled;
key_pressed(event) => {
if (event.text == " " || event.text == "\n") {
touch_area.clicked();
return accept;
}
return reject;
}
}
background_layer := Rectangle {
border_radius: self.height / 2;
}
content_layer := HorizontalLayout {
text_label := MaterialText {
vertical_alignment: center;
horizontal_alignment: center;
style: MaterialTypography.body_large;
color: MaterialPalette.on_surface;
}
}
state_layer := StateLayer {
enabled: root.enabled;
border_radius: background_layer.border_radius;
pressed: touch_area.pressed;
has_hover: touch_area.has_hover;
has_focus: focus_scope.has_focus;
}
states [
// FIXME: states
disabled when !root.enabled : {
root.opacity: 0.38;
}
selected when root.selected : {
background_layer.background: MaterialPalette.primary;
text_label.color: MaterialPalette.on_primary;
}
today when root.today : {
background_layer.border_width: 1px;
background_layer.border_color: MaterialPalette.primary;
}
]
}
export component Calendar {
in property <int> column_count;
in property <int> row_count;
in property <length> delegate_size;
in property <int> start_column;
in property <[string]> header_model;
in property <int> month_count;
in property <Date> today;
in property <Date> selected_date;
in property <int> display_month;
in property <int> display_year;
callback select_date(date: Date);
// header
for day[index] in root.header_model : CalendarHeaderDelegate {
x: root.delegate_x(index);
y: root.delegate_y(index);
text: day;
}
// items
for index in root.month_count : CalendarDelegate {
property <Date> d: { day: index + 1, month: root.display_month, year: root.display_year };
x: root.delegate_x(root.index_on_calendar(index));
y: root.delegate_y(root.index_on_calendar(index));
width: root.delegate_size;
height: root.delegate_size;
text: index + 1;
selected: root.selected_date == self.d;
today: root.today == self.d;
clicked => {
root.select_date(self.d);
}
}
function index_on_calendar(index: int) -> int {
// add column count because items starts after header row
root.column_count + root.start_column + index
}
function row_for_index(index: int) -> int {
floor(index / root.column_count)
}
function column_for_index(index: int) -> int {
mod(index, root.column_count)
}
function delegate_x(index: int) -> length {
root.column_for_index(index) * root.delegate_size + root.column_for_index(index) * 1px /* * root.style.spacing */
}
function delegate_y(index: int) -> length {
root.row_for_index(index) * root.delegate_size + root.row_for_index(index) * 1px /* * root.style.spacing */
}
}
component YearSelection {
in property <[int]> model;
in property <length> spacing;
in property <int> visible_row_count;
in property <int> column_count;
in property <length> delegate_width;
in property <length> delegate_height;
in property <int> selected_year;
in property <int> today_year;
callback select_year(year: int);
property <length> row_height: root.height / root.visible_row_count;
property <int> row_count: root.model.length / root.column_count;
property <length> viewport_height: root.row_count * root.row_height;
property <length> start_x: root.width / (root.column_count + 1);
property <length> start_y: root.height / (root.visible_row_count + 1);
ScrollView {
width: 100%;
height: 100%;
viewport_width: root.width;
viewport_height: root.viewport_height;
for year[index] in root.model: CalendarDelegate {
x: root.delegate_center_x(index) - self.width / 2;
y: root.delegate_center_y(index) - self.height / 2;
width: root.delegate_width;
height: root.delegate_height;
text: year;
selected: year == root.selected_year;
today: year == root.today_year;
clicked => {
root.select_year(year);
}
}
}
function delegate_center_x(index: int) -> length {
root.start_x * (root.column_for_index(index) + 1)
}
function delegate_center_y(index: int) -> length {
root.start_y * (root.row_for_index(index) + 1)
}
function row_for_index(index: int) -> int {
floor(index / root.column_count)
}
function column_for_index(index: int) -> int {
mod(index, root.column_count)
}
}
export component SelectionButton {
in property <string> text <=> text-label.text;
in property <image> icon <=> icon-image.source;
in property <bool> enabled: true;
in-out property <bool> checked;
callback clicked();
min-width: content-layer.min-width;
min-height: max(40px, content-layer.min-height);
accessible-label: root.text;
accessible-role: button;
accessible-checkable: true;
accessible-checked: root.checked;
accessible-action-default => { touch-area.clicked(); }
forward-focus: touch-area;
touch-area := ExtendedTouchArea {
width: 100%;
height: 100%;
enabled: root.enabled;
clicked => {
root.checked = !root.checked;
root.clicked();
}
}
state-layer := StateLayer {
enabled: root.enabled;
pressed: touch-area.pressed;
has-hover: touch-area.has-hover;
has-focus: touch-area.has-focus;
background: text_label.color;
}
content-layer := HorizontalLayout {
alignment: center;
padding-left: 8px;
padding-right: 8px;
spacing: 8px;
text-label := MaterialText {
vertical-alignment: center;
color: MaterialPalette.on_surface_variant;
style: MaterialTypography.label_large;
}
icon-image := Image {
y: (parent.height - self.height) / 2;
width: MaterialStyleMetrics.size_18;
colorize: MaterialPalette.on_surface_variant;
rotation-angle: root.checked ? 180deg : 0;
rotation-origin-x: self.width / 2;
rotation-origin-y: self.height / 2;
animate rotation-angle { duration: 250ms; }
}
}
states [
disabled when !root.enabled : {
root.opacity: 0.38;
}
]
}
export component DatePickerPopup inherits PopupWindow {
in property <string> title;
in property <Date> date : { day: today[0], month: today[1], year: today[2] };
callback canceled();
callback accepted(date: Date);
// this is used for the navigation between months
property <string> input_title: @tr("Enter date");
property <string> input_placeholder_text: "mm/dd/yyyy";
property <string> input_format: "%m/%d/%Y";
property <string> cancel_text: @tr("Cancel");
property <string> ok_text: @tr("Ok");
property <Date> display_date: root.date;
property <Date> current_date: root.date;
property <length> delegate_size: 40px;
property <length> year_delegate_width: 72px;
property <int> calendar_column_count: 7;
property <int> calendar_row_count: 6;
property <length> calendar_min_width: root.delegate_size * root.calendar_column_count + (root.calendar_column_count - 1) * 1px;
property <length> calendar_min_height: root.delegate_size *(root.calendar_row_count + 1) + (root.calendar_row_count - 1) * 1px;
property <int> year_selection_column_count: 3;
property <int> year_selection_row_count: 5;
property <bool> year_selection;
property <bool> selection_mode: true;
property <string> current_input;
property <int> calendar_month_count: DatePickerAdapter.month_day_count(root.display_date.month, root.display_date.year);
property <[string]> calendar_header_model: [
@tr("One-letter abbrev for Sunday" => "S"),
@tr("One-letter abbrev for Monday" => "M"),
@tr("One-letter abbrev for Tuesday" => "T"),
@tr("One-letter abbrev for Wednesday" => "W"),
@tr("One-letter abbrev for Thursday" => "T"),
@tr("One-letter abbrev for Friday" => "F"),
@tr("One-letter abbrev for Saturday" => "S"),
];
property <[int]> today: DatePickerAdapter.date_now();
property <int> start_column: DatePickerAdapter.month_offset(root.display_date.month, root.display_date.year);
property <string> current_month: DatePickerAdapter.format_date("%B %Y", root.display_date.day, root.display_date.month, root.display_date.year);
property <[int]> year_model: [2024, 2025, 2026, 2027, 2028, 2029, 2031, 2032, 2033, 2034, 2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042, 2043];
property <int> selected_year: root.display_date.year;
property <int> today_year: root.today[2];
property <string> current_day: DatePickerAdapter.format_date("%a, %b %d", root.current_date.day, root.current_date.month, root.current_date.year);
property <[int]> input_formatted: DatePickerAdapter.parse_date(root.current_input, root.input_format);
close_policy: no_auto_close;
forward_focus: base;
base := BaseDialog {
width: 100%;
height: 100%;
title: root.title;
actions: [
root.cancel_text,
root.ok_text
];
content_layer := VerticalLayout {
spacing: MaterialStyleMetrics.spacing_12;
header := HorizontalLayout {
MaterialText {
text: root.selection_mode ? root.current_day : root.input_title;
horizontal_alignment: left;
vertical_alignment: center;
style: MaterialTypography.headline_large;
}
if root.selection_mode : IconButton {
icon: Icons.edit;
accessible_label: "Toggle selection mode";
clicked => {
root.toggle_selection_mode();
}
}
}
Rectangle {
height: 1px;
background: MaterialPalette.outline;
}
if root.selection_mode : HorizontalLayout {
// FIXME: spacing
VerticalLayout {
horizontal_stretch: 0;
alignment: center;
SelectionButton {
text: root.current_month;
icon: Icons.arrow_drop_down;
checked <=> root.year_selection;
}
}
Rectangle {}
IconButton {
icon: Icons.chevron_backward;
accessible_label: "Previous month";
clicked => {
root.show_previous();
}
}
IconButton {
icon: Icons.chevron_forward;
accessible_label: "Next month";
clicked => {
root.show_next();
}
}
}
if root.selection_mode : VerticalLayout {
if !root.year_selection : Calendar {
min_width: root.calendar_min_width;
min_height: root.calendar_min_height;
column_count: root.calendar_column_count;
row_count: root.calendar_row_count;
delegate_size: root.delegate_size;
header_model: root.calendar_header_model;
month_count: root.calendar_month_count;
today: { day: root.today[0], month: root.today[1], year: root.today[2] };
selected_date <=> root.current_date;
start_column: root.start_column;
display_month: root.display_date.month;
display_year: root.display_date.year;
select_date(date) => {
root.select_date(date);
}
}
if root.year_selection : YearSelection {
min_width: root.calendar_min_width;
min_height: root.calendar_min_height;
column_count: root.year_selection_column_count;
visible_row_count: root.year_selection_row_count;
delegate_width: root.year_delegate_width;
delegate_height: root.delegate_size;
model: root.year_model;
selected_year: root.selected_year;
today_year: root.today_year;
select_year(year) => {
root.select_year(year);
}
}
}
Rectangle {
height: 1px;
visible: root.year_selection;
background: MaterialPalette.outline;
}
if !root.selection_mode : TextField {
text <=> root.current_input;
placeholder_text: root.input_placeholder_text;
trailing_icon: Icons.calendar_today;
trailing_icon_clicked => {
root.toggle_selection_mode();
}
}
}
action_clicked(index) => {
// cancel
if index == 0 {
root.close();
root.canceled();
} else if index == 1 {
root.accepted(root.date);
}
}
close => {
root.close();
}
}
changed date => {
root.display_date = root.date;
root.current_date = root.date;
}
changed selection_mode => {
// check switch from input mode if input is valid
if root.selection_mode && root.current_input_valid() {
root.current_date = root.input_as_date();
root.display_date = root.current_date;
}
}
pure public function ok_enabled() -> bool {
root.selection_mode || root.current_input_valid()
}
public function get_current_date() -> Date {
if root.selection_mode {
return root.current_date;
}
root.input_as_date()
}
pure function current_input_valid() -> bool {
DatePickerAdapter.valid_date(root.current_input, root.input_format)
}
pure function input_as_date() -> Date {
{ day: root.input_formatted[0], month: root.input_formatted[1], year: root.input_formatted[2] }
}
function select_date(date: Date) {
root.current_date = date;
}
function select_year(year: int) {
root.current_date = { day: 1, month: 1, year: year };
root.display_date = root.current_date;
root.year_selection = false;
}
function show_next() {
if root.display_date.month >= 12 {
root.display_date = { day: 1, month: 1, year: root.display_date.year + 1 };
return;
}
root.display_date = { day: 1, month: root.display_date.month + 1, year: root.display_date.year };
}
function show_previous() {
if root.display_date.month <= 1 {
root.display_date = { day: 1, month: 12, year: root.display_date.year - 1 };
return;
}
root.display_date = { day: 1, month: root.display_date.month - 1, year: root.display_date.year };
}
function toggle_selection_mode() {
root.selection_mode = !root.selection_mode;
}
}

View file

@ -0,0 +1,233 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { TextButton } from "./text_button.slint";
import { FilledButton } from "./filled_button.slint";
import { MaterialText } from "./material_text.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { Modal } from "./modal.slint";
import { Icon } from "./icon.slint";
import { IconButton } from "./icon_button.slint";
import { Icons } from "../icons/icons.slint";
export component FullscreenDialog inherits PopupWindow {
in property <string> title;
in property <[string]> actions;
callback action_clicked(index: int);
close_policy: no_auto_close;
forward_focus: focus_scope;
focus_scope := FocusScope {
x: 0;
width: 0;
key_pressed(event) => {
if event.text == Key.Escape {
root.close();
return accept;
}
reject
}
}
background_layer := Rectangle {
width: 100%;
height: 100%;
opacity: 0;
background: MaterialPalette.surface;
VerticalLayout {
spacing: MaterialStyleMetrics.spacing_16;
HorizontalLayout {
padding: MaterialStyleMetrics.padding_24;
spacing: MaterialStyleMetrics.spacing_16;
vertical_stretch: 0;
IconButton {
icon: Icons.close;
clicked => {
root.close();
}
}
MaterialText {
text: root.title;
style: MaterialTypography.headline_small;
color: MaterialPalette.on_surface;
vertical_alignment: center;
}
for action[index] in root.actions : TextButton {
text: action;
clicked => {
root.action_clicked(index);
}
}
}
@children
}
}
Timer {
interval: 50ms;
triggered => {
background_layer.opacity = 1;
self.running = false;
}
}
}
export component BaseDialog {
in property <string> title;
in property <image> icon;
in property <[image]> action_button_icons;
in property <string> default_action_text;
in property <[string]> actions;
property <bool> has_icon: root.icon.width > 0 && root.icon.height > 0;
callback default_action_clicked();
callback action_button_clicked(index: int);
callback action_clicked(index: int);
callback close();
forward_focus: focus_scope;
focus_scope := FocusScope {
x: 0;
width: 0;
key_pressed(event) => {
if event.text == Key.Return && root.default_action_text != "" {
root.default_action_clicked();
return accept;
}
if event.text == Key.Escape {
root.close();
return accept;
}
reject
}
}
modal := Modal {
width: 100%;
height: 100%;
background_layer := Rectangle {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
width: layout.min_width;
height: layout.min_height;
opacity: 0;
border_radius: MaterialStyleMetrics.border_radius_28;
background: MaterialPalette.surface_container_high;
TouchArea {
layout := VerticalLayout {
padding: MaterialStyleMetrics.padding_24;
spacing: MaterialStyleMetrics.spacing_16;
if root.has_icon : Icon {
x: (parent.width - self.width) / 2;
colorize: MaterialPalette.on_surface;
source: root.icon;
}
if root.title != "" : MaterialText {
horizontal_alignment: root.has_icon ? center : left;
text: root.title;
style: MaterialTypography.headline_small;
color: MaterialPalette.on_surface;
}
@children
HorizontalLayout {
spacing: MaterialStyleMetrics.spacing_8;
for action_button[index] in root.action_button_icons : IconButton {
icon: action_button;
clicked => {
root.action_button_clicked(index);
}
}
// Spacer
Rectangle {}
for action[index] in root.actions : TextButton {
text: action;
clicked => {
root.action_clicked(index);
}
}
if root.default_action_text != "" : FilledButton {
text: root.default_action_text;
clicked => {
root.default_action_clicked();
}
}
}
}
}
animate opacity { duration: MaterialAnimations.opacity_duration; easing: MaterialAnimations.opacity_easing; }
}
clicked => {
root.close();
}
}
Timer {
interval: 50ms;
triggered => {
background_layer.opacity = 1;
self.running = false;
}
}
}
export component Dialog inherits PopupWindow {
in property <string> title <=> base.title;
in property <image> icon <=> base.icon;
in property <string> default_action_text <=> base.default_action_text;
in property <[string]> actions <=> base.actions;
callback default_action_clicked <=> base.default_action_clicked;
callback action_clicked <=> base.action_clicked;
close_policy: no_auto_close;
forward_focus: base;
base := BaseDialog {
width: 100%;
height: 100%;
@children
close => {
root.close();
}
}
}

View file

@ -0,0 +1,28 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
component BaseDivider {
Rectangle {
background: MaterialPalette.outline_variant;
}
}
export component VerticalDivider {
width: 1px;
BaseDivider {
width: 100%;
height: 100%;
}
}
export component HorizontalDivider {
height: 1px;
BaseDivider {
width: 100%;
height: 100%;
}
}

View file

@ -0,0 +1,89 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialText } from "./material_text.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { Modal } from "./modal.slint";
export component DrawerHeader {
in property <string> title;
min_height: max(MaterialStyleMetrics.size_56, title_label.min_width);
title_label := MaterialText {
x: MaterialStyleMetrics.padding_16;
width: root.width - MaterialStyleMetrics.padding_16;
text: root.title;
color: MaterialPalette.on_surface_variant;
vertical_alignment: center;
style: MaterialTypography.title_small;
}
}
export component Drawer {
in property <string> title;
min_width: max(MaterialStyleMetrics.size_360, layout.min_width);
TouchArea {
Rectangle {
background: MaterialPalette.surface_container_low;
border_radius: MaterialStyleMetrics.border_radius_16;
layout := VerticalLayout {
alignment: start;
padding: MaterialStyleMetrics.padding_12;
if root.title != "" : DrawerHeader {
width: 100%;
title: root.title;
}
@children
}
}
}
}
export component ModalDrawer inherits PopupWindow {
in property <bool> position_right;
in property <string> title;
close_policy: no_auto_close;
modal := Modal {
width: 100%;
height: 100%;
drawer := Drawer {
x: root.position_right ? parent.width : -self.width;
y: 0;
height: 100%;
title: root.title;
@children
animate x {
duration: MaterialAnimations.standard_accelerate_duration;
easing: MaterialAnimations.standard_easing;
}
}
clicked => {
root.close();
}
}
Timer {
interval: 50ms;
triggered => {
drawer.x = root.position_right ? root.width - drawer.width : 0;
self.running = false;
}
}
}

View file

@ -0,0 +1,125 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { TextField } from "./text_field.slint";
import { MenuItem } from "../items/menu_item.slint";
import { Icons } from "../icons/icons.slint";
import { MenuInner } from "./menu.slint";
import { StateLayer } from "./state_layer.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { ScrollView } from "./scroll_view.slint";
export component DropDownMenu {
in property <string> label <=> text_field.label;
in property <bool> enabled <=> text_field.enabled;
in property <image> leading_icon <=> text_field.leading_icon;
in property <[MenuItem]> items;
in_out property <int> current_index: -1;
callback selected(index: int);
property <length> item_height: MaterialStyleMetrics.size_56;
property <int> max_displayed_items: 6;
property <string> text <=> text_field.text;
min_width: text_field.min_width;
min_height: text_field.min_height;
forward_focus: text_field; accessible-role: combobox;
accessible_enabled: root.enabled;
accessible_expandable: true;
accessible_value <=> text_field.text;
accessible_action_expand => { menu.show(); }
text_field := TextField {
width: 100%;
height: 100%;
trailing_icon: Icons.arrow_drop_down;
read_only: true;
}
state_layer := StateLayer {
width: 100%;
height: 100%;
background: MaterialPalette.on_surface;
has_hover: touch_area.has_hover;
enabled: root.enabled;
}
touch_area := TouchArea {
width: 100%;
height: 100%;
enabled: root.enabled;
clicked => {
menu.show();
}
}
menu := PopupWindow {
x: 0;
width: root.width;
close_policy: close_on_click_outside;
forward_focus: inner_text_field;
VerticalLayout {
alignment: start;
inner_text_field := TextField {
width: root.width;
leading_icon: root.leading_icon;
trailing_icon: Icons.arrow_drop_up;
text: root.text;
label: root.label;
read_only: true;
leading_icon_clicked => {
menu.close();
}
trailing_icon_clicked => {
menu.close();
}
}
Rectangle {
width: 100%;
height: min(max(root.item_height, items.length * root.item_height), root.max_displayed_items * root.item_height);
background: MaterialPalette.surface_container;
ScrollView {
VerticalLayout {
menu_inner := MenuInner {
current_index: root.current_index;
items: root.items;
activated(index) => {
root.update_selection(index);
}
}
}
}
}
}
}
function update_selection(index: int) {
root.update_text(index);
root.current_index = index;
menu.close();
root.selected(index);
}
function update_text(index: int) {
if index < 0 || root.current_index >= root.items.length {
text_field.text = "";
return;
}
text_field.text = root.items[index].text;
}
changed current_index => {
root.update_text(root.current_index);
}
}

View file

@ -0,0 +1,45 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component ElevatedButton {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <string> tooltip <=> base.tooltip;
in property <bool> enabled <=> base.enabled;
callback clicked <=> base.clicked;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-action-default => { base.clicked(); }
elevation := Elevation {
border_radius: root.height / 2;
background: MaterialPalette.surface_container_low;
level: 1;
width: 100%;
height: 100%;
}
base := BaseButton {
border_radius: elevation.border_radius;
color: MaterialPalette.primary;
}
states [
disabled when !root.enabled : {
elevation.background: transparent;
elevation.level: 0;
base.color: MaterialPalette.on_surface;
}
hover when base.has_hover && !base.pressed && !base.enter_pressed : {
elevation.level: 3;
}
]
}

View file

@ -0,0 +1,108 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Palette } from "std-widgets.slint";
import { MaterialWindow } from "material_window.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component Elevation {
in property <brush> background;
in property <length> border_radius;
in property <int> level;
property <bool> dark: Palette.color_scheme == ColorScheme.dark;
outer_shadow := Rectangle {
border_radius: root.border_radius;
inner_shadow := Rectangle {
border_radius: root.border_radius;
background: root.background;
@children
}
}
states [
level_1_light when root.level == 1 && !root.dark: {
outer_shadow.drop_shadow_offset_y: 1px;
outer_shadow.drop_shadow_blur: 2px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_30;
inner_shadow.drop_shadow_offset_y: 1px;
inner_shadow.drop_shadow_blur: 2px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_15;
}
level_2_light when root.level == 2 && !root.dark: {
outer_shadow.drop_shadow_offset_y: 1px;
outer_shadow.drop_shadow_blur: 2px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_30;
inner_shadow.drop_shadow_offset_y: 2px;
inner_shadow.drop_shadow_blur: 6px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_15;
}
level_3_light when root.level == 3 && !root.dark: {
outer_shadow.drop_shadow_offset_y: 4px;
outer_shadow.drop_shadow_blur: 8px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 1px;
inner_shadow.drop_shadow_blur: 3px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
level_4_light when root.level == 4 && !root.dark: {
outer_shadow.drop_shadow_offset_y: 6px;
outer_shadow.drop_shadow_blur: 10px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 2px;
inner_shadow.drop_shadow_blur: 3px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
level_5_light when root.level == 5 && !root.dark: {
outer_shadow.drop_shadow_offset_y: 8px;
outer_shadow.drop_shadow_blur: 12px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 4px;
inner_shadow.drop_shadow_blur: 4px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
level_1_dark when root.level == 1 && root.dark: {
outer_shadow.drop_shadow_offset_y: 1px;
outer_shadow.drop_shadow_blur: 3px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 1px;
inner_shadow.drop_shadow_blur: 2px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
level_2_dark when root.level == 2 && root.dark: {
outer_shadow.drop_shadow_offset_y: 2px;
outer_shadow.drop_shadow_blur: 6px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 1px;
inner_shadow.drop_shadow_blur: 2px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
level_3_dark when root.level == 3 && root.dark: {
outer_shadow.drop_shadow_offset_y: 4px;
outer_shadow.drop_shadow_blur: 8px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 1px;
inner_shadow.drop_shadow_blur: 3px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
level_4_dark when root.level == 4 && root.dark: {
outer_shadow.drop_shadow_offset_y: 6px;
outer_shadow.drop_shadow_blur: 10px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 2px;
inner_shadow.drop_shadow_blur: 3px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
level_5_dark when root.level == 5 && root.dark: {
outer_shadow.drop_shadow_offset_y: 8px;
outer_shadow.drop_shadow_blur: 12px;
outer_shadow.drop_shadow_color: MaterialPalette.shadow_15;
inner_shadow.drop_shadow_offset_y: 4px;
inner_shadow.drop_shadow_blur: 4px;
inner_shadow.drop_shadow_color: MaterialPalette.shadow_30;
}
]
}

View file

@ -0,0 +1,57 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialAnimations } from "../styling/material_animations.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { ToolTip } from "tooltip.slint";
export component ExtendedTouchArea inherits TouchArea {
in property <string> tooltip;
in property <length> tooltip_offset;
out property <bool> has_focus <=> focus_scope.has_focus;
out property <bool> enter_pressed;
callback key_pressed(KeyEvent) -> EventResult;
forward-focus: focus-scope;
focus_scope := FocusScope {
x: 0;
width: 0px;
enabled: root.enabled;
key_pressed(event) => {
if !root.enabled {
return reject;
}
if (event.text == " " || event.text == "\n") && !root.enter_pressed {
root.enter_pressed = true;
root.clicked();
return accept;
}
root.key_pressed(event)
}
key_released(event) => {
if !root.enabled {
return reject;
}
if (event.text == " " || event.text == "\n") && root.enter_pressed {
root.enter_pressed = false;
return accept;
}
reject
}
}
@children
if root.tooltip != "" && root.has-hover && !root.pressed : ToolTip {
y: root.tooltip_offset;
text: root.tooltip;
}
}

View file

@ -0,0 +1,43 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component FilledButton {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <string> tooltip <=> base.tooltip;
in property <bool> enabled <=> base.enabled;
callback clicked <=> base.clicked;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-action-default => { base.clicked(); }
elevation := Elevation {
border_radius: root.height / 2;
background: MaterialPalette.primary;
width: 100%;
height: 100%;
}
base := BaseButton {
border_radius: elevation.border_radius;
color: MaterialPalette.on_primary;
}
states [
disabled when !root.enabled : {
elevation.background: transparent;
base.color: MaterialPalette.on_surface;
}
hover when base.has_hover && !base.pressed && !base.enter_pressed : {
elevation.level: 1;
}
]
}

View file

@ -0,0 +1,64 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component FilledIconButton {
in property <image> icon;
in property <image> checked_icon: root.icon;
in property <bool> checkable;
in_out property <bool> checked;
in property <string> tooltip <=> base.tooltip;
in property <bool> enabled <=> base.enabled;
callback clicked();
accessible-role: button;
accessible-enabled: true;
accessible-label: root.tooltip;
accessible-checkable: root.checkable;
accessible-checked: root.checked;
accessible-action-default => { base.clicked(); }
background_layer := Rectangle {
width: 100%;
height: 100%;
border_radius: base.border_radius;
background: root.checkable ? MaterialPalette.surface_container_highest : MaterialPalette.primary;
visible: root.enabled;
}
base := BaseButton {
icon: root.checked ? root.checked_icon : root.icon;
border_radius: self.height / 2;
color: !root.enabled ? MaterialPalette.on_surface_variant : root.checkable ? MaterialPalette.primary : MaterialPalette.on_primary;
button_horizontal_padding: 0;
button_vertical_padding: 0;
width: self.height;
icon_size: MaterialStyleMetrics.icon_size_24;
clicked => {
root.toggle();
root.clicked();
}
}
function toggle() {
if !root.checkable {
return;
}
root.checked = !root.checked;
}
states [
checked when root.enabled && root.checked : {
base.color: MaterialPalette.on_primary;
background_layer.background: MaterialPalette.primary;
}
]
}

View file

@ -0,0 +1,55 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export enum FABStyle {
small,
standard,
large
}
export component FloatingActionButton {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <string> tooltip <=> base.tooltip;
in property <FABStyle> style;
callback clicked <=> base.clicked;
accessible-role: button;
accessible-enabled: true;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-action-default => { base.clicked(); }
elevation := Elevation {
border_radius: root.style == FABStyle.small ? MaterialStyleMetrics.border_radius_12 :
root.style == FABStyle.standard ? MaterialStyleMetrics.border_radius_16 : MaterialStyleMetrics.border_radius_28;
background: MaterialPalette.primary;
level: 3;
width: 100%;
height: 100%;
}
base := BaseButton {
border_radius: elevation.border_radius;
color: MaterialPalette.on_primary;
button_vertical_padding: root.style == FABStyle.small ? MaterialStyleMetrics.padding_10 :
root.style == FABStyle.standard ? MaterialStyleMetrics.padding_14 : MaterialStyleMetrics.padding_30;
button_horizontal_padding: self.button_vertical_padding;
min_layout_width: self.min_layout_height;
min_layout_height: root.style == FABStyle.small ? MaterialStyleMetrics.size_40 :
root.style == FABStyle.standard ? MaterialStyleMetrics.size_56 : MaterialStyleMetrics.size_90;
icon_size: root.style != FABStyle.large ? MaterialStyleMetrics.icon_size_24 : MaterialStyleMetrics.icon_size_36;
}
states [
hover when base.has_hover : {
elevation.level: 4;
}
]
}

View file

@ -0,0 +1,10 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component Grid inherits GridLayout {
padding: MaterialStyleMetrics.padding_8;
spacing_vertical: MaterialStyleMetrics.spacing_8;
spacing_horizontal: MaterialStyleMetrics.spacing_8;
}

View file

@ -0,0 +1,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component Horizontal inherits HorizontalLayout {
padding: MaterialStyleMetrics.padding_8;
spacing: MaterialStyleMetrics.spacing_8;
}

View file

@ -0,0 +1,11 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component Icon inherits Image {
width: MaterialStyleMetrics.icon_size_18;
height: self.width;
colorize: MaterialPalette.on_background;
}

View file

@ -0,0 +1,62 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component IconButton {
in property <image> icon;
in property <image> checked_icon: root.icon;
in property <bool> checkable;
in_out property <bool> checked;
in property <string> tooltip <=> base.tooltip;
in property <bool> enabled <=> base.enabled;
in property <bool> inverse;
in property <bool> inline: false;
in property <bool> has_error;
callback clicked();
accessible-role: button;
accessible-enabled: true;
accessible-label: root.tooltip;
accessible-checkable: root.checkable;
accessible-checked: root.checked;
accessible-action-default => { base.clicked(); }
base := BaseButton {
icon: root.checked ? root.checked_icon : root.icon;
border_radius: self.height / 2;
color: root.has_error ? MaterialPalette.error : root.inverse ? MaterialPalette.inverse_on_surface : MaterialPalette.on_surface_variant;
button_horizontal_padding: 0;
button_vertical_padding: 0;
width: self.height;
display_background: false;
icon_size: root.inline ? MaterialStyleMetrics.icon_size_18 : MaterialStyleMetrics.icon_size_24;
min_layout_width: root.inline ? MaterialStyleMetrics.icon_size_18 : MaterialStyleMetrics.size_40;
min_layout_height: self.min_layout_width;
clip_ripple: !root.inline;
clicked => {
root.toggle();
root.clicked();
}
}
function toggle() {
if !root.checkable {
return;
}
root.checked = !root.checked;
}
states [
checked when root.enabled && root.checked : {
base.color: MaterialPalette.primary;
}
]
}

View file

@ -0,0 +1,107 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { StateLayerArea } from "./state_layer.slint";
import { MaterialText } from "./material_text.slint";
import { Icon } from "./icon.slint";
export component Avatar {
in property <image> image;
in property <color> background: MaterialPalette.primary;
width: MaterialStyleMetrics.size_32;
height: self.width;
Rectangle {
width: 100%;
height: 100%;
border_radius: self.width / 2;
background: root.background;
clip: true;
Image {
source: root.image;
}
}
}
export component ListTile {
in property <string> text;
in property <string> supporting_text;
in property <image> avatar_icon;
in property <string> avatar_text;
in property <color> avatar_background;
in property <color> avatar_foreground;
in property <bool> enabled <=> state_layer.enabled;
property <color> color: MaterialPalette.on_surface;
callback clicked <=> state_layer.clicked;
min_height: max(MaterialStyleMetrics.size_72, layout.min_height);
state_layer := StateLayerArea {
color: root.color;
layout := HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_16;
padding_right: MaterialStyleMetrics.padding_24;
padding_top: MaterialStyleMetrics.padding_8;
padding_bottom: self.padding_top;
spacing: MaterialStyleMetrics.spacing_16;
if root.avatar_background != #00000000 || root.avatar_text != "" || (root.avatar_icon.width > 0 && root.avatar_icon.height > 0) : Rectangle {
width: self.height;
border_radius: self.height / 2;
background: root.avatar_background;
clip: true;
if root.avatar_text != "" : MaterialText {
text: root.avatar_text;
vertical_alignment: center;
horizontal_alignment: center;
color: root.avatar_foreground;
style: MaterialTypography.title_medium;
}
if root.avatar_icon.width > 0 && root.avatar_icon.height > 0 : Icon {
width: parent.width;
source: root.avatar_icon;
colorize: root.avatar_foreground;
}
}
title_layout := VerticalLayout {
alignment: center;
horizontal_stretch: 1;
MaterialText {
text: root.text;
color: root.color;
overflow: elide;
style: MaterialTypography.body_large;
}
if root.supporting_text != "" : MaterialText {
text: root.supporting_text;
color: root.color;
overflow: elide;
style: MaterialTypography.body_medium;
}
}
@children
}
}
states [
disabled when !root.enabled : {
state_layer.display_background: false;
title_layout.opacity: 38%;
}
]
}

View file

@ -0,0 +1,8 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ScrollView } from "./scroll_view.slint";
export component ListView inherits ScrollView {
}

View file

@ -0,0 +1,14 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialTypography, TextStyle } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component MaterialText inherits Text {
in property <TextStyle> style: MaterialTypography.body_medium;
font_size: root.style.font_size;
font_weight: root.style.font_weight;
color: MaterialPalette.on_background;
overflow: elide;
}

View file

@ -0,0 +1,18 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
export global MaterialWindowAdapter {
in property <bool> disable_hover;
}
export component MaterialWindow inherits Window {
in property <bool> disable_hover;
background: MaterialPalette.background;
changed disable_hover => {
MaterialWindowAdapter.disable_hover = root.disable_hover;
}
}

View file

@ -0,0 +1,136 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { Elevation } from "elevation.slint";
import { StateLayerArea } from "./state_layer.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { Icon } from "./icon.slint";
import { MenuItem } from "../items/menu_item.slint";
import { MaterialText } from "./material_text.slint";
import { Icons } from "../icons/icons.slint";
component MenuItemTemplate {
in property <image> icon;
in property <string> text;
in property <string> trailing_text;
in property <bool> enabled <=> state_area.enabled;
in property <bool> selected;
callback clicked();
property <bool> has_icon: root.icon.width > 0 && root.icon.height > 0;
property <color> color: MaterialPalette.on_surface;
property <color> color_variant: MaterialPalette.on_surface_variant;
min_height: max(MaterialStyleMetrics.size_56, layout.min_height);
background_layer := Rectangle {
state_area := StateLayerArea {
color: root.color;
layout := HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_12;
padding_right: self.padding_left;
padding_top: MaterialStyleMetrics.padding_8;
padding_bottom: self.padding_top;
spacing: MaterialStyleMetrics.spacing_12;
if root.has_icon : VerticalLayout {
alignment: center;
Icon {
source: root.icon;
colorize: root.color;
}
}
MaterialText {
horizontal_stretch: 1;
text: root.text;
style: MaterialTypography.body_large;
color: root.color;
vertical_alignment: center;
}
if root.trailing_text != "" : MaterialText {
text: root.trailing_text;
style: MaterialTypography.body_large;
color: root.color_variant;
vertical_alignment: center;
}
}
clicked => {
root.clicked();
}
}
}
states [
disabled when !root.enabled : {
state_area.opacity: MaterialPalette.disable_opacity;
}
selected when root.selected : {
background_layer.background: MaterialPalette.surface_container_highest;
}
]
}
export component MenuInner {
in property <[MenuItem]> items;
in_out property <int> current_index: -1;
callback activated(index: int);
// used to fix clipping of shadows
out property <int> elevation: 2;
menu := Elevation {
width: 100%;
level: root.elevation;
height: layout.min_height + root.elevation * 1px;
border_radius: MaterialStyleMetrics.border_radius_4;
background: MaterialPalette.surface_container;
layout := VerticalLayout {
width: root.width;
padding_top: MaterialStyleMetrics.padding_8;
padding_bottom: self.padding_top;
for item[index] in root.items : MenuItemTemplate {
icon: item.icon;
text: item.text;
trailing_text: item.trailing_text;
enabled: item.enabled;
selected: index == root.current_index;
clicked => {
root.activated(index);
}
}
}
}
}
export component PopupMenu inherits PopupWindow {
in property <[MenuItem]> items;
callback activated(index: int);
width: MaterialStyleMetrics.size_200;
close_policy: close_on_click_outside;
VerticalLayout {
padding: inner.elevation * 1px;
inner := MenuInner {
items: root.items;
activated(index) => {
root.activated(index);
}
}
}
}

View file

@ -0,0 +1,28 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
export component Modal {
callback clicked <=> touch_area.clicked;
background_layer := Rectangle {
clip: true;
touch_area := TouchArea {
@children
}
animate background { duration: MaterialAnimations.opacity_duration; easing: MaterialAnimations.opacity_easing; }
}
Timer {
interval: 50ms;
triggered => {
background_layer.background = MaterialPalette.background_modal;
self.running = false;
}
}
}

View file

@ -0,0 +1,150 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Badge } from "./badge.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialText } from "./material_text.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { ExtendedTouchArea } from "./extended_touch_area.slint";
import { StateLayer, Ripple } from "./state_layer.slint";
import { Icon } from "./icon.slint";
import { NavigationItem } from "../items/navigation_item.slint";
import { BaseNavigationItemTemplate, BaseNavigation } from "./base_navigation.slint";
export component NavigationItemTemplate inherits BaseNavigationItemTemplate {
property <bool> has_icon: root.icon.width > 0 && root.icon.height > 0;
property <bool> has_selected_icon: root.selected_icon.width > 0 || root.selected_icon.height > 0;
property <color> color: MaterialPalette.on_surface;
in property <length> navitem_padding_top: MaterialStyleMetrics.padding_12;
in property <length> navitem_padding_bottom: MaterialStyleMetrics.padding_16;
Rectangle {
width: 100%;
height: 100%;
}
touch_area := ExtendedTouchArea {
HorizontalLayout {
alignment: center;
VerticalLayout {
alignment: start;
spacing: MaterialStyleMetrics.spacing_4;
padding_top: root.navitem_padding_top;
padding_bottom: root.navitem_padding_bottom;
HorizontalLayout {
alignment: center;
background_layer := Rectangle {
min_width: state_layout.min_width;
min_height: max(MaterialStyleMetrics.size_32, state_layout.min_height);
border_radius: self.height / 2;
state_layer := StateLayer {
background: root.color;
border_radius: parent.border_radius;
enabled: true;
has_focus: touch_area.has_focus;
has_hover: touch_area.has_hover;
pressed: touch_area.pressed || touch_area.enter_pressed;
width: 100%;
height: 100%;
if touch_area.pressed && !root.selected : Ripple {
width: root.width;
height: root.height;
pressed_x: touch_area.pressed_x;
pressed_y: touch_area.pressed_y;
color: root.color;
}
state_layout := HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_20;
padding_right: self.padding_left;
alignment: center;
VerticalLayout {
alignment: center;
if !root.has_icon && !root.has_selected_icon : Rectangle {
width: 12px;
height: self.width;
border_radius: self.width / 2;
background: root.color;
}
if root.has_icon || root.has_selected_icon : Icon {
source: root.icon;
colorize: root.color;
states [
selected when root.selected && root.has_selected_icon : {
source: root.selected_icon;
}
]
}
}
}
}
if root.show_badge || root.badge != "" : Badge {
x: parent.width / 2;
y: 0;
text: root.badge;
}
}
}
label := MaterialText {
text: root.text;
style: MaterialTypography.label_medium;
color: root.color;
horizontal_alignment: center;
}
}
}
clicked => {
root.clicked();
}
}
states [
selected when root.selected : {
background_layer.background: MaterialPalette.secondary_container;
state_layer.background: transparent;
label.style: MaterialTypography.label_medium_prominent;
}
]
}
export component NavigationBar inherits BaseNavigation {
min_height: max(MaterialStyleMetrics.size_80, layout.min_height);
Rectangle {
background: MaterialPalette.surface_container;
layout := HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_8;
padding_right: self.padding_left;
spacing: MaterialStyleMetrics.spacing_8;
for item[index] in root.items : NavigationItemTemplate {
icon: item.icon;
selected_icon: item.selected_icon;
text: item.text;
index: index;
selected: index == root.current_index;
show_badge: item.show_badge;
badge: item.badge;
clicked => {
root.select(index);
}
}
}
}
}

View file

@ -0,0 +1,211 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Drawer, ModalDrawer, DrawerHeader } from "./drawer.slint";
import { NavigationItem, NavigationGroup } from "../items/navigation_item.slint";
import { HorizontalDivider } from "./divider.slint";
import { StateLayerArea } from "./state_layer.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { Icon } from "./icon.slint";
import { MaterialText } from "./material_text.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { BaseNavigationItemTemplate } from "./base_navigation.slint";
component NavigationItemTemplate inherits BaseNavigationItemTemplate {
property <bool> has_icon: root.icon.width > 0 && root.icon.height > 0;
property <bool> has_selected_icon: root.selected_icon.width > 0 || root.selected_icon.height > 0;
property <color> color: MaterialPalette.on_surface_variant;
min_height: max(MaterialStyleMetrics.size_56, layout.min_height);
background_layer := Rectangle {
border_radius: self.height / 2;
state_layer := StateLayerArea {
border_radius: parent.border_radius;
color: root.selected ? transparent : root.color;
layout := HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_16;
padding_right: MaterialStyleMetrics.padding_24;
padding_top: MaterialStyleMetrics.padding_16;
padding_bottom: self.padding_top;
spacing: MaterialStyleMetrics.spacing_12;
VerticalLayout {
alignment: center;
if !root.has_icon && !root.has_selected_icon : Rectangle {
width: MaterialStyleMetrics.icon_size_24;
height: self.width;
Rectangle {
width: 12px;
height: self.width;
border_radius: self.width / 2;
background: root.color;
}
}
if root.has_icon || root.has_selected_icon : Icon {
source: root.icon;
colorize: root.color;
states [
selected when root.selected && root.has_selected_icon : {
source: root.selected_icon;
}
]
}
}
MaterialText {
horizontal_stretch: 1;
text: root.text;
style: MaterialTypography.label_large;
color: root.color;
vertical_alignment: center;
}
if root.badge != "" : MaterialText {
text: root.badge;
style: MaterialTypography.label_large;
color: root.color;
vertical_alignment: center;
}
}
clicked => {
root.clicked();
}
pointer_event(event) => {
root.pointer_event(event, {
x: self.mouse_x,
y: self.mouse_y,
});
}
}
animate background { duration: MaterialAnimations.opacity_duration; easing: MaterialAnimations.opacity_easing; }
}
states [
selected when root.selected : {
background_layer.background: MaterialPalette.secondary_container;
root.color: MaterialPalette.on_secondary_container;
}
]
animate color { duration: MaterialAnimations.opacity_duration; easing: MaterialAnimations.opacity_easing; }
}
component NavigationGroupTemplate {
in property <string> title;
in property <[NavigationItem]> items;
in property <int> current_index: -1;
in property <bool> has_divider;
callback select(index: int);
callback item_pointer_event(index: int, event: PointerEvent, position: Point);
VerticalLayout {
alignment: start;
if root.title != 0 : DrawerHeader {
title: root.title;
}
for item[index] in root.items : NavigationItemTemplate {
icon: item.icon;
selected_icon: item.selected_icon;
text: item.text;
badge: item.badge;
selected: index == root.current_index;
index: index;
clicked => {
root.select(index);
}
pointer_event(event, position) => {
root.item_pointer_event(index, event, {
x: self.x + position.x,
y: self.y + position.y,
});
}
}
if root.has_divider : HorizontalDivider {}
}
}
export component NavigationDrawer inherits Drawer {
callback selected(group-index: int, item_index: int);
callback item_pointer_event(group_index: int, item_index: int, event: PointerEvent, position: Point);
in property <[NavigationGroup]> groups;
in_out property <int> current_group;
in_out property <int> current_index;
accessible-role: tab-list;
// accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-index;
// accessible-label: root.title;
accessible-item-count: root.groups.length;
for group[group_index] in root.groups : NavigationGroupTemplate {
title: group.title;
items: group.items;
current_index: group_index == root.current_group ? root.current_index : -1;
has_divider: root.groups.length > 1 && group_index < root.groups.length - 1;
select(index) => {
root.select(group_index, index);
}
item_pointer_event(index, event, position) => {
root.item_pointer_event(group_index, index, event, { x: self.x + position.x, y: self.y + position.y });
}
}
function select(group_index: int, item_index: int) {
if group_index < 0 || group_index >= root.groups.length || item_index < 0 || item_index >= root.groups[group_index].items.length {
return;
}
root.current_group = group_index;
root.current_index = item_index;
root.selected(group_index, item_index);
}
}
export component ModalNavigationDrawer inherits ModalDrawer {
in property <[NavigationGroup]> groups;
in_out property <int> current_group;
in_out property <int> current_index;
accessible-role: tab-list;
// accessible-delegate-focus: root.current-focused >= 0 ? root.current-focused : root.current-index;
// accessible-label: root.title;
accessible-item-count: root.groups.length;
for group[group_index] in root.groups : NavigationGroupTemplate {
title: group.title;
items: group.items;
current_index: group_index == root.current_group ? root.current_index : -1;
has_divider: root.groups.length > 1 && group_index < root.groups.length - 1;
select(index) => {
root.select(group_index, index);
}
}
function select(group_index: int, item_index: int) {
if group_index < 0 || group_index >= root.groups.length || item_index < 0 || item_index >= root.groups[group_index].items.length {
return;
}
root.current_group = group_index;
root.current_index = item_index;
root.close();
}
}

View file

@ -0,0 +1,85 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { NavigationItem } from "../items/navigation_item.slint";
import { FloatingActionButton, FABStyle } from "./floating_action_button.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { IconButton } from "icon_button.slint";
import { Icons } from "../icons/icons.slint";
import { NavigationItemTemplate } from "navigation_bar.slint";
import { BaseNavigation } from "./base_navigation.slint";
export component NavigationRail inherits BaseNavigation {
in property <bool> has_menu;
in property <image> fab_icon;
in property <LayoutAlignment> alignment;
callback menu_clicked();
callback fab_clicked();
min_width: max(MaterialStyleMetrics.size_80, layout.min_width);
Rectangle {
background: MaterialPalette.surface;
border-radius: 5px;
layout := VerticalLayout {
padding_top: MaterialStyleMetrics.padding_44;
padding_bottom: MaterialStyleMetrics.padding_56;
spacing: MaterialStyleMetrics.spacing_4;
if root.has_menu : IconButton {
icon: Icons.menu;
clicked => {
root.menu_clicked();
}
}
if root.fab_icon.width > 0 && root.fab_icon.height > 0 : HorizontalLayout {
alignment: center;
FloatingActionButton {
icon: root.fab_icon;
style: FABStyle.standard;
clicked => {
root.fab_clicked();
}
}
}
// helper to place fab at the top if no items are present
if root.items.length == 0 : Rectangle {
horizontal_stretch: 1;
}
HorizontalLayout {
vertical_stretch: 1;
alignment: center;
VerticalLayout {
alignment: root.alignment;
for item[index] in root.items : NavigationItemTemplate {
icon: item.icon;
selected_icon: item.selected_icon;
text: item.text;
index: index;
selected: index == root.current_index;
show_badge: item.show_badge;
badge: item.badge;
navitem_padding_top: 0;
navitem_padding_bottom: MaterialStyleMetrics.padding_4;
clicked => {
root.select(index);
}
}
}
}
}
}
}

View file

@ -0,0 +1,42 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component OutlineButton {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <bool> enabled <=> base.enabled;
in property <string> tooltip <=> base.tooltip;
callback clicked <=> base.clicked;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-action-default => { base.clicked(); }
base := BaseButton {
border_radius: border.border_radius;
color: MaterialPalette.primary;
}
border := Rectangle {
border_radius: root.height / 2;
border_width: 1px;
border_color: MaterialPalette.outline;
width: 100%;
height: 100%;
}
states [
disabled when !root.enabled : {
base.color: MaterialPalette.on_surface;
base.transparent_background: true;
border.border_color: MaterialPalette.on_surface;
border.opacity: MaterialPalette.state_layer_opacity_hover;
}
]
}

View file

@ -0,0 +1,78 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component OutlineIconButton {
in property <image> icon;
in property <image> checked_icon: root.icon;
in property <bool> checkable;
in_out property <bool> checked;
in property <string> tooltip <=> base.tooltip;
in property <bool> enabled <=> base.enabled;
callback clicked();
accessible-role: button;
accessible-enabled: true;
accessible-label: root.tooltip;
accessible-checkable: root.checkable;
accessible-checked: root.checked;
accessible-action-default => { base.clicked(); }
background_layer := Rectangle {
width: 100%;
height: 100%;
border_radius: base.border_radius;
border_width: 1px;
border_color: MaterialPalette.outline;
}
base := BaseButton {
icon: root.checked ? root.checked_icon : root.icon;
border_radius: self.height / 2;
color: !root.enabled ? MaterialPalette.on_surface_variant : MaterialPalette.on_surface_variant;
button_horizontal_padding: 0;
button_vertical_padding: 0;
width: self.height;
icon_size: MaterialStyleMetrics.icon_size_24;
display_background: false;
clicked => {
root.toggle();
root.clicked();
}
}
function toggle() {
if !root.checkable {
return;
}
root.checked = !root.checked;
}
states [
disabled when !root.enabled : {
base.color: MaterialPalette.on_surface;
base.transparent_background: true;
background_layer.border_color: root.checked ? transparent : MaterialPalette.on_surface;
background_layer.background: root.checked ? MaterialPalette.on_surface : transparent;
background_layer.opacity: MaterialPalette.state_layer_opacity_hover;
}
checked when root.enabled && root.checked : {
base.color: MaterialPalette.inverse_on_surface;
background_layer.background: MaterialPalette.inverse_surface;
background_layer.border_width: 0;
}
checked_disabled when root.enabled && root.checked : {
base.color: MaterialPalette.inverse_on_surface;
background_layer.background: MaterialPalette.inverse_on_surface;
background_layer.border_width: 0;
}
]
}

View file

@ -0,0 +1,107 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component LinearProgressIndicator {
in property <float> progress;
in property <bool> indeterminate;
height: MaterialStyleMetrics.size_4;
Rectangle {
width: 100%;
height: 100%;
clip: true;
background: MaterialPalette.primary_container;
border_radius: MaterialStyleMetrics.border_radius_2;
track := Rectangle {
x: 0;
y: 0;
width: parent.width * root.progress;
height: 100%;
border_radius: parent.border_radius;
background: MaterialPalette.primary;
}
}
}
export component CircularProgressIndicator {
in property <float> progress;
in property <bool> indeterminate;
property <length> bar_height: MaterialStyleMetrics.size_4;
width: self.height;
min_height: MaterialStyleMetrics.size_40;
background_layer := Rectangle {
width: root.width;
height: root.height;
border_width: root.bar_height;
border_color: root.progress == 1 ? MaterialPalette.primary : MaterialPalette.primary_container;
border_radius: max(self.width, self.height) / 2;
}
track := Path {
property <float> radius: min(self.viewbox_width, self.viewbox_height) / 2;
property <float> start_x: self.viewbox_width / 2;
property <float> start_y: self.viewbox_height / 2;
property <float> inner_radius: self.radius - (root.bar_height * (self.viewbox_height / self.height));
property <float> start: !root.indeterminate ? 0 : 1 * mod(animation-tick(), 1.5s) / 1.5s;
property <float> progress: !root.indeterminate ? root.progress : 0.5;
x: background_layer.x;
y: background_layer.y;
viewbox_width: 100;
viewbox_height: 100;
width: background_layer.width;
height: background_layer.height;
fill: MaterialPalette.primary;
visible: self.progress > 0 && self.progress < 1;
MoveTo {
x: !root.indeterminate ? track.start_x : track.start_x - track.radius * sin(-track.start * 360deg);
y: !root.indeterminate ? 0 : track.start_y - track.radius * cos(-track.start * 360deg);
}
ArcTo {
radius_x: 1;
radius_y: 1;
x: !root.indeterminate ? track.start_x : track.start_x - track.inner-radius * sin(-track.start * 360deg);
y: !root.indeterminate ? root.bar_height * (track.viewbox_height / track.height) : track.start_y - track.inner_radius * cos(-track.start * 360deg);
}
ArcTo {
radius_x: track.inner_radius;
radius_y: track.inner_radius;
x: start_x - track.inner_radius * sin(-(track.start + track.progress) * 360deg);
y: start_y - track.inner_radius * cos(-(track.start + track.progress) * 360deg);
sweep: track.progress > 0;
large-arc: track.progress > 0.5;
}
ArcTo {
radius_x: 1;
radius_y: 1;
x: start_x - radius * sin(-(track.start + track.progress) * 360deg);
y: start_y - radius * cos(-(track.start + track.progress) * 360deg);
}
ArcTo {
radius_x: radius;
radius_y: radius;
x: start_x - radius * sin(-track.start * 360deg);
y: start_y - radius * cos(-track.start * 360deg);
sweep: track.progress < 0;
large-arc: track.progress > 0.5;
}
LineTo {
x: start_x - radius * sin(-track.start * 360deg);
y: start_y - radius * cos(-track.start * 360deg);
}
}
}

View file

@ -0,0 +1,99 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { StateLayerArea } from "./state_layer.slint";
import { ListTile } from "./list.slint";
import { ListView } from "./list_view.slint";
export component RadioButton {
in property <bool> checked;
in property <bool> enabled;
callback clicked();
min_width: MaterialStyleMetrics.size_40;
min_height: self.min_width;
accessible-enabled: root.enabled;
accessible-checkable: true;
accessible-checked <=> root.checked;
accessible-role: checkbox;
accessible-action-default => { state_area.clicked(); }
forward_focus: state_area;
state_area := StateLayerArea {
width: 100%;
height: 100%;
border_radius: max(self.width, self.height) / 2;
color: MaterialPalette.on_surface;
border := Rectangle {
width: MaterialStyleMetrics.size_20;
height: self.width;
border_radius: max(self.width, self.height) / 2;
border_color: MaterialPalette.on_surface_variant;
border_width: MaterialStyleMetrics.size_2;
indicator := Rectangle {
width: parent.width / 2;
height: self.width;
border_radius: max(self.width, self.height) / 2;
background: MaterialPalette.primary;
opacity: !root.checked ? 0 : 1;
animate opacity {
duration: MaterialAnimations.opacity_duration;
easing: MaterialAnimations.opacity_easing;
}
}
animate border_color {
duration: MaterialAnimations.opacity_duration;
easing: MaterialAnimations.opacity_easing;
}
}
clicked => {
root.clicked();
}
}
states [
disabled when !root.enabled : {
border.border_color: MaterialPalette.on_surface;
border.opacity: MaterialPalette.disable_opacity;
indicator.background: MaterialPalette.on_surface;
indicator.opacity: root.checked ? MaterialPalette.disable_opacity : 0;
}
highlighted when !root.checked && (state_area.pressed || state_area.has_focus || state_area.has_hover) : {
border.border_color: MaterialPalette.on_surface;
}
checked when root.checked : {
border.border_color: MaterialPalette.primary;
}
]
}
export component RadioButtonTile inherits ListTile {
in_out property <bool> checked <=> radio_button.checked;
callback radio_button_clicked();
Rectangle {
horizontal_stretch: 0;
radio_button := RadioButton {
enabled: root.enabled;
clicked => {
root.radio_button_clicked();
}
}
}
clicked => {
radio_button.clicked();
}
}

View file

@ -0,0 +1,166 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
export component ScrollBar {
in property <bool> enabled;
out property <bool> has_hover: touch_area.has_hover;
in_out property <bool> horizontal;
in_out property <length> maximum;
in_out property <length> page_size;
in_out property <length> value;
in property <ScrollBarPolicy> policy: ScrollBarPolicy.as_needed;
property <length> track_size: root.horizontal ? root.width - 2 * root.offset : root.height - 2 * offset;
property <length> step_size: 10px;
property <length> offset: 2px;
property <float> state_opacity: 40%;
visible: (self.policy == ScrollBarPolicy.always_on) || (self.policy == ScrollBarPolicy.as_needed && self.maximum > 0);
Rectangle {
clip: true;
thumb := Rectangle {
width: !root.horizontal ? parent.width : root.maximum <= 0phx ? 0phx : max(min(32px, root.width), root.track-size * root.page-size / (root.maximum + root.page-size));
height: root.horizontal ? parent.height : root.maximum <= 0phx ? 0phx : max(min(32px, root.height), root.track-size * (root.page-size / (root.maximum + root.page-size)));
x: !root.horizontal ? (parent.width - self.width) / 2 : root.offset + (root.track-size - thumb.width) * (-root.value / root.maximum);
y: root.horizontal ? (parent.height - self.height) / 2 : root.offset + (root.track-size - thumb.height) * (-root.value / root.maximum);
border-radius: (root.horizontal ? self.height : self.width) / 2;
background: MaterialPalette.on_background;
opacity: hide_timer.running || touch_area.has_hover || touch_area.pressed ? root.state_opacity : 0;
animate opacity { duration: 150ms; }
}
}
touch_area := TouchArea {
property <length> pressed_value;
width: parent.width;
height: parent.height;
pointer-event(event) => {
if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) {
self.pressed-value = -root.value;
}
}
moved => {
if (self.enabled && self.pressed) {
root.value = -max(0px, min(root.maximum, self.pressed-value + (
root.horizontal ? (touch-area.mouse-x - touch-area.pressed-x) * (root.maximum / (root.track-size - thumb.width))
: (touch-area.mouse-y - touch-area.pressed-y) * (root.maximum / (root.track-size - thumb.height))
)));
}
}
scroll-event(event) => {
if (root.horizontal && event.delta-x != 0) {
root.value = max(-root.maximum, min(0px, root.value + event.delta-x));
return accept;
} else if (!root.horizontal && event.delta-y != 0) {
root.value = max(-root.maximum, min(0px, root.value + event.delta-y));
return accept;
}
reject
}
changed has_hover => {
if !self.has_hover && !hide_timer.running {
hide_timer.running = true;
}
}
changed pressed => {
if self.pressed && hide_timer.running {
hide_timer.running = false;
}
}
}
changed value => {
if !self.has_hover && !hide_timer.running {
hide_timer.running = true;
}
}
hide_timer := Timer {
interval: 1.5s;
running: false;
triggered => {
self.running = false;
}
}
states [
pressed when touch_area.pressed : {
root.state_opacity: 80%;
}
hover when root.has_hover : {
root.state_opacity: 70%;
}
]
}
export component ScrollView {
in property <bool> enabled: true;
out property <length> visible_width <=> flickable.width;
out property <length> visible_height <=> flickable.height;
in_out property <length> viewport_width <=> flickable.viewport_width;
in_out property <length> viewport_height <=> flickable.viewport_height;
in_out property <length> viewport_x <=> flickable.viewport_x;
in_out property <length> viewport_y <=> flickable.viewport_y;
in property <ScrollBarPolicy> vertical_scrollbar_policy <=> vertical_bar.policy;
in property <ScrollBarPolicy> horizontal_scrollbar_policy <=> horizontal_bar.policy;
// FIXME: remove. This property is currently set by the ListView and is used by the native style to draw the scrollbar differently when it has focus
in_out property <bool> has_focus;
callback scrolled <=> flickable.flicked;
property <length> scroll_bar_size: 8px;
property <length> scroll_bar_padding: 1px;
min_height: 50px;
min_width: 50px;
horizontal_stretch: 1;
vertical_stretch: 1;
preferred_height: 100%;
preferred_width: 100%;
flickable := Flickable {
x: 0;
y: 0;
viewport_y <=> vertical_bar.value;
viewport_x <=> horizontal_bar.value;
width: root.width - (root.vertical_scrollbar_policy != ScrollBarPolicy.always_off ? root.scroll_bar_size - 2 * root.scroll_bar_padding : 0);
height: root.height - (root.horizontal_scrollbar_policy != ScrollBarPolicy.always_off ? root.scroll_bar_size : 0);
@children
}
vertical_bar := ScrollBar {
enabled: root.enabled;
x: flickable.width + root.scroll_bar_padding;
y: 0;
width: root.visible ? root.scroll_bar_size : 0;
height: root.vertical_scrollbar_policy != ScrollBarPolicy.always_off ? parent.height - horizontal_bar.height : parent.height;
horizontal: false;
maximum: flickable.viewport_height - flickable.height;
page-size: flickable.height;
}
horizontal_bar := ScrollBar {
enabled: root.enabled;
width: vertical_bar.visible ? parent.width - vertical_bar.width : parent.width;
height: root.horizontal_scrollbar_policy != ScrollBarPolicy.always_off ? root.scroll_bar_size : 0;
x: 0;
y: flickable.height + root.scroll_bar_padding;
horizontal: true;
maximum: flickable.viewport_width - flickable.width;
page_size: flickable.width;
}
}

View file

@ -0,0 +1,317 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { ListItem } from "../items/list_item.slint";
import { Avatar, ListTile } from "./list.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { StateLayerArea } from "./state_layer.slint";
import { MaterialText } from "./material_text.slint";
import { Icon } from "./icon.slint";
import { IconButton } from "./icon_button.slint";
import { Icons } from "../icons/icons.slint";
import { HorizontalDivider } from "./divider.slint";
import { ListView } from "./list_view.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
component SearchTile inherits ListTile {
in property <image> action_button_icon;
callback action_button_clicked();
if root.action_button_icon.width > 0 && root.action_button_icon.height > 0 : IconButton {
icon: root.action_button_icon;
clicked => {
root.action_button_clicked();
}
}
}
component SearchIcon {
in property <image> icon;
in property <color> color;
VerticalLayout {
alignment: center;
padding_left: MaterialStyleMetrics.padding_8;
padding_right: self.padding_left;
Icon {
source: root.icon;
colorize: root.color;
}
}
}
component SearchTextInput {
in_out property <string> text <=> text_input.text;
in property <string> placeholder_text;
property <length> computed_x;
callback accepted(text: string);
callback edited(text: string);
callback key_pressed(event: KeyEvent) -> EventResult;
callback key_released(event: KeyEvent) -> EventResult;
forward_focus: text_input;
horizontal_stretch: 1;
Rectangle {
clip: true;
text_input := TextInput {
x: min(0px, max(parent.width - self.width - self.text_cursor_width, root.computed_x));
width: max(parent.width - self.text-cursor-width, self.preferred-width);
height: 100%;
font_size: MaterialTypography.body_large.font_size;
font_weight: MaterialTypography.body_large.font_weight;
vertical_alignment: center;
single_line: true;
color: MaterialPalette.on_surface;
selection_background_color: MaterialPalette.secondary_container;
selection_foreground_color: self.color;
// Disable TextInput's built-in accessibility support as the component takes care of that.
accessible-role: none;
cursor_position_changed(cursor_position) => {
if cursor_position.x + root.computed_x < 0 {
root.computed_x = - cursor_position.x;
} else if cursor-position.x + root.computed_x > parent.width - self.text-cursor-width {
root.computed_x = parent.width - cursor_position.x - self.text-cursor-width;
}
}
accepted => {
root.accepted(self.text);
}
edited => {
root.edited(self.text);
}
key_pressed(event) => {
root.key_pressed(event)
}
key_released(event) => {
root.key_released(event)
}
init => {
if root.text.character_count > 0 {
self.set_selection_offsets(root.text.character_count, root.text.character_count);
}
}
}
if root.text == "" && root.placeholder_text != "" : MaterialText {
width: 100%;
height: 100%;
text: root.placeholder_text;
color: MaterialPalette.on_surface_variant;
vertical_alignment: center;
style: MaterialTypography.body_large;
}
}
}
export component SearchBar {
in property <image> leading_icon: Icons.menu;
in property <image> trailing_icon;
in property <image> avatar_icon;
in property <color> avatar_background: #00000000;
in property <string> placeholder_text;
in property <string> empty_text;
in_out property <string> text;
in_out property <int> current_index;
in property <[ListItem]> items;
callback accepted(text: string);
callback edited(text: string);
callback action_button_clicked(index: int);
callback key_pressed(event: KeyEvent) -> EventResult;
callback key_released(event: KeyEvent) -> EventResult;
property <color> color: MaterialPalette.on_surface_variant;
property <length> item_height: MaterialStyleMetrics.size_72;
min_height: max(MaterialStyleMetrics.size_56, layout.min_height);
forward_focus: state_layer;
Rectangle {
background: MaterialPalette.surface_container_high;
border_radius: MaterialStyleMetrics.border_radius_28;
state_layer := StateLayerArea {
border_radius: parent.border_radius;
color: root.color;
layout := HorizontalLayout {
padding: MaterialStyleMetrics.padding_4;
spacing: MaterialStyleMetrics.spacing_4;
SearchIcon {
icon: root.leading_icon;
color: root.color;
}
MaterialText {
text: root.text;
font_size: MaterialTypography.body_large.font_size;
font_weight: MaterialTypography.body_large.font_weight;
vertical_alignment: center;
color: MaterialPalette.on_surface;
states [
placeholder when root.text == "" : {
text: root.placeholder_text;
color: MaterialPalette.on_surface;
}
]
}
SearchIcon {
icon: root.trailing_icon;
color: root.color;
}
if (root.avatar_icon.width > 0 && root.avatar_icon.height > 0) || root.avatar_background != #00000000 : VerticalLayout {
alignment: center;
Avatar {
image: root.avatar_icon;
background: root.avatar_background;
}
}
}
changed has_focus => {
if self.has_focus {
popup.show();
}
}
clicked => {
popup.show();
}
}
}
popup := PopupWindow {
x: 0;
y: 0;
width: root.width;
height: root.height + clamp(root.item_height * root.items.length, 3 * root.item_height, 6 * root.item_height);
close_policy: close_on_click_outside;
forward-focus: popup_input;
background_layer := Rectangle {
x: 0;
y: 0;
width: 100%;
height: root.height;
background: MaterialPalette.surface_container_high;
border_radius: MaterialStyleMetrics.border_radius_28;
clip: true;
VerticalLayout {
HorizontalLayout {
padding: MaterialStyleMetrics.padding_4;
spacing: MaterialStyleMetrics.spacing_4;
IconButton {
icon: Icons.arrow_back;
clicked => {
popup.close();
}
}
popup_input := SearchTextInput {
placeholder_text: root.placeholder_text;
text: root.text;
accepted => {
root.accepted(self.text);
}
edited => {
root.text = self.text;
root.edited(self.text);
}
key_pressed(event) => {
root.key_pressed(event)
}
key_released(event) => {
root.key_released(event)
}
changed text => {
root.text = text;
}
}
IconButton {
icon: Icons.close;
clicked => {
root.text = "";
popup.close();
}
}
}
HorizontalDivider {}
if root.items.length == 0 : MaterialText {
text: root.empty_text;
color: root.color;
horizontal_alignment: center;
style: MaterialTypography.label_small;
}
if root.items.length == 0 : Rectangle {}
if root.items.length > 0 : ListView {
horizontal_stretch: 1;
for item[index] in root.items : SearchTile {
text: item.text;
supporting_text: item.supporting_text;
avatar_icon: item.avatar_icon;
avatar_text: item.avatar_text;
avatar_background: item.avatar_background;
avatar_foreground: item.avatar_foreground;
action_button_icon: item.action_button_icon;
clicked => {
root.text = self.text;
popup.close();
}
action_button_clicked => {
root.action_button_clicked(index);
}
}
}
}
animate height {
duration: MaterialAnimations.standard_accelerate_duration;
easing: MaterialAnimations.standard_easing;
}
}
Timer {
interval: 50ms;
triggered => {
background_layer.height = popup.height;
self.running = false;
}
}
}
}

View file

@ -0,0 +1,103 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { BaseButton } from "base_button.slint";
import { Icons } from "../icons/icons.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
export struct SegmentedItem {
icon: image,
text: string
}
component SegmentedItemTemplate {
in property <image> icon;
in property <string> text;
in property <int> index;
in property <bool> selected;
in property <bool> last;
callback clicked <=> base.clicked;
accessible-role: list-item;
accessible-label: root.text;
accessible-item-selectable: true;
accessible-item-selected: root.selected;
accessible-item-index: root.index;
if root.selected : Rectangle {
background: MaterialPalette.secondary_container;
}
base := BaseButton {
min_layout_width: 0;
min_layout_height: 0;
color: MaterialPalette.on_surface;
button_vertical_padding: MaterialStyleMetrics.padding_10;
button_horizontal_padding: MaterialStyleMetrics.padding_24;
spacing: MaterialStyleMetrics.spacing_8;
icon: root.icon;
text: root.text;
animate width { duration: MaterialAnimations.standard_accelerate_duration; easing: MaterialAnimations.standard_easing; }
}
if !root.last : Rectangle {
x: parent.width - self.width;
width: 1px;
height: 100%;
background: MaterialPalette.outline;
}
states [
selected when root.selected : {
base.icon: Icons.check;
}
]
}
export component SegmentedButton {
in property <[SegmentedItem]> items;
in_out property <int> current_index;
callback index_changed(index: int);
min_width: max(2 * MaterialStyleMetrics.size_40, layout.min_width);
min_height: max(MaterialStyleMetrics.size_40, layout.min_height);
accessible-role: list;
accessible-item-count: root.items.length;
Rectangle {
clip: true;
border_width: 1px;
border_radius: self.height / 2;
border_color: MaterialPalette.outline;
layout := HorizontalLayout {
for item[index] in root.items : SegmentedItemTemplate {
icon: item.icon;
text: item.text;
index: index;
last: index == root.items.length - 1;
selected: index == root.current_index;
clicked => {
root.select(index);
}
}
}
}
function select(index: int) {
if index < 0 || index >= root.items.length {
return;
}
root.current_index = index;
root.index_changed(index);
}
}

View file

@ -0,0 +1,136 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
//
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { StateLayer } from "./state_layer.slint";
export component Slider {
in property <bool> enabled;
in_out property <float> value;
in property <float> minimum;
in property <int> stop_count: 0;
in property <float> maximum: 100;
callback released(value: float);
callback value_changed(value: float);
property <int> steps: root.stop_count > 0 ? (root.maximum - root.minimum) / root.stop_count : 1;
min_height: 20px;
accessible-role: slider;
accessible-enabled: root.enabled;
accessible-value: root.value;
accessible-value-minimum: root.minimum;
accessible-value-maximum: root.maximum;
accessible-value-step: min(steps, (root.maximum - root.minimum) / 100);
forward-focus: focus_scope;
focus_scope := FocusScope {
touch_area := TouchArea {
track := Rectangle {
x: 0;
height: MaterialStyleMetrics.size_16;
background: MaterialPalette.surface_container_highest;
border_radius: self.height / 2;
Rectangle {
x: 0;
width: state_layer.x - MaterialStyleMetrics.size_6;
border-top-left-radius: track.border_radius;
border-bottom-left-radius: track.border_radius;
border-top-right-radius: MaterialStyleMetrics.size_1;
border-bottom-right-radius: MaterialStyleMetrics.size_1;
background: thumb.background;
}
if root.stop_count > 0: Rectangle {
width: 100%;
height: 100%;
for i in (root.maximum - root.minimum) / root.steps: Rectangle {
x: root.value_to_length(track.width, (i + 1) * root.steps) - self.width / 2;
height: MaterialStyleMetrics.size_4;
width: self.height;
border_radius: self.height / 2;
background: MaterialPalette.outline;
}
}
}
state_layer := Rectangle {
x: root.value_to_length(track.width, root.value) - (self.width) / 2;
width: 0;
height: track.height + (MaterialStyleMetrics.size_14 * 2);
StateLayer {
height: track.height + (MaterialStyleMetrics.size_16 * 2);
width: thumb.width + (MaterialStyleMetrics.size_2 * 2);
border_radius: self.width / 2;
pressed: touch_area.pressed;
has_hover: touch_area.has_hover;
has_focus: focus_scope.has_focus;
enabled: root.enabled;
background: thumb.background;
display_background: thumb_area.pressed || thumb_area.has_hover || focus_scope.has_focus;
thumb_area := TouchArea {
height: track.height + (MaterialStyleMetrics.size_14 * 2);
width: self.height;
thumb := Rectangle {
width: MaterialStyleMetrics.size_4;
border_radius: self.width / 2;
background: MaterialPalette.primary;
}
moved => {
root.set_value(root.length_to_value(state_layer.x + self.mouse_x, track.width));
}
pointer_event(event) => {
if event.kind == PointerEventKind.up {
root.released(root.value);
}
}
}
}
}
pointer_event(event) => {
if event.kind == PointerEventKind.down && event.button == PointerEventButton.left {
root.set_value(root.length_to_value(self.mouse_x, track.width));
}
if event.kind == PointerEventKind.up {
root.released(root.value);
}
}
}
key_pressed(event) => {
if event.text == Key.LeftArrow {
root.set_value(root.value - root.steps);
return accept;
}
if event.text == Key.RightArrow {
root.set_value(root.value + root.steps);
return accept;
}
reject
}
}
changed value => {
root.value_changed(root.value);
}
pure function value_to_length(width: length, value: float) -> length {
clamp(width * (value - root.minimum) / (root.maximum - root.minimum), 0, width)
}
pure function length_to_value(x: length, width: length) -> float {
x * (root.maximum - root.minimum) / width
}
function set_value(value: float) {
root.value = clamp(round(value / root.steps) * root.steps, root.minimum, root.maximum)
}
}

View file

@ -0,0 +1,88 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { Elevation } from "elevation.slint";
import { MaterialText } from "./material_text.slint";
import { TextButton } from "./text_button.slint";
import { IconButton } from "./icon_button.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { Icons } from "../icons/icons.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
export component SnackBar inherits PopupWindow {
in property <string> text;
in property <string> action_text;
in property <bool> has_close_button;
close_policy: no_auto_close;
callback action_clicked();
snack_bar := Elevation {
x: (parent.width - self.width) / 2;
y: parent.height - self.height;
width: MaterialStyleMetrics.size_344;
level: 3;
background_layer := Rectangle {
background: MaterialPalette.inverse_surface;
border_radius: MaterialStyleMetrics.border_radius_4;
HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_16;
padding_right: self.padding_left;
padding_top: MaterialStyleMetrics.padding_10;
padding_bottom: self.padding_top;
spacing: MaterialStyleMetrics.spacing_4;
MaterialText {
text: root.text;
vertical_alignment: center;
horizontal_alignment: left;
wrap: word_wrap;
style: MaterialTypography.body_medium;
color: MaterialPalette.inverse_on_surface;
}
if root.action_text != "" : TextButton {
text: root.action_text;
inverse: true;
clicked => {
root.raise_action();
}
}
if root.has_close_button : IconButton {
icon: Icons.close;
inverse: true;
clicked => {
root.close();
}
}
}
}
animate y, height {
duration: MaterialAnimations.standard_accelerate_duration;
easing: MaterialAnimations.standard_easing;
}
}
Timer {
interval: 50ms;
triggered => {
snack_bar.y = root.height - snack_bar.height - MaterialStyleMetrics.padding_30;
self.running = false;
}
}
function raise_action() {
root.action_clicked();
root.close();
}
}

View file

@ -0,0 +1,171 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialAnimations } from "../styling/material_animations.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { ToolTip } from "./tooltip.slint";
import { MaterialWindowAdapter } from "./material_window.slint";
export component Ripple {
in property <length> border_radius;
in property <length> pressed_x;
in property <length> pressed_y;
in property <color> color;
in property <bool> clip_ripple: true;
Rectangle {
width: 100%;
height: 100%;
border_radius: root.border_radius;
clip: root.clip_ripple;
ripple := Rectangle {
x: root.pressed_x - self.width / 2;
y: root.pressed_y - self.height / 2;
height: self.width;
border_radius: self.width / 2;
opacity: MaterialPalette.state_layer_opacity_press;
background: root.color;
init => {
self.width = root.width * 2 * 1.4142;
}
animate width { duration: MaterialAnimations.ripple_duration; easing: MaterialAnimations.ripple_easing; }
}
}
}
export component StateLayer {
in property <color> background;
in property <length> border_radius;
in property <bool> display_background: true;
in property <bool> enabled;
in property <bool> pressed;
in property <bool> has_focus;
in property <bool> has_hover;
property <float> state_layer_opacity;
Rectangle {
width: 100%;
height: 100%;
opacity: root.state_layer_opacity;
background: root.background;
border_radius: root.border_radius;
}
Rectangle {
width: 100%;
height: 100%;
clip: true;
border_radius: root.border_radius;
@children
}
states [
disabled when !root.enabled && root.display_background : {
root.state_layer_opacity: MaterialPalette.state_layer_opacity_focus;
}
focus when root.enabled && root.has_focus : {
root.state_layer_opacity: MaterialPalette.state_layer_opacity_focus;
}
pressed when root.enabled && root.pressed : {
root.state_layer_opacity: MaterialPalette.state_layer_opacity_press;
}
hover when root.enabled && root.has_hover && !MaterialWindowAdapter.disable_hover : {
root.state_layer_opacity: MaterialPalette.state_layer_opacity_hover;
}
]
animate state_layer_opacity { duration: MaterialAnimations.opacity_duration; easing: MaterialAnimations.opacity_easing; }
}
export component FocusTouchArea inherits TouchArea {
out property <bool> has_focus <=> focus_scope.has_focus;
out property <bool> enter_pressed;
in property <string> tooltip;
in property <length> tooltip_offset;
callback key_pressed(KeyEvent) -> EventResult;
forward_focus: focus_scope;
focus_scope := FocusScope {
x: 0;
width: 0px;
enabled: root.enabled;
key_pressed(event) => {
if !root.enabled {
return reject;
}
if (event.text == " " || event.text == "\n") && !root.enter_pressed {
root.enter_pressed = true;
root.clicked();
return accept;
}
root.key_pressed(event)
}
key_released(event) => {
if !root.enabled {
return reject;
}
if (event.text == " " || event.text == "\n") && root.enter_pressed {
root.enter_pressed = false;
return accept;
}
reject
}
}
@children
if root.tooltip != "" && root.has_hover && !MaterialWindowAdapter.disable_hover && !root.pressed : ToolTip {
y: root.tooltip_offset;
text: root.tooltip;
}
}
export component StateLayerArea inherits FocusTouchArea {
in property <color> color;
in property <length> border_radius;
in property <bool> transparent_background;
in property <bool> display_background: true;
in property <bool> clip_ripple: true;
property <bool> popup_open;
property <bool> has_h: root.mouse-x > 0 && root.mouse-x < root.width && root.mouse-y > 0 && root.mouse-y < root.height;
if !root.enabled || (root.enabled && (root.has_focus || root.pressed || root.enter_pressed || root.has_hover)) : StateLayer {
width: 100%;
height: 100%;
background: root.transparent_background ? transparent : root.color;
border_radius: root.border_radius;
enabled: root.enabled;
pressed: root.pressed || root.enter_pressed;
has_focus: root.has_focus;
has_hover: root.has_hover;
display_background: root.display_background;
}
// ripple
if root.enabled && (root.pressed || root.enter_pressed) : Ripple {
pressed_x: root.pressed_x;
pressed_y: root.pressed_y;
width: 100%;
height: 100%;
border_radius: root.border_radius;
color: root.color;
clip_ripple: root.clip_ripple;
}
@children
}

View file

@ -0,0 +1,151 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { FocusTouchArea, StateLayer, Ripple } from "./state_layer.slint";
import { Icon } from "./icon.slint";
export component Switch {
in_out property <bool> checked;
in property <bool> enabled: true;
in property <string> tooltip <=> touch_area.tooltip;
in property <image> on_icon;
in property <image> off_icon;
property <bool> has_icon: (root.checked && root.on_icon.width > 0 && root.on_icon.height > 0) || (root.off_icon.width > 0 && root.off_icon.height > 0);
property <color> foreground: root.checked ? MaterialPalette.on_primary_container : MaterialPalette.surface_container_highest;
callback checked_state_changed(checked: bool);
min_width: MaterialStyleMetrics.size_52;
min_height: MaterialStyleMetrics.size_32;
accessible-enabled: root.enabled;
accessible-checkable: true;
accessible-checked <=> root.checked;
accessible-role: checkbox;
accessible-action-default => { touch_area.clicked(); }
forward_focus: touch_area;
touch_area := FocusTouchArea {
enabled: root.enabled;
background_layer := Rectangle {
background: root.checked ? MaterialPalette.primary : MaterialPalette.surface_container_highest;
border_radius: self.height / 2;
border_width: root.checked ? 0 : 2px;
border_color: MaterialPalette.outline;
state_layer := StateLayer {
x: indicator.x + (indicator.width - self.width) / 2;
width: self.height;
height: root.height + MaterialStyleMetrics.size_8;
border_radius: self.height / 2;
has_hover: touch_area.has_hover;
has_focus: touch_area.has_focus;
pressed: touch_area.pressed;
enabled: root.enabled;
// ripple
if root.enabled && (touch_area.pressed || touch_area.enter_pressed) : Ripple {
pressed_x: touch_area.pressed_x;
pressed_y: touch_area.pressed_y;
width: 100%;
height: 100%;
border_radius: state_layer.border_radius;
color: state_layer.background;
clip_ripple: true;
}
}
indicator := Rectangle {
property <length> indicator-padding: root.checked || root.has_icon ? MaterialStyleMetrics.padding_4 : MaterialStyleMetrics.padding_8;
property <length> default_height: background_layer.height - 2 * self.indicator-padding;
x: self.indicator-padding - (self.height - self.default_height) / 2;
width: self.height;
height: self.default_height;
border_radius: self.height / 2;
background: root.checked ? MaterialPalette.primary_container : MaterialPalette.outline;
if root.has_icon : Icon {
source: root.checked ? root.on_icon : root.off_icon;
colorize: root.foreground;
animate colorize {
duration: MaterialAnimations.opacity_duration;
easing: MaterialAnimations.opacity_easing;
}
}
animate height {
duration: MaterialAnimations.emphasized_accelerate_duration;
easing: MaterialAnimations.emphasized_easing;
}
animate background {
duration: MaterialAnimations.opacity_duration;
easing: MaterialAnimations.opacity_easing;
}
states [
checked when root.checked : {
x: background_layer.width - self.indicator-padding - self.width;
in {
animate x {
duration: MaterialAnimations.emphasized_accelerate_duration;
easing: MaterialAnimations.emphasized_easing;
}
}
out {
animate x {
duration: MaterialAnimations.emphasized_accelerate_duration;
easing: MaterialAnimations.emphasized_easing;
}
}
}
]
}
animate background, border_color {
duration: MaterialAnimations.opacity_duration;
easing: MaterialAnimations.opacity_easing;
}
}
clicked => {
root.toggle();
}
}
function toggle() {
root.checked = !root.checked;
}
changed checked => {
root.checked_state_changed(root.checked);
}
states [
disabled when !root.enabled : {
state_layer.display_background: false;
indicator.background: root.checked ? MaterialPalette.surface : MaterialPalette.outline.with_alpha(MaterialPalette.disable_opacity);
background_layer.background: (root.checked ? MaterialPalette.on_surface : MaterialPalette.surface_variant).with_alpha(MaterialPalette.state_layer_opacity_disabled);
background_layer.border_color: MaterialPalette.on_surface.with_alpha(MaterialPalette.state_layer_opacity_disabled);
foreground: root.checked ? MaterialPalette.on_surface.with_alpha(MaterialPalette.disable_opacity) : MaterialPalette.surface_container_highest;
}
pressed when touch_area.pressed: {
state_layer.background: indicator.background;
indicator.height: background_layer.height - 2px;
indicator.background: root.checked ? MaterialPalette.primary_container : MaterialPalette.on_surface_variant;
}
hover when touch_area.has_hover: {
state_layer.background: indicator.background;
}
]
}

View file

@ -0,0 +1,210 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { NavigationItem } from "../items/navigation_item.slint";
import { BaseNavigationItemTemplate, BaseNavigation } from "./base_navigation.slint";
import { StateLayerArea } from "./state_layer.slint";
import { MaterialText } from "./material_text.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { Icon } from "./icon.slint";
component TabItemTemplate inherits BaseNavigationItemTemplate {
out property <length> label_width: label.width;
property <bool> has_selected_icon: root.selected_icon.width > 0 || root.selected_icon.height > 0;
StateLayerArea {
color: MaterialPalette.primary;
VerticalLayout {
padding_top: MaterialStyleMetrics.padding_10;
padding_bottom: MaterialStyleMetrics.padding_8;
spacing: MaterialStyleMetrics.spacing_2;
if root.icon.width > 0 && root.icon.height > 0 : HorizontalLayout {
alignment: center;
Icon {
source: root.icon;
colorize: label.color;
states [
selected when root.selected && root.has_selected_icon : {
source: root.selected_icon;
}
]
}
}
HorizontalLayout {
alignment: center;
label := MaterialText {
text: root.text;
style: MaterialTypography.title_small;
color: MaterialPalette.on_surface_variant;
}
}
}
clicked => {
root.clicked();
}
}
states [
selected when root.selected : {
label.color: MaterialPalette.primary;
}
]
}
component SecondaryTabItemTemplate inherits BaseNavigationItemTemplate {
property <bool> has_selected_icon: root.selected_icon.width > 0 || root.selected_icon.height > 0;
StateLayerArea {
color: label.color;
HorizontalLayout {
alignment: center;
spacing: MaterialStyleMetrics.spacing_8;
if root.icon.width > 0 && root.icon.height > 0 : VerticalLayout {
alignment: center;
Icon {
source: root.icon;
colorize: label.color;
states [
selected when root.selected && root.has_selected_icon : {
source: root.selected_icon;
}
]
}
}
label := MaterialText {
text: root.text;
style: MaterialTypography.title_small;
color: MaterialPalette.on_surface_variant;
vertical_alignment: center;
}
}
clicked => {
root.clicked();
}
}
states [
selected when root.selected : {
label.color: MaterialPalette.on_surface;
}
]
}
export component TabBar inherits BaseNavigation {
property <length> indicator_width;
property <length> item_width: root.width / root.items.length;
property <length> current_x: root.item_width * self.current_index;
min_height: max(MaterialStyleMetrics.size_49, layout.min_height);
Rectangle {
background: MaterialPalette.surface;
layout := HorizontalLayout {
for item[index] in root.items : tab_item := TabItemTemplate {
icon: item.icon;
selected_icon: item.selected_icon;
text: item.text;
index: index;
selected: index == root.current_index;
show_badge: item.show_badge;
badge: item.badge;
clicked => {
root.select(index);
root.set_indicator_width(index, self.label_width);
}
Timer {
interval: 50ms;
triggered => {
root.set_indicator_width(index, tab_item.label_width);
self.running = false;
}
}
}
}
indicator := Rectangle {
x: root.current_x + (root.item_width - self.width) / 2;
y: parent.height - self.height;
width: root.indicator_width;
height: MaterialStyleMetrics.size_3;
border_top_left_radius: self.height / 2;
border_top_right_radius: self.border_top_left_radius;
background: MaterialPalette.primary;
animate x, width {
duration: MaterialAnimations.standard_accelerate_duration;
easing: MaterialAnimations.standard_easing;
}
}
}
function set_indicator_width(index: int, width: length) {
if index != root.current_index {
return;
}
root.indicator_width = width;
}
}
export component SecondaryTabBar inherits BaseNavigation {
property <length> item_width: root.width / root.items.length;
property <length> current_x: root.item_width * self.current_index;
min_height: max(MaterialStyleMetrics.size_49, layout.min_height);
Rectangle {
background: MaterialPalette.surface;
layout := HorizontalLayout {
for item[index] in root.items : SecondaryTabItemTemplate {
icon: item.icon;
selected_icon: item.selected_icon;
text: item.text;
index: index;
selected: index == root.current_index;
show_badge: item.show_badge;
badge: item.badge;
clicked => {
root.select(index);
}
}
}
indicator := Rectangle {
x: root.current_x;
y: parent.height - self.height;
width: root.item_width;
height: MaterialStyleMetrics.size_2;
background: MaterialPalette.primary;
animate x {
duration: MaterialAnimations.standard_accelerate_duration;
easing: MaterialAnimations.standard_easing;
}
}
}
}

View file

@ -0,0 +1,33 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component TextButton {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <bool> enabled <=> base.enabled;
in property <string> tooltip <=> base.tooltip;
in property <bool> inverse;
callback clicked <=> base.clicked;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-action-default => { base.clicked(); }
base := BaseButton {
border_radius: root.height / 2;
color: root.inverse ? MaterialPalette.inverse_primary : MaterialPalette.primary;
}
states [
disabled when !root.enabled : {
base.color: MaterialPalette.on_surface;
base.transparent_background: true;
}
]
}

View file

@ -0,0 +1,209 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialAnimations } from "../styling/material_animations.slint";
import { MaterialText } from "./material_text.slint";
import { IconButton } from "./icon_button.slint";
import { Icon } from "./icon.slint";
export component TextField {
in property <string> label;
in property <string> placeholder_text;
in property <string> supporting_text;
in property <bool> has_error;
in property <image> leading_icon;
in property <image> trailing_icon;
in property <bool> enabled: true;
in property <bool> read_only <=> input.read_only;
out property <bool> has_focus: input.has_focus;
in_out property <string> text <=> input.text;
callback leading_icon_clicked();
callback trailing_icon_clicked();
callback accepted(text: string);
callback edited(text: string);
callback key_pressed(event: KeyEvent) -> EventResult;
callback key_released(event: KeyEvent) -> EventResult;
property <bool> has_leading: root.leading_icon.width > 0 && root.leading_icon.height > 0;
property <bool> has_trailing: root.trailing_icon.width > 0 && root.trailing_icon.height > 0;
property <length> computed_x;
property <color> highlight: root.enabled && root.has_error ? MaterialPalette.error : root.has_focus ? MaterialPalette.primary : MaterialPalette.on_surface_variant;
forward_focus: input;
accessible-role: text-input;
accessible-enabled: root.enabled;
accessible-value <=> text;
accessible-placeholder-text: root.text == "" ? placeholder_text : "";
accessible-action-set-value(v) => { text = v; edited(v); }
layout := VerticalLayout {
spacing: MaterialStyleMetrics.spacing_4;
background_layer := Rectangle {
border_top_left_radius: MaterialStyleMetrics.border_radius_4;
border_top_right_radius: MaterialStyleMetrics.border_radius_4;
background: MaterialPalette.surface_container_highest;
min_height: max(MaterialStyleMetrics.size_56, inner_layout.min_height);
inner_layout := HorizontalLayout {
padding_left: root.has_leading ? MaterialStyleMetrics.padding_4 : MaterialStyleMetrics.padding_16;
padding_right: root.has_trailing ? MaterialStyleMetrics.padding_4 : MaterialStyleMetrics.padding_16;
padding_top: MaterialStyleMetrics.padding_4;
padding_bottom: self.padding_top;
spacing: MaterialStyleMetrics.spacing_2;
if root.has_leading : IconButton {
icon: root.leading_icon;
enabled: root.enabled;
clicked => {
root.leading_icon_clicked();
}
}
HorizontalLayout {
padding_top: MaterialStyleMetrics.padding_4;
padding_bottom: self.padding_top;
Rectangle {
horizontal_stretch: 1;
clip: true;
min_height: input.min_height;
input := TextInput {
x: min(0px, max(parent.width - self.width - self.text_cursor_width, root.computed_x));
width: max(parent.width - self.text-cursor-width, self.preferred-width);
font_size: MaterialTypography.body_large.font_size;
font_weight: MaterialTypography.body_large.font_weight;
text_cursor_width: MaterialStyleMetrics.size_3;
vertical_alignment: bottom;
single_line: true;
color: MaterialPalette.on_surface;
selection_background_color: MaterialPalette.primary.with_alpha(0.5);
selection_foreground_color: self.color;
enabled: root.enabled;
// Disable TextInput's built-in accessibility support as the component takes care of that.
accessible-role: none;
cursor_position_changed(cursor_position) => {
if cursor_position.x + root.computed_x < 0 {
root.computed_x = - cursor_position.x;
} else if cursor-position.x + root.computed_x > parent.width - self.text-cursor-width {
root.computed_x = parent.width - cursor_position.x - self.text-cursor-width;
}
}
accepted => {
root.accepted(self.text);
}
edited => {
root.edited(self.text);
}
key_pressed(event) => {
root.key_pressed(event)
}
key_released(event) => {
root.key_released(event)
}
}
text_label := MaterialText {
y: (parent.height - self.height) / 2;
width: 100%;
text: root.label;
color: root.highlight;
style: MaterialTypography.body_large;
states [
on_top when root.text != "" || input.has_focus : {
y: 0;
style: MaterialTypography.body_small;
}
]
animate font_size, y {
duration: MaterialAnimations.standard_fast_duration;
easing: MaterialAnimations.standard_easing;
}
}
}
}
if root.trailing_icon.width > 0 && root.trailing_icon.height > 0 : IconButton {
icon: root.trailing_icon;
enabled: root.enabled;
has_error: root.has_error;
clicked => {
root.trailing_icon_clicked();
}
}
}
active_indicator := Rectangle {
y: parent.height - self.height;
height: MaterialStyleMetrics.size_1;
background: root.highlight;
animate height {
duration: MaterialAnimations.standard_fast_duration;
easing: MaterialAnimations.standard_easing;
}
}
}
if root.supporting_text != "" : HorizontalLayout {
padding_left: MaterialStyleMetrics.padding_16;
padding_right: self.padding_left;
supporting_text := MaterialText {
text: root.supporting_text;
style: MaterialTypography.body_small;
color: root.highlight;
opacity: root.enabled ? 1 : MaterialPalette.disable_opacity;
}
}
}
public function set_selection_offsets(start: int, end: int) {
input.set_selection_offsets(start, end);
}
public function select_all() {
input.select_all();
}
public function clear_selection() {
input.clear_selection();
}
public function cut() {
input.cut();
}
public function copy() {
input.copy();
}
public function paste() {
input.paste();
}
states [
disabled when !root.enabled : {
background_layer.background: MaterialPalette.surface_container_highest.with_alpha(MaterialPalette.disable_opacity);
text_label.opacity: MaterialPalette.disable_opacity;
input.opacity: MaterialPalette.disable_opacity;
}
focused when input.has_focus : {
active_indicator.height: MaterialStyleMetrics.size_3;
}
]
}

View file

@ -0,0 +1,674 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { BaseDialog } from "./dialog.slint";
import { Icons } from "../icons/icons.slint";
import { MaterialText } from "./material_text.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { StateLayerArea } from "state_layer.slint";
component TimeSelector inherits Rectangle {
in property <bool> selected;
in property <int> value;
callback clicked <=> touch_area.clicked;
width: max(MaterialStyleMetrics.size_48, text_label.min_width);
height: max(MaterialStyleMetrics.size_48, text_label.min_height);
border_radius: max(root.width, root.height) / 2;
vertical_stretch: 0;
horizontal_stretch: 0;
touch_area := TouchArea {
text_label := MaterialText {
text: root.value;
vertical_alignment: center;
horizontal_alignment: center;
style: MaterialTypography.body_large;
color: MaterialPalette.on_surface;
}
}
states [
selected when root.selected: {
text_label.color: MaterialPalette.on_primary;
}
]
}
component Clock {
in property <[int]> model;
in property <bool> two_columns;
in property <int> total;
in_out property <int> current_index;
in property <int> current_value;
callback current_index_changed(index: int);
property <length> radius: max(root.width, root.height) / 2;
property <length> picker_ditameter: MaterialStyleMetrics.size_48;
property <length> center: root.radius - root.picker_ditameter / 2;
property <length> outer_padding: 2px;
property <length> inner_padding: 32px;
property <length> radius_outer: root.center - root.outer_padding;
property <length> radius_inner: root.center - root.inner_padding;
property <int> half_total: root.total / 2;
property <angle> rotation: 0.25turn;
property <length> current_x: get_index_x(root.current_value);
property <length> current_y: get_index_y(root.current_value);
min_width: MaterialStyleMetrics.size_256;
min_height: self.min_width;
vertical_stretch: 0;
horizontal_stretch: 0;
background_layer := Rectangle {
border_radius: max(self.width, self.height) / 2;
background: MaterialPalette.surface_container_highest;
if root.current_index >= 0 || root.current_index < root.model.length: Path {
stroke: MaterialPalette.primary;
stroke_width: 2px;
viewbox_width: self.width / 1px;
viewbox_height: self.height / 1px;
MoveTo {
x: root.width / 2px;
y: root.height / 2px;
}
LineTo {
x: (root.current_x + root.picker_ditameter / 2) / 1px;
y: (root.current_y + root.picker_ditameter / 2) / 1px;
}
}
Rectangle {
width: MaterialStyleMetrics.size_8;
height: self.width;
background: MaterialPalette.primary;
border_radius: self.width / 2;
}
if root.current_index < root.model.length: Rectangle {
x: root.current_x;
y: root.current_y;
width: root.picker_ditameter;
height: root.picker_ditameter;
border_radius: root.picker_ditameter / 2;
background: MaterialPalette.primary;
if root.current_index < 0: Rectangle {
width: MaterialStyleMetrics.size_4;
height: self.width;
border_radius: self.width / 2;
background: MaterialPalette.on_primary;
}
}
for val[index] in root.model: TimeSelector {
x: get_index_x(val);
y: get_index_y(val);
width: root.picker_ditameter;
height: root.picker_ditameter;
value: val;
selected: index == root.current_index;
accessible_role: button;
accessible_label: @tr("{} Hours or minutes of {}", val, root.total);
accessible_action_default => {
self.clicked();
}
clicked => {
root.set_current_index(index);
}
}
}
pure function value_to_angle(value: int) -> angle {
if root.two_columns {
if value >= root.half_total {
return clamp((value - root.half_total) / root.half_total * 1turn, 0, 0.999999turn) - root.rotation;
}
return clamp(value / root.half_total * 1turn, 0, 0.99999turn) - root.rotation;
}
clamp(value / root.total * 1turn, 0, 0.99999turn) - root.rotation
}
pure function get_index_x(value: int) -> length {
if root.two_columns && value >= root.half_total {
return root.center + (root.radius_inner / 1px * cos(root.value_to_angle(value))) * 1px;
}
root.center + (root.radius_outer / 1px * cos(root.value_to_angle(value))) * 1px
}
pure function get_index_y(value: int) -> length {
// this is only for 24 mode
if root.total == 24 && value == 0 {
return root.center + (root.radius_inner / 1px * sin(root.value_to_angle(value))) * 1px;
}
if root.total == 24 && value == 12 {
return root.center + (root.radius_outer / 1px * sin(root.value_to_angle(value))) * 1px;
}
if root.two_columns && value >= root.half_total {
return root.center + (root.radius_inner / 1px * sin(root.value_to_angle(value))) * 1px;
}
root.center + (root.radius_outer / 1px * sin(root.value_to_angle(value))) * 1px
}
function set_current_index(index: int) {
root.current_index_changed(index);
}
}
component TimePickerInput {
in property <bool> read_only <=> text_input.read_only;
in property <bool> checked;
in_out property <string> text <=> text_input.text;
callback clicked;
callback edited(value: int);
width: MaterialStyleMetrics.size_90;
min_height: max(MaterialStyleMetrics.size_80, layout.min_height);
vertical_stretch: 0;
horizontal_stretch: 0;
forward_focus: text_input;
background_layer := Rectangle {
border_radius: MaterialStyleMetrics.border_radius_8;
background: MaterialPalette.surface_container_highest;
clip: true;
layout := HorizontalLayout {
padding: MaterialStyleMetrics.padding_8;
text_input := TextInput {
vertical_alignment: center;
horizontal_alignment: center;
color: MaterialPalette.on_surface;
font_size: MaterialTypography.display_large.font_size;
font_weight: MaterialTypography.display_large.font_weight;
input_type: number;
visible: !root.read_only;
edited => {
root.edited(self.text.to_float());
}
}
}
if root.read_only : StateLayerArea {
border_radius: background_layer.border_radius;
color: MaterialPalette.on_surface;
HorizontalLayout {
padding: MaterialStyleMetrics.padding_8;
MaterialText {
text: root.text.to_float() >= 10 ? root.text : "0" + root.text;
style: MaterialTypography.display_large;
color: MaterialPalette.on_surface;
horizontal_alignment: center;
vertical_alignment: center;
}
}
clicked => {
root.clicked();
}
}
}
states [
has_focus when text_input.has_focus && !root.read_only: {
background_layer.background: MaterialPalette.primary_container;
background_layer.border_width: 2px;
background_layer.border_color: MaterialPalette.primary;
text_input.color: MaterialPalette.on_primary_container;
}
checked when root.checked: {
background_layer.background: MaterialPalette.primary_container;
text_input.color: MaterialPalette.on_primary_container;
}
]
}
component PeriodSelectorItem {
in property <string> text <=> label.text;
in property <bool> checked;
callback clicked <=> state_layer.clicked;
background_layer := Rectangle {
state_layer := StateLayerArea {
color: label.color;
label := MaterialText {
color: MaterialPalette.on_surface_variant;
horizontal_alignment: center;
}
}
}
states [
checked when root.checked: {
background_layer.background: MaterialPalette.tertiary_container;
label.color: MaterialPalette.on_tertiary_container;
}
]
}
component PeriodSelector {
in property <bool> am_selected;
in property <bool> horizontal;
callback update_period(am_selected: bool);
min_width: max(MaterialStyleMetrics.size_52, layout.min_width);
min_height: max(root.horizontal ? MaterialStyleMetrics.size_38 : MaterialStyleMetrics.size_80, layout.min_height);
accessible_label: "AM or PM";
accessible_role: checkbox;
accessible_checked: root.am_selected;
Rectangle {
border_radius: border.border_radius;
clip: true;
layout := VerticalLayout {
if root.horizontal : HorizontalLayout {
PeriodSelectorItem {
text: "AM";
checked: root.am_selected;
clicked => {
if root.am_selected {
return;
}
root.update_period(true);
}
}
Rectangle {
width: 1px;
background: border.border_color;
vertical_stretch: 0;
}
PeriodSelectorItem {
text: "PM";
checked: !root.am_selected;
clicked => {
if !root.am_selected {
return;
}
root.update_period(false);
}
}
}
if !root.horizontal : VerticalLayout {
PeriodSelectorItem {
text: "AM";
checked: root.am_selected;
clicked => {
if root.am_selected {
return;
}
root.update_period(true);
}
}
Rectangle {
height: 1px;
background: border.border_color;
vertical_stretch: 0;
}
PeriodSelectorItem {
text: "PM";
checked: !root.am_selected;
clicked => {
if !root.am_selected {
return;
}
root.update_period(false);
}
}
}
}
}
border := Rectangle {
border_radius: MaterialStyleMetrics.border_radius_8;
border_width: 1px;
border_color: MaterialPalette.outline;
}
}
export struct Time {
hour: int,
minute: int,
second: int
}
export component TimePickerPopup inherits PopupWindow {
in property <bool> use_24_hour_format: true;
in property <string> title: @tr("Select time");
in property <string> cancel_text: @tr("Cancel");
in property <string> ok_text: @tr("Ok");
in property <Time> time: { hour: 12 };
in property <string> hour_label: @tr("Hour");
in property <string> minute_label: @tr("Minute");
property <bool> keyboard_mode;
property <bool> minutes_selected;
property <bool> am_selected: true;
property <[int]> twelfth_hour_model: [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
property <[int]> use-24_hour_format_model: [
12,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
0,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23
];
property <[int]> minute_model: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
property <[int]> current_model: root.get_current_model();
property <int> current_index: root.minutes_selected ? root.index_of_minute(root.current_time.minute) : root.index_of_hour(root.current_time.hour);
property <Time> current_time: root.time;
property <int> time_picker_hour: hour_time_picker.text.to_float();
property <int> time_picker_minute: minute_time_picker.text.to_float();
property <bool> horizontal: root.width >= MaterialStyleMetrics.size_572;
callback ok();
close_policy: no_auto_close;
forward_focus: base;
base := BaseDialog {
width: 100%;
height: 100%;
title: root.title;
action_button_icons: [
root.keyboard_mode ? Icons.schedule : Icons.keyboard
];
actions: [
root.cancel_text,
root.ok_text
];
layout := HorizontalLayout {
spacing: MaterialStyleMetrics.spacing_52;
VerticalLayout {
alignment: center;
spacing: MaterialStyleMetrics.spacing_8;
HorizontalLayout {
alignment: center;
hour_time_picker := TimePickerInput {
read_only: !root.keyboard_mode;
checked: self.read_only && !root.minutes_selected;
text: root.current_time.hour;
accessible_role: AccessibleRole.text_input;
accessible_value: self.text;
accessible_label: "hour";
clicked => {
root.minutes_selected = false;
}
}
separator := MaterialText {
min_width: MaterialStyleMetrics.size_24;
text: ":";
color: MaterialPalette.on_surface;
style: MaterialTypography.display_large;
vertical_alignment: center;
horizontal_alignment: center;
}
minute_time_picker := TimePickerInput {
read_only: !root.keyboard_mode;
checked: self.read_only && root.minutes_selected;
text: root.current_time.minute;
accessible_role: AccessibleRole.text_input;
accessible_value: self.text;
accessible_label: "minute";
clicked => {
root.minutes_selected = true;
}
}
// spacer
if !root.use_24_hour_format && (!root.horizontal || root.keyboard_mode) : Rectangle {
width: MaterialStyleMetrics.spacing_12;
}
if !root.use_24_hour_format && (!root.horizontal || root.keyboard_mode) : PeriodSelector {
am_selected: root.am_selected;
update-period(am) => {
root.am-selected = am;
}
}
}
// labels
if root.keyboard_mode : HorizontalLayout {
alignment: start;
MaterialText {
width: hour_time_picker.width;
text: root.hour_label;
color: MaterialPalette.on_surface_variant;
}
Rectangle {
width: MaterialStyleMetrics.size_24;
}
MaterialText {
text: root.minute_label;
color: MaterialPalette.on_surface_variant;
}
}
if !root.use_24_hour_format && !root.keyboard_mode && root.horizontal : PeriodSelector {
am_selected: root.am_selected;
horizontal: true;
update-period(am) => {
root.am-selected = am;
}
}
}
if root.horizontal && !root.keyboard_mode : Clock {
model: root.current_model;
two_columns: !root.minutes_selected && root.use-24_hour_format;
current_index: root.current_index;
total: root.minutes_selected ? 60 : root.use-24_hour_format ? 24 : 12;
current_value: root.minutes_selected ? root.current_time.minute : root.current_time.hour;
current_index_changed(index) => {
root.update_time_by_item(index);
if !root.minutes_selected {
root.minutes_selected = true;
}
}
}
}
// spacer
if !root.horizontal && !root.keyboard_mode : Rectangle {
height: MaterialStyleMetrics.spacing_12;
}
if !root.horizontal && !root.keyboard_mode : Clock {
model: root.current_model;
two_columns: !root.minutes_selected && root.use-24_hour_format;
current_index: root.current_index;
total: root.minutes_selected ? 60 : root.use-24_hour_format ? 24 : 12;
current_value: root.minutes_selected ? root.current_time.minute : root.current_time.hour;
height: self.width;
current_index_changed(index) => {
root.update_time_by_item(index);
if !root.minutes_selected {
root.minutes_selected = true;
}
}
}
action_button_clicked(index) => {
if index == 0 {
root.keyboard_mode = !root.keyboard_mode;
}
}
action_clicked(index) => {
// cancel
if index == 0 {
root.close();
} else if index == 1 {
root.ok();
}
}
close => {
root.close();
}
}
pure public function ok_enabled() -> bool {
if !root.keyboard_mode {
return root.current_time.hour <= 23 && root.current_time.minute <= 59;
}
if root.use-24_hour_format {
return root.time_picker_hour <= 23 && root.time_picker_minute <= 59;
}
root.time_picker_hour <= 12 && root.time_picker_minute <= 59
}
public function get_current_time() -> Time {
if root.keyboard_mode {
if !root.use-24_hour_format && !root.am_selected {
if root.current_time.hour == 12 {
return { hour: 0, minute: root.current_time.minute };
}
return { hour: root.current_time.hour + 12, minute: root.current_time.minute };
}
return root.current_time;
}
return { hour: root.time_picker_hour, minute: root.time_picker_minute };
}
changed keyboard_mode => {
if root.keyboard_mode {
return;
}
root.update_time(min(root.time_picker_hour, root.use-24_hour_format ? 23 : 12), min(root.time_picker_minute, 59));
}
changed time => {
root.current_time = root.time;
}
function update_time_by_item(index: int) {
root.update_time_by_value(root.current_model[index]);
}
function update_time_by_value(value: int) {
if root.minutes_selected {
root.update_time(root.current_time.hour, value);
return;
}
root.update_time(value, root.current_time.minute);
}
function update_time(hour: int, minute: int) {
root.current_time = { hour: hour, minute: minute };
hour_time_picker.text = root.current_time.hour;
minute_time_picker.text = minute;
}
pure function index_of_minute(minute: int) -> int {
if mod(minute, 5) != 0 {
return -1;
}
minute / 5
}
pure function index_of_hour(hour: int) -> int {
if hour == 12 {
return 0;
}
if hour == 0 {
return 12;
}
if !root.use-24_hour_format && hour > 12 {
return hour - 12;
}
hour
}
pure function get_current_model() -> [int] {
if root.minutes_selected {
return root.minute_model;
}
if root.use-24_hour_format {
return root.use-24_hour_format_model;
}
root.twelfth_hour_model
}
}

View file

@ -0,0 +1,44 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
export component TonalButton {
in property <image> icon <=> base.icon;
in property <string> text <=> base.text;
in property <bool> enabled <=> base.enabled;
in property <string> tooltip <=> base.tooltip;
callback clicked <=> base.clicked;
accessible-role: button;
accessible-enabled: root.enabled;
accessible-label: root.text == "" ? root.tooltip : root.text;
accessible-action-default => { base.clicked(); }
elevation := Elevation {
border_radius: root.height / 2;
background: MaterialPalette.secondary_container;
width: 100%;
height: 100%;
}
base := BaseButton {
border_radius: elevation.border_radius;
color: MaterialPalette.on_secondary_container;
}
states [
disabled when !root.enabled : {
elevation.background: transparent;
elevation.level: 0;
base.color: MaterialPalette.on_surface;
}
hover when base.has_hover && !base.pressed && !base.enter_pressed : {
elevation.level: 2;
}
]
}

View file

@ -0,0 +1,63 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Elevation } from "./elevation.slint";
import { BaseButton } from "./base_button.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component TonalIconButton {
in property <image> icon;
in property <image> checked_icon: root.icon;
in property <bool> checkable;
in_out property <bool> checked;
in property <string> tooltip <=> base.tooltip;
in property <bool> enabled <=> base.enabled;
callback clicked();
accessible-role: button;
accessible-enabled: true;
accessible-label: root.tooltip;
accessible-checkable: root.checkable;
accessible-checked: root.checked;
accessible-action-default => { base.clicked(); }
background_layer := Rectangle {
width: 100%;
height: 100%;
border_radius: base.border_radius;
background: root.checkable ? MaterialPalette.surface_container_highest : MaterialPalette.secondary_container;
visible: root.enabled;
}
base := BaseButton {
icon: root.checked ? root.checked_icon : root.icon;
border_radius: self.height / 2;
color: !root.enabled ? MaterialPalette.on_surface_variant : MaterialPalette.on_secondary_container;
button_horizontal_padding: 0;
button_vertical_padding: 0;
width: self.height;
icon_size: MaterialStyleMetrics.icon_size_24;
clicked => {
root.toggle();
root.clicked();
}
}
function toggle() {
if !root.checkable {
return;
}
root.checked = !root.checked;
}
states [
checked when root.enabled && root.checked : {
background_layer.background: MaterialPalette.secondary_container;
}
]
}

View file

@ -0,0 +1,33 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialText } from "./material_text.slint";
import { MaterialPalette } from "../styling/material_palette.slint";
import { MaterialTypography } from "../styling/material_typography.slint";
export component ToolTip {
in property <string> text;
z: 10000;
background_layer := Rectangle {
border_radius: 4px;
background: MaterialPalette.inverse_surface;
layout := HorizontalLayout {
max_width: 200px;
padding_top: 4px;
padding_bottom: 4px;
padding_left: 8px;
padding_right: 8px;
MaterialText {
text: root.text;
vertical_alignment: center;
color: MaterialPalette.inverse_on_surface;
style: MaterialTypography.body_small;
wrap: word-wrap;
}
}
}
}

View file

@ -0,0 +1,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { MaterialStyleMetrics } from "../styling/material_style_metrics.slint";
export component Vertical inherits VerticalLayout {
padding: MaterialStyleMetrics.padding_8;
spacing: MaterialStyleMetrics.spacing_8;
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>account-box</title><path d="M6,17C6,15 10,13.9 12,13.9C14,13.9 18,15 18,17V18H6M15,9A3,3 0 0,1 12,12A3,3 0 0,1 9,9A3,3 0 0,1 12,6A3,3 0 0,1 15,9M3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3H5C3.89,3 3,3.9 3,5Z" /></svg>

After

Width:  |  Height:  |  Size: 299 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>

After

Width:  |  Height:  |  Size: 162 B

View file

@ -0,0 +1,2 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 10l5 5l5-5H7z"/></svg>

After

Width:  |  Height:  |  Size: 119 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7 14l5-5l5 5H7z"/></svg>

After

Width:  |  Height:  |  Size: 118 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 17l5-5l-5-5v10z"/></svg>

After

Width:  |  Height:  |  Size: 121 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M200-80q-33 0-56.5-23.5T120-160v-560q0-33 23.5-56.5T200-800h40v-80h80v80h320v-80h80v80h40q33 0 56.5 23.5T840-720v560q0 33-23.5 56.5T760-80H200Zm0-80h560v-400H200v400Zm0-480h560v-80H200v80Zm0 0v-80 80Z"/></svg>

After

Width:  |  Height:  |  Size: 325 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M560-240 320-480l240-240 56 56-184 184 184 184-56 56Z"/></svg>

After

Width:  |  Height:  |  Size: 178 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M504-480 320-664l56-56 240 240-240 240-56-56 184-184Z"/></svg>

After

Width:  |  Height:  |  Size: 178 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

After

Width:  |  Height:  |  Size: 203 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>cog</title><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" /></svg>

After

Width:  |  Height:  |  Size: 1,004 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M200-200h57l391-391-57-57-391 391v57Zm-80 80v-170l528-527q12-11 26.5-17t30.5-6q16 0 31 6t26 18l55 56q12 11 17.5 26t5.5 30q0 16-5.5 30.5T817-647L290-120H120Zm640-584-56-56 56 56Zm-141 85-28-29 57 57-29-28Z"/></svg>

After

Width:  |  Height:  |  Size: 329 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>home</title><path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" /></svg>

After

Width:  |  Height:  |  Size: 141 B

View file

@ -0,0 +1,27 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// Copyright © Zervó Zadachin <contact@zervo.org>
// SPDX-License-Identifier: MIT
export global Icons {
out property <image> calendar_today: @image-url("calendar_today.svg");
out property <image> check: @image-url("check.svg");
out property <image> close: @image-url("close.svg");
out property <image> chevron_backward: @image-url("chevron_backward.svg");
out property <image> chevron_forward: @image-url("chevron_forward.svg");
out property <image> edit: @image-url("edit.svg");
out property <image> menu: @image-url("menu.svg");
out property <image> arrow_back: @image-url("arrow_back.svg");
out property <image> arrow_right: @image-url("arrow_right.svg");
out property <image> arrow_drop_down: @image-url("arrow_drop_down.svg");
out property <image> arrow_drop_up: @image-url("arrow_drop_up.svg");
out property <image> remove: @image-url("remove.svg");
out property <image> schedule: @image-url("schedule.svg");
out property <image> keyboard: @image-url("keyboard.svg");
// extended:
out property <image> home: @image-url("home.svg");
out property <image> list_box: @image-url("list_box.svg");
out property <image> shape: @image-url("shape.svg");
out property <image> account_box: @image-url("account_box.svg");
out property <image> cog: @image-url("cog.svg");
out property <image> package_variant: @image-url("package_variant.svg");
}

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e3e3e3"><path d="M0 0h24v24H0V0zm0 0h24v24H0V0z" fill="none"/><path d="M20 7v10H4V7h16m0-2H4c-1.1 0-1.99.9-1.99 2L2 17c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2zm0 3h2v2h-2zM8 8h2v2H8zm0 3h2v2H8zm-3 0h2v2H5zm0-3h2v2H5zm3 6h8v2H8zm6-3h2v2h-2zm0-3h2v2h-2zm3 3h2v2h-2zm0-3h2v2h-2z"/></svg>

After

Width:  |  Height:  |  Size: 401 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>list-box</title><path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3M7 7H9V9H7V7M7 11H9V13H7V11M7 15H9V17H7V15M17 17H11V15H17V17M17 13H11V11H17V13M17 9H11V7H17V9Z" /></svg>

After

Width:  |  Height:  |  Size: 287 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>

After

Width:  |  Height:  |  Size: 146 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>package-variant</title><path d="M2,10.96C1.5,10.68 1.35,10.07 1.63,9.59L3.13,7C3.24,6.8 3.41,6.66 3.6,6.58L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.66,6.72 20.82,6.88 20.91,7.08L22.36,9.6C22.64,10.08 22.47,10.69 22,10.96L21,11.54V16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V10.96C2.7,11.13 2.32,11.14 2,10.96M12,4.15V4.15L12,10.85V10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V12.69L14,15.59C13.67,15.77 13.3,15.76 13,15.6V19.29L19,15.91M13.85,13.36L20.13,9.73L19.55,8.72L13.27,12.35L13.85,13.36Z" /></svg>

After

Width:  |  Height:  |  Size: 737 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e3e3e3"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13H5v-2h14v2z"/></svg>

After

Width:  |  Height:  |  Size: 177 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e3e3e3"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>

After

Width:  |  Height:  |  Size: 342 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>shape</title><path d="M11,13.5V21.5H3V13.5H11M12,2L17.5,11H6.5L12,2M17.5,13C20,13 22,15 22,17.5C22,20 20,22 17.5,22C15,22 13,20 13,17.5C13,15 15,13 17.5,13Z" /></svg>

After

Width:  |  Height:  |  Size: 233 B

View file

@ -0,0 +1,12 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export struct ListItem {
text: string,
supporting_text: string,
avatar_icon: image,
avatar_text: string,
avatar_background: color,
avatar_foreground: color,
action_button_icon: image
}

View file

@ -0,0 +1,9 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export struct MenuItem {
icon: image,
text: string,
trailing_text: string,
enabled: bool
}

View file

@ -0,0 +1,15 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export struct NavigationItem {
icon: image,
selected_icon: image,
text: string,
show_badge: bool,
badge: string
}
export struct NavigationGroup {
title: string,
items: [NavigationItem]
}

View file

@ -0,0 +1,17 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export global MaterialAnimations {
out property <easing> emphasized_easing: cubic-bezier(0.05, 0.7, 0.1, 1.0);
out property <duration> emphasized_duration: 500ms;
out property <duration> emphasized_decelerate_duration: 400ms;
out property <duration> emphasized_accelerate_duration: 200ms;
out property <easing> standard_easing: cubic-bezier(0, 0, 0, 1);
out property <duration> standard_decelerate_duration: 250ms;
out property <duration> standard_accelerate_duration: 200ms;
out property <duration> standard_fast_duration: 150ms;
out property <easing> ripple_easing: ease_out;
out property <duration> ripple_duration: 2s;
out property <easing> opacity_easing: ease;
out property <duration> opacity_duration: 250ms;
}

View file

@ -0,0 +1,173 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
import { Palette } from "std-widgets.slint";
import { MaterialSchemes, MaterialScheme } from "material_schemes.slint";
export global MaterialPalette {
out property <color> primary: root.scheme.primary;
out property <color> surface_tint: root.scheme.surfaceTint;
out property <color> on_primary: root.scheme.onPrimary;
out property <color> primary_container: root.scheme.primaryContainer;
out property <color> on_primary_container: root.scheme.onPrimaryContainer;
out property <color> secondary: root.scheme.secondary;
out property <color> on_secondary: root.scheme.onSecondary;
out property <color> secondary_container: root.scheme.secondaryContainer;
out property <color> on_secondary_container: root.scheme.onSecondaryContainer;
out property <color> tertiary: root.scheme.tertiary;
out property <color> on_tertiary: root.scheme.onTertiary;
out property <color> tertiary_container: root.scheme.tertiaryContainer;
out property <color> on_tertiary_container: root.scheme.onTertiaryContainer;
out property <color> error: root.scheme.error;
out property <color> on_error: root.scheme.onError;
out property <color> error_container: root.scheme.errorContainer;
out property <color> on_error_container: root.scheme.onErrorContainer;
out property <color> background: root.scheme.background;
out property <color> on_background: root.scheme.onBackground;
out property <color> surface: root.scheme.surface;
out property <color> on_surface: root.scheme.onSurface;
out property <color> surface_variant: root.scheme.surfaceVariant;
out property <color> on_surface_variant: root.scheme.onSurfaceVariant;
out property <color> outline: root.scheme.outline;
out property <color> outline_variant: root.scheme.outlineVariant;
out property <color> shadow: root.scheme.shadow;
out property <color> scrim: root.scheme.scrim;
out property <color> inverse_surface: root.scheme.inverseSurface;
out property <color> inverse_on_surface: root.scheme.inverseOnSurface;
out property <color> inverse_primary: root.scheme.inversePrimary;
out property <color> primary_fixed: root.scheme.primaryFixed;
out property <color> on_primary_fixed: root.scheme.onPrimaryFixed;
out property <color> primary_fixed_dim: root.scheme.primaryFixedDim;
out property <color> on_primary_fixed_variant: root.scheme.onPrimaryFixedVariant;
out property <color> secondary_fixed: root.scheme.secondaryFixed;
out property <color> on_secondary_fixed: root.scheme.onSecondaryFixed;
out property <color> secondary_fixed_dim: root.scheme.secondaryFixedDim;
out property <color> on_secondary_fixed_variant: root.scheme.onSecondaryFixedVariant;
out property <color> tertiary_fixed: root.scheme.tertiaryFixed;
out property <color> on_tertiary_fixed: root.scheme.onTertiaryFixed;
out property <color> tertiary_fixed_dim: root.scheme.tertiaryFixedDim;
out property <color> on_tertiary_fixed_variant: root.scheme.onTertiaryFixedVariant;
out property <color> surface_dim: root.scheme.surfaceDim;
out property <color> surface_bright: root.scheme.surfaceBright;
out property <color> surface_container_lowest: root.scheme.surfaceContainerLowest;
out property <color> surface_container_low: root.scheme.surfaceContainerLow;
out property <color> surface_container: root.scheme.surfaceContainer;
out property <color> surface_container_high: root.scheme.surfaceContainerHigh;
out property <color> surface_container_highest: root.scheme.surfaceContainerHighest;
out property <color> shadow_15: #000000.with_alpha(15%);
out property <color> shadow_30: #000000.with_alpha(30%);
out property <float> state_layer_opacity_hover: 8%;
out property <float> state_layer_opacity_focus: 10%;
out property <float> state_layer_opacity_press: 10%;
out property <float> state_layer_opacity_disabled: 12%;
out property <float> state_layer_opacity_drag: 16%;
out property <float> disable_opacity: 38%;
out property <color> background_modal: #000000.with_alpha(50%);
in property <MaterialSchemes> schemes: {
light: {
primary: rgb(68, 94, 145),
surfaceTint: rgb(68, 94, 145),
onPrimary: rgb(255, 255, 255),
primaryContainer: rgb(216, 226, 255),
onPrimaryContainer: rgb(0, 26, 66),
secondary: rgb(87, 94, 113),
onSecondary: rgb(255, 255, 255),
secondaryContainer: rgb(219, 226, 249),
onSecondaryContainer: rgb(20, 27, 44),
tertiary: rgb(113, 85, 115),
onTertiary: rgb(255, 255, 255),
tertiaryContainer: rgb(252, 215, 251),
onTertiaryContainer: rgb(41, 19, 45),
error: rgb(186, 26, 26),
onError: rgb(255, 255, 255),
errorContainer: rgb(255, 218, 214),
onErrorContainer: rgb(65, 0, 2),
background: rgb(249, 249, 255),
onBackground: rgb(26, 27, 32),
surface: rgb(249, 249, 255),
onSurface: rgb(26, 27, 32),
surfaceVariant: rgb(225, 226, 236),
onSurfaceVariant: rgb(68, 71, 79),
outline: rgb(117, 119, 127),
outlineVariant: rgb(196, 198, 208),
shadow: rgb(0, 0, 0),
scrim: rgb(0, 0, 0),
inverseSurface: rgb(47, 48, 54),
inverseOnSurface: rgb(240, 240, 247),
inversePrimary: rgb(173, 198, 255),
primaryFixed: rgb(216, 226, 255),
onPrimaryFixed: rgb(0, 26, 66),
primaryFixedDim: rgb(173, 198, 255),
onPrimaryFixedVariant: rgb(43, 70, 120),
secondaryFixed: rgb(219, 226, 249),
onSecondaryFixed: rgb(20, 27, 44),
secondaryFixedDim: rgb(191, 198, 220),
onSecondaryFixedVariant: rgb(63, 71, 89),
tertiaryFixed: rgb(252, 215, 251),
onTertiaryFixed: rgb(41, 19, 45),
tertiaryFixedDim: rgb(222, 188, 223),
onTertiaryFixedVariant: rgb(88, 62, 91),
surfaceDim: rgb(217, 217, 224),
surfaceBright: rgb(246, 246, 255),
surfaceContainerLowest: rgb(255, 255, 255),
surfaceContainerLow: rgb(243, 243, 250),
surfaceContainer: rgb(238, 237, 244),
surfaceContainerHigh: rgb(232, 231, 239),
surfaceContainerHighest: rgb(226, 226, 233),
},
dark: {
primary: rgb(173, 198, 255),
surfaceTint: rgb(173, 198, 255),
onPrimary: rgb(17, 47, 96),
primaryContainer: rgb(43, 70, 120),
onPrimaryContainer: rgb(216, 226, 255),
secondary: rgb(191, 198, 220),
onSecondary: rgb(41, 48, 65),
secondaryContainer: rgb(63, 71, 89),
onSecondaryContainer: rgb(219, 226, 249),
tertiary: rgb(222, 188, 223),
onTertiary: rgb(64, 40, 67),
tertiaryContainer: rgb(88, 62, 91),
onTertiaryContainer: rgb(252, 215, 251),
error: rgb(255, 180, 171),
onError: rgb(105, 0, 5),
errorContainer: rgb(147, 0, 10),
onErrorContainer: rgb(255, 218, 214),
background: rgb(17, 19, 24),
onBackground: rgb(226, 226, 233),
surface: rgb(17, 19, 24),
onSurface: rgb(226, 226, 233),
surfaceVariant: rgb(68, 71, 79),
onSurfaceVariant: rgb(196, 198, 208),
outline: rgb(142, 144, 153),
outlineVariant: rgb(68, 71, 79),
shadow: rgb(0, 0, 0),
scrim: rgb(0, 0, 0),
inverseSurface: rgb(226, 226, 233),
inverseOnSurface: rgb(47, 48, 54),
inversePrimary: rgb(68, 94, 145),
primaryFixed: rgb(216, 226, 255),
onPrimaryFixed: rgb(0, 26, 66),
primaryFixedDim: rgb(173, 198, 255),
onPrimaryFixedVariant: rgb(43, 70, 120),
secondaryFixed: rgb(219, 226, 249),
onSecondaryFixed: rgb(20, 27, 44),
secondaryFixedDim: rgb(191, 198, 220),
onSecondaryFixedVariant: rgb(63, 71, 89),
tertiaryFixed: rgb(252, 215, 251),
onTertiaryFixed: rgb(41, 19, 45),
tertiaryFixedDim: rgb(222, 188, 223),
onTertiaryFixedVariant: rgb(88, 62, 91),
surfaceDim: rgb(17, 19, 24),
surfaceBright: rgb(55, 57, 62),
surfaceContainerLowest: rgb(12, 14, 19),
surfaceContainerLow: rgb(26, 27, 32),
surfaceContainer: rgb(30, 31, 37),
surfaceContainerHigh: rgb(40, 42, 47),
surfaceContainerHighest: rgb(51, 53, 58),
},
};
property <MaterialScheme> scheme: Palette.color_scheme == ColorScheme.dark ? root.schemes.dark : root.schemes.light;
}

View file

@ -0,0 +1,59 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export struct MaterialScheme {
primary: color,
surfaceTint: color,
onPrimary: color,
primaryContainer: color,
onPrimaryContainer: color,
secondary: color,
onSecondary: color,
secondaryContainer: color,
onSecondaryContainer: color,
tertiary: color,
onTertiary: color,
tertiaryContainer: color,
onTertiaryContainer: color,
error: color,
onError: color,
errorContainer: color,
onErrorContainer: color,
background: color,
onBackground: color,
surface: color,
onSurface: color,
surfaceVariant: color,
onSurfaceVariant: color,
outline: color,
outlineVariant: color,
shadow: color,
scrim: color,
inverseSurface: color,
inverseOnSurface: color,
inversePrimary: color,
primaryFixed: color,
onPrimaryFixed: color,
primaryFixedDim: color,
onPrimaryFixedVariant: color,
secondaryFixed: color,
onSecondaryFixed: color,
secondaryFixedDim: color,
onSecondaryFixedVariant: color,
tertiaryFixed: color,
onTertiaryFixed: color,
tertiaryFixedDim: color,
onTertiaryFixedVariant: color,
surfaceDim: color,
surfaceBright: color,
surfaceContainerLowest: color,
surfaceContainerLow: color,
surfaceContainer: color,
surfaceContainerHigh: color,
surfaceContainerHighest: color,
}
export struct MaterialSchemes {
light: MaterialScheme,
dark: MaterialScheme
}

View file

@ -0,0 +1,75 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export global MaterialStyleMetrics {
// sizes
out property <length> size_1: 1px;
out property <length> size_2: 2px;
out property <length> size_3: 3px;
out property <length> size_4: 4px;
out property <length> size_6: 6px;
out property <length> size_8: 8px;
out property <length> size_14: 14px;
out property <length> size_16: 16px;
out property <length> size_18: 18px;
out property <length> size_20: 20px;
out property <length> size_24: 24px;
out property <length> size_30: 30px;
out property <length> size_32: 32px;
out property <length> size_36: 36px;
out property <length> size_38: 38px;
out property <length> size_40: 40px;
out property <length> size_48: 48px;
out property <length> size_49: 49px;
out property <length> size_52: 52px;
out property <length> size_56: 56px;
out property <length> size_64: 64px;
out property <length> size_72: 72px;
out property <length> size_80: 80px;
out property <length> size_90: 96px;
out property <length> size_200: 200px;
out property <length> size_256: 256px;
out property <length> size_344: 344px;
out property <length> size_360: 360px;
out property <length> size_572: 572px;
out property <length> size_640: 640px;
// icon
out property <length> icon_size_18: 18px;
out property <length> icon_size_24: 24px;
out property <length> icon_size_36: 36px;
out property <length> icon_size_90: 27px;
// padding
out property <length> padding_4: 4px;
out property <length> padding_6: 6px;
out property <length> padding_10: 10px;
out property <length> padding_12: 12px;
out property <length> padding_8: 8px;
out property <length> padding_14: 14px;
out property <length> padding_16: 16px;
out property <length> padding_20: 20px;
out property <length> padding_24: 24px;
out property <length> padding_28: 28px;
out property <length> padding_30: 30px;
out property <length> padding_44: 44px;
out property <length> padding_56: 56px;
// spacing
out property <length> spacing_2: 2px;
out property <length> spacing_6: 6px;
out property <length> spacing_4: 4px;
out property <length> spacing_8: 8px;
out property <length> spacing_12: 12px;
out property <length> spacing_16: 16px;
out property <length> spacing_40: 40px;
out property <length> spacing_52: 52px;
// border_radius
out property <length> border_radius_2: 2px;
out property <length> border_radius_4: 4px;
out property <length> border_radius_8: 8px;
out property <length> border_radius_12: 12px;
out property <length> border_radius_16: 16px;
out property <length> border_radius_28: 28px;
}

View file

@ -0,0 +1,93 @@
// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT
export struct TextStyle {
font_size: length,
font_weight: int
}
export global MaterialTypography {
out property <int> regular: 300;
out property <int> medium: 600;
out property <int> semibold: 900;
out property <TextStyle> display_large: {
font_size: 57px,
font_weight: root.regular
};
out property <TextStyle> display_medium: {
font_size: 45px,
font_weight: root.regular
};
out property <TextStyle> display_small: {
font_size: 36px,
font_weight: root.regular
};
out property <TextStyle> headline_large: {
font_size: 32px,
font_weight: root.regular
};
out property <TextStyle> headline_medium: {
font_size: 28px,
font_weight: root.regular
};
out property <TextStyle> headline_small: {
font_size: 24px,
font_weight: root.regular
};
out property <TextStyle> title_large: {
font_size: 22px,
font_weight: root.regular
};
out property <TextStyle> title_medium: {
font_size: 16px,
font_weight: root.medium
};
out property <TextStyle> title_small: {
font_size: 14px,
font_weight: root.medium
};
out property <TextStyle> label_large: {
font_size: 14px,
font_weight: root.medium
};
out property <TextStyle> label_medium: {
font_size: 12px,
font_weight: root.medium
};
out property <TextStyle> label_medium_prominent: {
font_size: 12px,
font_weight: root.semibold
};
out property <TextStyle> label_small: {
font_size: 11px,
font_weight: root.medium
};
out property <TextStyle> body_large: {
font_size: 16px,
font_weight: root.regular
};
out property <TextStyle> body_medium: {
font_size: 14px,
font_weight: root.regular
};
out property <TextStyle> body_small: {
font_size: 12px,
font_weight: root.regular
};
}

23
src/lib.rs Normal file
View file

@ -0,0 +1,23 @@
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
slint::include_modules!();
fn ui() -> MainWindow {
MainWindow::new().unwrap()
}
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
pub fn main() {
let ui = ui();
ui.run().unwrap();
}
#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(android_app: slint::android::AndroidApp) {
slint::android::init(android_app).unwrap();
let ui = ui();
MaterialWindowAdapter::get(&ui).set_disable_hover(true);
ui.run().unwrap();
}

6
src/main.rs Normal file
View file

@ -0,0 +1,6 @@
// In order to be compatible with both desktop, wasm, and android, the example is both a binary and a library.
// Just forward to the library in main
fn main() {
parts_rs_lib::main();
}

19
ui/home.slint Normal file
View file

@ -0,0 +1,19 @@
import { VerticalBox } from "std-widgets.slint";
import { MaterialPalette } from "../material-1.0/ui/styling/material_palette.slint";
export component ViewHome {
Rectangle {
border-radius: 10px;
background: MaterialPalette.surface;
VerticalBox {
alignment: center;
padding: 16px;
Text {
text: "Home View";
font-size: 28px;
font-weight: 700;
}
}
}
}

61
ui/main.slint Normal file
View file

@ -0,0 +1,61 @@
import { HorizontalBox, VerticalBox } from "std-widgets.slint";
import { NavigationRail } from "@material";
import { Icons } from "../material-1.0/ui/icons/icons.slint";
import { MaterialWindow } from "../material-1.0/material.slint";
import { ViewHome } from "home.slint";
export { MaterialWindowAdapter } from "@material";
export component MainWindow inherits MaterialWindow {
in-out property <int> current_view: 0;
callback navigate(int);
preferred-width: 1000px;
preferred-height: 700px;
title: "PartsRS";
HorizontalBox {
NavigationRail {
width: 80px;
index_changed(index) => {
root.current_view = index;
root.navigate(index)
}
items: [
{
icon: Icons.home,
text: "Home"
},
{
icon: Icons.list_box,
text: "Parts"
},
{
icon: Icons.shape,
text: "Categories"
},
{
icon: Icons.package_variant,
text: "Locations"
},
{
icon: Icons.cog,
text: "Settings"
}
];
}
VerticalBox {
if root.current_view == 0: ViewHome {}
if root.current_view == 1: Text { text: "parts"; }
if root.current_view == 2: Text { text: "categories"; }
if root.current_view == 3: Text { text: "locations"; }
if root.current_view == 4: Text { text: "settings"; }
}
}
}
export global NavigationLogic {
callback navigate(int);
}

4
ui/slint-logo.svg Normal file
View file

@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.6632 55.828L48.7333 37.0309C48.7333 37.0309 50 36.3278 50 35.2182C50 33.7406 48.3981 33.2599 48.3981 33.2599L32.9557 27.355C32.4047 27.1462 31.6464 27.7312 32.3564 28.4728L37.4689 33.4165C37.4689 33.4165 38.889 34.765 38.889 35.6494C38.889 36.5338 38.017 37.322 38.017 37.322L19.4135 54.6909C18.7517 55.3089 19.6464 56.4294 20.6632 55.828Z" fill="white"/>
<path d="M43.3368 8.17339L15.2667 26.9677C15.2667 26.9677 14 27.6708 14 28.7804C14 30.258 15.6019 30.7387 15.6019 30.7387L31.0443 36.6464C31.5953 36.8524 32.3565 36.2674 31.6436 35.5286L26.5311 30.5684C26.5311 30.5684 25.111 29.2226 25.111 28.3355C25.111 27.4483 25.983 26.6628 25.983 26.6628L44.5752 9.30769C45.2483 8.68973 44.3565 7.56916 43.3368 8.17339Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 846 B